适用于可热补丁操作系统的 API 挂钩
一种利用编译器热补丁支持的新 API 挂钩方法。
引言
API 挂钩是 CodeProject 上的一个热门话题,而Ivo Avonov 的文章对您可以采取的许多不同的 API 挂钩方法进行了精彩的概述。
这些方法中的大多数都相当复杂(迷你挂钩引擎包含一个运行时反汇编器!)。
本文将描述一种比大多数现有方法更直接的新方法,代码仅 50 行。
背景
目前,大多数 API 挂钩方法已经存在数年了,而随着 Server 2003 和 Vista(以及在某种程度上 XP SP2)的推出,Microsoft 为在操作系统中可靠地替换函数开辟了一条新途径。
基本技术是公开的,但我目前不知道有其他文章将其应用于第三方 API 挂钩。
操作系统现在包含 Microsoft 所称的“热修复”支持,允许在无需重启的情况下应用补丁。他们以一种非常巧妙的方式实现了这一点,我们可以利用它来进行通用的 API 挂钩。
首先,我们来看看“传统”函数在汇编级别上是什么样的。几乎每个函数都会像这样设置堆栈帧
55 push ebp
8bec mov ebp,esp
Visual Studio 2008 的编译器包含一个新选项(Microsoft 显然已经内部使用了好几年)/hotpatch,它做了两件事:
a) 在每个函数的开头放置一个 2 字节指令“mov edi, edi”
b) 隐含 /FUNCTIONPADMIN 链接器开关,该开关在每个函数*之前*在内存中保留 5 字节的空间。(在 x86 上)。
这意味着函数在内存中的布局如下:
90 nop
90 nop
90 nop
90 nop
90 nop
FunctionEntry:
8bff mov edi,edi
55 push ebp
8bec mov ebp,esp
这正是可靠函数挂钩/热修复所需的。5 个字节的 nops 可以被一个长跳转操作码(操作码 0xE9)和一个 4 字节地址覆盖。两个字节的“mov edi, edi”指令可以用一个 2 字节的相对短跳转返回到我们的长跳转。为什么 Microsoft 不要在每个函数的开头放两个“nop”指令?一个重要原因——您可以安全地将单个 2 字节指令覆盖为另一个 2 字节指令,而无需担心另一个线程的指令指针是否位于指令中间。因此,热修复支持是 100% 线程安全的。它(显然?)也很重要,先写出长跳转,然后写出返回它的短跳转。如果 nop
s 被覆盖,然后线程被切换,然后跳转回被写入(并且 2 字节短跳转可以原子地写入),这也不会有什么坏处。
代码
bool HotPatch(void *oldProc, void *newProc, void**ppOrigFn)
{
bool bRet = false;
DWORD oldProtect = NULL;
WORD* pJumpBack = (WORD*)oldProc;
BYTE* pLongJump = ((BYTE*)oldProc-5);
DWORD* pLongJumpAdr = ((DWORD*)oldProc-1);
VirtualProtect(pLongJump, 7, PAGE_EXECUTE_WRITECOPY, &oldProtect);
// don’t hook functions which have already been hooked
if ((0xff8b == *pJumpBack) &&
(0x90 == *pLongJump) &&
(0x90909090 == *pLongJumpAdr))
{
*pLongJump = 0xE9; // long jmp
*pLongJumpAdr = ((DWORD)newProc)-((DWORD)oldProc); //
*pJumpBack = 0xF9EB; // short jump back -7 (back 5, plus two for this jump)
if (ppOrigFn)
*ppOrigFn = ((BYTE*)oldProc)+2;
bRet = true;
}
VirtualProtect(pLongJump, 7, oldProtect, &oldProtect);
return bRet;
}
bool HotUnpatch(void*oldProc) // the original fn ptr, not "ppOrigFn" from HotPatch
{
bool bRet = false;
DWORD oldProtect = NULL;
WORD* pJumpBack = (WORD*)oldProc;
VirtualProtect(pJumpBack, 2, PAGE_EXECUTE_WRITECOPY, &oldProtect);
if (0xF9EB == *pJumpBack)
{
*pJumpBack = 0xff8b; // mov edi, edi = nop
bRet = true;
}
VirtualProtect(pJumpBack, 2, oldProtect, &oldProtect);
return bRet;
}
用法
如果您想使用修改后的参数或返回值调用原始函数,这在 API 挂钩(与 OS 补丁相反)中很常见,您只需通过 ppOrigFn 调用原始函数即可。只需确保您的函数指针类型与原始函数具有相同的调用约定。
这是一个示例用法,显示了用一个修改了一个参数并传递其余参数的自定义函数替换 user32 函数 MessageBoxA。请注意,这在 API 挂钩领域代码相对干净——没有 __asm 块或 declspec(naked) 函数。
typedef int (WINAPI MSGBOXFN)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
MSGBOXFN* pfnOrigMessageBox;
int WINAPI OurMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
return pfnOrigMessageBox(hWnd, "replaced text", lpCaption, uType);
}
int main(int argc, char*argv[])
{
MessageBox(NULL, "text", "caption1", MB_OK);
HotPatch(MessageBox, (void*) OurMessageBox, (void**) &pfnOrigMessageBox);
MessageBox(NULL, "text", "caption2", MB_OK);
HotUnpatch(MessageBox);
MessageBox(NULL, "text", "caption3", MB_OK);
}
上面的简单示例显示了一个系统 API 被挂钩以用于单个进程。许多 API 挂钩用户希望挂钩其他进程。本文不涉及问题的这一半。有其他文章讨论了将代码注入其他进程的方法(主要是 CreateRemoteThread
和 SetWindowsHookEx
),但一旦您的代码运行,上述技术就可以像对待自己的进程一样使用。事实上,对于挂钩已运行的应用程序,此技术比许多其他技术更可取,因为它具有线程安全性。
注释
- Vista 上几乎所有函数都支持热修复,我查看的更新的 XPSP2 系统上的大多数函数也支持。在使用此代码之前,请确保在各种操作系统和补丁级别上进行测试。
- 此方法不链接挂钩。这将是一个明显的扩展,随着热补丁的普及,它可能会变得必要。一种方法是返回 ppOrigFn 中已有的 pLongJumpAdr。
- 此代码仅适用于 x86,因为我没有安装 x64 操作系统,但它应该易于改编。
参考文献
- Ishai 的博客:为什么编译器会在函数开头生成 MOV EDI, EDI 指令?
- FreiK 的博客:什么是“热修复性”?有什么用?
历史
在此处保持您所做的任何更改或改进的实时更新。