PE文件格式(输入表)

华盟原创文章投稿奖励计划

可执行文件使用来自其他DLL的代码或数据的动作作为输入。例如当exe程序弹出一个消息框时,是通过调用User32.dll的MessageBoxA(或MessageBoxW)函数来实现的

Win32 API是区分字符集的,两个版本:ANSI版本与Unicode版本
输入表是一个类型为IMAGE_IMPORT_DESCRIPTOR(简称IID)的数组,每一个DLL都对应有一个IID结构,由于没有字段指明这个IID数组的长度,所以通过在末尾添加一个空的IID结构来表示数组的结束

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics; 
        DWORD   OriginalFirstThunk;         // RVA,指向输入名称表(INT),INT是一个类型为IMAGE_THUNK_DATA结构的数组,同样的,通过在数组末尾附加一个空的IMAGE_THUNK_DATA结构来表示数组的结束,每一个输入的函数都有一个对应的IMAGE_THUNK_DATA结构
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;         
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;                                                        // 以“00”结尾的ASCII字符的RVA,指向DLL的名字,如“User32.dll”
    DWORD   FirstThunk;                     // 指向输入地址表(IAT)的RVA,IAT也是一个类型为IMAGE_THUNK_DATA结构的数组,同样的,通过在数组末尾附加一个空的IMAGE_THUNK_DATA结构来表示数组的结束,每一个输入的函数都有一个对应的IMAGE_THUNK_DATA结构
} IMAGE_IMPORT_DESCRIPTOR;

OriginalFirstThunk与FirstThunk极其相似,前者指向INT,后者指向IAT

PE文件格式(输入表) 

INT和IAT都是类型为IMAGE_THUNK_DATA的数组,两个数组的结束都是有一个值为0的IMAGE_THUNK_DATA元素表示

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // 指向一个转向者字符串的RVA
        DWORD Function;             // 被输入的函数的内存地址
        DWORD Ordinal;                                // 被输入的API的序列值
        DWORD AddressOfData;        // 指向 PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;

该类型仅有的一个成员u1,是一个联合体(union),联合体内的类型都是DWORD,所以IMAGE_THUNK_DATA的大小是4字节
每个IMAGE_THUNK_DATA元素对应于一个从可执行文件输入的函数
当IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号的方式进行输入,这时候低31位(或一个64位可执行文件的低63位)的值就表示函数的序号;当该类型的最高位为0时,表示函数以字符串类型的函数名方式输入,这时候值就表示一个指向IMAGE_IMPORT_BY_NAME结构的RVA

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

Hint:表示函数在其原始DLL文件的输出表中的序号。该域被PE装载器用来在DLL的输出表里快速查询函数。该值不是必须的
Name:含有输入函数的函数名。以“NULL”字符结尾。这是一个可变的大小
OriginalFirstThunk指向的INT数组和FirstThunk指向的IAT数组到底有什么区别
当PE文件被加载时,PE加载器会遍历INT结构中的数组项,通过其指向的IMAGE_IMPORT_BY_NAME结构来找到函数的名字,PE加载器可以通过函数的名字找到函数的地址,随后把得到的函数地址填充到IAT结构中,此后,通过IAT结构中的函数地址就可以进行函数调用了

PE文件格式(输入表) 

PE装载器把导入函数输入至IAT的顺序

1、        读取IID的Name成员,获取库名称字符串(“Kernel32.dll”)
2、        装载相应库,LoadLibrary("Kernel32.dll")
3、        读取IID的OriginalFirstThunk成员,获取INT地址
4、        读取INT数组中的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)
5、        使用IMAGE_IMPORT_BY_NAME的Hint或Name项,获取相应函数的起始地址
6、        读取IID的FirstThunk(IAT)成员,获得IAT地址
7、        将上面获得的函数地址输入相应的IAT数组值
8、        重复4-7步骤,直到INT结束(遇到NULL时)

IMAGE_OPTIONAL_HEADER32 ——> DataDirectory ——> VirtualAddress的值即是IMAGE_IMPORT_DESCRIPTOR结构体数组的起始地址(RVA值)
以某文件为例,查看IMAGE_OPTIONAL_HEADER32.DataDirectory结构体的值;第一个4字节为虚拟地址,第二个4字节为Size成员

PE文件格式(输入表) 

因为RVA是2524,故文件偏移为1724,计算方法参考上面的:RAW = RVA - VirtualAddress + PointerToRawData
IID的大小为20字节

PE文件格式(输入表) 

用工具计算验证一下

PE文件格式(输入表) 

PE文件格式(输入表) 

将上面得到的输入表第一个IID结构的数据与IID结构体的成员对应

IMAGE_IMPORT_DESCRIPTOR {
    OriginalFirstThunk;  = 00 00 25 F8
    TimeDateStamp;   = 00 00 00 00
    ForwarderChain;  = 00 00 00 00
    Name;  =  00 00 26 A8
    FirstThunk;  = 00 00 20 34
}

Name是一个字符串指针,它指向导入函数所属的库文件名称(RVA:26 A8 ——> RAW:18A8)

PE文件格式(输入表) 

可以看到VCRUNTIME140.dll字符串

PE文件格式(输入表) 

OriginalFirstThunk -- INT,包含导入函数信息的结构体指针数组

(RVA:25F8 ——> RAW:17F8),

每个地址值分别指向IMAGE_IMPORT_BY_NAME结构体

PE文件格式(输入表) 

跟踪第一个值2684(RVA),计算参照上面方法,可以看到导入的API函数的名称字符串

PE文件格式(输入表) 

0x0048为Ordinal,是库中函数固有编号,后面为函数名称字符串,以00结尾

PE文件格式(输入表) 

本文原创,作者:congtou,其版权均为华盟网所有。如需转载,请注明出处:https://www.77169.net/html/271834.html

发表评论