深入解析McSema:将x86二进制转换为LLVM位码的技术挑战

本文详细介绍了McSema框架如何将x86二进制代码转换为LLVM位码,重点分析了浮点运算指令的翻译难点,包括FPU寄存器管理、状态标志处理和指令语义转换等核心技术实现。

McSema预览:x86到LLVM位码转换的技术挑战

2014年6月23日,Artem Dinaburg在Trail of Bits博客上介绍了即将在REcon 2014会议上展示的McSema项目。McSema是一个将x86二进制文件转换为LLVM位码的框架,这一过程与编译器的工作方向相反——编译器将LLVM位码转换为x86机器码,而McSema则执行逆向转换。

为什么需要这样的转换?

主要原因是分析现有二进制应用程序时,基于LLVM位码进行推理比直接处理x86指令更加简单。LLVM位码不仅易于推理,还便于操作和重定向到不同架构。许多程序分析工具(如KLEE、PAGAI、LLBMC)都是为LLVM位码设计的,现在这些工具可以应用于现有应用程序。此外,在保持原始应用功能的同时,以复杂方式转换应用也变得更加简单。

McSema将LLVM程序分析和操作工具引入二进制可执行文件领域。虽然存在其他x86到LLVM位码的转换器,但McSema具有以下优势:

  • 将控制流恢复与翻译分离,允许使用自定义控制流恢复前端
  • 支持FPU指令
  • 开源且采用宽松许可证
  • 有详细文档,实际可用,并在REcon演讲后即将发布

翻译示例:timespi函数

本文以简单的timespi函数为例,展示将使用浮点运算的x86指令转换为LLVM位码的挑战。timespi函数接受一个参数k,返回k * PI的值。其源代码为:

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

使用Microsoft Visual Studio 2010编译后,汇编代码如IDA Pro截图所示。经过McSema转换并重新生成为x86二进制后,汇编代码变得复杂许多,代码量显著增加。

翻译背景

McSema将x86指令建模为对寄存器上下文的操作。即存在一个包含所有寄存器和标志的寄存器上下文结构,指令语义表示为对结构成员的修改。例如,ADD EAX, EBX指令会被转换为context[EAX] += context[EBX]。

翻译难点

timespi这样的小函数也面临严重的翻译挑战:

  1. PI值从数据段读取:控制流恢复必须检测到第一个FLD指令引用数据并正确识别数据大小。McSema通过IDAPython脚本利用IDA的优秀CFG恢复功能。

  2. 支持x86 FPU寄存器、标志和控制位:FPU寄存器不像整数寄存器那样独立命名。它们是8个数据寄存器(ST(0)到ST(7))的堆栈,通过TOP标志索引。指令引用ST(i)实际上指向寄存器上下文中的st_registers[(TOP + i) % 8]。

  3. FPU标签字的影响:FPU标签字是位图,定义浮点寄存器内容是有效值、零、特殊值(如NaN或Infinity)还是空。确定FPU寄存器的值需要同时参考标签字和寄存器内容。

  4. 支持FLD、FSTP和FMUL指令:实现加载、存储和乘法等操作相对简单,困难的是实现FPU执行语义。

FPU存储有关指令的状态信息,如最后指令指针、最后数据指针和最后操作码。其中一些概念难以转换为LLVM位码。例如,当单个FPU指令被转换为多个LLVM操作时,“最后指令指针"的含义变得模糊。

此外,精度控制和舍入控制等FPU标志会影响指令操作。精度控制标志影响算术操作,而不是存储寄存器的精度。

翻译步骤

以FMUL指令为例,IA-32软件开发手册将其定义为"将ST(0)乘以m64fp并将结果存储在ST(0)中”。将其转换为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位码

这仅是一个指令的部分工作,还不包括函数入口和出口点、外部调用以及地址被获取的函数所需的额外步骤。

结论

翻译浮点操作是一项复杂困难的任务。看似简单的浮点指令隐藏了大量操作,转换为大量LLVM位码。转换后的代码量大是因为McSema暴露了浮点操作的隐藏复杂性。考虑到尚未尝试优化指令翻译,当前的输出结果已经相当不错。

如需了解更多关于McSema的详细信息,请参加Artem和Andrew在REcon的演讲,并继续关注Trail of Bits博客的更多公告。

编辑:McSema现已开源,详见相关公告。


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

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