我不明白为什么要减去函数的两个地址以获得跳转目标。

mov    eax, [ebp+func1]
sub    eax, [ebp+func2]
sub    eax, 5
mov    [ebp+var_4], eax


然后其使用方式如下:在func2的开头应插入一个跳转到func1的位置。跳转位置是在第一个代码段中计算出来的。是吗?

我不明白为什么通过两个内存地址的差来计算位置?为什么不直接使用func1的地址?

注意:此示例摘自实用恶意软件分析书(Lab11-2),主题为内联挂钩。

评论

早在Stack Overflow上已经问过这个问题:stackoverflow.com/questions/1546141/…

相似,但是不会像以前那样完全(不是全部)阐明问题。

我不是在抱怨或暗示您做错了什么。我只是将其发布在这里以供将来参考。

#1 楼

为了完整起见,尽管OP清楚地了解正在发生的事情,并且主要询问其背后的原因,但我还是会简要地回顾一下代码。

第一个代码段可以像下面这样轻松地编写在C中:

dword var_4 = &func1 - &func2 - 5;


这段代码本身引发了一些问题,我们将稍作回答,但首先让我们更深入地研究第二个程序集片段:

mov    edx, [ebp+func2]
mov    [edx], 0E9h         ;E9 is opcode for jmp


func2的第一个字节设置为0xE9,这是“跳近,相对,立即”跳转的操作码。

mov    eax, [ebp+func2]
mov    ecx, [ebp+var_4]
mov    [eax+1], ecx


然后,将func的下四个字节(1到5)设置为先前在第一个代码段中计算出的偏移量。

现在,这可能会增加几个问题:


为什么偏移量然后减少5?指令,因此减去5将删除5个额外的字节o f跳转指令本身。一种更准确的观察方法是应从&func2 + 5计算偏移量。原始等式(&func1 - &func2 - 5)显然与&func1 - (&func2 + 5)相同。 ,正如这里的某些人已经暗示的那样,钩跳的长度很重要。这是非常正确的(尽管并不能说明相对跳转首选项背后的全部原因)。挂钩的长度(或跳转序列)很重要,因为它会产生奇怪的边缘情况。正如人们可能会想到的那样,这不只是一些次要的性能优化或保持简单。

一个重要的考虑因素是,您需要替换所有覆盖的指令。您用于跳转的那些字节具有含义。而且它们必须保存在某个地方。覆盖更多字节意味着您必须在其他位置复制更多字节。例如,相对指令固定在原始指令序列上。您需要确保不要在指令后留下半条指令。


为什么要使用相对跳转而不是绝对地址? br />我认为许多人会随着时间的流逝而忽略或忘记这一点,但是正如仔细阅读跳转指令所揭示的那样,x86跳转操作码缺少近,立即,绝对的跳转。

我们已经在x86中有三种不同类型的跳转:



E9用于近乎立即的偏移量(偏移量直接硬编码为指令内部的整数)。

FF /4用于近乎绝对的跳转。
我们已经有了EA用于远近的绝对跳转。 (在保护模式下是完全不同的用法),因此很少将其本身用作常规跳转,而在执行上下文等之间切换时用作调用门。

绝对地址跳转操作码(EA)不接受立即数。它只能跳转到存储在寄存器中或存储在内存中的值。因此,使用它将需要您:为此,每个钩子函数的钩子例程,或者
对寄存器加载指令进行硬编码,这会将寄存器设置为绝对值。像FF /4mov eax, <absolute value> / jump eax之类的东西。因此,尽管准确地说使用绝对地址将需要更长的指令序列,但这并不能说明全部情况。

这又引发了另一个问题:


那么为什么在x86中没有近距离,立即,绝对的跳跃?


简单的答案是,没有答案。可以推测指令集设计决策背后的原因,但添加指令既昂贵又复杂。我认为实际上并不需要几乎绝对的立即跳转,因为确实是在极少数情况下,您需要跳转到提前知道的地址,而相对跳转不会发生。

