【第38节】windows原理:DLL注入与Hook是什么

目录

一、DLL注入

1.1 起源目的

1.2 基本原理

1.3 常见dll注入方式

1.4 完整远程dll注入代码

二、万能Hook

2.1 什么是hook技术

2.2 Hook和DLL注入的区别

三、系统消息钩子

3.1 系统消息钩子的使用

3.2 附加作用

四、自定义钩子

五、完整示例代码

5.1 32位IAT hook代码

5.2 Win7 X64 下 HOOK OPENPROCESS


一、DLL注入

        DLL(Dynamic Link Library,动态链接库)注入技术是一种将DLL文件插入到其他正在运行的进程中的技术。

1.1 起源目的

        最初源自程序员想要对其他第三方应用程序进行功能扩展的需求。比如,一些软件原本没有某种特定功能,但通过DLL注入技术,可以将实现该功能的DLL注入到软件进程中,从而为软件增加新功能。如今,它依然是构建系统复杂功能或让应用程序实现复杂操作的基础性支撑技术。例如,在一些安全防护软件中,会利用DLL注入来监控其他程序的运行状态。

1.2 基本原理

        操作系统中的进程有自己独立的地址空间。DLL注入的核心就是想办法让目标进程加载并执行我们指定的DLL文件。一旦DLL被注入到目标进程中,DLL中的代码就可以在目标进程的上下文中运行,从而实现各种目的,如修改进程的行为、获取进程中的数据等。

        一开始,程序员为了给第三方应用程序拓展功能,就想出了DLL注入的办法。直到现在,在搭建复杂系统功能,或者让应用程序完成复杂操作时,DLL注入仍然是一项基础性的关键技术。之前,我们讲过利用`CreateRemoteThread()`来进行DLL注入的操作,不过这仅仅是众多DLL注入技术中的一种。

1.3 常见dll注入方式

当前,大家比较熟知的DLL注入方法,主要有下面这几种:
1. 注册表注入:通过修改注册表实现DLL注入。
2. ComRes注入:借助ComRes相关机制实现注入。
3. APC注入:利用APC技术完成注入操作。
4. 消息钩子注入:基于消息钩子来达成DLL注入。
5. 远程线程注入:就像之前讲的利用`CreateRemoteThread()`创建远程线程实现注入。
6. 依赖可信进程注入:依靠可信进程完成DLL注入。
7. 劫持进程创建注入:在进程创建过程中实施劫持,实现DLL注入。
8. 输入法注入:通过输入法相关途径实现DLL注入 。


下面是远程线程注入的示例伪代码:

if (szDIIName[0] == NULL) return -1;
//1.打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 进程ID);
if (hProcess == INVALID_HANDLE_VALUE) return -1;

//2.在远程进程中申请空间
LPVOID pszDIIName = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (NULL == pszDIIName) return -1;

//3.向远程进程中写入数据
if (!WriteProcessMemory(hProcess, pszDIIName, szDIIName, MAX_PATH, NULL)) return -1;

//4.在远程进程中创建远程线程
HANDLE hInjecthread = CreateRemoteThread(
    hProcess,           //远程进程句柄
    NULL,               //安全属性
    0,                  //栈大小
    (LPTHREAD_START_ROUTINE)LoadLibrary, //进程处理函数
    pszDIIName,         //传入参数
    NULL,                //默认创建后的状态
    NULL);               //线程ID

if (NULL == hInjecthread) return -1;

//5.等待线程结束返回
DWORD dw = WaitForSingleObject(hInjecthread, -1);

//6.获取线程退出码,即LoadLibrary的返回值,即dll的首地址
DWORD dwExitCode;
GetExitCodeThread(hInjecthread, &dwExitCode);
HMODULE hMod = (HMODULE)dwExitCode;

//7.释放空间
if (!VirtualFreeEx(hProcess, pszDIIName, 4096, MEM_DECOMMIT)) return -1;
CloseHandle(hProcess);

1.4 完整远程dll注入代码

远程线程注入主程序:

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

bool injectDll( DWORD dwPid , const char* pszDllPath );

int _tmain(int argc, _TCHAR* argv[])
{
	DWORD	dwPid;
	char	szDllPath[ MAX_PATH ] = { "E:\\Debug\\testDll.dll"};


	printf( "输入要注入到的进程PID:" );
	scanf_s( "%d[*]" , &dwPid );

	printf( "输入要注入到的DLL:" );
	//scanf_s( "%s" , szDllPath , MAX_PATH );

	injectDll( dwPid , szDllPath );

	return 0;
}


// 
bool injectDll( DWORD dwPid , const char* pszDllPath ) {

	/**
	* 远程注入的目标: 将一个DLL注入到其它进程的地址空间中.
	* 注入方法:
	* 背景知识:
	* 1. windwos中有一个创建远程线程的API. 这个API能够在
	*    目标进程中创建一个线程. 在创建线程时,能够由自己指
	*	 定线程的回调函数, 但这个函数的地址必须在目标进程
	*    的地址空间中. 线程被创建起来之后, 这个函数就会被
	*	 执行.
	* 2. 当一个DLL被进程加载后, 操作系统会在物理内存中分配
	*    一块空间来保存它, 当这个DLL再次被其它进程加载时,
	*	 系统不会再次分配物理内存来保存这个DLL,而是将这个DLL
	*	 所在的物理内存映射到新进程的虚拟地址空间中.
	*	 因此,系统DLL在每次开机之后,它们的加载地址都是不变的,
	*	 所以,所有进程的系统DLL的加载基址都是相同的,因为它们
	*	 的加载基址相同,故每一个API的地址都在任何进程中也都是
	*    相同的.
	*    使用CreateRemoteThread函数,在目标进程中创建线程.
	*	 CreateRemoteThread需要指定线程回调函数,这个回调
	*    函数只有一个参数, 而LoadLibrary这个系统API刚好也
	*    只有一个参数.而LoadLibrary是一个系统DLL中的函数,
	*	 它在所有进程中的地址都是同一个, 正好为我们所用.
	*	 这样一来, 当我们创建远程线程,LoadLibrary就会被调
	*	 用,现在我们只需要给LoadLibrary函数传一个DLL路径
	*    就成功了. 但这个字符串必须保存在目标进程的地址空间中.
	*	 因为,远程线程的回调函数LoadLibrary虽然在任何进程中
	*    的地址都是同一个, 但是执行它的是其它进程,因此,它的
	*    参数中用到的地址也必须是它所在进程的地址.
	* 注入过程:
	* 1. 使用VirtualAllocEx在目标进程中开辟内存空间.
	* 2. 使用WriteProcessMemory将DLL路径写入到目标进程新
	*	 开的内存空间中.
	* 3. 创建远程线程, 使用LoadLibrary函数作为线程的回调函数,
	*    使用VirtualAllocEx开辟出的内存空间首地址作为回调函数的参数
	* 4. 等待线程退出.
	* 5. 销毁VirtualAllocEx开辟出来的内存空间
	*/

	bool	bRet			= false;
	HANDLE	hProcess		= 0;
	HANDLE	hRemoteThread	= 0;
	LPVOID	pRemoteBuff		= NULL;
	SIZE_T 	dwWrite			= 0 ;
	DWORD	dwSize			= 0;



	// 打开进程
	hProcess = OpenProcess(
		PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION |PROCESS_VM_WRITE ,/*创建线程和写内存权限*/
		FALSE ,
		dwPid /*进程ID*/
		);

	if( hProcess == NULL ) {
		printf( "打开进程失败,可能由于本程序的权限太低,请以管理员身份运行再尝试\n" );
		goto _EXIT;
	}


	// 1. 在远程进程上开辟内存空间
	pRemoteBuff = VirtualAllocEx( hProcess ,
								  NULL ,
								  64 * 1024 , /*大小:64Kb*/
								  MEM_COMMIT ,/*预定并提交*/
								  PAGE_EXECUTE_READWRITE/*可读可写可执行的属性*/
								  );
	if( pRemoteBuff == NULL ) {
		printf( "在远程进程上开辟空降失败\n" );
		goto _EXIT;
	}

	// 2. 将DLL路径写入到新开的内存空间中
	dwSize = strlen( pszDllPath ) + 1;
	WriteProcessMemory( hProcess ,
					   pRemoteBuff,	/* 要写入的地址 */
					   pszDllPath,	/* 要写入的内容的地址 */
					   dwSize,		/* 写入的字节数 */
					   &dwWrite		/* 输出:函数实际写入的字节数 */
						);
	if( dwWrite != dwSize ) {
		printf( "写入DLL路径失败\n" );
		goto _EXIT;
	}

	//3. 创建远程线程,
	//   远程线程创建成功后,DLL就会被加载,DLL被加载后DllMain函数
	//	 就会被执行,如果想要执行什么代码,就在DllMain中调用即可.
	hRemoteThread = CreateRemoteThread(
		hProcess ,
		0 , 0 ,
		(LPTHREAD_START_ROUTINE)LoadLibraryA ,  /* 线程回调函数 */
		pRemoteBuff ,							/* 回调函数参数 */
		0 , 0 );

	// 等待远程线程退出.
	// 退出了才释放远程进程的内存空间.
	WaitForSingleObject( hRemoteThread , -1 );


	bRet = true;


_EXIT:
	// 释放远程进程的内存
	VirtualFreeEx( hProcess , pRemoteBuff , 0 , MEM_RELEASE );
	// 关闭进程句柄
	CloseHandle( hProcess );

	return bRet;
}

testDll.dll  代码:

#include <windows.h>

BOOL APIENTRY DllMain(HMODULE hModule,
					  DWORD  ul_reason_for_call,
					  LPVOID lpReserved
)
{
	MessageBox(0, L"Dll被加载", L"DLL内部的弹窗", 0);
	if(ul_reason_for_call == DLL_PROCESS_ATTACH)
	{
		
	}
	return TRUE;
}

被注入到的进程程序:

#include "stdafx.h"
#include <windows.h>

void fun( ) {

}
int _tmain(int argc, _TCHAR* argv[])
{
	DWORD dwFunctionAddress = (DWORD)&fun;

	system( "pause" );
	MessageBox( 0, 
				L"---------" , 
				L"****" , 0 );
	system( "pause" );
	return 0;
}

先运行被注入到的进程程序,查找到它的进程id,再运行远程线程注入主程序,输入进程id看效果。

二、万能Hook

2.1 什么是hook技术

        Hook 技术,中文叫钩子技术,本质上是在程序运行过程中,拦截特定事件或函数调用,改变其执行路径,插入自定义代码,来实现特定功能的一种编程技术。

        Hook在程序设计中是一种灵活多变的技术手段,它主要用于改变程序原有的执行流程,让程序执行开发者自己定义的代码。在Windows系统下,Hook有两种含义,一是系统提供的消息Hook机制,由一系列API提供服务,通过这些API可以对大多数应用程序的关键节点进行Hook操作,Windows会为每种Hook类型维护一个钩子链表;二是自定义的Hook编程技巧,这是基于特定系统结构、文件结构以及汇编语言的高级技术。

2.2 Hook和DLL注入的区别

        (1)概念不同:Hook是一种拦截和处理系统或应用程序消息、函数调用等事件的技术,通过设置钩子函数来监视和修改程序的执行流程。DLL注入则是将DLL文件插入到其他正在运行的进程中,使DLL中的代码能够在目标进程的上下文中运行。

        (2)目的不同:Hook的主要目的是监控和修改程序的行为,比如拦截特定的消息、在函数调用前后执行自定义代码等,常用于实现消息拦截、键盘鼠标监控等功能。DLL注入的目的更侧重于扩展进程的功能,将外部的功能代码注入到目标进程中,让目标进程能够执行原本不具备的功能,或者对目标进程进行恶意攻击,如窃取数据、篡改程序行为等。

        (3)实现方式不同:Hook通常是通过调用系统提供的API来设置钩子,将钩子函数注册到系统或应用程序中,当特定事件发生时,系统会自动调用钩子函数。而DLL注入有多种实现方式,如注册表注入、远程线程注入、消息钩子注入等,这些方式的共同点是将DLL文件加载到目标进程的地址空间中,然后让目标进程执行DLL中的代码。以消息钩子注入这种DLL注入方式为例,它虽然利用了Hook的机制,但与单纯的Hook不同,它不仅仅是拦截和处理消息,更重要的是通过消息钩子触发DLL的注入,使DLL中的代码在目标进程中运行,而普通的Hook不一定会涉及到DLL的注入和在其他进程中执行代码。

三、系统消息钩子

3.1 系统消息钩子的使用

        Windows操作系统采用事件驱动机制。事件会被包装成消息发送给窗口,比如点击菜单、按钮,移动窗口,按下键盘等操作都会产生消息。以按下键盘为例:
