一、引入IAT表
1.调用自己写的函数
1)在内存中(ImageBuffer)
-
编写一个C程序,调用自己写好的函数
#include "stdafx.h" void my_method(){ int a = 1; a++; printf("%d",a); } int main(int argc, char* argv[]){ my_method(); return 0; } -
可以通过OD查看程序再运行时的数据,也可以直接在VC上运行查看反汇编(这里使用VC查看方便一些)。发现:这里调用函数时,call后面跟的是0xFFFF3B7D(前面说过硬编码为E8的call后面跟的地址是相对地址:0x40100a - (0x40D488 + 5) = 0xFFFF3B7D),即实际上call的是0x40100a这个地址;再按F11跳到0x40100a,发现这是一个跳板:
jmp 0x401010(硬编码是E9 01 00 00 00),最终0x401010就是自己写的函数开始的地址
2)在硬盘上(FileBuffer)
-
先通过PETool查看程序的ImageBase和文件、内存对齐粒度,可以算出0x40100a对应的文件偏移地址为0x100a
-
接着我们用Winhex打开程序,查看一下程序未运行时0x100a地址处和运行时跳板处硬编码一样:都是
E9 01 00 00 00,那么jmp真正要跳转的地址为:0x00000001 + 0x0000100F = 0x00001010(跟E8后面算地址的方法一样)
-
最后看一下0x1010地址处刚好就是自己写的函数的硬编码
3)结论
-
一个程序调用了自己写的函数:无论运行前还是运行时,call后面的地址已经“写死”,就是一个跳板的绝对地址,而跳板跳的就是自己写的函数地址;故程序在编译后,call后面的地址就写死了
-
敢写死是因为,自己写的函数存储地址都是在程序本身的.exe中,而程序运行时,.exe装载到程序的虚拟内存中是不会有别的PE文件和它争抢位置的,所以.exe会按照程序的ImageBase正常装载,进而这些写死的地址值就是有效的
和调用系统DLL中的函数结论做一个对比
2.调用DLL中的函数
1)在内存中(ImageBuffer)
-
MessageBox()函数位于user32.dll中,属于系统提供的API -
现在编写一个C程序,调用
MessageBox()函数;#include "stdafx.h" #include <windows.h> int main(int argc, char* argv[]){ MessageBox(0,0,0,0); ExitProcess(0); //这是一个退出进程函数,也是系统API return 0; } -
编译成Release版后,使用OD打开这个.exe程序,找到程序OEP,回车进去(前面学过,一般情况下,找三个连续的push,后面这个call就是程序入口)
-
找到程序调用
MessageBox()函数这里,发现call后面跟的是一个间接寻址:[0040509C],即真正call的实际上是0x40509C地址中的值!所以我们输入db 40509C,去看看0x40509C这个地址中存的什么:发现存的是MessageBox函数的内存中的绝对地址:0x77D36476,这个函数地址不是.exe程序本身的地址,而是使用的一个DLL中的地址,我们可以再打开工具栏的E查看一下小提示:这里call的硬编码是FF15(不是E8),FF15的call后面跟的地址是内存的绝对地址(E8的是相对地址)
2)在硬盘上(FileBuffer)
-
现在我们看看0x40509C内存绝对地址转换成程序没运行时的地址,这个地址存的是什么?所以首先需要先用PETools查看程序的ImageBase和文件、内存偏移粒度,将0x40509C转化成文件地址为:0 + (0x40509C - 0x400000) = 0x509C
-
接着打开Winhex查看程序的0x509C地址中存的是:0x00005538,这是一个文件==偏移地址==
-
最后查看0x5538地址中存的是:MessageBoxA的函数名称字符串的ASCII码。会发现这里有很多函数名字符串!(这里就是函数名称表
IMAGE_IMPORT_BY_NAME)
3)结论并引入IAT表
-
发现一个使用了系统DLL函数的程序,在调用DLL函数时都使用了间接寻址,但是
- 在运行时:
call [0x40509C],0x40509C地址存储的是0x77D36476(这是MessageBox()函数的内存绝对地址),即实际上call的是0x77D36476 - 运行前:
call [0x509C],0x509C地址存储的是0x5538(这里没有直接写MessageBox函数的内存绝对地址),即实际上call的是0x5538
- 在运行时:
-
所以使用了系统DLL函数的程序,编译后(运行前)并没有直接把函数在DLL中的绝对地址(DLL的ImageBase + RVA = 0x77D36476)写到call后面,而是写了程序自己的.exe空间中的一个文件偏移地址(0x5538),这个文件偏移地址指向了此程序使用的DLL中的函数名称
- 一个程序会使用很多别的DLL中的函数,所以==运行前:会有很多个上述的文件偏移地址,这些指向了函数名称的文件偏移地址就构成了IAT表!==
-
但是程序运行时,程序先装载自己的.exe到程序的虚拟内存中,再依次装载使用到的DLL到程序的虚拟内存中,此时DLL在虚拟内存中的位置已经固定了,所以操作系统会把call后面的值改为函数在DLL中的绝对地址(此时DLL的ImageBase + RVA)
- 运行时:操作系统会把IAT表中的文件偏移地址改成此时对应函数的绝对地址,即运行后==IAT表中的值就全变成了使用的DLL函数在内存中的绝对地址==
注意:如果发生了DLL装载地址冲突,DLL会往后找空闲内存存放,此时操作系统会先修正重定位表,接着把修正后的函数在DLL中的绝对地址放到call后面
-
所以IAT表的内容在程序执行前和执行后是不一样的!
4)问题
- 那为什么编译时不直接把MessageBox函数在DLL中的绝对地址放到call后面?
- 就是因为day37.1-重定位表中学过的:MessageBox所在的DLL在装载时并不一定会按照其指定的ImageBase装载,可能会出现DLL装载地址冲突,导致该DLL最终装载的地址和其ImageBase不一致!那么所有写死的绝对地址都失效了!
- 但是我们自己写的函数在程序的.exe中,而一个程序运行时它本身的.exe装载地址就是自己的ImageBase,一般不会有其他的PE文件和它抢占,所以直接把函数的内存绝对地址放到call后面即可
3.易错误区
-
调用函数的地址问题和修复重定位表关系:二者没有任何关系!!!!
-
程序调用DLL的函数:编译时,call后面的地址值不会写死,而是间接寻址,指向一个DLL中函数名字字符串的地方;但是在运行时,会直接把此时DLL装载到内存后的函数绝对地址写到call后面(间接指向)!所以这里修改的是程序自己的.exe中调用函数的call语句中的地址值
-
而重定位表修复:修复的地址是DLL中的地址,而不会像上述一样还会帮使用自己函数的程序修复程序调用函数时call后面的地址!所以重定位表修复的是DLL自身当中的一些地址(比如DLL中的函数绝对地址、DLL中的全局变量等),因为可能会遇到DLL装载时没有按照指定的ImageBase正常装载,所以要修改DLL自身中写死的地址
二、导入表
1.导入表是什么
-
前面的IAT表,运行前和运行时的内容是怎么确定下来的,会在导入表的讲解中迎刃而解,所以导入表和IAT表有很大的关系
-
导入表:即一个PE文件(.dll/.exe等),它如果需要使用使用别的PE文件的函数,就需要一个清单来记录使用的别的PE文件的函数相关信息(使用了哪些DLL、使用了这个DLL里的哪些函数、叫什么名、去哪找等)
-
所以一般而言:程序通过导入表中的信息来装载DLL到虚拟内存中(比如通过导入表的所有Name成员,知道要装载哪些DLL;或者如果导入表的某个结构中OriginalFirstThunk和FirstThunk都为0,系统就不会加载这个导入表结构对应的DLL,因为操作系统知道你没有使用这个DLL中的任何函数)
上面内容复习的时候再看。程序需要导入表中的一些信息来加载DLL,但修复导入表中的某些信息是在DLL装载完成后(比如修复IAT表),所以这些都是有过程的,不要混为一谈
-
.exe一般没有导出表、有导入表;.dll一般既有导出表、又有导入表(因为一个.dll可能也需要使用别的.dll提供的函数,且.dll也会提供函数给别的PE文件用)
2.定位导入表
-
一个PE文件的导入表数据目录在PE文件第二个数据目录结构体
-
再根据导入表数据目录的VirtualAddress成员找到导出表(RVA转FOA)
3.导入表结构
-
导入表中的结构:
struct _IMAGE_IMPORT_DESCRIPTOR{ union{ DWORD Characteristics; DWORD OriginalFirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组(INT表) }; DWORD TimeDateStamp; //时间戳 (用于判断是否有绑定导入表/IAT表中是否已经绑定绝对地址) DWORD ForwarderChain; DWORD Name; //RVA,指向dll名字字符串存储地址 DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组(IAT表) }; //导入表有很多个这种结构(成员全为0,表示结束) -
注意:导入表不可能就一个这种结构,因为一个PE文件可能会使用多个DLL中的多个函数,一个这样的结构只表示一个DLL中的函数;所以PE文件使用了多少个DLL中的函数,就有多少个这种结构
-
当遇到有
sizeof(_IMAGE_IMPORT_DESCRIPTOR)个0x00时,表示导出表结束(相当于一个导入表结构中的成员全为0x00000000,就表示导入表结束) -
由于IAT表在程序运行前后是不一样的,所以导入表和IAT表的具体关系结构图示如下:
-
PE文件运行前:
-
PE文件运行后:
-
1)OriginalFirstThunk
- RVA,指向INT表(导入名称表),如果想得到内存地址,即用ImageBase + RVA即可;如果想得到文件地址,即把RVA转FOA即可
2)Name
- RVA,指向DLL名字字符串所在地址;比如Name指向的DLL名称为“user32.dll\0”,那么这个结构中记录的就是user32.dll中函数的相关信息;所以一个PE文件的导入表有多少中这种结构,就使用了多少个其他DLL中的函数
3)FirstThunk
- RVA,指向IAT表(导入地址表)
4)TimeDataStamp
时间戳字段看不懂没事,详情在day40.1-绑定导入表
- 为0(即0x00000000):表示这个导入表结构对应的DLL中的函数绝对地址没有绑定到IAT表中
- 为**-1**(即0xFFFFFFFF):表示这个导入表结构对应的DLL中函数绝对地址已经绑定到IAT

文章详细阐述了PE文件(如.exe和.dll)中导入表(ImportTable)的作用,包括IAT(ImportAddressTable)和INT(ImportNameTable)表的结构和功能。在程序运行前,调用DLL函数时,call指令后面的地址是间接寻址,指向INT表中的函数名称或序号;运行后,IAT表会被填充为DLL中函数的实际内存地址。文章还介绍了如何通过结构体解析PE文件的导入表信息,以及不同程序在运行前后IAT表的变化情况,强调了导入表在动态链接过程中的关键角色。
最低0.47元/天 解锁文章
3394

被折叠的 条评论
为什么被折叠?



