65.9K
CodeProject 正在变化。 阅读更多。
Home

适用于可热补丁操作系统的 API 挂钩

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (20投票s)

2008年6月28日

CPOL

4分钟阅读

viewsIcon

62159

downloadIcon

664

一种利用编译器热补丁支持的新 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% 线程安全的。它(显然?)也很重要,先写出长跳转,然后写出返回它的短跳转。如果 nops 被覆盖,然后线程被切换,然后跳转回被写入(并且 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 挂钩用户希望挂钩其他进程。本文不涉及问题的这一半。有其他文章讨论了将代码注入其他进程的方法(主要是 CreateRemoteThreadSetWindowsHookEx),但一旦您的代码运行,上述技术就可以像对待自己的进程一样使用。事实上,对于挂钩已运行的应用程序,此技术比许多其他技术更可取,因为它具有线程安全性。

注释

  1. Vista 上几乎所有函数都支持热修复,我查看的更新的 XPSP2 系统上的大多数函数也支持。在使用此代码之前,请确保在各种操作系统和补丁级别上进行测试。
  2. 此方法不链接挂钩。这将是一个明显的扩展,随着热补丁的普及,它可能会变得必要。一种方法是返回 ppOrigFn 中已有的 pLongJumpAdr。
  3. 此代码仅适用于 x86,因为我没有安装 x64 操作系统,但它应该易于改编。

参考文献

© . All rights reserved.