在使用 Hopper 反转 32 位 Mach-O 二进制文件时,我注意到了这种奇特的方法。0x0000e506 上的指令似乎正在调用指令正下方的地址。
这是什么原因?这是某种寄存器清理技巧吗?
在使用 Hopper 反转 32 位 Mach-O 二进制文件时,我注意到了这种奇特的方法。0x0000e506 上的指令似乎正在调用指令正下方的地址。
这是什么原因?这是某种寄存器清理技巧吗?
这是用于位置无关代码。该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
进行了讨论,所以我会认为它们是由目前已知的-否则指的是其他的答案。基本上与call
IA-32上的任何内容一样,返回地址(在 之后的指令的地址call
)进入push
堆栈ret
,假设堆栈没有同时被粉碎,则被调用函数内的指令可能会返回到该地址。
当它看到一个ret
操作码时,即使是像 IDA 这样复杂的反汇编器会做什么?好吧,它会假设已经达到了函数边界。下面是一个例子:
现在这不是我第一次看到这样的事情,我继续删除了这个函数,所以 IDA 不再假设它是一个函数边界。如果我然后告诉它反汇编下一个字节 ( 0Fh
) 我得到这个:
反汇编器无法意识到的,以及像 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
我们得到的地址ADDR
中rdx
的后,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]
让我们计算操作码字节数(在您的工具中,您也可以通过偏移量进行数学计算,如果您愿意的话):
5A
48
83
C2
08
52
C3
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),二进制文件可能会在内存中重新定位。