我正在编写一个为(我们)modders提供API的游戏的扩展程序。这个API提供了各种各样的东西,但它有一个限制。 API仅用于“引擎”,这意味着基于引擎发布的所有修改(mod)都不提供/具有任何类型的(特定于mod)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&amp; 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调用约定并不是很难(?)
答案 0 :(得分:12)
这个项目是否要开发一个&#34;框架&#34;那将允许其他人在不同的二进制文件中挂钩不同的功能?或者只是你需要挂钩你拥有的这个特定程序?
首先,让我们假设你想要第二件事,你只需要一个二进制函数,你想要挂钩,编程和可靠。普遍这样做的主要问题是,可靠地做这件事是一场非常艰难的比赛,但如果你愿意做出妥协,那么它绝对可行。另外,我们假设这是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个字节。)现在使用与原始函数相同的参数和相同的调用约定来调用函数。如果你的函数想要调用原始函数(但它不必),你创建一个&#34; trampoline&#34;,1)运行被覆盖的原始指令2)jmps到其余的原来的功能:
Trampoline:
MOV EDI,EDI
PUSH EBP
MOV EBP,ESP
JMP 00E32BD3
就是这样,你只需要在运行时通过发出处理器指令来构造trampoline函数。这个过程的难点在于让它可靠地工作,用于任何函数,任何调用约定和不同的OS /平台。其中一个问题是,如果要覆盖的5个字节在指令的中间结束。检测&#34;指令的结尾&#34;你基本上需要包含一个反汇编程序,因为在函数的开头可以有任何指令。或者,当函数本身短于5个字节时(总是返回0的函数可以写为XOR EAX,EAX; RETN
,只有3个字节)。
大多数当前的编译器/汇编器都会生成一个5字节长的函数序列,正是为了这个目的而挂钩。看到MOV EDI, EDI
?如果你想知道,&#34;为什么他们会把edi移到edi?那什么都没做!&#34;你是绝对正确的,但这是prolog的目的,正好是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的作用),例如你可能想要&#34;不要打电话给原来的&#34;但返回一些不同的价值。或者两次调用原始功能。相反,让你的钩子处理程序决定它是否以及在何处调用原始函数。此外,您不需要两个处理程序函数用于挂钩。
如果您对此所需的技术(主要是装配)知之甚少,或者不知道如何进行挂钩,我建议您研究一下Detours的作用。挂钩你自己的二进制文件并拿一个调试器(例如OllyDbg)来查看汇编级别它究竟做了什么,放置了什么指令以及放在哪里。 this tutorial也可以派上用场。
无论如何,如果您的任务是将特定程序中的某些功能挂钩,那么这是可行的,如果您遇到任何问题,请再次询问此处。基本上你可以做很多假设(如函数prologs或使用的约定),这将使你的任务更容易。
如果你想创建一个可靠的钩子框架,那么它仍然是一个完全不同的故事,你应该首先为一些简单的应用程序创建简单的钩子。
另请注意,此技术不是特定于操作系统的,它在所有x86平台上都相同,它可以在Linux和Windows上运行。什么 OS特定的是你可能必须改变代码的内存保护(&#34;解锁&#34;它,所以你可以写它),这是用{{1在Linux上,在Windows上使用mprotect
。调用约定也是不同的,这是您可以通过在编译器中使用正确的语法来解决的问题。
另一个麻烦是&#34; DLL注入&#34; (在Linux上它可能会被称为#34;共享库注入&#34;但术语 DLL注入是众所周知的)。您需要将代码(执行挂钩)放入程序中。我的建议是,如果可能,只需使用LD_PRELOAD环境变量,您可以在其中指定一个库,该库将在它运行之前加载到程序中。这已在很多时候描述过,例如:What is the LD_PRELOAD trick?。如果必须在运行时执行此操作,我担心您需要使用gdb或ptrace,我认为这很难(至少是ptrace的事情)。但是,您可以阅读this article on codeproject或this ptrace tutorial。
我也找到了一些不错的资源:
另外还有一点:这是&#34;内联修补&#34;不是唯一的方法。甚至有更简单的方法,例如如果函数是虚拟或者它是库导出函数,你可以跳过所有汇编/反汇编/ JMP事物并简单地替换指向该函数的指针(在虚函数表中)或在导出的符号表中。)