如何动态地去找导入表
为什么要动态地查找?
静态地查找导入表非常简单,我们只需要解析 PE 文件头中的数据目录(Data Directory),找到导入表的结构体 IMAGE_IMPORT_DESCRIPTOR
,然后就可以找到所有的导入函数。但这种方法有几个局限性:
- 脱壳(Unpacking): 许多恶意软件会使用加壳技术(packer),将原始的 PE 文件压缩或加密。这种情况下,原始的导入表会被隐藏或破坏,静态分析工具无法找到它。当程序运行时,加壳器会自行解压和修复导入表,因此只有在内存中才能找到真正的导入表
- 动态加载(Dynamic Loading): 程序可能会使用
LoadLibrary
和GetProcAddress
等函数在运行时动态加载 DLL 和获取函数地址。这种方式下,导入的函数根本不会出现在 PE 文件的静态导入表中 - 防止逆向工程: 有些程序开发者故意混淆或破坏导入表,以增加逆向工程的难度
因此,动态地查找导入表是进行脱壳、恶意软件分析和深入逆向工程的必备技能
方法一:利用 EAT
(导出地址表)
这是最直接、也是最不寻常的方法。如果一个程序(比如一个 DLL)将自己的导入表中的函数地址作为导出函数暴露出来,你就可以通过解析它的导出表 (Export Address Table, EAT) 来找到导入表。但这种情况非常少见,通常只在一些特殊的系统 DLL 或驱动程序中出现。这种方法不具有通用性
方法二:利用函数调用指令
这是最常见、最实用的方法。当程序调用一个导入函数时,通常会使用 CALL
指令,其目标地址就是导入表中的一个条目
具体步骤:
- 调试器附加: 使用调试器(如 OllyDbg、x64dbg、IDA Pro Debugger)附加到目标进程
- 设置断点: 在程序执行的早期,比如
main
函数或WinMain
函数的入口点设置断点 - 单步调试/跟踪: 逐步执行(Step Over)程序,并密切关注
CALL
指令 - 识别导入调用:
- 相对
CALL
: 如果你看到CALL [地址]
这样的指令,并且这个地址是一个外部函数的地址,那么这个[地址]
就是一个导入表项。例如,CALL DWORD PTR [EAX]
- 直接
CALL
: 如果你看到CALL MessageBoxA
,这通常是 IDA Pro 这样的反汇编器帮你标记的,它已经识别出这个调用指向了一个导入函数
- 相对
- 内存转储和分析: 当你找到一个导入表项的地址后,你可以从这个地址开始,向前和向后扫描内存,寻找连续的、看起来像函数指针的地址序列。这个序列很可能就是完整的导入表。然后你可以将这一块内存转储出来进行进一步分析
这种方法需要对汇编语言有深入理解,并且需要耐心和细致的调试
方法三:利用内存断点和内存扫描
当程序加载时,加载器会向导入表写入外部函数的真实地址。我们可以利用这个特性
具体步骤:
- 调试器附加: 附加到目标进程
- 查找
IMAGE_IMPORT_DESCRIPTOR
: 使用静态分析工具(如 PE Explorer、CFF Explorer)找到 PE 文件中导入表的RVA
(Relative Virtual Address) - 计算内存地址: 将
RVA
加上基址(ImageBase)得到导入表在内存中的实际地址 - 设置硬件断点: 在导入表的第一个条目上设置一个硬件写入断点(Hardware Write Breakpoint)
- 运行程序: 运行程序。当加载器填充导入表时,断点会被触发。这通常发生在
LoadLibrary
函数调用之后,但在main
函数之前 - 分析内存: 断点触发后,你就可以检查内存中的导入表,它的内容已经被加载器填充好了。如果程序进行了脱壳,此时的导入表才是真实的
另一种高级变体是:
- 在程序执行早期,在整个
.text
段(代码段)设置一个硬件写入断点。当断点触发时,检查写入的地址是否在代码段内部,并分析写入的指令。这可以用来检测自修改代码,是更高级的逆向技术