C/C++ x32 Inline Hook 代码封装

C/C++
394
0
0
2023-02-15

Hook 技术常被叫做挂钩技术,挂钩技术其实早在DOS时代就已经存在了,该技术是Windows系统用于替代DOS中断机制的具体实现,钩子的含义就是在程序还没有调用系统函数之前,钩子捕获调用消息并获得控制权,在执行系统调用之前执行自身程序,简单来说就是函数劫持.

HOOK技术的实现方法比较多,常见的HOOK方法有 Inline Hook、IAT Hook、EAT Hook 这三种,钩子的应用范围非常广泛,比如输入监控、API拦截、消息捕获、改变程序执行流程等,杀毒软件也会HOOK钩住一些特殊的API函数,起到监控系统运行状态的目的,黑客们也会通过钩子技术截获一些有价值的数据,例如键盘消息等.

该笔记是针对32位Hook的简易封装,自己留着也没什么意思,还是分享出来吧,转载请加出处,谢谢!

Hook 实现去弹窗: 首先我们来实现一个小功能,这里有一个小程序,当我们点击弹窗时会自动的弹出一个MessageBox提示,我们的目标是通过注入DLL的方式Hook钩挂住MessageBox从而实现去除这个弹窗的目的,先来看一下Hook的思路:

1.调用 GetModuleHandle 来获取到user32.dll模块的基址 2.调用 GetProcAddress 获取到MessageBoxA弹窗的基址 3.调用 VirtualProtect 来修改MsgBox前5个字节内存属性 4.计算 Dest - MsgBox - 5 重定位跳转地址,并写入JMP跳转指令 5.计算 Dest + Offset + 5 = MsgBox +5 得到需要跳转回ret的位置 6.最后调用 VirtualProtect 来将内存属性修改为原始状态

首先我们载入带有MsgBox弹窗的程序,然后在X64DBG上按下Ctrl+G输入MessageBoxA找到我们需要Hook的地方,如下所示我们为了完成弹窗转向功能,只需要在函数开头写入jmp无条件跳转指令即可,在32位系统中JMP指令默认占用5个字节,前三条指令恰好5个字节,为了能够保持堆栈平衡,我们需要记下前三条指令,并在自己的中转函数中补齐.

759F1F70 | 8BFF                     | mov edi,edi                    | Src 替换为 jmp xxxx
759F1F72 | 55                       | push ebp                       | 替换为 jmp xxxx
759F1F73 | 8BEC                     | mov ebp,esp                    | 替换为 jmp xxxx
759F1F75 | 6A FF                    | push 0xFFFFFFFF                |
759F1F77 | 6A 00                    | push 0x0                       |
759F1F79 | FF75 14                  | push dword ptr ss:[ebp+0x14]   |
759F1F7C | FF75 10                  | push dword ptr ss:[ebp+0x10]   |
759F1F7F | FF75 0C                  | push dword ptr ss:[ebp+0xC]    |
759F1F82 | FF75 08                  | push dword ptr ss:[ebp+0x8]    |
759F1F85 | E8 D6010000              | call <MessageBoxTimeoutA>      |
759F1F8A | 5D                       | pop ebp                        | Dest
759F1F8B | C2 1000                  | ret 0x10                       |

我们还需要计算出程序的返回地址,使用759F1F8A - 772A1F70 = 1A从而得出返回地址就是基址加上1A,这里的返回地址其实就是返回到原MessageBox弹窗的ret 0x10的位置759F1F8B,从这里可以看出屏蔽弹窗的原理就是通过中转函数跳过了弹窗函数的执行,我们直接编译这段代码,并注入到弹窗程序测试,会发现弹窗被去除了.

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

DWORD jump = 0;
// naked 关键字的作用是,不给我添加任何的汇编修饰
__declspec(naked) void Transfer(){
	__asm{
		mov edi, edi
		push ebp
		mov ebp, esp
		mov ebx, jump     // 取出跳转地址
		jmp ebx           // 无条件转向
	}
}

bool APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
	HMODULE hwnd = GetModuleHandle(TEXT("user32.dll"));
	DWORD base = (DWORD)GetProcAddress(hwnd, "MessageBoxA");
	DWORD oldProtect = 0;
	// 将内存设置为可读可写可执行状态,并将原属性保存在oldProtect方便恢复
	if (VirtualProtect((LPVOID)base, 5, PAGE_EXECUTE_READWRITE, &oldProtect))
	{
		DWORD value = (DWORD)Transfer - base - 5;    // 计算出需要Hook的地址
		jump = base + 0x1a;                          // 计算出返回地址
		__asm{
			mov eax, base
			mov byte ptr[eax], 0xe9        // e9 = jmp 指令机器码
			inc eax                        // 递增指针
			mov ebx, value                 // 需要跳转到的地址
			mov dword ptr[eax], ebx
		}
		// 恢复内存的原始属性
		VirtualProtect((LPVOID)base, 5, oldProtect, &oldProtect);
	}
	return true;
}

