强网杯qwb 2018 reverse writeup

强网杯2018题目:《simple》《picture lock》《hide》、《babyre》、《re》writeup。

文件链接:https://github.com/LeadroyaL/attachment_repo/tree/master/qwb_2018

一、simple

一个安卓题目,简单题,java 层做了一些数学运算,总结一下就是一元二次方程,我懒得解,反正128种可能,直接爆破就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = [0, 146527998, 205327308, 94243885, 138810487, 408218567, 77866117, 71548549, 563255818, 559010506, 449018203, 576200653, 307283021, 467607947, 314806739, 341420795, 341420795, 469998524, 417733494, 342206934, 392460324, 382290309, 185532945, 364788505, 210058699, 198137551, 360748557, 440064477, 319861317, 676258995, 389214123, 829768461, 534844356, 427514172, 864054312]
b = [13710, 46393, 49151, 36900, 59564, 35883, 3517, 52957, 1509, 61207, 63274, 27694, 20932, 37997, 22069, 8438, 33995, 53298, 16908, 30902, 64602, 64028, 29629, 26537, 12026, 31610, 48639, 19968, 45654, 51972, 64956, 45293, 64752, 37108]
c = [38129, 57355, 22538, 47767, 8940, 4975, 27050, 56102, 21796, 41174, 63445, 53454, 28762, 59215, 16407, 64340, 37644, 59896, 41276, 25896, 27501, 38944, 37039, 38213, 61842, 43497, 9221, 9879, 14436, 60468, 19926, 47198, 8406, 64666]
d = [0, -341994984, -370404060, -257581614, -494024809, -135267265, 54930974, -155841406, 540422378, -107286502, -128056922, 265261633, 275964257, 119059597, 202392013, 283676377, 126284124, -68971076, 261217574, 197555158, -12893337, -10293675, 93868075, 121661845, 167461231, 123220255, 221507, 258914772, 180963987, 107841171, 41609001, 276531381, 169983906, 276158562]
result = [0]
# a[i] == b[i] * bak_input[i] * bak_input[i] + c[i] * bak_input[i] + d[i]
# a[i + 1] == b[i] * bak_input[i + 1] * bak_input[i + 1] + c[i] * bak_input[i + 1] + d[i])

for i in range(34):
for j in range(127):
if a[i + 1] == b[i] * j * j + c[i] * j + d[i]:
result.append(j)
print result

flag = ""
for r in result:
flag += chr(r)
print flag
# flag{MAth_i&_GOOd_DON7_90V_7hInK?}

二、picture-lock

安卓题,和加密勒索软件的套路有点像,输入一个文件,输出其加密后的结果。目标是将某个加密后的文件解密出来,flag 就在里面。

java 层基本没东西,算一下签名的md5,将原本文件、加密后文件、md5带入 native。

没有init_array,没有JNI_OnLoad,直接看JNI 方法。

