- DWORD nLen = UserFuncAddr – SysFuncAddr - 指令大小;
- Jmp nLen;
- 1 SetWindowsHookEx(
- 2 WH_KEYBOARD, // 键盘消息
- 3 KeyboardProc, // 钩子函数(处理键盘输入的函数)
- 4 hInstance, // 钩子函数所在DLL的Handle
- 5 0 // 该参数用于设定要Hook的线程ID,为0时表示监视所有线程
- 6 )
\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Des ktop Item Position Saver
- // 获取类别(class)为ProgMan的窗口并校验
- HWND hWnd = GetFirstChild(GetFirstChild(FindWindow(TEXT("ProgMan"), NULL)));
- chASSERT(IsWindow(hWnd));
- // 设置将DLL注入资源管理器地址空间的钩子
- chVERIFY(SetMsgHook(GetWindowThreadProcessId(hWnd, NULL)));
- // 这个线程ID是ListView的父线程的线程ID, 也就是Explorer进程的子线程
- BOOL WINAPI SetMsgHook(DWORD dwThreadId) {
-
- BOOL bOk = FALSE;
-
- if (dwThreadId != 0) {
- // 校验是否已经注入
- chASSERT(g_hHook == NULL);
-
- // 保存当前DLL线程ID, 当server窗口创建完成GetMsgProc函数会post消息到这个线程
- g_dwThreadIdHook = GetCurrentThreadId();
-
- // 给指定线程安装消息钩子
- g_hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_hInstDll, dwThreadId);
-
- bOk = (g_hHook != NULL);
- if (bOk) {
- // 此时, hook已经安装成功; 强行Post Msg到Explorer
- // 进程的子线程的消息队列,触发间接调用Hook函数
- bOk = PostThreadMessage(dwThreadId, WM_NULL, 0, 0);
- }
- } else {
- chASSERT(g_hHook != NULL);
- bOk = UnhookWindowsHookEx(g_hHook);
- g_hHook = NULL;
- }
-
- return(bOk);
- }
- // 注意:这个线程属于Explorer进程
- LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam) {
- static BOOL bFirstTime = TRUE;
-
- if (bFirstTime) {
- bFirstTime = FALSE;
- // 创建Hook服务窗口处理客户端请求
- CreateDialog(g_hInstDll, MAKEINTRESOURCE(IDD_HOOK), NULL, Dlg_Proc);
-
- // 唤醒MsgHook进程
- PostThreadMessage(g_dwThreadIdHook, WM_NULL, 0, 0);
- }
-
- return(CallNextHookEx(g_hHook, nCode, wParam, lParam));
- }
- // 等待Hook服务窗口创建
- MSG msg;
- GetMessage(&msg, NULL, 0, 0);
-
- // 找到隐藏的服务窗口句柄
- HWND hWndHook = FindWindow(NULL, TEXT("Wintellect Hook"));
-
- // 确定窗口是否创建
- chASSERT(IsWindow(hWndHook));
- // 告诉服务窗口 ListView 窗口的元素需要Save或者Restore
- BOOL bSave = (cWhatToDo == TEXT('S'));
- SendMessage(hWndHook, WM_APP, (WPARAM) hWnd, bSave);
- INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
- switch (uMsg) {
- chHANDLE_DLGMSG(hWnd, WM_CLOSE, Dlg_OnClose);
- case WM_APP:
- if (lParam)
- SaveListViewItemPositions((HWND) wParam);
- else
- RestoreListViewItemPositions((HWND) wParam);
- break;
- }
-
- return(FALSE);
- }
- // 通知Hook窗口关闭, 必须先销毁对话框,再清除挂钩
- SendMessage(hWndHook, WM_CLOSE, 0, 0);
-
- chASSERT(!IsWindow(hWnd));
-
- // 卸载钩子, 从Explorer进程的地址空间移除Hook对话框
- SetMsgHook(0);


