跳至主要內容

59-API Hooking - Detours库

Maldevacademy大约 8 分钟安全开发

概述

Detours Hooking 库open in new window,是 Microsoft Research 开发的一个软件库,允许截取和重定向 Windows 中的函数调用。该库将特定函数的调用重定向到用户定义的替换函数,从而可以执行其他任务或修改原始函数的行为。Detours 通常用于 C/C++ 程序,并且适用于 32 位和 64 位应用程序。

该库的 wiki 页面此处open in new window提供。

事务

Detours 库用跳转到用户提供的 detour 函数(即代替执行的函数)的无条件跳转指令替换目标函数(即要挂钩的函数)的前几个指令。无条件跳转术语也称为 trampoline。

此库使用 事务 从目标函数安装和卸载挂钩。事务允许挂钩例程将多个函数挂钩组合在一起,并将其作为单个单元应用,这在对程序的行为进行多个更改时可能是有益的。它还具有使用户可以轻松撤消所有更改(如果需要)的优势。使用事务时,可以启动一个新事务、添加函数挂钩,然后提交。提交事务后,添加到该事务中的所有函数挂钩都将应用到程序中,就像取消挂钩的情况一样。

使用 Detours 库

要使用 Detours 库函数,必须下载并编译 Detours 存储库,以获取编译所需的静态库文件(.lib)。此外,还应包含 detours.hopen in new window 头文件,这在 Detours Wiki 的 使用 Detoursopen in new window 部分中有说明。

有关将 .lib 文件添加到项目的其他帮助,请查看 Microsoft 文档open in new window

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.libdetoursx86.lib 文件,当以 64 位项目编译 Detours 库时会创建 detoursx64.lib,同样,当以 32 位项目编译 Detours 库时会创建 detoursx86.lib

Detours API 函数

在使用任何钩子方法时,第一步总是获取要钩住的 WinAPI 函数的地址。函数的地址是确定将放置跳转指令的位置的必要条件。在这个模块中,MessageBoxA 函数将被用作要钩住的函数。

以下是 Detours 库提供的 API 函数:

上述函数返回一个 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

另一个值得一提的更通用的解决方法,是调用一个与目标函数具有相同功能的不同“未挂钩”函数。例如 MessageBoxAMessageBoxWVirtualAllocVirtualAllocEx

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
image

运行第二个 MessageBoxA(已钩取)

image
image

运行第三个 MessageBoxA(未钩取)

image
image