0%

导入表的获取

导入表

百度百科:


Import Address Table 由于导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL 中.当PE 文件被装入内存的时候,Windows 装载器才将DLL 装入,并将调用导入函数的指令和函数实际所处的地址联系起来(动态连接),这操作就需要导入表完成.其中导入地址表就指示函数实际地址。


其实简单的说就是你这个程序用了哪些DLL,用了这个DLL的名称和DLL地址是在哪个地方,都在这里有说明。

获取导入表

首先是GetModuleHandle这个函数,以前用的时候只知道返回一个句柄,但现在看来好像不是那么简单,这个句柄应该就是一个地址,就是程序的一个起始地址(不是函数的入口点,就是开头是MZ的那个,下面会说到)。
通过这个函数我们获得内存中镜像的一个地址之后,我们就可以获取IAT(导出表)了,通过PE结构。
首先还是要看下比较重要的几个结构体。

IMAGE_DOS_HEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

这个之前博客说过,就是用来获取PE头文件的,简单看下就好。

IMAGE_OPTIONAL_HEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;

//
// NT additional fields.
//

DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

这里我这个是Win32的所以,宏定义我就到这里来了。里面包含的就是一些模块的、代码的的基地址,还有些线程堆栈之类的一些信息。

IMAGE_IMPORT_DESCRIPTOR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

这个就是比较重要的导出表关键结构体。

  1. OriginalFirstThunk:(函数序号名称)表的偏移量,导入函数的名称
  2. Name:导入模块名称字符串的偏移量
  3. FirstThunk:IAT导入表的一个偏移量,导入函数的地址

相应地址的推导

1
2
3
4
5
HMODULE hMod = GetModuleHandle(NULL);
IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER *)hMod;
IMAGE_OPTIONAL_HEADER * pOptHeader = (IMAGE_OPTIONAL_HEADER *)((BYTE *)hMod + pDosHeader->e_lfanew + 24);
IMAGE_IMPORT_DESCRIPTOR * pImportDesc= (IMAGE_IMPORT_DESCRIPTOR *)((BYTE *)hMod +
pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

这个东西其实就是这个样子搞出来的,24是由于:NT签名(PE00)占四个字节,后面20个是IMAGE_FILE_HEADER这个结构体,之前说过的。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "stdafx.h"
#include <stdio.h>

int _tmain(int argc, _TCHAR* argv[])
{
HMODULE hMod = GetModuleHandle(NULL);
IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER *)hMod;
IMAGE_OPTIONAL_HEADER * pOptHeader = (IMAGE_OPTIONAL_HEADER *)((BYTE *)hMod + pDosHeader->e_lfanew + 24);
IMAGE_IMPORT_DESCRIPTOR * pImportDesc= (IMAGE_IMPORT_DESCRIPTOR *)((BYTE *)hMod +
pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
printf("程序入口:%x",hMod);
while (pImportDesc->FirstThunk)
{
char * pszDllName = (char * )((BYTE * )hMod + pImportDesc->Name);
printf("\n 模块名称: %s \n",pszDllName);
IMAGE_THUNK_DATA * pThunk = (IMAGE_THUNK_DATA * )((BYTE*)hMod + pImportDesc->OriginalFirstThunk);
int n = 0;
while(pThunk->u1.Function)
{
char * pszFunName = (char *)((BYTE*)hMod + (DWORD)pThunk->u1.AddressOfData+2);//前两个是函数的序号
PDWORD lpAddr = (DWORD * )((BYTE*)hMod + pImportDesc->FirstThunk)+n;
printf(" 导出函数:%-25s",pszFunName);
printf("函数地址:%X\n",lpAddr);
n++;pThunk++;
}
pImportDesc++;
}
system("pause");
return 0;
}

其实这个IAT就是一个DWORD类型的数组,这里需要注意的是,++这个操作符适用于增加一个结构大小的在这里,然后这个+n也是增加结构体大小,基础有说过。

程序运行结构:
运行结果

分析结构

我们用OD载入,注意的是可能OD有提示代码的一个问题,不用管就好。
首先我们打印那个GetModuleHandle返回的结构对应的值,我们也能够dd查看对应的ASCII码,会看到如下:
起始地址
起始可以发现,就是文件的一个签名开头罢了。

然后我们来验证函数的一个正确性,还是用dd进行查看:
导出表

我们代码的编写是没有什么问题的,这个时候我们既然有了这个地址,就好办了,我们就可以偷梁换柱,将我们自己的函数给替换上去。

HOOK函数

这个东西很有用,但是针对于自身程序的话呢还是有一定限制,并且最后我还是没有研究出原因,之后慢慢分析。
我们对MessageBox进行Hook替换,其实原理也不是很高深,就是对我们的导入表函数地址进行改写,但在这里我踩了不少坑。
首先我们先来编写这个获取某个函数的地址的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
BOOL SetHook(LPCTSTR ModuleName)
{
HMODULE hMod = GetModuleHandle(ModuleName);
IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER *)hMod;
IMAGE_OPTIONAL_HEADER * pOptHeader = (IMAGE_OPTIONAL_HEADER *)((BYTE *)hMod + pDosHeader->e_lfanew + 24);
IMAGE_IMPORT_DESCRIPTOR * pImportDesc= (IMAGE_IMPORT_DESCRIPTOR *)((BYTE *)hMod +
pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (pImportDesc->FirstThunk)
{
char * pszDllName = (char * )((BYTE * )hMod + pImportDesc->Name);
if (lstrcmp(pszDllName,"USER32.dll") == 0)
{
break;
}
pImportDesc++;
}
if (pImportDesc->FirstThunk)
{
IMAGE_THUNK_DATA * pThunk = (IMAGE_THUNK_DATA * )((BYTE*)hMod + pImportDesc->FirstThunk);
while(pThunk->u1.Function)
{
PDWORD lpAddr = (DWORD *)&(pThunk->u1.Function);
if (*lpAddr == (DWORD)g_orgProc)
{
PDWORD lpNewFun = (PDWORD)MyMessageBox;
DWORD dwWrite;
/*修改页面保护属性*/
DWORD dwOldProect;
VirtualProtect(lpAddr,sizeof(DWORD),PAGE_READWRITE,&dwOldProect);
WriteProcessMemory(GetCurrentProcess(),lpAddr,&lpNewFun,sizeof(DWORD),&dwWrite);
VirtualProtect(lpAddr,sizeof(DWORD),dwOldProect,0);
if (dwWrite == sizeof(DWORD))
return TRUE;
else
return FALSE;

}
pThunk++;
}
}
return FALSE;

}

参数是我们要修改的模块名称,我们填写NULL的话呢我们就是修改我们自身,和上面的代码一样,我们显示寻找IAT,当找到USER.dll的时候我们跳出,这个时候要注意了,这个是大写,不是小写,xp是小写,我们这个是大写。跳出之后我们在继续寻找函数,寻找函数我们不能用名字,我们这里用函数的地址,所以我们需要先声明MessageBox的地址是多少:

1
PROC g_orgProc = (PROC)MessageBox;//原始的信息框地址

这个样子我们就获取到了MessageBox的函数地址。进行比较,找到之后我们将这个内存地址的值修改为我们的函数的内存地址,这里需要注意的是,使用WriteProcess函数的时候我们在Release版本下面的时候我们需要进行内存权限的修改,就在这里除了问题有可能,找到之后替换我们的函数,由于要求堆栈的平衡,所以函数我们要完全按格式来写:
首先先定义函数:

1
typedef int (WINAPI *PUNMESSAGEBOX)(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);

这句话你就是将PUNMESSAGEBOX这个东西定义成了一个MessageBox。
然后下面写我们的MessageBox:

1
2
3
4
int WINAPI MyMessageBox(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType)
{
return ((PUNMESSAGEBOX)g_orgProc)(hWnd,"New Fun","HOOK",uType);
}

其实就是将原来的函数执行,要执行g_orgProc,我们要进行强制转换,转换称上面宏定义的内容,我们改为我们的函数。
这个样子我们的程序就算编写完毕:
完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include "stdafx.h"
#include <stdio.h>

BOOL SetHook(LPCTSTR ModuleName);

typedef int (WINAPI *PUNMESSAGEBOX)(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);
PROC g_orgProc = (PROC)MessageBox;//原始的信息框地址

int _tmain(int argc, _TCHAR* argv[])
{
MessageBox(NULL,"Org Fun","Test",MB_OK);

//if(!SetHook(NULL))
// return -1;
SetHook(NULL);
MessageBox(NULL,"Org Fun","Test",MB_OK);
system("pause");
return 0;
}

int WINAPI MyMessageBox(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType)
{
return ((PUNMESSAGEBOX)g_orgProc)(hWnd,"New Fun","HOOK",uType);
}

BOOL SetHook(LPCTSTR ModuleName)
{
HMODULE hMod = GetModuleHandle(ModuleName);
IMAGE_DOS_HEADER * pDosHeader = (IMAGE_DOS_HEADER *)hMod;
IMAGE_OPTIONAL_HEADER * pOptHeader = (IMAGE_OPTIONAL_HEADER *)((BYTE *)hMod + pDosHeader->e_lfanew + 24);
IMAGE_IMPORT_DESCRIPTOR * pImportDesc= (IMAGE_IMPORT_DESCRIPTOR *)((BYTE *)hMod +
pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (pImportDesc->FirstThunk)
{
char * pszDllName = (char * )((BYTE * )hMod + pImportDesc->Name);
if (lstrcmp(pszDllName,"USER32.dll") == 0)
{
break;
}
pImportDesc++;
}
if (pImportDesc->FirstThunk)
{
IMAGE_THUNK_DATA * pThunk = (IMAGE_THUNK_DATA * )((BYTE*)hMod + pImportDesc->FirstThunk);
while(pThunk->u1.Function)
{
PDWORD lpAddr = (DWORD *)&(pThunk->u1.Function);
if (*lpAddr == (DWORD)g_orgProc)
{
PDWORD lpNewFun = (PDWORD)MyMessageBox;
DWORD dwWrite;
/*修改页面保护属性*/
DWORD dwOldProect;
VirtualProtect(lpAddr,sizeof(DWORD),PAGE_READWRITE,&dwOldProect);
WriteProcessMemory(GetCurrentProcess(),lpAddr,&lpNewFun,sizeof(DWORD),&dwWrite);
VirtualProtect(lpAddr,sizeof(DWORD),dwOldProect,0);
if (dwWrite == sizeof(DWORD))
return TRUE;
else
return FALSE;

}
pThunk++;
}
}
return FALSE;

}

在这里还需要再说几句,PDWORD lpAddr = (DWORD *)&(pThunk->u1.Function);这句话就是获取我们保存MessageBox地址的地址,也就是说这个是个间接寻址,所以我们要用到取地址,然再用这个地址的值去做比较,那为什么不直接用值呢。因为我们WriteProcessMemory写入的时候是需要用到这个地址的。
其实这个东西是有点乱,我现在看着也有点乱,而且你要想记住是有一些难度的,这个东西就是要用的时候去查就可以了。

看下运行结果:
运行结果

错误

这个地方在我们Release版本下面我实在在代码层找不到原因为什么替换之后还是执行之前的,我用OD分析了一下,虽然不愿因深入去看,但是我却知道了原因,看下面OD的内容:
奇怪的地方
首先我们看到,我们的MessageBox函数地址换了,系统领空到了我们程序的领空,但是,大家注意,程序突然就call了ESI,ESI又是之前MessageBox,这个就让我很奇怪,我严重怀疑是编译器的问题!!!