手动重建 IAT
破坏原程序的输入表是加密外壳必备的功能,因此在脱壳中,输入表的处理是一个关键环节,这需要脱壳者对 PE 格式中输入表的概念必须非常清楚;
手动修复可以更清晰的理解重建输入表的过程及原理,但很辛苦,需要细心细心再细心,中间因为数值填错而苦苦寻找,想象一下在一堆二进制中找一个数值错误,很可怕,但很值得,弄懂了原理,以后就善用工具了;
使用工具
- OllyDbg 1.10原版,简称
OD
; OD
汉化
和插件
均来自互联网;- UnPackMe来自互联网,仅供学习使用;
- 加壳工具为ASPACK:收费软件,可以试用;
- Dump 工具为 LoadPE,来自互联网;
- 16 进制修改器为 WinHex;
- 文中特殊数字均是
HEX
,为了书写方便采用DEC
;
输入表重建的原理
在输入表结构中,与实际运行相关的主要是 IAT 结构,这个结构用于保存 API 的实际地址;
PE 文件运行时将初始化输入表的这一部分:
- Windows 加载器首先搜索 OriginalFirstThunk;
- 如果存在,加载程序将迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址;
- 然后,加载器用函数真正的入口地址代替由 FirstThunk 指向的 IMAGE_THUNK_DATA 数组中元素的值;
- 初始化结束后,输入表中的其它部分就不重要了,程序依靠 IAT 提供的函数地址就可以正常运行;
外壳程序一般都会修改原程序文件的输入,然后自己模仿 PE 装载器来填充 IAT 中的相关数据,也就是说,内存中只有一个 IAT,原程序的输入表不在内存中;
输入表重建就是根据这个 IAT 还原整个输入表的结构,包括 IID 结构以及其它各成员指向的数据等;
一些加密软件为了防止输入表被还原,在 IAT 加密上大作文章,此时,由外壳填充到 IAT 中的不是实际的 API 地址,而是用于 Hook API 的外壳代码的地址;
这样,外壳中的代码一旦完成了加载工作,在进入原程序的代码之后,仍然能够间接获得程序的控制权;
因为程序总要与系统打交道,与系统打交道的途径是 API,而 API 的地址已经被替换为外壳的 Hook API 的地址,所以,每次程序与系统打交道,都会让外壳程序获得一次控制权;
这样,外壳就可以进行反跟踪,从而继续保护软件,同时完成某些特殊任务了;
综上所述,重建输入表的关键是获取未加密的 IAT,一般的做法是跟踪加壳程序对 IAT 的处理过程,修改相关指令,不让外壳加密 IAT;
确定 IAT 的地址和大小
输入表重建的关键是 IAT 的获得;
一般程序的 IAT 是连续排列的,以一个 DWORD 字的 0 作为结束,因此,只要确定 IAT 的一个点,就能获得整个 IAT 的地址和大小;
程序中的每一个 API 函数在 IAT 中都有自己的位置,这样,无论在代码中调用函数多少次,都会通过 IAT 中的同一个函数指针来完成;
程序调用输入函数分为直接调用和间接调用:
直接调用:CALL DWORD PTR [00401506]
,直接调用跳转的地址就是函数的行首;
间接调用:CALL <JMP.&KERNEL32.GetModuleHandleA>
,间接调用是获取跳转地址存储的内容,然后调用;
以 CM 为例:
此处为间接调用,在选择行按下 Enter 键即可到调用位置:
这里有很多跳转至输入函数的指令,也可以说是 IAT 吧(IAT 跳转表);
IAT 是一块连续排列的数据,因此,可以向上滚动屏幕,直到没有跳转,就是 IAT 的起始位置;
可以看到,当前地址之前的数据为 0,所以可以确定,这里就是 IAT 的起始位置;
既然起始位置是向上滚动,结束位置肯定是向下滚动:
需要注意的是,IAT 中的 IID 结构数组以 NULL 确定数据的结尾,所以,IAT 的结尾不是最后一个跳转指向的地址,而是下一个 DWORD 00000000
;
为了更直观地观察,可以让数据窗口直接显示这些 API 函数,以确定 IAT 是否正确:
设置数据窗口显示方式后,可以更直观地看到 IAT 的起始地址和结束地址;
根据 IAT 重建输入表
使用 ASPACK 加密 CM,然后导入 OD;
使用栈平衡法定位并跳转至 OEP;
运行 LoadPE,将内存数据 Dump 出来并保存(Dump 过程中不能关闭 OD):
Dump(转存)是指把内存指定地址的映像文件读出,用文件等形式将其保存下来的过程;
在程序到达 OEP 且没有运行时,Dump 是正确的,而程序运行后,由于一些变量已经初始化了,所以不适合 Dump;
在外壳处理过程中,外壳要把压缩后的全部代码数据释放到内存中,并初始化一些项目,因此,在此过程中也可以选择合适的位置进行 Dump;
常用的 Dump 软件有 LoadPE、PETools 等,这类工具一般利用 Module32Next 来获取欲 Dump 进程的基本信息;
首先设置 LoadPE,勾选完整转存选项:
设置完成后,在 LoadPE 的进程窗口中选择 CM 的进程,然后单击右键,在弹出的快捷菜单中执行“完整转存”命令,抓取并保存:
注意保存文件的后缀,默认为 .dll 需要修改为 .exe;
运行 dumped.exe 发现不能运行:
将 dumped.exe 导入 OD,在弹出错误弹窗后,程序并没有停在 OEP 位置,说明异常是在初始化时产生的:
查看 log 窗口,发现创建进程后异常就发生了,程序都没有完成初始化;
回到反汇编窗口,goto 到 OEP,然后来到 IAT 表:
发现 PE 加载器在搜索 OriginalFirstThunk 数组时异常了;
这时,就需要修复了;
回到之前的 OD,通过查看 IAT 可以看到,CM 使用了 5 个 DLL,分别是:user32.dll、kernel32.dll、comctl32.dll、GDI32.dll、comdlg32.dll,它们分别对应一个 IAT,IAT 之间以一个 DWORD 类型的 0 隔开,整理 IAT 成员的函数:
user32.dll kernel32.dll comctl32.dll GDI32.dll comdlg32.dll KillTimer GetLocalTime InitCommonControls TextOutA GetSaveFileNameA GetSystemMetrics OpenFile CreateToolbarEx StartPage GetOpenFileNameA LoadCursorA GlobalFree CreateToolbar StartDocA PrintDlgA LoadAcceleratorsA GlobalAlloc GetTextMetricsA MessageBeep lstrlenA GetStockObject GetWindowRect CloseHandle EndPage LoadStringA WriteFile EndDoc LoadIconA GetModuleHandleA DeleteObject LoadBitmapA ReadFile DeleteDC SetFocus ExitProcess MessageBoxA PostQuitMessage WinHelpA InvalidateRect TranslateAcceleratorA MoveWindow TranslateMessage LoadMenuA ShowWindow SendMessageA SetTimer SetWindowPos UpdateWindow RegisterClassA BeginPaint CreateWindowExA DefWindowProcA DialogBoxParamA DispatchMessageA DrawMenuBar EndDialog EndPaint FindWindowA GetDC GetDlgItem GetDlgItemTextA GetMessageA 接下来就是修复了,使用 WinHex 打开 dumped.exe,在文件中找到一块空白空间,将表中的 DLL 名和函数名写进去:
为了加深对输入表的理解,手动。。。
写入数据时:
每个函数前面要留 2 个字节来存放函数的序号,序号可以为 0;
每个函数后的 1 字节为 0,即以 0 结尾;
每个函数名或 DLL 名的起始位置必须按偶数对齐,空隙用 0 填充;
因为 dumped.exe 是内存映像文件,所以文件偏移地址和相对虚拟地址(RVA)是相等的;
整理 DLL 名和 API 名所在的偏移地址:
DLL 或 API 名称 地址 API 名称 地址 user32.dll 00002200 user32.dll 中的 API 👇👇👇 KillTimer 00002240 GetSystemMetrics 0000224C LoadCursorA 00002260 LoadAcceleratorsA 0000226E MessageBeep 00002282 GetWindowRect 00002290 LoadStringA 000022A0 LoadIconA 000022AE LoadBitmapA 000022BA SetFocus 000022C8 MessageBoxA 000022D4 PostQuitMessage 000022E2 WinHelpA 000022F4 InvalidateRect 00002300 TranslateAcceleratorA 00002312 MoveWindow 0000232A TranslateMessage 00002338 LoadMenuA 000234C ShowWindow 00002358 SendMessageA 00002366 SetTimer 00002376 SetWindowPos 00002382 UpdateWindow 00002392 RegisterClassA 000023A2 BeginPaint 000023B4 CreateWindowExA 000023C2 DefWindowProcA 000023D4 DialogBoxParamA 000023E6 DispatchMessageA 000023F8 DrawMenuBar 0000240C EndDialog 0000241A EndPaint 00002426 FindWindowA 00002432 GetDC 00002440 GetDlgItem 00002448 GetDlgItemTextA 00002456 GetMessageA 00002468 kernel32.dll 0000220C kernel32.dll 中的 API 👇👇👇 GetLocalTime 00002476 OpenFile 00002486 GlobalFree 00002492 GlobalAlloc 000024A0 lstrlenA 000024AE CloseHandle 000024B8 WriteFile 000024C6 GetModuleHandleA 000024D2 ReadFile 000024E6 ExitProcess 000024F2 comctl32.dll 0000221A comctl32.dll 中的 API 👇👇👇 InitCommonControls 00002500 CreateToolbarEx 00002516 CreateToolbar 00002528 GDI32.dll 00002228 GDI32.dll 中的 API 👇👇👇 TextOutA 00002538 StartPage 00002544 StartDocA 00002550 GetTextMetricsA 0000255C GetStockObject 0000256E EndPage 00002580 EndDoc 0000258A DeleteObject 00002594 DeleteDC 000025A4 comdlg32.dll 00002232 comdlg32.dll 中的 API 👇👇👇 GetSaveFileNameA 000025B0 GetOpenFileNameA 000025C4 PrintDlgA 000025D8 接着,构造指向函数名地址的 IMAGE_THUNK_DATA 数组:
位置随意,两个数组之间的间隔为 2 字节,用 0 填充(小端序构建);
然后构建其 IID 数组:
DLL OrignalFirstThunk TimeDateStamp ForwardChain Name FirstThunk user32.dll 00290000 00000000 00000000 00220000 84310000 kernel32.dll 98290000 00000000 00000000 0C220000 1C320000 comctl32.dll C4290000 00000000 00000000 1A220000 48320000 GDI32.dll D4290000 00000000 00000000 28220000 58320000 comdlg32.dll FC290000 00000000 00000000 32220000 80320000 (结束标志 ) 00000000 00000000 00000000 00000000 00000000 接下来,使用 LoadPE 修改输入表地址:
修改完成后,运行程序,一切正确,这就完了吗?并没有;
将 dumped.exe 导入 OD:
程序会弹出提示入口点超出代码段范围的警告,所以,还需要修改 OEP:
修改 OEP 为正常数值,并保存修改到可执行文件;
将保存的文件导入 OD,然后检查 IAT,有数据才算正常:
总结一下:
- 构建 IMAGE_IMPORT_BY_NAME 结构体,用来存储 DLL 名和 API 名;
- 构建 IMAGE_THUNK_DATA 结构体,指向函数名对应的地址;
- 构建 IID 结构体,OrignalFirstThunk 指向 IMAGE_THUNK_DATA 对应的起始地址,Name 指向 IMAGE_IMPORT_BY_NAME 对应的地址,FirstThunk 指向原程序的 IAT 表;
- 构建过程中,每项的注意事项不再赘述;
所有修改如下:
IMAGE_IMPORT_BY_NAME:
IMAGE_THUNK_DATA:
IID: