跳至主要內容

61-API 挂接 - 自定义代码

Maldevacademy大约 8 分钟安全开发

导言

到目前为止,开源库已被用于实现 API 挂钩。然而,这种方法的一个主要问题是这些库的源代码是公开可用的,使得安全研究人员和安全产品供应商可以很直接地构建 IoC。因此,本模块将手动实现 API 挂钩,虽然不如前面演示的库复杂,但足以在没有 IoC 的情况下实现预期结果。

如果只想挂钩单个函数,自定义挂钩代码会是一个更好的选择。这样可以避免链接其他库的额外工作,以及避免这些库给二进制文件大小带来的额外负担。

创建跳转壳码

一种实现函数钩子的方法是改写其前几条指令,用我们想执行的新指令覆盖。新指令通常称为跳转壳码,负责将函数的执行流转到替代函数。该跳转壳码通常是一个小的 jmp 壳码,它执行一个 jmp 指令,跳到要执行的函数的地址上。为了执行 jmp 指令,必须将要跳转到的地址保存在一个寄存器中。在给出的示例中,在 32 位处理器上使用的寄存器是 eax,在 64 位处理器上使用的寄存器是 r10。将地址保存在这些寄存器中时,会用到 mov 指令。

跳转壳码只需要一条 mov 指令和一条 jmp 指令即可。深入了解这些指令如何使用不在本模块的讨论范围内。如果你想进一步了解它们,可以访问 felixcloutier.com/x86/mov 和 felixcloutier.com/x86/jmp 以获取更多详细信息。

64 位跳转 Shellcode

64 位跳转 Shellcode 如下:

mov r10, pAddress  
jmp r10

其中 pAddress 是要跳转到的函数地址(例如 0x0000FFFEC32A300)。在代码中使用这些指令之前,必须先将其转换为 opcode

0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pAddress
0x41, 0xFF, 0xE2                                            // jmp r10

32 位 Jump Shellcode

32 位版本:

mov eax, pAddress  
jmp eax

同样,将指令转换为操作码。

0xB8, 0x00, 0x00, 0x00, 0x00,     // mov eax, pAddress
0xFF, 0xE0                        // jmp eax

请注意,pAddress 表示为 NULL,这就解释了 0x00 序列。这些 0x00 操作码是占位符,在运行时将被覆盖。

检索 pAddress

Hook 是在运行时安装的,因此必须在运行时检索并向 shellcode 添加 pAddress 值。可以使用 GetProcAddress 检索地址,一经完成,memcpy 用于将地址复制到 shellcode 中的正确位置。

64 位补丁

uint8_t		uTrampoline[] = {
			0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 将 r10 寄存器设置为 pFunctionToRun 的值
			0x41, 0xFF, 0xE2                                            // 跳转到 r10 寄存器的值
};

uint64_t uPatch = (uint64_t)pAddress;
memcpy(&uTrampoline[2], &uPatch, sizeof(uPatch)); // 将地址复制到 uTrampoline 中偏移量为 '2' 的位置

32 位补丁

uint8_t		uTrampoline[] = {
	   0xB8, 0x00, 0x00, 0x00, 0x00,     // mov eax, pFunctionToRun
	   0xFF, 0xE0                        // jmp eax
};
  
uint32_t uPatch = (uint32_t)pAddress;
memcpy(&uTrampoline[1], &uPatch, sizeof(uPatch)); // 将地址复制到 uTrampoline 中的偏移量“1”处

如前所述,pAddress是目标函数的地址。uint32_tuint64_t数据类型用于确保地址为正确数量的字节,即32位机器为4字节,64位机器为8字节。uint32_t的大小为4字节,uint64_t的大小为8字节。memcpy将通过覆盖0x00占位字节,将地址放入跳转代码中。

编写跳转代码

在使用准备好的 shellcode 覆盖目标函数的前几个指令之前,将跳转代码要写入的内存空间标记为可写非常重要。在大多数情况下,内存区域不可写,需要使用 VirtualProtect WinAPI 将内存权限更改为 PAGE_EXECUTE_READWRITE。值得注意的是,该内存区域必须可写且可执行,因为当程序调用该函数时,它需要执行在只写内存中不允许的指令。

