手动重建 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 的起始位置;

IAT 的起始位置

可以看到,当前地址之前的数据为 0,所以可以确定,这里就是 IAT 的起始位置;

既然起始位置是向上滚动,结束位置肯定是向下滚动:

IAT 结束位置

需要注意的是,IAT 中的 IID 结构数组以 NULL 确定数据的结尾,所以,IAT 的结尾不是最后一个跳转指向的地址,而是下一个 DWORD 00000000

为了更直观地观察,可以让数据窗口直接显示这些 API 函数,以确定 IAT 是否正确:

直观地观察

设置数据窗口显示方式后,可以更直观地看到 IAT 的起始地址和结束地址;

根据 IAT 重建输入表

  1. 使用 ASPACK 加密 CM,然后导入 OD;

  2. 使用栈平衡法定位并跳转至 OEP;

  3. 运行 LoadPE,将内存数据 Dump 出来并保存(Dump 过程中不能关闭 OD):

    Dump(转存)是指把内存指定地址的映像文件读出,用文件等形式将其保存下来的过程;

    在程序到达 OEP 且没有运行时,Dump 是正确的,而程序运行后,由于一些变量已经初始化了,所以不适合 Dump;

    在外壳处理过程中,外壳要把压缩后的全部代码数据释放到内存中,并初始化一些项目,因此,在此过程中也可以选择合适的位置进行 Dump;

    常用的 Dump 软件有 LoadPE、PETools 等,这类工具一般利用 Module32Next 来获取欲 Dump 进程的基本信息;

    首先设置 LoadPE,勾选完整转存选项:

    设置 LoadPE

    设置完成后,在 LoadPE 的进程窗口中选择 CM 的进程,然后单击右键,在弹出的快捷菜单中执行“完整转存”命令,抓取并保存:

    Dump

    注意保存文件的后缀,默认为 .dll 需要修改为 .exe;

  4. 运行 dumped.exe 发现不能运行:

    不能运行

    将 dumped.exe 导入 OD,在弹出错误弹窗后,程序并没有停在 OEP 位置,说明异常是在初始化时产生的:

    异常是在初始化时产生的

    查看 log 窗口,发现创建进程后异常就发生了,程序都没有完成初始化;

  5. 回到反汇编窗口,goto 到 OEP,然后来到 IAT 表:

    来到 IAT 表

    发现 PE 加载器在搜索 OriginalFirstThunk 数组时异常了;

    这时,就需要修复了;

  6. 回到之前的 OD,通过查看 IAT 可以看到,CM 使用了 5 个 DLL,分别是:user32.dll、kernel32.dll、comctl32.dll、GDI32.dll、comdlg32.dll,它们分别对应一个 IAT,IAT 之间以一个 DWORD 类型的 0 隔开,整理 IAT 成员的函数:

    user32.dllkernel32.dllcomctl32.dllGDI32.dllcomdlg32.dll
    KillTimerGetLocalTimeInitCommonControlsTextOutAGetSaveFileNameA
    GetSystemMetricsOpenFileCreateToolbarExStartPageGetOpenFileNameA
    LoadCursorAGlobalFreeCreateToolbarStartDocAPrintDlgA
    LoadAcceleratorsAGlobalAllocGetTextMetricsA
    MessageBeeplstrlenAGetStockObject
    GetWindowRectCloseHandleEndPage
    LoadStringAWriteFileEndDoc
    LoadIconAGetModuleHandleADeleteObject
    LoadBitmapAReadFileDeleteDC
    SetFocusExitProcess
    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
  7. 接下来就是修复了,使用 WinHex 打开 dumped.exe,在文件中找到一块空白空间,将表中的 DLL 名和函数名写进去:

    函数数据

    为了加深对输入表的理解,手动。。。

    写进去

    写入数据时:

    • 每个函数前面要留 2 个字节来存放函数的序号,序号可以为 0;

    • 每个函数后的 1 字节为 0,即以 0 结尾;

    • 每个函数名或 DLL 名的起始位置必须按偶数对齐,空隙用 0 填充;

    因为 dumped.exe 是内存映像文件,所以文件偏移地址和相对虚拟地址(RVA)是相等的;

    整理 DLL 名和 API 名所在的偏移地址:

    DLL 或 API 名称地址API 名称地址
    user32.dll00002200user32.dll 中的 API 👇👇👇
    KillTimer00002240GetSystemMetrics0000224C
    LoadCursorA00002260LoadAcceleratorsA0000226E
    MessageBeep00002282GetWindowRect00002290
    LoadStringA000022A0LoadIconA000022AE
    LoadBitmapA000022BASetFocus000022C8
    MessageBoxA000022D4PostQuitMessage000022E2
    WinHelpA000022F4InvalidateRect00002300
    TranslateAcceleratorA00002312MoveWindow0000232A
    TranslateMessage00002338LoadMenuA000234C
    ShowWindow00002358SendMessageA00002366
    SetTimer00002376SetWindowPos00002382
    UpdateWindow00002392RegisterClassA000023A2
    BeginPaint000023B4CreateWindowExA000023C2
    DefWindowProcA000023D4DialogBoxParamA000023E6
    DispatchMessageA000023F8DrawMenuBar0000240C
    EndDialog0000241AEndPaint00002426
    FindWindowA00002432GetDC00002440
    GetDlgItem00002448GetDlgItemTextA00002456
    GetMessageA00002468
    kernel32.dll0000220Ckernel32.dll 中的 API 👇👇👇
    GetLocalTime00002476OpenFile00002486
    GlobalFree00002492GlobalAlloc000024A0
    lstrlenA000024AECloseHandle000024B8
    WriteFile000024C6GetModuleHandleA000024D2
    ReadFile000024E6ExitProcess000024F2
    comctl32.dll0000221Acomctl32.dll 中的 API 👇👇👇
    InitCommonControls00002500CreateToolbarEx00002516
    CreateToolbar00002528
    GDI32.dll00002228GDI32.dll 中的 API 👇👇👇
    TextOutA00002538StartPage00002544
    StartDocA00002550GetTextMetricsA0000255C
    GetStockObject0000256EEndPage00002580
    EndDoc0000258ADeleteObject00002594
    DeleteDC000025A4
    comdlg32.dll00002232comdlg32.dll 中的 API 👇👇👇
    GetSaveFileNameA000025B0GetOpenFileNameA000025C4
    PrintDlgA000025D8

    接着,构造指向函数名地址的 IMAGE_THUNK_DATA 数组:

    IMAGE_THUNK_DATA 数组

    位置随意,两个数组之间的间隔为 2 字节,用 0 填充(小端序构建);

    然后构建其 IID 数组:

    DLLOrignalFirstThunkTimeDateStampForwardChainNameFirstThunk
    user32.dll0029000000000000000000000022000084310000
    kernel32.dll9829000000000000000000000C2200001C320000
    comctl32.dllC429000000000000000000001A22000048320000
    GDI32.dllD429000000000000000000002822000058320000
    comdlg32.dllFC29000000000000000000003222000080320000
    (结束标志 )0000000000000000000000000000000000000000

    接下来,使用 LoadPE 修改输入表地址:

    使用 LoadPE修改输入表地址

    修改完成后,运行程序,一切正确,这就完了吗?并没有;

    将 dumped.exe 导入 OD:

    弹出警告

    程序会弹出提示入口点超出代码段范围的警告,所以,还需要修改 OEP:

    修改OEP

    修改 OEP 为正常数值,并保存修改到可执行文件;

    将保存的文件导入 OD,然后检查 IAT,有数据才算正常:

    检查IAT

  8. 总结一下:

    • 构建 IMAGE_IMPORT_BY_NAME 结构体,用来存储 DLL 名和 API 名;
    • 构建 IMAGE_THUNK_DATA 结构体,指向函数名对应的地址;
    • 构建 IID 结构体,OrignalFirstThunk 指向 IMAGE_THUNK_DATA 对应的起始地址,Name 指向 IMAGE_IMPORT_BY_NAME 对应的地址,FirstThunk 指向原程序的 IAT 表;
    • 构建过程中,每项的注意事项不再赘述;
  9. 所有修改如下:

    • IMAGE_IMPORT_BY_NAME:

      123

    • IMAGE_THUNK_DATA:

      4

    • IID:
      5