CFG是Windows控制流防护机制,通过校验间接调用目标是否为编译器标记的合法函数入口来防御劫持攻击;它能拦截非法函数指针、虚函数等间接调用,但无法阻止ret指令驱动的ROP链,因不校验ret目标地址。

什么是 CFG,它真能拦住 ROP 吗
CFG 不是“拦截 ROP”,而是让大多数未经许可的间接调用(比如 vtable 调用、std::function 执行、函数指针跳转)在运行时被系统拒绝。ROP 攻击依赖构造 gadget 链并跳转到非预期函数入口(尤其是 DLL 中未导出的中间地址),而 CFG 强制所有间接调用目标必须是编译器标记为“合法目标”的函数开头——即满足 __guard_icall_target 检查。但它不验证栈上 gadget 序列,也不防 JOP 或 COOP;若攻击者能找到大量已标记为 CFG 目标的函数(如 memcpy、VirtualProtect、pop/pop/ret 等),仍可能绕过。
Visual Studio 中启用 CFG 的关键步骤
- 在项目属性中启用:Configuration Properties → General → Control Flow Guard → Yes (/guard:cf)
- 必须同时开启 Linker → Advanced → Control Flow Guard → Yes (/guard:cf),否则链接器不会注入 CFG 表和检查桩
- 编译器会自动为每个函数入口插入
__guard_icall_target 校验逻辑,但仅对间接调用生效(call eax、call [rbx] 等),直接调用(call func)完全绕过 CFG
- 若使用 /LTCG(链接时代码生成),需确保整个模块(含静态库)都启用 /guard:cf,否则混合编译会导致部分函数未注册进 CFG 表,造成误报或漏检
哪些间接调用会被 CFG 拦住?哪些不会
- 会被拦截的典型场景:
-
void (*fp)() = get_func_ptr(); fp();(函数指针调用)
-
obj->vfunc();(虚函数调用,且 vtable 在堆/栈上被篡改)
-
std::function f = ...; f(); (内部通过函数指针间接跳转)
- 不会被拦截的常见情况:
- 直接 call 指令(如
call printf)——CFG 完全不介入
- jmp 指令(
jmp eax)——Windows CFG 只检查 call,不检查 jmp
- 返回指令(ret)本身——CFG 不校验 ret 目标,这是 ROP 能存在的根本原因
- 手动内联汇编跳转(
_asm { call eax })——若未经过 MSVC 编译器生成的间接调用路径,可能逃逸
启用后必须验证 CFG 表是否真正生效
- 运行时验证:用
dumpbin /headers your.exe | findstr "guard" 查看输出是否含 guaranteed to be guarded 和 Guard CF Table 字样
- 检查 PE 可选头:
DataDirectory[IMAGE_DIRECTORY_ENTRY_GUARD_CF] 必须非零,否则系统加载时不启用 CFG
- 常见失效原因:
- 混用了未开启 /guard:cf 的第三方 .lib(尤其是旧版 OpenSSL、zlib)
- 使用了 /SAFESEH:NO 或 /DYNAMICBASE:NO —— CFG 要求 ASLR 开启(/DYNAMICBASE)才能可靠工作
- Release 模式下启用了 /O2 但未开 /guard:cf,而 Debug 模式开了——实际部署的是 Release 版本
__guard_icall_target 校验逻辑,但仅对间接调用生效(call eax、call [rbx] 等),直接调用(call func)完全绕过 CFG - 会被拦截的典型场景:
-
void (*fp)() = get_func_ptr(); fp();(函数指针调用) -
obj->vfunc();(虚函数调用,且 vtable 在堆/栈上被篡改) -
std::function(内部通过函数指针间接跳转)f = ...; f();
-
- 不会被拦截的常见情况:
- 直接 call 指令(如
call printf)——CFG 完全不介入 - jmp 指令(
jmp eax)——Windows CFG 只检查 call,不检查 jmp - 返回指令(ret)本身——CFG 不校验 ret 目标,这是 ROP 能存在的根本原因
- 手动内联汇编跳转(
_asm { call eax })——若未经过 MSVC 编译器生成的间接调用路径,可能逃逸
- 直接 call 指令(如
启用后必须验证 CFG 表是否真正生效
- 运行时验证:用
dumpbin /headers your.exe | findstr "guard" 查看输出是否含 guaranteed to be guarded 和 Guard CF Table 字样
- 检查 PE 可选头:
DataDirectory[IMAGE_DIRECTORY_ENTRY_GUARD_CF] 必须非零,否则系统加载时不启用 CFG
- 常见失效原因:
- 混用了未开启 /guard:cf 的第三方 .lib(尤其是旧版 OpenSSL、zlib)
- 使用了 /SAFESEH:NO 或 /DYNAMICBASE:NO —— CFG 要求 ASLR 开启(/DYNAMICBASE)才能可靠工作
- Release 模式下启用了 /O2 但未开 /guard:cf,而 Debug 模式开了——实际部署的是 Release 版本
dumpbin /headers your.exe | findstr "guard" 查看输出是否含 guaranteed to be guarded 和 Guard CF Table 字样 DataDirectory[IMAGE_DIRECTORY_ENTRY_GUARD_CF] 必须非零,否则系统加载时不启用 CFG - 混用了未开启 /guard:cf 的第三方 .lib(尤其是旧版 OpenSSL、zlib)
- 使用了 /SAFESEH:NO 或 /DYNAMICBASE:NO —— CFG 要求 ASLR 开启(/DYNAMICBASE)才能可靠工作
- Release 模式下启用了 /O2 但未开 /guard:cf,而 Debug 模式开了——实际部署的是 Release 版本
CFG 生效的前提是整个调用链都在编译器可控范围内。一旦涉及手写汇编、动态生成代码(JIT)、或第三方闭源模块,CFG 就可能变成一层薄纱。别把它当成 ROP 终结者,而是当作让攻击者多花几倍时间找 gadget 的门槛。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











