PE3-27 IAT表&&导入表
IAT:Import Address Table 导入地址表
间接寻址调用
调用 DLL 函数时,汇编代码通过 间接寻址 实现,不直接
call
函数地址,而是通过一个中间地址跳转。示例:调用
MessageBox
系统函数(属于 DLL)时,汇编代码为:1
call dword ptr [004322d4]
004322d4
中存储地址X
,程序通过跳转到X
执行函数。004322d4
属于.exe
的内存区域,而X
可以指向 DLL 的内存区域(如77d5050b
)。
为什么需要间接寻址?
- DLL 在加载时可能因重定位占用不同的内存地址。
- 因此,调用 DLL 函数不能写死地址,而需要动态更新。
- 程序先将
.exe
中某个位置(如004322d4
)写入代码,然后由操作系统在运行时填充实际的函数地址。
文件状态与加载状态的区别
- 未运行(文件状态):
004322d4
指向的地址仅存储一个字符串,例如MessageBox.USER32.dll
。- 需要根据 RVA(相对虚拟地址)转换为 FOA(文件偏移地址),再减去 ImageBase 才能找到位置。
- 运行中(内存状态):
.exe
和.dll
加载到内存时,操作系统修复了重定位表。004322d4
的内容被替换为77d5050b
(DLL 函数的绝对地址)。
IAT 的作用
- IAT(Import Address Table,导入地址表)存储
.dll
和函数信息的中间地址。 - 加载完成后,IAT 中的值从字符串变为实际的绝对地址。
总结
- IAT 是由存放 DLL 函数信息的中间地址组成的表。
- 文件加载前,IAT 存储的内容是函数名的字符串地址。
- 文件加载后,IAT 被系统动态更新为函数的绝对地址,用于实现动态链接调用。
导入表
导出表和导入表的作用
- 导出表:
- 提供给外部使用的函数清单,相当于饭店的菜单。
- 用于告诉其他程序 DLL 中有哪些可用的函数。
- 导入表:
- 程序使用的 DLL 函数清单,相当于客人点的菜品单。
- 记录
.exe
所需使用的 DLL 和其中的函数。
- 数据目录:
- 数据目录的第一项为导出表。
- 数据目录的第二项为导入表。
导入表结构
1 | typedef struct _IMAGE_IMPORT_DESCRIPTOR |
定位导入表
VirtualAddress
指向多个导入表结构,每个结构对应一个 DLL。- 导入表以
sizeOf(IMAGE_IMPORT_DESCRIPTOR)
个全0结构结尾,表示结束。 Name
指向 DLL 名称的 RVA。- OriginalFirstThunk 指向 INT 表(Import Name Table)。
- FirstThunk 指向 IAT 表(Import Address Table)。
INT 表和 IAT 表:
- INT 表:在文件加载前存储函数信息(如 RVA 指向的函数名)。
- IAT 表:文件加载完成后,存储 DLL 函数的实际内存地址。
- 虽然内容在加载前一致,但它们位于不同的内存区域。
IMAGE_IMPORT_DESCRIPTOR 的解析
- OriginalFirstThunk:
指向 INT 表(存储IMAGE_THUNK_DATA
结构数组)。 - FirstThunk:
指向 IAT 表(存储IMAGE_THUNK_DATA
结构数组)。找到这张表有两种方式,一种就是通过导入表这里找到,第二种就是通过数据目录表,倒数第三个项就是指向的 IAT 表。 - INT 表和 IAT 表的初始化:
- 加载前,两个表内容相同,均指向函数名或序号。
- 加载后,系统调用
GetProcAddress
更新 IAT 表为函数的绝对地址。
IMAGE_THUNK_DATA 结构
1 | typedef struct _IMAGE_THUNK_DATA32 |
加载前:
- 存储一个 RVA,指向
IMAGE_IMPORT_BY_NAME
。 - 如果是序号调用,则存储序号。
- 存储一个 RVA,指向
加载后:
- 更新为函数的绝对地址。
IMAGE_IMPORT_BY_NAME 结构
1 | typedef struct _IMAGE_IMPORT_BY_NAME { |
Hint
:导出表中函数地址表的索引,可为 0。Name
:变长数组,用于存储以'\0'
结尾的函数名。
变长数组的使用:
结构中定义为BYTE Name[1]
,但在内存中动态分配实际长度以存储函数名,以优化内存使用。在这个结构中,BYTE Name[1]
使用的技巧是所谓的 “结构体变长数组”,这种技巧通常用于存储可变长度的数据。尽管声明为BYTE Name[1]
,实际上这个字段可以容纳一个以 null 终止的字符串,而字符串的长度可以是任意的。这是因为在PE文件的导入表中,函数名称并不是固定长度的,每个函数名称的长度都不同。因此,为了有效地存储这些不定长度的字符串,使用了变长数组的技巧。结构体只定义了一个字节的数组,但在实际使用时,根据函数名称的长度动态分配所需的内存,然后将函数名称存储在这个内存块中,以 null 终止字符串。
这种方法允许节省内存,因为不需要为每个结构分配固定大小的缓冲区以容纳字符串,而可以根据实际需要进行动态分配。在C/C++中,这种技巧在很多情况下用于处理可变长度的数据,例如字符串数组,以提高内存使用效率。在实际使用时,程序员通常会动态分配足够的内存以存储实际的字符串,然后将字符串内容复制到该内存中,以确保字符串正确存储和 null 终止。
导入表的加载过程
- 系统加载文件,将
.exe
和 DLL 放入内存。 - 修复重定位表,将所有相关地址调整到内存实际位置。
- 遍历 INT 表,根据函数名或序号调用
GetProcAddress
,获取函数的实际地址并填入 IAT 表。
定位导入表的具体步骤
- 第一层循环:
遍历所有导入表(IMAGE_IMPORT_DESCRIPTOR
),以全零结构为结束标志。 - 第二层循环:
遍历OriginalFirstThunk
指向的 INT 表:- INT 表中的每个表项为
IMAGE_THUNK_DATA32
结构。 - 表项值为 0 时,表示结束。
- 表项内容可能是函数序号或函数名偏移(需进一步解析)。
- INT 表中的每个表项为
Work:打印导入表
示例代码
1 |
|
运行结果
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Hexo!