修复 IAT 重定向(一)

使用工具

  • OllyDbg 1.10原版,简称OD
  • OD 汉化插件均来自互联网;
  • UnPackMe来自互联网,仅供学习使用;
  • Dump 工具为 OD 插件 OllyDump,来自互联网;
  • IAT 重建工具为 ImportREC,来自互联网;
  • 文中特殊数字均是HEX,为了书写方便采用DEC

输入表加密

在脱壳过程中,输入表修复是一个重点,修复关键是得到未加密的 IAT,对与 IAT 相关的位置设断,从而找到外壳处理 IAT 的代码,然后找对策;

加壳程序处理输入表有以下几种情况:

  1. 完整地保留了输入表,外壳加载时未对 IAT 加密;

    当外壳解压数据时,完整的输入表会在内存中出现;

    外壳用显式装载 DLL 的方式获得各函数的地址,并将地址填充到 IAT 中;

    脱壳时,可以在内存映像文件刚生成时抓取输入表,此时,外壳还没来得及破坏原始的输入表;

    此类壳有 ASPack、PECompact 等;

  2. 完整地保留了原输入表,当外壳加载时对 IAT 进行加密处理

    当外壳解压数据时,完整的输入表会在内存中出现;

    外壳用显式装载 DLL 的方式获得各函数的地址,并对这些地址进行处理(即 Hook API);

    最后将 Hook API 的外壳代码的地址填充到 IAT 中;

    由于 IAT 已经被加密,直接使用 ImportREC 是无法重建输入表的,但可以在外壳还没来得及加密 IAT 时抓取输入表,或者跳过对 IAT 进行加密的代码;

    此类壳有 tElock 等;

  3. 加壳时破坏了原输入表,外壳装载时未对 IAT 进行加密处理;

    外壳已经完全破坏原输入表,在外壳刚解压的映像文件中的是输入函数的字符串;

    外壳用显式装载 DLL 的方式获得这些函数的地址,直接将函数地址填充到 IAT 中;

    因为 IAT 未加密,所以在脱壳时可以用 ImportREC 根据 IAT 重建一个输入表;

    此类壳有 UPX 等;

  4. 加壳时破坏了原输入表,装载外壳时对 IAT 进行加密处理;

    如果外壳已经完全破坏了原输入表,外壳将用显式装载 DLL 的方式获取各函数地址,并对地址进行处理(即 Hook API),最后将 Hook API 的外壳代码填充到 IAT 中;

    在脱壳时,不仅可以利用 ImportREC 的一些插件来对付这些加密的 IAT,也可以修改外壳处理输入函数地址的代码,使其生成的 IAT 不被加密,然后用 ImportREC 重建输入表;

    此类壳有 ASProtect 等;

操作流程

  1. 本次 CM 为第 2 种情况, tElock 壳;

  2. 将 CM 导入 OD,可以使用最后一次异常法编译语言特点法(GetVersion)来到达 OEP,栈平衡法内存访问法无法到达 OEP,原因是有硬件断点和 CC 断点检测;

  3. 到达 OEP 后,定位 IAT,然后查看:

    OEP

    可以看到 IAT 中间有大段的未知数据,但这些数据所在的区段是存在的,可能不是无效数据;

    如果不是无效数据,既然 IAT 中有加密的函数,也有未加密的函数,那么外壳程序中一定会有判断和跳转来分别处理这些函数;

  4. 记录当前区段信息,然后重载程序进行比较:

    进行比较

    可以看到,程序载入 OD 后,在没有运行的情况下,其它区段是不存在的,也就是说,多出来的区段是由外壳生成的;

    所以,IAT 中处于这些区段的未知数据应该不是无效信息,可能是被加密了(Hook API);

  5. 再次来到 OEP,通过查看代码也可以确认这些数据不是无效信息:

    确认

    可以看到,指令中有 CALL 指向了 IAT 中的未知数据,如果是无效信息,这是不成立的;

    单步步入到 CALL,是否能获得有用信息:

    单步步入

    也就是说,这个 CALL 其实是调用了 kernel32 中的 GetVersion 函数,而 460ADC 中 存储的未知信息就是 Hook API 的地址;

  6. 确认这些未知数据是否无效的终极大招是尝试一下:

    使用 OllyDump 将内存中的数据 Dump 下来并保存到可执行文件;

    然后使用 ImportREC 获取并剔除未知数据后修复程序,程序并不能运行,所以这些数据不是无效的,而是加密的;

  7. 既然 IAT 中有加密的函数也有未加密的函数,如果设置写入断点,能否获取到外壳处理 IAT 的关键代码呢?值得一试:

    • 以 IAT 表中第 1 个未加密的函数 ADVAPI32.RegCloseKey 为例,写入地址为 00460818,写入数据为 77DA6C27;
    • 还需要一个加密的函数,以 kernel32.GetVersion 为例,这个是当前已知的加密函数,写入地址为 00460ADC,写入数据为 00A206F7;
    1. 重载程序后,在数据窗口 goto 到 00460818 位置,设置内存写入断点,然后运行程序:

      00460818

      程序会中断多次,但通过查看相关信息,都不是关键位置,直到中断在此处;

      这里的指令执行后,会将 EAX 中的数据拷贝至 EDI,而 EAX 中的数据就是对应的 IAT 中的数据 77DA6C27,而 EDI 对应的则是 IAT 中的写入地址 00460818;

      记录当前指令的地址,然后 F8 单步执行程序,来到起始位置,记录起始地址;

    2. 重载程序,在数据窗口 goto 到 00460ADC 位置,设置内存写入断点,然后运行程序:

      00460ADC

      同样的流程后,程序来到了相似的位置且提示窗口和寄存器窗口都显示了相关信息,写入地址为 00460ADC,写入数据为 00A206F,也是 IAT 中对应的数据;

      而且两次中断指令的位置很接近,应该在同一个函数逻辑中,F8 单步运行程序,来到起始位置:

      起始地址

      对比两次的起始地址相同,确认此处为外壳加密 IAT 的关键代码;

    3. 分析代码后,确认此处为关键跳转,修改为 JMP,然后给代码段设置内存访问断点并取消其它断点,然后运行程序:

      关键跳转

      到达 OEP 后定位 IAT,然后查看数据:

      查看 IAT

      可以看到,断点之后的所有未知数据已经解密了,说明前面修改的跳转确实为关键跳转;

  8. 重载程序,在反汇编窗口中跟随到关键代码的起始地址,发现 goto 到此处的指令与运行到此处的指令不一致,看来,外壳程序是边运行边解压;

  9. 接下来就是获取完整的原程序 IAT 了,由于不知道外壳程序会先解密哪个 IAT,所以给 IAT 的所有字节设置内存写入断点,然后运行程序:

    整个 IAT 设置内存写入断点

    第 1 次中断是访问 IAT 的起始地址,在 CPU 窗口 goto 到关键代码的起始地址,发现代码已经解压恢复了:

    解压恢复

    修改关键跳转为 JMP,取消所有断点并给代码段设置内存访问断点,然后运行程序:

    全部恢复了

    到达 OEP 后查看 IAT 已经全部解密了;

  10. 使用 ImportREC 获取解密后的输入表,然后修复 dumped.exe:

    ImportREC

    全部获取到了,没有无效信息;

    运行正常

    程序正常运行,且导入 OD 后查看 IAT 也正常,脱壳完成;