8

我正在为一个为(我们)modder 提供 API 的游戏编写扩展程序。这个 API 提供了各种各样的东西,但它有一个限制。API 仅适用于“引擎”,这意味着基于引擎发布的所有修改(模组)不提供/具有任何类型的(特定于模组的)API。我创建了一个“签名扫描器”(注意:我的插件作为共享库加载,使用 -share 和 -fPIC 编译)找到感兴趣的功能(这很容易,因为我在 linux 上)。所以来解释一下,我将举一个具体的例子:我找到了一个感兴趣的函数的地址,它的函数头很简单int * InstallRules(void);. 它需要一个空值(void)并返回一个整数指针(指向我感兴趣的对象)。现在,我想做的是创建一个绕道(并记住我有函数的起始地址)到我自己的函数,我想这样做:

void MyInstallRules(void)
{
    if(PreHook() == block) // <-- First a 'pre' hook which can block the function
        return;
    int * val = InstallRules(); // <-- Call original function
    PostHook(val); // <-- Call post hook, if interest of original functions return value
}

现在这是交易;我对函数挂钩没有任何经验,而且我对内联汇编(仅限 AT&T)知之甚少。网上预制的detour包适用于windows或者是使用其他方法(即预加载一个dll来覆盖原来的)。所以基本上; 我应该怎么做才能走上正轨?我应该阅读调用约定(在这种情况下为 cdecl)并了解内联汇编,还是应该做什么?最好的可能是用于 linux 绕行的已经有功能的包装类。最后,我想要这样简单的东西:

void * addressToFunction = SigScanner.FindBySig("Signature_ASfs&43"); // I've already done this part
void * original = PatchFunc(addressToFunction, addressToNewFunction); // This replaces the original function with a hook to mine, but returns a pointer to the original function (relocated ofcourse)
// I might wait for my hook to be called or whatever
// ....

// And then unpatch the patched function (optional)
UnpatchFunc(addressToFunction, addressToNewFunction);

我知道我无法在这里得到一个完全令人满意的答案,但我非常感谢一些关于方向的帮助,因为我在这里如履薄冰......我读过关于绕道但几乎没有根本没有任何文档(特别是针对 linux),我想我想实现所谓的“蹦床”,但我似乎无法找到获取这些知识的方法。

注意:我也对 _thiscall 感兴趣,但从我读过的内容来看,使用 GNU 调用约定调用并不难(?)

4

1 回答 1

12

这个项目是否要开发一个“框架”,允许其他人在不同的二进制文件中挂钩不同的功能?还是只是需要挂钩您拥有的这个特定程序?

首先,假设您想要第二件事,您只需在二进制文件中有一个要以编程方式且可靠地挂钩的函数。普遍这样做的主要问题是可靠地做到这一点是一个非常艰难的游戏,但如果你愿意做出一些妥协,那么它绝对是可行的。还让我们假设这是 x86 的事情。

如果你想挂钩一个函数,有几个选项可以做到这一点。Detours所做的是内联修补。他们很好地概述了它在Research PDF 文档中的工作原理。基本思想是你有一个功能,例如

00E32BCE  /$ 8BFF           MOV EDI,EDI
00E32BD0  |. 55             PUSH EBP
00E32BD1  |. 8BEC           MOV EBP,ESP
00E32BD3  |. 83EC 10        SUB ESP,10
00E32BD6  |. A1 9849E300    MOV EAX,DWORD PTR DS:[E34998]
...
...

现在,您将函数的开头替换为函数的 CALL 或 JMP,并将您用补丁覆盖的原始字节保存在某处:

00E32BCE  /$ E9 XXXXXXXX    JMP MyHook
00E32BD3  |. 83EC 10        SUB ESP,10
00E32BD6  |. A1 9849E300    MOV EAX,DWORD PTR DS:[E34998]

