0x00 前言

学学Trace,应对一下控制流混淆

0x01 解

解压出来一个txt,查看010发现是ELF

改下后缀,直接拖IDA,发现函数很少,应该是有壳

查壳发现是UPX,没有改过,可以直接upx -d

这边不演示了,直接进到脱壳后

进到main函数发现完全看不懂,发现有很多混淆

从输入入手,要求输入时进行暂停,随便输点,回到用户代码,发现在这里接收输入

一执行就会清空前面的寄存器,但根据linux调用规则,存放输入的应该是rsi寄存器,提前记录寄存器指向的地址,输入后转到这个地址下硬断

断下来在libc里面,出来是strlen

使用IDA的跟踪功能,选择指令跟踪,然后打开跟踪窗口

直接开始执行(注意要关闭断点),跟长点,后面要手动分析,跟短了可能都没进加密(这里不好说跟多少,凭感觉吧)

由于混淆太恶心,无法下手,只能根据题目给的tea猜测是TEA算法,而这就要考虑使用的是哪一种TEA了

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# TEA
def TEA_encrypt(v,k):
v0=c_uint32(v[0])
v1=c_uint32(v[1])
sum1=c_uint32(0)
delta=0x9E3449B8
while(sum1.value != 0x987E55D0):
sum1.value+=delta & 0xFFFFFFFF
v0.value+=((v1.value+sum1.value)^((v1.value>>4)+k[0])^((v1.value<<5)+k[1]))
v1.value+=((v0.value+sum1.value)^((v0.value>>4)+k[2])^((v0.value<<5)+k[3]))
return v0.value,v1.value

# XTEA
def XTEA_encrypt(rounds, v, k):
v0 = v[0]
v1 = v[1]
x = 0
delta = 0x9E3779B9
for i in range(rounds):
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (x + k[x & 3])
v0 = v0 & 0xFFFFFFFF
x += delta
x = x & 0xFFFFFFFF
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (x + k[(x >> 11) & 3])
v1 = v1 & 0xFFFFFFFF
v[0] = v0
v[1] = v1
return v

# XXTEA
def shift(z, y, x, k, p, e):
return ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((x ^ y) + (k[(p & 3) ^ e] ^ z)))

def XXTEA_encrypt(v, k):
delta = 0x9E3779B9
n = len(v)
rounds = int(6 + 52 / n)
x = 0
z = v[n - 1]
for i in range(rounds):
x = (x + delta) & 0xFFFFFFFF
e = (x >> 2) & 3
for p in range(n - 1):
y = v[p + 1]
v[p] = (v[p] + shift(z, y, x, k, p, e)) & 0xFFFFFFFF
z = v[p]
p += 1
y = v[0]
v[n - 1] = (v[n - 1] + shift(z, y, x, k, p, e)) & 0xFFFFFFFF
z = v[n - 1]
return v

判断方法很多,找找不同算法的不同点下手就行

将跟踪结果按照指令倒序进行排列,先从xor指令入手,不行就查addshlshr,总能找到有用的

略过与常数进行操作的指令,直接看寄存器之间相互异或的

排查一些重复的,无意义的内容(好恶心的混淆),发现在这里对输入的内容进行了异或

需要注意的是这里寄存RDX存的是输入的内容,这点通过查看下面其他的xor edx, ecx指令或者更换输入来判断

输入flag的组成为:flag{1234567890...},由此可以判断这里取的是flag的第二个四字节

结合原始数据出现在xor指令中,可以判断是XXTEA算法(独有的将输入内容丢进去异或((x ^ y) + (k[(p & 3) ^ e] ^ z))

1
2
3
# XXTEA
def shift(z, y, x, k, p, e):
return ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((x ^ y) + (k[(p & 3) ^ e] ^ z)))

可见这条指令很有用,先下个断点

确定是XXTEA后,要找的就是keydelta、密文还有轮数了

由于输入太长,无法确定这个xor中的明文属于z还是y,所以需要进一步进行判断

根据XXTEA的性质,当输入的长度为12(Bytes)时,在第一轮加密中有z==v[2]y==v[1],将输入内容设为111122223333,重新追踪,通过观察发现,在上面相同的位置上,RDX寄存器的内容是v[1]2222,所以可以确定RCX存的是delta

1
delta = 0x7f8a6b4c

查看下一条xor指令,发现是一个非常小的数字,而后面也有这种小数字,因此可以确定这条指令是(p & 3) ^ e,用来得到key的索引

而索引后面的就是key了,这个根据RDX==v[2]也可以得出来

image-20241221222849410

根据索引一个个按顺序提取一下可得(注意要追长一点,太短了拿不到全部的key,上面这图就是反面例子,60000多条是不够的)

1
key = [0x7f8a6b4c,0x9E3779B9,0xDEAFBEEF,0xCAFEC3C3]

而根据XXTEA的性质,当输入的长度为8(Bytes)时,在第一轮的加密中有z==y==v[1],因此将输入变为一个比较简单的数字(这边用11112222),然后重新进行跟踪

由于已知加密的z==y==0x32323232,所以(((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4)))中的每一个操作结果都可以被计算出来

1
2
3
4
5
6
7
Ly = y.value << 2 = 0xc8c8c8c8
Ry = y.value >> 3 = 0x6464646
Lz = z.value << 4 = 0x323232320
Rz = z.value >> 5 = 0x1919191
Rz ^ Ly = 0xc9595959
Ry ^ Lz = 0x325656566
(Rz ^ Ly) + (Ry ^ Lz) = 0x3EEBEBEBF = 0xEEBEBEBF(取低8位)

由此可以判断题目是否对这些操作进行了更改

所幸没有,但直接上自己的XXTEA发现不对,没办法,去看看原始值进行了什么操作

