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 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 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 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
指令入手,不行就查add
、shl
或shr
,总能找到有用的
略过与常数进行操作的指令,直接看寄存器之间相互异或的
排查一些重复的,无意义的内容(好恶心的混淆),发现在这里对输入的内容进行了异或
需要注意的是这里寄存RDX
存的是输入的内容,这点通过查看下面其他的xor edx, ecx
指令或者更换输入来判断
输入flag的组成为:flag{1234567890...}
,由此可以判断这里取的是flag的第二个四字节
结合原始数据出现在xor
指令中,可以判断是XXTEA算法(独有的将输入内容丢进去异或((x ^ y) + (k[(p & 3) ^ e] ^ z))
)
1 2 3 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后,要找的就是key
、delta
、密文还有轮数了
由于输入太长,无法确定这个xor
中的明文属于z
还是y
,所以需要进一步进行判断
根据XXTEA的性质,当输入的长度为12(Bytes)时,在第一轮加密中有z==v[2]
和y==v[1]
,将输入内容设为111122223333
,重新追踪,通过观察发现,在上面相同的位置上,RDX
寄存器的内容是v[1]
即2222
,所以可以确定RCX
存的是delta
查看下一条xor
指令,发现是一个非常小的数字,而后面也有这种小数字,因此可以确定这条指令是(p & 3) ^ e
,用来得到key
的索引
而索引后面的就是key
了,这个根据RDX==v[2]
也可以得出来
根据索引一个个按顺序提取一下可得(注意要追长一点,太短了拿不到全部的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_dbgimport ida_naltimport ida_idabase = 0 count = 0 f =0 class MyDbgHook (ida_dbg.DBG_Hooks): def __init__ (self ): ida_dbg.DBG_Hooks.__init__(self) 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) debughook = MyDbgHook() debughook.hook() ep = ida_ida.inf_get_start_ip() if ida_dbg.request_run_to(ep): ida_dbg.run_requests() base = ida_nalt.get_imagebase() while 1 : ida_dbg.run_to(base+0x9CBF ) ida_dbg.wait_for_next_event(ida_dbg.WFNE_SUSP, -1 ) if f == 1 : ida_dbg.request_exit_process() break
XXTEA中shift
的次数与rounds
和n
有关,可以得到如下式子
1 2 3 4 rounds * (n-1 ) + rounds = shift次数 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次数,注意要考虑取整的问题,建议多取几组输入长度丢进去
最后可以解得
至于密文,由于上文已经知道了赋值的地方,但不知道密文长度,也不知道有没有长度检测,保险起见,开头使用flag头以便能顺利通过,长度先猜测是跟scanf中寄存器rdi
存的%42s
有关,将其填满,也就是flag{(36字节长内容)}
,改一下上面的脚本,让其在最后一轮停下(2375)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 debughook = MyDbgHook() debughook.hook() ep = ida_ida.inf_get_start_ip() if ida_dbg.request_run_to(ep): ida_dbg.run_requests() base = ida_nalt.get_imagebase() while 1 : ida_dbg.run_to(base+0x9CBF ) 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 import idceax = idc.get_reg_value('EAX' ) print (eax,end=',' )
1 2 3 import idcidc.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 structdef 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)