这种方法自称的原因是什么?


30

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

这是什么原因?这是一种注册清理欺骗吗?

39

这是用于位置独立的代码。 call 0xe50b指令推入下一条指令的地址,然后跳转。它跳转到紧跟在后面的指令,这没有任何作用。下一条指令pop eax将自己的地址加载到eax(因为它是由call推送的值)。

再往下它使用从EAX偏移:

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

的值被减去,0xe50b,是我们移入eax地址。如果代码未被移动到任何位置,eax-0xe50b将为零,但是如果代码已被移至其他位置,则它将成为偏移量。然后,我们添加地址objc_msg_close,因此即使代码已移至内存中,我们也可以引用它。

Hopper是实际上是相当聪明的它,因为指令刚(从ndisasm)说:

mov eax,[eax+0x45fe75] 

但霍珀知道eax包含0xe50b指令指针的值,所以使用该偏移为你找到符号。


12

这是一种经常使用的“特技”确定的指令的以下的call的地址,即,呼叫指令将堆,其在此情况下对应于0xe50b上的返回地址。弹出指令后,eax包含该地址。例如,这个习语用于位置无关代码(pic),但在混淆代码中也很常见。

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


11

A CALL指令具有在执行控制传输到呼叫目标之前将返回地址压入堆栈的效果。

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

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

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

  0

Dougall的答案很好。链接器通过重定位/修正表中的条目处理ASLR。这个机制与确定位置无关,因为它可以确定编译代码所期望的地址与运行时的实际地址之间的相对偏移量*差异*。 08 4月. 132013-04-08 21:21:53


11

现在我不可能知道确切的原因在这里,但还有另一个很好的理由,迄今为止没有提到,使用这种方法:在静态分析期间抛弃反汇编程序。

call $+5的机制已经讨论过了,所以我会假设他们现在已经知道 - 否则请参考其他答案。基本上与IA-32上的任何call一样,返回地址(call后面的指令地址)得到push,堆栈和ret指令在该被调用函数内大概会返回到该地址,假设堆栈没有同时被砸了。

愚弄静态分析工具

时,看到一个ret操作码是什么,甚至将一个复杂的反汇编如IDA吗?那么,它会假设已经达到了函数边界。这里有一个例子:

IDA trips over this trick

现在这不是我第一次见过这样的事情,我去和删除的功能,所以IDA停止假设它是一个函数的边界。如果我再告诉它拆卸非常下一个字节(0Fh)我得到这样的:

IDA trips over this trick #2

什么反汇编器无法实现的,什么是为什么像漏斗和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 

我们rdx获得地址ADDR在执行pop指令后。我们从其他答案中对机制的描述中了解了很多。但随后它会奇怪:

add rdx, 8 

我们添加...嗯八个字节到该地址(ADDR+8),然后我们把它push堆栈,并呼吁ret

push rdx 
retn 

如果你还记得如何一个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 

或:

No longer tricked thanks to reverse engineer intervening

表观单四字节指令0F 5A 5B 59现在发现是假的,而是我们不得不忽视0F,然后在5A恢复,其解码为pop rdx

查看Ange's excellent opcode tables here了解更多关于指令如何在IA-32上编码的信息。

+2

我见过一些应用程序(特别是一种称为MetaFortress的反黑客保护),它使用这种技术将数据嵌入应用程序的.text区域。使用该调用跳过您的嵌入数据,然后使用该调用的返回地址作为指向嵌入数据的指针。 12 12月. 132013-12-12 00:34:15


4

正如其他人所说,这是为了获取当前指令的地址。但是它不推荐,因为它会伤害性能,因为它不会在任何地方返回,从而导致返回地址的分歧在数据堆栈,并在CPU内部的调用堆栈

推荐的方法是

GetCurrentAddress: 
    mov eax, [esp] 
    ret 
... 
    call GetCurrentAddress 
    mov [currentInstruction], eax 

http://blogs.msdn.com/b/oldnewthing/archive/2004/12/16/317157.aspx

原因是处理器内部的“隐藏变量”。所有现代处理器都包含比您从指令序列中看到的更多的状态。有TLB,L1和L2缓存,各种你看不到的东西。这里重要的隐藏变量是返回地址预测器。

最近的奔腾处理器(我也相信Athlon处理器)会维护一个内部堆栈,每个CALL和RET指令都会更新它。执行CALL时,将返回地址同时推送到实际堆栈(ESP寄存器指向的那个堆栈)以及内部返回地址预测器堆栈; RET指令会弹出返回地址预测器堆栈的顶部地址以及实际堆栈。

当处理器解码RET指令时使用返回地址预测器栈。它看起来在返回地址预测器堆栈的顶部,并说,“我打赌RET指令将返回到该地址。”然后推测性地执行该地址的指示。由于程序很少摆弄堆栈中的返回地址,因此这些预测往往非常准确。