August 26, 2023

WMCTF2023 REVERSE

ezAndroid

0x00 Daily Shell Check

无壳安卓逆向

0x01 Java层

找到主活动,逻辑比较简单,就是对我们的用户名和密码分别进行 check1 和 check2,这两个函数是 native 层的,所以转到 so 层

image-20230826142540764

0x02 So 层

Anti Frida

在 so 层里搜这两个函数,发现没搜到,所以猜测是动态注册的,于是我们尝试打印出所有动态注册的 native 函数

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
function hook_RegisterNatives() {
var _symbols = Process.getModuleByName('libart.so').enumerateSymbols();
var RegisterNativesAddr = null;
for (var i = 0; i < _symbols.length; i++ ) {
var _symbol = _symbols[i];
if (_symbol.name.indexOf("RegisterNatives") != -1 && _symbol.name.indexOf("CheckJNI") == -1){
RegisterNativesAddr = _symbol.address;
}
}

Interceptor.attach(RegisterNativesAddr, {
onEnter:function (args) {
console.log("Attach success!");
var env = Java.vm.tryGetEnv();
var className = env.getClassName(args[1]);
var methodCount = args[3].toInt32();
for (let i = 0; i < methodCount; i++) {
var methodName = args[2].add(Process.pointerSize * 3 * i).readPointer().readCString();
var signature = args[2].add(Process.pointerSize * 3 * i).add(Process.pointerSize).readPointer().readCString();
var fnPtr = args[2].add(Process.pointerSize * 3 * i).add(Process.pointerSize * 2).readPointer();
var module = Process.findModuleByAddress(fnPtr);
console.log(className, methodName, signature, fnPtr, module.name, fnPtr.sub(module.base));
}
}, onLeave:function (retval) {

}
})
}

然而 frida 直接闪退了,可以猜到可以 frida 反调试,检查 init 段发现该函数中 pthread_create 启了个子线程

image-20230826150901977

