原文地址:http://drops.wooyun.org/papers/11032

Ke Sun([email protected])

原文:http://en.wooyun.io/2015/12/08/33.html

0x00 摘要


混淆是一种能增加二进制分析和逆向工程难度与成本的常用技术。主流的混淆技术都是着眼于使用与目标CPU相同的机器代码,在相同的处理器模式下,隐藏代码并进行控制。本文中引入了一种新的混淆方法,这一方法利用了64Windows系统中的32位/64位交叉模式编码。针对静态和动态分析工具的案例研究证明了这种混淆技术虽然简单,但是十分有效。

0x01 64位Windows中的模式转换


所有的64位Windows操作系统都能向下兼容未修改过的 32位应用,这是通过WoW64(64位Windows上的32位Windows)子系统实现的。WoW64使用了3个动态链接库(Wow64.dll,Wow64win.dll 和 Wow64cpu.dll),提供了一个兼容层,用作64位内核与32位应用{1}之间的接口,借此,处理器模式就可以在32位和64位之间动态地切换。

在任何时间中,处理器模式都是由当前代码段(CS)描述符中的控制位决定的。图1就是一个段描述符的示意图。L位(21位)是“64位代码段”控制标记:“值为1则说明这个代码段中的指令会在64位模式下执行,值为0则说明在这个代码段中的指令会在兼容(32位)模式下执行。” {2}

p1

图1-段描述符的示意图 {2}

64位Windows下的任何应用,无论是32位还是64位,在载入到内存中时,都至少有两个代码段。这两个代码段的32位地址空间映射是完全重合的,但是段选择器是不同的:其中一个的选择器是0x23,L标记为0(32位代码段);另一个的选择器是0x33,L标记为1(64位代码段)。 在一个应用的PE(可移植可执行)标头中,“Machine”字段的标签是32位还是64位PE决定了这个应用的默认代码段。

当远分支指令把控制流从一个代码段上转移到另一个有着不同L标记的代码段上时,处理器模式交换就会被触发。例如,如果一个32位应用在64位Windows中执行,默认的CS选择器就会是0x23,处理器会是32位模式。如果,使用了远转移来跳转到一个选择器是0x33的代码段上,那么在执行完这条指令后,处理器模式就会切换到64位,在从32位向64位切换时也是一样的。

有很多远分支指令都可以用于触发模式转换,包括(1)远转移(2)远调用(3)远返回和(4)中断返回。这些指令就像是不同维度的平行世界之间的传送门,在需要时可以通过这些门穿梭于不同的世界。因为这种方便的可控模式交换,混淆技术才有了机会来使用交叉模式编码,这种方法既可以用在32位应用中,也可以用在64位应用上。

唯一需要注意的是64位应用,在64位模式中使用的内存地址需要限制在低于2G的内存空间中,否则,在切换到32位模式时,会出现内存地址冲突。

0x02 交叉模式混淆的概念


所有的IA指令都可以分为两类:兼容交叉模式的指令和不兼容交叉模式的指令。兼容交叉模式指的是指令的机器代码在32位和64位模式下都是有效的,并且含义也都是完全相同的,否则指令就会被视作不兼容交叉模式。

英特尔的软件开发人员手册中列出了每条指令及其可应用模式的详细信息。在这个指令表中,“64-bit Mode”字段和“Compat/Leg Mode”(32位)字段都显示是“有效的”(类似于图2中的“MOV r32, imm32”),这就是一条兼容指令。如果其中任何一个字段不是“有效的”,那么这就是一条不兼容指令。这两类指令都可以用于交叉模式混淆。

p2

图2-兼容与不兼容指令示例

2.1兼容指令

虽然兼容指令的执行在32位和64位模式下都是相同的,但是,考虑到两种模式的差异,得到的结果也有可能是不同的。图3中就是一个简单的例子。从图中可以看到,无论是在32位还是64位模式下,在运行时反汇编同一组指令会得到完全相同的结果。

p3

图3-兼容指令在不同模式下的执行示例

“call”只是简单地调用了下一条指令“mov ebx, esp”,并且这个代码块仅仅会检查栈指针在“call”指令的执行前后,修改了多少字节。由于栈框架大小的区别,在32位模式下,eax中的结果是4;在64位模式下,eax中的结果是8。这就证明了,同一组指令的结果要取决于模式,也就是说可以利用混淆数据或控制流来对抗某种单一模式的分析工具。

2.2不兼容指令