考虑到这一点,跳转代码应首先修改目标函数的权限,然后再复制 shellcode。

// 将 pFunctionToHook 处的内存权限更改为 PAGE_EXECUTE_READWRITE
if (!VirtualProtect(pFunctionToHook, sizeof(uTrampoline), PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
    return FALSE;
}

// 将跳转 shellcode 复制到 pFunctionToHook
memcpy(pFunctionToHook, uTrampoline, sizeof(uTrampoline));

其中 pFunctionToHook 是要挂钩的函数地址,uTrampoline 是跳转 shellcode。

取消钩子

当被钩取的函数被调用时,跳转外壳代码应该同时适用于 64 位和 32 位架构。然而,我们还没有讨论如何取消钩子。要做到这一点,需要使用在安装跳转外壳代码之前创建的包含这些字节的缓冲区,还原被跳转外壳覆盖的原始字节。然后,取消钩子时应将此缓冲区用作 memcpy 函数中的源缓冲区。

memcpy(pFunctionToHook, pOriginalBytes, sizeof(pOriginalBytes));

其中,pFunctionToHook 是被钩取的函数的地址,pOriginalBytes 是保存函数原始字节的缓冲区,这些字节应该在钩取前保存,可以通过 memcpy 调用来完成。pOriginalBytes 缓冲区的大小应与跳转外壳代码大小相同,这样只能覆盖外壳代码。最后,建议还原内存权限,可以通过以下代码段完成。

if (!VirtualProtect(pFunctionToHook, sizeof(uTrampoline), dwOldProtection, &dwOldProtection)) {
	return FALSE;
}

其中,dwOldProtection 是第一个 VirtualProtect 调用返回的旧内存权限。

HookSt 结构体

为了方便实现,创建了 HookSt 结构体。此结构体将包含用来对特定函数进行挂接和取消挂接所需的信息。 对于设置为编译为 64 位应用程序的程序,将 TRAMPOLINE_SIZE 值设置为 13;而对于设置为在 32 位模式下编译的程序,则将其设置为 7。值 13 和 7 是 trampoline(跳转代码)shellcode 的大小,分别在前面显示的 uTrampoline 变量中表示 64 位和 32 位系统。

typedef struct _HookSt {
	PVOID	pFunctionToHook; // 要挂接的函数的地址
	PVOID	pFunctionToRun; // 要改为运行的函数的地址
	BYTE	pOriginalBytes[TRAMPOLINE_SIZE]; // 缓冲区,用于存储一些原始字节(清理时需要)
	DWORD	dwOldProtection; // 保存“要挂接的函数”地址的旧内存保护(清理时需要)
} HookSt, *PHookSt;

通过以下预处理程序代码设置 TRAMPOLINE_SIZE 值:

// 如果编译为 64 位
#ifdef _M_X64
#define TRAMPOLINE_SIZE		13
#endif // _M_X64

// 如果编译为 32 位
#ifdef _M_IX86
#define TRAMPOLINE_SIZE		7
#endif // _M_IX86

安装钩子

以下函数使用 HookSt 来安装钩子。

BOOL InstallHook (IN PHookSt Hook) {

#ifdef _M_X64
	// 64 位跳转代码
	uint8_t	uTrampoline [] = {
			0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, pFunctionToRun
			0x41, 0xFF, 0xE2                                            // jmp r10
	};

	// 将调用地址 (pFunctionToRun) 补丁到 shellcode 中
	uint64_t uPatch = (uint64_t)(Hook->pFunctionToRun);
	// 将调用地址复制到 uTrampoline 中的偏移量 '2'
	memcpy(&uTrampoline[2], &uPatch, sizeof(uPatch));
#endif // _M_X64


#ifdef _M_IX86
	// 32 位跳转代码
	uint8_t	uTrampoline[] = {
	   0xB8, 0x00, 0x00, 0x00, 0x00,     // mov eax, pFunctionToRun
	   0xFF, 0xE0                        // jmp eax
	};
	
	// 将调用地址 (pFunctionToRun) 补丁到 shellcode 中
	uint32_t uPatch = (uint32_t)(Hook->pFunctionToRun);
	// 将调用地址复制到 uTrampoline 中的偏移量 '1'
	memcpy(&uTrampoline[1], &uPatch, sizeof(uPatch));
#endif // _M_IX86

	
	// 放置跳转代码函数 - 安装钩子
	memcpy(Hook->pFunctionToHook, uTrampoline, sizeof(uTrampoline));

	return TRUE;
}

卸载钩子

下面的函数使用 HookSt 移除钩子。

BOOL RemoveHook (IN PHookSt Hook) {

    DWORD	dwOldProtection = NULL;

    // 复制原始字节
    memcpy(Hook->pFunctionToHook, Hook->pOriginalBytes, TRAMPOLINE_SIZE);
    // 清理我们的缓冲区
    memset(Hook->pOriginalBytes, '\0', TRAMPOLINE_SIZE);
    // 将旧内存保护设置回钩入前的状态
    if (!VirtualProtect(Hook->pFunctionToHook, TRAMPOLINE_SIZE, Hook->dwOldProtection, &dwOldProtection)) {
        printf("[!] VirtualProtect 失败,错误代码:%d \n", GetLastError());
        return FALSE;
    }

    // 全部设为 null
    Hook->pFunctionToHook   = NULL;
    Hook->pFunctionToRun    = NULL;
    Hook->dwOldProtection   = NULL;

    return TRUE;
}

填充 HookSt 结构

InitializeHookStruct 函数用于用执行挂钩所需的信息填充 HookSt 结构。

BOOL InitializeHookStruct(IN PVOID pFunctionToHook, IN PVOID pFunctionToRun, OUT PHookSt Hook) {

	// 填充结构
	Hook->pFunctionToHook   = pFunctionToHook;
	Hook->pFunctionToRun    = pFunctionToRun;

	// 保存我们将覆盖的相同大小的原始字节(即 TRAMPOLINE_SIZE)
	// 这是为了在完成时能够进行清理
	memcpy(Hook->pOriginalBytes, pFunctionToHook, TRAMPOLINE_SIZE);

	// 将保护更改为 RWX 以便我们可以修改字节
	// 我们将旧保护保存到结构中(以便在清理时重新放置它)
	if (!VirtualProtect(pFunctionToHook, TRAMPOLINE_SIZE, PAGE_EXECUTE_READWRITE, &Hook->dwOldProtection)) {
		printf("[!] VirtualProtect 失败,错误代码:%d \n", GetLastError());
		return FALSE;
	}

	return TRUE;
}

主函数

下面的主函数调用了前面演示过的函数,并挂接WinAPI的MessageBoxA

int main() {

	// 初始化结构体(在安装/移除钩子之前需要)
	HookSt st = { 0 };

	如果无法初始化钩子结构(&MessageBoxA,&MyMessageBoxA,&st)) {
		返回 -1
	}

	// 将运行
	MessageBoxA(NULL, "你对恶意软件开发有什么看法?", "原始 MsgBox", MB_OK | MB_ICONQUESTION);

	// 钩子
	如果无法安装钩子(&st)) {
		返回 -1
	}
	
	// 不会运行 - 已钩子
	MessageBoxA(NULL, "恶意软件开发是错误的", "原始 MsgBox", MB_OK | MB_ICONWARNING);


	// 摘钩
	如果无法移除钩子(&st)) {
		返回 -1
	}


	// 将运行 - 钩子已禁用
	MessageBoxA(NULL, "再次返回正常 MsgBox", "原始 MsgBox", MB_OK | MB_ICONINFORMATION);


	返回 0;
}

演示

由于基于跳转的钩子,不可能调用全局原始函数指针来恢复执行。因此,MessageBoxW WinAPI 将在 MyMessageBoxA 钩子函数中调用。

执行第一个 MessageBoxA(未钩取)。

image
image

钩取前 MessageBoxA 的原始指令。

image
image

执行第二个 MessageBoxA(已钩取)。

image
image

跳转壳码已在内存中。

image
image

执行第三个 MessageBoxA(未钩取)。

image
image