二进制安全优化实践
“这样存在毫无意义,但安全地存在才有价值” (麦克白 3:1)
编译器不仅要生成高效代码,还必须生成安全代码。尽管编译器开发过程中进行了大量测试和正确性认证,但其优化过程仍可能无意间引入信息泄露或删除程序员编写的安全关键操作。图1展示了CWE-733漏洞的典型案例:编译器优化移除了对加密密钥变量的清零操作(死存储消除优化),导致密钥可能被攻击者恢复。
构建强大的代码复用攻击
攻击者使用ROP/JOP等技术复用受害程序中的代码片段(gadgets)构建攻击载荷。每个gadget由有用指令序列和间接分支指令(ret/jmp/call)组成,通过链式调用实现攻击逻辑。由于gadgets来自合法程序,防代码注入的防御机制对其无效。而gadgets的数量和实用性直接取决于编译器的代码生成和优化行为。
研究发现总结
通过分析GCC/clang编译的1000多个程序变体,我们发现:
- 85%的优化会增大gadget集合规模或提升其可用性
- 两个最具影响力的编译器行为:
- 间接分支指令复制:如GCC的omit-frame-pointer优化会复制ret指令
- 二进制布局变化:指令位移变化可能意外产生新的gadget(如图4所示)
解决方案:二进制安全优化
我们在Egalito二进制重编译器中实现了五种优化方案:
- 返回指令合并:合并函数内所有ret到单个实例
- 间接跳转合并:合并相同目标的间接跳转
- 指令屏障扩展:消除跨指令的特殊gadget
- 位移填充:消除跳转位移中的gadget
- 函数重排序:消除调用偏移中的gadget
测试结果显示:
- 平均消除31.8%的有用gadgets
- 78%的变体gadget效用降低
- 对执行速度无影响
- 平均仅增加6.1KB代码大小
结论
编译器优化行为会显著影响二进制程序的攻击面。通过二进制重编译技术,我们可以在不牺牲性能的前提下消除这些安全隐患。未来工作将关注寄存器分配等问题,并探索这些优化对控制流完整性(CFI)等防御机制的辅助作用。