1. 按下键盘时,会产生一个消息,按键消息被加入到系统消息队列。
2. 操作系统从系统消息队列中取出消息,并添加到相应程序的消息队列中。
3. 应用程序通过消息泵从自身消息队列中取出`WM_KEYDOWN`消息,然后调用消息处理函数。

        我们可以在系统消息队列到程序消息队列之间添加消息钩子,这样就能在系统消息队列中的消息发送给应用程序之前捕获到消息。还可以多次添加钩子,形成钩子链,依次调用函数。

        消息钩子是Windows操作系统提供的机制,SPY++截获窗口消息的功能,正是基于这一机制。下面是相关API:

/*设置钩子*/
HHOOK WINAPI SetWindowsHookEx(
    _In_ int idHook,         //钩子类型
    _In_ HOOKPROC lpfn,      //钩子函数
    _In_ HINSTANCE hMod,     //应用程序实例句柄(包含有钩子函数)
    _In_ DWORD dwThreadId);  //欲勾住的线程(为0则不指定,全局)

/*为钩子链中的下一个子程序设置钩子*/
LRESULT WINAPI CallNextHookEx(
    _In_opt_ HHOOK hhk,      //钩子句柄
    _In_ int nCode,          //钩子事件代码
    _In_ WPARAM wParam,      //传给钩子子程序
    _In_ LPARAM lParam);     //传给钩子子程序

/*卸载钩子*/
BOOL WINAPI UnhookWindowsHookEx(
    _In_ HHOOK hhk);         //钩子句柄

能够设置的钩子类型:

下面是示例代码:

//导出函数:安装钩子,当触发Hook消息WH_KEYBOARD后,目标进程加载DLL
BOOL InstallHook() {
    g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_Histance, 0);
    if (NULL == g_Hook) return FALSE;
    return TRUE;
}

//导出函数:卸载钩子
BOOL UninstallHook() {
    if (NULL == g_Hook) return FALSE;
    return UnhookWindowsHookEx(g_Hook);
}

LRESULT CALLBACK KeyboardProc(
    int code,         //消息类型
    WPARAM wParam,    //虚拟码
    LPARAM lParam) {  //按键信息
    //判断是否wParam与lParam都有键盘消息,是的话则执行打印操作
    if (code == HC_ACTION) {
        //将256个虚拟键的状态拷贝到指定的缓冲区中,如果成功则继续
        BYTE KeyState[256] = { 0 };
        if (GetKeyboardState(KeyState)) {
            //得到第16 - 23位,键盘虚拟码
            LONG KeyInfo = lParam;
            UINT keyCode = (KeyInfo >> 16) & 0x00ff;
            WCHAR wKeyCode = 0;
            ToAscii((UINT)wParam, keyCode, KeyState, (LPWORD)&wKeyCode, 0);
            //将其打印出来
            WCHAR szInfo[512] = { 0 };
            swprintf_s(szInfo, _countof(szInfo), L"Hook%c", (char)wKeyCode);
            OutputDebugString(szInfo);
            return 0;
        }
    }
    return CallNextHookEx(g_Hook, code, wParam, lParam);
}

3.2 附加作用

        一般来讲,我们会把设置消息钩子的函数放在一个DLL文件里。当成功钩住一个GUI线程后,只要有消息产生,系统就会进行检查。要是系统发现包含钩子函数的DLL,并不在当前进程内,便会自动将这个DLL强制加载到产生消息的进程中。正因为存在这样的机制,我们就能借助它实现DLL注入。 

四、自定义钩子

        钩子主要用来改变程序原本的运行步骤,让程序执行咱们编写的代码。除了使用钩子,直接修改程序代码,同样能达到这个目的。

        另外,有些函数存放在特定地方,调用这些函数前,得先找到它们的存储位置,获取函数地址。要是我们提前把这些位置存的函数地址,换成自己函数的地址,那么当程序调用目标函数时,实际上调用的就是咱们自己的函数。

        在修改代码,实现跳转操作时,JMP指令地址换算公式十分关键:
        地址偏移 = 目标地址 - JMP所在地址 - 5

五、完整示例代码

5.1 32位IAT hook代码

#include "stdafx.h"
#include <windows.h>

bool	hookIat( const char* pszDllName , const char* pszFunction , LPVOID pNewFunction );


