强大的 x86/x64 Mini Hook-Engine






4.92/5 (39投票s)
一个强大的 x86/x64 Hook-Engine
引言
我为一篇更大的文章写了这个小型的 Hook-Engine。有时,将有价值的代码用于与代码主题不直接相关的大型文章似乎是一种浪费。这常常会导致那些正在寻找这段代码的人找不到它。
我个人会使用 Microsoft 的 Detour hook engine,但其免费许可证仅适用于 x86 应用程序,这对我来说似乎有点太限制了。因此,我决定编写自己的引擎以支持 x64。我从未下载过 Detour,也从未见过它的 API,但从 Microsoft 提供的总体概述来看,很容易猜测它是如何工作的。
正如我所说的,这只是一个更大项目的一部分。它并不完美,但很容易做到完美。由于这不是关于 Hook 的初学者指南,我假设读者已经具备理解该材料所需的知识。如果你从未听说过这个主题,最好从另一篇文章开始。那里有足够多的指南,没有必要在这里重复同样的事情。
众所周知,Hook Win32 API 只有一种简单且安全的方法:在代码开头放置一个无条件跳转,将其重定向到被 Hook 的函数。我说的安全只是意味着我们的 Hook 不会被绕过。当然,还有其他一些方法,但它们要么复杂,要么疯狂,或者两者兼而有之。例如,代理 DLL 在某些情况下可能有效,但对于系统 DLL 来说,它相当疯狂。覆盖 IAT 有两个原因不安全:
- 程序可能会使用
GetProcAddress
来检索 API 的地址(在这种情况下,我们也应该处理这个 API)。 - 并非总是可能,对于打包程序等许多情况,IAT 是由保护代码而不是 Windows 加载程序构建的。
好的,我想你已经信服了。我们不妨说,微软在本篇文章中提出的方法是有原因的。
工作原理
与无条件跳转结合使用的常用技术是

这种方法在多线程环境中可能看起来不安全,而且确实如此。它可能有效,但我们的技术要强大得多。好吧,没什么新东西,我们只是在要 Hook 的代码开头放置我们的无条件跳转,并将 API 的原始指令放在内存中的其他位置。当被 Hook 的函数跳转到我们的代码时,我们可以调用我们创建的桥接器,它在执行完第一条指令后,会跳转到我们无条件跳转之后的 API 代码。

