内容:
从EXE文件的运行过程中分析导入表和IAT表
一个最简单的API HOOK程序
关于绑定
从EXE文件的运行过程中分析导入表和IAT表
样本程序代码:
#include <stdio.h>
#include <windows.h>
void func() {
printf("func/n");
}
int main() {
printf("hello world/n");
func();
MessageBoxA(NULL, "ok", "ok", MB_OK);
return 0;
}
编译选项使用VC++6.0生成一个空的cosole project默认。
在pebrowse中调用生成的ApiHookTest.exe可执行文件。我们在mainCRTStartup代码中直接定位到main函数的调用,单步进入main函数的执行。main函数中的printf是为了看C运行库函数的调用,func是为了看来自本程序定义的函数的调用,MessageBoxA是为了看来自user32.dll中的函数调用。
main函数是通过一个JMP跳转到函数体执行的,不管它,直接进入main的函数体。执行printf函数的调用:
Disassembly of ApiHookTest.exe!@ILT+0(_main) (0x00401005)
0x401088: 6828204200 PUSH 'hello world|' ; (0x422028)
0x40108D: E85E000000 CALL printf <Int> ; (0x4010F0)
0x401092: 83C404 ADD ESP,0x4
“helloworld “字符串的地址是00422028,图 1是pebrowse的运行截图,其中分析了ApiHookTest.exe文件的各种数据结构的起始地址。比较地址可以清楚地看到,”hello world”字符串存放在文件的.rdata区块中。如下:
0x00422028
***`string'***
0x00422028 68 65 6C 6C hell
0x0042202C 6F 20 77 6F o wo +04
0x00422030 72 6C 64 0A rld. +08
0x00422034 00 00 00 00 .... +0C
图 1 pebrowse运行截图
printf函数的地址在004010F0处,这个地址处存放着printf函数体的地址。如下:
Disassembly of ApiHookTest.exe!printf (0x004010F0)
; Section: .text
;= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
; SYM:printf <Int>
; printf.c - Line 47
0x4010F0: 55 PUSH EBP
0x4010F1: 8BEC MOV EBP,ESP
printf函数的代码是嵌入到可执行文件中的。执行完printf,接着往下看func函数的调用,这是程序中自己定义的函数:
Disassembly of ApiHookTest.exe!main (0x00401070)
0x401095: E870FFFFFF CALL func <Void> ; (0x40100A) ; @ILT+5(_func)
单步进入,程序执行到了0040100A处的JMP语句上:
0x40100A: E911000000 JMP func <Void> ; (0x401020); (*+0x16)
最终的地址00401020处是func函数体的地址。很奇怪,func函数是自己程序中定义的函数,func这个符号是能找到它的定义的,那为什么这种函数还要采用这样的JMP方式来调用呢?为什么不能像printf的调用一样直接一次CALL指令就跳到函数体上去执行呢?而且这种JMP不像hook.c中调用MessageBoxA那样的JMP,这种JMP的操作码是E9,而调用MessageBoxA的JMP是直接寻址,它的操作码是FF,25。E9是JMP NEAR的操作码,这的功能是:(EIP)=(EIP)+32位偏移量或(IP)=(IP)+16位偏移量。执行到上述0040100A处的时候,该处JMP指令五个字节,所以EIP的值为0040100F,当前指令码为E911000000,所以偏移量为00000011,所以执行JMP后EIP=0040100F+00000011==00401020,这里就是func函数的函数体。对于程序中自己定义的函数的调用,使用的是JMP的相对寻址。对于从外部DLL导入的函数的调用,使用的是JMP直接寻址。包括main函数的调用也都是和func函数一样的调用方式。func这个函数名符号是指向JMP指令的,而不是指向func函数体的。
接下来执行完func,执行到MessageBoxA函数:
Disassembly of ApiHookTest.exe!main (0x00401070)
0x40109A: 8BF4 MOV ESI,ESP
0x40109C: 6A00 PUSH 0x0
0x40109E: 6824204200 PUSH 'ok' ; (0x422024)
0x4010A3: 6824204200 PUSH 'ok' ; (0x422024)
0x4010A8: 6A00 PUSH 0x0
0x4010AA: FF15ACA24200 CALL DWORD PTR [USER32.DLL!MessageBoxA]; (0x42A2AC)
0x4010B0: 3BF4 CMP ESI,ESP
0x4010B2: E8B9000000 CALL __chkesp ; (0x401170)
MessageBoxA函数的执行前后有几句特殊的代码,在将参数压入栈前,有一句:
MOV ESI, ESP
执行完后,有:
CMP ESI, ESP
CALL __chkesp
很显然,这些都是为了防止栈被破坏的。直接看调用MessageBoxA的那句:
CALL DWORD PTR [0X42A2AC]
从图 1可以看出来,这个地址(0042A2AC)是位于.idata区块中的IAT表中的一个地址。现在我们来解决第一个疑问,IAT表和导入表之间是什么关系。
从可选头部的数据目录表中看到,导入表的RVA是0002A000,IAT表的RVA是0002A18C,这两个地址加上程序的加载地址00400000后,分别是0042A000和0042A18C。从图 1中来看,位于.idata区块内的Import表和IAT表的地址正是这两个地址。如下所示:
DataDirectory[0] - IMAGE_DIRECTORY_ENTRY_EXPORT
(+0x60) VirtualAddress: 0x00000000
(+0x64) Size: 0x00000000
DataDirectory[1] - IMAGE_DIRECTORY_ENTRY_IMPORT
(+0x68) VirtualAddress: 0x0002A000
(+0x6C) Size: 0x0000003C
DataDirectory[2] - IMAGE_DIRECTORY_ENTRY_RESOURCE
(+0x70) VirtualAddress: 0x00000000
(+0x74) Size: 0x00000000
DataDirectory[3] - IMAGE_DIRECTORY_ENTRY_EXCEPTION
(+0x78) VirtualAddress: 0x00000000
(+0x7C) Size: 0x00000000
DataDirectory[4] - IMAGE_DIRECTORY_ENTRY_SECURITY
(+0x80) VirtualAddress: 0x00000000
(+0x84) Size: 0x00000000
DataDirectory[5] - IMAGE_DIRECTORY_ENTRY_BASERELOC
(+0x88) VirtualAddress: 0x0002B000
(+0x8C) Size: 0x00000B18
DataDirectory[6] - IMAGE_DIRECTORY_ENTRY_DEBUG
(+0x90) VirtualAddress: 0x00022000
(+0x94) Size: 0x0000001C
DataDirectory[7] - IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
(+0x98) VirtualAddress: 0x00000000
(+0x9C) Size: 0x00000000
DataDirectory[8] - IMAGE_DIRECTORY_ENTRY_GLOBALPTR
(+0xA0) VirtualAddress: 0x00000000
(+0xA4) Size: 0x00000000
DataDirectory[9] - IMAGE_DIRECTORY_ENTRY_TLS
(+0xA8) VirtualAddress: 0x00000000
(+0xAC) Size: 0x00000000
DataDirectory[10] - IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
(+0xB0) VirtualAddress: 0x00000000
(+0xB4) Size: 0x00000000
DataDirectory[11] - IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
(+0xB8) VirtualAddress: 0x00000000
(+0xBC) Size: 0x00000000
DataDirectory[12] - IMAGE_DIRECTORY_ENTRY_IAT
(+0xC0) VirtualAddress: 0x0002A18C
(+0xC4) Size: 0x00000150
DataDirectory[13] - IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
(+0xC8) VirtualAddress: 0x00000000
(+0xCC) Size: 0x00000000
DataDirectory[14] - IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR (aka IMAGE_COR20_HEADER)
(+0xD0) VirtualAddress: 0x00000000
(+0xD4) Size: 0x00000000
DataDirectory[15]
(+0xD8) VirtualAddress: 0x00000000
(+0xDC) Size: 0x00000000
导入表的格式是非常清楚的。在数据目录表的第二项(索引为1)中的RVA所指向的地址处是一个IMAGE_IMPORT_DESCRIPTOR数据结构。该数据结构定义如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk;// RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
这个结构定义在winnt.h中。其中最重要的两项是第一项OriginalFirstThunk和最后一项FirstThunk。这两个都是RVA,分别指向两个元素为IMAGE_THUNK_DATA的数组。它们所指向的两个数组是完全一样的。IMAGE_THUNK_DATA在winnt.h中的定义如下:
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
IMAGE_THUNK_DATA是一个DWORD。它有四种解释:ForwarderString,Function,Ordinal,AddressOfData。当它的最高位为1时,它的低16位是导入函数的序数(即此时它作Ordinal解释),当它的最高位为0时,它是一个指向IMAGE_IMPORT_BY_NAME结构的RVA。如果它作一个指向IMAGE_IMPORT_BY_NAME结构的RVA解释时,它所指向的那个地方就是一个IMAGE_IMPORT_BY_NAME结构,结构的前两个字节是导入函数的序数,紧随其后的是一个ASCII字符串,表示导入函数的名字。上面已经知道,导入表的RVA是0002A000,加上程序的加载地址0040000后是0042A000,我们查看这个地址处的内容:
0x0042A000
***__IMPORT_DESCRIPTOR_USER32***
0x0042A000 0002A15C .../ ;OriginalFirstThunk
0x0042A004 00000000 .... +04 ;TimeDateStamp
0x0042A008 00000000 .... +08 ;ForwarderString
0x0042A00C 0002A2EA .... +0C ;Name:User32.dll
0x0042A010 0002A2AC .... +10 ;FirstThunk
***__IMPORT_DESCRIPTOR_KERNEL32***
0x0042A014 0002A03C ...< +14
0x0042A018 00000000 .... +18
0x0042A01C 00000000 .... +1C
0x0042A020 0002A680 .... +20 ;Name:Kernel32.dll
0x0042A024 0002A18C .... +24
***__NULL_IMPORT_DESCRIPTOR***
0x0042A028 00000000 .... +28
0x0042A02C 00000000 .... +2C
0x0042A030 00000000 .... +30
0x0042A034 00000000 .... +34
0x0042A038 00000000 .... +38
0x0042A03C 0002A4EC .... +3C
注意到上面的kernel32.dll的FirstThunk值,它的值就是IAT表的RVA。在可选头部的数据目录表中索引为12处单独有一项IMAGE_DIRECTORY_ENTRY_IAT,现在我们知道了它实际上就是导入表中的FirstThunk所指向的数组的集合。每一个导入的DLL都会有一个IMAGE_IMPORT_DESCRIPTRO数据结构对应,这些数据结构的FirstThunk指针所指向的数组就是IAT表的起始地址,或者是IAT表中的地址。以上面的User32.dll的IMAGE_IMPORT_DESCRIPTOR数据结构来说,它的OriginalFirstThunk和FirstThunk分别是0002A15C和0002A2AC,我们分别查看0042A15C和0042A2AC地址处的内容:
0x0042A15C 0002A2DC ....
到0042A2DC处查看:
0x0042A2DC BE 01 4D 65 73 73 61 67 ..Messag
0x0042A2E4 65 42 6F 78 41 00 55 53 eBoxA.US +08
很明显,0042A15C处是一个IMAGE_THUNK_DATA,它指向一个IMAGE_IMPORT_BY_NAME,所指向的IMAGE_IMPORT_BY_NAME前两个字节是01BE,即函数序数号。后面是函数名:MessageBoxA。而0042A2AC处的内容:
+0x0042A2AC 0B 05 D5 77 ...w USER32.dll!MessageBoxA
这个地方的内容是77D5050B,是MessageBoxA函数体的地址。本程序中对MessageBoxA函数的调用
CALL DWORD PTR [0X42A2AC]
使用的正是这个IAT中的地址。这里也验证了各种讲解PE文件格式的文章中说的, OriginalFirstThunk是一个备份,当可执行文件被加载器加载到内存后,加载器会修改FirstThunk所指向的数组,这个时候FirstThunk所指向的数组元素不再是IMAGE_THUNK_DATA了,而是导入函数的实际内存地址。一个最简单的API HOOK程序
明白了这些东西,现在来写一个最简单的API HOOK,它的功能是找到MessageBoxA在IAT中的地址并将这个地址处的内容替换成自己程序中定义的一个函数的地址(实际上是一条JMP指令的地址)。程序代码如下:
#include <windows.h>
#include <string.h>
PROC lpMessageBoxA = 0; // 保存原来的MessageBoxA的地址
int WINAPI func(HWND hWnd, LPCTSTR text, LPCTSTR caption, UINT flag) {
if(lpMessageBoxA) {
lpMessageBoxA(NULL, "func", "func", MB_OK);
return lpMessageBoxA;
}
return 0;
}
int main() {
int *addr = MessageBoxA, *original = NULL, *iat = NULL, i;
// get module handle
HMODULE hModule = GetModuleHandle(NULL);
PIMAGE_DOS_HEADER pIdh = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS pInh = (PIMAGE_NT_HEADERS)((char*)pIdh + pIdh->e_lfanew);
PIMAGE_IMPORT_DESCRIPTOR pIid
= (PIMAGE_IMPORT_DESCRIPTOR)(pInh->OptionalHeader.ImageBase
+ pInh->OptionalHeader.DataDirectory[1].VirtualAddress);
// find MessageBoxA
while(pIid->OriginalFirstThunk != 0) {
original = pInh->OptionalHeader.ImageBase + (DWORD)pIid->OriginalFirstThunk;
for(i = 0; original[i] != 0; i++) {
if(strcmp("MessageBoxA",
(const char*)(&(((PIMAGE_IMPORT_BY_NAME)(pInh->OptionalHeader.ImageBase + original[i]))->Name))) == 0) {
goto finish;
}
}
pIid++;
}
finish:
if(pIid->OriginalFirstThunk) {
iat = (int*)(pInh->OptionalHeader.ImageBase + (DWORD)pIid->FirstThunk);
lpMessageBoxA = iat[i];
iat[i] = func;
}
MessageBoxA(NULL, "main", "main", MB_OK);
return 0;
}
程序的思路很简单,就是取得IMAGE_IMPORT_DESCRIPTOR结构的地址,然后在每个IMAGE_IMPORT_DESCRIPTOR的OriginalFirstThunk中查找MessageBoxA字符串,找到后,记录下索引,则该IMAGE_IMPORT_DESCRIPTRO中的FirstThunk对应索引处就存放着MessageBoxA的实际函数体地址,首先把原来的地址保存起来,然后把它替换成程序中定义的func函数的地址。这样,下面的程序中调用MessageBoxA的时候实际上调用的是func函数。func函数声明成与MessageBoxA一样的原型是为了平衡堆栈。如果知道堆栈的情况,也不用一定要那样写。比如,下面的代码也能运行:
__declspec(naked) void f() {
__asm pushad
if(lpMessageBoxA) {
lpMessageBoxA(NULL, "this is f", "ok", MB_OK);
__asm {
popad
ret 10h
}
}
}
在上面的程序中把iat[i] = func;一句改成 iat[i] = f;运行正常。关于绑定
现在来看绑定的影响。在命令行下执行bind命令:
D:/My Documents/My Projects/ApiHookTest/Debug>bind -o -u -v -y ApiHookTest.exe
BIND: binding ApiHookTest.exe using DllPath Default
BIND: ApiHookTest.exe - Imports from USER32.dll
BIND: ApiHookTest.exe - MessageBoxA Bound to 77d5050b
BIND: ApiHookTest.exe - Imports from KERNEL32.dll
BIND: ApiHookTest.exe - GetVersionExA Bound to 7c812851
BIND: ApiHookTest.exe - CloseHandle Bound to 7c809b77
BIND: ApiHookTest.exe - GetCommandLineA Bound to 7c812c8d
BIND: ApiHookTest.exe - GetVersion Bound to 7c8114ab
BIND: ApiHookTest.exe - ExitProcess Bound to 7c81caa2
BIND: ApiHookTest.exe - DebugBreak Bound to 7c859956
BIND: ApiHookTest.exe - GetStdHandle Bound to 7c812ca9
BIND: ApiHookTest.exe - WriteFile Bound to 7c810f9f
BIND: ApiHookTest.exe - InterlockedDecrement Bound to 7c809794
BIND: ApiHookTest.exe - OutputDebugStringA Bound to 7c859b5c
BIND: ApiHookTest.exe - GetProcAddress Bound to 7c80ac28
BIND: ApiHookTest.exe - LoadLibraryA Bound to 7c801d77
BIND: ApiHookTest.exe - InterlockedIncrement Bound to 7c80977b
BIND: ApiHookTest.exe - GetModuleFileNameA Bound to 7c80b357
BIND: ApiHookTest.exe - TerminateProcess Bound to 7c801e16
BIND: ApiHookTest.exe - GetCurrentProcess Bound to 7c80e00d
BIND: ApiHookTest.exe - UnhandledExceptionFilter Bound to 7c862b8a
BIND: ApiHookTest.exe - FreeEnvironmentStringsA Bound to 7c81dc3f
BIND: ApiHookTest.exe - FreeEnvironmentStringsW Bound to 7c81485f
BIND: ApiHookTest.exe - WideCharToMultiByte Bound to 7c80a0c7
BIND: ApiHookTest.exe - GetEnvironmentStrings Bound to 7c81cc23
BIND: ApiHookTest.exe - GetEnvironmentStringsW Bound to 7c812c78
BIND: ApiHookTest.exe - SetHandleCount Bound to 7c80c6cf
BIND: ApiHookTest.exe - GetFileType Bound to 7c811069
BIND: ApiHookTest.exe - GetStartupInfoA Bound to 7c801eee
BIND: ApiHookTest.exe - GetModuleHandleA Bound to 7c80b529
BIND: ApiHookTest.exe - GetEnvironmentVariableA Bound to 7c81486a
BIND: ApiHookTest.exe - HeapDestroy Bound to 7c811110
BIND: ApiHookTest.exe - HeapCreate Bound to 7c812929
BIND: ApiHookTest.exe - Forwarder HeapFree not snapped [7c80906e]
BIND: ApiHookTest.exe - VirtualFree Bound to 7c809b14
BIND: ApiHookTest.exe - Forwarder RtlUnwind not snapped [7c809208]
BIND: ApiHookTest.exe - IsBadWritePtr Bound to 7c809f29
BIND: ApiHookTest.exe - IsBadReadPtr Bound to 7c809eb3
BIND: ApiHookTest.exe - HeapValidate Bound to 7c85e79b
BIND: ApiHookTest.exe - Forwarder GetLastError not snapped [7c80903d]
BIND: ApiHookTest.exe - SetConsoleCtrlHandler Bound to 7c81b25b
BIND: ApiHookTest.exe - GetCPInfo Bound to 7c812be6
BIND: ApiHookTest.exe - GetACP Bound to 7c809943
BIND: ApiHookTest.exe - GetOEMCP Bound to 7c81e82a
BIND: ApiHookTest.exe - Forwarder HeapAlloc not snapped [7c809058]
BIND: ApiHookTest.exe - VirtualAlloc Bound to 7c809a81
BIND: ApiHookTest.exe - Forwarder HeapReAlloc not snapped [7c809080]
BIND: ApiHookTest.exe - FlushFileBuffers Bound to 7c80cd58
BIND: ApiHookTest.exe - SetFilePointer Bound to 7c810da6
BIND: ApiHookTest.exe - MultiByteToWideChar Bound to 7c809cad
BIND: ApiHookTest.exe - LCMapStringA Bound to 7c832e2b
BIND: ApiHookTest.exe - LCMapStringW Bound to 7c80cec4
BIND: ApiHookTest.exe - GetStringTypeA Bound to 7c838cb9
BIND: ApiHookTest.exe - GetStringTypeW Bound to 7c80a480
BIND: ApiHookTest.exe - SetStdHandle Bound to 7c81d8cb
BIND: binding ApiHookTest.exe
绑定对于程序代码没有任何影响。通过绑定可以把导入表的地址写入IAT中,也就是写入IMAGE_IMPORT_DESCRIPTOR的FirstThunk所指向的数组中。这样,加载器就会省掉修改这个数组的工作。程序启动的会更快一些。
因为在windows的内存管理机制下,每个进程有独立的地址空间。所以进程启动后各个模块的内存分布通常不会发生改变。所以,无论这个DLL是系统的还是用户自定义的,在外界情况不变的情况下绑定一般不会失效。