- Q1:函数真实地址?
- Q2:函数调用和真实地址之间桥梁?
- Q3:PE文件记录DLL信息?
- 1.数据结构
- part 1.整体分析
- part 2.局部分析
- 2.数据结构总结
- 3. 加载流程
- Q4:INT和IAT为什么同时存在?
- 参考
正常开发的程序一般会用到微软或者第三方提供的DLL中的函数,会涉及到2个相对的概念(导入函数和导出函数);下面以notepad.exe打开文件一定会调用的 KERNEL32!CreateFileA为例进行说明
- 导出函数:CreateFileA是KERNEL32.dll提供给其他人使用的函数,相对于KERNEL32.dll,它就是导出函数
- 导入函数:notepad.exe中打开文件最终会调用CreateFileA,CreateFileA相对于notepad.exe就是导入函数
提前总结:输入表中保存的是函数名及所属的DLL名等动态链接的信息;对于磁盘上的PE文件,是不知道要使用的其他DLL的(导出)函数在内存中的真实地址;只有PE文件被装载器加入内存后(相关dll也会被一起加载),才会将调用输入函数的指令和函数在内存中实际地址进行关联
提示:输入表中最关键的是IAT
通过几个问题介绍输入表的原理
Q1:函数真实地址?详细:引用的其他模块的导入函数在内存中的真实地址,是什么时候被PE文件知道的呢?
是在PE文件被加载到内存的时候,此时Windows加载器会将相关的DLL载入(隐式链接),将需要的外部函数的真实地址填写到自己的IAT中,并将调用输入函数的指令和函数在内存中实际地址进行关联
这就引出了2种链接方式:
- 隐式链接DLL:PE加载器加载PE文件进入内存时,会将PE文件使用的外部API函数所属的DLL进行隐式加载(类似LoadLibrary + GetProcAddress的代码被执行),间接依赖的DLL也会被加载
- 显示链接DLL:LoadLibrary(加载DLL) + GetProcAddress(寻找需要的API地址)
先提前说一下,是输入表中的IAT(输入地址表,Import Address Table),后面会有详细的介绍;下面展示一下最常用的函数调用形式
可以看到IsDebuggerPresent函数不是直接被调用的,而是通过了0x6D6088来实现的中转,转到kernelbase.dll中真实的IsDebuggerPresent函数地址入口开始执行
- 桥梁:0x6D6088就属于IAT的内容,更直观的可以简单理解成内存区域的下面这个部分是IAT,是函数调用和真实函数入口之间的桥梁
- 建议:使用OD时,当然不想每次函数调用看到的都是一些地址,设置:Options -> Debugging options -> Disasm下的Show symbolic address勾选上,有如下更直观的显示
为什么不直接使用内存地址呢,用IAT做桥梁不影响效率吗?使用IAT有如下几点考虑
-
1.兼容性:利用TCP中只要解决不了兼容性,就加一层的套路
源程序是不知道自己会运行在哪个平台的,即使Windows就有xp、win7、vista等很多版本;因此只能使用IAT的方式,执行文件时,PE装载器将IsDebuggerPresent函数的地址写到0x6D6088即可
-
2.应对DLL的重定向:后面会有详细介绍
-
3.PE头中标识的地址不使用VA,都是RVA
1.数据结构详细:磁盘中的PE文件是怎么记录每个dll的详细信息的?
结合数据结构,从整体和局部分析来介绍PE是怎么记录DLL中有用数据的
part 1.整体分析注意:下面介绍的很枯燥,如果不研究逆向和脱壳可以不用看
PE文件的可选头中的DataDirectory[1]指向输入表(地址和大小)
一个DLL对应一个IMAGE_IMPORT_DESCRIPTOR(IID)结构体,即一个DLL的所有信息(DLL名称和一组函数指针)都在这个结构体里,CFF截图如下:
一组函数指针被称为输入地址表(import address table,IAT),每一个被引入的函数都有一个位置;一旦模块载入,IAT就包含每个函数的入口地址了
相关函数:LoadLibrary负责加载DLL
//一个DLL对应一个IMAGE_IMPORT_DESCRIPTOR结构
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; //0 for terminating null import descriptor
DWORD OriginalFirstThunk; //输入名称表(INT),RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIOnNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; //DLL名字的指针,是一个以“00”结尾的ASCII字符的RVA地址
DWORD FirstThunk; //RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
part 2.局部分析
简单说明,DLL里函数名称被存放在IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk中,即INT(输入名称表,Import Name Table)
notepad.exe被加载完成后,它的IAT(输入地址表,Import Address Table)里面包含所有需要函数的真实内存地址
相关函数:GetProcAddress负责在DLL中找函数的基址
//DLL中的一个函数对应一个IMAGE_THUNK_DATA
typedef IMAGE_THUNK_DATA32 IMAGE_THUNK_DATA;
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; //PBYTE
DWORD Function; //PDWORD,被输入的函数的内存地址
DWORD Ordinal; //被输入API的序号
DWORD AddressOfData; //PIMAGE_IMPORT_BY_NAME构
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
//IMAGE_THUNK_DATA最高位:
// 是1,函数以序号方式输入,后面位数代表函数序号
// 是0,函数以名称方式输入,DWORD整体是一个RVA,指向_IMAGE_IMPORT_BY_NAME结构
//OriginalFirstThunk和FirstThunk指向相同的数组IMAGE_THUNK_DATA
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //本函数在其驻留的DLL的输出表中的序号,非必须
CHAR Name[1]; //指向输入函数名的ASCII的首地址,NULL结尾(定义的是可变长度)
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
2.数据结构总结
一般情况下,PE文件中OriginalFirstThunk(指向INT)和FirstThunk(指向IAT)本质上指向相同的数组IMAGE_THUNK_DATA结构
有几点共性总结如下:
-
一个DLL对应一个IMAGE_IMPORT_DESCRIPTOR结构,一个函数对应一个IMAGE_THUNK_DATA结构
-
结束标志是空结构体
-
IMAGE_IMPORT_DESCRIPTOR结构体:大小是0x14 bytes,因此结束标志是5个00 00 00 00
-
IMAGE_THUNK_DATA结构体:大小是4 bytes,因此结束标志是00 00 00 00
#WinDbg计算结构体的大小(这个结构体是可变大小的),算出的大小是4 bytes 0:000> ?? sizeof(combase!_IMAGE_IMPORT_BY_NAME) unsigned int 4
-
-
Name的意义?
- IMAGE_IMPORT_DESCRIPTOR结构体:DLL名字的指针,是一个以“00”结尾的ASCII字符的RVA地址
- IMAGE_THUNK_DATA结构体:指向输入函数名的ASCII的首地址,NULL结尾(定义的是可变长度)
下图是PE可选头的DataDirectory[1](输入表)索引到IAT(输入地址表)经过的主要数据结构的示意图(IMAGE_THUNK_DATA最高位是0的场景),详细的数据结构可以参看WinNT.h
提示:第一次看这个图,如果看不懂没关系,只要简单理解输入表的整个数据处理流程就是沿着红色线条进行流动就可以了
完整的IAT加载流程总结如下:
- 1.读取IID的Name成员,获取库名称的字符串(“kernelbase.dll”)
- 2.装载相应库,类似LoadLibrary("kernelbase.dll"),后续操作获取的地址就是内存中的地址了
- 3.读取IID的OriginalFirstThunk成员,获取INT的地址;且遍历INT中数组的值获取相应的IMAGE_IMPORT_BY_NAME地址(RVA)
- 4.根据IMAGE_IMPORT_BY_NAME的Hint或者Name项,获取相应函数的起始地址;类似GetProcAddress("IsDebuggerPresent")
- 5.获取IID中FirstThunk(IAT)成员,获取IAT的地址,将步骤4获取的地址更新到IAT相应位置中;不断更新直到INT被遍历完
详细:为什么要定义2个指针(INT和IAT)指向一样的结构?
- OriginalFirstThunk(指向INT):不可以更改,有时也称为提示名表(Hint-name table)
- FirstThunk(指向IAT):Windows装载器要进行重写
实际情况,OriginalFirstThunk和FirstThunk指向的数组里面的内容都是一样的,简单的IAT更新流程:
- 1.程序在初始化时,根据OriginalFirstThunk找到函数名
- 2.调用类似GetProcAddress(OriginalFirstThunk找到函数名)找到函数的入口地址
- 3.将这个入口地址取代掉FirstThunk指向的地址字符串中的对应值
#汇编中 jmp dword ptr 地址 ;这里的 地址 就是FirstThunk(指向IAT)数组中的地址
通过几个问题,总结了输入表存在的依据和PE文件加载流程中IAT的填充逻辑,这些都是逆向和脱壳中的基础知识;网上有很多介绍的文章,但是还是建议自己整理一份笔记,方便自己对知识细节的掌握
参考- 1.《加密与解密》第11章 PE文件格式
- 2.《逆向工程核心原理》第13章 PE文件格式