1
2
3
4
5
6
7
__int64 sub_3538()
{
pthread_t newthread[2]; // [xsp+20h] [xbp-10h] BYREF

newthread[1] = *(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
return pthread_create(newthread, 0LL, sub_34C8, 0LL);
}

于是我们去 hook 该启动线程的函数,将用于反调试函数的线程取消

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_AntiFrida_func() {
var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
Interceptor.attach(pthread_create_addr, {
onEnter(args) {
let func_addr = args[2];
if (args[3] == 0) {
Interceptor.replace(func_addr, new NativeCallback(function(){
console.log("[*] Replace antiFrida success!");
}, 'void', []))
}
}
});
}

随后再以 spawn 方式启动来挂钩得到回显,得知注册了这两个函数

1
2
3
4
[*] Replace antiFrida success!
Attach success!
com.wmctf.ezandroid.MainActivity CheckUsername (Ljava/lang/String;)I 0x75c420e5a4 libezandroid.so 0x35a4
com.wmctf.ezandroid.MainActivity check2 (Ljava/lang/String;)I 0x75c420ef0c libezandroid.so 0x3f0c

sub_35A4

经过了 OLLVM 的混淆,开启 D810 会稍微好看一点,不过稍微浏览也能很快定位到这里的数据比较,而这里的数据在

image-20230826151434860

不难猜测应该是我们的输入经过了 6C2C 函数后与该数据进行比较,于是查看 6C2C 函数,可以发现就是 RC4 函数,经过唯一经过的改变就是多明文多异或了个下标

image-20230826151603813

于是去取到密文与密钥即可解密,密文直接按X比较的数据发现是在 init 段初始化了,而密钥可以通过 frida 直接 hook 该函数进行打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hook_sub_6C2C() {
var soAddr = Module.findBaseAddress("libezandroid.so");
var sub_6C2C = soAddr.add(0x6C2C);
Interceptor.attach(sub_6C2C, {
onEnter: function (args) {
console.log("[*] sub_6C2C called!");
console.log("arg0: " + args[0].readCString());
console.log("arg1: " + args[1].readCString());
console.log("arg2: " + args[2]);
console.log("arg3: " + args[3].readCString());
console.log("arg4: " + args[4]);
}, onLeave: function (retval) {

}
});
}

输入十位用户名得到结果,12345678 就是我们的密钥

1
2
3
4
5
6
[*] sub_6C2C called!
arg0: 1231231231
arg1:
arg2: 0xa
arg3: 12345678
arg4: 0x8

于是 RC4 解密再异或下标得到用户名 Re_1s_eaSy

sub_3F0C

同样翻一下很快能定位到比较函数,那么同样密文可以从 init 里获取

image-20230826152331205

而该加密函数(虽然我已经写了)我们可以通过里面的常量和大概的操作可以判断出是AES,findcrypt插件同样也可以定位到这里面,那么一样我们通过 frida 来获取密钥,可以回忆下 java 层是先判断用户名,再判断密码,所以在用户名输入刚刚解密出的正确用户名,再随意输入 16 位密码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hook_sub_AFC() {
var soAddr = Module.findBaseAddress("libezandroid.so");
var sub_AFC = soAddr.add(0xAFC);
Interceptor.attach(sub_AFC, {
onEnter: function (args) {
console.log("[*] sub_AFC called!");
console.log("arg0: " + args[0].readCString());
console.log("arg1: " + args[1]);
console.log("arg2: " + args[2].readCString());
}, onLeave:function (retval) {

}
})
}

得到回显

1
2
3
4
[*] sub_AFC called!
arg0: 1234123412341234
arg1: 0x10
arg2: Re_1s_eaSy123456

0x03 Get Flag

随后我们进行 AES 解密,发现不成功,于是去查看哪里魔改了,发现只是魔改了我们的 Sbox,同样可以在 init 段找到我们魔改的 SBOX,拿出来逆一下 S 盒即可解密拿到我们的 flag

1
WMCTF{Re_1s_eaSy_eZ_Rc4_@nd_AES!}

RightBack

0x00 Daily Shell Check

pyc文件,查看Read可知运行在python3.9下

0x01 Deobfuscation

Find Flower

Pyc文件直接使用pycdc进行反编译,不过可以发现直接报错,所以去观察一下python的字节码

image-20230726153928565

不难发现有大量花指令,看似好像每个花指令都不一样,但其实形式都是一样的,形如

1
2
3
4
5
6
# JUMP_FORWARD  0       6E 0
# JUMP_FORWARD 4 6E 4
# 4个无意义字节 1 2 3 4
# JUMP_FORWARD 2 6E 2
# 两个无意义字节 5 6
# org

所以我们只需要把这些全部nop就能解决了?对于这种类型的花是可以全部 nop 解决,而有些样本的花不能解决,因为会影响到 co_lnotab

  1. 这是指令与行号的对应表
  2. Python通过co_lnotab将字节码和源码行数对齐,服务于源码调试
  3. 当我们改成了nop后,带参数的指令变成不带参数的指令定然会导致字节码偏移计算错误

https://svn.python.org/projects/python/branches/pep-0384/Objects/lnotab_notes.txt

Remove Flower

于是我就换了个思路,就是直接去除花指令的字节码,但同时也要考虑python的结构体的修复,其中与我们字节码长度相关的结构体就是co_code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'co_argcount'      # code需要的位置参数个数,不包括变长参数(*args 和 **kwargs)
'co_cellvars' # code 所用到的 cellvar 的变量名,tuple 类型, 元素是 PyStringObject('s/t/R')
'co_code' # PyStringObject('s'), code对应的字节码
'co_consts' # 所有常量组成的 tuple
'co_filename' # PyStringObject('s'), 此 code 对应的 py 文件名
'co_firstlineno' # 此 code 对应的 py 文件里的第一行的行号
'co_flags' # 一些标识位,也在 code.h 里定义,注释很清楚,比如 CO_NOFREE(64) 表示此 PyCodeObject 内无 freevars 和 cellvars 等
'co_freevars' # code 所用到的 freevar 的变量名,tuple 类型, 元素是 PyStringObject('s/t/R')
'co_lnotab' # PyStringObject('s'),指令与行号的对应表
'co_name' # 此 code 的名称
'co_names' # code 所用的到符号表, tuple 类型,元素是字符串
'co_nlocals' # code内所有的局部变量的个数,包括所有参数
'co_stacksize' # code段运行时所需要的最大栈深度
'co_varnames' # code 所用到的局部变量名, tuple 类型, 元素是 PyStringObject('s/t/R')

那边这里举一个简单的例子,VNCTF2022的BabyMaze

直接uncompyle6 发现直接G,那就是在字节码上动了什么手脚

BabyMaze/image-20220208173840212

于是我们去看看字节码

1
2
3
4
5
6
7
import marshal, dis

f = open("BabyMaze.pyc", "rb").read()

code = marshal.loads(f[16:]) #这边从16位开始取因为是python3 python2从8位开始取

dis.dis(code)

发现开头就是花指令

BabyMaze/image-20220208174202488

通过python的opcode.h文件 翻译成十六进制

BabyMaze/image-20220208174615675

发现就是这6个十六进制在来回跳

BabyMaze/image-20220208174655142

去掉花的同时也要改co_code,这个是记录字节码的长度,所以我们减去6个这个也要减6

BabyMaze/image-20220208174817071

于是去掉那6个,再把co_code减6即可

BabyMaze/image-20220208174937176

成功uncompyle6

BabyMaze/image-20220208175032919

也就是在修改了字节码后,同时要改co_code的长度,而根据python字节码的机制,每个函数会分成一个个代码段,在我们去除了每个代码段花的同时同时也要修改相对应的co_code,同时我们也可以根据每个代码段的头都是 0x73 来读取每个代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def slice_code(code):
# 记录代码段的 开头 与 长度
code_attribute = []
for i in range(len(code)):
if code[i] == 0x73:
size = int(struct.unpack("<I", bytes(code[i + 1:i + 5]))[0])
try:
if code[size + i + 5 - 2] == 0x53:
code_attribute.append({
'index': i + 5,
'len': size
})
except:
pass
# 取出每个代码段
code_list = []
for i in range(len(code_attribute)):
code_list.append(code[code_attribute[i]['index']: code_attribute[i]['index'] + code_attribute[i]['len']])

return code_attribute, code_list

读取完毕后,就可以开始修复代码段了,要注意的点有

  1. 记录去除的指令长度修改co_code
  2. 修复跳转语句
  3. 去除所有的花指令

由于python的跳转是硬编码进去的,所以当我们去除了字节,整个跳转就乱掉了,于是要修改每个跳转语句,而跳转语句总结一下就是

1
2
3
4
5
两类跳转
1. 相对跳转
1.1 检测当前地址到目标地址中间的cnt
2. 绝对跳转
2.1 检测起始地址到目标地址之前的cnt

那么按照这个思路就可以完整的去除整个文件了

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


def sliceCode(code):
code_attribute = []
for i in range(len(code)):
if code[i] == 0x73 :
size = int(struct.unpack("<I", bytes(code[i + 1:i + 5]))[0])
try:
num = 3 + i
if code[size + num] == 0x53:
code_attribute.append({
'index': i + 5,
'len': size
})
except:
pass
code_list = []
for i in range(len(code_attribute)):
code_list.append(code[code_attribute[i]['index']: code_attribute[i]['index'] + code_attribute[i]['len']])

return code_attribute, code_list


def repaitJump(code):
flower_target = [0x6E, 0x0]
relative_jump_list = [0x6E, 0x78, 0x5D]
absolute_jump_list = [0x6F, 0x70, 0x71, 0x72, 0x73, 0x77]

for i in range(len(code)):
if code[i] in relative_jump_list:
cnt = 0
jmp_range = code[i + 1] + i + 2
if jmp_range > len(code):
continue
for j in range(i + 2, jmp_range):
if code[j] == flower_target[0] and code[j + 1] == flower_target[1] and j % 2 == 0:
cnt += 1
code[i + 1] -= cnt * 12
elif code[i] in absolute_jump_list:
cnt = 0
jmp_range = code[i + 1]
if jmp_range > len(code):
continue
for j in range(jmp_range):
if code[j] == flower_target[0] and code[j + 1] == flower_target[1] and j % 2 == 0:
cnt += 1
code[i + 1] -= cnt * 12

return code


def removeFlower(code):
org_code = b''
flowerTarget = [0x6E, 0x0]
flower_index = []

for i in range(len(code)):
if code[i] == flowerTarget[0] and code[i + 1] == flowerTarget[1] and i % 2 == 0:
flower_index.append(i)

try:
org_code += bytes(code[:flower_index[0]])
for i in range(1, len(flower_index)):
org_code += bytes(code[flower_index[i - 1] + 12:flower_index[i]])
else:
org_code += bytes(code[flower_index[i] + 12:])
except:
org_code = bytes(code)

return org_code


def main():
filename = "obf_RightBack.pyc"
f = list(open(filename, "rb").read())


code_attribute, code_list = sliceCode(f)
file = b''
for i in range(len(code_attribute)):
if len(code_list[i]) <= 0x100:
code_list[i] = repaitJump(code_list[i])
code_list[i] = removeFlower(code_list[i])
if i == 0:
file += bytes(f[:code_attribute[i]['index'] - 4]) + struct.pack('<I', len(code_list[i])) + bytes(code_list[i])
else:
file += bytes(f[code_attribute[i - 1]['index'] + code_attribute[i - 1]['len']:code_attribute[i]['index'] - 4]) + struct.pack('<I', len(code_list[i])) + bytes(code_list[i])
else:
file += bytes(f[code_attribute[i]['index'] + code_attribute[i]['len']:])


filename = 'rev_' + filename
with open(filename, 'wb') as f:
f.write(file)
print("OK!")


if __name__ == "__main__":
main()

随后成功反编译

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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# -*- coding: utf-8 -*-
import struct


def T(num, round):
numArr = bytearray(struct.pack("<I", num))
for i in range(4):
numArr[i] = Sbox[numArr[i]]

return struct.unpack("<I", numArr)[0] ^ Rcon[round]


def p1(s, key):
j = 0
k = []
for i in range(256):
s.append(i)
k.append(key[i % len(key)])
for i in range(256):
j = (j + s[i] + ord(k[i])) % 256
s[i], s[j] = s[j], s[i]


def p2(key):
w = [0] * 44
for i in range(4):
w[i] = struct.unpack('<I', key[i * 4:i * 4 + 4])[0]

cnt = 0
for i in range(4, 44, 1):
if ( i % 4 == 0 ):
w[i] = w[i - 4] ^ T(w[i - 1], cnt)
cnt += 1
else:
w[i] = w[i - 4] ^ w[i - 1]

return w


def p3(s, p):
i = j = 0
for z in range(len(p)):
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]
p[z] ^= s[(s[i] + s[j]) % 256]

