McSema预览 - Trail of Bits博客
Artem Dinaburg
2014年6月23日
编译器, 会议, mcsema
6月28日,Artem Dinaburg和Andrew Ruef将在REcon 2014会议上介绍名为McSema的项目。McSema是一个将x86二进制文件转换为LLVM位码的框架。这种转换与编译器内部发生的过程相反:编译器将LLVM位码转换为x86机器码,而McSema则将x86机器码转换为LLVM位码。
为什么要做这么疯狂的事情?因为我们想要分析现有的二进制应用程序,而对LLVM位码进行推理比x86指令容易得多。不仅推理更容易,而且操作和将位码重新定位到不同架构也更为简单。现在,许多为LLVM位码编写的程序分析工具(如KLEE、PAGAI、LLBMC)可以用于现有应用程序。此外,在保持原始应用程序功能的同时,以复杂方式转换应用程序变得简单得多。
McSema将LLVM程序分析和操作工具的世界带到了二进制可执行文件中。虽然还有其他x86到LLVM位码的转换器,但McSema具有几个优势:
- McSema将控制流恢复与翻译分离,允许使用自定义控制流恢复前端
- McSema支持FPU指令
- McSema是开源的,采用宽松许可证
- McSema有文档记录,能够工作,并将在REcon演讲后不久提供
这篇博客文章将是McSema的预览,并将探讨将一个使用浮点运算的简单函数从x86指令转换为LLVM位码的挑战。我们将转换的函数称为timespi
,它接受一个参数k,并返回k * PI的值。timespi
的源代码如下:
|
|
当使用Microsoft Visual Studio 2010编译时,汇编代码看起来像下面的IDA Pro截图。
使用McSema转换为LLVM位码,然后将位码重新发射为x86二进制文件后,汇编代码看起来大不相同。
新的代码明显更大。下面我们解释原因。
你可能会想:“哇,这么小的函数代码膨胀这么多?这些人在做什么?”
我们特别想使用这个例子,因为它展示了浮点支持——这是McSema的独特功能,并且因为它展示了x86到LLVM位码转换固有的困难。
翻译背景
McSema将x86指令建模为对寄存器上下文的操作。也就是说,有一个寄存器上下文结构包含所有寄存器和标志,指令语义表示为结构成员的修改。这个概念通过简化的伪代码示例最容易理解。像ADD EAX, EBX
这样的操作将被翻译为context[EAX] += context[EBX]
。
翻译困难
现在让我们看看为什么像timespi这样的小函数会带来严重的翻译挑战。
- PI的值从数据段读取
- 控制流恢复必须检测第一个
FLD
指令引用数据并正确识别数据大小。McSema将控制流恢复与翻译分离,因此可以通过IDAPython脚本利用IDA优秀的CFG恢复功能 - 翻译需要支持x86 FPU寄存器、FPU标志和控制位
FPU寄存器不像整数寄存器。整数寄存器(EAX、ECX、EBX等)是命名且独立的。引用EAX
的指令将始终引用寄存器上下文中的相同位置。
FPU寄存器是8个数据寄存器(ST(0)到ST(7))的堆栈,由TOP
标志索引。引用ST(i)
的指令实际上引用寄存器上下文中的st_registers[(TOP + i) % 8]
。
整数寄存器仅由寄存器内容定义。FPU寄存器部分由寄存器内容定义,部分由FPU标记字定义。FPU标记字是一个位图,定义浮点寄存器的内容是:
- 有效(即正常浮点值)
- 零值
- 特殊值,如NaN或Infinity
- 空(寄存器未使用)
要确定FPU寄存器的值,必须同时查阅FPU标记字和寄存器内容。
翻译需要至少支持FLD
、FSTP
和FMUL
指令。
实际指令操作(如加载、存储和乘法)相对容易支持。困难的部分是实现FPU执行语义。
例如,FPU存储有关FPU指令的状态,如:
- 最后指令指针:最后执行的FPU指令的位置
- 最后数据指针:FPU指令的最新内存操作数的地址
- 操作码:最后执行的FPU指令的操作码
其中一些概念比其他概念更容易转换为LLVM位码。存储最后内存操作数的地址转换得很好:如果翻译的指令引用内存,则将内存地址存储在寄存器上下文的最后数据指针字段中。其他概念根本不转换。例如,当单个FPU指令转换为多个LLVM操作时,“最后指令指针"意味着什么?
自引用状态并不是翻译困难的终点。像精度控制和舍入控制标志这样的FPU标志会影响指令操作。精度控制标志影响算术操作,而不是存储寄存器的精度。因此,可以通过FLD
在ST(0)
和ST(1)
中加载双扩展精度值,但FMUL
可能在ST(0)
中存储单精度结果。
翻译步骤
既然我们已经探讨了翻译的困难,让我们看看仅翻译timespi核心——FMUL
指令所需的步骤。IA-32软件开发手册将此FMUL
实例定义为"将ST(0)乘以m64fp并将结果存储在ST(0)中”。以下是将FMUL转换为LLVM位码所需的一些步骤:
- 检查ST(0)的FPU标记字,确保其不为空
- 读取
TOP
标志 - 从
st_registers[TOP]
读取值。除非FPU标记字说值为零,否则读取零 - 加载
m64fp
指向的值 - 执行乘法
- 检查精度控制标志。根据需要调整结果的精度
- 将调整后的结果写入
st_registers[TOP]
- 更新ST(0)的FPU标记字以匹配结果。也许我们乘以零?
- 更新寄存器上下文中的FPU状态标志。对于
FMUL
,这只是C1
标志 - 更新最后的FPU操作码字段
- 我们的指令引用数据了吗?确实!将最后的FPU数据字段更新为
m64fp
- 跳过更新最后的FPU指令字段,因为它目前不真正映射到LLVM位码…
对于单个指令来说,这是很多工作,而且列表甚至还不完整。除了翻译原始指令的工作外,还必须对函数入口和出口点、外部调用以及地址被采用的函数采取额外步骤。这些额外细节将在REcon演讲中涵盖。
结论
翻译浮点操作是一项棘手且困难的工作。看似简单的浮点指令隐藏了许多操作,并转换为大量的LLVM位码。翻译后的代码很大,因为McSema暴露了浮点操作的隐藏复杂性。考虑到目前还没有尝试优化指令翻译,我们认为当前的输出相当不错。
要更详细地了解McSema,请参加Artem和Andrew在REcon的演讲,并继续关注Trail of Bits博客以获取更多公告。
编辑:McSema现已开源。请参阅我们的公告以获取更多信息。