让我们举一个实际的例子。如果我们想要 Hook 的函数/API 的第一条指令是
mov edi, edi
push ebp
mov ebp, esp
xor ecx, ecx
它们将被我们的替换
00400000 jmp our_code
00400005 xor ecx, ecx
我们的桥接器看起来会像这样
mov edi, edi
push ebp
mov ebp, esp
jmp 00400005
当然,要确定我们要替换的指令的长度,我们需要一个 x86 和 x64 的反汇编器。我在 Google 上搜索了一个 x64 反汇编器,找到了 diStorm64 disassembler。我从它的主页引用:
diStorm64 是一个专业级的开源反汇编库,支持 AMD64,采用 BSD 许可证。
diStorm 是一个二进制流反汇编器。它能够反汇编 64 位(AMD64, X86-64)以及 16 位和 32 位的 80x86 指令。此外,它还反汇编 FPU、MMX、SSE、SSE2、SSE3、SSSE3、SSE4、3DNow!(带扩展)、新的 x86-64 指令集、VMX 和 AMD 的 SVM!diStorm 被编写为能够快速、准确地解码每条指令。稳健的解码,同时特别注意有效或未使用的前缀,这使得这个反汇编器非常强大,尤其适合研究。另一个可能派上用场的好处是,该模块是以多线程方式编写的,这意味着你可以同时反汇编多个流。
为了快速使用,diStorm 编译为 Python 版本,并且也可以很容易地在 C 中使用。diStorm 最初是在 Windows 下编写的,后来移植到了 Linux 和 Mac。源代码是可移植的,并且是平台独立的(支持小端和大端)。
它还可以用作 ring0 反汇编器(已在 Windows 下使用 DDK 作为内核驱动程序进行测试)!
这对我来说听起来相当不错。既然我们有了反汇编器,就可以开始工作了!
我首先想知道的是,是否可以在不重定位跳转的情况下创建桥接器。正如读者所知,跳转大多数时候操作数是相对地址,而不是绝对地址。这导致了一个问题,即我无法重定位跳转而不必重新计算其相对地址。此外,我还想测试这个反汇编器是否真的有效。于是,我写了一个小程序,它记录了 DLL 中所有导出函数中将被无条件跳转覆盖的代码指令。代码如下:
#include "stdafx.h"
#include "distorm.h"
#include <stdlib.h>
#include <stdlib.h>
#include <Windows.h>
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva);
VOID AddFunctionToLog(FILE *Log, BYTE *FileBuf, DWORD FuncRVA);
VOID GetInstructionString(char *Str, _DecodedInst *Instr);
int _tmain(int argc, _TCHAR* argv[])
{
if (argc < 2) return 0;
//
// Open log file
//
FILE *Log = NULL;
if (_tfopen_s(&Log, argv[2], _T("w")) != 0)
return 0;
//
// Open PE file
//
HANDLE hFile = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
fclose(Log);
return 0;
}
DWORD FileSize = GetFileSize(hFile, NULL);
BYTE *FileBuf = new BYTE [FileSize];
DWORD BRW;
if (FileBuf)
ReadFile(hFile, FileBuf, FileSize, &BRW, NULL);
CloseHandle(hFile);
IMAGE_DOS_HEADER *pDosHeader = (IMAGE_DOS_HEADER *) FileBuf;
IMAGE_NT_HEADERS *pNtHeaders = (IMAGE_NT_HEADERS *) ((FileBuf != NULL ?
pDosHeader->e_lfanew : 0) + (ULONG_PTR) FileBuf);
if (!FileBuf || pDosHeader->e_magic != IMAGE_DOS_SIGNATURE ||
pNtHeaders->Signature != IMAGE_NT_SIGNATURE ||
pNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == 0)
{
fclose(Log);
if (FileBuf)
delete FileBuf;
return 0;
}
//
// Walk through export dir's functions
//
DWORD ET_RVA = pNtHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
IMAGE_EXPORT_DIRECTORY *pExportDir = (IMAGE_EXPORT_DIRECTORY *)
(RvaToOffset(pNtHeaders, ET_RVA) + (ULONG_PTR) FileBuf);
DWORD *pFunctions = (DWORD *) (RvaToOffset(pNtHeaders,
pExportDir->AddressOfFunctions) + (ULONG_PTR) FileBuf);
for (DWORD x = 0; x < pExportDir->NumberOfFunctions; x++)
{
if (pFunctions[x] == 0) continue;
AddFunctionToLog(Log, FileBuf, pFunctions[x]);
}
fclose(Log);
delete FileBuf;
return 0;
}
//
// This function adds to the log the instructions
// at the beginning of each function which are going
// to be overwritten by the hook jump
//
VOID AddFunctionToLog(FILE *Log, BYTE *FileBuf, DWORD FuncRVA)
{
#define MAX_INSTRUCTIONS 100
IMAGE_NT_HEADERS *pNtHeaders = (IMAGE_NT_HEADERS *)
((*(IMAGE_DOS_HEADER *) FileBuf).e_lfanew + (ULONG_PTR) FileBuf);
_DecodeResult res;
_DecodedInst decodedInstructions[MAX_INSTRUCTIONS];
unsigned int decodedInstructionsCount = 0;
#ifdef _M_IX86
_DecodeType dt = Decode32Bits;
#define JUMP_SIZE 10 // worst case scenario
#else ifdef _M_AMD64
_DecodeType dt = Decode64Bits;
#define JUMP_SIZE 14 // worst case scenario
#endif
_OffsetType offset = 0;
res = distorm_decode(offset, // offset for buffer, e.g. 0x00400000
(const BYTE *) &FileBuf[RvaToOffset(pNtHeaders, FuncRVA)],
50, // function size (code size to disasm)
dt, // x86 or x64?
decodedInstructions, // decoded instr
MAX_INSTRUCTIONS, // array size
&decodedInstructionsCount // how many instr were disassembled?
);
if (res == DECRES_INPUTERR)
return;
DWORD InstrSize = 0;
for (UINT x = 0; x < decodedInstructionsCount; x++)
{
if (InstrSize >= JUMP_SIZE)
break;
InstrSize += decodedInstructions[x].size;
char Instr[100];
GetInstructionString(Instr, &decodedInstructions[x]);
fprintf(Log, "%s\n", Instr);
}
fprintf(Log, "\n\n\n");
}
VOID GetInstructionString(char *Str, _DecodedInst *Instr)
{
wsprintfA(Str, "%s %s", Instr->mnemonic.p, Instr->operands.p);
_strlwr_s(Str, 100);
}
DWORD RvaToOffset(IMAGE_NT_HEADERS *NT, DWORD Rva)
{
DWORD Offset = Rva, Limit;
IMAGE_SECTION_HEADER *Img;
WORD i;
Img = IMAGE_FIRST_SECTION(NT);
if (Rva < Img->PointerToRawData)
return Rva;
for (i = 0; i < NT->FileHeader.NumberOfSections; i++)
{
if (Img[i].SizeOfRawData)
Limit = Img[i].SizeOfRawData;
else
Limit = Img[i].Misc.VirtualSize;
if (Rva >= Img[i].VirtualAddress &&
Rva < (Img[i].VirtualAddress + Limit))
{
if (Img[i].PointerToRawData != 0)
{
Offset -= Img[i].VirtualAddress;
Offset += Img[i].PointerToRawData;
}
return Offset;
}
}
return NULL;
}
命令行语法是: pefile logfile
(例如: disasmtest ntdll.dll ntdll.log
)。如你所见,我为 x86 Hook 预留了 10 字节。x86/x64 可以使用 5 字节跳转,但有必要检查原始函数和我们的代码之间,以及桥接器和原始函数之间的距离是否小于 2 GB。嗯,我们也要在 x86 上检查这一点,但可能性非常大。x86 和 x64 的最坏情况场景是这个绝对跳转:
jmp [xxxxx]
xxxxx: absolute address (DWORD on x86 and QWORD on x64)
这意味着 x86 最坏情况为 10 字节,x64 最坏情况为 14 字节。在这个 Hook Engine 中,我只使用了最坏情况(而不是 5 字节相对地址),因为如果原始函数和被 Hook 函数之间的空间大于 2 GB,或者原始函数和桥接器之间的空间大于 2 GB,那么我每次 Hook/Unhook 函数时都必须从头开始重新创建桥接器。一个专业的引擎应该这样做(而且工作量不大),但我会保持简单(对我来说),只使用绝对跳转。至于上面小程序的结果,我为 x86 和 x64 的 ntdll.dll 和 advapi32.dll 创建了日志。例如,这里是 ntdll.dll x86 日志的一小部分:
mov eax, 0x44
mov edx, 0x7ffe0300
mov eax, 0x45
mov edx, 0x7ffe0300
mov eax, 0x46
mov edx, 0x7ffe0300
mov eax, 0x47
mov edx, 0x7ffe0300
mov eax, 0x48
mov edx, 0x7ffe0300
mov eax, 0x49
mov edx, 0x7ffe0300
mov eax, 0x4a
mov edx, 0x7ffe0300
mov eax, 0x4b
mov edx, 0x7ffe0300
mov eax, 0x4c
mov edx, 0x7ffe0300
这当然是相当令人鼓舞的,但让我们看看 x64 平台的结果。
sub rsp, 0x48
mov rax, [rsp+0x78]
mov byte [rsp+0x30], 0x0
mov [rsp+0x10], rbx
mov [rsp+0x18], rbp
mov [rsp+0x20], rsi
push rsi
push r14
push r15
sub rsp, 0x480
mov rax, rsp
mov [rax+0x8], rbx
mov [rax+0x10], rsi
mov [rax+0x18], r12
sub rsp, 0x38
mov [rsp+0x20], r8
mov r9d, edx
mov r8, rcx
mov rax, rsp
mov [rax+0x8], rsi
mov [rax+0x10], rdi
mov [rax+0x18], r12
mov [rsp+0x10], rbx
mov [rsp+0x18], rsi
push rdi
push r12
sub rsp, 0x68
mov rax, r9
mov r9d, [rsp+0xb0]
但那些在将数字移入寄存器后就调用系统调用的函数(如 NtCreateProcess
、NtOpenKey
等)呢?这些函数只有很少的指令,而我们的 14 字节跳转会覆盖比函数本身更多的代码。但这似乎不成问题,因为我们可以从反汇编器中看到,这些函数有 16 字节的对齐。因此,我们不会覆盖其他函数的代码。

