通过减法计算jmp地址

逆向工程 拆卸 部件 x86 函数挂钩
2021-06-26 00:30:50

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

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

然后按如下方式使用:

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

这段代码的意图应该是在func2跳转的开头func1插入。跳转位置在第一个片段中计算。是对的吗?

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

注意:此示例来自有关内联挂钩主题的实用恶意软件分析书 (Lab11-2)。

4个回答

为了完整起见,我将首先简要回顾代码,即使 OP 清楚地了解发生了什么并且主要询问其背后的推理

第一段代码可以很容易地用 C 编写,如下所示:

dword var_4 = &func1 - &func2 - 5;

这段代码本身就提出了一些问题,我们将在稍后回答,但首先让我们深入研究第二个程序集片段:

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

的第一个字节func2设置为0xE9,这是“Jump near,relative,immediate”跳转的操作码。

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

然后,接下来的四个字节func(1 到 5)被设置为先前在第一个片段中计算的偏移量。

现在,这可能会引发几个问题:

为什么偏移量然后减少了5

这样做是因为相对跳转是相对于下一条指令的,因此减去 5 会删除跳转指令本身的 5 个附加字节。一种更准确的查看方式是偏移量应从 计算&func2 + 5原方程 ( &func1 - &func2 - 5) 显然与 相同&func1 - (&func2 + 5)

为什么我们一开始就如此关心指令长度?

所以,正如这里的一些人已经暗示的那样,勾手跳跃的长度很重要。这是非常正确的(尽管并没有说明相对跳跃偏好背后的全部原因)。钩子(或跳跃序列)的长度很重要,因为它会产生奇怪的边缘情况。正如人们可能假设的那样,这不仅仅是一些小的性能优化或保持简单。

一个重要的考虑因素是您需要替换您覆盖的任何指令。您用于跳转的那些字节是有意义的。它们必须保存在某个地方。覆盖更多字节意味着您必须在其他地方复制更多字节。例如,固定原始指令序列上的相关指令。你需要确保你没有留下一半的说明。

为什么使用相对跳转而不是绝对地址?

对不起,花了一段时间才到这里;D

我认为随着时间的推移,很多人会忽略或忘记这一点,但仔细查看跳转指令会发现,x86 跳转操作码缺乏接近立即绝对跳转

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

  1. E9对于近乎立即的偏移量(偏移量直接硬编码为指令本身内部的整数)。
  2. FF /4对于近乎绝对的跳跃。
  3. 我们已经得到EA非常直接的绝对跳跃。

跳操作码(EA)是缓慢的,主要用于改变段寄存器(其具有在保护模式完全不同的使用),则因此很少用作正常跳每本身,而是作为一个呼叫栅极,用于切换执行上下文等。

绝对地址跳转的操作码(FF /4不接受立即值。它只能跳转到存储在寄存器中或存储在内存中的值。因此,使用它需要您:

  1. 将绝对偏移量存储在某个保留的内存空间中,由钩子例程专门为此目的为每个钩子函数分配,或
  2. 硬编码寄存器加载指令,该指令会将寄存器设置为绝对值。mov eax, <absolute value> / jump eax或 之类的东西push <absolute value> / ret

理解这一点,很明显,使用近、直接、相对跳跃比这两种方法都容易得多。

因此,尽管准确地说使用绝对地址将需要更长的指令序列,但这并不能说明全部情况。

这又引出另一个问题:

那么,为什么在 x86 中没有近乎、直接、绝对的跳跃呢?

简单的答案是没有。人们可以推测指令集设计决策背后的推理,但添加指令既昂贵又复杂。我认为没有真正需要近乎绝对的立即跳转,因为这确实是一种罕见的情况,您需要跳转到提前已知的地址,而相对跳转是行不通的。

E9是一个相对跳转,因为它应该被插入到函数的开头,所以sub-tracting 两个地址是计算字节差异的方法。

为什么相对跳转而不是绝对跳转?它更短,所以如果需要记住原始字节,它只是 3 个而不是 5 个字节。

我没有获得这本书让我们说func1的地址开始0x10,并func2开始在0x30因此func2之间的距离func10x20字节。

如果你想从开头跳转func1func2你有两个选择(使用伪汇编):

  • 使用相对跳转(操作码E9):

    0x10 JR +0x20 ; will jump to 0x10 + func2-func1 = 0x10 + 0x30-0x10 = 0x30
    
  • 使用绝对跳转(操作码EA):

    0x10 JP 0x30 ; will jump 0x30 = func2
    

在您的情况下,两者都实现了相同的目标。相对跳转的好处是,你只需要知道有多远func2的距离func1您不必知道或关心可执行加载程序将在内存中的确切位置加载二进制文件。在我的示例中,它是0x10forfunc10x30forfunc2但实际上程序可能最终在0x120forfunc10x140for func2如果您有绝对跳转,则必须跳转到,0x140但如果您有相对跳转,则func2之间的差异func1保持不变0x20

在您的示例中,您已经知道 的实际地址,func2因此您也可以直接跳转到func2.

相对跳码花费较少的字节不是绝对跳转,但缺点是,如果之间的距离func2func1太大(具体取决于您的寻址模式),您将无法使用它。

让我尝试对您的代码片段进行可能的解释,而不考虑相对寻址似乎是迄今为止最直接的解决方案这一事实,正如 Pawel 已经指出的那样。

如果你用func1编写一个小程序func2,比如在 VS2015 中,并检查编译器生成的内容,你可能会发现以下内容: 编译器生成一个长相对 jmp 来输入函数func1在它的实现中,操作码E9已经就位。

这是编译器生成的:

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

对于真正的调用func1(由程序员用 C 编写),它生成以下内容:

003D4D6B E8 B6 C4 FF FF       call        func1 (03D1226h)

现在,如果您尝试将编译器的相对 jmp 替换为直接绝对 jmp(您的问题),则必须找到不长于相对 jmp(5 个字节)的汇编语句,以免破坏后续代码。我认为这并不容易。

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

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