忽略细节导致耗时较多的一道逆向题Babyalgo

0x00 前言

早闻Nu1L出了一本《从0到1:CTFer成长之路》

今天逛BUU的时候发现多了一栏N1BOOK

点开做一下,发现第二题竟然被卡住了,输出的flag后面有不可见字符而且后五六个字符似乎没啥含义

折腾挺大一会才发现原来是因为忽略了一个细节…

耗时比我想象中的多,就当吸取不仔细的教训吧…

0x01 分析

来到main函数,可以看到程序定义了一个数组

但是这里有一个导致我后期输出的flag一直有问题的细节

可以看到…前一个还是[rbp+var_2A],下一个就变成了[rbp+var_28],也就是说[rbp+var_29]不见了…

后期我还纳闷…明明flag要求长度45,但为什么这个数组只有44长度…然后自己在最后补了个0…

根据常识可知[rbp+var_29] = 0

继续分析,进行了一番变量、函数重命名以后得到如下反汇编结果

这是后来才知道的这个函数是RC4算法函数……

由于第一次见这个算法的时候没想起来这是RC4,我直接去看相关算法然后逆的

进去看看RC4函数

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall RC4(__int64 a1, __int64 input, __int64 a3)
{
__int64 v3; // ST08_8
char v5; // [rsp+20h] [rbp-110h]
unsigned __int64 v6; // [rsp+128h] [rbp-8h]

v3 = a3;
v6 = __readfsqword(0x28u);
sub_40067A((const char *)a1, (__int64)&v5);
sub_400753((__int64)&v5, (const char *)input, v3);
return 0LL;
}

根据sub_40067A((const char *)a1, (__int64)&v5);所传入的参数可以知道,这是一个与我们输入的flag无关的函数

具体分析了一下可以知道它的作用是根据main函数中的key去得到一个数组v5。

显然可以通过gdb调试(简便)或者自己写个算法(较麻烦,因为如果用python写,还要处理一些数据类型如unsigned int/unsigned __int8等的问题)得到这个数组v5。

得到的v5会传给sub_400753((__int64)&v5, (const char *)input, v3);函数。

其作用是生成校验数组v3,也就是a3,也就是RC4的第三个参数RC4output,然后去与main函数中生成的数组进行对比校验,具体操作是通过一个数与我们的输入进行异或。

分析一下,上一句中谈到的”一个数”,是与输入的flag无关的,即无论输入什么flag,只要满足flag.length == 45,在while循环中每次异或时循环次数相同时这个数都是固定不变的,那么可以将这些数(对应xor那句下断,它存在rdx中)提取出来作为一个数组,直接与main函数中的数组异或,便可得到flag。

0x02 思路

思路一

由于sub_40067A生成v5数组的过程是与输入无关的,那么可以通过gdb调试或者算法等方法生成v5,然后模拟sub_400753的逆过程生成flag。

思路二

根据上面的分析,可以将sub_400753中与input[i]异或的那个数提取出来作为数组,与main函数的数组异或即可生成flag。

思路三

后来才知道这个是RC4算法,有了密文(main数组即可转换为密文)和key(Nu1Lctf233),即可生成明文flag。

不过网上各个RC4算法好像不太一致,在线网站生成结果也不太一样……

最终找到了一个能用的算法。

0x03 解密脚本

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
# -*- coding: UTF-8 -*-

'''
根据IDA的显示结果,得到的L是这样的。
L = [0xc6,0x21,0xca,0xbf,0x51,0x43,0x37,0x31,
0x75,0xe4,0x8e,0xc0,0x54,0x6f,0x8f,0xee,
0xf8,0x5a,0xa2,0xc1,0xeb,0xa5,0x34,0x6d,
0x71,0x55,0x08,0x07,0xb2,0xa8,0x2f,0xf4,
0x51,0x8e,0x0c,0xcc,0x33,0x53,0x31,0x40,
0xd6,0xca,0xec,0xd4,0x00] #最后的0x00单纯是我为了凑够长度而补的...
很可惜这不对,原因在于
.text:00000000004009B1 mov [rbp+var_2D], 0CCh
.text:00000000004009B5 mov [rbp+var_2C], 33h
.text:00000000004009B9 mov [rbp+var_2B], 53h
.text:00000000004009BD mov [rbp+var_2A], 31h #注意这里, 下一行直接是var_28,少了var_29
.text:00000000004009C1 mov [rbp+var_28], 40h #也即默认[rbp+var_29] = 0,并且以下都有一位移位
.text:00000000004009C5 mov [rbp+var_27], 0D6h
.text:00000000004009C9 mov [rbp+var_26], 0CAh

通过gdb调试或者正确分析了以后可以知道真正的L是这样的
'''

L = [0xc6,0x21,0xca,0xbf,0x51,0x43,0x37,0x31,
0x75,0xe4,0x8e,0xc0,0x54,0x6f,0x8f,0xee,
0xf8,0x5a,0xa2,0xc1,0xeb,0xa5,0x34,0x6d,
0x71,0x55,0x08,0x07,0xb2,0xa8,0x2f,0xf4,
0x51,0x8e,0x0c,0xcc,0x33,0x53,0x31,0x00,
0x40,0xd6,0xca,0xec,0xd4,]