这是 Hook Engine 的主代码(所有代码约 300 行)。
//
// This function creates a bridge of the original function
//
VOID *CreateBridge(ULONG_PTR Function, const UINT JumpSize)
{
if (pBridgeBuffer == NULL) return NULL;
#define MAX_INSTRUCTIONS 100
_DecodeResult res;
_DecodedInst decodedInstructions[MAX_INSTRUCTIONS];
unsigned int decodedInstructionsCount = 0;
#ifdef _M_IX86
_DecodeType dt = Decode32Bits;
#else ifdef _M_AMD64
_DecodeType dt = Decode64Bits;
#endif
_OffsetType offset = 0;
res = distorm_decode(offset, // offset for buffer
(const BYTE *) Function, // buffer to disassemble
50, // function size (code size to disasm)
// 50 instr should be _quite_ enough
dt, // x86 or x64?
decodedInstructions, // decoded instr
MAX_INSTRUCTIONS, // array size
&decodedInstructionsCount // how many instr were disassembled?
);
if (res == DECRES_INPUTERR)
return NULL;
DWORD InstrSize = 0;
VOID *pBridge = (VOID *) &pBridgeBuffer[CurrentBridgeBufferSize];
for (UINT x = 0; x < decodedInstructionsCount; x++)
{
if (InstrSize >= JumpSize)
break;
BYTE *pCurInstr = (BYTE *) (InstrSize + (ULONG_PTR) Function);
//
// This is an sample attempt of handling a jump
// It works, but it converts the jz to jmp
// since I didn't write the code for writing
// conditional jumps
//
/*
if (*pCurInstr == 0x74) // jz near
{
ULONG_PTR Dest = (InstrSize + (ULONG_PTR) Function)
+ (char) pCurInstr[1];
WriteJump(&pBridgeBuffer[CurrentBridgeBufferSize], Dest);
CurrentBridgeBufferSize += JumpSize;
}
else
{*/
memcpy(&pBridgeBuffer[CurrentBridgeBufferSize],
(VOID *) pCurInstr, decodedInstructions[x].size);
CurrentBridgeBufferSize += decodedInstructions[x].size;
//}
InstrSize += decodedInstructions[x].size;
}
WriteJump(&pBridgeBuffer[CurrentBridgeBufferSize], Function + InstrSize);
CurrentBridgeBufferSize += GetJumpSize((ULONG_PTR)
&pBridgeBuffer[CurrentBridgeBufferSize],
Function + InstrSize);
return pBridge;
}
//
// Hooks a function
//
extern "C" __declspec(dllexport)
BOOL __cdecl HookFunction(ULONG_PTR OriginalFunction, ULONG_PTR NewFunction)
{
//
// Check if the function has already been hooked
// If so, no disassembling is necessary since we already
// have our bridge
//
HOOK_INFO *hinfo = GetHookInfoFromFunction(OriginalFunction);
if (hinfo)
{
WriteJump((VOID *) OriginalFunction, NewFunction);
}
else
{
if (NumberOfHooks == (MAX_HOOKS - 1))
return FALSE;
VOID *pBridge = CreateBridge(OriginalFunction,
GetJumpSize(OriginalFunction, NewFunction));
if (pBridge == NULL)
return FALSE;
HookInfo[NumberOfHooks].Function = OriginalFunction;
HookInfo[NumberOfHooks].Bridge = (ULONG_PTR) pBridge;
HookInfo[NumberOfHooks].Hook = NewFunction;
NumberOfHooks++;
WriteJump((VOID *) OriginalFunction, NewFunction);
}
return TRUE;
}
//
// Unhooks a function
//
extern "C" __declspec(dllexport)
VOID __cdecl UnhookFunction(ULONG_PTR Function)
{
//
// Check if the function has already been hooked
// If not, I can't unhook it
//
HOOK_INFO *hinfo = GetHookInfoFromFunction(Function);
if (hinfo)
{
//
// Replaces the hook jump with a jump to the bridge
// I'm not completely unhooking since I'm not
// restoring the original bytes
//
WriteJump((VOID *) hinfo->Function, hinfo->Bridge);
}
}
//
// Get the bridge to call instead of the original function from hook
//
extern "C" __declspec(dllexport)
ULONG_PTR __cdecl GetOriginalFunction(ULONG_PTR Hook)
{
if (NumberOfHooks == 0)
return NULL;
for (UINT x = 0; x < NumberOfHooks; x++)
{
if (HookInfo[x].Hook == Hook)
return HookInfo[x].Bridge;
}
return NULL;
}
我将其实现为一个 DLL(但你也可以将其包含在你的代码中)。
Using the Code
使用代码非常简单。基本上,DLL 只导出三个函数:一个用于 Hook,一个用于 Unhook,还有一个用于获取被 Hook 函数的桥接器地址。当然,我们需要获取桥接器的地址,否则我们就无法调用被 Hook 函数的原始代码。
让我们看一个在 x86 和 x64 上都可以工作的简单代码示例:
#include "stdafx.h"
#include "NtHookEngine_Test.h"
BOOL (__cdecl *HookFunction)(ULONG_PTR OriginalFunction,
ULONG_PTR NewFunction);
VOID (__cdecl *UnhookFunction)(ULONG_PTR Function);
ULONG_PTR (__cdecl *GetOriginalFunction)(ULONG_PTR Hook);
int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption,
UINT uType, WORD wLanguageId, DWORD dwMilliseconds);
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)
{
//
// Retrieve hook functions
//
HMODULE hHookEngineDll = LoadLibrary(_T("NtHookEngine.dll"));
HookFunction = (BOOL (__cdecl *)(ULONG_PTR, ULONG_PTR))
GetProcAddress(hHookEngineDll, "HookFunction");
UnhookFunction = (VOID (__cdecl *)(ULONG_PTR))
GetProcAddress(hHookEngineDll, "UnhookFunction");
GetOriginalFunction = (ULONG_PTR (__cdecl *)(ULONG_PTR))
GetProcAddress(hHookEngineDll, "GetOriginalFunction");
if (HookFunction == NULL || UnhookFunction == NULL ||
GetOriginalFunction == NULL)
return 0;
//
// Hook MessageBoxTimeoutW
//
HookFunction((ULONG_PTR) GetProcAddress(LoadLibrary(_T("User32.dll")),
"MessageBoxTimeoutW"),
(ULONG_PTR) &MyMessageBoxW);
MessageBox(0, _T("Hi, this is a message box!"), _T("This is the title."),
MB_ICONINFORMATION);
//
// Unhook MessageBoxTimeoutW
//
UnhookFunction((ULONG_PTR) GetProcAddress(LoadLibrary(_T("User32.dll")),
"MessageBoxTimeoutW"));
MessageBox(0, _T("Hi, this is a message box!"), _T("This is the title."),
MB_ICONINFORMATION);
return 0;
}
int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption,
UINT uType, WORD wLanguageId, DWORD dwMilliseconds)
{
int (WINAPI *pMessageBoxW)(HWND hWnd, LPCWSTR lpText,
LPCWSTR lpCaption, UINT uType, WORD wLanguageId,
DWORD dwMilliseconds);
pMessageBoxW = (int (WINAPI *)(HWND, LPCWSTR, LPCWSTR, UINT, WORD, DWORD))
GetOriginalFunction((ULONG_PTR) MyMessageBoxW);
return pMessageBoxW(hWnd, lpText, L"Hooked MessageBox",
uType, wLanguageId, dwMilliseconds);
}
在这个示例中,我 Hook 了 API MessageBoxTimeoutW
。我曾尝试 Hook MessageBoxW
,在 x86 上工作正常,然后在 x64 上尝试时,代码引发了异常。所以,我反汇编了 x64 上的 MessageBoxW
函数:

