Let’s talk about CFI: clang edition
什么是CFI?
控制流完整性(CFI)是一种类似于栈保护(DEP)、数据执行保护(ASLR)的漏洞缓解技术。CFI的目标是防止程序漏洞被转化为可利用的安全问题。当程序存在缓冲区溢出、类型混淆或整数溢出等漏洞时,攻击者可能改变程序执行流程。CFI通过在运行时强制执行编译器在编译时确定的控制流图(CFG)来阻止这类攻击。
从图论角度理解,程序的控制流可以表示为有向图(CFG),其中节点是基本块,边是可能的控制流转移。CFI确保运行时遵循编译时确定的CFG。
Clang中的CFI实现
Clang从3.7版本开始在主分支支持CFI,作为消毒剂套件的一部分。要启用CFI需要:
- 使用支持链接时优化(LTO)的链接器(如GNU gold或MacOS ld)
- 在编译和链接标志中添加
-flto
- 添加
-fvisibility=hidden
和-fsanitize=cfi
编译标志
完整构建命令示例:
# 调试版本
clang-3.9 -fvisibility=hidden -flto -fno-sanitize-trap=all -fsanitize=cfi -o output input
# 发布版本
clang-3.9 -fvisibility=hidden -flto -fsanitize=cfi -o output input
CFI选项详解
1. -fsanitize=cfi-icall
保护间接函数调用:
- 验证调用目标是否为有效函数入口
- 验证目标函数签名与编译时一致
示例攻击场景:
// 攻击者将write()调用改为system()
typedef int (*func_ptr)(int);
func_ptr fp = (func_ptr)system;
fp(123); // CFI会阻止此非法调用
限制:
- 不保护跨共享库的调用
- 需所有编译单元启用该选项
- 仅支持x86/x86_64架构
2. -fsanitize=cfi-vcall
保护虚函数调用:
- 验证虚函数调用目标在类继承体系中
示例攻击场景:
class Base { virtual void print(); };
class Evil { void makeAdmin(); };
Base* obj = new Evil();
obj->print(); // CFI会检测到类型混淆
3. -fsanitize=cfi-nvcall
保护非虚成员函数调用:
- 验证调用对象运行时类型与编译时一致
示例攻击场景:
class Admin { void doAdminWork(); };
class User {};
User* user = new User();
((Admin*)user)->doAdminWork(); // CFI阻止权限提升
4. -fsanitize=cfi-unrelated-cast
防止不相关类型转换:
- 验证类型转换在相同类继承体系中
- 验证void*转换回原始类型
5. -fsanitize=cfi-derived-cast
防止非法基类到派生类转换:
class Base {};
class Derived { int secret; };
Base* base = new Base();
Derived* derived = (Derived*)base; // CFI阻止内存泄露
6. -fsanitize=cfi-cast-strict
加强版派生类转换保护,针对特定边缘情况:
- 单继承
- 未引入新虚函数
- 仅重写隐式虚析构函数
结论
CFI是重要的漏洞缓解技术,能有效防止控制流劫持攻击。Clang提供了完整的CFI实现,通过7种不同的保护选项覆盖了各种攻击场景。虽然存在一些限制(如需要全程序LTO、特定架构支持等),但在支持的环境中使用CFI能显著提高软件安全性。
实际测试表明,CFI能有效阻止示例中的所有攻击场景,包括:
- 间接调用劫持
- 虚函数表污染
- 类型混淆攻击
- 非法类型转换
建议所有安全关键项目启用CFI保护,只需添加简单的编译标志即可获得强大的运行时保护。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码