return p


def F1(part1, part2):
global REG
REG = {'EAX': 0, 'EBX': 0, 'ECX': 0, 'EDX': 0, 'R8':0, 'CNT': 0, 'EIP': 0}
REG['EAX'] = part1
REG['EBX'] = part2


def F2(v1, v2, v3):
global REG

if v1 == 1:
global extendKey
REG[reg_table[str(v2)]] = extendKey[REG[reg_table[str(v3)]]]
elif v1 == 2:
REG[reg_table[str(v2)]] = REG[reg_table[str(v3)]]
elif v1 == 3:
REG[reg_table[str(v2)]] = v3

REG['EIP'] += 4


def F3(v1, v2, v3):
global REG

if v1 == 1:
global extendKey
REG[reg_table[str(v2)]] = (REG[reg_table[str(v2)]] + extendKey[REG[reg_table[str(v3)]]]) & 0xFFFFFFFF
elif v1 == 2:
REG[reg_table[str(v2)]] = (REG[reg_table[str(v2)]] + REG[reg_table[str(v3)]]) & 0xFFFFFFFF
elif v1 == 3:
REG[reg_table[str(v2)]] = (REG[reg_table[str(v2)]] + v3) & 0xFFFFFFFF

REG['EIP'] += 4

# 8
def F4(v1, v2):
global REG

