首页
论坛
课程
招聘
[原创]Win PE系列之导入表解析与IAT Hook技术
2021-10-11 10:43 3057

[原创]Win PE系列之导入表解析与IAT Hook技术

2021-10-11 10:43
3057

一.RVA,FOA与VA

在对PE文件进行解析的过程中,经常需要对这三类地址进行转换。

  • RVA:相对虚拟内存地址,指的是文件加载到内存中以后相对于起始地址的偏移。

  • FOA:文件偏移地址,指的是文件保存在磁盘中时候,相对于起始地址的偏移。

  • VA:虚拟内存地址,文件在内存中的真实地址。

依然是昨天的这张图,可以看到,由于FileAlignment和SectionAlignment的不同,节区中的地址相对于起始地址偏移是不同的。以地址0x401为例,这个地址是在第一个节区中,由于在文件中由于FileAlignment是0x400,所以这个地址在文件中就是0x401。而在内存中,由于SectionAlignment是0x1000,所以前面PE头的数据会被填充到0x1000,所以这个地址就是0x1001。

据此也可以知道RVA和FOA的转换关系,如果地址是在PE头中,那么FOA==RVA。而如果地址是在节区中,就需要判断文件是在哪一个节区,然后根据这个地址和节区的偏移来算出它在文件中的位置FOA。

比如,如果RVA是0x1001,那么在内存中,它就是在第一个节区,它和第一个节区的偏移就是0x1。那么FOA就等于文件中第一个节区的位置加上这个偏移,也就是0x400+1=0x401。在PE的结构中,只要是VirtualAdresss字段名的数据,都是RVA,实际操作的时候都需要转换成FOA。

相应的RVA转FOA的代码如下

DWORD PEParse::RVAToFOA(DWORD dwRVA)
{
	DWORD dwFOA = dwRVA;
	WORD wSectionNum = this->pFileHead->NumberOfSections, i = 0;

	for (i = 0; i < wSectionNum; i++)
	{
		if (this->pSectionHead[i].VirtualAddress <= dwRVA && 
			dwRVA <= this->pSectionHead[i].VirtualAddress + this->pSectionHead[i].Misc.VirtualSize)	//判断在哪个节区中
		{
			dwFOA = this->pSectionHead[i].PointerToRawData + dwRVA - this->pSectionHead[i].VirtualAddress;	//根据节区的偏移算出FOA
			break;
		}
	}

	return dwFOA;
}

由于PE文件加载在内存中的起始地址是由可选头中的ImageBase来指定的,通常对于exe程序它都是0x40000。所以在计算FA,也就是内存中的真实地址的时候,是需要把这个起始地址算进去的,也就是说FA=RVA + ImageBase。

二.导入表作用

在Windows系统中,可以用两种方式加载DLL。分别称为隐式链接和显示链接,其区别就是隐私链接进来的DLL会随着程序的启动而为函数分配相应的内存,而显示链接则是在程序需要调用相关函数的时候获取相应的地址。但是随着系统版本的不同,加载的DLL数量顺序等等的不同,DLL中的函数地址是不确定的。

为了可以让程序每次都能找到正确的地址,就需要程序用一块地址来专门记录这些隐式链接的DLL中的函数地址,这块专门的地址就是叫导入表。每次启动的时候,操作系统都会获取正确的函数地址填入到导入表中。

比如下面这张图是对MessageBox的调用,可以看到程序不是E8的直接调用,而是E9的间接调用,所调用的是地址0x0042A2AC地址中保存的数据。而0x0042A2AC这个地址其实就是IAT表,后面会详细说这个表。

在内存窗口中查看这个地址中的内容,可以看到调试器已经告诉了我们这是一个MessageBox的函数地址。

要获取导入表的地址,首先就需要到可选头的最后一个成员,也就是IMAGE_DATA_DIRECTORY数组DataDirectory中去找。数组中的第一项保存的就是导入表的信息,在文件中有如下的定义

#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory

而IMAGE_DATA_DIRECTORY的定义如下

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

其中的VirtualAddress就是导入表在内存中的RVA,要在文件中找到就需要转换成相应的FOA。获取到的地址就是导入表的起始地址,所指的就是第一个导入表的地址,保存的是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;

由于程序会需要多个导入表,而每个导入表都有这样的一个结构,它们在内存中顺序存储下去形成数组。所以,为了说明已经加载了全部的导入表,系统会在最后一个导入表后面加入一块IMAGE_IMPORT_DESCRIPTOR结构体大小的0来说明已经结束。

结构中的Name指向的是地址保存了导入的Dll文件的文件名,而OriginalFirstThunk和FirstThunk保存的地址分别是INT和IAT表的地址,这3个地址都是RVA,使用的时候都要转成FOA。

