59-API Hooking - Detours库
概述
Detours Hooking 库,是 Microsoft Research 开发的一个软件库,允许截取和重定向 Windows 中的函数调用。该库将特定函数的调用重定向到用户定义的替换函数,从而可以执行其他任务或修改原始函数的行为。Detours 通常用于 C/C++ 程序,并且适用于 32 位和 64 位应用程序。
该库的 wiki 页面此处提供。
事务
Detours 库用跳转到用户提供的 detour 函数(即代替执行的函数)的无条件跳转指令替换目标函数(即要挂钩的函数)的前几个指令。无条件跳转术语也称为 trampoline。
此库使用 事务 从目标函数安装和卸载挂钩。事务允许挂钩例程将多个函数挂钩组合在一起,并将其作为单个单元应用,这在对程序的行为进行多个更改时可能是有益的。它还具有使用户可以轻松撤消所有更改(如果需要)的优势。使用事务时,可以启动一个新事务、添加函数挂钩,然后提交。提交事务后,添加到该事务中的所有函数挂钩都将应用到程序中,就像取消挂钩的情况一样。
使用 Detours 库
要使用 Detours 库函数,必须下载并编译 Detours 存储库,以获取编译所需的静态库文件(.lib)。此外,还应包含 detours.h 头文件,这在 Detours Wiki 的 使用 Detours 部分中有说明。
有关将 .lib 文件添加到项目的其他帮助,请查看 Microsoft 文档。
32 位与 64 位 Detours 库
此模块中的共享代码包含预处理器代码,该代码根据所用计算机的架构确定要包含哪个版本的 Detours .lib
文件。为此,使用了宏 _M_X64
和 _M_IX86
。这些宏由编译器定义,以指示计算机运行的是 64 位还是 32 位版本的 Windows。预处理器代码如下所示:
// 如果编译为 64 位
#ifdef _M_X64
#pragma comment (lib, "detoursx64.lib")
#endif // _M_X64
// 如果编译为 32 位
#ifdef _M_IX86
#pragma comment (lib, "detoursx86.lib")
#endif // _M_IX86
#ifdef _M_X64
检查宏 _M_X64
是否已定义,如果已定义,则后面的代码将包含在编译中。如果未定义,则将忽略该代码。类似地,#ifdef _M_IX86
检查宏 _M_IX86
是否已定义,如果已定义,则后面的代码将包含在编译中。#pragma comment(lib, "detoursx64.lib")
用于在编译 64 位系统期间链接 detoursx64.lib 库,而 #pragma comment(lib, "detoursx86.lib")
用于在编译 32 位系统期间链接 detoursx86.lib 库。
在编译 Detours 库时会创建 detoursx64.lib 和 detoursx86.lib 文件,当以 64 位项目编译 Detours 库时会创建 detoursx64.lib,同样,当以 32 位项目编译 Detours 库时会创建 detoursx86.lib。
Detours API 函数
在使用任何钩子方法时,第一步总是获取要钩住的 WinAPI 函数的地址。函数的地址是确定将放置跳转指令的位置的必要条件。在这个模块中,MessageBoxA
函数将被用作要钩住的函数。
以下是 Detours 库提供的 API 函数:
- DetourTransactionBegin - 开始一个新的事务用于附加或分离钩子。钩住和解钩时应首先调用此函数。
- DetourUpdateThread - 更新当前事务。Detours 库使用它将一个线程“登记”到当前事务。
- DetourAttach - 在当前事务中,将钩子安装到目标函数。在调用
DetourTransactionCommit
之前不会提交此钩子。 - DetourDetach - 在当前事务中,从目标函数中移除钩子。在调用
DetourTransactionCommit
之前不会提交此钩子。 - DetourTransactionCommit - 提交当前附加或分离钩子的事务。
上述函数返回一个 LONG
值,用于理解函数执行的结果。Detours API 将在成功时返回 NO_ERROR
(即 0),在失败时返回一个非零值。非零值可用作调试目的的错误代码。
替换已经挂钩的 API
下一步是创建一个函数来替换已经挂钩的 API。替换函数应该具有相同的数据类型,并且可以选择使用相同参数,这允许检查或修改参数值。例如,以下函数可以用作 MessageBoxA
的钩子函数,允许检查原始参数值。
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
// 这里可以检查 hWnd - lpText - lpCaption - uType 参数
}
需要注意的是,替换函数可以使用较少参数,但不能使用比原始函数更多的参数,因为这样会访问无效地址,并抛出访问冲突异常。
无限循环问题
当连接到一个函数并触发钩子时,会执行自定义函数。但是,为了继续执行,自定义函数必须返回原始挂钩函数应返回的有效值。一种简单的做法是在挂钩中通过调用原始函数来返回相同的值。这可能会导致问题,因为会调用替换函数,从而导致无限循环。这是一个通用的挂钩问题,而不是 Detours 库中的错误。
为了更好地理解这一点,下面的代码段展示了替换函数 MyMessageBoxA
调用 MessageBoxA
。这样会导致无限循环。程序会陷入运行 MyMessageBoxA
的状态,这是因为 MyMessageBoxA
正在调用 MessageBoxA
,而 MessageBoxA
又会再次指向 MyMessageBoxA
函数。
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
// 打印原始参数值
printf("Original lpText Parameter : %s\n", lpText);
printf("Original lpCaption Parameter : %s\n", lpCaption);
// 不要这样做
// 更改参数值
return MessageBoxA(hWnd, "different lpText", "different lpCaption", uType); // 调用 MessageBoxA(已挂钩)
}
解决方案一:全局原始函数指针
Detour 库可以通过在挂钩函数之前保存指向原始函数的指针来解决此问题。该指针可以存储在全局变量中,并在 detour 函数中调用,而不是挂钩函数。
// 用作在 `MyMessageBoxA` 中未挂钩的 MessageBoxA
fnMessageBoxA g_pMessageBoxA = MessageBoxA;
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
// 打印原始参数值
printf("原始 lpText 参数:%s\n", lpText);
printf("原始 lpCaption 参数:%s\n", lpCaption);
// 更改参数值
// 调用未挂钩的 MessageBoxA
return g_pMessageBoxA(hWnd, "不同的 lpText", "不同的 lpCaption", uType);
}
解决方案 2 - 使用不同的 API
另一个值得一提的更通用的解决方法,是调用一个与目标函数具有相同功能的不同“未挂钩”函数。例如 MessageBoxA
和 MessageBoxW
、VirtualAlloc
和 VirtualAllocEx
。
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
// 打印原始参数值
printf("原始 lpText 参数 :%s\n", lpText);
printf("原始 lpCaption 参数:%s\n", lpCaption);
// 修改参数值
return MessageBoxW(hWnd, L"不同的 lpText", L"不同的 lpCaption", uType);
}
Detours 钩子函数
如前所述,Detours 库通过事务来工作,因此要钩取一个 API 函数,必须创建一个事务、提交一个操作(钩取/解钩)到事务,然后提交事务。下面的代码片段执行了这些步骤。
// 在 `MyMessageBoxA` 中用作未钩取的 MessageBoxA
// 并被 `DetourAttach` 和 `DetourDetach` 使用
fnMessageBoxA g_pMessageBoxA = MessageBoxA;
// 钩取后将在 MessageBoxA 代替运行的函数
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
printf("[+] 原始参数:\n");
printf("\t - lpText:%s\n", lpText);
printf("\t - lpCaption:%s\n", lpCaption);
return g_pMessageBoxA(hWnd, "不同的 lpText", "不同的 lpCaption", uType);
}
BOOL InstallHook() {
DWORD dwDetoursErr = NULL;
// 创建事务并更新它
if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
printf("[!] DetourTransactionBegin 失败,错误为:%d\n", dwDetoursErr);
return FALSE;
}
if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
printf("[!] DetourUpdateThread 失败,错误为:%d\n", dwDetoursErr);
return FALSE;
}
// 在 g_pMessageBoxA 代替执行 MyMessageBoxA,g_pMessageBoxA 就是 MessageBoxA
if ((dwDetoursErr = DetourAttach((PVOID)&g_pMessageBoxA, MyMessageBoxA)) != NO_ERROR) {
printf("[!] DetourAttach 失败,错误为:%d\n", dwDetoursErr);
return FALSE;
}
// 实际的钩子会在 `DetourTransactionCommit` 之后安装——提交事务
if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
printf("[!] DetourTransactionCommit 失败,错误为:%d\n", dwDetoursErr);
return FALSE;
}
return TRUE;
}
Detours 取消挂钩例程
以下代码段展示了与上一节相同的例程,但这是用于取消挂钩的。
// 用于作为在 `MyMessageBoxA` 中取消挂钩的 MessageBoxA
// 并由 `DetourAttach` 和 `DetourDetach` 使用
fnMessageBoxA g_pMessageBoxA = MessageBoxA;
// 在挂钩时将代替 MessageBoxA 运行的函数
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
printf("[+] 原始参数 : \n");
printf("\t - lpText : %s\n", lpText);
printf("\t - lpCaption : %s\n", lpCaption);
return g_pMessageBoxA(hWnd, "不同的 lpText", "不同的 lpCaption", uType);
}
BOOL Unhook() {
DWORD dwDetoursErr = NULL;
// 创建事务并更新它
if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
printf("[!] DetourTransactionBegin 出错,错误码 : %d \n", dwDetoursErr);
return FALSE;
}
if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
printf("[!] DetourUpdateThread 出错,错误码 : %d \n", dwDetoursErr);
return FALSE;
}
// 从 MessageBoxA 中移除挂钩
if ((dwDetoursErr = DetourDetach((PVOID)&g_pMessageBoxA, MyMessageBoxA)) != NO_ERROR) {
printf("[!] DetourDetach 出错,错误码 : %d \n", dwDetoursErr);
return FALSE;
}
// 实际的挂钩移除发生在 `DetourTransactionCommit` 之后 - 提交事务
if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
printf("[!] DetourTransactionCommit 出错,错误码 : %d \n", dwDetoursErr);
return FALSE;
}
return TRUE;
}
主函数
前面展示的挂钩和取消挂钩的例程不包括主函数。下面展示了主函数,它仅从挂钩和未挂钩版本调用 MessageBoxA
。
int main() {
// 直接运行,未挂钩
MessageBoxA(NULL, "您如何看待恶意软件开发?", "原始 MsgBox", MB_OK | MB_ICONQUESTION);
//------------------------------------------------------------------
// 挂钩
if (!InstallHook())
return -1;
//------------------------------------------------------------------
// 不会直接运行 - 将运行 MyMessageBoxA
MessageBoxA(NULL, "恶意软件开发是错误的", "原始 MsgBox", MB_OK | MB_ICONWARNING);
//------------------------------------------------------------------
// 取消挂钩
if (!Unhook())
return -1;
//------------------------------------------------------------------
// 直接运行,已取消挂钩
MessageBoxA(NULL, "正常 MsgBox 已恢复", "原始 MsgBox", MB_OK | MB_ICONINFORMATION);
return 0;
}
演示
运行第一个 MessageBoxA(未钩取)
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/detours-113692112-13168cc0-dd84-4b71-9c9a-c639b6bcd3e8.png)
运行第二个 MessageBoxA(已钩取)
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/detours-213692174-164b9d16-059a-4587-a4d2-3e264f3ac539.png)
运行第三个 MessageBoxA(未钩取)
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/detours-313692221-be94d5d0-34a4-42a9-9545-a4934e5878ef.png)