【攻防世界】BabyXor
收获
使用 ESP 定律法进行脱壳
使用
int __cdecl _filbuf(FILE *)函数实现get输入
思路一
用 exeinfo PE 打开:
貌似有壳,提示用 DIE v3.x 查看,但是依然检测不出来
用 IDA 打开,发现无法反汇编:
因为加了壳,IDA 中什么都看不到
先用 OllyDBG 打开调试:
首先看到 pushadpushad 是将所有的寄存器压栈,一般是开始位置
在地址 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):
内容就是简单的移位、异或操作,最后将结果返回给 v8Src = sub_401041(&unk_435DC0, &dword_435DF8, 0x38u) 函数会执行 sub_401240(a1, a2, Size):
操作也是移位、异或,将结果返回给 Srcv5 = 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}






