(请注意,我覆盖了 5 个字节。)现在,您的函数使用与原始函数相同的参数和调用约定进行调用。如果您的函数想要调用原始函数(但不是必须),则创建一个“蹦床”,即 1) 运行被覆盖的原始指令 2) jmps 到原始函数的其余部分:

Trampoline:
    MOV EDI,EDI
    PUSH EBP
    MOV EBP,ESP
    JMP 00E32BD3

就是这样,您只需要在运行时通过发出处理器指令来构造蹦床函数。这个过程的难点在于让它可靠地工作,适用于任何功能、任何调用约定以及不同的操作系统/平台。其中一个问题是,如果您要覆盖的 5 个字节在指令中间结束。要检测“指令结束”,您基本上需要包含一个反汇编程序,因为函数开头可以有任何指令。或者当函数本身短于 5 个字节时(始终返回 0 的函数可以写成XOR EAX,EAX; RETN只有 3 个字节)。

大多数当前的编译器/汇编器产生一个 5 字节长的函数序言,正是为了这个目的,钩子。看到了MOV EDI, EDI吗?如果您想知道,“他们为什么将 edi 移到 edi?那什么都没做!?” 你是绝对正确的,但这是序言的目的,正好是 5 个字节长(不是在指令中间结束)。请注意,反汇编示例不是我编造的,它是 Windows Vista 上的 calc.exe。

其余的钩子实现只是技术细节,但它们会给您带来数小时的痛苦,因为这是最难的部分。还有您在问题中描述的行为:

void MyInstallRules(void)
{
    if(PreHook() == block) // <-- First a 'pre' hook which can block the function
        return;
    int * val = InstallRules(); // <-- Call original function
    PostHook(val); // <-- Call post hook, if interest of original functions return value
}

似乎比我所描述的(以及 Detours 所做的)更糟糕,例如,您可能想要“不调用原始值”但返回一些不同的值。或者两次调用原始函数。相反,让您的钩子处理程序决定是否以及在何处调用原始函数。此外,您不需要两个处理函数来进行挂钩。

如果您对此所需的技术(主要是组装)没有足够的了解,或者不知道如何进行挂钩,我建议您研究一下 Detours 是做什么的。挂钩您自己的二进制文件并使用调试器(例如 OllyDbg)在汇编级别查看它到底做了什么,放置了哪些指令以及放置在哪里。本教程也可能会派上用场。

无论如何,如果您的任务是在特定程序中挂钩某些功能,那么这是可行的,如果您有任何问题,请再次在这里询问。基本上你可以做很多假设(比如函数序言或使用的约定),这将使你的任务更容易。

如果你想创建一些可靠的钩子框架,那仍然是一个完全不同的故事,你应该首先为一些简单的应用程序创建简单的钩子。

另请注意,此技术不是特定于操作系统的,它在所有 x86 平台上都是相同的,它适用于 Linux 和 Windows。特定于操作系统的是,您可能必须更改代码的内存保护(“解锁”它,以便您可以写入它),这mprotect在 Linux 和VirtualProtectWindows 上都可以完成。调用约定也不同,这就是您可以通过在编译器中使用正确的语法来解决的问题。

另一个麻烦是“DLL 注入”(在 Linux 上它可能被称为“共享库注入”,但DLL 注入这个术语已广为人知)。您需要将代码(执行挂钩)放入程序中。我的建议是,如果可能,只需使用 LD_PRELOAD 环境变量,您可以在其中指定将在程序运行之前加载到程序中的库。这已经被描述过很多次了,就像这里:什么是 LD_PRELOAD 技巧?. 如果您必须在运行时执行此操作,恐怕您需要使用 gdb 或 ptrace,我认为这很难(至少 ptrace 的事情)。

I also found some nice resources:

Also one other point: This "inline patching" is not the only way to do this. There are even simpler ways, e.g. if the function is virtual or if it's a library exported function, you can skip all the assembly/disassembly/JMP thing and simply replace the pointer to that function (either in the table of virtual functions or in the exported symbols table).

于 2012-05-02T19:25:14.760 回答