我使用一些混淆技巧分析了 x86/x86-64 中的一些二进制文件。一种叫做重叠指令。有人可以解释这种混淆是如何工作的以及如何解决的吗?
什么是“重叠指令”混淆?
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
)的部分。应该注意的是,这可以通过使用单字节来吃掉05
s来更有效地完成,例如3C05
which is cmp al, 0x5
,并且在不关心标志的代码中是无害的。
在上面的模式中,您可以轻松地将所有05
s替换为90
(nop) 以查看正确的反汇编。通过使用05
s 作为隐藏代码的直接值(执行取决于),这可以变得更加棘手。实际上,混淆代码的人可能不会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=