Hook 实现改标题: 通常情况下,程序设置标题会调用SetWindowTextA这个API函数,我们可以拦截这个函数,并传入自定义的窗口名称,从而实现修改指定窗口的标题的目的,代码只是在上面代码的基础上稍微改一下就能实现效果.

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

DWORD jump = 0;

__declspec(naked) bool _stdcall Transfer(HWND hwnd, LPCSTR lpString){
	__asm{
		mov edi, edi
		push ebp
		mov ebp, esp
		mov ebx, jump
		jmp ebx
	}
}

bool __stdcall MySetWindowTextA(HWND hwnd, LPCSTR lpString){
	char * lpText = "LyShark 破解版";
	return Transfer(hwnd, lpText);
}

bool APIENTRY DllMain(HANDLE handle, DWORD dword, LPVOID lpvoid)
{
	HMODULE hwnd = GetModuleHandle(TEXT("user32.dll"));
	DWORD base = (DWORD)GetProcAddress(hwnd, "SetWindowTextA");
	DWORD oldProtect = 0;

	if (VirtualProtect((LPVOID)base, 5, PAGE_EXECUTE_READWRITE, &oldProtect))
	{
		DWORD value = (DWORD)MySetWindowTextA - base - 5;
		jump = base + 5;
		__asm{
			mov eax, base
			mov byte ptr[eax], 0xe9
			inc eax
			mov ebx, value
			mov dword ptr[eax], ebx
		}
		VirtualProtect((LPVOID)base, 5, oldProtect, &oldProtect);
	}
	return true;
}

针对Hook代码封装: 上面代码并不具备通用性,这里我们可以使用C++将其封装成类,这样使用会更方便,通常封装类都会存在两个文件,这里我们将头文件定义为hook.h将实现文件定义为hook.cpp分别实现这两个文件代码逻辑.

#pragma once
#include <Windows.h>

#ifdef __cplusplus
extern "C"{
#endif

class MyHook
{
public:
	PROC m_pfnOrig;       // 保存函数地址
	BYTE m_bOldBytes[5];  // 保存函数入口代码
	BYTE m_bNewBytes[5];  // 保存Inlie Hook代码
public:
	MyHook();
	~MyHook();

	BOOL Hook(LPSTR pszModuleName, LPSTR pszFuncName, PROC pfnHookFunc);
	BOOL UnHook();
	BOOL ReHook();
};
#ifdef __cplusplus
}
#endif

如下是代码实现部分MyHook()构造函数用来初始化,析构函数用来清空并恢复钩子,Hook则是具体实现挂钩的细节,在Hook()成员函数中完成了3项工作,首先是获得了被HOOK函数的函数地址,接下来是保存了被HOOK函数的前5字节,最后是用构造好的跳转指令来修改被HOOK函数的前5字节的内容.

#include "hook.h"

// 构造函数: 负责初始化
MyHook::MyHook()
{
	m_pfnOrig = NULL;
	ZeroMemory(m_bOldBytes, 5);
	ZeroMemory(m_bNewBytes, 5);
}

MyHook::~MyHook()
{
	UnHook();
	m_pfnOrig = NULL;
	ZeroMemory(m_bOldBytes, 5);
	ZeroMemory(m_bNewBytes, 5);
}

