McSema深度解析:将x86二进制转换为LLVM比特码的革命性框架

本文深入解析McSema框架如何将x86机器码转换为LLVM比特码,重点探讨浮点运算指令的转换挑战,包括FPU寄存器建模、控制流恢复和指令语义转换等核心技术实现细节。

McSema预览 - Trail of Bits博客

Artem Dinaburg
2014年6月23日
compilers, conferences, 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具有多个优势:

  • 将控制流恢复与翻译分离,允许使用自定义控制流恢复前端
  • 支持FPU指令
  • 开源且采用宽松许可证
  • 经过完整文档记录,实际可用,并将在REcon演讲后很快发布

本篇博客将预览McSema,并探讨将一个使用浮点运算的简单函数从x86指令转换为LLVM比特码所面临的挑战。我们将转换的函数名为timespi,它接受一个参数k并返回k * PI的值。timespi的源代码如下:

1
2
3
4
long double timespi(long double k) {
    long double pi = 3.14159265358979323846;
    return k*pi;
}

使用Microsoft Visual Studio 2010编译后,其汇编代码如以下IDA Pro截图所示。

(此处原文章包含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]

(此处原文章包含Intel IA-32软件开发手册图8-2)

整数寄存器仅由寄存器内容定义。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比特码所需的部分步骤:

  1. 检查ST(0)的FPU标记字,确保其不为空
  2. 读取TOP标志
  3. 从st_registers[TOP]读取值。除非FPU标记字说值为零,否则只读零
  4. 加载m64fp指向的值
  5. 执行乘法
  6. 检查精度控制标志。根据需要调整结果的精度
  7. 将调整后的结果写入st_registers[TOP]
  8. 更新ST(0)的FPU标记字以匹配结果。可能我们乘以了零?
  9. 更新寄存器上下文中的FPU状态标志。对于FMUL,这只是C1标志
  10. 更新最后FPU操作码字段
  11. 我们的指令引用了数据吗?确实!将最后FPU数据字段更新为m64fp
  12. 跳过更新最后FPU指令字段,因为它目前无法映射到LLVM比特码…

对于单个指令来说,这是很多工作,而且列表还不完整。除了转换原始指令的工作外,还需要在函数入口和退出点、外部调用以及地址被采用的函数上采取额外步骤。这些额外细节将在REcon演讲中涵盖。

结论

转换浮点操作是一项棘手而困难的任务。看似简单的浮点指令隐藏了许多操作,并转换为大量的LLVM比特码。转换后的代码很大,因为McSema暴露了浮点操作的隐藏复杂性。考虑到尚未尝试优化指令翻译,我们认为当前的输出相当不错。

要更详细地了解McSema,请参加Artem和Andrew在REcon的演讲,并继续关注Trail of Bits博客以获取更多公告。

编辑:McSema现已开源。请参阅我们的公告以获取更多信息。

如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News


页面内容
转换背景
转换困难
PI值从数据段读取
转换需要支持x86 FPU寄存器、FPU标志和控制位
转换需要至少支持FLD、FSTP和FMUL指令
转换步骤
结论
近期文章
使用Deptective调查您的依赖关系
系好安全带,Buttercup,AIxCC的评分回合正在进行中!
使您的智能合约超越私钥风险
Go解析器中意想不到的安全隐患
我们审查首批DKLs23库从Silence Laboratories学到的经验
© 2025 Trail of Bits.
使用Hugo和Mainroad主题生成。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计