SUSCTF 2022部分逆向题解

记录一下

DigitalCircuits

py文件打包的exe,使用pyinstxtractor解包,填充pyc头,最后使用uncompyle6反编译得到py代码。

如题目的名称,此程序中的加法,移位,异或运算都是作者自己从二进制的角度实现的。

加法:

image-20220228154125971

逻辑右移:
image-20220228154029768

逻辑左移:
image-20220228154056555

异或:
image-20220228154201894

这样来看此程序就是很明显的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
def tea_decode(enc, k):
v0 = enc[0]
v1 = enc[1]
sum = (delta*32)&0xffffffff
for i in range(32):
v1 -= ((v0<<4) + k[2]) ^ (v0 + sum) ^ ((v0>>5) + k[3])
v1 &= 0xffffffff
v0 -= ((v1<<4) + k[0]) ^ (v1 + sum) ^ ((v1>>5) + k[1])
v0 &= 0xffffffff
sum -= delta
return (v0, v1)

enc = '001111101000100101000111110010111100110010010100010001100011100100110001001101011000001110001000001110110000101101101000100100111101101001100010011100110110000100111011001011100110010000100111'
enc = [int(enc[i:i+32], 2) for i in range(0, len(enc), 32)]

k = [0]*4
k[0] = int('0100010001000101'.zfill(32), 2)
k[1] = int('0100000101000100'.zfill(32), 2)
k[2] = int('0100001001000101'.zfill(32), 2)
k[3] = int('0100010101000110'.zfill(32), 2)
delta = int('10011110001101110111100110111001', 2)

flag = b''
for i in range(0, len(enc), 2):
ans = tea_decode(enc[i:i+2], k)
flag += ans[0].to_bytes(4, 'big')+ans[1].to_bytes(4, 'big')

print(b'SUSCTF{'+flag+b'}')

hell_world

字符串定位关键函数,从case7能推出输入长度及case9是加密操作。

image-20220228160432623

其中case9是将输入转化为二进制,只是0和1使用2和3来替换,接着执行的case10是使用硬编码的数组数据与输入进行异或操作且转化为二进制。

image-20220228161525085

再来看到紧接着case10执行的case11,关键就是比较操作,密文在dword_7FF759245B80数组。

image-20220228161753597

所以说,使用两个数组异或就解密了。

1
2
3
4
5
6
7
>>> enc
[5, 143, 158, 121, 42, 192, 104, 129, 45, 252, 207, 164, 181, 85, 95, 228, 157, 35, 214, 29, 241, 231, 151, 145, 6, 36, 66, 113, 60, 88, 92, 48, 25, 198, 245, 188, 75, 66, 93, 218, 88, 155, 36, 64]
>>> x
[86, 218, 205, 58, 126, 134, 19, 181, 29, 157, 252, 151, 140, 49, 107, 201, 251, 26, 226, 45, 220, 211, 241, 244, 54, 9, 32, 66, 4, 106, 113, 83, 120, 164, 151, 143, 122, 114, 57, 232, 61, 250, 64, 61]
>>> flag = [enc[i]^x[i] for i in range(len(enc))]
>>> bytes(flag)
b'SUSCTF{40a339d4-f940-4fe0-b382-cabb310d2ead}'

tttree

程序中的几种花指令:

1、

image-20220228165327363

2、

image-20220228171125684

开始想的是把所有汇编代码复制到文本中,用python来提取计算出要跳转的地址,然后记录出要替换的地址及要替换成的汇编代码,最后批量patch一下,但是在做的时候对第二种花指令没有考虑前面的push操作,所以最后弄完没啥效果,就干脆不去除了。

从最后判断入手,有5个error的字符串,因为我将大部分的花指令都正确替换成了对应的jmp,所以从字符串error就能找到对应的被调用地址,调试定位到其中有2个是check输入长度和格式不对时要输出的error。

image-20220228173239435

其次另外3个error是最后的checK加密结果的,不断向上回溯看汇编可以发现最后的check中循环用到了dword_1400073C0这个数组。

image-20220228173416091

重新打开一个未patch过的程序,在引用了dword_1400073C0数组对应的地址下断点,运行看对应的内存。
image-20220228174506800

