逆向分析 —SMC

了解 SMC 代码自修改程序在逆向分析中的表示,并体验其分析过程。

初识程序

打开所给程序,发现里面什么都没有,在里面试着输入一些内容后,发现它返回 try again,看来只有想办法看到源代码了。

把这个程序拖入 IDA,发现可以找到 main 函数,且结构并不是很复杂,所以现在可以着手开始看了~

逐层分析并解密

第一层加密

从结构图来看,程序整体是分支 - 循环结构,且观察下边的分支可以大概分析出,我们的输入多半走了最左端的红线,输出了一个 Try again(但是因为没有 system("pause") 而直接退出了程序)。而想要继续分析下去,找到 flag,右边的循环部分是重点。来,看看循环吧。

输入的内容长度必须为 281Ch),才能进入到右边的绿线。

当最后一个字符为 7Dh(} 右花括号)时,进入右侧的循环。右侧的循环一共进行了 67 次(43h),每次都对 byte_414C3C[i] 中的内容异或了一个常数 0x7D。67 次循环结束后,程序 call 了一下 [ebp-6Ch] 中的内容,结合刚才的过程,[ebp-6Ch] 中的东西应该是一个函数,但是现在我们还不知道它是什么,我们需要写一个脚本来把隐藏的函数弄出来。

Hex-Rays 为快速修改二进制码提供了一个 API 接口,其 Python 接口称为 IDAPython,文档为 https://www.hex-rays.com/products/ida/support/idapython_docs/,我们只需要其中的 patch_bytes(address, buf) 即可。

IDA-Python 常用的 API

可以看到,idaapi.py 提供了这些常用函数,对于本次实验来说已经足够。

1
2
3
4
get_bytes(address,count) # 从address处读取count个字节的内容
patch_bytes(address,buf) # 将address地址处patch为buf的内容
Xrefsto(address,flags=0) # 找到所有引用了address的地址
byte(address) # 获取address地址的一个字节的内容
part_1
1
2
3
4
5
def xorize(start_loc, end_loc, num):
for addr in range(start_loc, end_loc):
patch_bytes(addr, get_bytes(addr) ^ num))
addr+=1
xorize(0x00414c3c, 0x00414c7f, 0x7d) # for part 1

可以观察到,执行脚本后,byte_414C3C 中的内容发生了改变,我们紧接着将其转换为汇编代码(按 C 键),可以看到.data 字段变红,然后,右键起始地址,选择 create function,将其反编译为函数,

现在这样就非常容易分析了,我们输入字符的前五位一定是 flag{,加上结尾的},已经解决了 28 个字符的 6 个,剩下的还需慢慢来。

第二层加密

我们留意到,下方有一个 do-while 循环,一共循环了 90 次,它的作用是将 a2 中的内容与 0x43 异或,想要解密,我们异或回去即可。留意到前面 main 函数调用的方式,可知这次利用到了 unk_414BE0 中的内容。在这一步结束后,对剩余的部分调用 a2 函数,我们现在不知道它是什么,必须先把它解出来。

继续写 payload,加载 payload。来到第三层加密。

part_2
1
xorize(0x00414be0, 0x00414c3a, 0x43) # for part 2, see previous for xorize() definition

第三层加密

终于来到了第三层函数,留意一下 sub_414C3C 中的 a2(a1 + 5, &unk_414A84);unk_414A84 其中的内容需要经过 347 次循环,每一次将一位内容与 0x55 异或,步骤类似前面。

pre_part_3
1
xorize(0x00414a84, 0x00414bdf, 0x55) # for part 3, see previous for xorize() definition

经过以上 Payload 得到以下这样:

注意到这里还有一层异或加密,先解开再说。由 414be0 中的 a2(a1 + 4, (const char *)&unk_414A30); 得知,是 414a30 中的东西被调用了,所以要把它解开,逐位异或 0x4d 83 次即可

part_3_smc
1
xorize(0x00414a30, 0x00414a84, 0x4d) # for part 3, see previous for xorize()

(截图中 v2 的值不太对,实际上是 - 1)

第三层的主要作用则是判断接下来的 4 个字符与 0xCC 异或后的结果要与一串硬编码的值相同(注意到 v5 = 0x93A9A498)。经过运算该值为 The_

第三层的解密脚本如下(注意逆序)

part_3
1
2
3
4
5
6
7
def part_3():
src=[0x98,0xa4,0xa9,0x93]
res=''
for num in src:
num^=0xcc
res+=chr(num)
print(res)

第四层加密

经过提示,sub_414A84 中进行的是 base64 运算,之后的几个字符串加密应该得到 cmVhbEN0Rl8=,随便找一个在线 base64 解密网站得到该字符串为 realCtF_

第五层加密

第五层,所得字符每一个加一,得到字符串 kvtu`C4h"o (s [5]=`),经脚本计算后得到字符串为 just_B3g!n

第五层的解密代码如下

last_part
1
2
3
4
5
def last_part():
str='''kvtu`C4h"o'''
ori_str=list(str)
new_str=[chr(ord(i)-1) for i in ori_str]
print(''.join(new_str))

所有的 payload 代码如下所示

all_payload.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from idaapi import *

def xorize(start_loc,end_loc,num):
for addr in range(start_loc,end_loc):
patch_byte(addr,get_byte(addr)^num)
addr+=1

def last_part():
str='''kvtu`C4h"o'''
ori_str=list(str)
new_str=[chr(ord(i)-1) for i in ori_str]
print(''.join(new_str))

if __name__=="__main__":
# may run separately
xorize(0x00414c3c,0x00414c7f,0x7d) # part 1
xorize(0x00414be0,0x00414c3a,0x43) # part 2
xorize(0x00414a84,0x00414bdf,0x55) # part 3
xorize(0x00414a30,0x00414a84,0x4d) # part 4
last_part() # last_part

快结束了!

所有的东西拼在一起为 flag{The_realCtF_just_B3g!n},此即为最终的 flag 目标。