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

这是什么原因?这是某种套准清洁骗术吗?



#1 楼

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

其后,它使用与eax的偏移量: br />
要减去的值0xe50b是我们移入eax的地址。如果代码没有移到任何地方,则eax-0xe50b将为零,但如果代码已移到其他位置,则为偏移量。然后,我们添加地址objc_msg_close,因此即使代码已在内存中移动,我们也可以引用它。

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

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


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

#2 楼

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

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

#3 楼

现在,我可能不知道确切的原因是什么,但是使用这种方法还有一个很好的原因(到目前为止尚未提到):在静态分析过程中抛出反汇编程序。

已经讨论过call $+5的问题,所以我假设它们现在是已知的-否则请参考其他答案。基本上就像IA-32上的任何call一样,返回地址(call后面的指令的地址)被push ed到堆栈,并且假设堆栈没有返回,则被调用函数内部的ret指令可能会返回到该地址。同时被捣毁。

愚弄静态分析工具

即使看到IDA之类的复杂反汇编程序,在看到ret操作码时也会做什么?好吧,我们假设已经达到了功能边界。这是一个示例:



现在这不是我第一次看到这样的东西,我继续删除了该函数,因此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。我们从其他答案中对机制的描述中了解到了很多。但是然后它变得奇怪了: br />
add     rdx, 8

如果您还记得ADDR+8的工作原理,那么您会记得它将返回地址压入堆栈,然后将执行传递给被调用的函数,然后该函数随后调用push以便返回到堆栈上找到的地址。这种知识在这里被利用。它在“返回”地址之前操纵“返回地址”。但是回头看一下我们的反汇编,我们会感到惊讶(或不是;)):

push    rdx
retn


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

但是,请稍等一下,这意味着我们实际上将执行权传递给了这个奇特的ret的中间吗?那就对了。因为5A是在IA-32上对指令进行编码时使用的前缀之一。因此,程序员欺骗了我们的反汇编程序,但他不会欺骗我们。取消定义代码,然后跳过48前缀,我们得到:

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]


或:



现在发现单个四字节指令83是伪造的,取而代之的是我们必须忽略C2,然后在08处恢复,将其解码为52

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

评论


我已经看到了一些应用程序(特别是称为MetaFortress的反黑客保护),这些应用程序使用此技术将数据嵌入到应用程序的.text区域中。使用该调用跳过您的嵌入式数据,然后使用该调用的返回地址作为指向嵌入式数据的指针。

–ajs410
2013年12月12日0:34在

#4 楼

在执行向调用目标的控制转移之前,CALL指令的作用是将返回地址压入堆栈。

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

这是一种在运行时获取指令在内存中位置的简单技术。 />由于地址空间布局随机化(ASLR),二进制文件可能会在内存中重新定位,因此链接器有时无法始终在编译时计算指令位置。

评论


来自道加尔的答案是优越的。链接器通过重定位/修复表中的条目处理ASLR。该机制并不是要确定位置,而是要确定编译后的代码所期望的地址与运行时的实际地址之间的相对偏移差。

–达斯塔
13年4月8日在21:21



#5 楼

正如其他人所说,这是为了获取当前指令的地址。但是不建议这样做,因为它会损害性能,因为它不会在任何地方返回,从而导致数据堆栈和CPU内部调用堆栈中的返回地址不一致。
推荐的方法是
GetCurrentAddress:
    mov eax, [esp]
    ret
...
    call GetCurrentAddress
    mov [currentInstruction], eax


原因是处理器内部的“隐藏变量”。所有现代处理器所包含的状态远远超出了从指令序列中可以看到的状态。有TLB,L1和L2缓存,各种各样您看不到的东西。这里重要的隐藏变量是返回地址预测器。
较新的奔腾(我相信也是速龙)处理器维护一个内部堆栈,该堆栈由每个CALL和RET指令更新。当执行CALL时,返回地址被推入实际堆栈(ESP寄存器指向的堆栈)以及内部返回地址预测器堆栈。一条RET指令会弹出返回地址预测器堆栈以及实际堆栈的最高地址。
当处理器解码RET指令时,将使用返回地址预测器堆栈。它位于返回地址预测变量堆栈的顶部,并说:“我敢打赌RET指令将返回该地址。”然后,它推测性地执行该地址上的指令。由于程序很少在栈上摆弄返回地址,因此这些预测趋向于高度准确。
https://devblogs.microsoft.com/oldnewthing/20041216-00/?p=36973

>