REG[reg_table[str(v1)]] ^= REG[reg_table[str(v2)]]

REG['EIP'] += 3


def F5(v1, v2):
global REG

REG[reg_table[str(v1)]] &= v2

REG['EIP'] += 3


def F6(v1, v2, v3):
global REG

if v1 == 1:
global extendKey
REG[reg_table[str(v2)]] -= extendKey[v3]
elif v1 == 2:
REG[reg_table[str(v2)]] -= REG[reg_table[str(v3)]]
elif v1 == 3:
REG[reg_table[str(v2)]] -= v3

REG['EIP'] += 4


def F7(v1, v2):
global REG

REG[reg_table[str(v1)]] |= REG[reg_table[str(v2)]]

REG['EIP'] += 3


def F8(v1, v2):
global REG

REG[reg_table[str(v1)]] = (REG[reg_table[str(v1)]] >> REG[reg_table[str(v2)]]) & 0xFFFFFFFF

REG['EIP'] += 3


def F9(v1, v2):
global REG

REG[reg_table[str(v1)]] = (REG[reg_table[str(v1)]] << REG[reg_table[str(v2)]]) & 0xFFFFFFFF

REG['EIP'] += 3


def FA(v1, v2, v3):
global REG

if v1 == 1:
global extendKey
REG[reg_table[str(v2)]] *= extendKey[v3]
elif v1 == 2:
REG[reg_table[str(v2)]] *= REG[reg_table[str(v3)]]
elif v1 == 3:
REG[reg_table[str(v2)]] *= v3