不幸的是,正如你所注意到的,这个 API 的第一条指令包含一个 jz
,它将被我们的无条件跳转覆盖。而且由于我们在桥接器中不重定位跳转,所以我们无法 Hook 这个函数。因此,我不得不 Hook 函数 MessageBoxTimeoutW
,它在 MessageBoxW
内部调用,并且开头没有跳转。
在代码示例中,我首先 Hook 了函数并调用它,然后 Unhook 它并再次调用它。所以,输出将是:

就是这样。当然,这段代码只有在 MessageBoxTimeoutW
可用的情况下才有效。我不太确定它是什么时候首次引入的,因为它是一个未公开的 API。我猜它是在 XP 中引入的,所以这个特定的 Hook 在 Windows 2000 上可能无效。
结论
正如从前面的例子中可以看出,Hook Engine 并不完美,但它可以很容易地改进。我之所以没有进一步开发它,是因为我现在不需要更强大的功能。我只是需要一个没有许可限制的 x86/x64 Hook Engine。我用一天时间写了这个引擎和文章,工作量真的不大。这种 Hook Engine 的大部分工作是编写反汇编器,而我没有做这件事。所以,在我看来,为 Hook Engine 付费意义不大。我真正无法在这个引擎中提供的是对 Itanium 的支持。那是因为我没有这个平台的反汇编器。但我宁愿自己写一个,也不愿购买一个 Hook Engine。我将来也许会添加一个 Itanium 反汇编器,谁知道呢……
希望你能发现这段代码很有用。
历史
- 23/11/2007: 初次发布
- 05/04/2008: 修复了 x64 上的一个严重 bug