// 挂钩
BOOL MyHook::Hook(LPSTR pszModuleName, LPSTR pszFuncName, PROC pfnHookFunc)
{
	BOOL bRet = FALSE;

	// 获取指定模块中函数的地址
	m_pfnOrig = (PROC)GetProcAddress(GetModuleHandle(pszModuleName),pszFuncName);

	if (m_pfnOrig != NULL)
	{
		// 保存该地址处 5 字节的内容
		DWORD dwNum = 0;
		ReadProcessMemory(GetCurrentProcess(),m_pfnOrig,m_bOldBytes,5,&dwNum);

		// 构造 JMP 指令
		m_bNewBytes[0] = '\xe9'; // jmp Opcode

		// pfnHookFunc 是 HOOK 后的目标地址
		// m_pfnOrig 是原来的地址
		// 5 是指令长度
		*(DWORD *)(m_bNewBytes + 1) = (DWORD)pfnHookFunc - (DWORD)m_pfnOrig - 5;

		// 将构造好的地址写入该地址处
		WriteProcessMemory(GetCurrentProcess(),m_pfnOrig,m_bNewBytes,5,&dwNum);
		bRet = TRUE;
	}
	return bRet;
}
// 恢复钩子
BOOL MyHook::UnHook()
{
	if (m_pfnOrig != 0)
	{
		DWORD dwNum = 0;
		WriteProcessMemory(GetCurrentProcess(),m_pfnOrig,m_bOldBytes,5,&dwNum);
	}
	return TRUE;
}
// 重新挂钩
BOOL MyHook::ReHook()
{
	BOOL bRet = FALSE;
	if (m_pfnOrig != 0)
	{
		DWORD dwNum = 0;
		WriteProcessMemory(GetCurrentProcess(),m_pfnOrig,m_bNewBytes,5,&dwNum);
		bRet = TRUE;
	}
	return bRet;
}

到此为止整个Inline Hook的封装已经完成了,在后面的代码中,可以很容易地实现对函数的HOOK功能,这里我再多说一句,如果我们需要在自己实现的MessageBox函数中要调用原始的API函数,则需要恢复Inline Hook,否则程序将会崩溃.

#include <Windows.h>
#include "hook.h"

MyHook MsgHook;

int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
	// 先来恢复Hook 之所以要恢复是因为我们需要调用原始的MsgBox弹窗
	MsgHook.UnHook();

	MessageBoxA(hWnd, "hook inject", lpCaption, uType);

	// 弹窗完成后重新Hook
	MsgHook.ReHook();
	return 0;
}

int main(int argc, char * argv[])
{
	// 开始Hook
	MsgHook.Hook("user32.dll", "MessageBoxA", (PROC)MyMessageBoxA);
	MessageBoxA(NULL, "hello lyshark", "Msg", MB_OK);
	// 结束Hook
	MsgHook.UnHook();
	return 0;
}

第二种封装方式: 该封装方式直接将定义与实现写到hook.h头文件中,使用时直接包含一个文件即可.

#pragma once
#include <Windows.h>

#ifdef __cplusplus
extern "C"{
#endif

#pragma once
	class MyHook
	{
	public:
		static DWORD Hook(LPCWSTR lpModule, LPCSTR lpFuncName, PROC lpFunction)
		{
			DWORD dwAddr = (DWORD)GetProcAddress(GetModuleHandle(lpModule), lpFuncName);
			BYTE jmp[] =
			{
				0xe9,                      // jmp
				0x00, 0x00, 0x00, 0x00,    // address
				0xc3                       // retn
			};

			ReadProcessMemory(GetCurrentProcess(), (LPVOID)dwAddr, MemoryAddress(), 6, 0);
			DWORD dwCalc = ((DWORD)lpFunction - dwAddr - 5);
			memcpy(&jmp[1], &dwCalc, 4);
			WriteProcessMemory(GetCurrentProcess(), (LPVOID)dwAddr, jmp, 6, 0);

			return dwAddr;
		}

		static BOOL UnHook(LPCWSTR lpModule, LPCSTR lpFuncName)
		{
			DWORD dwAddr = (DWORD)GetProcAddress(GetModuleHandle(lpModule), lpFuncName);
			if (WriteProcessMemory(GetCurrentProcess(), (LPVOID)dwAddr, MemoryAddress(), 6, 0))
				return TRUE;
			return FALSE;
		}

		static BYTE* MemoryAddress()
		{
			static BYTE backup[6];
			return backup;
		}
	};
#ifdef __cplusplus
}
#endif
#include "hook.h"

MyHook MsgHook;

int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
	// 先来恢复Hook 之所以要恢复是因为我们需要调用原始的MsgBox弹窗
	MsgHook.UnHook(L"user32.dll","MessageBoxA");

	MessageBoxA(hWnd, "hook inject", lpCaption, uType);

	// 弹窗完成后重新Hook
	MsgHook.Hook(L"user32.dll", "MessageBoxW", (PROC)MyMessageBoxA);
	return 0;
}

int main(int argc, char * argv[])
{
	// 开始Hook
	MsgHook.Hook(L"user32.dll", "MessageBoxA", (PROC)MyMessageBoxA);

	MessageBoxA(NULL, "hello lyshark", "Msg", MB_OK);

	// 结束Hook
	MsgHook.UnHook(L"user32.dll", "MessageBoxA");
	return 0;
}