Windows逆向之脱壳-IAT修复
Aking9 发表于 河南 历史精选 1441浏览 · 2024-07-29 16:35

前置知识

idata概述

  • NtHeader.OptionalHeader.DataDirArry[1]=Import

    • `Import.VirtualAddress:存储.idata节区的RVA

      可以转换为FOA

    • Import.Size:存储.idata的大小

  • SectionHeaders[]中存储着.idata在虚拟内存和物理内存的信息

    • Name:长度为8的ASCII节区名(比如.text)。
    • VirtualAddress-0x0C:内存中节区起始地址(RVA)
    • PointerToRawData-0X14:磁盘文件中的起始地址FOA
  • .idata开始存储的是导入表的描述符结构-IMAGE_IMPORT_DESCRIPTOR

    IID存储着一下内容

    • OriginalFirstThunk:指向导入名称表(INT)的RVA

    • Name:表示DLL模块的名称的RVA。

    • FirstThunk:表示每个DLL的导入地址表(Import Address Table,IAT)的RVA。

      PS,注意,当未加载到内存时,里面的内容与INT内容一样

      只有当加载到内存中后,才会填充未真正的IAT

所以.idata可以分为以下内容

  • 导入描述符:存储INT和IAT的地址,以及要导入的DLL的Name的地址
  • 导入名称表:存储API_Name的指针
  • 导入地址表:
    • 未导入内存时:存储API_Name的指针
    • 导入内存后:存储API的地址
  • 名称表:DLL和API的Name
    • 前一部分存储所有要导入的DLL_Name
    • 后一半部分存储各DLL的API的Name

IAT 填充过程

  1. 解析PE文件头

    当一个可执行文件(EXE或DLL)加载到内存中时,操作系统的PE(Portable Executable)加载器首先解析PE文件头,以找到导入表(Import Table)的位置。

  2. 定位导入表

    导入表包含了程序需要的所有DLL及其导入的函数名称。导入表的结构包括一个或多个导入描述符,每个描述符对应一个DLL。

  3. 加载DLL

    对于每个导入描述符,加载器会加载相应的DLL文件到内存中。如果DLL文件尚未加载,则加载器调用LoadLibrary函数加载它。

  4. 获取函数地址

    加载器解析导入描述符中的函数名称,并调用GetProcAddress获取每个导入函数的实际内存地址。

  5. 填充IAT

    一旦函数地址被解析,加载器将这些地址填充到IAT中。此时,IAT中的每个条目都指向实际的DLL函数地址。

IAT重定向

在Windows逆向工程中,IAT重定向通常用于修改程序,通过IAT重定向可以让程序调用自定义的函数,而不是原始的函数。

这种技术用于调试、分析程序行为,或者注入恶意代码。

IAT重定向过程:

  1. 定位IAT

  2. 备份原始IAT (可选)

  3. 修改IAT

    将IAT中的函数地址替换为自定义函数的地址。这样当程序调用原始函数时,会转而调用自定义函数

  4. 恢复IAT(可选)

这种技术常用于软件保护、反调试和恶意软件分析中。通过修改IAT,分析人员可以截获并分析程序与操作系统或其他库的交互。

如何判断IAT重定向:

  • OllyDbg:反汇编窗口中单击鼠标右键选择-Search for-All intermodular calls。

  • 在OllyDbg对比,EP与OEP时的内存分布。

    加壳程序在自解压时,创建新的地址,Call Address-1

    Address-1:存储的Address-2时中间指针

    Address-2:存储真正的API地址

    Address-2的地址是在解压中新创建的。

IAT修复

普通修复

这里以UPX加壳程序为案例来表示

对于UPX加壳程序,我们使用ESP定律来定位到OEP

F9,程序运行到0x00409D3F,F8运行到OEP

Dump程序

使用ImportREC修复IAT

将IAT填充到dump出的程序中,会新建一个CRACKME-UPX-Dump_.EXE

我们查看程序,查看是否修复完成

程序导入OllyDbg,查看

查看我们修复的新的 IAT表

IAT重定向修复

这里面我们使用tElock0加壳程序来学习IAT重定向修复

该程序使用最后一次异常法到达OEP,要注意Olly'Dbg的设置

一直shift+F9,当运行到0x004666F1时,我们需要设置断点

对程序的代码段设置CC断点

shift + F9跟踪几次后,到达OEP

我们查看程序的IAT:反汇编窗口右键 --> 查找 -->所有模块间的调用

我们知道0x00460ADC处是重定向,我们运行到0x004271D6

F7跟入查看自定义函数的内容,F8单步跟踪到0x00A20705,获取到GetVerison的地址


CALL DWORD PTR DS:[460ADC] DS:[00460ADC]=00A206F7,在0x00A206F7处函数内部才会获取到API的地址

我们查看程序的结构分布

因此我们知道:加壳程序在自解压时,创建新的地址,Call Address-1

  • Address-1:存储的是Address-2-自定义函数地址
  • Address-2:自定义函数中有调用真正API的指令
  • 地址A的dress-2是在解压中新创建的。

手动修复

OllyDbg直接修复

在程序被定位到OEP时,不Dump程序,而是先自行修改IAT,再Dump,再使用Import REC导出

注意,这里为了追踪地址,所以EIP不是OEP,

我们需要在OEP将所有的IAT修复,这里为了演示,只修复一个。

ImportREC直接修复

我们可以在ImportREC中修复重定向,先dump,再

RUN跟踪

EIP is in range

这些是系统DLL的区段,当EIP位于主程序的区段或者壳创建的区段中,我们让OD自动继续跟踪,OD跟进到DLL中的时候,我们需要OD停止跟踪,

用户空间(User Space):一般为前2 GB(0x00000000到0x7FFFFFFF),供应用程序使用。

我们选择主菜单中Debug-Set condition,设置跟踪停止的条件 5D170000-7FFFFFFF

我们按下Ctrl + F11跟踪步入,这里我们可以看到堆栈窗口显示的返回地址正好是我们刚刚设置了断点的那个返回地址4271DC

EIP is outside the range

当EIP超出了主程序所在区段的范围就会停止自动跟踪。

范围0x00400000 - 0x00B12000

Ctrl + F11 程序中断在0x7C811752

Condition is TRUE

有少数壳会将区段创建在系统DLL的区段之间,这样的话,我们需要用到Condition is TRUE.

这里我们搜索EIP指向的指令为RET,并且栈顶指针指向的是4271DC的指令

[ESP] == 4271DC && byte [EIP] == 0C3

  • [ESP] == 4271DC即栈顶指针指向了返回地址4271DC。
  • byte [EIP] == 0C3即EIP指向了API函数的返回指令RET。

Ctrl + F11跟踪,现在我们位于API函数的返回指令RET处,这里由于我们并不在该API函数的入口地址处,所以OD并不会提示该API函数的名称/或提示有错

ImportREC快捷修复

Plugin

对于某些 特用的壳,ImportREC有相对应的插件来使得我们可以快速修复IAT

我们使用tElock1插件,我们可以看到有四项为解决

那么剩下的我们就可以尝试使用手工来修复

Tracers

ImportREC自带有常规的Tracers,只适用于简单壳

修复失败的项上面单击鼠标右键,有三个等级

  • Tracer Level1(Disam)
  • Tracer Level2(Hook)
  • Tracer Level3(Trap Flag)

关键跳法

关键跳法适用于大多数加壳程序,这种方法就是定位壳填充IAT的时机,看看何时填充正确IAT项,何时填充重定向过的IAT项。

我们要先找到两个IAT项,一个是重定向项,一个是未重定向项

将程序导入OllyDbg,定位到0EP

我们可以在反汇编窗口 右键 --> 查找 --> 所有模块间的调用,我们找到了我们要对比的两项

  • 重定向项 460ADC --> 00A206F7 --> GetVersion
  • 未重定向项 460BA8 -->OLEAUT32.VariantClear

我们在数据窗口中查看,查看其内存中的的数据

因此我们知道以下数据

  • 重定向项 460ADC --> 00A206F7 --> GetVersion
  • 未重定向项 460BA8 -->770F4920 (VariantClear)

重定向项跟踪

我们将加壳程序重新导入OllyDbg,我们可以看到此时0x00460ADC内存单元中存储的值是70 08 83 3D

在壳的自解执行过程中会将重定向过的值00A206F7填充到460ADC这个内存单元中。

这里我们对460ADC内存单元设置内存写入断点(PS:该程序会检测硬件断点),让壳在此处写入重定向过的IAT值的时候断下来。

然后我们跟踪程序,Shift + F9,当程序运行到0x00465D91STOS BYTE PTR ES:[EDI],程序向0x00460ADC写入数据。

我们继续跟踪,发现程序在0x00465FC2MOV BYTE PTR DS:[EDI],AL,也向0x00460ADC中写入数据;

且程序在0x00465FE0STOS BYTE PTR ES:[EDI]处时,也向0x00460ADC写入数据,因此我们知道,这两处写入在同一个循环中

当循环结束后,我们发现在0x00466128处也向0x00460ADC写入数据

我们继续追踪,我们在0x004664E5处,我们发现了我们要找到的地方,我们继续追踪,我们在0x004664E5处,我们发现了我们要找到的地方。

我们总结重定向项在IAT填入时的过程如下

  • 第一次写入:0x00465D91
  • 第二此写入:0x00465FC20x00465FE0
  • 第三次写入:0x00466128
  • 关键写入:0x004664E5-MOV DWORD PTR DS:[EAX],ECX

未重定向跟踪

这里我们重新将加壳程序导入到OllyDbg,我们跟踪460BA8写入与460ADC有什么不同

在未填充前00460BA8的内容为59 43 6D 4E,在填充后00460BA8的内容为20 49 0F 77

我们同样对00460BA8内存访问断点,运行追踪

我们跟踪到0x00465D91STOS BYTE PTR ES:[EDI],程序开始向0x00460BA8写入数据,跟重定向项一样

我们继续跟踪,发现程序在0x00465FC2MOV BYTE PTR DS:[EDI],AL,也向0x00460BA8中写入数据,且程序在0x00465FE0STOS BYTE PTR ES:[EDI]处时,也向0x00460BA8写入数据。

继续追踪程序,如重定向项一样,程序在0x00466128处向0x00460BA8写入数据。

到这里,我们已经知道,未重定向项与重定向项在前几处的写入语句是一样的,我们猜测下一个写入数据的地方会有所不同。

我们继续跟踪,发现在0x004665B1处,程序向0x00460BA8写入VariantClear函数的地址

我们总结未重定向项的IAT项填充过程

  • 第一次写入:0x00465D91
  • 第二此写入:0x00465FC20x00465FE0
  • 第三次写入:0x00466128
  • 关键写入:0x004665B1-MOV DWORD PTR DS:[EDI],EAX

关键跳转寻找

我们知道,IAT表的填入是一项一项填入的,那么肯定在写入后,会运行一个跳转语句,执行下一项的写入

我们使用单步跟踪法跟踪程序,我们发现最终都会执行到004665CB ^\E9 BAFDFFFF JMP UnPackMe.0046638A这条指令

PS:0x00460ADC重定向向跟踪过程会比0x00460BA8长,因为中间会有一个定向到GetVersion的过程

我们继续单步执行,寻找这两项不同的跳转之处。

这里有一点要注意,我们0x00460ADC0x00460BA8的项已经填充过了,那么我们就要保证这两项的写一个IAT项是不同的。

即还是一个是重定向IAT项,另一个是未重定向项IAT。

我们可以看到,我们找的这两项符合要求

00460ADC  00A206F7
00460AE0  00A20708

00460BA8  770F4920  OLEAUT32.VariantClear
00460BAC  770F4C7E  OLEAUT32.SysStringLen

PS:这里还有一个坑点,就是我们这里默认按照IAT的每一项是顺序填充的,有的程序可能是跳转填充,这时候就要我们给注意了

如果遇到跳转填充,那对于现在的我们就是一个苦逼的过程了....

那么我们继续单步追踪法来寻找关键的跳转,我们发现下面这两句是关键的跳转因素

004663CB    80A5 D7CC4000 F>AND BYTE PTR SS:[EBP+40CCD7],0FF
004663D2    0F84 23010000   JE UnPackMe.004664FB

其跳转结果如下:

  • 重定向项:JE UnPackMe.004664FB不实现
  • 未重定向项:JE UnPackMe.004664FB实现

关键跳转处修改

我们跟踪到了关键跳转,此时就有人会想到,我们在刚导入程序的时候就更改语句不就可以了,事情远没有这么简单

我们对比刚导入程序时0x004663D2处的指令与脱壳过程中的指令有什么不同

我们可以看到在程序刚开始的时候,这里都是一些垃圾指令,壳会随后的某个时间点写入实际的功能代码。

那么我们就有以下思路来进行跟踪修改关键条

  • 第一次写入:0x00465D91
  • 第二此写入:0x00465FC20x00465FE0
  • 第三次写入:0x00466128

我们对这四处写入语句的前后要观察0x004663D2的指令,看看其是在哪一步更改的

我们转到数据窗口,对460818~460F28这片IAT区域设置内存写入断点,因为有的程序可能会不按顺序填充IAT

这里我们对所有的IAT项都设置上了内存写入断点,当第一个IAT项被写入的时候就会断下来。

那么我们此时查看0x004663D2处的指令是否修改

我们发现已经修改,那么我们就可以将其修改为JMP 004664FB

IAT修复

我们将该关键跳转JE替换成JMP,接着清除之前设置的内存断点,接着跟踪到OEP处。

这里有两种可能,一种就是壳有自校验,运行起来会失败。另一种就是运行很正常,这里我们顺利的跟踪到了OEP处。

或者反汇编窗口右键 --> 查找 --> 所有模块间的调用

我们使用OllyDbg插件OllyDump来Dump程序,程序保存为UnPackMe_tElock0.98-Dump.exe

接着打ImportREC填入OEP,RVA,SIZE等数据,单击Get Imports- 获取导入表。

我们可以看到所有的项都是有效的,说明刚刚修改的关键跳起作用了,接着单击Fix dump-转正存储修复刚刚的dump文件。

我们运行程序,查看是否可以正常运行

导入OllyDbg查看新建的IAT表

反思

这里提一嘴,逆向过程中,最主要的就是思维要活跃,很多程序都需要我们大胆的去猜,那么对于这个程序,我们就可以大胆去猜,该程序是顺序填充IAT,那么我们就可以只对0x00460818设置内存写入断点来跟踪查看0x004663D2的指令是否修改。

实际上,我们可以根据重定向的过程来大胆猜一下,我们知道,重定向项和未重定向项在前三步都是一样的

  • 第一次写入:0x00465D91
  • 第二此写入:0x00465FC20x00465FE0
  • 第三次写入:0x00466128

仅仅在第四步是有所不同:

  • 重定向项关键写入:0x004664E5-MOV DWORD PTR DS:[EAX],ECX

  • 未重定向项关键写入:0x004665B1-MOV DWORD PTR DS:[EDI],EAX

对于IAT写入的两个关键信息

  • 关键的跳转为0x004663D2 -JE UnPackMe.004664FB
  • 循环跳转语句为004665CB - JMP UnPackMe.0046638A

因此我们可以知道,该程序的IAT填充如下,可以分为4个循环

  • 第一次循环写入:0x00465D91

  • 第二次循环写入:0x00465FC20x00465FE0

  • 第三次循环写入:0x00466128

  • 第四次循环:0046638A

    关键跳转:0x004663D2 -JE UnPackMe.004664FB

    • 重定向项:0x004664E5-MOV DWORD PTR DS:[EAX],ECX
    • 未重定向项:0x004665B1-MOV DWORD PTR DS:[EDI],EAX

    004665CB - JMP UnPackMe.0046638A

总结

对于IAT重定向修复,不同壳的关键跳转可能会不一样,但是我们只要细心对比正常的IAT项和重定向项,就会发现差异

注意,如果某个程序没有正常的IAT项,那么我们就要联想到IAT重定向的原理是什么。

IAT重定向不是不获取API的地址,他只是间接获取API的地址,所以他总会填充。

附件:
  • 复件 Packer-Shelling.rar 下载
0 条评论
某人
表情
可输入 255

没有评论