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的值。其源代码为:
|
|
使用Microsoft Visual Studio 2010编译后,汇编代码如IDA Pro截图所示。经过McSema转换并重新生成为x86二进制后,汇编代码变得复杂许多,代码量显著增加。
翻译背景
McSema将x86指令建模为对寄存器上下文的操作。即存在一个包含所有寄存器和标志的寄存器上下文结构,指令语义表示为对结构成员的修改。例如,ADD EAX, EBX指令会被转换为context[EAX] += context[EBX]。
翻译难点
timespi这样的小函数也面临严重的翻译挑战:
-
PI值从数据段读取:控制流恢复必须检测到第一个FLD指令引用数据并正确识别数据大小。McSema通过IDAPython脚本利用IDA的优秀CFG恢复功能。
-
支持x86 FPU寄存器、标志和控制位:FPU寄存器不像整数寄存器那样独立命名。它们是8个数据寄存器(ST(0)到ST(7))的堆栈,通过TOP标志索引。指令引用ST(i)实际上指向寄存器上下文中的st_registers[(TOP + i) % 8]。
-
FPU标签字的影响:FPU标签字是位图,定义浮点寄存器内容是有效值、零、特殊值(如NaN或Infinity)还是空。确定FPU寄存器的值需要同时参考标签字和寄存器内容。
-
支持FLD、FSTP和FMUL指令:实现加载、存储和乘法等操作相对简单,困难的是实现FPU执行语义。
FPU存储有关指令的状态信息,如最后指令指针、最后数据指针和最后操作码。其中一些概念难以转换为LLVM位码。例如,当单个FPU指令被转换为多个LLVM操作时,“最后指令指针"的含义变得模糊。
此外,精度控制和舍入控制等FPU标志会影响指令操作。精度控制标志影响算术操作,而不是存储寄存器的精度。
翻译步骤
以FMUL指令为例,IA-32软件开发手册将其定义为"将ST(0)乘以m64fp并将结果存储在ST(0)中”。将其转换为LLVM位码需要以下步骤:
- 检查ST(0)的FPU标签字,确保不为空
- 读取TOP标志
- 从st_registers[TOP]读取值(如果FPU标签字指示值为零,则直接读取零)
- 加载m64fp指向的值
- 执行乘法
- 检查精度控制标志,根据需要调整结果精度
- 将调整后的结果写入st_registers[TOP]
- 更新ST(0)的FPU标签字以匹配结果
- 更新寄存器上下文中的FPU状态标志(对于FMUL,主要是C1标志)
- 更新最后FPU操作码字段
- 如果指令引用数据,更新最后FPU数据字段为m64fp
- 暂时跳过更新最后FPU指令字段,因为它不直接映射到LLVM位码
这仅是一个指令的部分工作,还不包括函数入口和出口点、外部调用以及地址被获取的函数所需的额外步骤。
结论
翻译浮点操作是一项复杂困难的任务。看似简单的浮点指令隐藏了大量操作,转换为大量LLVM位码。转换后的代码量大是因为McSema暴露了浮点操作的隐藏复杂性。考虑到尚未尝试优化指令翻译,当前的输出结果已经相当不错。
如需了解更多关于McSema的详细信息,请参加Artem和Andrew在REcon的演讲,并继续关注Trail of Bits博客的更多公告。
编辑:McSema现已开源,详见相关公告。
如果您喜欢这篇文章,请分享到:Twitter、LinkedIn、GitHub、Mastodon、Hacker News