// 盗版的MessageBox
DWORD WINAPI MyMessageBox( HWND hWnd , TCHAR* pText , TCHAR* pTitle , DWORD type ) {

	// 还原IAT
	hookIat( "User32.dll" , 
			 "MessageBoxW" , 
			 GetProcAddress(GetModuleHandleA("User32.dll"),"MessageBoxW" )
			 );

	// 调用原版函数
	MessageBox( 0 , L"在盗版的MessageBox中弹出此框" , L"提示" , 0 );


	// HOOK IAT
	hookIat( "User32.dll" , "MessageBoxW" , &MyMessageBox );

	return 0;
}



int _tmain(int argc, _TCHAR* argv[])
{
	MessageBox( 0 , L"正版API" , L"提示" , 0 );
	// HOOK IAT
	hookIat( "User32.dll" , "MessageBoxW" , &MyMessageBox );

	MessageBox( 0 , L"正版API" , L"提示" , 0 );
	MessageBox( 0 , L"正版API" , L"提示" , 0 );
	return 0;
}


bool hookIat( const char* pszDllName ,
			  const char* pszFunction ,
			  LPVOID pNewFunction ) {

	// PE文件中,所有的API的地址都保存到了导入表中.

	// 程序调用一个API时, 先会从导入表中得到API
	// 的地址, 再调用这个地址.
	// 如果将导入表中的API地址替换掉, 那么调用
	// API时, 就会调用被替换的地址.

	// HOOK IAT的步骤:
	// 1. 解析PE文件,找到导入表
	// 2. 找到导入表中对应的模块
	// 3. 找到对应模块的对应函数.
	// 4. 修改函数地址.

	// 导入表中有两张表, 一张是导入名称表, 一张是导入
	// 地址表, 这两张表的元素一一对应的.
	// 导入名称表中存放的是函数名
	// 导入地址表中存放的是函数地址.

	HANDLE hProc = GetCurrentProcess( );
	
	PIMAGE_DOS_HEADER			pDosHeader; // Dos头
	PIMAGE_NT_HEADERS			pNtHeader;	// Nt头
	PIMAGE_IMPORT_DESCRIPTOR	pImpTable;	// 导入表
	PIMAGE_THUNK_DATA			pInt;		// 导入表中的导入名称表
	PIMAGE_THUNK_DATA			pIat;		// 导入表中的导入地址表
	DWORD						dwSize;
	DWORD						hModule;
	char*						pFunctionName;
	DWORD						dwOldProtect;

	hModule = ( DWORD)GetModuleHandle( NULL );

	// 读取dos头
	pDosHeader = (PIMAGE_DOS_HEADER)hModule;

	// 读取Nt头
	pNtHeader = (PIMAGE_NT_HEADERS)( hModule + pDosHeader->e_lfanew );


	// 获取导入表
	pImpTable = ( PIMAGE_IMPORT_DESCRIPTOR )
		(hModule + pNtHeader->OptionalHeader.DataDirectory[1].VirtualAddress);

	// 遍历导入表
	while( pImpTable->FirstThunk != 0 && pImpTable->OriginalFirstThunk != 0 ) {


		// 判断是否找到了对应的模块名
		if( _stricmp( (char*)(pImpTable->Name+hModule) , pszDllName ) != 0 ) {
			++pImpTable;
			continue;
		}
		
		// 遍历名称表,找到函数名
		pInt = (PIMAGE_THUNK_DATA)( pImpTable->OriginalFirstThunk + hModule );
		pIat = (PIMAGE_THUNK_DATA)( pImpTable->FirstThunk + hModule );

		while( pInt->u1.AddressOfData != 0 ) {

			// 判断是以名称导入还是以需要导入
			if( pInt->u1.Ordinal & 0x80000000 == 1 ) {
				// 以序号导入

				// 判断是否找到了对应的函数序号
				if( pInt->u1.Ordinal == ( (DWORD)pszFunction ) & 0xFFFF ) {
					// 找到之后,将钩子函数的地址写入到iat
					VirtualProtect( &pIat->u1.Function ,
									4 ,
									PAGE_READWRITE ,
									&dwOldProtect
									);

					pIat->u1.Function = (DWORD)pNewFunction;

					VirtualProtect( &pIat->u1.Function ,
									4 ,
									dwOldProtect ,
									&dwOldProtect
									);
					return true;
				}
			}
			else {
				// 以名称导入
				pFunctionName = (char*)( pInt->u1.Function + hModule + 2);

				// 判断是否找到了对应的函数名
				if( strcmp( pszFunction , pFunctionName ) == 0 ) {

					VirtualProtect( &pIat->u1.Function ,
									4 ,
									PAGE_READWRITE ,
									&dwOldProtect
									);

					// 找到之后,将钩子函数的地址写入到iat
					pIat->u1.Function = (DWORD)pNewFunction;

					VirtualProtect( &pIat->u1.Function ,
									4 ,
									dwOldProtect ,
									&dwOldProtect
									);

					return true;
				}
			}
			
			++pIat;
			++pInt;
		}


		++pImpTable;
	}

	return false;
	
}