xor的位置下断点,断下来之后查找字节序列,在内存中找到输入的内容然后下断点(可能会搜到很多个,可以都下然后查看断哪个)

后面可以发现断在这了

这个地方对应的就是

1
v[p] = (v[p] + shift(z, y, x, k, p, e)) & 0xFFFFFFFF

1
v[n - 1] = (v[n - 1] + shift(z, y, x, k, p, e)) & 0xFFFFFFFF

这个可以通过输出自己的XXTEA中间步骤来证明

两边同时运行,发现自己的XXTEA的结果出现

但是程序还能继续运行

这说明轮数变了

根据轮数的构成

1
2
n = len(v)
rounds = int(6 + 52 / n)

用IDA脚本获取轮数

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
import ida_dbg
import ida_nalt
import ida_ida
# global items
base = 0
count = 0
f =0
class MyDbgHook(ida_dbg.DBG_Hooks):

def __init__(self):
ida_dbg.DBG_Hooks.__init__(self) # important
self.steps = 0

def log(self, msg):
print(">>> %s" % msg)

def dbg_bpt(self, tid, ea):
self.log("Break point at 0x%x pid=%d" % (ea, tid))
global count
count +=1
return 0

def dbg_process_exit(self, pid, tid, ea, code):
self.log("Process exited pid=%d tid=%d ea=0x%x code=%d" % (pid, tid, ea, code))
global count
global f
f = 1
print(count) # 获取的是shift的次数,根据XXTEA的shift次数来进行求解


# Install the debug hook
debughook = MyDbgHook()
debughook.hook()
ep = ida_ida.inf_get_start_ip()
if ida_dbg.request_run_to(ep): # Request stop at entry point
ida_dbg.run_requests() # Launch process
base = ida_nalt.get_imagebase() # get base
while 1:
ida_dbg.run_to(base+0x9CBF) # loop the xor
ida_dbg.wait_for_next_event(ida_dbg.WFNE_SUSP, -1)
if f == 1:
ida_dbg.request_exit_process()
break

XXTEA中shift的次数与roundsn有关,可以得到如下式子

1
2
3
4
rounds * (n-1) + rounds = shift次数
# 设常量为x,分子为y,则有
int(x+y/len(v)) * (len(v)-1) + int(x+y/len(v)) = shift次数
int(x+y/len(v)) * (len(v)-1) + int(x+y/len(v)) = xor次数/6

随便取几个不同长度的输入丢进去,得到不同的shift次数,注意要考虑取整的问题,建议多取几组输入长度丢进去

最后可以解得

1
2
x = 32
y = 52

至于密文,由于上文已经知道了赋值的地方,但不知道密文长度,也不知道有没有长度检测,保险起见,开头使用flag头以便能顺利通过,长度先猜测是跟scanf中寄存器rdi存的%42s有关,将其填满,也就是flag{(36字节长内容)},改一下上面的脚本,让其在最后一轮停下(2375)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Install the debug hook
debughook = MyDbgHook()
debughook.hook()
ep = ida_ida.inf_get_start_ip()
if ida_dbg.request_run_to(ep): # Request stop at entry point
ida_dbg.run_requests() # Launch process
base = ida_nalt.get_imagebase() # get base
while 1:
ida_dbg.run_to(base+0x9CBF) # loop the xor
ida_dbg.wait_for_next_event(ida_dbg.WFNE_SUSP, -1)
if count == 2375:
break
if f == 1:
ida_dbg.request_exit_process()
break

之后到赋值的地方,搜索内存,将所有搜索出来的加密结果下硬件断点

跑起来,会发现断在将加密结果赋值给了一个地址的地方,走一步,然后给这个接收加密结果的地址下硬断

之后又会断下来

一步步走,发现下面的cmp就是将加密结果拿去做了对比

通过分析可以知道,cmp之后会执行setnz指令,即根据ZF寄存器来取反进行赋值

由于对比不会通过,所以ZF寄存器的结果一定为0,取反之后就是1,所以为了让其能继续执行,需要设置其为1才能让其正常运行

给断点写点脚本

1
2
3
4
# cmp
import idc
eax = idc.get_reg_value('EAX')
print(eax,end=',')

1
2
3
# setnz
import idc
idc.set_reg_value(1,'ZF')

得到密文之后就可以解密了

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
import struct

def shift(z, y, x, k, p, e):
return ((((z >> 5) ^ (y << 2)) + ((y >> 3) ^ (z << 4))) ^ ((x ^ y) + (k[(p & 3) ^ e] ^ z)))

def decrypt(v, k):
delta = 0x7f8a6b4c
n = len(v)
rounds = int(32 + 52 / n)
x = (rounds * delta) & 0xFFFFFFFF
y = v[0]
for i in range(rounds):
e = (x >> 2) & 3
for p in range(n - 1, 0, -1):
z = v[p - 1]
v[p] = (v[p] - shift(z, y, x, k, p, e)) & 0xFFFFFFFF
y = v[p]
p -= 1
z = v[n - 1]
v[0] = (v[0] - shift(z, y, x, k, p, e)) & 0xFFFFFFFF
y = v[0]
x = (x - delta) & 0xFFFFFFFF
return v


if __name__ == '__main__':
k = [0x7f8a6b4c,0x9E3779B9,0xDEAFBEEF,0xCAFEC3C3]
enctxt = [1214171175,3066933611,4062421290,3922368852,2540993788,1332676358,22469667,3633624522,2241914041,3542748258,1546291565]
decrypted = decrypt(enctxt, k)
tmp = b''
for i in range(len(decrypted)):
tmp += struct.pack('<L', decrypted[i])
print(tmp)
# b'flag{9aad3962-8ad5-48d0-83dc-1e60a81439a1}\x00\x00'