一进来先初始化了 AES 的 SBox,比较骚的地方在于他初始化了2组 AES 的 SBox,也就是相当于有2个 AES_Cipher,使用的 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
if ( new_fd )
{
old_file_buffer = (char *)malloc(0x100u);
newFile = (char *)old_fd;
bbb_1024 = malloc(0x100u);
for ( i = 0; ; ++i )
{
v26 = md5String[i & 0x1F];
nextChar = fread(old_file_buffer, 1u, md5String[i & 0x1F], (FILE *)newFile);
dataLen = nextChar;
if ( !nextChar )
goto done;
if ( nextChar <= 0xF )
{
v29 = &old_file_buffer[nextChar];
if ( 16 != (dataLen & 0xF) )
{
_aeabi_memset(v29, 16 - (dataLen & 0xF), 16 - (dataLen & 0xF));
v29 = &old_file_buffer[16 - (dataLen & 0xF) + dataLen];
}
newFile = (char *)old_fd;
dataLen = 16;
*v29 = 0;
}

然后开始读文件,每次读取 md5[i&0x1F] 个字节,如果长度小于16,就 PKCS5 到16字节。

1
2
3
left_or_right = (int **)&g_buf_0x180_p0x30;
if ( !(v26 & 1) )
left_or_right = &g_buf_0x180;

对读入的字节前16byte 进行 AES_ECB 加密,使用的 KEY 是第奇数次使用md5[0:16] ,第偶数次使用 md5[16:32]

1
2
3
4
5
6
7
8
9
10
11
12
13
if ( dataLen >= 0x11 )
{
kk = 16;
p_md5String_1 = md5String;
do
{
bbb_1024[kk] = old_file_buffer[kk] ^ p_md5String_1[kk % 32];
++kk;
}
while ( kk < dataLen );
}
if ( fwrite(bbb_1024, 1u, dataLen, new_fd) != dataLen )
break;

16字节以后的,plain[index] 逐位 xor上md5[index] 。之后将这些 byte 写到加密后的文件里。

写一点 testcase 验证一下我们的猜想,发现是正确的,下文是解密的 python 脚本。

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
from Crypto.Cipher import AES

md5 = "f8c49056e4ccf9a11e090eaf471f418d"
odd_key = "1e090eaf471f418d"
even_key = "f8c49056e4ccf9a1"

odd_cipher = AES.new(odd_key, AES.MODE_ECB)
even_cipher = AES.new(even_key, AES.MODE_ECB)

with open('/Users/leadroyal/CTF/2018/qwb/assets/flag.jpg.lock') as f:
data = f.read()

offset = 0
i = 0
output = ""
count = 0
while True:
count += 1
current = data[offset:offset + ord(md5[i])]
if current == '':
break
offset += ord(md5[i])
if ord(md5[i]) % 2 == 0:
left = even_cipher.decrypt(current[0:16])
output += left
else:
left = odd_cipher.decrypt(current[0:16])
output += left
for j in range(16, len(current)):
output += chr(ord(current[j]) ^ ord(md5[j % 32]))
i += 1
i %= 32


print len(data)
print len(output)

# print output.encode('hex')
with open('/tmp/flag.jpg', 'wb') as fd:
fd.write(output)

三、hide

这题偷鸡了,不会做,说是的 upx 的壳,但似乎做了一些修改,瞎 jb 做居然做出来了。

1、运行过程中尝试去 attach,发现已经被 trace了,那肯定是被反调试了。

2、直接运行和使用 gdb 运行结果不一致,调试情况下连输出都没有,直接 exit 掉了,所以肯定是被反调试了。

最开始比较害怕是多层 upx,因为调试时候看到很多次 mmap,比较害怕。反正不会做,不小心看到一个叫“在所有syscall 上下断点”,叫 catch syscall 。

既然是加壳的,肯定会有 mmap 、mprotect 这样的操作,于是就"catch syscall"、"c",这样一直按,一直按,大概按到五六十次时候,发现了一些 ptrace,管他呢,跳过再说。之后就到了要求输入 flag 的位置,开心,dump 一下这个内存块,ida 打开就可以看到逻辑了!

看起来非常舒服,检查了首尾,然后按照顺序交替调用了6次加密函数。

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
__int64 __usercall sub_C8CC0@<rax>(unsigned int *input@<rdi>)
{
__int64 result; // rax@7
unsigned int tmp_i32; // [rsp+18h] [rbp-48h]@3
unsigned int tmp_i64[2]; // [rsp+1Ch] [rbp-44h]@3
signed int i; // [rsp+24h] [rbp-3Ch]@1
signed int j; // [rsp+28h] [rbp-38h]@3
int keyPool[4]; // [rsp+40h] [rbp-20h]@1
__int64 v7; // [rsp+58h] [rbp-8h]@1

v7 = canary;
keyPool[0] = 1883844979;
keyPool[1] = 1165112144;
keyPool[2] = 2035430262;
keyPool[3] = 861484132;
for ( i = 0; i <= 1; ++i )
{
tmp_i32 = input[2 * i];
*(_QWORD *)tmp_i64 = input[2 * i + 1];
for ( j = 0; j <= 7; ++j )
{
tmp_i32 += (keyPool[(unsigned __int64)(tmp_i64[1] & 3)] + tmp_i64[1]) ^ (((tmp_i64[0] >> 5) ^ 16 * tmp_i64[0])
+ tmp_i64[0]);
tmp_i64[1] += 1735289196;
tmp_i64[0] += (keyPool[(unsigned __int64)((tmp_i64[1] >> 11) & 3)] + tmp_i64[1]) ^ (((tmp_i32 >> 5) ^ 16 * tmp_i32)
+ tmp_i32);
}
input[2 * i] = tmp_i32;
input[2 * i + 1] = tmp_i64[0];
}
result = canary ^ v7;
if ( canary != v7 )
result = ((__int64 (*)(void))loc_C8B9A)();
return result;
}

这个很像 tea 加密,是可逆的。

1
2
3
4
5
6
7
8
9
10
11
12
char *__usercall sub_C8E50@<rax>(char *a1@<rdi>)
{
char *result; // rax@3
signed int i; // [rsp+14h] [rbp-4h]@1

for ( i = 0; i <= 15; ++i )
{
result = &a1[i];
*result ^= i;
}
return result;
}

这个就是普通的 xor,也是可逆的。

写个 python 反一下

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
keyPool = [1883844979, 1165112144, 2035430262, 861484132, ]
array_car = [1735289196, 3470578392, 910900292, 2646189488, 86511388, 1821800584, 3557089780, 997411680]
target = [0x7f13b852, 0x1bf28c35, 0xd28663f4, 0x311e4f73]
# target = [0xc234e08, 0x4ce42924, 0xd28663f4, 0x311e4f73]
# target = [0x221d5a3e, 0xd9c589da, 0x141d0409, 0x41e88c85]


def de_xor(enc):
for _i in range(4):
current = enc[_i]
a = current & 0xFF
b = (current & 0xFF00) >> 8
c = (current & 0xFF0000) >> 16
d = (current & 0xFF000000) >> 24
a ^= (_i * 4 + 0)
b ^= (_i * 4 + 1)
c ^= (_i * 4 + 2)
d ^= (_i * 4 + 3)
enc[_i] = a (b << 8) (c << 16) (d << 24)
return enc


def encrypt(i32_para1, i32_para2):
foo = i32_para1
bar = i32_para2
car = 0
for _i in range(8):
tmp_a = keyPool[(car & 3)] + car
tmp_b = ((bar >> 5) ^ (bar << 4)) + bar
foo += tmp_a ^ tmp_b
foo &= 0xffffffff
car += 1735289196
car &= 0xffffffff
tmp_a = keyPool[((car >> 11) & 3)] + car
tmp_b = ((foo >> 5) ^ 16 * foo) + foo
bar += tmp_a ^ tmp_b
bar &= 0xffffffff
# print hex(foo), hex(bar), hex(car)
# array_car.append(car)
# print array_car
return foo, bar


def solver(enc_foo, enc_bar):
foo = enc_foo
bar = enc_bar
car = array_car[7]
for _i in range(8):
tmp_a = keyPool[((car >> 11) & 3)] + car
tmp_b = ((foo >> 5) ^ 16 * foo) + foo
bar -= tmp_a ^ tmp_b
bar = (bar + 0xffffffff + 1) & 0xffffffff
car -= 1735289196
car = (car + 0xffffffff + 1) & 0xffffffff
tmp_a = keyPool[(car & 3)] + car
tmp_b = ((bar >> 5) ^ (bar << 4)) + bar
foo -= tmp_a ^ tmp_b
foo = (foo + 0xffffffff + 1) & 0xffffffff
# print hex(foo), hex(bar), hex(car)

return foo, bar

target = de_xor(target)

print "====="
for t in target:
print hex(t)

for i in range(2):
target[i * 2], target[i * 2 + 1] = solver(target[i * 2], target[i * 2 + 1])

print "====="
for t in target:
print hex(t)

target = de_xor(target)

print "====="
for t in target:
print hex(t)

for i in range(2):
target[i * 2], target[i * 2 + 1] = solver(target[i * 2], target[i * 2 + 1])

print "====="
for t in target:
print hex(t)

target = de_xor(target)

print "====="
for t in target:
print hex(t)

for i in range(2):
target[i * 2], target[i * 2 + 1] = solver(target[i * 2], target[i * 2 + 1])

print "====="
for t in target:
print hex(t)

print "====="
for t in target:
print hex(t)[2:].decode('hex')[::-1]

# f1Nd_TH3HldeC0dE

四、baby_re

直接执行文件,输出"nope"。

代码里有大量没用的反调试代码,最后发现有个函数有用,而且有两个特征。

  • 输出"nope"是在这个函数里的
  • 这个函数有读文件的操作,打开了叫"nothing"的文件

于是手动创建"nothing"的文件,随便写点东西进去,再执行这个exe,发现确实被加密了,但最后的几个byte是完整的,看起来是16byte一组的ECB模式。

这时候直接set RIP到这个函数,发现功能没有出问题,确实其他代码是反调试代码,全都NOP掉就行了。

主要就是逆sub_140002B60吧,没什么好讲的,还是这个套路。

python如下

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
target = [0xb, 0xe8, 0xa3, 0xd6, 0xf7, 0x19, 0x19, 0x4c, 0x12, 0x42, 0x0, 0x54, 0x3d, 0x41, 0xbb, 0x16, 0xe5, 0x6a, 0x87, 0xec, 0xd0, 0xeb, 0xfa, 0x62, 0x3d, 0xce, 0x61, 0x1e, 0xe, 0xc9, 0x11, 0xed, 0x68, 0x74, 0x3f, 0x7d, ]
# target = [0x62, 0x3f, 0xc6, 0x1f, 0xca, 0x03, 0x0b, 0xae, 0xe2, 0x05, 0xf8, 0xf7, 0xe1, 0xe1, 0x81, 0x46]
plain = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, ]
keyPool = [0xface, 0xdead, 0xbabe, 0xd00d]
magic = 0x61c88647

before_array = [0, 2654435769, 1013904242, 3668340011, 2027808484, 387276957, 3041712726, 1401181199, 4055616968, 2415085441, 774553914, 3428989683, 1788458156, 147926629, 2802362398, 1161830871, 3816266640, 2175735113, 535203586, 3189639355, 1549107828, 4203543597, 2563012070, 922480543, 3576916312, 1936384785, 295853258, 2950289027, 1309757500, 3964193269, 2323661742, 683130215]
after_array = [2654435769, 1013904242, 3668340011, 2027808484, 387276957, 3041712726, 1401181199, 4055616968, 2415085441, 774553914, 3428989683, 1788458156, 147926629, 2802362398, 1161830871, 3816266640, 2175735113, 535203586, 3189639355, 1549107828, 4203543597, 2563012070, 922480543, 3576916312, 1936384785, 295853258, 2950289027, 1309757500, 3964193269, 2323661742, 683130215, 3337565984]

print len(before_array)
print len(after_array)

before = 0
after = 0
right = plain[0] (plain[1] << 8) (plain[2] << 16) (plain[3] << 24)
left = plain[0 + 4] (plain[1 + 4] << 8) (plain[2 + 4] << 16) (plain[3 + 4] << 24)

print hex(left), hex(right)
for i in range(0x20):
adder1 = (before + keyPool[after & 3]) ^ (left + (16 * left ^ (left >> 5)))
adder1 &= 0xffffffff
right += adder1
right &= 0xffffffff
before -= magic
before &= 0xffffffff
after = before
adder2 = (before + keyPool[(before >> 11) & 3]) ^ (right + (16 * right ^ (right >> 5)))
adder2 &= 0xffffffff
left += adder2
left &= 0xffffffff
print hex(left), hex(right), hex(adder1), hex(adder2), hex(before)
print hex(left), hex(right)

print "===================================="

target_left = 0xae0b03ca
target_right = 0x1fc63f62

for j in range(4):
target_right = target[0 + j * 8] (target[1 + j * 8] << 8) (target[2 + j * 8] << 16) (target[3 + j * 8] << 24)
target_left = target[0 + 4 + j * 8] (target[1 + 4 + j * 8] << 8) (target[2 + 4 + j * 8] << 16) (target[3 + 4 + j * 8] << 24)

for i in range(0x20):
before = after_array[0x20 - i - 1]
after = before_array[0x20 - i - 1]
sub1 = (before + keyPool[(before >> 11) & 3]) ^ (target_right + (16 * target_right ^ (target_right >> 5)))
sub1 &= 0xffffffff
target_left -= sub1
target_left &= 0xffffffff
before += magic
before &= 0xffffffff
sub2 = (before + keyPool[after & 3]) ^ (target_left + (16 * target_left ^ (target_left >> 5)))
sub2 &= 0xffffffff
target_right -= sub2
target_right &= 0xffffffff
print hex(target_left), hex(target_right), hex(sub2), hex(sub1)
print hex(target_left)[2:].decode('hex')[::-1], hex(target_right)[2:].decode('hex')[::-1]

# tf{t qwbc
# is_n his_
# hat_ ot_t
# _rig hard
# ht?}

# qwbctf{this_is_not_that_hard_right?}

五、re

这是一个固件逆向,找对工具是关键。

https://raw.githubusercontent.com/themadinventor/ida-xtensa/master/xtensa.py 这个可以让 ida 正确识别文件。

https://github.com/espressif/esp-idf 这个是编译器和反汇编器。

每个寄存器是32bit,函数调用时a2-a3 是返回值,a10-a13 是参数,还有各种各样奇奇怪怪的指令,没法调试,只能猜各个指令是干嘛的,非常浪费时间。

大概呢,就是顺序执行 qwb1/2/3/4 ,求和结果为0,也就是说每个的结果都是0。这几个函数里面长得基本一样,只是数字不一样,做一些浮点运算,最后取一个约等于(round 取整函数,一开始我脑抽了,以为是循环的函数,逆了好久)。

qwb1/2/3/4 是差不多的,只是某几个常量有点不一样,将输入的4个char拓展为double,按照一定次序,分别带入qwb5,返回的值乘以16,再相加。大致是下面这样:

qwb5(a) * 16 + qwb5(b) == X
qwb5(c) * 16 + qwb5(d) == Y

qwb5的逻辑是将0-9,a-z,A-z 转为0~36 ,所以a 和 b 其实就是 X 的十六进制表示。

比如qwb1里的 X=87,Y=113 。可以拿到8个数字,再拿到它们的hexString,拼起来就是 flag 了。

这里存在大小写多解的情况,所以有2个 flag “577155095533b964 ”,“577155095533B964 ”。