绕路和GCC内联汇编(Linux)

8
我正在为一个游戏编写扩展,该游戏为(我们)修改器提供了API。这个API提供了各种各样的东西,但它有一个限制。API仅适用于“引擎”,这意味着基于引擎发布的所有修改(mod)都没有任何特定于(mod的)API。我创建了一个“签名扫描器”(注意:我的插件作为共享库加载,使用-share和-fPIC编译),它可以找到感兴趣的函数(因为我在Linux上)。因此,为了解释清楚,我会举一个具体例子:我已经找到了一个感兴趣的函数的地址,它的函数头非常简单int * InstallRules(void);。它不需要任何参数(void),并返回一个整数指针(指向我感兴趣的对象)。现在,我想要做的是创建一个detour(记住我有函数的起始地址),来调用我的自己的函数,我希望它的行为类似于这样:
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格式的内联汇编的知识。互联网上预制的拦截包适用于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调用约定并不难以调用。

只是想提一下,你考虑过非技术方案,也就是“请求”作者提供API吗? - Karl Bielefeldt
@KarlBielefeldt 这会很难... GoldSrc(半条命引擎)现在已经有大约12年了吧? - Elliott Darfink
1个回答

12

这个项目是开发一个“框架”,让其他人可以钩取不同二进制文件中的不同函数吗?还是说只是你需要钩取这个特定程序的函数?

首先,假设你想要第二种情况,你只是想编程可靠地钩取二进制文件中的某个函数。普遍做到这一点的主要问题是可靠性非常难以保证,但如果你愿意做出一些妥协,那么这是可以做到的。还假设这是x86事情。

如果你想要钩取一个函数,有几种选项可以实现。Detours所做的是内联修补。他们在Research PDF document中对其如何工作进行了很好的概述。基本思路是你有一个函数,例如:

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) 跳转到原始函数的其余部分:
Trampoline:
    MOV EDI,EDI
    PUSH EBP
    MOV EBP,ESP
    JMP 00E32BD3

这就是了,你只需要通过发出处理器指令来在运行时构建跳板函数。这个过程中的难点在于使其能够可靠地工作,对于任何函数、任何调用约定和不同的操作系统/平台都适用。其中一个问题是,如果你想要覆盖的5个字节以指令的中间结束,为了检测“指令结束”,你基本上需要包括反汇编器,因为函数开头可以是任何指令。或者当函数本身比5个字节还短时(一个总是返回0的函数可以写成,只有3个字节)。
大多数当前的编译器/汇编器产生一个长度为5个字节的函数序言,正是为了这个目的,钩子。看到了吗?如果你想知道,“他们为什么把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)在汇编级别查看它到底做了什么,放置了哪些指令以及在哪里。此外,本教程可能会有所帮助。
总之,如果您的任务是挂钩特定程序中的某些函数,则可以做到这一点,如果遇到任何困难,请再次在此处询问。基本上,您可以做出很多假设(如函数prologs或使用的约定),这将使您的任务变得更加容易。
如果您想创建一些可靠的钩取框架,那么还是完全不同的故事,并且您应该首先为一些简单的应用程序创建简单的钩子。
请注意,这种技术不是特定于操作系统的,在所有x86平台上都是相同的,可以在Linux和Windows上使用。但需要注意的是,更改代码的内存保护(“解锁”它以便您可以写入它)可能是特定于操作系统的,Linux上使用mprotect,Windows上使用VirtualProtect。此外,调用约定也不同,但通过在编译器中使用正确的语法可以解决这个问题。
另一个问题是“DLL注入”(在Linux上可能称为“共享库注入”,但术语“DLL注入”是广为人知的)。您需要将执行钩子的代码放入程序中。我的建议是,如果可能的话,只需使用LD_PRELOAD环境变量,在其中可以指定将在运行程序之前加载到程序中的库。这已经在许多地方进行了描述,例如这里:什么是LD_PRELOAD技巧?。如果您必须在运行时执行此操作,恐怕您需要使用gdb或ptrace,而在我看来(至少是ptrace)这相当困难。但是,您可以阅读例如这篇codeproject文章这个ptrace教程
我还发现了一些不错的资源: 此外,还有其他方法可以实现这种“内联补丁”,比如如果该函数是虚拟的或者它是一个库导出函数,你可以跳过所有汇编/反汇编/JMP步骤,仅仅替换指向该函数的指针(无论是在虚拟函数表中还是在导出符号表中)。

这就是我为什么喜欢StackOverflow。我无法找到比这更好的答案了!我一定会给出更详细的评论,但我想先阅读你所有的链接,但我必须询问“DLL注入”。这对我有用吗?由于我的插件是一个dll / so,而'mod'/game会(动态)加载它,这意味着我只需将我的'hooking'代码放入我的库中,而不是使用LD_PRELOAD? - Elliott Darfink
我正在为著名FPS游戏《反恐精英》编写所谓的“元模组插件”(使用称为GoldSrc的引擎,已有大约12年历史)。令人沮丧的是,我几乎拥有我需要的一切。我实际上已经拥有了我试图挂钩的函数的源代码,我可以动态地找到它的地址并知道它的调用约定,因此我非常渴望解决这个难题的最后一部分,但我还没有准备好花费两个月时间逐一查阅汇编书籍... - Elliott Darfink
我明白了,但如果你知道JMP、MOV和PUSH是什么,那就足够完成该任务所需的汇编了。 - kuba
是的,根据您的解释,这似乎确实很容易。我只是试图弄清楚如何应用它。例如,既然我有地址,我应该在地址上使用memset(并指定jmp(0xE9),然后是我的钩子函数的地址(我猜这就是为什么它恰好是5个字节的原因?)),还是我应该使用汇编语言来应用这个“内存补丁”(我现在不知道如何做)。也许您对此有想法? :) - Elliott Darfink
如果您不熟悉汇编语言,建议首先在某个调试器中手动尝试挂钩。只需在调试器中断程序,并汇编指令以查看生成的二进制代码即可。 - kuba
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接