DLL文件的脱壳与EXE文件步骤差不多, 所不同的是, DLL文件多了个基址重定位表等要考虑。
在2003年出版的《加密与解密》(第二版)中以UPX, PECompact为例讲述了DLL重定位重建的方法, 由于本人的思路限制, 当时只是从UPX, PECompact自身特点找思路解决这问题, 即先分析UPX, PECompact对重定位表处理算法, 然后写工具逆算法还原重定位表, 如UPXAngela.exe等工具。 这种思路的通用性不好, 针对不同的壳和版本, 要重写工具, 并且逆算法可能不完美, 从而存在bug。
后来, askformore在“重建重定位表脚本”一文中, 提出了一种更通用性的解决办法, 利用外壳重定位相关数据时, 会根据外壳转储的重定位表确定要重定位的RVA, 完成代码重定位工作。 将这些要重定位的RVA提取出来, 再将这些RVA根据重定位表的定义重新生成一份新的重定位表。 shoooo也曾提到过这个思路。 于是, 在第三版重写这部分时, 根据这个思路写了一款工具来完成这个重建功能, 详见附件的ReloREC。 另外, ReloREC重构重定位表的算法代码, 参考了ccfer在看雪论坛.珠海金山2007逆向分析挑战赛 第二阶段第三题 提交的代码。 在此一并表示感谢!
声明:本文以第三版“13.5 DLL文件脱壳”一文和其他章节临时整理组织, 稍有简化, 可能有部分地方用词和描述不是太连贯。
加壳的DLL处理重定位表有以下几种情况:
1)完整的保留了原重定位表;
2)对原重定位表进行了加密处理;
等等
像ASPack,ASProtect等壳属于第1种情况, 没有加密重定位表, 脱壳后, 只需找到重定位的地址和大小即可。
像UPX, PECompact等壳属于第2种情况, 必须重建重定位表, 这也是本文所要讨论的, 本文以UPX为例来讲述一下重定位的重建。
用UPX v3.01将EdrLib.dll文件加壳, 用PE工具查看其PE信息。
EntryPoint:E640h
ImageBase:400000h
13.5.1 寻找OEP
当DLL被初次映射到进程的地址空间中时, 系统将调用DllMain函数, 当卸载DLL时也会再次调用DllMain函数。 也就是说, DLL文件相比EXE文件运行有一些特殊性, EXE的入口点只在开始时执行一次, 而DLL的入口点在整个执行过程中至少要执行两次。 一次是在开始时, 用来对DLL做一些初始化。 至少还有一次是在退出时, 用来清理DLL再退出。 所以DLL找OEP也有两条路可以走, 一是载入时找, 另一方法是在退出时找。 而且一般来说前一种方法外壳代码较复杂, 建议用第二种方法。
UPX壳比较简单, 往下翻翻, 就可看到跳到OEP的代码:
代码:
003DE7F5 . 58 pop eax
003DE7F6 . 61 popad
003DE7F7 . 8D4424 80 lea eax, dword ptr [esp-80]
003DE7FB > 6A 00 push 0
003DE7FD . 39C4 cmp esp, eax
003DE7FF .^ 75 FA jnz short 003DE7FB
003DE801 . 83EC 80 sub esp, -80
003DE804 >- E9 372AFFFF jmp 003D1240 //跳到OEP
13.5.2 Dump映像文件
停在OEP后, 运行LordPE, 在进程窗口选择loaddll.exe进程, 在下方窗口中的EdrLib.dll模块上单击右键, 执行“dump full”菜单命令, 将文件抓取并保存到文件里, 如图13.43所示。
dll.gif
图13.45 抓取DLL内存映像
对于DLL文件来说, Windows系统没有办法保证每一次运行时提供相同的基地址。 如果DLL基址所在内存空间被占用或该区域不够大, 系统会寻找另一个地址空间的区域来映射DLL, 此时外壳将对DLL执行某些重定位操作。 从图13.43得知, 此时DLL被映射到内存的地址是03D000h, 与EdrLib.dll默认的基址400000h不同, 被重定位项所指向的地方是已经重定位了的代码数据。
例如这句:
代码:
003D1266 A1 58B43D00 mov eax, dword ptr [3DB458]
为了得到与加壳前一样的文件, 必须找到重定位的代码, 跳过它, 让其不被重定位。 重新加载DLL, 对上句重定位的地址3D1267h下内存写断点, 中断几下, 就可来到重定位的处理代码。
代码:
003DE79E mov al, byte ptr [edi] ;指向UPX自己加密过的重定位表
003DE7A0 inc edi ;指针移向下一位
003DE7A1 or eax, eax ;EAX=0?结束标志
003DE7A3 je short 003DE7C7
003DE7A5 cmp al, 0EF
003DE7A7 ja short 003DE7BA
003DE7A9 add ebx, eax ;EBX的初值为(0xFFC+基址)
003DE7AB mov eax, dword ptr [ebx] ;EBX指向需要重定位的数据, 取出放到EAX
003DE7AD xchg ah, al
003DE7AF rol eax, 10
003DE7B2 xchg ah, al
003DE7B4 add eax, esi ; ESI指向UPX0区块的VA, 本例=3D1000
003DE7B6 mov dword ptr [ebx], eax ;重定位
003DE7B8 jmp short 003DE79C
003DE7BA and al, 0F
003DE7BC shl eax, 10
003DE7BF mov ax, word ptr [edi]
003DE7C2 add edi, 2
003DE7C5 jmp short 003DE7A9
003DE7C7 mov ebp, dword ptr [esi+E044] ;改好ESI为401000后, 按F4到这里
UPX壳己将原基址重定位表清零, 重定位操作时, 使用其自己的重定位表。 地址3DE7B4h处ESI指向UPX0区块的VA, 本例为3D1000h, 为了让代码以默认ImageBase的值400000h重定位代码, 可以在这句强制将ESI的值改为401000h。 来到这句后, 双击ESI寄存器, 改成401000h, 然后按F4来到3DE7C7h这时。 此时代码段的数据没被重定位, 可以Dump了。
代码:
003D1253 833D 68AD4000 00 cmp dword ptr [40AD68], 0
运行LordPE将DLL映像抓取, 并保存为upx_dumped.dll。
13.5.3 重建DLL的输入表
ImportREC能很好地支持DLL的输入表的重建, 首先, 在Options里将“Use PE Header From Disk”默认的选项去除。 这是因为ImportREC需要获得基址计算RVA值, DLL如果重定位了, 从磁盘取默认基址计算会导致结果错误。
1)在ImportREC下拉列表框中选择DLL装载器的进程, 此处为loaddll.exe进程。
2)单击“Pick DLL”按钮, 在DLL进程列表中选择EdrLib.dll进程(见图13.47)。
dll2.gif
图13.47 选择DLL进程
3)在OEP处, 填上DLL入口的RVA值1240h, 单击IAT AutoSearch按钮获取IAT地址。 如果失败, 必须手工判断DLL的IAT位置和大小, 其RVA为7000h, Size为E8h。
4)单击“Get Import”按钮, 让其分析IAT结构重建输入表。
5)勾选Add new section, 单击“Fix Dump”按钮, 并选择刚抓取的映像文件dumped.dll, 它将创建一个dumped_.dll文件。
13.5.4 构造重定位表
原理请参考本文开始处的说明。
先来回顾一个重定位表的结构:
代码:
IMAGE_BASE_RELOCATION STRUCT
VirtualAddress dd 0
SizeOfBlock dd 0
Type1 dw 0; 其中:Bit15—Bit12为类型 type, Bit11--Bit0 为ItemOffset
IMAGE_RELOCATION ENDS
重定位表以1000h大小为一个段, 因为ItemOffset最长为12位, 即刚好为1000h。 如果还有更多段, 将重复上面数据结构, 直到VirtualAddress为NULL, 表示结束。
ReloREC工具可以根据一组重定位的RVA, 重新构造一个新的重定位表。 首先要做的工作是将UPX外壳这些要重定位的RVA提取出来。
在处理重定位代码语句中, 下面这句就是对代码重定位, 其中EBX保存的就是要重定位的地址。
代码:
003DE7B6 mov dword ptr [ebx], eax ;EBX指向要重定位的RVA
补丁的思路是找块代码空间, 跳过去执行补丁代码, 将重定位的地址转成RVA, 并保存下来。 如下语句跳到补丁代码处:
代码:
003DE7B8 jmp short 003DE80A
我们键入的补丁代码:
003DE80A pushad
003DE80B mov edx, dword ptr [3E0000] ;从全局变量3E0000h取一地址指针
003DE811 sub ebx, 3D0000 ;减外壳基址, 将ebx中的地址转成RVA
003DE817 mov dword ptr [edx], ebx ;将获得的RVA保存下来
003DE819 add edx, 4 ;指向下一个DWRD地址
003DE81C mov dword ptr [3E0000], edx ;将指针保存到全局变量中
003DE822 popad
003DE823 jmp 003DE79C ;跳回外壳代码
3E0000h这个地址是OllyDbg的插件HideOD临时分配的, 其初始值设为3E0010h, 如图13.71。
dll3.gif
补丁代码键入完成后, 外壳在处理重定位相关代码时, 这段补丁代码将需要重定位的RVA全部提取出来。 执行完补丁代码, 数据窗口将保存需要重定位的RVA,
将需要重定位的RVA复制出来(选取数据时, 最后一个DWORD数据是0), 操作时单击鼠标右键, 执行菜单Binary/Binary copy(二进制复制)功能, 再运行WinHex, 新建一文档, 将这段二进制数据粘贴进去, 粘贴时, 选择ASCII Hex模式(图13.52), 然后将提取的数据保存为Relo.bin。
Relo.bin中保存的就是需要重定位的地址, 以RVA表示。 部分数据如下:
代码:
0000101D
00001031
0000106E
0000108D
000010A1
000010DE
000010FB
00001109
0000110F
……
ReloREC这款工具, 就是根据这些RVA重新生成一份新的重定位表
准备工作完成, 运行ReloREC, 将Relo.bin拖放到ReloREC主界面上可打开此文件, 如图13.53。 然后在dumped_.dll里找一块空白代码处保存重定位表(一般在UPX1或UPX2区块里找), 在这选择C000h处。 在Relocation's RVA域里填上原始重定位表的RVA地址, 本例为C000h, 最后单击“Fix Dump”按钮, 打开上节刚修复输入表的dumped_.dll文件, 即可完成重定位表的修复。
……