INT和IAT所指地址的内容是一块数组,数组中的每个元素都是IMAGE_THUNK_DATA32结构体的定义如下

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;

结构体中,是一个联合体,而联合体中的每个成员都是4字节的内容,所以我们可以认为INT表和IAT表所指向的是一个整型数组。数组中保存的值的最高位是1的时候,去除最高位,得到的就是函数的导出序号,否则就是一个指向IMAGE_IMPORT_BY_NAME的RVA,结构体定义如下

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

其中的Name保存的就是导入的函数的名称,该名称是以0作为结束符。

对于判断最高位是否位1,在文档中有如下的定义

#define IMAGE_ORDINAL_FLAG64 0x8000000000000000
#define IMAGE_ORDINAL_FLAG32 0x80000000                //32位下,最高位位1
#define IMAGE_ORDINAL64(Ordinal) (Ordinal & 0xffff)
#define IMAGE_ORDINAL32(Ordinal) (Ordinal & 0xffff)
#define IMAGE_SNAP_BY_ORDINAL64(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG64) != 0)
#define IMAGE_SNAP_BY_ORDINAL32(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG32) != 0)    //判断最高位是否位1

据此,便可以找到程序导入的所有的Dll文件名以及导入的相应函数,如下图。

在文件加载到内存中之前,INT和IAT所指的内容是一样的,都是指向保存了函数名地址。而在程序装载进内存的时候,操作系统会根据导入表的名称和导入函数的名称将正确的函数地址填入到IAT表中。

据此可以写出解析导入表的代码如下

void PEParse::PrintImportTable()
{
	char *pDllName = NULL;
	DWORD i = 0;
	PDWORD pINT = NULL, pIAT = NULL;


	if (this->pImportTable == NULL)
	{
		printf("the file doest have import table\n");
		goto exit;
	}

	printf("=============Import Table Information===============\n");
	while (this->pImportTable[i].OriginalFirstThunk != 0 || 
		   this->pImportTable[i].FirstThunk != 0)
	{
		pDllName = (char *)((DWORD)(this->pDosHead) + this->RVAToFOA(this->pImportTable[i].Name));
		printf("The import Dll name is %s\n", pDllName);

		printf("The INT Information:\n");	// get INT table
		pINT = (PDWORD)((DWORD)(this->pDosHead) + 
									this->RVAToFOA(this->pImportTable[i].OriginalFirstThunk));
		while (*pINT)
		{
			if (IMAGE_SNAP_BY_ORDINAL32(*pINT)) //the highest bit is 1?
			{
				DWORD dwOrder = *pINT & ~IMAGE_ORDINAL_FLAG32;	//clear highest bit
				printf("The function order is %d\n", dwOrder);
			}
			else
			{
				PIMAGE_IMPORT_BY_NAME pFunName = (PIMAGE_IMPORT_BY_NAME)((DWORD)this->pDosHead + this->RVAToFOA(*pINT));
				printf("The function name is %s\n", pFunName->Name);
			}
			pINT++;
		}


		printf("The IAT Information:\n"); // get IAT table
		pIAT = (PDWORD)((DWORD)(this->pDosHead) + this->RVAToFOA(this->pImportTable[i].FirstThunk));
		while (*pIAT)
		{
			if (IMAGE_SNAP_BY_ORDINAL32(*pIAT))		//the highest bit is 1?
			{
				DWORD dwOrder = *pIAT & ~IMAGE_ORDINAL_FLAG32;	//clear highest bit
				printf("The function order is %d\n", dwOrder);
			}
			else
			{
				PIMAGE_IMPORT_BY_NAME pFunName = (PIMAGE_IMPORT_BY_NAME)((DWORD)this->pDosHead +
																	this->RVAToFOA(*pIAT));
				printf("The function name is %s\n", pFunName->Name);
			}
			pIAT++;
		}
		i++;
		printf("\n");
	}
	printf("=============Import Table Information===============\n");

exit:;

}

程序运行结果如下

三.IAT Hook

通过上面的解析可以知道,INT与IAT在文件加载进内存前保存的内容是一样的,而当文件装置进内存以后,系统就会根据所指的函数的地址填进IAT表中。所以IATHook的思路就是,找到相应的保存函数地址的IAT位置,替换成我们要执行的函数,这样在程序对这特定函数进行调用的时候,就会调用我们替换进去的函数。

下面这份代码就是通过IATHook劫持了MessageBox函数,由于此时函数已经运行了起来,所以所有的RVA地址不再需要转换成FOA地址就可以找到对应的数据

#include <cstdio>
#include <Windows.h>

