Inline&IAT Hook原理

本文是对作者炒鸡嗨客协管徐文章的学习,环境Windows 10 企业版 LTSC 1809 + VS2019 16.8.2 (使用Unicode字符集),源代码下载

Hook实现调用某个函数时我们能截获、修改、取消这次调用

Hook大概分两种
  • 一种是修改函数代码跳转到钩子过程(Inline Hook)
  • 另一种是修改地址表指向钩子过程(IAT Hook、SSDT Hook、虚函数表Hook)

Inline hook

Inline Hook就是修改函数代码跳转到钩子过程,就像CE注入代码那样
可以把目标函数开头的代码修改成jmp或call指令实现,不过用jmp才能取消调用

完整的Inline Hook例程:
#include <stdio.h>
#include <Windows.h>

void Hook();
void Unhook();

#pragma pack(push)
#pragma pack(1)
#ifndef _WIN64
// 32位版本 jmp xxxxxxxx
struct JmpCode
{
private:
	// jmp指令的机器码,近跳转为E9,可跳至同一个段的范围内的地址
	const BYTE jmp;
	// 相对地址 = 目标地址 - 下一条指令地址 = 目标地址 - 当前地址 - jmp指令长度
	DWORD address;

public:
	JmpCode(DWORD srcAddr, DWORD dstAddr) : jmp(0xe9)
	{
		SetAddress(srcAddr, dstAddr);
	}

	void SetAddress(DWORD srcAddr, DWORD dstAddr)
	{
		// jmp指令长度 = sizeof(JmpCode) = 5
		address = dstAddr - srcAddr - sizeof(JmpCode);
	}
};
#else // !_WIN64
// 64位版本,jmp qword ptr [xxxxxxxxxxxxxxxx],64位的jmp不能直接用操作数,需要存到变量或寄存器里
struct JmpCode
{
private:
	// jmp指令的机器码
	BYTE jmp[6];
	// 绝对地址
	uintptr_t address;

public:
	JmpCode(uintptr_t srcAddr, uintptr_t dstAddr)
	{
		static const BYTE JMP[] = { 0xff, 0x25, 0x00, 0x00, 0x00, 0x00 };
		memcpy(jmp, JMP, sizeof(jmp));
		SetAddress(srcAddr, dstAddr);
	}

	void SetAddress(uintptr_t srcAddr, uintptr_t dstAddr)
	{
		address = dstAddr;
	}
};
#endif
#pragma pack(pop)

