首页
社区
课程
招聘
[翻译]随机填充页表地址如何实现? Windows中的动态值重定位表(DVRT)详解
发表于: 2025-3-29 17:04 1401

[翻译]随机填充页表地址如何实现? Windows中的动态值重定位表(DVRT)详解

2025-3-29 17:04
1401

随机填充页表地址如何实现? Windows中的动态值重定位表(DVRT)详解

翻译: 既视感安全实验室

原文: Dynamic Value Relocation Table (DVRT) details | How Meltdown and Spectre haunt Anti-Cheat: DVRT details

Retpoline机制

Retpoline修改是在内核模块加载到内存时即时进行的,目的是为了缓解Spectre处理器漏洞。Retpoline允许用一段具有安全预测行为的指令序列来替换间接调用或跳转。问题在于操作系统如何完成这些修改——它如何确定需要在哪些位置应用替换?这些信息又存储在何处?

DVRT

动态值重定位表(DVRT)是在编译阶段构建并嵌入二进制文件中的元数据。它并非专为retpoline设计的全新格式——这种格式早已存在,只是为了应对Spectre v2漏洞而进行了扩展。可以通过Windows PE32可执行文件的加载配置目录找到它(目前官方文档尚未收录)。例如,执行dumpbin /loadconfig win32k.sys将得到以下输出:

Load Config in dumpbin

注: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节中的一种结构,用于描述这些补丁位置。我们已经详细剖析并记录了这些新型数据结构。


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册