REG['EIP'] += 4


def FB():
global REG

REG['R8'] = REG['CNT'] == 21

REG['EIP'] += 1

# 16
def WC():
global REG

if not REG['R8']:
REG['EIP'] = 16
else:
REG['EIP'] += 1


def VM(part1, part2):
global REG
global opcode

F1(part1, part2)
while True:
EIP = REG['EIP']
if opcode[EIP] == 0x50:
F2(opcode[EIP + 1], opcode[EIP + 2], opcode[EIP + 3])
elif opcode[EIP] == 0x1D:
F3(opcode[EIP + 1], opcode[EIP + 2], opcode[EIP + 3])
elif opcode[EIP] == 0x71:
F4(opcode[EIP + 1], opcode[EIP + 2])
elif opcode[EIP] == 0x72:
F5(opcode[EIP + 1], opcode[EIP + 2])
elif opcode[EIP] == 0x96:
F6(opcode[EIP + 1], opcode[EIP + 2], opcode[EIP + 3])
elif opcode[EIP] == 0x57:
F7(opcode[EIP + 1], opcode[EIP + 2])
elif opcode[EIP] == 0x74:
F8(opcode[EIP + 1], opcode[EIP + 2])
elif opcode[EIP] == 0x29:
F9(opcode[EIP + 1], opcode[EIP + 2])
elif opcode[EIP] == 0xDC:
FA(opcode[EIP + 1], opcode[EIP + 2], opcode[EIP + 3])
elif opcode[EIP] == 0x7:
FB()
elif opcode[EIP] == 0x99:
WC()
else:
break


def Have():
Hello = """
|| / | / /
|| / | / / ___ // ___ ___ _ __ ___ __ ___ ___
|| / /||/ / //___) ) // // ) ) // ) ) // ) ) ) ) //___) ) / / // ) )
||/ / | / // // // // / / // / / / / // / / // / /
| / | / ((____ // ((____ ((___/ / // / / / / ((____ / / ((___/ /


|| / | / / /| //| | // ) ) /__ ___/ // / / ___ ___ ___ ___
|| / | / / //| // | | // / / //___ // ) ) // ) ) // ) ) // ) )
|| / /||/ / // | // | | // / / / ___ ___/ / // / / ___/ / __ / /
||/ / | / // | // | | // / / // / ____/ // / / / ____/ ) )
| / | / // |// | | ((____/ / / / // / /____ ((___/ / / /____ ((___/ /
"""
print(Hello)
return input('RightBack: ').encode()


def Fun(right):
if len(right) != 64:
print("XD")
exit()
back = b''

for i in range(0, len(right), 8):
part1 = struct.unpack('>I', right[i + 0:i + 4])[0]
part2 = struct.unpack('>I', right[i + 4:i + 8])[0]
if i != 0:
part1 ^= struct.unpack('>I', back[i - 8:i - 4])[0]
part2 ^= struct.unpack('>I', back[i - 4:i])[0]
VM(part1, part2)
back += struct.pack(">I", REG['EAX'])
back += struct.pack(">I", REG['EBX'])