- // 附加进程, 将目标进程附加在当前进程准备进行调试
- // DebugActiveProcess是一个函数程序,使调试器附加到一个活动进程并且调试它。
- if (!DebugActiveProcess(dwProcessID)) {
- printf("DebugActiveProcess(%d) failed!!!\n"
- "Error Code = %d\n", dwProcessID, GetLastError());
- return 1;
- }
- DEBUG_EVENT DebugEvent;
- DWORD dwContinueStatus; // 等待调试事件
-
- while (WaitForDebugEvent(&DebugEvent, INFINITE)) {
- dwContinueStatus = DBG_CONTINUE; // 调试事件为创建进程, 首次
-
- if (CREATE_PROCESS_DEBUG_EVENT == DebugEvent.dwDebugEventCode) {
- OnCreateProcessDebugEvent(&DebugEvent);
- }
- }
- LPVOID WriteFileAddress = NULL;
- CREATE_PROCESS_DEBUG_INFO CreateProcessDebugInfomation;
- BYTE INT3 = 0xCC, OldByte = 0;
-
- BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pDebugEvent)
- {
- // WriteFile()函数地址
- WriteFileAddress = GetProcAddress(GetModuleHandleA("kernelbase.dll"), "WriteFile"); // 获得WriteFile()的地址
-
- // 将WriteFile()函数的首个字节改为0xCC
- memcpy(&CreateProcessDebugInfomation, &pDebugEvent->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
- ReadProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress, &OldByte, sizeof(BYTE), NULL); // 保存原函数首地址的首字节
- WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress, &INT3, sizeof(BYTE), NULL); // 写入0xCC(调试中断指令),下软件断点。
-
- return TRUE;
- }
只要pcs_subject.exe有写log操作,就会触发调用WriteFile函数,从而触发软中断指令。
- // 调试事件入口, 需要被调试进程触发 只要触发WriteFile()函数地址就会进来
- else if (EXCEPTION_DEBUG_EVENT == DebugEvent.dwDebugEventCode) {
- if (OnExceptionDebugEvent(&DebugEvent))
- continue;
- }
接着就是具体实现了。先恢复,以免进入死循环 主要是为了避免多次进入。
- BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pDebugEvent)
- {
- CONTEXT Context;
- PBYTE lpBuffer = NULL;
- DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
- PEXCEPTION_RECORD pExceptionRecord = &pDebugEvent->u.Exception.ExceptionRecord;
-
- // 软件终端异常
- if (EXCEPTION_BREAKPOINT == pExceptionRecord->ExceptionCode)
- {
- // 确认发生异常的地方是否为我们要钩取的WriteFile()函数
- if (WriteFileAddress == pExceptionRecord->ExceptionAddress)
- {
- // 1. Unhook 先恢复,以免进入死循环 主要是为了避免多次进入
- WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress,
- &OldByte, sizeof(BYTE), NULL);
获得线程上下背景文 为了修改Eip的值,来使进程恢复正常运行。
- // 2. 获得线程上下背景文 为了修改Eip的值,来使进程恢复正常运行
- Context.ContextFlags = CONTEXT_CONTROL;
- GetThreadContext(CreateProcessDebugInfomation.hThread, &Context);
根据ESP寄存器来获得WriteFile()函数的参数,以达到修改数据的目的。
- // 3. 根据ESP寄存器来获得WriteFile()函数的参数,以达到修改数据的目的
- /*
- BOOL WriteFile(
- HANDLE hFile,//文件句柄
- LPCVOID lpBuffer,//数据缓存区指针
- DWORD nNumberOfBytesToWrite,//你要写的字节数
- LPDWORD lpNumberOfBytesWritten,//用于保存实际写入字节数的存储区域的指针
- LPOVERLAPPED lpOverlapped//OVERLAPPED结构体指针
- );
- */
- ReadProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)(Context.Esp + 0x8), // 此参数是存缓冲区的起始地址
- &dwAddrOfBuffer, sizeof(DWORD), NULL);
- ReadProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)(Context.Esp + 0xC), // 此参数是存缓冲区的大小
- &dwNumOfBytesToWrite, sizeof(DWORD), NULL);
获取数据缓冲区的地址和大小。
- // 4. 获取数据缓冲区的地址和大小
- lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);
- memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);
将其内容读到调试器进程空间,控制台打印。
- // 5. 将其内容读到调试器进程空间
- ReadProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)dwAddrOfBuffer,
- lpBuffer, dwNumOfBytesToWrite, NULL);
- printf("\n### original string ###\n%s\n", lpBuffer);
修改数据:把小写字母改为'@'字符,控制台打印。
- //6. 修改数据:把所有小写字母改为'@'字符
- for (i = 0; i < dwNumOfBytesToWrite; i++) {
- if (0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)
- //lpBuffer[i] -= 0x20;
- lpBuffer[i] = '@';
- }
-
- printf("\n### converted string ###\n%s\n", lpBuffer);
将修改后的数据写回进程的地址空间,如图8所示。
- // 7. 然后将修改后的大写字母覆写到原位置。
- WriteProcessMemory(CreateProcessDebugInfomation.hProcess, (LPVOID)dwAddrOfBuffer,
- lpBuffer, dwNumOfBytesToWrite, NULL);
- free(lpBuffer);
两次打印对比如下图,发先log已经被修改了。

