什么是“重叠指令”混淆?

逆向工程 混淆 二元分析 去混淆
2021-06-28 01:55:55

我使用一些混淆技巧分析了 x86/x86-64 中的一些二进制文件。一种叫做重叠指令有人可以解释这种混淆是如何工作的以及如何解决的吗?

4个回答

x86 可执行文件的静态分析一很好地解释了重叠指令。以下示例取自它(第 28 页):

0000: B8 00 03 C1 BB  mov eax, 0xBBC10300
0005: B9 00 00 00 05  mov ecx, 0x05000000
000A: 03 C1           add eax, ecx
000C: EB F4           jmp $-10
000E: 03 C3           add eax, ebx
0010: C3              ret

通过查看代码,在返回指令中 eax 的值是什么并不明显(或者是否曾经到达返回指令,就此而言)。这是由于从 000C 跳转到 0002,该地址未明确出现在列表中(jmp $-10 表示从当前程序计数器值的相对跳转,即 0xC,0xC10 = 2)。此跳转将控制转移到地址 0000 处的五字节长移动指令的第三个字节。执行从地址 0002 开始的字节序列将展开一个全新的指令流:

0000: B8 00 03 C1 BB  mov eax, 0xBBC10300
0005: B9 00 00 00 05  mov ecx, 0x05000000
000A: 03 C1           add eax, ecx
000C: EB F4           jmp $-10
0002: 03 C1           add eax, ecx
0004: BB B9 00 00 00  mov ebx, 0xB9
0009: 05 03 C1 EB F4  add eax, 0xF4EBC103
000E: 03 C3           add eax, ebx
0010: C3              ret

了解 Ida Pro,尤其是 Hex Rays 插件是否/如何处理这个问题会很有趣。也许@IgorSkochinsky 可以对此发表评论......

它也被称为“中间跳跃”技巧。

解释

执行规则

  • 大多数指令需要一个以上的字节来编码
    • 它们在现代 CPU 上最多可占用 15 个字节
  • 只要权限有效,就可以在任何位置开始执行

所以在一条指令的第一个之后的任何字节都可以重新用于启动另一条指令。

滥用反汇编程序

  • 直接的反汇编程序在最后一条指令结束后立即开始下一条指令。

所以这样的反汇编器(不遵循流程)将隐藏可见指令中间的指令

例子

不重要的

00: EB 01           jmp  3
02: 68 c3 90 90 90  push 0x909090c3

将有效地执行为

00: EB 01           jmp  3
03: C3              retn
...

因为第一个jmp跳过68了以下指令的第一个字节(编码立即推送)。

多重重叠

例中,69 84定义了一条imul最多可占用 11 个字节指令。因此,您可以在其“假”操作数中放入几行指令。

00: EB02                    jmp  4
02: 69846A40682C104000EB02  imul eax, [edx + ebp*2 + 0102C6840], 0x002EB0040
0D: ....

实际上将被执行为

00: EB02       jmp  4
04: 6A40       push 040
06: 682C104000 push 0x40102C
0B: EB02       jmp  0xF
0F: ...

指令重叠

该指令在其自身的第二个字节中跳转:

00: EBFF    jmp 1
02: C0C300  rol bl, 0

实际上将被执行为

00: EBFF    jmp 1
01: FFC0    inc eax
03: C3      retn

不同的CPU模式

这种混淆可以扩展到跳转到相同的 EIP 但在不同的 CPU 模式下:

  • 64b CPU 仍然支持 32b 指令
  • 64b 模式0x33用于cs
  • 某些指令仅在特定模式下可用:
    • arpl 在 32b 模式下
    • movsxd 在 64b 模式下

所以你可以跳转到相同EIP但不同的CS,并获得不同的指令。

在这个例子中,这段代码首先在 32b 模式下执行:

00: 63D8   arpl   ax,bx
02: 48     dec    eax
03: 01C0   add    eax,eax
05: CB     retf

然后在 64 位模式下重新执行为:

00: 63D8   movsxd rbx,eax
02: 4801C0 add    rax,rax
05: CB     retf

在这种情况下,指令重叠,不是因为不同的EIP,而是因为CPU暂时从32b模式变成了64b模式。

几乎任何多字节指令都可以用作 x86/x86_64 中的重叠指令。原因很简单:x86 和x86_64 指令集是CISC。这意味着,除其他外,指令没有固定长度。因此,由于指令是可变长度的,仔细编写该机器代码,每条指令都容易隐藏重叠指令。

例如,给定以下代码:

[0x00408210:0x00a31e10]> b
0x000050f5 (01) 56                     PUSH ESI 
0x000050f6 (04) 8b742408               MOV ESI, [ESP+0x8] 
0x000050fa (01) 57                     PUSH EDI 
0x000050fb (03) c1e603                 SHL ESI, 0x3 
0x000050fe (06) 8bbe58a04000           MOV EDI, [ESI+0x40a058] 
0x00005104 (01) 57                     PUSH EDI 
0x00005105 (06) ff15f4804000           CALL 0x004080f4  ; 1 KERNEL32.dll!GetModuleHandleA
0x0000510b (02) 85c0                   TEST EAX, EAX 
0x0000510d (02) 750b                   JNZ 0x0000511a   ; 2 

