这个方法调用自己的原因是什么?

逆向工程 混淆 x86 部件 料斗
2021-06-12 00:42:17

在使用 Hopper 反转 32 位 Mach-O 二进制文件时,我注意到了这种奇特的方法。0x0000e506 上的指令似乎正在调用指令正下方的地址。

这是什么原因?这是某种寄存器清理技巧吗?

4个回答

这是用于位置无关代码。call 0xe50b指令压入下一条指令的地址,然后跳转。它跳转到紧随其后的指令,该指令无效。下一条指令pop eax将其自己的地址加载到eax(因为它是由 压入的值call)。

再往下,它使用来自 eax 的偏移量:

mov eax, dword [ds:eax-0xe50b+objc_msg_close]

减去的值是0xe50b我们移入的地址eax如果代码没有移动到任何地方,eax-0xe50b则为零,但如果代码已移动到不同的位置,它将是偏移量。然后我们添加 address objc_msg_close,这样我们就可以引用它,即使代码已在内存中移动。

Hopper 实际上对此非常聪明,因为指令只是说(来自 ndisasm):

mov eax,[eax+0x45fe75]

但 Hopper 知道eax包含指令指针的值0xe50b,因此使用该偏移量为您找到符号。

这是一个经常使用的“技巧”,用于确定 之后的指令的地址call,即调用指令将返回地址压入堆栈,在这种情况下对应于0xe50b在 pop 指令之后,eax 包含该地址。例如,这个习语用于位置无关代码 (pic),但在混淆代码中也很常见。

其他反汇编程序通常将此代码序列显示为call $+5(例如 IDA)。

现在我不可能知道这里的确切原因是什么,但是使用这种方法还有另一个非常好的原因,目前尚未提及:在静态分析期间扔掉反汇编程序。

的机制call $+5进行了讨论,所以我会认为它们是由目前已知的-否则指的是其他的答案。基本上与callIA-32上的任何内容一样,返回地址(在 之后的指令的地址call)进入push堆栈ret,假设堆栈没有同时被粉碎,则被调用函数内指令可能会返回到该地址。

欺骗静态分析工具

当它看到一个ret操作码时,即使是像 IDA 这样复杂的反汇编器会做什么好吧,它会假设已经达到了函数边界。下面是一个例子:

IDA 绊倒这个技巧

现在这不是我第一次看到这样的事情,我继续删除了这个函数,所以 IDA 不再假设它是一个函数边界。如果我然后告诉它反汇编下一个字节 ( 0Fh) 我得到这个:

IDA 使用这个技巧 #2

反汇编器无法意识到的,以及像 Hopper 和 IDA 这样的交互式反汇编器如此受欢迎的原因是什么,是这里发生了一些特别的事情。我们来看看指令:

51                                      push    rcx
53                                      push    rbx
52                                      push    rdx
E8 00 00 00 00                          call    $+5
5A                                      pop     rdx
48 83 C2 08                             add     rdx, 8
52                                      push    rdx
C3                                      retn
0F 5A 5B 59                             cvtps2pd xmm3, qword ptr [rbx+59h]
89 DF                                   mov     edi, ebx
52                                      push    rdx
48 31 D2                                xor     rdx, rdx

前导字节是二进制中的实际字节,后面是它们的助记符。但要特别注意这部分:

call    $+5
pop     rdx ; <- = ADDR
add     rdx, 8
push    rdx
retn

我们得到的地址ADDRrdx的后,pop被执行的指令。我们从其他答案中对机制的描述中了解了很多。但随后就变得奇怪了:

add     rdx, 8

我们将 ... 呃 8 个字节添加到该地址 ( ADDR+8) 然后我们将push其添加到堆栈并调用ret

push    rdx
retn

如果您还记得 a 是如何call工作的,那么您就会记得它将返回地址推送到堆栈,然后将执行传递给被调用的函数,该函数稍后会调用ret以返回到在堆栈上找到的地址。这些知识在这里被利用。它在“返回”之前操纵“返回地址”。但是回顾我们的反汇编,我们惊讶地发现(或不;)):

E8 00 00 00 00                          call    $+5
5A                                      pop     rdx
48 83 C2 08                             add     rdx, 8
52                                      push    rdx
C3                                      retn
0F 5A 5B 59                             cvtps2pd xmm3, qword ptr [rbx+59h]

让我们计算操作码字节数(在您的工具中,您也可以通过偏移量进行数学计算,如果您愿意的话):

  1. 5A
  2. 48
  3. 83
  4. C2
  5. 08
  6. 52
  7. C3
  8. 0F

但是等一下,这意味着我们实际上是在将执行传递到这个奇特的中间cvtps2pd xmm3, qword ptr [rbx+59h]那就对了。因为0Fh是在 IA-32 上编码指令时使用的前缀之一。所以程序员欺骗了我们的反汇编器,但他不会欺骗我们。取消定义该代码,然后跳过0Fh我们得到前缀:

51                                      push    rcx
53                                      push    rbx
52                                      push    rdx
E8 00 00 00 00                          call    $+5
5A                                      pop     rdx
48 83 C2 08                             add     rdx, 8
52                                      push    rdx
C3                                      retn
0F                                      db  0Fh
5A                                      pop     rdx
5B                                      pop     rbx
59                                      pop     rcx
89 DF                                   mov     edi, ebx
52                                      push    rdx
48 31 D2                                xor     rdx, rdx

或者:

由于逆向工程师干预,不再受骗

明显的单个四字节指令0F 5A 5B 59现在被发现是伪造的,相反我们必须忽略0F,然后继续在5A,解码为pop rdx

在此处查看Ange 出色的操作码表,以了解有关如何在 IA-32 上编码指令的更多信息。

CALL指令具有按下一个返回地址压入堆栈中,执行控制转移到呼叫目标之前的效果。

在上面的示例中,CALL指令会将值 0x0000E50B 压入堆栈,然后再将控制权转移到 0x0000E50B。POP然后 0x0000E50B 处指令将从堆栈顶部弹出最后一个值,进入 EAX。POP由于CALL指令推送返回值该值将是指令自己的地址

这是一种在运行时获取内存中指令位置的简单技术。

链接器不能总是在编译时计算指令位置,因为由于地址空间布局随机化 (ASLR),二进制文件可能会在内存中重新定位。