效果:

5.2 Win7 X64 下 HOOK OPENPROCESS

代码实现:

#include <windows.h>
#include <Psapi.h>

// 用于存储原始的 OpenProcess 函数地址
SIZE_T GetAddr = 0;

// 自定义的 OpenProcess 函数
HANDLE WINAPI NewOpenProcess(
    _In_  DWORD dwDesiredAccess,
    _In_  BOOL bInheritHandle,
    _In_  DWORD dwProcessId
) {
    // 这里可以先简单实现,后续可根据需求添加具体逻辑
    HANDLE hp = NULL;
    return hp;
}

// 针对 64 位系统的处理函数
#ifdef _M_AMD64
void fun() {
    // 获取 Kernel32.dll 模块句柄
    HMODULE hm = GetModuleHandleW(L"Kernel32.dll");
    // 获取 OpenProcess 函数地址并加上偏移
    SIZE_T addr = (SIZE_T)GetProcAddress(hm, "OpenProcess") + 8; 
    BYTE oldCode[7] = { 0 };
    // 保存原始代码
    memcpy_s(oldCode, 6, (void*)addr, 6);
    SIZE_T offset = 0;
    // 解析偏移
    memcpy_s(&offset, 4, (BYTE*)addr + 2, 4);
    size_t* funaddr = (size_t*)(addr + offset + 6);
    // 获取原始 NtOpenProcess 地址
    GetAddr = *funaddr; 
    size_t myfun = (size_t)NewOpenProcess;
    DWORD oldProtect;
    // 修改内存保护属性以便写入
    VirtualProtect((void*)funaddr, 10, PAGE_EXECUTE_READWRITE, &oldProtect);
    // 进行 IAT Hook
    memcpy_s(funaddr, 8, &myfun, 8); 
    // 恢复原始内存保护属性
    VirtualProtect((void*)funaddr, 10, oldProtect, &oldProtect);
}
// 针对 32 位系统的处理函数
#else
void fun() {
    // 获取 Kernel32.dll 模块句柄
    HMODULE hm = GetModuleHandleW(L"Kernel32.dll");
    // 获取 OpenProcess 函数地址并加上偏移
    SIZE_T addr = (SIZE_T)GetProcAddress(hm, "OpenProcess") + 13; 
    BYTE oldCode[7] = { 0 };
    // 保存原始代码
    memcpy_s(oldCode, 6, (void*)addr, 6);
    // 获取真正的 OpenProcess 地址
    GetAddr = **(DWORD**)(oldCode + 2); 
    // 计算跳转偏移
    SIZE_T offset = (SIZE_T)NewOpenProcess - addr - 5; 
    DWORD oldProtect;
    // 修改内存保护属性以便写入
    VirtualProtect((void*)addr, 7, PAGE_EXECUTE_READWRITE, &oldProtect);
    // 清空代码区域
    memset((BYTE*)addr, 0x90, 6); 
    BYTE* temp = (BYTE*)addr;
    // 设置跳转指令
    temp[0] = 0xe9; 
    // 写入跳转偏移
    memcpy(temp + 1, (BYTE*)&offset, 4); 
    // 恢复原始内存保护属性
    VirtualProtect((void*)addr, 7, oldProtect, &oldProtect);
}
#endif

// DLL 入口函数
BOOL APIENTRY DllMain(
    HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
) {
    static int num = 1;
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
        if (num == 1) {
            // 执行 Hook 操作
            fun(); 
        }
        num++;
        break;
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}    

效果: