收获

  • 使用 ESP 定律法进行脱壳

  • 使用 int __cdecl _filbuf(FILE *) 函数实现 get 输入


【攻防世界】BabyXor


思路一

用 exeinfo PE 打开:

攻防世界_BabyXor1.png

貌似有壳,提示用 DIE v3.x 查看,但是依然检测不出来

攻防世界_BabyXor2.png

用 IDA 打开,发现无法反汇编:

攻防世界_BabyXor3.png

攻防世界_BabyXor4.png

因为加了壳,IDA 中什么都看不到

先用 OllyDBG 打开调试:

攻防世界_BabyXor5.png

首先看到 pushad
pushad 是将所有的寄存器压栈,一般是开始位置
在地址 0x0043F01E 之后,有很多 add byte ptr ds:[eax], al 的操作,无法直接看到正常的汇编代码

但是在地址 0x0043F0120x0043F016 之间可以看到一个循环操作:

0043F012    8033 23         xor byte ptr ds:[ebx],0x23
0043F015    43              inc ebx
0043F016  ^ E0 FA           loopdne short babyXor.0043F012

这里使用循环 xor 来修正代码,所以导致 IDA 无法正常解析

pushad 开始

先 F8 单步步过一次

攻防世界_BabyXor6.png

观察右侧寄存器窗口,发现 EAX ~ EDI 中只有 ESP 为红色,说明可以使用 ESP 定律进行脱壳

在寄存器窗口中选中 ESP,右键 --> 数据窗口中跟随

攻防世界_BabyXor7.png

注意数据窗口中是否跳转:

攻防世界_BabyXor8.png

从该地址处的第一个字节开始(我这里是 00),左键选择任意长度的数据

然后右键 --> 断点 --> 硬件访问 --> Byte/Word/Dword(三选一,均可)

攻防世界_BabyXor9.png

检查一下断点是否成功:调试 --> 硬件断点

攻防世界_BabyXor10.png

直接 F9 运行程序
然后 F8 连续单步步过找到 OEP(程序的入口点)
程序停在地址 0x0043F019 的位置

攻防世界_BabyXor11.png

在脱壳之前,先删除前面下的断点:

攻防世界_BabyXor12.png

在停下的地址处:右键 --> 用 OllyDump脱壳调试进程

攻防世界_BabyXor13.png

点击脱壳,并将脱壳后的程序进行保存

攻防世界_BabyXor14.png

将保存后的程序用 exeinfo PE 打开:

攻防世界_BabyXor15.png

已经显示无壳

用 IDA 打开:

攻防世界_BabyXor16.png

已经可以被 IDA 正常分析了,脱壳成功

进入主函数

攻防世界_BabyXor17.png

开始的两句作用是输出:”世界上最简单的Xor”
注意到后面有一个 if else 语句:

攻防世界_BabyXor18.png

这个不是很懂,但是在网上看到了比较好的解释:C语言学习趣事_关于C语言中的输入输出流_续一 - volcanol

这段代码实现的是 getc() 函数,即:获取用户的输入
其实根据运行程序时的输出,也大致可以猜到,不影响做题

getc()

在 VC 6.0 中有两个 get() 的定义, 一个是宏,一个是函数

  1. 宏的定义如下:
    #define getc(_stream) (--(_stream)->_cnt >= 0 ? 0xff & *(_stream)->_ptr++ : _filbuf(_stream))
  2. 函数定义如下:
    _CRTIMP int __cdecl getc(FILE *)

在C语言的各家编译器提供厂商里面有一个不成为的“潜规则”,那就是:
如果一个标识符前面是以下划线开头,这样的标识符通常是编译器预定义的宏,或者预定义的标志符

我们看宏定义,这里用到的宏实际还用到了一个预定义的函数:
_CRTIMP int __cdecl _filbuf(FILE *)

从这个函数可以看出在 getc() 宏中使用的:_stream 是一个具有文件指针类型性质的预定义标识符

在 IDA 伪代码中,_filbuf(&File) 的定义:

攻防世界_BabyXor19.png

继续往下:
v8 = sub_40108C(&unk_435DC0, 56) 函数会执行 sub_401190(a1, a2)

攻防世界_BabyXor20.png

内容就是简单的移位、异或操作,最后将结果返回给 v8
Src = sub_401041(&unk_435DC0, &dword_435DF8, 0x38u) 函数会执行 sub_401240(a1, a2, Size)

攻防世界_BabyXor21.png

操作也是移位、异或,将结果返回给 Src
v5 = sub_4010C3(&unk_435DC0, Src, &dword_435E30, 56) 函数会执行 sub_401320(a1, a2, a3, a4)

攻防世界_BabyXor22.png

跟前面都是差不多的,也是移位、异或,最后将结果返回给 v5
最后执行 sub_40101E(v8, Src, v5)

攻防世界_BabyXor23.png

发现三个通过 for 循环的赋值操作
同时,三个参数都使用 sub_4010A5() 函数进行了处理,sub_4010A5() 函数会执行 sub_401460(a1)

跟进一下:

攻防世界_BabyXor24.png

这里的 i 是一个指针,a1 也是一个指针

首先将 i 的初值设置为 a1 所指向的地址(其实就是参数 v8Srcv5 各自的首地址)
for 循环的结束条件就是将 a1 所指向的非 '\0' 元素全部遍历完,也就是 i 指向参数 v8Srcv5 各自的末尾
最后返回的 i - a1 是两个地址的差,差值其实就是字符串的长度

再结合三个 for 循环的内容,可知:
sub_40101E(v8, Src, v5) 函数的功能是将 a1( v8 )、a2( Src )、a3( v5 ) 的内容拼接到 v10 所指向的地址中

查看一下这三个移位、异或函数所使用的数据
unk_435DC0

攻防世界_BabyXor25.png

dword_435DF8

攻防世界_BabyXor26.png

dword_435E30

攻防世界_BabyXor27.png

通过 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脱壳调试进程 进行脱壳的时候

攻防世界_BabyXor29.png

左下角有两种方式:

我前面是选择的 方式 1,虽然成功脱壳了,可以 IDA 静态分析,但是却无法运行程序
后来选了 方式 2 试了一下,发现既可以 IDA 静态分析,也可以运行程序了

(脱壳的时候最好两种方式都试一试)


结果

flag{2378b077-7d6e-4564-bdca-7eec8eede9a2}

攻防世界_BabyXor28.png