输入表结构如下:
在数据成员目录的第二项可以找到输入表的offset
输入表其实是一个IMAGE_IMPORT_DESCRIPTOR结构数组,看下Winnt.h中的详细定义
typedef struct _IMAGE_IMPORT_DESCRIPTOR { // 0 for terminating null import descriptor
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
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;
每一个DLL都对应于一个IMAGE_IMPORT_DESCRIPTOR(输入描述符),以一个全0的输入描述符作为结束标志。
第一项用到的其实只是OriginalFirstThunk,它指向一个IMAGE_THUNK_DATA结构数组,每一个IMAGE_THUNK_DATA又与该PE文件调用的每一个输入函数有关系,给出IMAGE_THUNK_DATA结构。
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
IMAGE_THUNK_DATA是一个DWORD大小的一个联合,它在不同的时候有着不同的含义,跟IMAGE_IMPORT_DESCRIPTOR类似,以一个全0的IMAGE_IMPORT_DESCRIPTOR结束。
ForwarderString是转向它的第一个索引的函数的名称的RVA(不懂Forwarder的可以搜一下中转函数,了解一下,我本人在DLL劫持中用过)
Function代表着输入函数的地址,这个之后再讲。
Ordinal代表该函数在导入的DLL里的序号,不过至今很少用了。只有当IMAGE_THUNK_DATA的最高位为1时才代表使用序号导入,此时低31位代表在导入DLL中的该函数的序号。
AddressOfData,是一个指向IMAGE_IMPORT_BY_NAME的一个指针,它表示用函数名进行导入。当IMAGE_THUNK_DATA的最高位为0时代表使用函数名进行导入,此时这个四字节代表着该IMAGE_IMPORT_BY_NAME的RVA。
IMAGE_IMPORT_BY_NAME的结构如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
由一个word类型和一个可变长的ASCII字符串组成,因为字符串,即导入函数的函数名长度不定,所以这里用一个BYTE类型粗略代替。
Hint用于被PE装载器快速查找导入函数的位置,但该值可有可无。
Name数组是该导入函数的函数名,是一个以NULL结尾的ASCII字符串。
觉得抽象的根据最上面的那副图结合着理解。
TimeDateStamp是一个时间戳,我们不研究这个。
ForwardChain,这是第一个被转向的API的索引,我们不研究这个。
Name,指向该DLL的名字的以一个以0结尾的ASCII字符串的RVA地址。
FirstThunk,这是一个非常重要的一个域,它跟OriginalFirstThunk一样也指向一个IMAGE_THUNK_DATA结构数组。只所以有这样一个并行数组是因为FirstThunk域才是PE加载器加载PE文件到内存后所用到的真正的域,PE加载器会根据OriginalFirstThunk指向的函数进行迭代搜索,将导入函数的实际地址填入到FirstThunk指向的地址去,然后PE文件仅有FirstThunk这个域就可以实现调用导入函数的功能了。
随便找一个PE文件,我们用IDA打开看一下PE文件是如何利用导入表进行调用的
这里有一个外部函数,相当于一个jmp跳到了put例程里去了
然后这里第一条指令又是一个jmp指令跳到了导入表里,而这个jmp呢,其实是跳到了FirstThunk指向的内存里的puts函数的地址,类似于假如FirstThunk里存储的是0x2000,那么跳转的时候就是jmp [ImageBase+0x2000+ord(__imp_puts) * sizeof(DWORD) ],ord是puts的序号,有兴趣的可以再去od动态调试看看,你会发现你跳到的那一大片值就是一堆函数指针的地方。
结构图类似于这样:
ImportTable的offset是0x2040,跳到这里,记得转换成文件偏移,我们来实战分析下第一个DLL:
第一个IMAGE_IMPORT_DESCRIPTOR
TimeDateStamp为0,ForwarderChain也为0,Name函数名的RVA是0x2174,这里是user32.dll
OriginalFirstThunk指向0x208C的RVA地址,换算一下,是0x48c。
值为0x2110,说明是用函数名导入的,继续换算,找到了下面的地址
hint是019B,函数名是LoadIconA
查找其余函数
这里就不详细分析了,直接列出
RVA 文件偏移 Hint 函数名
211C 51C 01DD PostQuitMessage
20F4 4F4 0128 GetMessageA
20E0 4E0 0094 DispatchMessageA
2150 550 027D TranslateMessage
2164 564 028B UpdateWindow
2102 502 0197 LoadCursorA
20CE 4CE 0083 DefWindowProcA
20BC 4BC 0058 CreateWindowsExA
212E 52E 01EF RegisterClassExA
2142 542 0265 ShowWindow
可以推测下导出表的这些函数,很像建立一个窗口并且分发消息的固有流程,首先RegisterClassExA,然后CreateWindows,接着ShowWindow,然后DispatchMessage分发相应的消息,所以有时候我们对恶意代码进行静态分析就可以先从导入表入手。