另外从其对这个数组的访问下标看到都是乘以了28,因此可以知道这个数据是28个字节为一组,7个4字节变量。

image-20220228174739680

观察这个内存数据,可以发现每组的最后一个4字节变量正好对应我们的输入且在这个数组中的存放顺序就是我们输入顺序,idapython导出数据便于观察。

1
2
3
4
5
6
7
8
9
10
11
from ida_bytes import *

s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'

addr = 0x1400073C0+7*4
ans = []
for i in range(32):
ans += [[s[i], get_dword(addr), get_dword(addr+4), get_dword(addr+12)]]
addr += 7*4

print(ans)
1
[['A', 0, 19, 5], ['B', 15, 18, 17], ['C', 0, 0, 1], ['D', 0, 11, 2], ['E', 4, 21, 32], ['F', 0, 26, 3], ['G', 0, 0, 1], ['H', 0, 6, 4], ['I', 28, 1, 7], ['J', 0, 3, 2], ['K', 0, 0, 1], ['L', 22, 0, 3], ['M', 0, 0, 1], ['N', 0, 29, 4], ['O', 8, 12, 9], ['P', 10, 0, 3], ['Q', 0, 0, 1], ['R', 14, 13, 7], ['S', 16, 0, 4], ['T', 0, 0, 2], ['U', 25, 24, 29], ['V', 23, 0, 2], ['W', 0, 0, 1], ['X', 0, 0, 1], ['Y', 2, 27, 27], ['Z', 7, 0, 2], ['2', 9, 17, 9], ['3', 0, 0, 1], ['4', 20, 0, 3], ['5', 0, 0, 0], ['6', 0, 0, 0], ['7', 0, 0, 0]]

再通过看汇编与调试分析出在check时,如果每组的第一个4字节变量与第二个4字节变量不为0,那么他和输入有如下关系:

如果是第一个4字节变量不为0:input[i]+23*i == b[i]

如果是第二个4字节变量不为0:input[i]+23*i == c[i]

另外每次还要check每组的第三个4字节变量是否等于a[i]

以上的a,b,c分别对应程序中如下的三个硬编码密文表:
image-20220228180145280

再通过调试dword_1400073C0的填充与最后check过程,能得出这是一个树的构造过程(又根据提示确定是二叉树),那么我们上面看到的每组28个字节的数据应该就是一个节点,且每组的第一个4字节变量与第二个4字节变量分别指向的它的左子树和右子树的位置,下标从1开始,0表示空,这也正好对应dword_1400073C0数组的第一个28字节单元为0。

为了知道这个二叉树的构成,我输入测试数据 ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 ,然后根据dword_1400073C0数组数据画出生成的二叉树,但完全不符合二叉搜索树的构造,,后面好久才反应过来这个二叉树并不是将输入的ascii值作为权重值,而是将输入经过运算后的值,这个值就对应每组28字节数据中的第三个4字节数据,这样也就正好符合我画出的二叉树了。

接着跟踪输入的运算,得出对对输入的运算就是一个加法值,而这个加法的值生成是将 19600416 作为初始化数据,然后一些乘法、模除、加法运算。

image-20220228195930604

image-20220228200021101

image-20220228200039151

写成python代码:

1
2
3
4
5
x = []
sum = 19600416
for i in range(32):
sum = sum*48271%0x000000007FFFFFFF
x += [sum%107+0x61+i]

再接着就是根据我画出的二叉数看汇编与调试check过程,得出是后序遍历取数据进行对比的,那么我们就可以根据密文数组a(上面提到的)画出还原的二叉树,因为这个是二叉搜索树,所以根据后序遍历是能唯一确定一棵二叉树的。

