本文提供有关 PE 映像中控制流防护(CFG)元数据的其他详细信息。 假定你熟悉 PE 映像中 CFG 元数据的结构。 有关 PE 映像中 CFG 元数据的高级文档,请参阅 PE 格式 主题。
GuardCFFunctionTable 附加到加载配置目录的函数中列出了有效的间接调用目标的函数,有时称为 GFIDS 表,以便简洁。 这是相对虚拟地址(RVA)的排序列表,其中包含有关有效 CFG 调用目标的信息。 通常,这些地址采用函数符号。 希望 CFG 强制实施的图像必须枚举其 GFIDS 表中获取的所有地址。 GFIDS 表中的 RVA 列表必须正确排序,否则不会加载映像。 GFIDS 表是 4 个 + n 字节的数组,其中 n 由 (GuardFlags & IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK) >> IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_SHIFT 提供)。 “GuardFlags”是加载配置目录的 GuardFlags 字段。 这样,将来就可以将额外的元数据附加到 CFG 调用目标。 当前唯一定义的元数据是可选的 1 字节额外标志字段(“GFIDS 标志”),附加到每个 GFIDS 条目(如果有任何调用目标都有元数据)。 定义了两个 GFIDS 标志:
IMAGE_GUARD_FLAG_FID_SUPPRESSED/0x1 明确禁止调用目标(不将其视为 CFG 有效) IMAGE_GUARD_FLAG_EXPORT_SUPPRESSED/0x2 取消调用目标。 有关更多详细信息,请参阅 导出抑制 为了获得将来的兼容性,工具不应设置 尚未定义的 GFIDS 标志,并且不应包括除当前定义的 1 字节之外的其他 GFIDS 额外的元数据字节,因为尚未分配其他标志或其他元数据的含义。 可以通过转储 GFIDS 二进制文件(如新式 Windows 10 OS 版本上的 Ntdll.dll)来查找包含额外元数据字节的图像示例。
工具应仅将函数符号声明为有效的调用目标,这对于可能采用标签的汇编程序代码而言,这值得额外考虑。 出于历史原因,汇编程序代码可能依赖于 PROC 或 .altentry 以外的代码标签,因为链接器未转换为 CFG 调用目标。
此外,出于历史原因,代码可能故意将代码声明为数据,以避免包含在 GFIDS 表中。 例如,一个对象文件可能实现符号作为代码,而另一个对象文件可能将其声明为数据,以便获取符号的地址而不生成有效的 CFG 目标记录。 为了兼容,建议工具集支持这种做法。
支持 CFG 且想要或执行 CFG 检查的映像应设置IMAGE_GUARD_CF_INSTRUMENTED和IMAGE_GUARD_CF_FUNCTION_TABLE_PRESENT GuardFlags 位,并应在映像标头中设置 IMAGE_DLLCHARACTERISTICS_GUARD_CF DllCharacteristics 位。
加载配置目录播发两个函数指针:GuardCFCheckFunctionPointer 和 GuardCFDispatchFunctionPointer(后者仅支持某些体系结构(如 AMD64)。 这些函数指针应指向只读内存,以便 CFG 安全性有效;作系统的 DLL 加载程序将在图像加载期间暂时重新保护内存,以存储函数指针。 典型的用法可能是将这些内容合并到包含导入地址表(IAT)的同一部分。 GuardCFCheckFunctionPointer 提供了 OS 加载程序提供的符号的地址,该符号可以使用第一个整数参数寄存器(x86 上的 ECX)中的函数指针调用,如果调用目标不是有效的 CFG 目标,它将返回成功或中止进程。 GuardCFDispatchFunctionPointer 提供了 OS 加载程序提供的符号的地址,该符号在寄存器 RAX 中采用调用目标,并执行对调用目标的合并 CFG 检查和尾分支优化调用(将保留 R10/R11 供 GuardCFDispatchFunctionPointer 使用)和整数参数寄存器保留供最终调用目标使用。 图像中 CFG 符号的默认地址应指向仅返回(GuardCFCheckFunctionPointer)或返回受保护的符号(或最好完全省略执行“jmp rax”指令的 GFIDS 表符号)的函数。 对于 AMD64 GuardCFDispatchFunctionPointer,当映像加载到 CFG 感知作系统上并且启用了 CFG 时,OS DLL 加载程序将安装适当的函数指针,这将实现向后兼容性。 如果映像不打算使用 CFG 调度设施,则可以在负载配置中为 GuardCFDispatchFunctionPointer 提供 0。 这应该为非 AMD64 体系结构实现将来的兼容性,以防这些体系结构最终支持某种形式的 CFG 调度机制。 请注意,Windows 8.1 AMD64 不支持 CFG 调度,并将保留 GuardCFDispatchFunctionPointer 的默认函数指针。 CFG 调度仅在 Windows 10 及更高版本的作系统上受支持。
只能对标记为地址空间布局随机化(ASLR)兼容的图像(由 /DYNAMICBASE 选项与Microsoft链接器指定的)强制实施用户模式 CFG。 这是因为 OS 在内部如何处理 CFG,其中基本上连接到 ASLR 基础结构。 通常,CFG 的用户应为其映像启用 ASLR 作为第一步。 工具不应假定 OS 将始终忽略不设置 ASLR 的 CFG,但通常应同时设置两者。
编译器指令
调用目标可以使用 __declspec(guard(suppress)修饰符或 /guardsym:symname,S 链接器指令(例如 asm 代码)显式禁止调用目标。 这会导致调用目标包含在 GFIDS 表中,但以这样方式标记了 OS 将调用目标视为无效。 某些非生产方案(例如在某些较旧的作系统上启用某些应用程序验证程序检测)可能会使禁止调用目标被视为有效,但一般情况下,这些方案不应是生产方案。 此指令可用于批注不应被视为有效调用目标的“危险”函数,即使正常的 CFG 规则将包含它们。
代码可以使用 __declspec(guard(nocf)修饰符指示不需要 CFG 检查。 这指示编译器不插入整个函数的任何 CFG 检查。 编译器应注意将此指令传播到内联函数提供的任何代码,该内联函数标记为不需要 CFG 检查。 此方法通常仅在程序员手动插入“CFG 等效”保护的特定情况下使用。 程序员知道,他们正在通过一些只读函数表调用,该表的地址是通过只读内存引用获取的,并且索引被屏蔽为函数表限制。 此方法还可能应用于未内联的小型包装函数,并且只执行通过函数指针进行调用。 由于此指令的用法不正确可能会损害 CFG 的安全性,因此程序员必须使用该指令非常小心。 通常,此用法仅限于仅调用一个函数的非常小的函数。
导入处理
通过 IAT 的调用不应使用 CFG 保护。 IAT 在新式图像中是只读的(假设 IAT 在 PE 标头中声明,在这种情况下它必须位于自己的页面上)。 IAT 可用于访问被禁止的函数,因此这是一个正确性要求。 通过 IAT 取代 CFG 的内存保护,因为解析映像导入快照后调用目标绑定是不可变的,绑定分辨率会细化。
受保护的延迟负载:通过延迟加载 IAT 的调用不应使用 CFG 保护,原因与标准 IAT 相同。 延迟加载 IAT 应位于其自己的部分中,映像应设置 IMAGE_GUARD_CF_PROTECT_DELAYLOAD_IAT GuardFlags 位。 这表示,如果使用作系统的延迟负载支持本机到 Windows 8 及更高版本的作系统,作系统的 DLL 加载程序应在导出解析期间更改延迟加载 IAT 的保护。 如果本机作系统延迟加载支持正在使用(例如 ResolveDelayLoadedAPI),则此步骤的同步由作系统 DLL 加载程序管理,因此任何其他组件都不应重新保护跨越已声明延迟加载 IAT 的页面。 为了向后兼容较旧的预 CFG作系统,工具可以启用将延迟加载 IAT 移动到其自己的部分(canonically“.didat”)的选项,在映像标头中作为读/写进行保护,并额外设置IMAGE_GUARD_CF_DELAYLOAD_IAT_IN_ITS_OWN_SECTION标志。 此设置将导致 CFG 感知作系统 DLL 加载程序重新保护包含IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT表的整个部分在映像加载期间只读内存。 如果你不关心在预先提供 CFG 支持的作系统上运行映像,则可能需要将延迟加载 IAT 置于其自己的部分中的选项,但工具应根据映像所需的最低作系统支持做出该决定。
如果映像不使用作系统的本机延迟加载支持,它仍然可以设置与 GuardFlags 位相关的受保护延迟负载。 在此配置中,作系统加载程序将仅提供支持,以保护延迟加载 IAT 作为运行时只读(如果平台支持),并且它将成为映像的内部延迟加载解析存根的责任,以同步和管理对延迟负载 IAT 的保护。 如果负载配置表存储在只读内存中(建议),则映像 GuardFlags 字段中存在或缺少受保护的延迟负载 IAT 位可能用作图像的内部延迟加载解析存根的内部提示,以指示它是否应保护延迟加载 IAT。
如果启用了 CFG,建议默认启用受保护的延迟加载。 在较旧的作系统版本上运行并使用作系统的本机延迟负载支持(如前所述)的映像可以使用其自己的部分支持的延迟加载 IAT 以实现向后兼容性。 这反对将延迟加载 IAT 标记为只读,并将其与另一部分合并,这将中断旧作系统,而该部分不了解受保护的延迟负载,并提供本机延迟加载解析支持。 所有 Windows 10 版本和支持 CFG 的第一个 Windows 8.1/Windows Server 2012 R2 版本(即 2014 年 11 月更新)都引入了对作系统中受保护延迟负载的支持。
函数对齐
- 如果可能,这些函数是在 GFIDS 表中采用的地址,因此应将其包含在 GFIDS 表中。 这并不总是可能的。 例如,对于非 COMDAT 函数,这些函数是非 CFG 感知工具作为一个单元组合在一起的对象文件的一部分,某些汇编程序可能会生成的工具的用户必须适当地设置对齐方式。 在这种情况下,工具可能会选择发出诊断警告,以便用户可以采取适当的纠正措施。 原因是 CFG 将调用目标标记为 16 字节边界有效或无效,以提高效率的快速 CFG 检查。 如果函数未对齐 16 字节,则必须将整个 16 字节槽标记为有效,这是不安全的,因为可以调用未在函数开头的代码中不对齐。 首次为项目启用 CFG 时,支持此方案以简化互作性。 对于兼容性的任何调用目标对齐,非 CFG 感知图像同样标记为有效。 与以前一样,如果调用目标不对齐可降低 CFG 的安全优势,因此当需要 CFG 时,工具应自动与 GFIDS 表中的任何内容保持一致的 16 字节边界。 不在 GFIDS 表中的符号不需要为 CFG 提供特定的对齐方式。
导出抑制
CFG 导出抑制(CFG ES)是一种可选模式,使进程能够指示仅因为它们是 dllexport 符号而有效的调用目标,并且尚未由 GetProcAddress 动态解析,将被视为对 CFG 无效。 这减少了从系统 DLL 导出到 CFG 的外围应用。 导出抑制涉及使用IMAGE_GUARD_FLAG_EXPORT_SUPPRESSED GFIDS 标志标记符合条件的“导出禁止”dllexport 调用目标。 出于生成 GFIDS 表的目的,应隐式考虑 Dllexport 符号和 PE 映像入口点的地址。 如果导出符号是 16 字节对齐的,并且该符号不是 dllexport 以外的其他原因采用的地址,则可以在函数表中用导出禁止 GFIDS 标志进行标记。 非 16 字节对齐 的调用目标不得 使用 IMAGE_GUARD_FLAG_EXPORT_SUPPRESSED GFIDS 标志进行标记,并且不能限制为仅在 GetProcAddress 时间作为有效调用目标动态启用。
支持 CFG ES 的图像包括 GuardAddressTakenIatEntryTable,其计数由 GuardAddressTakenIatEntryCount 作为加载配置目录的一部分提供。 此表在结构上的格式与 GFIDS 表的格式相同。 它使用相同的 GuardFlags IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK机制对采用 IAT 表的地址中的额外可选元数据字节进行编码,但对于采用的 IAT 表,所有元数据字节必须为零,并且保留。 采用的 IAT 表的地址指示导入图文的 RVA 的排序数组,这些 RVA 已导入为采用调用目标的符号地址。 此构造支持远程模块中存在的地址采用的符号,这些符号是 dllexports,并且正在使用 CFG ES。 此类代码构造的示例如下:
mov rcx, [__imp_DefWindowProc] call foo ; where foo takes the actual address of DefWindowProc.
必须枚举所有采用导入图块的此类地址,以便作系统加载程序可以找到它们,并在加载映像并贴靠其导入时使相应的调用目标有效。 如果没有已获取地址的导入 thunk,则表和计数可以为 0。
模块设置 IMAGE_GUARD_CF_EXPORT_SUPPRESSION_INFO_PRESENT GuardFlags 位,以指示它已枚举其地址中采用的所有地址,采用 IAT 表,并且符合 CFG ES 条件的所有导出都标有IMAGE_GUARD_FLAG_EXPORT_SUPPRESSED GFIDS 标志。 请注意,可能存在零这样的 thunk,也可能有零这样的 dllexport 符号。 未能维护采用的 IAT 表的地址可能是一个正确性问题,因为某些调用目标在 DLL 加载时可能无法有效。
模块将IMAGE_GUARD_CF_ENABLE_EXPORT_SUPPRESSION GuardFlags 位设置为指示它希望为进程启用 CFG ES。 在实践中,这仅适用于当前 EXE。 启用 CFG ES 的进程不应加载使用 CFG ES 生成的 DLL,或者运行时失败可能是因为采用未签名的地址而采用 IAT 符号。 支持启用 CFG ES 应是启用 CFG 的单独选择加入选项。 默认情况下,使用 CFG 提供 CFG ES 元数据是安全的,但工具集必须小心确保它们生成正确的元数据。 否则,生成的映像可能无法在 CFG ES 进程中正常运行。 应在强制实施 CFG ES 的测试过程中全面测试此类支持。 作系统内置系统 DLL 支持了解 CFG ES 的新式 Windows 10作系统版本的 CFG ES 元数据。 此支持之前的作系统版本根本不了解 CFG ES,并且将忽略映像中的任何 CFG ES 相关指令。 此类映像仍向后兼容较旧的作系统版本。
从工具集的角度来看,CFG ES 支持是可选的,但建议工具集至少包括支持来枚举足够的信息,以便在需要 CFG ES 的进程中运行图像。 如上所述,必须对工具集支持进行全面测试,以确保它与 CFG ES 兼容,因为大多数进程尚未启用 CFG ES。
异常处理和展开
__C_specific_handler等特定于语言的处理程序(由 .pdata 注册中的异常处理程序信息指定)不应标记为 GFIDS 表中的有效调用目标。 而是通过遍历只读内存来查找它们。 同样,Microsoft C 语言特定处理程序使用只读内存搜索来查找异常处理程序的 funclet,因此不会将其 funclet 声明为 GFIDS 表中的有效调用目标。
长跳处理(对于非 x86 目标(如 AMD64):使用 CFG 和支持 setjmp()/longjmp()编译的工具集应实现与结构化异常处理(SEH)互作的“安全跳远”。 这意味着长跳作为对 RtlUnwindEx 的调用实现,STATUS_LONGJUMP作为提供的异常记录中的状态代码,以及 ExceptionInformation[0] 指向的标准_JUMP_BUFFER。 跳转展开目标应为展开的 TargetIp。 跳转缓冲区表示在长跳完成后由作系统还原的寄存器上下文。 使用 STATUS_LONGJUMP调用时,RtlUnwind(Ex)对于 CFG 具有特殊意义。 跳远目标(_JUMP_BUFFER。撕裂或_JUMP_BUFFER。ARM64 上的 Lr 在作系统在只读内存中维护的已加载模块列表中查找。 如果跳转目标(“目标模块”)的包含模块在其 GuardFlags 字段中设置了IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT标志,则加载配置目录具有 GuardLongJumpTargetTable,这是加载配置 GuardLongJumpTargetCount 字段指定的元素计数。 此表在结构上的格式与 GFIDS 表的格式相同,并使用相同的 GuardFlags IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK 机制对长跳表中的可选额外元数据字节进行编码。 长跳表的所有元数据字节必须为零,并且保留。
长跳表表示有效的长跳目标 RVA 的排序数组。 如果长跳目标模块在其 GuardFlags 字段中设置IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT,则必须在 LongJumpTargetTable 中枚举所有跳远目标。 即使模块具有零长跳目标,如果工具集支持 CFG 的长跳强化,它仍应设置IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT标志。 这明确意味着该映像没有长跳目标,并且不是作系统必须假定的旧映像可以在无法执行长跳目标检查的未标记位置具有有效的跳远目标。
如果支持 CFG,建议默认启用长跳强化。 这是Microsoft编译器的处置。 不了解长跳强化(Windows 10 或更早版本的 Windows 10 版本)的作系统不会执行长跳强化检查并忽略任何长跳强化元数据,因此长跳强化与旧作系统版本向后兼容。
对于内核模式映像,防护长跳目标表不应包含在可丢弃部分。 防护长跳目标表应始终存储在只读内存中,以便其安全属性有效。
COFF 信息
有对象文件标记可以声明对象文件是否符合 CFG。 符合 CFG 的对象文件将列出它生成的有效调用目标,以及采用任何地址的 IAT 元数据。 不符合 CFG 的对象文件必须通过检查 obj 文件的 COFF 重定位来推断调用目标,以查找指向函数符号开头的重定位。 这可能过度应用有效的 CFG 调用目标,因此,最好工具标记其 obj 文件,这些文件是 CFG 感知的,如果用 CFG 进行编译,则包括 CFG obj 文件元数据。
有一些对象文件标记可以声明 CFG 强化的长跳目标,该目标应填充为 CFG 编译模式。