return back


if __name__ == "__main__":
REG = {}
EIP = 0
reg_table = {'1': 'EAX', '2': 'EBX', '3': 'ECX', '4': 'EDX', '5': 'R8', '6': 'CNT', '7': 'EIP'}
Sbox = [0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d]
Rcon = [0x01000000, 0x02000000, 0x04000000, 0x08000000, 0x10000000, 0x20000000, 0x40000000, 0x80000000, 0x1b000000, 0x36000000]
s = []


key = "CalmDownBelieveU"
p1(s, key)

key = [61, 15, 58, 65, 177, 180, 182, 248, 192, 143, 37, 238, 50, 29, 215, 190]
key = bytes(p3(s, key))

extendKey = p2(bytes(key))

opcode = [69, 136, 121, 24, 179, 67, 209, 20, 27, 169, 205, 146, 212, 160, 124, 49, 20, 155, 157, 253, 52, 71, 174, 164, 134, 60, 184, 203, 131, 210, 57, 151, 77, 241, 61, 6, 13, 52, 235, 37, 100, 178, 8, 238, 205, 27, 194, 159, 230, 165, 211, 221, 100, 217, 111, 202, 185, 207, 226, 50, 88, 4, 58, 73, 10, 92, 24, 230, 246, 245, 21, 110, 182, 151, 85, 28, 181, 191, 185, 236, 92, 98, 222, 85, 228, 14, 235, 93, 77, 161, 61, 140, 222, 74, 124, 13, 211, 75, 134, 235, 164, 228, 235, 16, 29, 41, 49, 105, 188, 51, 232, 65, 209, 165, 35, 182, 248, 245, 69, 18, 152, 71, 223, 85, 114]
opcode = p3(s, opcode)

right = Have()
back = Fun(right)

data1 = [228, 244, 207, 251, 194, 124, 252, 61, 198, 145, 97, 98, 89, 25, 92, 208, 155, 38, 34, 225, 98, 206, 234, 245, 223, 54, 214, 137, 35, 86, 180, 66, 223, 234, 90, 136, 5, 189, 166, 117, 111, 222, 39, 156, 163, 173, 36, 174, 47, 144, 15, 160, 45, 239, 211, 11, 190, 181, 24, 164, 234, 114, 174, 27]
data1 = bytes(p3(s, data1))

data2 = [165, 83, 203, 51, 99, 164, 30, 91, 230, 64, 181, 55, 190, 47, 125, 240, 186, 173, 116, 47, 89, 64, 68, 215, 124, 138, 34, 175, 60, 136, 77, 216, 250, 127, 14, 14, 66, 168, 198, 247, 252, 189, 243, 239, 25, 63, 143, 7, 177, 13, 99, 226, 100, 6, 207, 77, 46, 136, 251, 123, 225, 27, 76, 183]
data2 = bytes(p3(s, data2))

data3 = [95, 219, 46, 178, 111, 141, 17, 168, 254, 60, 68, 59, 41, 183, 182, 118, 3, 47, 150, 240, 140, 159, 110, 238]
data3 = bytes(p3(s, data3))

if back == data2:
print(bytes(data1).decode())
else:
print(bytes(data3).decode())

0x02 Decryption

那么在解混淆后来审计python代码就比较容易了,所有字符串都经过了RC4加密,不过恢复了源码就可以直接打印出来看字符串或是所需要的数据,稍微审计则可发现程序只经过一个 Have 函数的加密,而这里面一个 VM 加密

image-20230726161233485

而有源码所有的中间数据都可以调试获取,有了正确的opcode,写出一个解释器即可得知程序对输入进行了什么样的加密,当然加密不是很长直接手动分析也可,于是可以得知加密过程如下

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
init
初始化寄存器

mov ecx, 0 ; 0x50, 3, 3, 0
add eax, key ecx eax += key0 ; 0x1D, 1, 1, 3
mov ecx, 1 ; 0x50, 3, 3, 1
add ebx, key ecx ebx += key1 ; 0x1D, 1, 2, 3



