大家好,其实是前几天写简历的时候写了自己能手动脱壳,(手动脱壳,从寻找OEP->dump->IAT修复,最后成功拆除)
然而在脱壳方面的文章,一直没时间写,因为本来打算写逆向C++的,但是c++的内容绝不是三几句话能包含的,我打算自己
整理好以后会一并发出来;还忘大家谅解; 由于前不久瑞星电话面试的时候问我是不是能手动脱壳,遇到不熟悉的壳怎么办,
答: OEP-》DUMP—》IAT修复,最后问了找OEP的方法,为了证明下自己能做这个事,所以打算写篇脱壳的文章;当然也是为
了学习。技术方面的东西,我是报着学习的态度面对的;因为我知道我还是只小菜; 嘿嘿。。。废话不说;开始:
1:PE文件的装载
我们知道,一个普通的PE文件存放在磁盘中,在你不点击它之前,它其实和你电脑里的一张图片一样,形象的说来,
它成了个摆设,当鼠标双击它之后,shell调用CreateProcess函数打开一个有效的windows可执行文件,并且创建了
一个内存区对象,为的是稍后将它映射到内存空间中;然后通过调用WIndows的内部函数NtCreateProcess函数,创建了一个
windows执行体对象,以运行该映像;而创建执行体对象涉及以下几步(由创建线程来完成的):
1) 建立EPROCESS块
2)创建初始的进程地址空间
3)初始化内核进程块(KPROCESS)
4)结束地址空间的创建过程
5)建立PEB
6)完成执行体对象的创建过程
由于,本文写的是脱壳,更多详细,请参考 Windows.Internals.Fourth.Edition
PE文件装载过程:(Undocumented Windows)
我们来看一下 loader 是如何解释 PE 文件,又是如何为执行准备内存 image 的。 loader 需要找到空闲的虚拟地址空间来将文件映射到内存。
loader 尝试着将 image 加载在 preferred base address。成功后,loader 将 sections 映射入内存。loader 扫描 section table,
用每一个 section 的 RVA 加上基地址算出 section 的加载地址,然后将 sections 加载在相应的地址上。页属性是根据 section 的特征要求设定的。
将 section 映射入内存后,若基地址不等于 preferred base address,则 loader 开始进行基址重定位。之后检查 import table 并加载所需的 DLLs。
加载 DLL 与加载可执行文件的过程一样——映射 sections,基址重定位,解析
imports等等。所有的 DLL 都加载了之后,就修改 IAT 使之指向实际的 imported 函数的地址。
成了! image 已准备好执行了。关于Load的文章,What Goes On Inside Windows 2000: Solving the Mysteries of the Loader 我放附件里,
由于时间爱你关系,只翻译了一小点,大家凑合看吧;
2 :壳以及壳的加载过程:
1)什么是壳?
我们可以把壳看成一个子程序,由它处理后的Pe文件在磁盘中一般是以加密后的形式存在的,有的壳还带有压缩功能,使得exe文件更加小巧,加壳在一定程度上可以防止破解者对程序文件的非法修改,同时可以防止程序被反编译;壳附加在原程序上通过Load载入内存后,却抢先于原程序执行,也就是在PE文件代码段执行之前抢先得到控制权; 然后在执行过程中对原PE文件加密,还原,还原后在把控制权交还给原程序;
2)壳的加载过程
a:保存入口参数,加壳程序初始化时保存各寄存器的值,其实对windows来说,在每个子程序执行之前,总要保存ebx,edi,esi,ebp寄存器的值,而ecx,edx的值是不固定的,不能在返回时应用。特别注意:从 Windows API 函数中返回后,eax,ecx,edx 中的值和调用前不一定相同。当函数返回时,返回值放在eax中。如果您应用程序中的函数提供给 Windows 调用时,也必须尊守这一点,即在函数入口处保存段寄存器和 ebx,esp,esi,edi 的值并在函数返回时恢复。如果不这样一来的话,您的应用程序很快会崩溃。而通常,我们都用pushad/popad pushfd/popfd指令保存和恢复现场环境,注意:上面说过,壳可以看成一子程序,它只是比没加壳的代码提前获得了控制权,所以基本在没一个壳的开头,总能看到这个指令:
00413000 > 60 pushad
b:处理多次进入
c:模拟PE加载器完成相应的功能,处理完后将控制权交还给原程序;将控制权交给程序原入口点就是大家熟悉的(OEP)了; 在这一步中,还包括对输入表的处理,重定位表的处理,等; 由于加密加密三书上写的很详细,所以不在重复;
关于壳的加载过程,网上有篇文章:
http://blog.chinaunix.net/u1/51827/showart_1757935.html 我转在我的blog上,有兴趣的可以看看;
3:手动脱壳三部曲
1) 查找程序的真正入口(oep)
2) 抓取内存映像文件(dump)
3) Pe文件重建
到这里,我们可以开始练手了,注意: 不要在不熟悉pe文件格式的情况下,就想着手动脱壳,至少不要在还没搞清楚导入表和导出表,重定位表的情况下就想着更进一步,如果这样,无疑,你是在自找苦吃;
我用的加密解密三的例子:RebPe.exe
好了废话不说,开始三部曲第一部: 寻找OEP
这一部分涉及几种下断的方法和原理,如果不清楚的,赶紧翻开加密解密三,第二章,看吧,最近在看深入浅出MFC,记住一句话:勿在浮沙上筑高台;
寻找OEP的几种方法:
要是你对壳很熟悉,你当然可以一条指令一条指令的来,一直跟踪到代码段,兄弟我除了佩服你的技术精湛之余,还有向你学习的冲动; 但是,我们还是来用用书上的剩下的一些方法看看;
1:内存二次访问断点找OEP
原理:外壳首先要将原来压缩的代码解压,并放到对应的区块上,处理完毕,将跳到代码段执行。这种方法的关键,要等到代码段解压完毕,再对代码段设置内存访问断点,而一般的壳,会依次对.code .data .rsrc区块进行解压处理,所以,可以现在非代码段上下内存访问断点,此时代码段已经解压,在对代码段设内存访问断点;操作如下:
a:)OD载入RebPe.exe 看到代码如下:
00413000 > 60 pushad
00413001 E8 C2000000 call 004130C8
00413006 2E:3001 xor byte ptr cs:[ecx], al
00413009 0000 add byte ptr [eax], al
0041300B 0000 add byte ptr [eax], al
0041300D 0000 add byte ptr [eax], al
0041300F 0000 add byte ptr [eax], al
00413011 003E add byte ptr [esi], bh
00413013 3001 xor byte ptr [ecx], al
00413015 002E add byte ptr [esi], ch
b:)Alt+M 打开内存窗口,对rdata设内存访问断点 : F2(,你也可以对其它的非代码段设断;)F9运行,暂停在:
00413145 A4 movs byte ptr es:[edi], byte ptr [esi>
00413146 B3 02 mov bl, 2
00413148 E8 6D000000 call 004131BA
0041314D ^ 73 F6 jnb short 00413145
在Alt+M ,对.Text设断 F2后,F9
003A0282 61 popad
003A0283 68 30114000 push 401130 ; OEP嘿嘿,看到了吧;
003A0288 C3 retn
003A0289 3011 xor byte ptr [ecx], dl
我们跟踪,会发现,指令可读性较差,不用担心: ctrl +a ,OD会帮你: 此时,来到如下;也就是我们Pe的真正入口处了:
00401130 /. 55 push ebp
00401131 |. 8BEC mov ebp, esp
00401133 |. 6A FF push -1
00401135 |. 68 B8504000 push 004050B8
0040113A |. 68 FC1D4000 push 00401DFC ; SE 处理程序安装
0040113F |. 64:A1 0000000>mov eax, dword ptr fs:[0]
00401145 |. 50 push eax
00401146 |. 64:8925 00000>mov dword ptr fs:[0], esp
0040114D |. 83EC 58 sub esp, 58
00401150 |. 53 push ebx
00401151 |. 56 push esi
00401152 |. 57 push edi
00401153 |. 8965 E8 mov dword ptr [ebp-18], esp
00401156 |. FF15 28504000 call dword ptr [405028] ; kernel32.GetVersion ;这个函数熟悉吧
0040115C |. 33D2 xor edx, edx
随便用PE查看器,看下入口点:00400000
00401130 – 00400000 = 1130 (入口RVA)
2:堆栈平衡原理找oep
我们说过,在windows中调用子程序前,必须保存现场环境,而当子程序调用之前,必须恢复现场; 这一部分,可能你要,至少要对堆栈有个基本的了解,你可以参考我前面写的逆向C++中,函数,那一节,有一小部分介绍;而要继续深入,推荐arhat的 the shellcode handbook 一书。
我们知道,PUSHAD(Push All 32-bit General Registers)
指令格式:PUSHAD ;80386+
其功能是把寄存器EAX、ECX、EDX、EBX、ESP、EBP、ESI和EDI等压栈。
POPAD(Pop All 32-bit General Registers)
指令格式:POPAD ;80386+
其功能是依次把寄存器EDI、ESI、EBP、ESP、EBX、EDX、ECX和EAX等弹出栈,它与PUSHAD对称使用即可。
我们看看堆栈的变化:
Popad未执行前:
0012FFC4 7C817067 返回到 kernel32.7C817067 ; 当前esp指向
0012FFC8 0012BBC4
0012FFCC 73FB49E4
0012FFD0 7FFD8000
0012FFD4 8054C6B8
执行后:
0012FFA4 0012BBC4 ;edi
0012FFA8 73FB49E4 ;esi
0012FFAC 0012FFF0 ;ebp
0012FFB0 0012FFC4 ;esp
0012FFB4 7FFD8000 ;ebx
0012FFB8 7C92E4F4 ntdll.KiFastSystemCallRet ;edx
0012FFBC 0012FFB0 ;ecx
0012FFC0 00000000 ;eax
0012FFC4 7C817067 返回到 kernel32.7C817067
对0012FFA4下硬件断点: hr 0012FFA4
F9 ,程序中断在:
003A0282 61 popad
003A0283 68 30114000 push 401130 ;熟悉吧,OEP
003A0288 C3 retn
其实堆栈平衡原理,是找第一个pushad 配对的popad,因为,在很多壳中,可能在处理数据的时候,还会调用其它子程序,这样,如果用观察法找配对 pushad 和popad
指令就会让初学者,眼花缭乱,相信我,我有过这种经历;所以,用一个硬件断点,方便了很多吧;
脱壳二部曲: dump 抓取内存映像
如果用OD里的dump插件,哈哈,那么IAT你都不用修复拉; OD真的很好用,偶喜欢;好期待有天我也能弄个这NX的插件出来,奉献给大家;不过,有什么用呢; 大牛们都写好了摆好了; 你会发现,脱壳后运行的程序,运行的非常好;
在这里,有一点补充下,有可能你加壳后,会发现文件执行的时候有错误;kanxue补充如下:
将记事本和计算器 中Directory Table 中的LoadConfig值清零,即可加。外壳程序处理时没有考虑LoadConfig。
下面用loadPE来: 右键—》完全转存; dump.Exe 双击,55555.。。。 不能用了吧;
笑不出来了吧;没关系,今天宿舍就停电了,本来打算连IAT修复也写完的,555.。。。我不想在宿舍住拉,每次干得兴起就断电; 那就明天在来吧;今天先到这;
IAT修复:
1:)先来回顾下基础知识:
首先,PE文件中的数据按照装入内存后的页面属性被分成多个节,并由节表中的数据来描述这些节;一个节中的数据仅仅是属性相同而已,并不一定是同一种用途;
其次,由于不同用途的数据可能被放在同一个节中,仅仅靠节表是无法确定它的存放的位置的,PE文件中依靠可选头中的数据目录表来指出它们的位置;结构如下:typedef struct _IMAGE_OPTIONAL_HEADER {
//
//标准域
//
USHORT Magic; //魔数
UCHAR MajorLinkerVersion; //链接器主版本号
UCHAR MinorLinkerVersion; //链接器小版本号
ULONG SizeOfCode; //代码大小
ULONG SizeOfInitializedData; //已初始化数据大小
ULONG SizeOfUninitializedData; //未初始化数据大小
ULONG AddressOfEntryPoint; //入口点地址
ULONG BaseOfCode; //代码基址
ULONG BaseOfData; //数据基址
//
//NT增加的域
//
ULONG ImageBase; //映像文件基址
ULONG SectionAlignment; //节对齐
ULONG FileAlignment; //文件对齐
USHORT MajorOperatingSystemVersion;//操作系统主版本号
USHORT MinorOperatingSystemVersion;//操作系统小版本号
USHORT MajorImageVersion; //映像文件主版本号
USHORT MinorImageVersion; //映像文件小版本号
USHORT MajorSubsystemVersion; //子系统主版本号
USHORT MinorSubsystemVersion; //子系统小版本号
ULONG Reserved1; //保留项1
ULONG SizeOfImage; //映像文件大小
ULONG SizeOfHeaders; //所有头的大小
ULONG CheckSum; //校验和
USHORT Subsystem; //子系统
USHORT DllCharacteristics; //DLL特性
ULONG SizeOfStackReserve; //保留栈的大小
ULONG SizeOfStackCommit; //指定栈的大小
ULONG SizeOfHeapReserve; //保留堆的大小
ULONG SizeOfHeapCommit; //指定堆的大小
ULONG LoaderFlags; //加载器标志
ULONG NumberOfRvaAndSizes; //RVA的数量和大小
IMAGE_DATA_DIRECTORY DataDirectory [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录数组
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
typedef struct _IMAGE_DATA_DIRECTORY {
ULONG VirtualAddress; //虚拟地址
ULONG Size; //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY
// 各个目录项
// 输出目录
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
// 输入目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
// 资源目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
// 异常目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
// 安全目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
// 基址重定位表
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
// 调试目录
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
// 描述字符串
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
// 机器值(MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
// TLS(线程本地存储)⑥目录
#define IMAGE_DIRECTORY_ENTRY_TLS 9
// 载入配置目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
我们知道,从数据目录表引出的,仅仅是这些数据的RVA和数据块的尺寸,很明显,不同数据块的数据组织方式是不同的,比如导入表和导出表,描述它们的数据结构是不同的;如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
ULONG Characteristics; //特征
ULONG TimeDateStamp; //时间日期戳
USHORT MajorVersion; //主版本号
USHORT MinorVersion; //小版本号
ULONG Name; //名字
ULONG Base; //基址
ULONG NumberOfFunctions; //函数数
ULONG NumberOfNames; //名字数
PULONG *AddressOfFunctions; //函数的地址
PULONG *AddressOfNames; //名字的地址
PUSHORT *AddressOfNameOrdinals; //名字序数的地址
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
我们知道,PE文件在没有装入内存之前,导入表结构中由OriginalFirstThunk和FirstThunk指向的
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
结构数组中的值是相同的,注意,这个结构定义为一个联合,实际上就是一双字,这个结构用来指定一个导入函数,但双字的最高位是1时,表示函数是以序号的方式导入的,这时双字的低位字就是函数的序号,但双字的高位为0时,表示函数以字符类型的函数名方式导入,这时的双字的值就是一个RVA,指向一个结构:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
其中Hint表示函数的序号,name【】定义了导入函数的名称;
但是为什么要两个一模一样的IMAGE_THUNK_DATA数组呢,让我们先来回顾下调用导入函数的指令:
在RebPE中找到oep后,F8跟踪,你能看到这个函数:
00401150 |. 53 push ebx
00401151 |. 56 push esi
00401152 |. 57 push edi
00401153 |. 8965 E8 mov dword ptr [ebp-18], es>
00401156 |. FF15 28504000 call dword ptr [405028] ; kernel32.GetVersion
Call dword ptr[405028], 直接寻址方式,将405028处的数据内存属性转换为Dword后作为函数的地址调用;
我们可以在提示窗口中:
ds:[00405028]=7C81126A (kernel32.GetVersion)
可见,在这个地址中存放的是(kernel32.GetVersion)导入函数的入口地址;这样,call 调用将Eip压入堆栈后,程序执行流就到kernel32中了;
还有一种调用方式:
就是call aaaaaaaa
Aaaaaaaa jmp dword ptr 【xxxxxxxx】,这个指令是一个间接寻址的跳转指令,如果你在16进制下查看这个ptr[xxxxxxx]的xxxxxxxx,你会发现它存放的是一个指向欲调用的导入函数名的字符串的RVA地址;当PE文件被装载时,windows装载器根据这个xxxxxxxx处的Rva得到函数名,然后用GetProcAddress函数找到内存中此导入函数的地址,并将xxxxxxxx处的内容替换成真正的函数地址;此时firstthunk指向的DWOWD数组中原本指向函数名的RVA被替换成导入函数的真正入口地址;
在PE文件中,所有DLL对应的导入地址数组在位置上是被排列在一起的,全部这写数组的组合也被称为导入地址表,IAT;导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的FirstThunk字段指向的就是IAT的起始地址;还有就是数据目录表中的13项,可以直接用来定位IAT的地址; 不过一般还是以上一种方法为准;
到这里,我们可以来思考手动查找IAT的方法了,由于所有DLL模块的firstThunk数组一般都被放在一块,组成了输入地址表,注意: 这里是一般,不是完全,加密解密三上就有IAT被分开存放的例子,大家可以看下;
所以我们只要在找到OEP后,进入到原PE文件的代码块后,找到一个API调用的地址;如:
004011A4 |. FF15 24504000 call dword ptr [405024] ; [GetCommandLineA
004011AA |. A3 F8894000 mov dword ptr [4089F8], ea>
在[405024]右键-》数据窗口中跟随,内存地址;在数据窗口中数据如下:
00405024 AD 2F 81 7C 6A 12 81 7C FA CA 81 7C 1A 1E 80 7C ?亅j 亅?亅