【攻防世界】BabyXor
收获
使用 ESP 定律法进行脱壳
使用
int __cdecl _filbuf(FILE *)
函数实现get
输入
思路一
用 exeinfo PE 打开:
貌似有壳,提示用 DIE v3.x 查看,但是依然检测不出来
用 IDA 打开,发现无法反汇编:
因为加了壳,IDA 中什么都看不到
先用 OllyDBG 打开调试:
首先看到 pushad
pushad
是将所有的寄存器压栈,一般是开始位置
在地址 0x0043F01E
之后,有很多 add byte ptr ds:[eax], al
的操作,无法直接看到正常的汇编代码
但是在地址 0x0043F012
到 0x0043F016
之间可以看到一个循环操作:
0043F012 8033 23 xor byte ptr ds:[ebx],0x23
0043F015 43 inc ebx
0043F016 ^ E0 FA loopdne short babyXor.0043F012
这里使用循环 xor 来修正代码,所以导致 IDA 无法正常解析
从 pushad
开始
先 F8 单步步过一次:
观察右侧寄存器窗口,发现 EAX ~ EDI 中只有 ESP 为红色,说明可以使用 ESP 定律进行脱壳
在寄存器窗口中选中 ESP,右键 --> 数据窗口中跟随
注意数据窗口中是否跳转:
从该地址处的第一个字节开始(我这里是 00),左键选择任意长度的数据
然后右键 --> 断点 --> 硬件访问 --> Byte/Word/Dword
(三选一,均可)
检查一下断点是否成功:调试 --> 硬件断点
直接 F9
运行程序
然后 F8
连续单步步过找到 OEP(程序的入口点)
程序停在地址 0x0043F019
的位置
在脱壳之前,先删除前面下的断点:
在停下的地址处:右键 --> 用 OllyDump脱壳调试进程
点击脱壳,并将脱壳后的程序进行保存
将保存后的程序用 exeinfo PE 打开:
已经显示无壳
用 IDA 打开:
已经可以被 IDA 正常分析了,脱壳成功
进入主函数
开始的两句作用是输出:”世界上最简单的Xor”
注意到后面有一个 if else
语句:
这个不是很懂,但是在网上看到了比较好的解释:C语言学习趣事_关于C语言中的输入输出流_续一 - volcanol
这段代码实现的是 getc()
函数,即:获取用户的输入
其实根据运行程序时的输出,也大致可以猜到,不影响做题
getc()
在 VC 6.0 中有两个
get()
的定义, 一个是宏,一个是函数
- 宏的定义如下:
#define getc(_stream) (--(_stream)->_cnt >= 0 ? 0xff & *(_stream)->_ptr++ : _filbuf(_stream))
- 函数定义如下:
_CRTIMP int __cdecl getc(FILE *)
在C语言的各家编译器提供厂商里面有一个不成为的“潜规则”,那就是:
如果一个标识符前面是以下划线开头,这样的标识符通常是编译器预定义的宏,或者预定义的标志符我们看宏定义,这里用到的宏实际还用到了一个预定义的函数:
_CRTIMP int __cdecl _filbuf(FILE *)
从这个函数可以看出在
getc()
宏中使用的:_stream
是一个具有文件指针类型性质的预定义标识符
在 IDA 伪代码中,_filbuf(&File)
的定义:
继续往下:v8 = sub_40108C(&unk_435DC0, 56)
函数会执行 sub_401190(a1, a2)
:
内容就是简单的移位、异或操作,最后将结果返回给 v8
Src = sub_401041(&unk_435DC0, &dword_435DF8, 0x38u)
函数会执行 sub_401240(a1, a2, Size)
:
操作也是移位、异或,将结果返回给 Src
v5 = sub_4010C3(&unk_435DC0, Src, &dword_435E30, 56)
函数会执行 sub_401320(a1, a2, a3, a4)
:
跟前面都是差不多的,也是移位、异或,最后将结果返回给 v5
最后执行 sub_40101E(v8, Src, v5)
:
发现三个通过 for
循环的赋值操作
同时,三个参数都使用 sub_4010A5()
函数进行了处理,sub_4010A5()
函数会执行 sub_401460(a1)
跟进一下:
这里的 i
是一个指针,a1
也是一个指针
首先将 i
的初值设置为 a1
所指向的地址(其实就是参数 v8
、Src
、v5
各自的首地址)
for 循环的结束条件就是将 a1
所指向的非 '\0'
元素全部遍历完,也就是 i
指向参数 v8
、Src
、v5
各自的末尾
最后返回的 i - a1
是两个地址的差,差值其实就是字符串的长度
再结合三个 for 循环的内容,可知:sub_40101E(v8, Src, v5)
函数的功能是将 a1( v8
)、a2( Src
)、a3( v5
) 的内容拼接到 v10
所指向的地址中
查看一下这三个移位、异或函数所使用的数据unk_435DC0
:
dword_435DF8
:
dword_435E30
:
通过 IDA 生成 Python 列表:
unk_435DC0 = [0x66, 0x00, 0x00, 0x00, 0x6D, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x6B, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3B, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00]
dword_435DF8 = [0x37, 0x00, 0x00, 0x00, 0x6F, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x7A, 0x00, 0x00, 0x00]
dword_435E30 = [0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x1D, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00]
由于数据是小端序存放,每组中间间隔的 “0x00, 0x00, 0x00” 是高位
即:内存中 "0x1A, 0x00, 0x00, 0x00"
代表 "0x0000001A"
根据 a2 >> 2
也可知,56 >> 2 = 14
,每 4 个十六进制一组,共 56 / 4 = 14 组
所以导出的数据其实可以简化如下: (可以在 Pycharm 中使用 Ctrl + F 进行替换快速得到)
unk_435DC0 = [0x66, 0x6D, 0x63, 0x64, 0x7F, 0x37, 0x35, 0x30, 0x30, 0x6B, 0x3A, 0x3C, 0x3B, 0x20]
dword_435DF8 = [0x37, 0x6F, 0x38, 0x62, 0x36, 0x7C, 0x37, 0x33, 0x34, 0x76, 0x33, 0x62, 0x64, 0x7A]
dword_435E30 = [0x1A, 0x00, 0x00, 0x51, 0x05, 0x11, 0x54, 0x56, 0x55, 0x59, 0x1D, 0x09, 0x5D, 0x12]
按照程序逻辑,编写脚本,分别使用三个函数生成三个字符串,然后进行拼接
脚本
unk_435DC0 = [0x66, 0x6D, 0x63, 0x64, 0x7F, 0x37, 0x35, 0x30, 0x30, 0x6B, 0x3A, 0x3C, 0x3B, 0x20]
dword_435DF8 = [0x37, 0x6F, 0x38, 0x62, 0x36, 0x7C, 0x37, 0x33, 0x34, 0x76, 0x33, 0x62, 0x64, 0x7A]
dword_435E30 = [0x1A, 0x00, 0x00, 0x51, 0x05, 0x11, 0x54, 0x56, 0x55, 0x59, 0x1D, 0x09, 0x5D, 0x12]
v8 = ""
for i in range(0, 14):
v8 += chr(i ^ unk_435DC0[i])
print(v8)
Src = ""
Src += chr(dword_435DF8[0]) # 下面的循环是从第二个元素开始,不要忘了还有个没改变的第一个值
for j in range(1, 14):
Src += chr(unk_435DC0[j] ^ dword_435DF8[j] ^ unk_435DC0[j - 1])
print(Src)
Source = ""
for k in range(0, 13):
Source += chr(k ^ dword_435E30[k + 1] ^ ord(Src[k]))
Destination = ""
Destination = chr(dword_435DF8[0] ^ dword_435E30[0])
v5 = Destination + Source
print(v5)
flag = v8 + Src + v5
print(flag)
思路二
由于发现 flag 与程序输入无关,是由程序内部的数据运算得到的
并且 sub_40101E(v8, Src, v5)
函数中直接拼接得到了 flag,所以 flag 一定会出现在程序中,于是可以通过调试来观察 flag
用 OllyDBG 打开,定位到最后拼接 flag 的 sub_40101E(v8, Src, v5)
函数处
根据 call sub_40101E
的地址 0x00401712
处下断点,直接运行看堆栈数据就能得出 flag
但是前面我脱壳之后的程序只能在 IDA 中正常分析,却无法双击运行原因找到了,在
右键 --> 用 OllyDump脱壳调试进程
进行脱壳的时候左下角有两种方式:
我前面是选择的
方式 1
,虽然成功脱壳了,可以 IDA 静态分析,但是却无法运行程序
后来选了方式 2
试了一下,发现既可以 IDA 静态分析,也可以运行程序了(脱壳的时候最好两种方式都试一试)
结果
flag{2378b077-7d6e-4564-bdca-7eec8eede9a2}