最近在看《黑卡免杀攻防》,对讲解的PE文件导入表、导出表的作用与原理有了更深刻的理解,特此记录。
首先,要知道什么是导入表?
导入表机制是PE文件从其他第三方程序(一般是DLL动态链接库)中导入API,以提供本程序调用的机制。而在Windows平台下,PE文件中的导入表结构就承担了完成这一工作的引导者角色。
IMAGE_IMPORT_DESCRIPTOR结构
typedef 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,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
一般来说,对于导入表,我们只需要关注它的两个字段,分别是OriginalFirstThunk与FirstThunk,这两个字段分别指向了包含导出名称和导出地址的IMAGE_THUNK_DATA结构数组,这个数组以空的IMAGE_THUNK_DATA结构结尾。
IMAGE_THUNK_DATA结构
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString; // 转发字符串的RAV
PDWORD Function; // 被导入函数的地址
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData; // 指向输入名称表
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
ForwarderString是转发用的,暂时不用考虑,Function表示函数地址,如果是按序号导入Ordinal就有用了,若是按名字导入AddressOfData便指向名字信息。可以看出这个结构体就是一个大的union,大家都知道union虽包含多个域但是在不同时刻代表不同的意义那到底应该是名字还是序号,该如何区分呢?可以通过Ordinal判断,如果Ordinal的最高位是1,就是按序号导入的,这时候,低16位就是导入序号,如果最高位是0,则AddressOfData是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,用来保存名字信息,由于Ordinal和AddressOfData实际上是同一个内存空间,所以AddressOfData其实只有低31位可以表示RVA,但是一个PE文件不可能超过2G,所以最高位永远为0,这样设计很合理的利用了空间。实际编写代码的时候微软提供两个宏定义处理序号导入:IMAGE_SNAP_BY_ORDINAL判断是否按序号导入,IMAGE_ORDINAL用来获取导入序号。
了解一下IMAGE_IMPORT_BY_NAME结构。
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
Hint: 保存着需要导入函数的序号
Name: 保存着需要导入函数的名称
知道了基本的组成,那么导入表又是怎样尽职尽责的工作呢?
记录一个实例流程。
借助LordPE工具,快速定位导入表的位置。
Importtable的RVA地址是0x0000233C;那么它的文件OFFSET是多少?
首先查看各个段的RVA和OFFSET:
显然,Importtable在.rdata区段
.rdata区段的RVA为0x2000,.rdata区段的起始Offset是0x00001000
RVA到Offset的转化:
导出表Offset = 导出表RVA - 导出表所在区段的RVA + 导出表所在区段的 Offset
求得导出表Offset = 0x0000133c
导出表地址计算参照值 d = 导出表RVA - 导出表Offset = 0x00001000 (便于计算各个字段的Offset)
得到Offset就可以在winhex里面找到importtable的内容了:
由于是以空的结构体结尾的,很清楚的看到是有三个IMAGE_IMPORT_DESCRIPTOR结构:
TABLE1:
字段 | OriginalFirstThunk | TimeDataStamp | ForwarderChain | Name | FirstThunk |
值(RVA) | 0x0000238c | 0x00000000 | 0x00000000 | 0x000023dc | 0x00002000 |
转化为Offset | 0x0000138c | 0x000013dc | 0x00001000 |
TABLE2:
字段 | OriginalFirstThunk | TimeDataStamp | ForwarderChain | Name | FirstThunk |
值(RVA) | 0x00002450 | 0x00000000 | 0x00000000 | 0x000024f8 | 0x000020c4 |
转化为Offset | 0x00001450 | 0x000013dc | 0x000010c4 |
TABLE3:
字段 | OriginalFirstThunk | TimeDataStamp | ForwarderChain | Name | FirstThunk |
值(RVA) | 0x000023cc | 0x00000000 | 0x00000000 | 0x0000252e | 0x00002040 |
转化为Offset | 0x000013cc | 0x0000152e | 0x00001040 |
由这些信息(INT、IAT、映像名的起始Offset);就可以找它们的具体结构了
映像名、INT、IAT
NAME | OriginalFirstThunk1 | OriginalFirstThunk2 | FirstThunk1 | FirstThunk2 | |
KERNEL32.dll | 0x00002458 | 0x00002466 | 0x00002458 | 0x00002466 | |
USER32.dll | 0x000024ea | NULL | 0x000024ea | NULL | |
MSVCR110.dll | 0x00002558 | 0x00002568 | 0x00002558 | 0x00002568 |
根据上述的导入信息,转化为Offset;再从文件中找到导入函数和倒入序号:
NAME | 序号1 | 函数名1 | 序号2 | 函数名2 | |
KERNEL32.dll | 0x016d | ExitProcess | 0x0223 | GetCurrentProcess | |
USER32.dll | 0X010a | FindWindowW | NULL | NULL | |
MSVCR110.dll | 0x01a4 | __getmainorgs | 0x01e0 | __set_app_type |
总结:
导入表的工作原理步骤:
1、根据IMANE_IMPORT_DESCRIPTOR的字段(NAME,OriginalFirstThunk,FirstThunk),来找到映像名、INT、IAT;
2、根据INT表的信息(OriginalFirstThunk字段),找到_IMAGE_IMPORT_BY_NAME结构的位置,从而得出函数的函数名、函数序号。