PE结构:导入/导出表
导入导出表是PE文件中重要的两张资源表,这两张表会在PE文件执行是被加载进入内存,RVA在映像可选头部的数据目录表中可以查询。
一般会被放在.rdata(资源数据段)中
EXPORT 导出表
DLL链接库文件中的函数有两种导出方式:按名字与按序号
导出表格式
EXPORT {
DWORD Export flags; //保留,必须为0
DWORD Time/Data_Stamp //导出表创建的时间戳
WORD MajorVersion //主版本号,可以被用户设置
WORD MInorVersion //从版本号
DWORD NameRVA //指向DLL名字的RVA,如“kernel32.dll”
DWORD Base //序号基址
DWORD AddressTableEntries //地址表项个数
DWORD NumberOfNames //函数名个数
**DWORD Export_Address_Table_RVA //地址表入口**
**DWORD Name_Pointer_RVA //名字表RVA**
**DWORD Odinals_Table //名字序号表RVA**
}
导出表中重要的表为后三个,当函数以序号导出时,序号 - BASE即为地址表索引,可以直接查询到函数入口
当函数以名字导出时,查询顺序额为名字表 -> 名字序号表 -> 地址表;
以函数名在名字表中的索引为名字序号表的索引,查到名字序号表中的整数,再以这个整数为索引查地址表,找到函数入口
讲起来可能有点复杂,其实举例说明就非常简单:
查询函数B的过程:
- 名字表中有四个函数名 A B C D,查询到下标为1
- 序号表中有四个整数 3 0 1 2,下标1对应的整数为0
- 地址表中有四个地址 F0 F4 F8 FC, 下标0对应整数为F0
- 所以B函数入口地址为F0
IMPORT 导入表
struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; *// 0 for terminating null import descriptor*
DWORD OriginalFirstThunk; *// RVA to original unbound IAT (PIMAGE_THUNK_DATA)* }
DUMMYUNIONNAME;
DWORD TimeDateStamp; *// 0 if not bound,*
DWORD ForwarderChain; *// -1 if no forwarders*
DWORD Name;
DWORD FirstThunk; *// RVA to IAT (if bound this IAT has actual addresses)*
} ;
导入项表项中有几个非常关键的字段:
OriginalFirstThunk导入查找表的RVA,指向一个IMAGE_THUNK_DATA数组,这里为2000H
TimeDataStamp 时间戳,用于映像绑定技术,绑定前为0,绑定后为dll的时间戳
ForwardChain 转发链,如果没有转发器,值为-1
Name 指向导入模块名字的RVA,这里指向了“kernel32.dll”这一字符串
FirstThunk RVA,也指向一个IMAGE_THUNK_DATA数组,
IMAGE_THUNK_DATA(也就是大家说的Thunk)到底是什么呢?
其定义如下:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
可见thunk其实是一个32位的联合结构,ForwarderString用于转发,可以先不理会;Function是一个函数地址,这个字段不会在被加载前用到,我们待会聊。重点我们来看Ordinal和AddressOfData字段。
这个32位结构中,如果最高位为1,那么低16位表示函数的导入序号,此时Ordinal字段生效。如果最高位是0,那么这个结构是一个RVA,此时AddressOfData字段生效,指向IMAGE_IMPORT_BY_NAME结构,这个结构具体定义如下:
IMAGE_IMPORT_BY_NAME{
DWORD Hint;
char[n] Name;
}
Name字段很好理解,为导入的函数名。在执行exe文件时,加载器需要将导入的dll中相应的函数也加载进入内存中。
Hint字段是这个函数在dll导出表中的索引,当然,我们在知道函数名的情况下其实就已经能够将相应的函数加载进内存了,hint可以让加载器的工作更快一些。
当hint出错(通过hint在导出表中找到的函数名与Name不匹配时),函数将继续用名字导入。
总结:Thunk可能是一个序号,可能是一个指向函数名结构体的RVA。
我们之前提到的两个指针OriginalFirstThunk(OFT)与FirstThunk(FT),他们都为2000H,指向同一个thunk数组。一般来说我们将OFT指向的表称为INT(Import Name Table)。在加载之后,OFT指针指向的内容不会发生改变,但FT指针指向的内容却会被改写为具体的函数地址,此时thunk数组的function字段生效。这时FT指向的内容就是著名的IAT(Import Address Table).
IAT表的建立:双桥结构
在PE装载的过程中,IAT的建立是非常重要的任务。
导出表在这个过程中起到的作用是指引和参考,加载器首先会查询OFT指向的Thunk数组中的序号或名字,Thunk中的最高位表明了这个函数是以什么方式导出的。用序号或名字去导出表中按上述的规则进行查询,得到函数的RVA,之后将这个RVA填到FT指向的Thunk数组中。这个过程完成后,FT指向的表就是IAT. 导入表中通过的OFT与FT两个指针与导出表发生联系,所以这个结构也被形象地称为双桥结构。
一个经典的疑问是,为什么我们需要双桥,如果我们只用FT,并且在找到正确的函数入口后直接覆盖不就可以了?
-
PE程序执行中依然需要函数名的信息,而不是只需要知道它的入口就可以了
-
**“导入绑定”**技术可以帮助加载器跳过去导出表查找对应函数入口的步骤,而直接将真实地址填到IAT中。但这存在着风险,如果dll在未预期的位置加载或者dll进行了安全更新,绑定技术将失效,这时OFT指向的表将是你找到函数入口的唯一途径。