add cnt, 1 循环开始 ; 0x1D, 3, 6, 1
xor eax, ebx A^B ; 0x71, 1, 2
mov ecx, eax ecx = A ^ B ; 0x50, 2, 3, 1
mov r8, ebx r8 = B ; 0x50, 2, 5, 2
and ebx, 0x1F B & 0x1F ; 0x72, 2, 0x1F
shl eax, ebx eax = (A^B) << (B & 0x1F) ; 0x29, 1, 2
mov edx, 32 ; 0x50, 3, 4, 32
sub edx, ebx 32 - (B & 0x1F) ; 0x96, 2, 4, 2
shr ecx, edx ecx = (A^B) >> (32 - (B & 0x1F)) ; 0x74, 3, 4
or eax, ecx A = (A^B) << (B & 0x1F) | (A^B) >> (32 - (B & 0x1F)) ; 0x57, 1, 3
mov ebx, cnt ; 0x50, 2, 2, 6
mul ebx, 2 ; 0xDC, 3, 2, 2
mov ecx, key ebx ; 0x50, 1, 3, 2
add eax, ecx A += roundkey[2 * i] ; 0x1D, 2, 1, 3


mov ebx, r8 ; 0x50, 2, 2, 5
xor ebx, eax B ^ A ; 0x71, 2, 1
mov ecx, ebx ecx = B ^ A ; 0x50, 2, 3, 2
mov edx, eax ; 0x50, 2, 4, 1
and edx, 0x1F A & 0x1F ; 0x72, 4, 0x1F
shl ebx, edx ebx = (B ^ A) << (A & 0x1F) ; 0x29, 2, 4
mov r8, 32 ; 0x50, 3, 5, 32
sub r8, edx r8 = 32 - (A & 0x1F) ; 0x96, 2, 5, 4
shr ecx, r8 ; 0x74, 3, 5
or ebx, ecx ebx = (B^A) << (A & 0x1F) | (B^A) >> (32 - (A & 0x1F)) ; 0x57, 2, 3
mov ecx, cnt ; 0x50, 2, 3, 6
mul ecx, 2 ; 0xDC, 3, 3, 2
add ecx, 1 ; 0x1D, 3, 3, 1
mov edx, key ecx ; 0x50, 1, 4, 3
add ebx, edx B += roundkey[2 * i + 1] ; 0x1D, 2, 2, 4

cmp cnt, 21 ; 0x7
jnz add cnt, 1
exit 0xFF

0x03 GetFlag

还原成C语言代码不难发现这是个RC5加密,唯一修改的地方就是轮数改成了21轮,所需要的key在原程序中可以直接打印获得,于是搓出脚本

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
#include <stdio.h>
#include <stdint.h>

#define WORD_SIZE 32
#define KEY_SIZE 16
#define NUM_ROUNDS 21


void RC5_Decrypt(uint32_t *ct, uint32_t *pt, uint32_t *roundKey) {
uint32_t i;
uint32_t B = ct[1];
uint32_t A = ct[0];

for (i = NUM_ROUNDS; i >= 1; i--) {
B -= roundKey[2 * i + 1];
B = (B << (WORD_SIZE - (A & (WORD_SIZE - 1)))) | (B >> (A & (WORD_SIZE - 1)));
B ^= A;
A -= roundKey[2 * i];
A = (A << (WORD_SIZE - (B & (WORD_SIZE - 1)))) | (A >> (B & (WORD_SIZE - 1)));
A ^= B;
}

pt[1] = B - roundKey[1];
pt[0] = A - roundKey[0];
}