利用不兼容指令进行混淆更加直接,因为,这类指令只在一种模式下是有效的。一条64位的不兼容指令要么会被视作有效指令,要么就在32位模式下解释成完全不同的指令,反之亦然(如图4),不支持处理动态模式转换的分析工具在这时就会遇到问题。

p4

图4-不兼容指令在不同模式下的执行示例

0x03 案例研究交叉模式混淆


一个很简单的例子就能证明交叉模式混淆的有效性。图5是交叉模式代码的示意图,这些代码首先会作为32位原生指令来执行。然后,通过使用远转移,跳转到CS 0x33,把处理器转换成64位模式,并继续执行64位原生指令(在32位模式中不兼容)。64位代码在执行后会解码另一组32位原生指令,在通过远返回把处理器切换回32位模式后,接着这些32位指令就会运行。最终,这一部分的32位代码会解密出秘钥。

p5

图5-使用了交叉模式编码的示例程序示意图

图6中是这个交叉模式代码在命令行中执行的结果。在十六进制中打印出的代码段选择器证实了,处理器确实从32位转换到了64位,又从64位转换成了32位。

p6

图6-交叉模式代码示例在命令行中的执行

3.1静态分析工具

当使用32位的IDA Pro来分析上面的交叉模式代码时,很明显,在通过远转移进行模式交换后,IDA仍然会把64位的原生代码反汇编成32位的指令,导致反汇编结果与实际的64位指令出现偏差。(图7)

p7

图7-对比IDA Pro的运行时反汇编与静态反汇编

如果使用64位版本的IDA Pro,IDA仍然会在32位模式下分析这一交叉模式代码,因为IDA会自动把这些代码识别成一个32位应用。此时,再通过远转移来转换模式,反汇编就会出错,并把控制流带到错误的地址(图8)。

p8

图8-64位IDA Pro的静态反汇编显示了错误的远转移地址

除了广泛使用的IDA,我们还使用了另一个静态分析工具-Radare2来测试我们的交叉模式代码。Radare2是一个开源的命令行框架,支持多个平台上的逆向工程。类似于IDA,Radare2在32位模式下无法正确地反汇编64位原生代码,而在64位下,无法正确地除了32位原生代码。

3.2动态分析工具

在运行时代码分析中,动态二进制插桩(DBI)是一种广泛应用的技术。在这次研究中,我们选择了两个常用的二进制翻译工具- DynamoRio 和 Pin来运行测试程序。

这两个工具都无法处理从32位到64位的运行时模式转换,并且在通过远转移进行模式转换时会崩溃。图9显示,DynamoRio 和 Pin都无法在64位模式下,打印出代码段选择器的值。

p9

(a)

p10

(b)

p11

(c)

图9-(a)命令行(b)DynamoRio(c)Pin执行交叉模式代码的情况

由于需要切换到32位模式,DynamoRio在执行64位应用时也遇到了类似的崩溃问题,而Pin直接停止运行,显示错误信息“不支持通过远返回来转换不同的代码段”。

有趣的是,在仅仅使用兼容指令时,有时候DBI工具可能会在运行了用于切换模式的远转移后,继续运行,但是,实际上模式并没有切换。图10中显示,DynamoRio使用兼容指令运行了代码,与2.1中讨论的例子一样。可以看到,程序运行到了模式交换点之后,但是,没能真正的转换到代码段 0x33,而是停留在了CS 0x23。eax中的返回值是4而不是8,这与直接执行时的结果不同,从而为DBI环境监测创造了机会。

p12

(a)

p13

(b)

图9-(a)命令行(b)DynamoRio执行兼容交叉模式的代码

3.3调试工具

我们还发现,在调试包含运行时模式切换的代码时,交叉模式代码还会给OllyDbg和WinDbg这样的常用调试工具造成问题。32位的OllyDbg(目前64位不可用)在从32位切换到6位模式后,无法继续调试。而64位的WinDbg可以正确的运行交叉模式代码,但是在交叉模式条件下,修改过的代码会出现单步执行问题。

0x04 总结


利用64位Windows系统向下兼容32位程序的优势,交叉模式编码可以作为一种非常有效的混淆技术。目前,大多数静态分析工具、DBI工具和调试工具都是基于单一的处理器模式,这些工具在处理包含运行时模式交换的代码时就会遇到问题。而且,只要在设计时稍加注意,交叉模式代码还可以用于检测DBI环境。

从理论上说,只要让这些分析工具能适应动态模式交换,那么交叉模式问题就会迎刃而解,只不过需要大量的工程努力。

0x05 致谢


感谢李晓宁,亚欧对此次研究和审阅做出的贡献。

0x06 参考