'''
思路1:似乎略微麻烦一些
gdb调试拿到sub_40067A的结果的第二个参数,即下方s
然后模拟执行sub_400753
其中异或过程为逆向过程
'''
s = '0x4e 0xc4 0xf7 0x68 0x09 0x26 0x08 0x0e 0x92 0x2d 0x9a 0x1a 0x64 0xb0 0x21 0xa4 0x0b 0x5d 0xa2 0x22 0x4a 0xd4 0x1b 0x7e 0xfb 0x76 0x80 0x55 0x32 0x27 0x4f 0x1d 0xf8 0x8e 0x88 0x0f 0x5a 0x69 0xe6 0xb4 0x2f 0x9c 0x8c 0x89 0x36 0x11 0x38 0x8d 0x25 0xe7 0xcd 0x42 0x87 0xdf 0x78 0x4c 0x5f 0x9f 0x99 0xa3 0xcf 0xa1 0xb2 0xd0 0x9b 0x47 0x6a 0xd1 0xbc 0x5e 0x85 0x13 0x59 0x1e 0xc6 0x2c 0x52 0xb6 0x4b 0x65 0x2b 0x40 0xaa 0xeb 0x96 0x75 0x41 0x49 0xf0 0x10 0x6d 0xf4 0x81 0xde 0x0c 0xab 0x0d 0x5b 0xc3 0xd2 0xe5 0x90 0xc0 0x66 0x29 0x01 0x94 0x44 0x48 0x20 0x57 0x98 0x61 0x83 0x8f 0x15 0xae 0x28 0xef 0xa8 0x39 0xd7 0xc5 0x2a 0xba 0xec 0x4d 0x93 0x79 0xa9 0xfc 0xe9 0xbb 0xbd 0xaf 0x45 0xe8 0xdd 0x0a 0xdc 0x04 0xc8 0xca 0xe2 0xd5 0xb9 0x03 0x00 0xad 0xa7 0x8a 0x6c 0x58 0xf5 0xee 0x30 0xff 0xbe 0x7f 0x19 0x56 0x54 0xc2 0xf3 0x72 0xbf 0x63 0x17 0xfa 0x9d 0x77 0xd3 0x91 0x14 0x35 0x7d 0x18 0xb7 0x23 0x8b 0xa0 0xda 0x05 0x7b 0xe1 0x7a 0xa6 0x62 0x71 0xcc 0xd8 0x6b 0x9e 0xf6 0x3f 0x1f 0xb3 0x2e 0x5c 0x12 0x1c 0x3a 0x6f 0x73 0xf1 0x67 0xcb 0xac 0x3e 0x07 0x74 0xc1 0x86 0x51 0xc7 0x82 0xce 0x43 0xe4 0xe3 0xed 0x53 0x33 0x16 0xb5 0x06 0x37 0xdb 0xc9 0xf2 0x84 0x50 0x95 0xa5 0xd6 0x46 0xb8 0xfd 0x70 0x60 0x24 0x97 0x6e 0xb1 0x3c 0x02 0xea 0x3b 0x34 0xfe 0xd9 0xf9 0x7c 0xe0 0x3d 0x31'
Li = s.split('\t')
v5 = []
for i in Li:
v5.append(int(i,16))

v6 = 0
v7 = 0
v8 = 0
flag = ''

#这是模拟sub_400753,其中异或为逆向过程
for i in range(45):
v6 = ((((((v6+1)>>31)&0xffffffff)>>24) + v6 + 1)&0xff) - ((((v6+1)>>31)&0xffffffff)>>24)
v3 = (((v7+(v5[v6]&0xff))>>31)&0xffffffff)>>24
v7 = (v3+v7 + v5[v6])&0xff - v3
v5[v6],v5[v7] = v5[v7],v5[v6]
tmp = v5[((v5[v6] + v5[v7])&0xff)]&0xff
flag += chr(L[i]^tmp)

print flag


'''
思路2:
由于不论输入的flag是什么,只要满足输入长度=45,在sub_400753中input[i] ^ 某个数中这个某个数都是不变的
固可以通过gdb调试,下断,依次读取其值(存在edx中),就能得到一个数组,然后去与我们的L进行异或
即可得到flag
得到的数组如下:
这个方法算是思路1的进阶版吧,虽然也有点麻烦,但是对于应对一些算法比较晦涩难懂的情况还是很好用的
(当然这个题算法....emmmm做完才知道是RC4....可以直接解密)
'''
ano = [0xa8,0x10,0xa8,0xd0,0x3e,0x28,0x4c,0x44,
0x06,0xd5,0xe0,0x87,0x0b,0x09,0xbc,0x8f,
0x8c,0x2f,0xd0,0xf2,0x98,0xfa,0x03,0x02,
0x2e,0x31,0x6d,0x30,0xd7,0xda,0x42,0xc5,
0x3f,0xbd,0x53,0xf8,0x5f,0x34,0x01,0x72,
0x29,0xe1,0xa2,0x81,0xa9]
flag = ''
for i in range(len(ano)):
flag += chr(ano[i]^L[i])
print flag

'''
思路3:
直接利用RC4解密
'''
from Crypto.Cipher import ARC4 as rc4cipher
import base64

def rc4_algorithm(encrypt_or_decrypt, data, key1):
if encrypt_or_decrypt == "encrypt":
key = bytes(key1)
enc = rc4cipher.new(key)
res = enc.encrypt(data.encode('utf-8'))
res=base64.b64encode(res)
res = str(res)
return res
elif encrypt_or_decrypt == "decrypt":
data = base64.b64decode(data)
key = bytes(key1)
enc = rc4cipher.new(key)
res = enc.decrypt(data)
res = str(res)
return res



key = 'Nu1Lctf233'
text = ''
for i in L:
text += chr(i)
res = base64.b64encode(text)
print rc4_algorithm('decrypt', res, key)

0x04 总结

细节很重要。

熟记各种常见算法会对逆向很有帮助。