int main() {
uint32_t ciphertext[] = {0x43af236, 0x56b19afc, 0xf71e21dc, 0xdb8f8e94, 0x4d34e79d, 0x9c520c6e, 0xfbfad5fd, 0x32f9782c, 0xbbbe39c1, 0xd98575b6, 0x28f8cc78, 0xa4e48592, 0xebd72c5, 0xaf87912a, 0x8bf1ef96, 0x1660d112};
uint32_t roundKey[] = {1835819331, 1853321028, 1768711490, 1432712805, 2177920767, 4020699579, 2261476601, 3551400604, 711874531, 3318306392, 1124217505, 2427199549, 3099853672, 2098025776, 1041196945, 2929936300, 246748610, 1941455090, 1303848803, 3809763535, 1395557789, 546751855, 1830937100, 2385871555, 2516030638, 3043054017, 3628118989, 1450520846, 1825094265, 3651791800, 32069749, 1469868411, 919887482, 4017993154, 4002737591, 3104343244, 4134211933, 420914335, 4152510760, 1317719524, 1990496755, 1873950060, 2553314372, 3602559392};
int i, j;
uint32_t flag[16] = { 0 };

for ( i = 0; i < 8; i++ )
{
RC5_Decrypt(ciphertext + 2 * i, flag + 2 * i, roundKey);
if (i != 0)
{
flag[i * 2] ^= ciphertext[2 * i - 2];
flag[i * 2 + 1] ^= ciphertext[2 * i - 1];
}
for ( j = 3; j >= 0; j-- )
printf("%c", (flag[i * 2] >> (j * 8)) & 0xFF);
for ( j = 3; j >= 0; j-- )
printf("%c", (flag[i * 2 + 1] >> (j * 8)) & 0xFF);
}


return 0;
}

Get Flag!

image-20230726161738691

Gohunt

0x00 Daily Shell Check

无壳64位

image-20230827171838072

0x01 Main

拖进 IDA,打开主函数发现是个 go 题,没有去符号,然而 main 函数有两千多行,所以要配合动调来解题

首先是字符串都经过了加密,通过这个base64动态解密,注意的是base64是换了码表,不过直接点进去取即可

image-20230827172035193

随后我们的输入进入了 xxtea 的加密,密钥也是动态解密,直接自己解密即可得到密钥

image-20230827172128136

可以发现 xxtea 非常长,而且字符串的提示可以确定是导入的 xxtea 的包到底这么长,并不是出题人写了这么长,赛后可以参考源码其实就一句话

1
encrypted := xxtea.Encrypt([]byte(flag), []byte(key))

随后对我们的 input 按 x 交叉引用追踪到 xxtea 结束的位置进行了异或的操作,该异或的密钥也可以直接解密即可

image-20230827172418207

随后把我们的输入转成了大数,进行的操作转成了 base 系列的字符串,我们通过附件扫描到的二维码得到的数据也是类似的,不难猜测这就是 base58 限定的字符集,然而直接解密发现不对,我们可以通过动调或者查找字符串的方式拿到修改后的码表

image-20230827172813359

随后的操作就是把密文生成二维码了

0x02 Get Flag

那么我们只需要 base58,xor,xxtea 即可拿到 flag

1
2
3
4
5
6
7
8
9
10
11
12
import base58
import xxtea

flag = base58.b58decode('YMQHsYFQu7kkTqu3Xmt1ruYUDLU8uaMoPpsfjqYF4TQMMKtw5KF7cpWrkWpk3', alphabet=b'nY7TwcE41bzWvMQZXa8fyeprJoBdmhsu9DqVgxRPtFLKN65UH2CikG3SAj')
flag = bytearray(flag)

xor_key = b'NPWrpd1CEJH2QcJ3'
for i in range(len(flag)):
flag[i] ^= xor_key[i % len(xor_key)]
xxtea_key = b'FMT2ZCEHS6pcfD2R'
flag = xxtea.decrypt(flag, xxtea_key, padding=False)
print(flag)

Get Flag!

image-20230827173136742

EzIOS

0x00 Daily Shell Check

IOS逆向,和安卓一样是个zip文件,直接解包分析与目录同名的文件

image-20230827194549888

0x01 Trace

由于没有IOS手机,只能嗯审 + 模拟执行,不过先要找到主要的代码逻辑,通过搜索字符串找到按钮相应的处理函数

image-20230827194722115

然而几乎每个函数都上了 ollvm 混淆,用 D810 会稍微好一点,然而还有字符串混淆,一般是采用运行起来再 patch 回去,现在跑不起来只能通过模拟执行来恢复,或者修改 data const 段的写权限去掉,会恢复一些字符串,xmmm类型的恢复不了

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

This post is written by P.Z, licensed under CC BY-NC 4.0.