处理混淆的 PUSH PUSH RET 指令

逆向工程 部件 x64dbg 去混淆
2021-07-08 17:36:31

我想在 VM 上运行一个应用程序,但有 PUSH PUSH RET 阻止我查看他们的反 VM 代码。当我在 VM 上运行它时有一个消息框。我在 at 设置了一个断点,MessageBoxA它被触发,并且 EAX 正在显示 VM 的消息。到目前为止一切都很好。

在此处输入图片说明

我正在使用 x64dbg 并打开Call Stack选项卡并双击 user32.MessageBoxA 之后的地址(基本上是第二条记录),因此我可以在实际 MessageBoxA 之前追溯调用。它让我在这里:

在此处输入图片说明

听说PUSH RET像一个jmpPUSH PUSH RET就是像一个call它们是如何工作的?他们可以用工具/插件或其他东西去混淆吗?

PUSH 0xC9DFBF 把我带到这里:

在此处输入图片说明

PUSH 0xA4DB49 把我带到这里:

在此处输入图片说明

当我遵循那个 jmp 时,它会导致与PUSH 0xC9DFBF's image类似的代码,该代码以一个 jmp 到 MessageBoxA 结尾。

在实际的 MessageBoxA 调用之前,您有什么建议我进入代码,以便我可以找到他们的反 VM 代码并修补它?我是否需要从头开始一步一步地按照说明进行操作?

转储:http : //ufile.io/56c5b2z6变基到 0x1270000 并找到 0181E65C 地址。该地址在可执行文件中。我还附上了两个转储的 DLL,因为它们被引用了。

2个回答

RET指令将控制转移到位于堆栈上的返回地址。通常这用于从函数返回到调用函数的位置,因为下一条指令的地址被指令压入堆栈CALL

但是,RET可能会被误用:a PUSHbeforeRET指令是一种典型的混淆技术。在这种情况下,当执行RET, 而不是返回调用者函数时,它的行为就像跳转到先前推送的地址。这不是您的反编译器所期望的,这可能会导致跳转到尚未反编译或被错误反编译的内存区域。

PUSH PUSH RET可用于实现CALL您提到的 a 。在这种情况下,第一个PUSH将返回地址压入堆栈(在 之后的那个RET)。第二个PUSH推送函数地址。RET(如前所述)跳转到函数地址(以及从栈中删除这个地址)。当函数地址执行其 时RET,它会跳回到当前位于堆栈中的地址(第一个PUSH),返回到函数的调用者。请注意,这不是您的情况。在您的情况下,推送的第一个地址不在当前函数中。这意味着这不是实现CALL. 这很可能被用作一种混淆技术,使跟踪调用变得更加复杂,并且可能会欺骗您的反汇编以错误地向您显示代码。

另外,请注意,执行 aPUSH不会带您到任何地方。它只是将地址压入堆栈。它是RET实际跳转到最后推送地址指令。这也意味着第二个推送地址在第一个地址之前被访问。这意味着,你正在寻找从到达代码A4DB49(该首地址RET是跳到另一个)JUMP

我建议您查看实用恶意软件分析手册的反反汇编章节,其中详细介绍了此类技术并附有示例。

从技术上讲,

PUSH A
RET

相当于

JMP A

PUSH A
PUSH B
RET

相当于

CALL B
JMP A

您可以编写一个 x64dbg 脚本来搜索上述模式并将其替换为简化的程序集。


例子

PUSH + RET 有字节模式

0:  68 44 33 22 11          push   0x11223344
5:  c3                      ret 

以下脚本将其替换为 ,JMP其余部分(如果有)替换为NOP

$base = 0x91e000
$search_size = 0x100

findall $base, "68 ?? ?? ?? ?? c3", $search_size
$count = $result

next:
    dec $count
    $addr = ref.addr($count)
    $jmp_addr = dis.imm($addr)
    asm $addr, "jmp 0x{$jmp_addr}"
    $asm_size = $result
    $remainder = 6 - $asm_size

    fill_nop:
        dec $remainder
        asm $addr+$asm_size+$remainder, "nop"
        test $remainder, $remainder
        jnz fill_nop

    test $count, $count
    jnz next

msg "Done"

同样对于PUSH + PUSH + RET,

0:  68 44 33 22 11          push   0x11223344
5:  68 dd cc bb aa          push   0xaabbccdd
a:  c3                      ret 

以下脚本将其替换为CALL + JMP.

$base = 0x91e000
$search_size = 0x100

findall $base, "68 ?? ?? ?? ?? 68 ?? ?? ?? ?? c3", $search_size
$count = $result

next:
    dec $count
    $addr = ref.addr($count)
    $ret_addr = dis.imm($addr)
    $call_addr = dis.imm($addr+5)
    asm $addr, "call 0x{$call_addr}"
    $asm_size = $result
    asm $addr+$asm_size, "jmp 0x{$ret_addr}"
    add $asm_size, $result
    $remainder = 0xb - $asm_size

    fill_nop:
        dec $remainder
        asm $addr+$asm_size+$remainder, "nop"
        test $remainder, $remainder
        jnz fill_nop

    test $count, $count
    jnz next

msg "Done"

但是,上述方法有一些注意事项:

  1. PUSH + PUSH + RETCALL + JMP仅当被调用者使用RET指令返回调用者时才能转换为(这在 cdecl 中是正常的)。如果它使用JMP指令混淆返回,它将不起作用那不是RET

    ADD ESP, 4
    JMP DWORD PTR [ESP-4]
    
  2. 模式搜索在大多数情况下都有效。但是,如果部分代码被加密,则可能会返回误报,并且您可能会无意中覆盖错误的数据。