脱钩,将API 函数的第一个字节恢复。把线程上下文的EIP地址修改为WriteFile()的起始地址,注意EIP当前的值为0xcc的下一条指令的地址,运行相应的API。
- // 设置EIP的值来实现正常运行,注意EIP的值为0xCC的下一条指令的地址。
- Context.Eip = (DWORD)WriteFileAddress;
- SetThreadContext(CreateProcessDebugInfomation.hThread, &Context);
- // 运行
- ContinueDebugEvent(pDebugEvent->dwProcessId, pDebugEvent->dwThreadId, DBG_CONTINUE);
- Sleep(0);
再次修改为0xCC,为了继续钩取
- // 再次钩取
- WriteProcessMemory(CreateProcessDebugInfomation.hProcess, WriteFileAddress,
- &INT3, sizeof(BYTE), NULL);
至此,我们成功实现了基于调试技术的API Hook。当然,通过这种方式可以Hook的进程很多,在这里只讲一个基础的例子,有时间大家可以自己去尝试。
以下三种Hook形式本质上都是通过改写函数的入口地址,使得执行流切换到自定义函数。
1)InLine Hook
(1)原理。
内联Hook直接修改内存中的任意函数的代码,将其劫持至Hook API。它的适用范围更广,比较简单,因为只要是内存中有的函数它都能Hook。
(2)案例。
效果为以下将用一个demo简单说明Inline Hook的基本原理。很简单,没有DLL注入,仅仅是Hook了我自己的一个模块的API,修改接口计算结果,这里先看下demo效果图,下面将会贴上代码以及详细解析,如图9所示。

测试机器:Win10 x64,测试软件x86
(3)实现。
add.dll实现add函数,返回两个int值相加后的结果; Hook.dll实现了具体Hook细节,含安装卸载钩子以及Hook函数的实现;CallAdd进程实现了加载dll UI入口。
首先,我们先要找到需要Hook的函数原型(不同的调用约定下的函数修饰后的符号有区别) Windows下可以用这个命令获取Dll所有导出符号,找到自己想要的就行:dumpbin /exports 目录/文件.dll,结果如图10所示。

