攻防世界-crackme-wp(手动脱壳)

这里以攻防世界的一道简单的题目来记录一下手动脱壳相关内容,其实理解了原理之后也是非常简单的一件事。

首先看看攻防世界的这道crackme。

查壳

PEiD查一查,发现有壳,nSpack,并不是常见的upx,也懒得去找工具,这样一道简单的题目肯定不会用很复杂的壳,所以开始徒手脱壳。

crackme-1

找OEP

手动脱壳的第一步,就是要找到程序的OEP,就是真正的程序入口点。这个要从壳的原理说起,所谓的压缩壳、加密壳,作用都是要隐藏程序真正的入口点,在壳加载的过程中对数据进行解压和解密并放到相应的段,然后壳的作用就结束了,这个时候会从壳进入真正的程序,这个时候会有一个真正的程序入口,就是OEP,不管脱什么壳,首先都是要找到OEP。

首先OD打开调试(不管用什么调试都差不多,IDA也是可以的,并且也有相应的Dump脚本,过程上没什么太大的差别)

crackme-2

首先看到pushfdpushad两条命令,这是记录所有寄存器,这里我根据esp定律,实际上也就是堆栈平衡的原理,在执行完pushad之后对esp下断点

crackme-3

下面简单介绍一下esp定律

一般的加壳软件在执行时,首先要初始化,保存环境(保存各个寄存器的值),一般利用pushad(相当于把eax,ecx,edx,ebx,esp,ebp,esi,edi都压栈),当加壳程序的外壳执行完毕以后,再来恢复各个寄存器的内容,通常会用POPAD(相当与把刚刚保存的寄存器的值都还原),在脱壳的时候,我们可以根据堆栈平衡来对ESP进行下断,进而快速到达OEP

使用硬件读断点(hw esp),这里只经过了两次跳转,然后就来到了OEP附近,这里也需要注意,OEP处的esp并不一定是之前记录的值,但是一定很近了,这个时候需要耐心的寻找疑似的OEP。这里的OEP我找到实在0x401000

crackme-4

这里显然就是程序真正的入口。还有一点需要注意,壳加载过程中会加载代码,所以如果遇到没有解析的数据,就Ctrl+A重新分析代码。

Dump

接下来要把内存中的完整程序Dump出来,好像OD有相关的插件,但我还是喜欢LordPE,使用也很简单,就是注意右键进程,修正镜像大小,因为有人会改镜像的大小,Dump出来的程序会有问题,所以一般不去判断,直接先修正,再Dump

修正IAT

这个时候Dump出来的程序时没法运行的,因为缺少了很关键的一部分内容,就是IAT,也就是输入表,我们可以把现在Dump出来的程序用IDA打开,可以看到printfscanf之类的函数都识别不出来,这就是因为我们Dump出来的程序没有输入表。至于输入表是什么,偷懒从百度百科抄了过来,这是PE结构中很重要的一部分。

Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中。当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.其中导入地址表就指示函数实际地址。

IAT当然可以自己动手改Dump出来的程序的16进制,但是我还是喜欢用ImportREC(明明有工具为什么还要自己动手……还容易写错)。

crackme-5

使用起来很简单,首先要把我们知道的OEP地址输入进去,注意这里是偏移地址,然后可以选择IAT Autosearch选项,但有的时候可能会不准确,这个时候可以手动寻找,RVA就是相对虚拟地址,Size就是大小,手动找的过程也很简单,只要找到调用的函数的部分,比如说printf,就可以在汇编代码里看到调用的地址,比如说dword ptr [402094],这是个间接取址,这里取的是402094处指针指向的地址,这里才是真正的printf函数的入口,我们转到402094的位置,可以看到上下都有很多这样的指针,指向外部引用的函数,这里就是输入表

crackme-6

两个不同的dll中间用0隔开,所以找到开始地址和结束地址,就能知道RVA和Size。

这个时候点Get Imports,中间窗口会显示出来,但是注意有时候会出现无效的值,这个时候可能需要手动修复或者是删除,如果全都显示有效,就可以Fix Dump,选择刚刚Dump出来的文件,成功的话会生成一个新的文件,这个文件是可以执行的程序,到这里脱壳的过程就结束了。

解题

回到题目本身,IDA打开

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
signed int start()
{
signed int result; // eax
int v1; // eax
char Buf; // [esp+4h] [ebp-38h]
char Dst; // [esp+5h] [ebp-37h]

Buf = 0;
memset(&Dst, 0, 0x31u);
printf("Please Input Flag:");
gets_s(&Buf, 0x2Cu);
if ( strlen(&Buf) == 42 )
{
v1 = 0;
while ( (*(&Buf + v1) ^ byte_402130[v1 % 16]) == dword_402150[v1] )
{
if ( ++v1 >= 42 )
{
printf("right!\n");
goto LABEL_8;
}
}
printf("error!\n");
LABEL_8:
result = 0;
}
else
{
printf("error!\n");
result = -1;
}
return result;
}

发现程序的判断非常简单,可见这题的主要目的是为了考脱壳,把需要的数据导出之后,直接简单逆向输出flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>

using namespace std;

int main() {
int dword_402150[] = { 18, 4, 8, 20, 36, 92, 74, 61, 86, 10, 16, 103, 0, 65, 0, 1, 70, 90, 68, 66, 110, 12, 68, 114, 12, 13, 64, 62, 75, 95, 2, 1, 76, 94, 91, 23, 110, 12, 22, 104, 91, 18};
char aThisIsNotFlag[] =
"this_is_not_flag";
string flag;
for(int i =0;i<42;i++)
{
flag+=(dword_402150[i]^aThisIsNotFlag[i%16]);
}
cout<<flag<<endl;
return 0;
}

输出得到flag

1
flag{59b8ed8f-af22-11e7-bb4a-3cf862d1ee75}
攻防世界-re部分题解(一) 花指令

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×