假设在最后一条指令之后的某处,显示代码中某条指令的中间有一个跳转,例如,跳转到 MOV ESI... 指令中的第二个字节:

[0x000050f7:0x00405cf7]> c
0x000050f7 (02) 7424                   JZ 0x0000511d    ; 1 
0x000050f7 ----------------------------------------------------------------------
0x000050f9 (03) 0857c1                 OR [EDI-0x3f], DL 
0x000050fc (02) e603                   OUT 0x3, AL 

原来这条指令变成了JZ。这是有效的。跳到第 3 个字节...

[0x000050f7:0x00405cf7]> s +1
[0x000050f8:0x00405cf8]> c
0x000050f8 (02) 2408                   AND AL, 0x8 
0x000050fa (01) 57                     PUSH EDI 
0x000050fb (03) c1e603                 SHL ESI, 0x3 
0x000050fe (06) 8bbe58a04000           MOV EDI, [ESI+0x40a058] 

跳转到 CALL 指令的第二个字节:

[0x000050f5:0x00405cf5]> s 0x5106
[0x00005106:0x00405d06]> c
0x00005106 (05) 15f4804000             ADC EAX, 0x4080f4    ; '\x8e\x91'
0x0000510b (02) 85c0                   TEST EAX, EAX 
0x0000510d (02) 750b                   JNZ 0x0000511a   ; 1 

如您所见,几乎任何多字节指令都容易被用作重叠指令。

这种反逆转技巧经常与不透明的谓词一起使用,以便对流图进行操作。

因为 x86 指令可以是任意长度并且不需要对齐,所以一条指令的立即值可以完全是另一条指令。例如:

00000000  0531C0EB01        add eax,0x1ebc031
00000005  055090EB01        add eax,0x1eb9050
0000000A  05B010EB01        add eax,0x1eb10b0
0000000F  EBF0              jmp short 0x1

这正是它所说的,直到跳转。当它跳转时,加到 eax 的立即数变成一条指令,所以代码看起来像:

00000000  05                db 5
00000001  31C0              xor ax,ax           xor ax, ax
00000003  EB01              jmp short 0x6
00000005  05                db 5
00000006  50                push ax             push ax
00000007  90                nop
00000008  EB01              jmp short 0xb
0000000A  05                db 5
0000000B  B010              mov al,0x10         mov al,0x10
....

实际重要的说明显示在右侧栏中。在这个例子中,短跳转指令用于跳过add eax指令(05部分应该注意的是,这可以通过使用单字节来吃掉05s来更有效地完成,例如3C05which is cmp al, 0x5,并且在不关心标志的代码中是无害的。

在上面的模式中,您可以轻松地将所有05s替换90(nop) 以查看正确的反汇编。通过使用05s 作为隐藏代码的直接值(执行取决于),这可以变得更加棘手实际上,混淆代码的人可能不会add eax一遍又一遍地使用,并且可能会更改执行顺序以使其更难以跟踪。

我使用上面的模式准备了一个样本。这是 base64 格式的 32 位 Linux ELF 文件。隐藏代码的效果是运行execve("//usr/bin/python", 0, 0)我建议您不要从 SE 答案中运行随机二进制文件。但是,您可以使用它来测试您的反汇编程序。IDA、Hopper 和 objdump 乍一看都失败得很惨,尽管我想你可以让 IDA 以某种方式正确地完成它。

f0VMRgEBAQAAAAAAAAAAAAIAAwABAAAAYIAECDQAAAAoAQAAAAAAADQAIAABACgAAwACAAEAAAAA
AAAAAIAECACABAgUAQAAFAEAAAUAAAAAEAAAAAAAAAAAAAAAAAAABTHA6wEFUJDrAQWwEOsBBffg
6wEF9+DrAQWJw+sBBbRu6wEFsG/rAQX34+sBBbRo6wEFsHTrAQVQkOsBBbR56wEFsHDrAQX34+sB
BbQv6wEFsG7rAQVQkOsBBbRp6wEFsGLrAQX34+sBBbQv6wEFsHLrAQVQkOsBBbRz6wEFsHXrAQX3
4+sBBbQv6wEFsC/rAQVQkOsBBTHJ6wEF9+HrAQWJ4+sBBbAL6wEFzYDrAelN////AC5zaHN0cnRh
YgAudGV4dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAEA
AAAGAAAAYIAECGAAAAC0AAAAAAAAAAAAAAAQAAAAAAAAAAEAAAADAAAAAAAAAAAAAAAUAQAAEQAA
AAAAAAAAAAAAAQAAAAAAAAA=