评论


很棒的帖子。感谢您提供有用的信息!现在背景变得清晰了。

– Pudi
18-09-26在19:56



感谢您的赞美和提问!如有任何疑问,请致电lmk,我将详细说明。

– NirIzr
18-09-26在19:58

实际上,x86:EA会立即出现绝对跳跃。这是一大步;又长又慢(因此现在很少使用,因为我们不再使用段了),但是它确实存在。

–user2347953
'18 -10-1在19:07

@ user2347953你是对的。我暗含了使用近距离跳转的原因,因为正如您所说,远距离跳转很慢,很少在保护模式下用作普通跳转。我将编辑答案以反映/解决该问题。谢谢!

– NirIzr
'18 -10-1在19:20

#2 楼

E9是一个相对跳转,由于应该在函数的开头插入它,因此sub-压缩两个地址是计算字节差的方法。

为什么相对跳跃而不是绝对跳跃?它较短,因此如果需要记住原始字节,则只需3个字节,而不是5个字节。

评论


是的,我明白了这一点,但背后没有原因。有什么理由在这里进行相对跳转而不是简单地使用函数的位置吗?

– Pudi
18-09-26在17:16

查看最新答案

–PawełŁukasik
18-09-26在17:40

#3 楼

我没有这本书的访问权限,所以假设func1从地址0x10开始,而func20x30开始。因此,func2func1之间的距离为0x20字节。

如果要从func1的开头跳到func2,则有两种选择(使用伪汇编): br />
使用相对跳转(操作码E9):

使用绝对跳转(操作码EA):

0x10 JR +0x20 ; will jump to 0x10 + func2-func1 = 0x10 + 0x30-0x10 = 0x30



在您的情况下,两者都可以达到相同的效果。相对跳转的优点是您只需要知道func2func1之间的距离即可。您不必知道或不在乎可执行加载程序将在二进制文件加载到内存的确切位置。在我的示例中,0x10func10x30func2,但实际上,该程序可能最终以0x120func10x140结束。如果您有绝对跳转,则必须跳转到func2,但是如果您有相对跳转,则0x140func2之间的差异仍然相同func10x20的大小,因此您也可以直接跳转到func2。寻址模式),您将无法使用它。

评论


“您不必知道或不在乎可执行加载程序将在何处加载二进制文件。”尽管在您的示例中确实确实知道了函数地址,但OP实例清楚地表明了函数地址是动态的,在编译时并不知道。而且,挂钩通常是在运行时在不同模块中加载的函数上完成的,因此偏移量确实会发生变化(即使没有ASLR)。

– NirIzr
18-09-26在19:50

我同意。我过于简化了我的答案,不确定不确定OP到底在问什么以及期望的详细程度是什么。您的答案涵盖了所有细节。

– zxxc
18-09-26在20:15

谢谢,欢迎来到RE.SE!希望我能在这里看到更多的答案:)

– NirIzr
18-09-26在20:16

#4 楼

让我为您的代码段尝试一个可能的解释,独立于相对寻址似乎是迄今为止最直接的解决方案,正如Pawel已经指出的那样。和func1(在VS2015中说)并检查编译器生成的内容,您可能会发现以下内容:
编译器生成一个较长的相对jmp来输入函数func2。在实现中,操作码func1已经存在。

这是编译器生成的:以下:

func1:
003D1226 E9 B5 0B 00 00       jmp         func1 (03D1DE0h) 


现在,如果您尝试用直接绝对jmp替换编译器的相对jmp(您的问题),则必须找到一个不再需要的汇编程序而不是相对的jmp(5字节),以免破坏后续代码。我认为这并不容易。

您可以在此处找到有关类似问题的讨论。

顺便说一句,如果您想自己尝试一下,则必须确保该代码段是可写的,而通常不是。在Windows中,您可以使用对“ VirtualProtect”的适当调用来实现它。