启用clang/llvm的cfi需组合-fsanitize=cfi-vcall、-fsanitize=cfi-icall、-fsanitize-cfi-cross-dso并强制配合-flto,否则虚函数和跨dso调用校验失效;影子栈还需cpu cet支持、内核≥5.19及-mshstk/-fcf-protection=full协同,缺一不可。

启用 Clang/LLVM 的 CFI 编译选项,必须匹配目标平台特性
Clang 的 -fsanitize=cfi 系列选项是目前最实用的前向边缘(Forward-Edge)CFI 实现,但默认不保护虚函数调用或函数指针——除非你显式启用更严格的子模式。只加 -fsanitize=cfi 会漏掉大量 C++ 多态场景。
实际编译时应组合使用:
-
-fsanitize=cfi -fsanitize-cfi-cross-dso:跨动态库的间接调用也校验(否则 libc.so 中的函数指针调用会被绕过) -
-fsanitize=cfi-icall:强制校验所有间接调用((*fn_ptr)()、std::function::operator()) -
-fsanitize=cfi-vcall:针对 C++ 虚函数调用插入类型签名检查(依赖-fvisibility=hidden和完整的 LTO 链接) - 必须配合
-flto(Link Time Optimization),否则虚函数表校验无法跨编译单元生效
漏掉 -flto 是常见错误:编译器在单个 .cpp 文件里能生成 vcall 检查代码,但链接时若未合并符号定义,运行时校验会因类型信息缺失而静默失败。
Intel CET 影子栈需硬件+内核+编译器三者协同,缺一不可
影子栈(Shadow Stack)是防御 ROP 的关键,但它不是“开个编译开关就能用”的功能。x86-64 下必须同时满足:
- CPU 支持 CET(Intel 11 代 Tiger Lake 及更新,或部分 Xeon Scalable)且 BIOS 中开启
Control-flow Enforcement Technology - Linux 内核 ≥ 5.19 并启用
CONFIG_X86_INTEL_CET=y,启动参数含cet-report=1(否则用户态无法访问影子栈指令) - Clang ≥ 14 且编译时加
-mshstk(启用setssbsy/rdsspd等指令)和-fcf-protection=full
典型错误现象:Illegal instruction (core dumped)——说明 CPU 或内核没准备好,但编译通过了。此时 cat /proc/cpuinfo | grep cet 应返回非空;dmesg | grep cet 应显示初始化成功。只靠编译器加 flag 不会自动降级,它会直接生成非法指令。
CFI + 影子栈不是叠加关系,而是分工覆盖不同攻击面
CFI(如 -fsanitize=cfi-vcall)管的是“调用去哪”,影子栈(-fcf-protection=full)管的是“返回去哪”。两者不能互相替代,但可以共存。一个典型误判是:以为开了 CFI 就不用影子栈了。
- CFI 对
ret指令完全无感——它不拦截返回,只拦call和jmp - 影子栈对虚函数调用、函数指针调用完全无感——它只保护每次
ret弹出的地址是否来自影子栈副本 - ROP 攻击链中,攻击者先用栈溢出覆盖返回地址(影子栈防这个),再用 gadget 里的
call rax跳转到 libc 函数(CFI 防这个)
所以真实加固必须双开:-fsanitize=cfi-vcall -fsanitize=cfi-icall -flto 用于前向,-fcf-protection=full -mshstk 用于后向。中间若缺任何一环,ROP 链仍可能完成拼接。
Windows 上 CFG 的局限性与绕过风险要心里有数
MSVC 的 Control Flow Guard(CFG)本质是静态间接调用白名单,靠编译器扫描所有 call [rax] 类指令并生成跳转目标表。但它有硬伤:
- 不校验虚函数调用——因为 vtable 地址在运行时才确定,MSVC 默认不把它纳入 CFG 表
- 不防护
SetThreadContext、SEH 链指针等 Windows 特有后向劫持入口 - 若程序加载了未启用 CFG 的第三方 DLL(如旧版 OpenSSL),其内部函数指针调用可被用作 gadget 跳板
也就是说,Windows CFG 单独启用时,对现代 ROP 利用链的阻断效果有限。它更适合配合 EMET 或 Windows Defender Exploit Guard 的“CFG 启用强制策略”一起用,而非当作独立防线。
真正难的不是加几个 flag,而是确认整个工具链、内核、CPU 微码都对齐了同一套控制流语义。一个环节版本错配,防护就变成幻觉。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











