一般来说,使用Detours 的代码都具有固定的模式。 Detours 1.5 和 Detours 2.1 的接口函数变了很多,这里按照 2.1 版本对基本的使用方法进行说明。常用的函数有下面几个:
DetourTransactionBegin():开始一次 截获 或者 解除截获 过程。
DetourUpdateThread():列入一个在 DetourTransaction 过程中要进行 update 的线程。这个函数的作用稍微有一些复杂,会在后面专门说明。
DetourAttach() : 添加一个要 截获 的 目标 函数。
DetourDetach(): 用来解除截获 的函数。
DetourTransactionCommit():执行当前的 Transaction 过程。在这个函数中才会真正进行 截获 或者 解除截获 操作。前面三个函数都只是做一些记录工作。
在使用的时候,这几个函数的调用步骤基本上也是按照上面列出来的顺序。举例来说, 我们要截获 API函数 MessageBoxA ,将消息框弹出的消息修改掉,可以按下面的方法做 (这里是 DLL注入的方式) 。
先写一个调用了 MessageBoxA的程序。 建立一个MFC 对话框工程 (在建立向导中选 "Dialog based",其余默认) , 打开 Resource View中的对话框 IDD_MESSAGEBOXAPP_DIALOG ,添加 OK按钮的单击 处理 事件OnBnClickedOk (),在其 中加入对MessageBoxA 函数的调用, 如下:
- void CMessageBoxAppDlg::OnBnClickedOk()
- {
- // TODO: Add your control notification handler code here
- ::MessageBoxA(NULL, "Execute target function: MessageBoxA", "Info", MB_OK | MB_ICONINFORMATION);
- OnOK();
- }
编译后的程序名称 为 MessageBoxApp .exe,运行它并单击 OK 按钮时会弹出相应消息框。现在我们要截获这个 EXE 文件中对 MessageBoxA 函数的调用,以跳转到我们自己定义的 D 函数处。我们需要先编写一个 DLL 以拦截目标函数,然后将该 DLL 注入到目标程序 MessageBoxApp .exe的空间中。
1、编写目标函数的拦截 DLL
建立一个 Win32的 DLL 工程(在 Win32 应用程序向导中选 Dll ),名为 ApiHook ,也可以不用 IDE 而是用原始的记事本来编写 DLL 。把 detours.h 和 detours.lib 、 detoured.lib 拷贝到工程目录下。源文件 ApiHook.cpp 的代码如下:
- // ApiHook.cpp : Defines the entry point for the DLL application.
- #include "stdafx.h"
- #include <stdio.h>
- #include "detours.h"
- #pragma comment(lib,"detours.lib")
- #pragma comment(lib,"detoured.lib")
- #ifdef _MANAGED
- #pragma managed(push, off)
- #endif
- //目标函数原型声明
- typedef int (WINAPI* pfnMessageBoxA)(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType);
- //声明一个指向目标函数的指针
- pfnMessageBoxA g_pMessageBoxA=::MessageBoxA;
- //截获函数
- int WINAPI HookMessageBoxA(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType){
- return g_pMessageBoxA(hWnd,"Target function /"MessageBoxA/" detoured./nExecute our HookMessageBoxA","Test Detour",MB_OK | MB_ICONINFORMATION);
- }
- //截获操作
- __declspec(dllexport) BOOL StartHook(){
- DetourTransactionBegin();
- DetourUpdateThread(GetCurrentThread()); //只有一个线程,所以用GetCurrentThread
- //关联目标函数和我们自定义的截获函数
- if(DetourAttach(&(PVOID&)g_pMessageBoxA,HookMessageBoxA)!=NO_ERROR){
- printf("HookMessageBoxA fail!/n");
- }
- //完成事务
- if(DetourTransactionCommit()!=NO_ERROR){
- printf("DetourTransactionCommit fail!/n");
- }else{
- printf("DetourTransactionCommit ok!/n");
- return TRUE;
- }
- return FALSE;
- }
- //解除截获操作
- __declspec(dllexport) BOOL StopHook(){
- DetourTransactionBegin();
- DetourUpdateThread(GetCurrentThread());
- //解除截获关系
- if(DetourDetach(&(PVOID&)g_pMessageBoxA,HookMessageBoxA)!=NO_ERROR){
- printf("HookMessageBoxA fail!/n");
- }
- //完成事务
- if(DetourTransactionCommit()!=NO_ERROR){
- printf("DetourTransactionCommit fail!/n");
- }else{
- printf("DetourTransactionCommit ok!/n");
- return TRUE;
- }
- return FALSE;
- }
- BOOL APIENTRY DllMain( HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
- {
- switch(ul_reason_for_call){
- case DLL_PROCESS_ATTACH:
- StartHook();
- break;
- case DLL_PROCESS_DETACH:
- StopHook();
- break;
- }
- return TRUE;
- }
- #ifdef _MANAGED
- #pragma managed(pop)
- #endif
拦截操作一般按如下步骤进行:
1) 首先需要定义目标函数的原型。如果目标函数是 Windows API ,可以到 MSDN 中查阅,但是需要注意 ANSI 版本和 Unicode 版本的区别。如果没有确切的原型声明,或者目标函数是通过逆向工程找出来的,那么需要定义一个和目标函数原型兼容的声明,即参数个数和调用约定要相同。如 MessageBoxA 的原型是:
- int MessageBoxA( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
则使用typedef 定义目标函数原型如下:
- typedef int (WINAPI *pfnMessageBoxA)( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
2) 定义指向目标函数的函数指针:
- pfnMessageBoxA g_pMessageBoxA = ::MessageBoxA;
3) 定义截获函数并编写代码,用于替换目标函数。
4) 调用 DetourTransactionBegin 开始一次 Detours 事务。
5) 对进程中每个可能调用到目标函数的线程,都需要使用 DetourUpdateThread 加入到 update 队列中。这是因为拦截时修改目标函数的前几个字节,如果某个线程刚好执行到这几个字节的位置时,粗暴的修改掉会造成该线程出现异常。 Detours 事务处理时,会先枚举并暂停 update 队列中所有线程,获取它们的指令指针,如果发现这种情况,则将指令指针修改到跳板代码的对应字节上。这样就避免出现崩溃的问题。
6) 对每个需要拦截的函数,调用 DetourAttach 加入到事务列表中。
7) 调用 DetourTransactionCommit 进行实际的拦截操作。
解除拦截的操作和上面的流程基本一样,只是第6 步改为调用 DetourDetach 函数。另外, Detours 还包含一系列其他函数,如果需要使用的话,可以参考 Detours 安装目录下的示例。
最后,在DLL 的加载事件加入拦截操作函数,在卸载事件中加入解除拦截的操作函数,把它编译成 ApiHook.dll 。
总体来说,Detours 库的代码是非常稳定的,但是如果使用方法不对,会造成一些问题。有下面一些地方需要特别注意:
1) 一定要枚举线程并调用 DetourUpdateThread 函数。否则可能出现很低几率的崩溃问题,这种问题很难被检查出来。
2) 如果拦截函数在 DLL 中,那么绝大多数情况下不能在 Unhook 之后卸载这个 DLL ,或者卸载存在造成崩溃的危险。因为某些线程的调用堆栈中可能还包含 Hook 函数,这时卸载掉 DLL ,调用堆栈返回到 Hook 函数时内存位置已经不是合法的代码了。
3) Detours库设计时并没有考虑到卸载的问题,这是因为钩子的卸载本身是不安全的。当 Detours 库代码存在于 DLL 中的时候,即使 Unhook 了所有函数,清理了所有自己使用到的函数,还是会占用一些内存。卸载这个 DLL 会造成内存泄露,特别是反复的进行加载 DLL->Hook->Unhook-> 卸载 DLL 的过程,会让这个问题变得非常严重。
4) 有一些非常短的目标函数是无法 Hook 的。因为 jmp 指令需要占用一定空间,有些函数太过短小,甚至不够 jmp 指令的长度,自然是没有办法 Hook 掉的。
5) Detours不支持 9x 内核的 Windows 系统。因为 9x 内核下的内存模型和 NT 内核下有非常大的差别。
2、将拦截 DLL 注入到目标应用程序中
为了将ApiHook.dll 注入到 MessageBoxApp.exe 中,这需要在 MessageBoxApp.exe 中加入 .detours 节。把 ApiHook.dll 以及 Detours 库中的 detoured.dll 拷贝到 MessageBoxApp.exe 所在目录(或者也可以将它们拷贝到 system32 目录等地方)。也就是我们在运行 MessageBoxApp.exe时要确保能访问到这些 DLL 。我们使用 Detours 自带的 setdll.exe 重写二进制可执行文件,只要使用命令 setdll /d:ApiHook.dll MessageBoxApp.exe 即可,在 MessageBoxApp.exe 中加入一个新的 .detours 节。这样再运行修改后的 MessageBoxApp.exe 即可看到拦截的结果。我们使用 depends.exe 观察 MessageBoxApp.exe 的变化,可以看到 MessageBoxApp.exe 加入了对 ApiHook.dll 的依赖关系。