typedef int (WINAPI *pFnMessageBox)(HWND, LPCSTR, LPCSTR, UINT);

int WINAPI HookMessageBoxA(HWND hWnd,
						   LPCSTR lpText,
						   LPCSTR lpCaption,
						   UINT uType);

void IATHook();
void UnIATHook();

DWORD dwOrgFuncAddr = 0;

int main()
{
	MessageBox(NULL, TEXT("1900"), TEXT("Test"), MB_OK);
	IATHook();
	MessageBox(NULL, TEXT("1900"), TEXT("Test"), MB_OK);
	UnIATHook();
	MessageBox(NULL, TEXT("1900"), TEXT("Test"), MB_OK);
	system("pause");

	return 0;
}

void IATHook()
{
	PVOID pBaseAddr = (PVOID)GetModuleHandle(NULL); //获取进程的装载进内存的基地址
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddr;
	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pBaseAddr + pDosHeader->e_lfanew + 4);
	PIMAGE_OPTIONAL_HEADER pOptionHeader = (PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);
	PIMAGE_IMPORT_DESCRIPTOR pImportTables = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pBaseAddr +
		pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
	PDWORD pIAT = NULL, pINT = NULL;
	PIMAGE_IMPORT_BY_NAME pFunName = NULL;

	while (pImportTables->FirstThunk != 0 && pImportTables->OriginalFirstThunk != 0)
	{
		pINT = (PDWORD)((DWORD)pBaseAddr + pImportTables->OriginalFirstThunk);
		pIAT = (PDWORD)((DWORD)pBaseAddr + pImportTables->FirstThunk);
		while (*pINT)
		{
			if (!IMAGE_SNAP_BY_ORDINAL32(*pINT))
			{
				pFunName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pBaseAddr + *pINT);
				if (strcmp((CONST CHAR*)pFunName->Name, "MessageBoxA") == 0)		//判断是不是要HOOK的函数
				{
					dwOrgFuncAddr = *pIAT;	//获取原来的函数地址
					*pIAT = (DWORD)HookMessageBoxA;	//将我们的函数地址复制进去
					printf("Hook Ok\n");
					break;
				}
			}
			pINT++;
			pIAT++;
		}
		pImportTables++;
	}
}


void UnIATHook()
{
	PVOID pBaseAddr = (PVOID)GetModuleHandle(NULL);	//获取进程的装载进内存的基地址
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddr;
	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((DWORD)pBaseAddr + pDosHeader->e_lfanew + 4);
	PIMAGE_OPTIONAL_HEADER pOptionHeader = (PIMAGE_OPTIONAL_HEADER)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);
	PIMAGE_IMPORT_DESCRIPTOR pImportTables = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pBaseAddr +
		pOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
	PDWORD pIAT = NULL, pINT = NULL;
	PIMAGE_IMPORT_BY_NAME pFunName = NULL;

	while (pImportTables->FirstThunk != 0 && pImportTables->OriginalFirstThunk != 0)
	{
		pINT = (PDWORD)((DWORD)pBaseAddr + pImportTables->OriginalFirstThunk);
		pIAT = (PDWORD)((DWORD)pBaseAddr + pImportTables->FirstThunk);
		while (*pINT)
		{
			if (!IMAGE_SNAP_BY_ORDINAL32(*pINT))
			{
				pFunName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pBaseAddr + *pINT);
				if (strcmp((CONST CHAR*)pFunName->Name, "MessageBoxA") == 0)		//判断是不是要HOOK的函数
				{
					*pIAT = (DWORD)dwOrgFuncAddr;	//将原来的函数恢复回去
					printf("UnHook Ok\n");
					break;
				}
			}
			pINT++;
			pIAT++;
		}
		pImportTables++;
	}
}

int WINAPI HookMessageBoxA(HWND hWnd,
	LPCSTR lpText,
	LPCSTR lpCaption,
	UINT uType)
{
	pFnMessageBox pMessagaBoxFun = (pFnMessageBox)dwOrgFuncAddr;

	pMessagaBoxFun(NULL, TEXT("Hooked"), TEXT("Success"), MB_OK);
	pMessagaBoxFun(hWnd, lpText, lpCaption, uType);

	return 0;
}

最终程序运行结果如下

在Hook之前,程序正常弹出了对话框

在Hook之后,程序在弹出对话框之前会首先执行弹出额外的对话框,然后在弹出正常的对话框

当卸载Hook以后,就又恢复了正常的弹框





【公告】【iPhone 13、ipad、iWatch】11月15日中午12:00,看雪·众安 2021 KCTF秋季赛 正式开赛【攻击篇】!!!文末有惊喜~

最后于 2021-10-11 10:47 被1900编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回