攻防世界LostKey

LostKey

检测参数,一共要4个参数

对每一个参数字符串clone一个进程进行处理

第一个参数是一个函数,每个字符串的都单独用一个函数处理

第二个参数是一个地址-0x1000 * (i + 1) + 0x100000 + dword_80EA3AC

剩下的重要的就是arg,把参数字符串和一个地址放进去

看clone的操作

这里对syscall的反编译不太好,动态调一下可以知道a1和a2是传进来的enc函数,a3是上面计算的地址,-0x1c得到v8,是sys_clone子进程的栈,a5是传进来的arg,包括一个数组和参数字符串

子进程创建之后会执行a1函数,也就是enc

enc函数里注意到修改了返回地址

sub_8079A50函数分析一下发现其实是syscall,0x1A是sys_ptrace,ptrace了自己,一方面反调,另一方面还有返回值,enc里面调用了两次sys_ptrace第一次会返回0,第二次因为已经被ptrace了,会返回-1,所以两个分支都会进入,enc剩下的部分就是打印一个没用的字符串,然后返回,进入被修改的返回地址

动态调试手动改进程的栈还有手动进入enc函数,看一下栈的内容

第一个是call的返回地址,从0x000开始看,第一个是arg参数,包括一个数组和参数字符串,后面的内容和计算出来的地址一样,主要看被改的返回地址

发现是一个ROP链,执行过来

参数字符串被放到了ebp里面,继续调后面的ROP

从字符串里取4个字符到eax

把4个字节异或0x466C7578再与0x210D191E比较,检测是否为flag,决定后面ROP链的调用,结果相等进入sub_804A240,结果失败进入sub_804A1E0

sub_804A1E0使进程结束

比较成功再取四个字符,异或0x78756C460x4B1D383D,检测是否为{Th3,比较失败进程退出,比较成功继续后续调用,后面的调用跟参数字符串无关了,主要是把参数字符串复制到一个地方

分析enc2

函数里面基本相似,修改了4个全局变量,然后把返回地址改到ROP链里面去

还是一样ebp指向参数字符串,这里做传参操作,然后调用函数sub_80714B0

从这里可以猜到要求第二段flag长度为16(实际上不是),剩余的操作应该是一个字符串复制,没有明显的根据结果返回到不同的函数

这里把ebp当参数,调用sub_8062D90,ebp是参数字符串

这个函数返回值是字符串的长度

这里如果前面返回结果小于等于1会把esp-0x1c,重新进入之前的ROP链,循环,正常情况会继续调用后面的ROP

这里从字符串里取一个字符,取反,高低4位交换,再取下一个字符,异或前一个处理后的字符,后面有rsp+8,实际上循环,该操作进行字符串长度-1次,如下

1
2
3
4
5
6
7
8
s = [ord(c) for c in "flag2_string"]

for i in range(len(s)-1):
d = s[i]
d ^= 0xff # 取反
d = ((d << 4) | (d >> 4)) & 0xff # 高低4位互换
d ^= s[i+1]
s[i] = d

上面操作结束后会到sub_804A300,这里是把最后一个字符异操作后异或0x41

1
2
3
4
5
6
7
8
9
s = [ord(c) for c in "flag2_string"]
s.append(0x41)
for i in range(len(s)-1):
d = s[i]
d ^= 0xff # 取反
d = ((d << 4) | (d >> 4)) & 0xff # 高低4位互换
d ^= s[i+1]
s[i] = d
s.pop()

4字节一比较,不相等置位cl

后面一直都是4字节一比较

实际不为0的长度是0x19

最后根据cl的值判断进入哪个分支,sub_804A1E0错误分支,后面的不用再分析,已经可以解密出第二段flag _key_1s_in_th3_secret_com

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ls2 = [0x61, 0x2C, 0xD0, 0x37, 0x3B, 0x9F, 0x97, 0x63, 0x07, 0x46, 0x7E, 0xD0,
0x4A, 0x93, 0x79, 0xAD, 0xCA, 0xBB, 0xBD, 0xDD, 0xE7, 0x69, 0xA6, 0x64, 0x68]
ls2.reverse()

key2 = 0x41
for i in range(len(ls2)):
t = key2 ^ ls2[i]
th = t & 0xF0
tl = t & 0x0F
t = (th >> 4) | (tl << 4)
t = 0xFF - t # 取反
ls2[i] = t
key2 = t
ls2.reverse()
flag2 = bytes(ls2).decode()
print(flag2)
# _key_1s_in_th3_secret_com

接下来是第三段enc3

传参给后面的函数

三个参数分别是字符串,数字6,还有一个缓冲区

这里很明显的初始化了md5最开始没看出来看题目源码才知道是md5

计算前6个字符的md5

这里是16字节的比较,比较的数据在栈里面,每次pop到esi

也就是6个字符的md5需要是7b4d6ff46ac46c3f628acc930d937d81,爆破得到第三段flag

p4rtme

第四段,enc4

分析方法与前三段类似,把字符串分三次进行加密操作,最后进行比较

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
f1 = 0x282786af
f2 = 0xb2f2ba70

ebx = 0
for i in range(0x20):
ebx = (ebx + 0x9E3779B9) & 0xffffffff
ecx = (f2 << 4) & 0xffffffff
ecx = (ecx + key1) & 0xffffffff
ecx = (ecx ^ (f2 + ebx)) & 0xffffffff
edx = f2 >> 5
edx = (edx + key2) & 0xffffffff
ecx = ecx ^ edx
f1 = (f1 + ecx) & 0xffffffff

ecx = (f1 << 4) & 0xffffffff
ecx = (ecx + key3) & 0xffffffff
ecx = (ecx ^ (f1 + ebx)) & 0xffffffff
edx = f1 >> 5
edx = (edx + key4) & 0xffffffff
ecx = ecx ^ edx
f2 = (f2 + ecx) & 0xffffffff

print(hex(f1), hex(f2))
# 第一轮
k1 = 0xA42D6EBF
k2 = 0x0EFE89E7
# 第二轮
# k1 = 0xAADD934D
# k2 = 0x4E4E7F13
# 第三轮
# k1 = 0x8EC32CA9
# k2 = 0x8559D4E9

实际上就是tea加密,这里用到的密钥就是在enc1、enc2、enc3里面被改的4个dword

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
# enc1
key1 = 0x466C7578
key2 = 0x78756C46
key3 = 0x78756C46
key4 = 0x466C7578

key1 ^= 0x6861636B
key2 ^= 0x6B636168
key3 ^= 0x6B636168
key4 ^= 0x6861636B

#enc2
key1 ^= 0xffffffff
key2 ^= 0xffffffff
key3 ^= 0xffffffff
key4 ^= 0xffffffff

key1 ^= 0x1010101
key2 ^= 0x1010101
key3 ^= 0x1010101
key4 ^= 0x1010101

#enc3
key1 ^= 0x10101010
key2 ^= 0x10101010
key3 ^= 0x10101010
key4 ^= 0x10101010

key1 ^= 0x2020202
key2 ^= 0x2020202
key3 ^= 0x2020202
key4 ^= 0x2020202

tea解密得到最后一段flag nt_of_your_t00l_sh3d...}

这道题比较有意思的地方在与用clone创建子进程设置进程的栈,通过ROP链来检测flag,最后通过共享内存的方式使主进程能够打印完整flag,还有就是ptrace的反调试,容易误认为其只会进入一个分支执行,但实际两个分支全部执行才能得到完整的key


攻防世界LostKey
https://blog.noxke.icu/2023/11/27/ctf_wp/攻防世界LostKey/
作者
noxke
发布于
2023年11月27日
许可协议