*CTF 2022

Last updated on January 27, 2024 am

NaCl Jump

NaCl

利用字符串可以快速的定位到关键函数

可以发现关键是函数`sub_8080900,跟进该函数

在这函数里面,发现了大量的jmp

也就是形如lea lea mov jmp的组合

同样还有大量的jmp rdi

截屏2024-01-01 02.06.54

经过调试可以发现,这里的lea lea mov jmp充当了call指令,lea mov and lea jmp充当了retn指令,实际上就是把r15当作rsp来使用。这样就使得函数块割裂,导致IDA识别不准

在由 Microsoft Visual C++ 编译器生成的代码中,经常可以找到函数块
.
编译器移动不常执行的代码段,用以将经常执行的代码段“挤入”不大可能被换出的内存页,由此便产生了函数块
.
如果一个函数以这种方式被分割,IDA 会通过跟踪指向每个块的跳转,尝试定位所有相关的块
.
多数情况下,IDA 都能找到所有这些块,并在函数的头部列出每一个块

.
有时候,IDA 可能无法确定与函数关联的每一个块,或者函数可能被错误地识别成函数块,而非函数本身
.
在这种情况下,需要创建自己的函数块,或删除现有的函数块
.
在反汇编代码清单中,函数块就叫做函数块;在 IDA 的菜单系统中,函数块叫做函数尾(function tail)
.
要删除现有的函数块,将光标放在要删除的块中的任何一行上,然后选择 Edit -> Functions -> Remove Function Tail
.
在初次将文件加载到 IDA 时,取消选择 Create function tails 加载器选项,IDA 将不创建函数块
.
如果禁用了函数尾,已经包含函数尾的函数将包含指向函数边界以外区域的跳转,IDA 会在反汇编代码清单左侧的箭头窗口中用红线和箭头突出显示这些跳转

基于此,可以等价的patch这两组指令,使用IDApython会相当方便。这里还要注意需要将Align块也要nop填充

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
start = 0x807FEC0
end = 0x8080AD1

call = ['lea','lea','mov','jmp']
ret = ['lea','mov','and','lea','jmp']

def nop(s,e):
while s < e:
patch_byte(s,0x90)
s += 1

def patch_call():
des = idc.get_operand_value(tmp[1],1)
nop(tmp[3]+5,des)
nop(tmp[0],tmp[3])
patch_byte(tmp[3],0xe8)


def patch_ret():
nop(tmp[0],tmp[4])
patch_byte(tmp[4],0x90)
patch_byte(tmp[4]+1,0xc3)

tmp = [0 for i in range(5)]

while start < end:
tmp[0]=start
tmp[1]=next_head(tmp[0])
tmp[2]=next_head(tmp[1])
tmp[3]=next_head(tmp[2])
tmp[4]=next_head(tmp[3])
# print(hex(start))
for i in range(4):
if idc.print_insn_mnem(tmp[i]) != call[i]:
break
else:
patch_call()

for i in range(4):
if idc.print_insn_mnem(tmp[i]) != ret[i]:
break
else:
patch_ret()

start = idc.next_head(start)

patch完后的汇编大概长这样子

还需要注意的地方是,IDA可能会错误识别函数的边界,需要记住只有retn才是函数的结束,在retn的地方按e标记为函数结束。还有其他应该识别为函数的,比如call sub_xxxx,但是相应地址可能只是loc_xxxx,那就要按p重新分析一下为函数;函数体内不应该存在的sub_xxxx,则需要先按u转为未定义,再按c转为代码。

最后的反汇编应该是这样子

截屏2024-01-01 02.19.40

简单分析一下,*(input+36)作为一个索引,将输入分为4个部分,而在之前的函数输入中我们可以得知flag应该是32位,那么这里就是8字节一组的加密,结合for循环的第一句,也验证了这个猜想。

跟入sub_8080720

截屏2024-01-01 02.22.10

这里面sub_8080360是在做数据准备,与输入无关。

经过调试可以将这段加密等价转为如下python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def ROL(value,count):
return ((value << count) & 0xffffffff) | (value >> (0x20 - count))


table = [67438087, 66051, 202182159, 134810123, 3443517467, 3619968119, 2671678006, 17297799, 4187212673, 3212056172, 3659105319, 436579327, 2466768356, 3414888005, 812513269, 2905604503, 2860953686, 1150444474, 1067728923, 507640087, 3335200381, 418057594, 3572254720, 3028744491, 2268592627, 1985887525, 3678221623, 3997455659, 298895692, 2606783435, 2419824819, 616754851, 2305691810, 575140032, 3092143274, 2368472304, 1002931244, 441505286, 1235325308, 2115810572, 4230461478, 1608376252, 3725575172, 2998077703]

v1 = 0x61616161
v2 = 0x62626262

for i in range(44):
v = v1
v6 = ROL(v1,1)
v7 = ROL(v1,8) & v6

v1 = v7 ^ ROL(v1,2) ^ v2 ^ table[i]
v2 = v

这里的v1,v2代表了8字节flag的低4字节和高4字节,table则是由函数sub_8080360得到

经过sub_8080720后,则是进入sub_8080100,这是个很明显的TEA算法

唯一需要注意的是循环轮次与之前的索引有关

现在可以得到完整的加密脚本

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
from ctypes import *

def ROL(value,count):
return ((value << count) & 0xffffffff) | (value >> (0x20 - count))


table = [67438087, 66051, 202182159, 134810123, 3443517467, 3619968119, 2671678006, 17297799, 4187212673, 3212056172, 3659105319, 436579327, 2466768356, 3414888005, 812513269, 2905604503, 2860953686, 1150444474, 1067728923, 507640087, 3335200381, 418057594, 3572254720, 3028744491, 2268592627, 1985887525, 3678221623, 3997455659, 298895692, 2606783435, 2419824819, 616754851, 2305691810, 575140032, 3092143274, 2368472304, 1002931244, 441505286, 1235325308, 2115810572, 4230461478, 1608376252, 3725575172, 2998077703]

v1 = 0x61616161
v2 = 0x62626262

for i in range(44):
v = v1
v6 = ROL(v1,1)
v7 = ROL(v1,8) & v6

v1 = v7 ^ ROL(v1,2) ^ v2 ^ table[i]
v2 = v

v1 = c_uint32(v1)
v2 = c_uint32(v2)
delta = 0x10325476
sum = c_uint32(0)
key = [0x3020100,0x7060504,0xb0a0908,0xf0e0d0c]
for i in range(2): # 总循环次数会变,是2的n次幂
v2.value += (((v1.value >> 5) ^ (v1.value << 4)) + v1.value) ^ (key[sum.value & 3] + sum.value)
sum.value += delta
v1.value += (((v2.value >> 5) ^ (v2.value << 4)) + v2.value) ^ (key[(sum.value >> 11) & 3] + sum.value)

据此写出逆运算

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
from ctypes import *
enc= [4260741734, 2050130566, 3465822212, 1574996700, 2800008458, 382381532, 332230412, 443930934]
table = [67438087, 66051, 202182159, 134810123, 3443517467, 3619968119, 2671678006, 17297799, 4187212673, 3212056172, 3659105319, 436579327, 2466768356, 3414888005, 812513269, 2905604503, 2860953686, 1150444474, 1067728923, 507640087, 3335200381, 418057594, 3572254720, 3028744491, 2268592627, 1985887525, 3678221623, 3997455659, 298895692, 2606783435, 2419824819, 616754851, 2305691810, 575140032, 3092143274, 2368472304, 1002931244, 441505286, 1235325308, 2115810572, 4230461478, 1608376252, 3725575172, 2998077703]
table = table[::-1]

def ROL(value,count):
return ((value << count) & 0xffffffff) | (value >> (0x20 - count))

for k in range(4):
v1 = c_uint32(enc[2*k+1])
v2 = c_uint32(enc[2*k])
delta = 0x10325476
sum = c_uint32(delta * (2**(k+1)))
key = [0x3020100,0x7060504,0xb0a0908,0xf0e0d0c]
for i in range(2**(k+1)):
v1.value -= (((v2.value >> 5) ^ (v2.value << 4)) + v2.value) ^ (key[(sum.value >> 11) & 3] + sum.value)
sum.value -= delta
v2.value -= (((v1.value >> 5) ^ (v1.value << 4)) + v1.value) ^ (key[sum.value & 3] + sum.value)

v1 = v1.value
v2 = v2.value

for i in range(44):
v = v2
v6 = ROL(v2,1)
v7 = ROL(v2,8) & v6

v2 = v7 ^ ROL(v2,2) ^ v1 ^ table[i]

v1 = v

for i in range(4):
print(chr(((v1 << (i*8)) & 0xff000000) >>24),end='')

for i in range(4):
print(chr(((v2 << (i*8)) & 0xff000000) >>24),end='')

# mM7pJIobsCTQPO6R0g-L8kFExhYuivBN

Jump

考点就是setjmp()longjmp()的配合,这两个函数可以在函数间实现类似goto的跳跃。

简单说就是setjmp会设置一个缓存点,需要一个jmp_buf参数来保存上下文信息,并且返回0零值

longjmp需要一个jmp_buf参数来知晓要去哪个缓存点,还需要一个val参数来告诉要返回什么值。这里要注意的是longjmp函数不返回,而是直接从setjmp返回,返回值就是val

整体的逻辑都比较简单,这里还用setjmplongjmp做了循环操作。

通过调试可以知道输入0x22个字符,然后每次循环左移1位得到二维数组给buffer2,比如输入985236147adgjlqwesxzcvbnmfuiopvx,就会得到

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
b'\x02985236147adgjlqwesxzcvbnmfuiopvx\x03'
b'985236147adgjlqwesxzcvbnmfuiopvx\x03\x02'
b'85236147adgjlqwesxzcvbnmfuiopvx\x03\x029'
b'5236147adgjlqwesxzcvbnmfuiopvx\x03\x0298'
b'236147adgjlqwesxzcvbnmfuiopvx\x03\x02985'
b'36147adgjlqwesxzcvbnmfuiopvx\x03\x029852'
b'6147adgjlqwesxzcvbnmfuiopvx\x03\x0298523'
b'147adgjlqwesxzcvbnmfuiopvx\x03\x02985236'
b'47adgjlqwesxzcvbnmfuiopvx\x03\x029852361'
b'7adgjlqwesxzcvbnmfuiopvx\x03\x0298523614'
b'adgjlqwesxzcvbnmfuiopvx\x03\x02985236147'
b'dgjlqwesxzcvbnmfuiopvx\x03\x02985236147a'
b'gjlqwesxzcvbnmfuiopvx\x03\x02985236147ad'
b'jlqwesxzcvbnmfuiopvx\x03\x02985236147adg'
b'lqwesxzcvbnmfuiopvx\x03\x02985236147adgj'
b'qwesxzcvbnmfuiopvx\x03\x02985236147adgjl'
b'wesxzcvbnmfuiopvx\x03\x02985236147adgjlq'
b'esxzcvbnmfuiopvx\x03\x02985236147adgjlqw'
b'sxzcvbnmfuiopvx\x03\x02985236147adgjlqwe'
b'xzcvbnmfuiopvx\x03\x02985236147adgjlqwes'
b'zcvbnmfuiopvx\x03\x02985236147adgjlqwesx'
b'cvbnmfuiopvx\x03\x02985236147adgjlqwesxz'
b'vbnmfuiopvx\x03\x02985236147adgjlqwesxzc'
b'bnmfuiopvx\x03\x02985236147adgjlqwesxzcv'
b'nmfuiopvx\x03\x02985236147adgjlqwesxzcvb'
b'mfuiopvx\x03\x02985236147adgjlqwesxzcvbn'
b'fuiopvx\x03\x02985236147adgjlqwesxzcvbnm'
b'uiopvx\x03\x02985236147adgjlqwesxzcvbnmf'
b'iopvx\x03\x02985236147adgjlqwesxzcvbnmfu'
b'opvx\x03\x02985236147adgjlqwesxzcvbnmfui'
b'pvx\x03\x02985236147adgjlqwesxzcvbnmfuio'
b'vx\x03\x02985236147adgjlqwesxzcvbnmfuiop'
b'x\x03\x02985236147adgjlqwesxzcvbnmfuiopv'
b'\x03\x02985236147adgjlqwesxzcvbnmfuiopvx'

这里的\x02\x03是程序加的。

然后函数sub_401F62则根据这个二维数组的第一列进行排序,得到

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
b'\x02985236147adgjlqwesxzcvbnmfuiopvx\x03'
b'\x03\x02985236147adgjlqwesxzcvbnmfuiopvx'
b'147adgjlqwesxzcvbnmfuiopvx\x03\x02985236'
b'236147adgjlqwesxzcvbnmfuiopvx\x03\x02985'
b'36147adgjlqwesxzcvbnmfuiopvx\x03\x029852'
b'47adgjlqwesxzcvbnmfuiopvx\x03\x029852361'
b'5236147adgjlqwesxzcvbnmfuiopvx\x03\x0298'
b'6147adgjlqwesxzcvbnmfuiopvx\x03\x0298523'
b'7adgjlqwesxzcvbnmfuiopvx\x03\x0298523614'
b'85236147adgjlqwesxzcvbnmfuiopvx\x03\x029'
b'985236147adgjlqwesxzcvbnmfuiopvx\x03\x02'
b'adgjlqwesxzcvbnmfuiopvx\x03\x02985236147'
b'bnmfuiopvx\x03\x02985236147adgjlqwesxzcv'
b'cvbnmfuiopvx\x03\x02985236147adgjlqwesxz'
b'dgjlqwesxzcvbnmfuiopvx\x03\x02985236147a'
b'esxzcvbnmfuiopvx\x03\x02985236147adgjlqw'
b'fuiopvx\x03\x02985236147adgjlqwesxzcvbnm'
b'gjlqwesxzcvbnmfuiopvx\x03\x02985236147ad'
b'iopvx\x03\x02985236147adgjlqwesxzcvbnmfu'
b'jlqwesxzcvbnmfuiopvx\x03\x02985236147adg'
b'lqwesxzcvbnmfuiopvx\x03\x02985236147adgj'
b'mfuiopvx\x03\x02985236147adgjlqwesxzcvbn'
b'nmfuiopvx\x03\x02985236147adgjlqwesxzcvb'
b'opvx\x03\x02985236147adgjlqwesxzcvbnmfui'
b'pvx\x03\x02985236147adgjlqwesxzcvbnmfuio'
b'qwesxzcvbnmfuiopvx\x03\x02985236147adgjl'
b'sxzcvbnmfuiopvx\x03\x02985236147adgjlqwe'
b'uiopvx\x03\x02985236147adgjlqwesxzcvbnmf'
b'vbnmfuiopvx\x03\x02985236147adgjlqwesxzc'
b'vx\x03\x02985236147adgjlqwesxzcvbnmfuiop'
b'wesxzcvbnmfuiopvx\x03\x02985236147adgjlq'
b'x\x03\x02985236147adgjlqwesxzcvbnmfuiopv'
b'xzcvbnmfuiopvx\x03\x02985236147adgjlqwes'
b'zcvbnmfuiopvx\x03\x02985236147adgjlqwesx'

再以这个二维数组的最后一列为密文。

那么恢复也比较简单,因为每一行首尾两个一定是相连的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cipher = [ 3, 106, 109,  71, 110,  95,  61, 117,  97,  83, 90,  76, 118,  78,  52, 119,  70, 120,  69,  54, 82,  43, 112,   2,  68,  50, 113,  86,  49,  67, 66,  84,  99, 107]

sort_ed = [2, 3, 43, 49, 50, 52, 54, 61, 66, 67, 68, 69, 70, 71, 76, 78, 82, 83, 84, 86, 90, 95, 97, 99, 106, 107, 109, 110, 112, 113, 117, 118, 119, 120]
print(t)

cur = q.index(2)
ans = []

while q[cur]!=3:
nxt = sort_ed[cur]
ans.append(nxt)
cur = cipher.index(nxt)

print(bytes(ans))

cipher就是排序后的最后一列,而sort_ed就是将cipher再进行排序,因为cipher包含了所有flag,那么就得到每一行的第一列。再只要一一对应即可。


*CTF 2022
http://example.com/2024/01/01/Nacl/
Author
yring
Posted on
January 1, 2024
Licensed under