int Foo1(int arg)
{
	printf("调用了Foo1,arg = %d
", arg);
	return arg;
}

int Foo2(int arg)
{
	printf("调用了Foo2,arg = %d
", arg);
	// 此时Foo1被hook了,Unhook防止调用Foo1再跳转到Foo2栈溢出
	Unhook();
	int ret = Foo1(9527);
	// 恢复Hook
	Hook();

	return arg;
}

BYTE origin[sizeof(JmpCode)];

void Hook()
{
	// 从Foo1的起始跳转到Foo2的起始
	JmpCode code((uintptr_t)Foo1, (uintptr_t)Foo2);
	// 改变虚拟内存保护,使Foo1起始可写
	DWORD originProtect, originProtect2;
	VirtualProtect(Foo1, sizeof(code), PAGE_EXECUTE_READWRITE, &originProtect);
	// 保存Foo1的起始指令
	memcpy(origin, Foo1, sizeof(code));
	// 修改Foo1的起始指令
	memcpy(Foo1, &code, sizeof(code));
	// 恢复虚拟内存保护
	VirtualProtect(Foo1, sizeof(code), originProtect, &originProtect2);
}

void Unhook()
{
	// 改变虚拟内存保护,使Foo1起始可写
	DWORD originProtect, originProtect2;
	VirtualProtect(Foo1, sizeof(origin), PAGE_EXECUTE_READWRITE, &originProtect);
	// 恢复Foo1起始指令
	memcpy(Foo1, origin, sizeof(origin));
	// 恢复虚拟内存保护
	VirtualProtect(Foo1, sizeof(origin), originProtect, &originProtect2);
}

int main()
{
	int ret;

	// 调用Foo1
	ret = Foo1(111);
	printf("ret = %d
", ret);

	Hook();
	// hook后实际在会调用Foo2
	ret = Foo1(222);
	printf("ret = %d
", ret);

	Unhook();
	// 调用Foo1
	ret = Foo1(333);
	printf("ret = %d
", ret);

	return 0;
}
结果
调用了Foo1,arg = 111
ret = 111
调用了Foo2,arg = 222
调用了Foo1,arg = 9527
ret = 222
调用了Foo1,arg = 333
ret = 333
请按任意键继续. . .

IAT hook

IAT Hook是一种修改地址表实现的Hook
什么是IAT?这里要补充一些PE文件的知识
IAT是导入地址表(Import Address Table),储存着其他模块(DLL)的函数指针
跟IAT相关的有导入表(不要和IAT弄混),储存着导入函数的信息(比如函数名)
一个PE文件被加载时系统会根据导入表加载这个文件需要的模块,并修改IAT中的函数指针
程序要调用其他模块的函数时就要查IAT然后CALL表中的函数地址,所以只要修改IAT中的函数地不用修改代码就能实现hook了
比如调用MessageBoxW的代码:

	MessageBoxW(NULL, L"hello", L"Message", MB_OK);
010C1E28  mov         esi,esp  
010C1E2A  push        0  
010C1E2C  push        offset string L"Message" (010C7B68h)  
010C1E31  push        offset string L"hello" (010C7B7Ch)  
010C1E36  push        0  
010C1E38  call        dword ptr [__imp__MessageBoxW@16 (010CB09Ch)]  
010C1E3E  cmp         esi,esp  
010C1E40  call        __RTC_CheckEsp (010C125Dh)  

其中0x010CB09C就是IAT中的地址,储存着MessageBoxW的指针

IAT Hook的完整例程:
#include <stdio.h>
#include <Windows.h>
#include <locale.h>

void** FindImportAddress(HANDLE hookModule, LPCSTR moduleName, LPCSTR functionName)
{
	// 被hook的模块基址
	uintptr_t hookModuleBase = (uintptr_t)hookModule;
	PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hookModuleBase;
	PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)(hookModuleBase + dosHeader->e_lfanew);
	// 导入表
	PIMAGE_IMPORT_DESCRIPTOR importTable = (PIMAGE_IMPORT_DESCRIPTOR)(hookModuleBase
		+ ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

	// 遍历导入的模块
	for (; importTable->Characteristics != 0; importTable++)
	{
		// 不是函数所在模块
		if (_stricmp((LPCSTR)(hookModuleBase + importTable->Name), moduleName) != 0)
			continue;

		PIMAGE_THUNK_DATA info = (PIMAGE_THUNK_DATA)(hookModuleBase + importTable->OriginalFirstThunk);
		void** iat = (void**)(hookModuleBase + importTable->FirstThunk);

		// 遍历导入的函数
		for (; info->u1.AddressOfData != 0; info++, iat++)
		{
			// 是用函数名导入的
			if ((info->u1.Ordinal & IMAGE_ORDINAL_FLAG) == 0) 
			{
				PIMAGE_IMPORT_BY_NAME name = (PIMAGE_IMPORT_BY_NAME)(hookModuleBase + info->u1.AddressOfData);
				if (strcmp((LPCSTR)name->Name, functionName) == 0)
					return iat;
			}
		}

		// 没找到要Hook的函数
		return NULL; 
	}

	// 没找到要Hook的模块
	return NULL; 
}

BOOL HookIAT(HANDLE hookModule, LPCSTR moduleName, LPCSTR functionName, void* hookFunction, void** originAddress)
{
	void** address = FindImportAddress(hookModule, moduleName, functionName);
	if (address == NULL)
		return FALSE;

	// 保存原函数地址
	if (originAddress != NULL)
		*originAddress = *address;

	// 修改IAT中地址为hookFunction
	DWORD oldProtect, oldProtect2;
	VirtualProtect(address, sizeof(DWORD), PAGE_READWRITE, &oldProtect);
	*address = hookFunction;
	VirtualProtect(address, sizeof(DWORD), oldProtect, &oldProtect2);

	return TRUE;
}

BOOL UnhookIAT(HANDLE hookModule, LPCSTR moduleName, LPCSTR functionName)
{
	HMODULE hModule = GetModuleHandleA(moduleName);
	if (hModule == NULL) return FALSE;
	// 取原函数地址
	void *oldAddress = GetProcAddress(hModule, functionName);
	if (oldAddress == NULL)
		return FALSE;

	// 修改回原函数地址
	return HookIAT(hookModule, moduleName, functionName, oldAddress, NULL);
}

// MessageBoxW的函数指针类型
typedef int (WINAPI* MessageBoxWType)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
// 原函数地址
MessageBoxWType RealMessageBoxW = NULL;

int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
{
	wprintf(L"截获到MessageBoxW调用,lpText = %s
", lpText);
	return RealMessageBoxW(hWnd, L"被截获了!", lpCaption, uType);
}

int main(int argc, TCHAR* argv[])
{
	// 让wprintf支持中文输出
	setlocale(LC_ALL, "chs");

	MessageBoxW(NULL, L"Hello world1!", L"", MB_OK);

	HookIAT(GetModuleHandle(NULL), "user32.dll", "MessageBoxW", MyMessageBoxW, (void**)&RealMessageBoxW);
	MessageBoxW(NULL, L"Hello world2!", L"", MB_OK);

	UnhookIAT(GetModuleHandle(NULL), "user32.dll", "MessageBoxW");
	MessageBoxW(NULL, L"Hello world3!", L"", MB_OK);

	return 0;
}
结果

这种Hook的优点就是调用原函数时不用Unhook了,缺点是不能截获全部的调用 比如用GetProcAddress获取函数指针再调用

高级一点的Inline Hook

前面的Inline Hook调用原函数时要Unhook,有没有办法不需要去Unhook呢?

#include <stdio.h>
#include <locale.h>
#include <Windows.h>

#pragma pack(push)
// 修改内存对齐,保证JmpCode不被无效数据填充
#pragma pack(1)
#ifndef _WIN64
// 32位版,JMP XXXXXXXX
struct JmpCode
{
private:
	const BYTE jmp; // JMP指令的机器码,近跳转为E9,可跳至同一个段的范围内的地址
	DWORD address; // 相对地址 = 目标地址 - 下一条指令地址 = 目标地址 - 当前地址 - JMP指令长度

public:
	JmpCode(DWORD srcAddr, DWORD dstAddr)
		: jmp(0xE9)
	{
		SetAddress(srcAddr, dstAddr);
	}

	void SetAddress(DWORD srcAddr, DWORD dstAddr)
	{
		address = dstAddr - srcAddr - sizeof(JmpCode); // JMP指令长度 = sizeof(JmpCode) = 5
	}
};
#else
// 64位版,JMP QWORD PTR [XXXXXXXXXXXXXXXX],64位JMP不能直接用操作数,需要存到变量或寄存器里
struct JmpCode
{
private:
	BYTE jmp[6]; // JMP指令的机器码
	uintptr_t address; // 绝对地址

public:
	JmpCode(uintptr_t srcAddr, uintptr_t dstAddr)
	{
		static const BYTE JMP[] = { 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 };
		memcpy(jmp, JMP, sizeof(jmp));
		setAddress(srcAddr, dstAddr);
	}

	void setAddress(uintptr_t srcAddr, uintptr_t dstAddr)
	{
		address = dstAddr;
	}
};
#endif
#pragma pack(pop)

// hookLength至少要有sizeof(JmpCode),要保证originalFunction开头前hookLength个字节是完整的指令
void Hook(void* originalFunction, void* hookFunction, void** newFunction, SIZE_T hookLength = sizeof(JmpCode))
{
	// 改变虚拟内存保护,使originalFunction开头可写,用WriteProcessMemory写内存的话好像不用改保护
	DWORD oldProtect, oldProtect2;
	VirtualProtect(originalFunction, hookLength, PAGE_EXECUTE_READWRITE, &oldProtect);

	// 保存originalFunction开头的指令,这里假设了originalFunction开头前hookLength个字节是完整的指令,如果截断了会出错
	// 更好的办法是用反汇编引擎判断开头前几个字节是完整指令
	// 另外如果原代码可能跳转到这几个字节之间也会出错,所以这个方法有局限性
	void* newFunctionCode = VirtualAlloc(NULL, hookLength + sizeof(JmpCode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	// 保存originalFunction开头前hookLength个字节
	memcpy(newFunctionCode, originalFunction, hookLength);
	// 从newFunctionCode起始第hookLength + 1字节跳到originalFunction起始第hookLength + 1字节的指令
	JmpCode code((uintptr_t)newFunctionCode + hookLength, (uintptr_t)originalFunction + hookLength);
	memcpy((BYTE*)newFunctionCode + hookLength, &code, sizeof(code));
	*newFunction = newFunctionCode;

	// 从originalFunction起始跳到hookFunction起始的指令
	code.SetAddress((uintptr_t)originalFunction, (uintptr_t)hookFunction);
	// 修改originalFunction起始的指令
	memcpy(originalFunction, &code, sizeof(code));

	// 恢复虚拟内存保护
	VirtualProtect(originalFunction, hookLength, oldProtect, &oldProtect2);
}

void Unhook(void* originalFunction, void** newFunction, SIZE_T hookLength = sizeof(JmpCode))
{
	// 改变虚拟内存保护,使originalFunction开头可写
	DWORD oldProtect, oldProtect2;
	VirtualProtect(originalFunction, hookLength, PAGE_EXECUTE_READWRITE, &oldProtect);
	// 恢复originalFunction开头的指令
	memcpy(originalFunction, *newFunction, hookLength);
	// 恢复虚拟内存保护
	VirtualProtect(originalFunction, hookLength, oldProtect, &oldProtect2);

	// 释放newFunction
	VirtualFree(*newFunction, 0, MEM_RELEASE);
	*newFunction = NULL;
}

// MessageBoxA的函数指针类型
typedef int (WINAPI* MessageBoxWType)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);

// 新的MessageBoxA入口,执行完MessageBoxA开头5字节代码后跳到MessageBoxA开头第6个字节
MessageBoxWType NewMessageBoxW = NULL;

int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
{
	wprintf(L"截获到MessageBoxA调用,lpText = %s
", lpText);
	return NewMessageBoxW(hWnd, L"被截获了!", lpCaption, uType);
}

int main(int argc, TCHAR* argv[])
{
	// 让wprintf支持中文输出
	setlocale(LC_ALL, "chs");

	//void* MessageBoxWAddress = GetProcAddress(GetModuleHandle(_T("user32.dll")), "MessageBoxW"); // 这个是查目标模块EAT
	void* MessageBoxWAddress = MessageBoxW; // 这个是查本模块IAT

	MessageBox(NULL, L"第一次调用", L"", MB_OK);

	Hook(MessageBoxWAddress, MyMessageBoxW, (void**)&NewMessageBoxW);
	MessageBox(NULL, L"第二次调用", L"", MB_OK);

	Unhook(MessageBoxWAddress, (void**)&NewMessageBoxW);
	MessageBox(NULL, L"第三次调用", L"", MB_OK);

	return 0;
}
结果与IAT Hook的结果相同

当然学习Hook的使用并不是为了Hook自身的进程的,本文做了一个概念上的讲解

原文地址:https://www.cnblogs.com/linxmouse/p/14102036.html