接下来看下add.dll的导出接口,这个就是我们后面即将Hook的接口,导出符号如上图。
- #ifdef ADD_EXPORTS
- #define ADD_API __declspec(dllexport)
- #else
- #define ADD_API __declspec(dllimport)
- #endif
-
- #ifdef __cplusplus //如果是c++文件,就将endif内的代码用c编译器编译
- extern "C" {
- #endif
- __declspec(dllexport) int WINAPI add(int a, int b) //__declspec(dllexport) 声明此函数为导出函数
- {
- return a + b;
- }
- #ifdef __cplusplus
- }
- #endif
接着,点击"开启钩子"按钮,开始加载Hook.dll
- HINSTANCE hinst = NULL;
- void CCallAddDlg::OnBnClickedButtonStartHook()
- {
- typedef BOOL(CALLBACK* inshook)(); // 函数原型定义
- inshook insthook;
-
- hinst = LoadLibrary(_T("Hook.dll")); // 加载dll文件
- if (hinst == NULL)
- {
- AfxMessageBox(_T("no Hook.dll!"));
- return;
- }
dll初始化开始安装钩子。
- // CHookApp 初始化
- BOOL CHookApp::InitInstance()
- {
- CWinApp::InitInstance();
-
- // 获得dll 实例,进程句柄
- hinst = ::AfxGetInstanceHandle();
- DWORD dwPid = ::GetCurrentProcessId();
- hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, dwPid);
-
- // 调用注射函数
- Inject();
- return TRUE;
- }
接下来就是比较核心的组织汇编代码、替换函数地址的逻辑了。 保证只注射一次,获取_add@8符号对应的地址,先保存这个地址,将JMP指令0xE9存入NewCode的首地址,然后将MyAdd的地址拼接进去。然后就可以开启钩子了。
- void Inject()
- {
- if (m_bInjected == false)
- { // 保证只调用1次
- m_bInjected = true;
-
- // 获取add.dll中的add()函数
- HMODULE hmod = ::LoadLibrary(_T("add.dll"));
- if (hmod == NULL) {
- return;
- }
-
- add = (AddProc)::GetProcAddress(hmod, "_add@8");
- pfadd = (FARPROC)add;
-
- if (pfadd == NULL)
- {
- AfxMessageBox(L"cannot locate add()");
- }
-
- // 将add()中的入口代码保存入OldCode[]
- _asm
- {
- lea edi, OldCode
- mov esi, pfadd
- cld
- /*
- movsd(dword==>四个字节)
- movsw(word==>两个字节)
- movsb(byte==>一个字节)
- */
- movsd
- movsb
- }
-
- NewCode[0] = 0xe9; // 实际上0xe9就相当于jmp指令
-
- // 获取Myadd()的相对地址
- _asm
- {
- lea eax, Myadd
- mov ebx, pfadd
- sub eax, ebx
- sub eax, 5
- mov dword ptr[NewCode + 1], eax
- }
-
- // 填充完毕,现在NewCode[]里的指令相当于Jmp Myadd
- HookOn(); // 可以开启钩子了
- }
- }
下面是开启钩子的代码如下:
- // 开启钩子的函数
- void HookOn()
- {
- ASSERT(hProcess != NULL);
-
- DWORD dwTemp = 0;
- DWORD dwOldProtect;
-
- // 将内存保护模式改为可写,老模式保存入dwOldProtect
- VirtualProtectEx(hProcess, pfadd, 5, PAGE_READWRITE, &dwOldProtect);
- // 将所属进程中add()的前5个字节改为Jmp Myadd
- WriteProcessMemory(hProcess, pfadd, NewCode, 5, 0);
- // 将内存保护模式改回为dwOldProtect
- VirtualProtectEx(hProcess, pfadd, 5, dwOldProtect, &dwTemp);
-
- bHook = true;
- }
钩子开启完成之后,回来继续点击"执行函数"按钮,此时add的地址已经被修改了。
- void CCallAddDlg::OnAddBnClickedButton()
- {
- HINSTANCE hAddDll = NULL;
- typedef int (WINAPI* AddProc)(int a, int b); // 函数原型定义
- AddProc add;
-
- if (hAddDll == NULL)
- {
- hAddDll = ::LoadLibrary(_T("add.dll")); // 加载dll
- }
-
- if (hAddDll == NULL) {
- return;
- }
-
- add = (AddProc)::GetProcAddress(hAddDll, "_add@8"); // 获取函数add地址
-
- int a = 1;
- int b = 2;
- int c = add(a, b); // 调用函数
-
- CString tem;
- tem.Format(_T("%d+%d=%d"), a, b, c);
- AfxMessageBox(tem);
- }
所以调用会直接跳转到下面这个函数中来,注意这里需要先HookOff卸载钩子。不然会自己调自己,造成死循环,拿到计算结果后再次开启钩子。
- // 然后,写我们自己的Myadd()函数
- int WINAPI Myadd(int a, int b)
- {
- // 截获了对add()的调用,我们给a加10
- a = a - 10;
-
- HookOff(); // 关掉Myadd()钩子防止死循环
-
- int ret;
- ret = add(a, b);
-
- HookOn(); // 开启Myadd()钩子
-
- return ret;
- }
然后,点击"卸载钩子"按钮,卸载钩子。
- void CCallAddDlg::OnBnClickedButtonStopHook()
- {
- if (hinst == NULL)
- {
- return;
- }
-
- typedef BOOL(CALLBACK* UnhookProc)(); // 函数原型定义
- UnhookProc UninstallHook;
-
- UninstallHook = ::GetProcAddress(hinst, "UninstallHook");// 获取函数地址
- if (UninstallHook != NULL)
- {
- UninstallHook();
- }
-
- if (hinst != NULL)
- {
- ::FreeLibrary(hinst);
- }
- }
-
- // 卸载鼠标钩子函数
- void UninstallHook()
- {
- if (hhk != NULL)
- {
- ::UnhookWindowsHookEx(hhk);
- }
-
- HookOff(); // 记得恢复原函数入口
- }
将之前保存的add函数的地址恢复,记得修改内存属性,否则会失败。
- // 关闭钩子的函数
- void HookOff() // 将所属进程中add()的入口代码恢复
- {
- ASSERT(hProcess != NULL);
-
- DWORD dwTemp = 0;
- DWORD dwOldProtect;
-
- VirtualProtectEx(hProcess, pfadd, 5, PAGE_READWRITE, &dwOldProtect);
- WriteProcessMemory(hProcess, pfadd, OldCode, 5, 0);
- VirtualProtectEx(hProcess, pfadd, 5, dwOldProtect, &dwTemp);
- bHook = false;
- }
再次点击"执行函数"按钮,发现调用原始接口,数据恢复为原始结果。
另外,对于c++虚函数Hook,虚函数调用是从虚函数表里面获得的函数地址进行调用的。因此对于Hook这类函数,就需要改写它的虚函数表了。一般来说,对于某个含有虚函数表的C++类,this指针指向的地址,取值就是虚函数表指针。虚函数表指针指向了虚函数表,里面的每一个元素都指向了实际要调用的函数的地址。因此,可以按照这样的方式访问虚函数表指针:
int** pVTable = (int**)this;
也就是将指向对象的指针强制转化成指针的指针,这样就可以通过取值就可以访问虚函数表:
- (*pVTable)[0] = address of virtual function 1;
- (*pVTable)[1] = address of virtual function 2;
- ...
因此,我们就可以改写虚函数的地址了,从而达到Hook的目的。
2)Hotfix Hook
从上节对Inline Hook方法的讲解中,我们会发现Inline Hook存在一个效率的问题,因为每次Inline Hook都要进行“挂钩+脱钩”的操作,也就是要对API的前5字节修改两次,这样,当我们要进行全局Hook的时候,系统运行效率会受影响。而且,当一个线程尝试运行某段代码时,若另一个线程正在对该段代码进行“写”操作,这时就会程序冲突,最终引发一些错误。
因此,使用HotFix Hook("热补丁")方法。如我们的大班云学生端app的热修复,原理是一样的。
测试机器:Win10 x64,测试dll: XP系统的
(1)原理为API的起始代码上都有这样的特色,5个NOP(空)指令,1个“MOV EDI,EDI”(占2字节),这7字节的指令实际没有任何意义,所以能够经过修改这7字节来实现HOOK操做,这种方法可使得进程处于运行状态时临时更改进程内存中的库文件,所以被称为打“热补丁”。在上述5字节代码修改技术中,脱钩是为了调用原函数,但使用HotFix Hook API时,在API代码被修改的状态下仍然可以正常的调用原API(从[原API起始地址+2]开始,仍能正常调用原API,且执行动作一致)。这种方法因为可以在进程处于运行状态时临时更改进程内存中的库文件,所以微软也常用这种方法来打“热补丁”。
该技术难的地方在于计算偏移地址。由于HotFix Hook需要修改7个字节的代码,所以并不是所有API都适用这种方法,若不适用,请使用5字节代码修改技术。
(2)现状为下图是用OllyDbg打开user32.dll,这个dll用的是网上下载的Xp的。因为我自己本机是Win10系统,这个dll可能是被更新掉了,没有找到可用的Hook点。网上下载的Xp的看起来是有的,估计是后面windows版本对dll安全性升级了。看来在Windows10系统上面这种Hook方式很难实现了。Win7/Win8没试过,有兴趣可以自行尝试,如图11所示。

下面两张图是XP的跟Win10的文件信息的对比,如图12与图13所示。


那么,这个Hook类型就不再展示代码案例了。了解一下有这么个方式就行,不过Win10肯定有可以Hook的API,没有找到而已。不过,重在了解原理,代码实现跟之前的大同小异。
3)SSDT(内核) Hook
SSDT Hook属于内核层Hook,也是最底层的Hook。由于用户层的API最后实质也是调用内核API,所以该Hook方法最为强大。不过值得注意的是内核通SSDT(System Service Descriptor Table)调用各种内核函数,SSDT就是一个函数表,只要得到一个索引值,就能根据这个索引值在该表中得到想要的函数地址。本质上其实内核层Hook并没想象中的那么高大上,Hook的原理相同,只不过Hook的对象不一样罢了。

当前安全软件很多也用到了 SSDT Hook技术来实现对系统的安全防护。例如图14所示,是360主动防御进程对 SSDT 的一个 Hook,Hook的目的是“取得系统R0权限,当有进程要结束自己的时候进行拦截,然后给出提示,拒绝访问”。比如上图:结束进程是由NtTerminateProcess 来完成的,Hook这个内核函数,那么在进程结束前,就有机会更改结果了,可以拒绝被结束。