深入解析微软控制流防护(CFG)技术

本文详细解析微软Control Flow Guard(CFG)技术的实现原理与应用实践,涵盖编译器集成、操作系统支持、有效目标验证机制,并通过具体示例展示其对间接调用和虚拟调用的保护能力,同时比较与Clang CFI的实现差异。

探讨控制流完整性:微软实现篇

我们如期带来控制流完整性系列的第二部分。本次将重点讨论微软的控制流完整性实现。需要重申的是,控制流完整性(CFI)是一种漏洞利用缓解技术,能防止程序缺陷转化为实际攻击。如需更详细说明,请阅读本系列首篇文章。

安全研究人员应当研究人们实际使用的产品,而微软在桌面计算市场占据绝对主导地位。Windows和Visual Studio中新的反攻击措施影响重大,这些措施直接关系大量用户的安全。

针对急于了解控制流防护的读者:只需在编译器和链接器标志中添加/guard:cf,并参考我们展示CFG功能与限制的示例。

微软的CFI实现

微软的CFI实现称为控制流防护(CFG),需要操作系统和编译器的双重支持。最低支持的操作系统为Windows 8.1 Update 3,最低编译器版本为Visual Studio 2015(推荐VS2015 Update 3)。本文所有示例均基于x86-64架构的Visual Studio 2015和Windows 10。

CFG拥有完善的文档支持——包括官方文档页面、编译器选项说明,甚至功能开发阶段的博客文章。CFG是CFI的直观实现:

  1. 编译器首先识别程序中所有间接分支
  2. 确定需要保护的分支(例如静态可识别目标的分支无需CFI检查)
  3. 在潜在脆弱分支处插入轻量级检查,确保分支目标为有效目的地

与前文相同,我们不深入探讨CFG的技术实现(已有大量优秀文献),而是聚焦如何在实际程序中使用CFG,并展示其保护范围与限制。但我们会提及CFG与Clang CFI实现的重要差异。

CFG与Clang CFI对比

本对比旨在展示不同实现如何将控制流完整性的理论概念转化为实际应用保护机制。两种实现并无优劣之分,它们针对不同的软件生态,都在现实约束(源码可用性、性能、易用性、API/ABI稳定性、向后兼容性等)下实现有效的软件保护。

保护范围

受微软CFG或Clang CFI保护的程序在间接控制流转移前执行轻量级检查,验证流目标是否属于预设的有效目标集合。

Windows程序存在大量无法被劫持的间接调用(例如通过IAT执行的API调用在程序加载后设为只读)。Visual Studio编译器会安全地省略这些调用的CFG检查。

Clang的CFI还包含非严格CFI相关的检查,如指针转换的运行时验证(详见前文)。

有效目标定义

控制流防护使用进程内统一的有效目标映射表,映射表中的所有内容均被视为有效目标(图1b)。CFG通过SetProcessValidCallTargets API支持运行时调整有效目标映射,这对处理JIT代码或手动加载动态库特别有用。

CFG还提供三个控制特定方法中CFG行为的编译器指令(定义于Windows SDK的ntdef.h但缺乏文档):

  • __declspec(guard(ignore)):禁用方法内所有间接调用的CFG检查,并忽略方法中引用的函数指针
  • __declspec(guard(nocf)):禁用方法内间接调用的CFG检查,但跟踪引用的函数指针并将其添加到有效目标映射
  • __declspec(guard(suppress)):防止导出函数成为有效CFG目标,用于禁止间接调用安全敏感函数(如SetProcessValidCallTargets即通过此方式保护)

Clang的CFI保护粒度更精细:每个间接控制流转移的目标必须匹配预期的类型签名(图1a)。根据启用选项,类成员函数调用还会验证是否在正确的类层次结构中。实际上,每个类型签名和类层次结构都有独立的有效目标映射,且目标集在编译时固定不可更改。

图1:cfg_icall示例中有效调用目标的差异。绿色为真实有效目标,红色为其他内容

  • (a) Clang CFI间接调用的有效目标:仅匹配预期函数签名的函数
  • (b) Visual Studio 2015 CFG的有效目标:所有合法函数入口点均包含其中

保护执行机制

控制流防护将执行职责分配给编译器和操作系统:编译器插入检查并提供初始有效目标集,操作系统维护目标集并验证目标。

Clang的CFI完全在编译器层面执行,操作系统不感知CFI。

动态库、JIT代码等边缘情况

控制流防护支持跨库调用,但仅当库也使用CFG编译时才执行保护。动态生成的代码页可添加至或排除出有效目标映射。通过GetProcAddress获取的外部函数始终是有效调用目标(使用__declspec(guard(suppress))保护的函数除外,这些函数必须通过导入表链接否则不可调用)。

Clang的CFI通过-fsanitize-cfi-cross-dso标志支持跨库调用(库和应用程序均需使用该标志编译)。据我们了解,动态生成代码不受CFI保护。使用-fsanitize-cfi-cross-dso时,通过dlsym获取的外部函数会自动添加为有效目标,否则会触发CFI违规。

在Visual Studio 2015中使用CFI

使用控制流防护极其简单:MSDN网站提供了通过GUI和命令行启用CFG的详细文档。快速总结:向编译器和链接器标志添加/guard:cf即可。

需要注意的是,如果计划通过SetProcessValidCallTargets动态调整有效间接调用目标:首先需要较新版本的Windows SDK(我们默认安装的VS2015版本缺少正确定义,需安装最新版本10.0.14393.0);其次必须将SDK目标设置为Windows 10(#define _WIN32_WINNT 0x0A00);最后须链接mincore.lib(包含必要的导入定义)。

控制流防护示例

我们创建了包含特殊设计缺陷的示例,展示CFG的使用方法及其防护的错误类型。这些示例中的缺陷无法被编译器静态识别,但会被CFG在运行时检测。我们尽可能模拟了CFG能阻止和不能阻止的潜在恶意行为。

这些CFG示例改编自Clang CFI示例,以展示两种实现中有效调用目标的不同定义。每个示例构建两个二进制文件(启用CFG的如cfg_icall.exe和未启用CFG的如no_cfg_icall.exe),基于相同源代码演示CFG特性和保护能力。

cfg_icall

本示例改编自Clang CFI博客文章的cfi_icall示例,稍作修改以适配Visual Studio 2015和CFG。示例二进制文件接受单个命令行参数(有效值0-3),每个值展示间接调用保护的不同方面:

  • 选项0:正常有效的间接调用(应在任何CFI方案下正常工作)
  • 选项1:无效间接调用(从数组边界外读取目标),但目标函数具有与有效调用相同的函数签名(在Clang CFI和CFG下均有效,但可能在未来某些方案中失败)
  • 选项2:无效间接调用,目标是有效函数入口但签名与调用者预期不同(在Clang CFI下失败但在CFG下有效)
  • 选项3:指向无效函数入口点的无效间接调用(应在任何CFI方案下失败,在Clang CFI和CFG下均失败)
  • 其他选项应指向未初始化内存,在两种CFI实现下均正确失败

cfg_vcall

cfg_vcall示例(源自前文的cfi_icall示例)显示当目标不是有效入口点时,虚拟调用受CFG保护。示例展示两个模拟缺陷:第一个是通过无效转换模拟类型混淆漏洞(在Clang CFI下失败但在CFG下成功);第二个模拟释放后使用或类似内存破坏,对象指针被攻击者创建的对象替换,函数指针指向函数中部(错误调用被Clang CFI和CFG共同阻止)。

图2:WinDbg中看到的控制流防护违规

cfg_valid_targets

此示例修改自cfg_icall,展示如何使用SetProcessValidCallTargets手动更新CFG位图,将bad_int_arg和float_arg从有效调用目标列表中移除。仅选项0能正常工作,其他选项均返回CFG错误。

cfg_guard_ignore

此示例展示如何使用__declspec(guard(ignore))编译器指令在指定方法内完全禁用CFG。

cfg_guard_nocf

此示例展示如何使用__declspec(guard(nocf))编译器指令在指定方法内禁用间接调用的CFG检查,但仍为引用的函数指针启用CFG,并对比__declspec(guard(nocf))__declspec(guard(ignore))的效果差异。

cfg_guard_suppress与cfg_suppressed_export

当库包含不应被间接调用的安全敏感方法时,__declspec(guard(suppress))指令可防止导出函数通过函数指针调用。这两个示例共同展示抑制导出的工作原理:cfg_suppressed_export是具有抑制导出和正常导出的DLL;cfg_guard_suppress尝试通过GetProcAddress获取的指针调用两个导出。

所有流必须终止

既然您已了解控制流防护及其应用保护机制,请立即为您的软件启用CFG!只需向编译器和链接器标志添加/guard:cf即可。要查看CFG保护软件的实际示例,请参考我们的CFG示例展示。期待微软在未来的Visual Studio版本中持续改进CFG。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计