0%

CUMTCTF2019-Final-WP

CUMTCTF2019决赛-逆向题WP

Hook

出题点在题目上就已经很清楚了,意思就是修改了题目中的函数进行了替换。首先查看main函数:

正常的读去用户输入然后限定长度是19个字符,再走过黄标的函数之后经过sub_401240函数的检验。

函数中的”fake flag”字样表明这并不是真正的检查函数,于是仔细看下之前的黄标函数。

里面开启内存的可写权限然后替换WriteFile函数为图中的黄标函数,进入黄标函数就可以看到真正的检查函数sub_401000

逆向其中的核心加密流程写出对应的解密脚本,其中第一位的信息在加密后的数据中并没有包含。先看看解密脚本。

1
2
3
4
5
6
7
8
target = [97, 108, 121, 103, 107, 72, 109, 48, 127, 95, 126, 49, 83, 104, 123, 70, 109, 110, 125]
index = 18
while index >= 2:
if index % 2 == 0:
target[index] = target[index - 2] ^ (index - 2)
index -= 1

print("".join(list(map(chr,target))))

跑完之后得到的结果是alag{Ho0k_w1th_Fun,其中第一位不难猜出就是f

Cracker

Windows的窗口程序管理查看WinMain函数内容,发现窗口处理函数为sub_4013E0

这个函数是一个很典型的按照接受到的事件进行分发处理的函数,对应的函数是黄标的sub_401350。其中进行加密和检验的函数是sub_401200

在这个函数中用户输入经过两个函数分别处理,也就是说变换了两次,先看第一次。

简单的下标计算进行变换,再看第二个,

这个函数中间有无用的数据,用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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
from z3 import *

opcode = '''
movzx eax, byte ptr [ecx+7]
add al, [ecx+18h]
mov [edx], al
movzx eax, byte ptr [ecx]
add al, [ecx+18h]
mov [edx+1], al
movzx eax, byte ptr [ecx+11h]
add al, [ecx]
mov [edx+2], al
movzx eax, byte ptr [ecx+22h]
add al, [ecx+11h]
mov [edx+3], al
movzx eax, byte ptr [ecx+22h]
add al, [ecx+0Ah]
mov [edx+4], al
movzx eax, byte ptr [ecx+1Bh]
add al, [ecx+0Ah]
mov [edx+5], al
movzx eax, byte ptr [ecx+1Bh]
add al, [ecx+3]
mov [edx+6], al
movzx eax, byte ptr [ecx+14h]
add al, [ecx+3]
mov [edx+7], al
movzx eax, byte ptr [ecx+14h]
add al, [ecx+25h]
mov [edx+8], al
movzx eax, byte ptr [ecx+0Dh]
add al, [ecx+25h]
mov [edx+9], al
movzx eax, byte ptr [ecx+0Dh]
add al, [ecx+1Eh]
mov [edx+0Ah], al
movzx eax, byte ptr [ecx+6]
add al, [ecx+1Eh]
mov [edx+0Bh], al
movzx eax, byte ptr [ecx+6]
add al, [ecx+17h]
mov [edx+0Ch], al
movzx eax, byte ptr [ecx+28h]
add al, [ecx+17h]
mov [edx+0Dh], al
movzx eax, byte ptr [ecx+28h]
add al, [ecx+10h]
mov [edx+0Eh], al
movzx eax, byte ptr [ecx+21h]
add al, [ecx+10h]
mov [edx+0Fh], al
movzx eax, byte ptr [ecx+21h]
add al, [ecx+9]
mov [edx+10h], al
movzx eax, byte ptr [ecx+1Ah]
add al, [ecx+9]
mov [edx+11h], al
movzx eax, byte ptr [ecx+1Ah]
add al, [ecx+2]
mov [edx+12h], al
movzx eax, byte ptr [ecx+13h]
add al, [ecx+2]
mov [edx+13h], al
movzx eax, byte ptr [ecx+13h]
add al, [ecx+24h]
mov [edx+14h], al
movzx eax, byte ptr [ecx+0Ch]
add al, [ecx+24h]
mov [edx+15h], al
movzx eax, byte ptr [ecx+0Ch]
add al, [ecx+1Dh]
mov [edx+16h], al
movzx eax, byte ptr [ecx+5]
add al, [ecx+1Dh]
mov [edx+17h], al
movzx eax, byte ptr [ecx+5]
add al, [ecx+16h]
mov [edx+18h], al
movzx eax, byte ptr [ecx+27h]
add al, [ecx+16h]
mov [edx+19h], al
movzx eax, byte ptr [ecx+27h]
add al, [ecx+0Fh]
mov [edx+1Ah], al
movzx eax, byte ptr [ecx+20h]
add al, [ecx+0Fh]
mov [edx+1Bh], al
movzx eax, byte ptr [ecx+20h]
add al, [ecx+8]
mov [edx+1Ch], al
movzx eax, byte ptr [ecx+19h]
add al, [ecx+8]
mov [edx+1Dh], al
movzx eax, byte ptr [ecx+19h]
add al, [ecx+1]
mov [edx+1Eh], al
movzx eax, byte ptr [ecx+12h]
add al, [ecx+1]
mov [edx+1Fh], al
movzx eax, byte ptr [ecx+12h]
add al, [ecx+23h]
mov [edx+20h], al
movzx eax, byte ptr [ecx+0Bh]
add al, [ecx+23h]
mov [edx+21h], al
movzx eax, byte ptr [ecx+0Bh]
add al, [ecx+1Ch]
mov [edx+22h], al
movzx eax, byte ptr [ecx+4]
add al, [ecx+1Ch]
mov [edx+23h], al
movzx eax, byte ptr [ecx+4]
add al, [ecx+15h]
mov [edx+24h], al
movzx eax, byte ptr [ecx+26h]
add al, [ecx+15h]
mov [edx+25h], al
movzx eax, byte ptr [ecx+26h]
add al, [ecx+0Eh]
mov [edx+26h], al
movzx eax, byte ptr [ecx+1Fh]
add al, [ecx+0Eh]
mov [edx+27h], al
movzx eax, byte ptr [ecx+1Fh]
add al, [ecx+7]
mov [edx+28h], al
'''

opcode = opcode.split('\n')[1:-1]

def stri(ss):
if ss == "":
return 0
if ss[-1] == 'h':
return int(ss[:-1],16)
else:
return int(ss, 16)

s = Solver()
inp = []
mid = []

target = [151, 177, 200, 205, 213, 210, 150, 148, 184, 180, 197, 218, 164, 113, 113, 143, 190, 148, 164, 208, 222, 243, 213, 212, 212, 196, 217, 229, 193, 196, 213, 218, 244, 231, 193, 190, 215, 193, 150, 100, 0x6a]

for i in range(41):
inp.append(BitVec("inp"+str(i),8))
mid.append(BitVec("mid"+str(i),8))

for i in range(41):
one = opcode[i*3]
two = opcode[i*3+1]
three = opcode[i*3+2]
one = stri(one[one.find('ecx')+4:-1])
two = stri(two[two.find('ecx')+4:-1])
three = stri(three[three.find('[edx')+5:three.find('],')])
print("mid[" + str(one)+ "] + mid[" + str(two) + "] => out[" + str(three) + "]")
s.add(mid[one] + mid[two] == target[three])


v2 = 0;
v4 = 0;
while (v4-23) < 966:
v5 = v4 % 41;
v4 += 23;
v2 += 33;
#mid[(v2 % 41)] = inp[v5];
print('inp[' + str(v5) + "] => mid[" + str(v2 % 41) + "]")
s.add(inp[v5] == mid[(v2 % 41)])

print(s.check())
print(s.model())

res = []

for i in inp:
res.append(s.model()[i].as_long())

print("".join(map(chr, res)))

Block

Block这道题同样是Windows的窗体程序,其中最引人瞩目的是sub_401060的这一段。

在这里创建了自己作为子进程然后进行调试,检查得到无效的机器码执行就把对应的位置+2的一段内存异或上0x76,这就是它变换自己程序代码的过程。利用Python脚本解密完之后得到对应的反编译代码如下。

加密逻辑就是简单的异或,直接编写脚本即可解密求解。

Cynic

第一步先根据文章修改function size。
文章链接:https://www.dllhook.com/post/217.html

将这个标记位修改完成之后,我们就能正常查看整个函数和进行F5反编译了,但是速度很慢。
耐性等待就好,只反编译一次就够用了。

反编译完成之后我们可以看到,基础的输入输出,判断长度和运算都没变,而且确实没有了VM结构。反而多了一堆异或运算。那么我们先确定这些异或运算会不会影响我们的输入输出。

随便点击几个可以发现他是迭代异或下去的,然而数据在我们对比的数据后门很多,那么就说明这是不影响我们输入输出的东西,就可以当做垃圾代码除掉了。(先将F5得到的代码全选保存成TXT文档)

1
2
3
4
5
6
7
8
9
10
11
12
13
import struct
import os
import re

f = open("origin_source.txt", "r")
wf = open("de_source.txt", "w")
for i in range(25036):
tmp = f.readline().strip()
if "byte_6941" in tmp:
wf.write(tmp + "\n")
print tmp
f.close()
wf.close()

除去之后就只剩这些与运算相关的数据了,但是这里因为他用的全局变量来存储的flag字符串,所以我们没有办法吧byte_xxxxx合并为一个数组,那么我们就用正则去匹配合并好了,同样也是只有简单运算,合并好了之后倒着运算一遍就出来结果了。

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

byte_overflow = lambda x: x & 0xff
cipher_arr = [192, 133, 249, 108, 226, 20, 187, 228, 13, 89, 28, 35, 136, 110, 155, 202, 186, 92, 55, 255, 72, 216, 31,
171, 0xa5]
op_switch = {'+=': lambda eindex, data: cipher_arr[eindex] - data, '-=': lambda eindex, data: cipher_arr[eindex] + data,
'^=': lambda eindex, data: cipher_arr[eindex] ^ data, '++': lambda eindex, data: cipher_arr[eindex] - 1,
'--': lambda eindex, data: cipher_arr[eindex] + 1}
f = open("de_source.txt", "r")
instruction = []
for i in range(2415):
tmp = f.readline().strip()
result = re.findall("byte_6941(.{2})\s(.{2})\s(.+);|(.{2})byte_6941(.{2})", tmp)[0]
if len(result) == 5:
# print result
instruction.append(list(result))
else:
print tmp
# print instruction
instruction = instruction[::-1]
for i in range(len(instruction)):
if instruction[i][0] != '':
vm_index = int(instruction[i][0], 16)
else:
vm_index = int(instruction[i][4], 16)
if instruction[i][1] != '':
vm_optation = instruction[i][1]
else:
vm_optation = instruction[i][3]
if instruction[i][2] != '':
if instruction[i][2].startswith("0x"):
vm_data = int(instruction[i][2].strip("u"), 16)
else:
vm_data = int(instruction[i][2].strip("u"), 10)
else:
vm_data = None
cipher_arr[vm_index] = op_switch[vm_optation](vm_index, vm_data)
print "".join(map(chr, map(byte_overflow, cipher_arr)))
f.close()

MBR

首先查看文件类型发现这可能是一个MBR主引导扇区文件。运行一下这个程序:

1
qemu-system-i386 -drive format=raw,file=main.bin

然后用32位IDA的16-bit模式打开main.bin,发现汇编代码,找到标志检查位,代码分析结果如下(注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
seg000:0066                 cmp     byte ptr ds:7DC8h, 13h
seg000:006B jle loc_10D
seg000:006F cmp dword ptr ds:1234h, 67616C66h ; galf
seg000:0078 jnz loc_14D
seg000:007C movaps xmm0, xmmword ptr ds:1238h ; 将flag后面的字符放入xmm0
seg000:0081 movaps xmm5, xmmword ptr ds:7C00h
seg000:0086 pshufd xmm0, xmm0, 1Eh ; 将16个字符按顺序分成4组,每组4个字符,从左到右分别是第一组到第四组,原来第一组变成第三组,第二组变成第四组,第三组变成第二组,第四组变成第一组。
seg000:008B mov si, 8
seg000:008E
seg000:008E loc_8E: ; CODE XREF: seg000:00C1↓j
seg000:008E movaps xmm2, xmm0 ; 将xmm0中的字符串给xmm2
seg000:0091 andps xmm2, xmmword ptr [si+7D90h] ; [si+7d90h]=[7d98h]
seg000:0096 psadbw xmm5, xmm2 ; 计算xmm5与xmm2中的压缩无符号字节整数的绝对差值;然后将8个低位差值单独相加,8个高位差值单独相加,以产生两个字整数结果。
seg000:0096 psadbw xmm5, xmm2 ; 计算xmm5与xmm2中的压缩无符号字节整数的绝对差值;然后将8个低位差值单独相加,8个高位差值单独相加,以产生两个字整数结果

根据之前做过的CSAW逆向题遇到过这种模式的题目,照着思路调试了一遍

模拟对应的指令操作写出解密脚本:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/usr/bin/env python
from pprint import pprint
from z3 import *
import struct

s = Solver()
ZERO = IntVal(0)

def z3_abs(x):
return If(x >= 0, x, -x)

def psadbw(xmm1, xmm2):
first = Sum([z3_abs(b1 - b2) for b1,b2 in zip(xmm1[:8], xmm2[:8])])
second = Sum([z3_abs(b1 - b2) for b1,b2 in zip(xmm1[8:], xmm2[8:])])
return (first, second)
[0x2DD02F6, 0x2DC02E8, 0x2D802ED, 0x2CE02E2, 0x2C402E2, 0x2D402DB, 0x2D902CD, 0x3110304]
_results = [
(0x02dd, 0x02f6),
(0x02dc, 0x02e8),
(0x02d8, 0x02ed),
(0x02ce, 0x02e2),
(0x02c4, 0x02e2),
(0x02d4, 0x02db),
(0x02d9, 0x02cd),
(0x0311, 0x0304)
] [::-1]

_xmm5s = [
[0xb8, 0x13, 0x00, 0xcd, 0x10, 0x0f, 0x20, 0xc0, 0x83, 0xe0, 0xfb, 0x83, 0xc8, 0x02, 0x0f, 0x22],
]

for x in _results[:-1]:
_xmm5s.append(list(map(ord, struct.pack('<Q', x[0]) + struct.pack('<Q', x[1]))))

xmm5s = [ [IntVal(x) for x in row] for row in _xmm5s ]
results = [ [IntVal(x) for x in row] for row in _results ]

f = [Int('flag{:02}'.format(i)) for i in range(16)]
for char in f:
s.add(char > 30, char < 127)

for i in range(8):
xmm5 = xmm5s[i]
xmm2 = list(f)
xmm2[i] = ZERO
xmm2[i+8] = ZERO
high,low = psadbw(xmm5, xmm2)
s.add(high == results[i][0])
s.add(low == results[i][1])

print(s.check())
m = s.model()

solution = ''
sats = []
for d in m.decls():
if 'flag' in d.name():
solution += chr(m[d].as_long())
sats.append((int(d.name()[4:]), chr(m[d].as_long())))
sats = sorted(sats, key=lambda x: x[0])
sats = [s[1] for s in sats]
flag = ''.join(sats)

# unshuffle the flag
flag = flag[12:] + flag[8:12] + flag[:8]
print('flag{%s}' % flag)

Morse

终于见到了一道Mobile逆向题。

打开之后随便点击几下屏幕,偶尔绿偶尔红,Logcat也会有输出,猜测是根据点击屏幕的时间长短来处理。

用C++写的native activity,manifest里什么东西都没有,dex里只有support的包,逻辑全都在libnative.so里。

看一下导入表,大概率是使用EGL去绘制一些东西,以及gettimeofday来计时。

看一下导出表,入口在android_main,注册了2个函数,sub_F3ECBF70看起来是onCreate,初始化EGL,sub_F3ECC154看起来是onTouch做check。

猜测:AMotionEvent_getAction拿到的是onKeyDown和onKeyUp的事件,分别调用gettimeofday,拿到按压的持续时长,并且按下时(v3+40)=0,会将屏幕主动调整为颜色A。大于0.2ms是1,小于0.2ms是0,这与题目说的Morse很像。之后index = 7 ((v3 + 52) % 4) + (v3 + 52) / 32与byte_F3ED0008[index]做xor,最后bit相同时候认为本次check成功,(v3+40)=0颜色调整为A,byte_F3ED0008[index] >>= 1, (v3 + 52)++,一旦(v3 + 52)大于224,就认为全部判定成功。若本次判定失败,则重置(v3 + 52) = 0,设定颜色*(v3 + 44) = 0x3F800000为颜色B。最后,如果上文一直没有return,就会复制一段rodata到bss段上,将byte_F3ED0008[28]初始化。

综上,(v3 + 52)最初为0,经过224次变化,每次变化会加一,而index = 7 ((v3 + 52) % 4) + (v3 + 52) / 32,dump出byte_F3ED0008[index]和index的序列,即可还原出flag的bit顺序。

之后遇到一个小问题,还原出来的224bit = 28 8bit = 327bit,而0对应0还是1,所以共有4种组合,最后得到32个字符最合理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
data = [0xA7,0xD6,0x61,0xB5,0x6E,0xBB,0xBA,0xE3,0xA9,0xDD,0xC4,0x77,0x6F,0xEE,0xEC,0xFF,0x62,0xC3,0xCF,0xDA,0x53,0xCE,0xFF,0x71,0x71,0x14,0xFF,0xF2]
res = []
for i in range(224):
v10 = 7 * (i % 4) + i / 32;
#print v10
if data[v10] & 1:
res.append(1)
else:
res.append(0)
tmp = data[v10] >> 1
data[v10] = tmp

out = ""
print res
#res.reverse()
for i in range(len(res)):
if i % 7 == 0:
out += chr(tmp)
tmp = 0
tmp = tmp << 1
if res[i]:
tmp = tmp | 1
out += chr(tmp)
print out