-
-
[翻译]随机填充页表地址如何实现? Windows中的动态值重定位表(DVRT)详解
-
发表于: 2025-3-29 17:04 1401
-
随机填充页表地址如何实现? Windows中的动态值重定位表(DVRT)详解
翻译: 既视感安全实验室
Retpoline机制
Retpoline修改是在内核模块加载到内存时即时进行的,目的是为了缓解Spectre处理器漏洞。Retpoline允许用一段具有安全预测行为的指令序列来替换间接调用或跳转。问题在于操作系统如何完成这些修改——它如何确定需要在哪些位置应用替换?这些信息又存储在何处?
DVRT
动态值重定位表(DVRT)是在编译阶段构建并嵌入二进制文件中的元数据。它并非专为retpoline设计的全新格式——这种格式早已存在,只是为了应对Spectre v2漏洞而进行了扩展。可以通过Windows PE32可执行文件的加载配置目录找到它(目前官方文档尚未收录)。例如,执行dumpbin /loadconfig win32k.sys
将得到以下输出:
注:dumpbin是Visual Studio原生工具集的一部分,需使用"x64 Native Tools Command Prompt"执行。win32k.sys位于C:\Windows\System32目录下。
DVRT格式包含大量信息,但本文只关注两个关键字段:
1 2 | uint32_t dynamicValueRelocTableOffset; uint16_t dynamicValueRelocTableSection; |
在构建过程中,编译器会收集所有间接跳转/调用的元数据,并将它们存储在.reloc节中,该节被重新用于DVRT目的。运行时,内核会解析DVRT数据并对其中记录的每个引用点应用retpoline补丁。可以通过注册表键值System\CurrentControlSet\Control\Session Manager\Memory Management\FeatureSettings
在系统级别启用retpoline功能。负责控制retpoline的标志位有:
1 2 | 0x100 - FEATURE_SETTINGS_DISABLE_RETPOLINE(禁用retpoline) 0x200 - FEATURE_SETTINGS_FORCE_ENABLE_RETPOLINE(强制启用retpoline) |
retpoline序列的代码存储在内核镜像的RETPOL部分。这部分会被映射到内核模块镜像结束后的紧邻页面(这意味着内存中每个模块镜像使用的是同一个页面)。所有间接调用和跳转的替换将改为引用这个带有retpoline的页面——这样能以安全的方式执行控制流,避免受到预测执行攻击。
格式详解
DVRT结构以以下头部开始:
1 2 3 4 5 | struct ImageDynamicRelocationTable { uint32_t version; uint32_t size; }; |
目前,DVRT头部只有一个版本(值为1)。size字段表示头部后包含retpoline信息的字节数。随后是各种类型retpoline的条目(下文详述)。每个块都以如下头部开始:
1 2 3 4 5 | struct ImageDynamicRelocation { uint64_t symbol; uint32_t baseRelocSize; }; |
symbol字段用于标识现有的动态重定位类型之一(值为3、4或5)。接着,对于每个页面,都有一个以重定位条目开始的块:
1 2 3 4 5 | struct PEBaseRelocation { uint32_t virtualAddress; uint32_t sizeOfBlock; }; |
其中virtualAddress是需要进行重定位的页面地址,sizeOfBlock是该页面所有条目的总大小(以字节计)。
之后是需要被retpoline跳转覆盖的所有位置的具体条目。这些条目使用的结构取决于上面指定的类型(symbol)。目前有三种类型的retpoline。每种类型的条目都包含pageRelativeOffset字段。内核利用该字段在virtualAddress + pageRelativeOffset地址处应用适当的替换。各类型条目的其余部分各不相同,下面将逐一介绍。
类型5 retpoline:
结构定义:
1 2 3 4 5 6 7 8 9 10 | union ImageSwitchtableBranchDynamicRelocation { struct Parts { uint16_t pageRelativeOffset : 12; uint16_t registerNumber : 4; }; Parts asParts; uint16_t asNumber; }; |
此类型用于那些参数存放在某个寄存器中的间接跳转指令。适用的指令格式包括:
1 2 | ff(e0 - e7) jmp rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp 41ff (e0 - e7) jmp r8, r9, r10, r11, r12, r13, r14, r15 |
具体使用哪个取决于寄存器编号。应用retpoline后,修改后的代码会变成:
1 | e9(xxxxxxxx) jmp <retpoline页面地址 + 0xA0 + 0x20 * registerNumber> |
跳转地址(在加载镜像时计算)会指向针对该情况的retpoline代码。在retpoline页面(即镜像结束后的下一页),偏移量0xA0处有为每个寄存器预留的0x20字节条目——包含适当的retpoline代码。
类型4 retpoline:
结构定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | union ImageIndirControlTransferDynamicRelocation { struct Parts { uint16_t pageRelativeOffset : 12; uint16_t isCall : 1; uint16_t rexWPrefix : 1; uint16_t cfgCheck : 1; uint16_t reserved : 1; }; Parts asParts; uint16_t asNumber; }; |
PageRelativeOffset的作用与前述相同。此外还使用了以下参数:
- isCall - 用于区分调用指令和跳转指令
- cfgCheck - 当调用/跳转是配置检查(即指针调用)或从rax寄存器进行间接调用时,此值为1。这对选择何种retpoline至关重要——当cfgCheck为1时,retpoline页面中的偏移量为0x2A0;cfgCheck为0时,偏移量为0x2E0
- rexWPrefix - 我们尚未遇到此标志被设置的情况,这将在后续文章中详细探讨
举例来说,对于cfgCheck为1且isCall为1的情况,原始代码
1 | ff15xxxxxxxx call qword ptr ds:[<cfgCheckAddressPtr>] |
会被替换为
1 2 | e8xxxxxxxx call <retpoline_page地址 + 0x2A0 > 90 nop |
类似地,对于cfgCheck为0且isCall为0的情况,原始代码
1 | ffe0 jmp rax |
会被替换为
1 2 | e9xxxxxxxx jmp <retpoline_page地址 + 0x2E0 > 90 nop |
类型3 retpoline:
结构定义:
1 2 3 4 5 6 7 8 9 10 11 | union ImageImportControlTransferDynamicRelocation { struct Parts { uint32_t pageRelativeOffset : 12; uint32_t isCall : 1; uint32_t iatIndex : 19; }; Parts asParts; uint32_t asNumber; }; |
此类型专用于对导入函数的调用或跳转。原始代码
1 2 | 48ff15xxxxxxxx call qword ptr ds:[<imported function address ptr>] 0f1f440000 nop dword ptr ds:[rax + rax * 1 ], eax |
会被替换为
1 2 | 4c8b15xxxxxxxx mov r10, qword ptr ds:[<imported function address ptr>] e8xxxxxxxx call <retpoline页面地址 + 0x420 > |
由于retpoline代码本身是固定不变的,导入函数的地址会先被移入r10寄存器,然后在retpoline代码中通过该寄存器执行间接调用。此情况下的具体retpoline地址偏移量为0x420。
同样,对于跳转指令,原始代码
1 2 | 48ff25xxxxxxxx jmp qword ptr ds:[<imported function address ptr>] cccccccccc int3 |
会被替换为
1 2 | 4c8b15xxxxxxxx mov r10, qword ptr ds:[<imported function address ptr>] e9xxxxxxxx jmp <retpoline页面地址 + 0x420 > |
在特定情况下,当启用导入优化功能时,指令不一定跳转到retpoline,而可能直接跳转到导入的函数模块。导入优化(又称导入链接)是另一种不使用retpoline就能解决Spectre v2问题的技术,适用于满足特定条件的场景。该功能也可通过注册表标志控制:
1 | 0x2000000 - FEATURE_SETTINGS_DISABLE_IMPORT_LINKING(禁用导入链接) |
对于类似于retpoline类型3的情况,指向导入函数的间接分支可能被直接调用目标所替代。要实现这一点,需满足以下条件:地址相对接近(小于2GB)、功能已开启、且镜像加载后IAT(导入访问表)内容不会变更。此功能与retpoline协同使用,能显著提升系统性能。
结论
反作弊软件必须能够区分游戏代码的良性修改和恶意篡改。操作系统一直会进行一些良性修改以应用基址重定位,并在导入地址表中存储导入函数地址。随着侧信道攻击缓解措施的出现,Windows内核现在需要修补更多位置,将调用和跳转重定向到retpoline区域,以缓解Spectre v2漏洞。DVRT是存储在.reloc节中的一种结构,用于描述这些补丁位置。我们已经详细剖析并记录了这些新型数据结构。