还原出二叉数后,接着就是还原出他们的下标了,先从要判断的前三个叶子节点开始用他们之间的满足的关系爆破出开始的正确下标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
a = [0, 0x00000000000000A8, 0x0000000000000131, 0x0000000000000113, 0x0000000000000047, 0x000000000000009E, 0x000000000000003B, 0x000000000000003A, 0x00000000000000BF, 0x0000000000000092, 0x00000000000000F0, 0x0000000000000174, 0x00000000000000C3, 0x0000000000000289, 0x0000000000000104, 0x0000000000000260, 0x000000000000004D, 0x00000000000002FB, 0x000000000000009E, 0x0000000000000191, 0x0000000000000158, 0x000000000000007D, 0x000000000000004A, 0x00000000000001E9, 0x0000000000000101, 0x00000000000000D0, 0x00000000000000FC, 0x0000000000000070, 0x000000000000011F, 0x0000000000000345, 0x0000000000000162, 0x00000000000002A4, 0x0000000000000092]
b = [0, 0x00000000000000AC, 0x00000000000000FD, 0x0000000000000247, 0x0000000000000115, 0x00000000000000D4, 0x00000000000002B5, 0x00000000000001FC, 0x000000000000028B, 0x000000000000014A, 0x000000000000004C, 0x000000000000008E, 0x00000000000000E9, 0x0000000000000055, 0x000000000000012C, 0x00000000000000F5, 0x00000000000000E3, 0x0000000000000081, 0x00000000000002E2, 0x00000000000001A8, 0x0000000000000117, 0x0000000000000152, 0x0000000000000101, 0x000000000000003A, 0x00000000000001D0, 0x00000000000000A8, 0x00000000000000CC, 0x0000000000000149, 0x0000000000000137, 0x0000000000000300, 0x00000000000001EC, 0x0000000000000276, 0x0000000000000247]
c = [0, 162, 175, 157, 183, 210, 203, 199, 198, 176, 213, 218, 227, 230, 232, 233, 243, 244, 239, 238, 247, 249, 255, 257, 245, 265, 287, 282, 326, 292, 271, 262, 223]

x = []
sum = 19600416
for i in range(32):
sum = sum*48271%0x000000007FFFFFFF
x += [sum%107+0x61+i]
#print(sum)

x = [0]+x
print(x)
for i in range(1, 33):
c = [0, 162, 175, 157, 183, 210, 203, 199, 198, 176, 213, 218, 227, 230, 232, 233, 243, 244, 239, 238, 247, 249, 255, 257, 245, 265, 287, 282, 326, 292, 271, 262, 223]
c[1] -= x[i]
c[2] = a[2]-23*i
if 175-c[2] in x:
j = x.index(175-c[2])
c[3] = b[3]-23*23
print(i, 23)
print(chr(c[1]), end = '')
print(chr(c[2]), end = '')
print(chr(c[3]), end = '')

上面有一个坑,就是取出的满足数据是123,而123这个数据在x数组中有2个,5和23,使用x.index()只能得到第一个,而满足条件的正好是后面一个,这里卡了好久,,,

得到第一个下标后,后面的所有的下标就都可以递推出来了。

部分运算过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 157-x[5]
30
>>> 157-x[23]
30
>>> 157-0x36
103
>>> x = [0, 193, 168, 197, 103, 123, 127, 133, 122, 182, 187, 112, 145, 165, 157, 131, 191, 204, 159, 191, 154, 207, 133, 123, 127, 216, 151, 195, 155, 228, 194, 186, 183]
>>> x.index(103)
4
>>> c.index(176)
9
>>> a[9]-4*23
54
>>> chr(54)
'6'

过程图,手画的,hh,

image-20220228210311728

1
2
3
4
5
6
7
8
>>> ans
[0, 249, 218, 247, 157, 223, 183, 230, 176, 238, 287, 162, 198, 265, 213, 233, 239, 255, 257, 292, 203, 262, 232, 175, 227, 271, 199, 245, 210, 326, 244, 243, 282]
>>> x
[0, 193, 168, 197, 103, 123, 127, 133, 122, 182, 187, 112, 145, 165, 157, 131, 191, 204, 159, 191, 154, 207, 133, 123, 127, 216, 151, 195, 155, 228, 194, 186, 183]
>>> flag = [ans[i]-x[i] for i in range(len(x))]
>>> flag = flag[1:]
>>> ''.join(map(chr ,flag))
'8226d8a68d25d8f03be17c4d7027b29c'
-------------本文结束感谢您的阅读-------------