• 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自身的进程的,本文做了一个概念上的讲解

  • 相关阅读:
    CF Spreadsheets (数学)
    CF Theatre Square
    CF Error Correct System
    CF Playing with Paper
    HDU 3533 Escape (BFS + 预处理)
    nginx配置文件
    nginx配置文件详解
    Jenkins+maven+gitlab+shell实现项目自动化部署
    jenkins升级
    jenkins相关下载链接
  • 原文地址:https://www.cnblogs.com/linxmouse/p/14102036.html
Copyright © 2020-2023  润新知