[2025]Windows的内存防护机制 huoji windows,内存防护 2025-06-23 1504 次浏览 2 次点赞 来自戎码高级安全研究员kanren3的投稿: # 背景 在 Windows XP 时代以后,微软意识到了系统安全的重要性,因此逐步加强 Windows 自身的安全机制,从早期的 **PatchGuard**,到如今的 **Virtualization-based Security (VBS)**,让漏洞利用变得越来越困难。这篇文章将简单介绍一些 Windows 基于硬件的内存安全防护机制以及它们会带来的影响。 # KPTI 在2018年,披露了一个名为 **Meltdown** 的漏洞。带有乱序执行功能的 **Intel** 处理器都受到了影响,存在数据内核泄露的风险,在这种情况下而操作系统需要对漏洞进行缓解,这个技术名为 **内核页表隔离(KPTI)** 。 在内核初始化的时候,系统会调用 `KiDetectKvaLeakage` 函数来检测当前 CPU 是否需要启用 KPTI,全局变量 `KiFeatureSettings` 与 `KiFeatureSimulations` 可以控制 KPTI 的启用与禁用,注册表项可以对其进行控制: ``` HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\FeatureSettingsOverride HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\FeatureSimulations ``` 当 KPTI 启用时,非管理员权限进程的页表会从一张变成两张,我们可以在 **Windbg** 进行双机调试,使用 `dt _KPROCESS` 命令查看对应进程的两个页表: - `+0x028 DirectoryTableBase` : `0x000000011c0df002` - `+0x388 UserDirectoryTableBase` : `0x000000011c0de001` 其中 **内核页表 (DirectoryTableBase)** 则同时映射了用户和内核的内存,但是 **用户页表 (UserDirectoryTableBase)** 只映射了用户内存和极小一部分的 **Shadow** 函数:  **Shadow** 函数都存放在一个名为 `KVASCODE` 的节中,它负责在系统调用,中断,异常的时候,将进程的 **CR3** 从 **用户页表 (UserDirectoryTableBase)** 切换到 **内核页表 (DirectoryTableBase)**,**用户页表 (UserDirectoryTableBase)** 无法访问 `KVASCODE` 节以外的内核内存:   只有在切换到 **内核页表 (DirectoryTableBase)** 以后,`KiSystemCall64` 的内存才可以访问:  而在进行 **CR3** 切换的时候,CPU 会刷新非全局页面的 TLB,所以 Windows 选择使用 **PCID** 来避免造成过多的性能损失。可以通过置位 **CR4.PCIDE** 来启动这个功能,这个时候 **CR3** 的低12位代表 PCID,在 **MOV TO CR3** 的时候,最高位 Bit 63 代表是否刷新当前 **PCID** 下的 TLB。 # HVCI 系统漏洞可以靠更新补丁进行及时的缓解,但是带有漏洞的驱动很难做到,甚至有很多带有漏洞的驱动已经停止了更新维护,带有任意内核内存读写原语的漏洞驱动,有可能获得内核执行 API 的权限,进而进行更高级的功能,比如加载驱动,卸载驱动等等,较为知名的例子有 **kdmapper**,这种漏洞驱动很难进行缓解,所以微软为 **VBS** 添加了一个基于 **SLAT** 实现的内存防护机制 **HVCI**。 当系统启用 **HVCI** 以后: - 分配的内核内存的 PTE 不再拥有执行权限,并且在 **SLAT** 中也设置为不允许执行。 - 映射出来的只读/可执行权限的内存不再拥有写入权限,并且在 **SLAT** 中也设置为不允许写入。 这样哪怕在 **PTE** 中设置了相应的位,也无法对内存进行非法的写入或执行,但是这也意味着很多古老的的驱动将无法加载,哪怕这些驱动没有漏洞,哪怕这些驱动已经没有人在更新维护。 ### MBEC 启用 **HVCI** 以后,虽然不允许分配 **读写执行 (RWX)** 的内核内存,但是在进程没有启用 **任意代码保护 (ACG)** 的情况下,依旧可以为进程分配 **读写执行 (RWX)** 的用户内存,于是就出现了下面这样一个问题。 既然内核分配的内存无法执行,那是否可以通过分配可写可执行的用户内存,然后在内核里进行执行呢?答案是可以的,但是无法直接执行,因为 CPU 存在 **SMEP** 机制,无法直接执行用户内存,绕过 **SMEP** 的方法有两种,一种是通过复位 **CR4.SMEP** 来关闭这个功能,另一个是将页表中 **PTE** 里的 **U/S** 复位: - 由于 **CR4** 寄存器被 **VMCS** 中的 **Cr4Mask** 和 **Cr4ReadShadow** 字段限制,所以无法复位 **CR4.SMEP**,尝试复位会产生 **#GP** 异常。 - 而 **HVCI** 因性能问题并未对页表进行保护,所以可以写入页表的的 **U/S** 位 将用户页面转变为内核页面。 这种安全隐患,微软当然也意识到了,所以引入了另一个技术: - 在 **Intel** 里叫 **Mode-based execute control (MBEC)**,它基于 **EPT**。 - 在 **AMD** 里叫 **Guest Mode Execute Trap (GMET)**,它基于 **NPT**。 **读写执行 (RWX)** 的用户内存在 **SLAT** 里会被设置如下字段: - **EPT** 会设置它的 **Read,** **Write,** **ExecuteForUserMode**。 - **NPT** 会设置它的 **P,** **R/W,** **U/S**。 这样只有在 **User Mode** 才可以执行,而 **Supervisor Mode** 下哪怕设置了页表中的 **U/S** 位,也会因会 **SLAT** 产生异常。这项技术并非所有 CPU 都支持,最少需要也得 **Intel Kaby Lake** 或 **AMD Zen 2** 才支持,这也代表会有很多老机器的 CPU 不支持 **MBEC / GMET**,在这种情况下 **HVCI** 会软件模拟出这种能力,微软将它称之为**Restricted User Mode (RUM)**。 **RUM** 的模拟过程有点类似于 **KPTI**,**Hypervisor** 会为用户内存分配两个 **SLAT**,其中一个设置为 **读写执行 (RWX)** 给 **User Mode** 用,另一个设置为 **读写 (RW)** 给 **Supervisor Mode** 用,系统调用指令从曾经的 `SYSCALL` 改为 `INT 2Eh` ,**Hypervisor** 在接收到中断或异常的时候,需要切换到当前对应的 **SLAT**。软件模拟出来的 **RUM** 性能远不如 **MBEC / GMET**,但是终究也是换来一定的安全性。 # HVPT 虽然 **HVCI** 保护的是物理地址,但是操作系统访问的依旧还是虚拟地址,所以攻击者可以通过修改 **PTE** 的 **PageFrameNumber**,将另一块物理内存重映射给这个虚拟地址,从而实现绕过 **HVCI** 进行内存写入,比如这两年在作弊圈子里兴起的 **PTE HOOK**,就是通过这种方式在绕过 **PatchGuard** 对 **SSDT** 函数进行挂钩,于是微软引进了一个名为 **hypervisor-managed linear-address translation (HLAT) **的技术,这个技术首次发布于 Intel 的 **Alder Lake** 上。 ### VMCS 在普通的分页模式中,会依靠 **CR3** 中的页表结构进行寻址,**HLAT** 提供了一种特殊的页表,我们称他为 **HLAT分页**,它是基于虚拟化实现的,所以在 **VMCS** 中会增加三个字段: - **enable HLAT** 字段负责控制 **HLAT** 的启用与禁用。 - **HLAT pointer (HLATP)** 字段负责代替普通分页模式中的 **CR3**。 - **HLAT prefix size** 字段负责控制哪些地址会走 **HLAT** 进行寻址。 其中 **HLAT prefix size** 的功能类似于掩码,它的最大值存放在 **IA32_VMX_EPT_VPID_CAP[53:48]** ,**HLAT prefix size** 的匹配规则如下: - 当它为 0 时,不会进行前缀匹配,所有的线性地址都会通过 **HLAT** 进行寻址。 - 当它为 20 时,线性地址的高 20 位都置位的情况下才会通过 **HLAT** 进行寻址,也就是从 **0xFFFFF00000000000** 到 **0xFFFFFFFFFFFFFFFF**。 同时在各级的页表结构中,也会增加一位 **Bit 11 (Restart)** ,在 **HLAT** 寻址的过程中,如果发现有页表结构设置了它,寻址过程会重启,使用 **CR3** 重新进行寻址。 ### EPT 在 **EPT** 为了防止重映射攻击,也在表项中增加了两个新的标志位: * **Paging-write access (PW)** * **Verify guest paging (VP)** 重映射攻击的原理是修改页表,如果使用 **EPT** 将 **某些** 页表设置为不可写入,那便可以缓解这种攻击,但是 CPU 在进行内存访问的过程中,可能会设置页表中的 **Accessed** 和 **Dirty** 位,这个过程称为 **分页写入 (Paging-write)**,这样会产生大量的 **EPT violation**,这是非常耗费性能的。 于是引入了 **Paging-write access (PW)**,在 **EPT** 设置为不可写的情况下设置此位,CPU 进行 **分页写入 (Paging-write)** 就不会产生 **EPT violation**,使用它可以在不影响性能的情况下,将物理内存与页表强行绑定,让攻击者无法通过修改页表来修改内存。 而 **Verify guest paging (VP)** 的作用则是验证寻址的四级表项的合法,如果 **GPA** 对应的 **EPT** 设置了此位,那么在 **GVA** 转换到 **GPA** 过程中访问的页表,也被要求需要在 **EPT** 中设置 **PW** 位,否则会产生 **EPT violation**,这样就可以让受保护的物理内存,不会被进行预期外的映射。 # 总结 以上仅仅是各项技术的原理与基本用法,更多设计上的东西,微软并未透露过多,可以在一些笔记博客,以及《Windows Internals 7th》中得到零零散散的信息,可以得到的结论是,这些技术确实有效的防止了攻击,但由于硬件方面的原因,想要普及任重而道远。 # References [Intel® 64 and IA-32 Architectures Software Developer’s Manuals](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html) [Intel VT-rp - Part 1. remapping attack and HLAT](https://tandasat.github.io/blog/2023/07/05/intel-vt-rp-part-1.html) [Intel VT-rp - Part 2. paging-write and guest-paging verification](https://tandasat.github.io/blog/2023/07/31/intel-vt-rp-part-2.html) [Hypervisor-enforced Paging Translation - The end of non data-driven Kernel Exploits](https://github.com/AaLl86/WindowsInternals/blob/master/Slides/Hypervisor-enforced%20Paging%20Translation%20-%20The%20end%20of%20non%20data-driven%20Kernel%20Exploits%20(Recon2024).pdf) 本文由 huoji 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。 点赞 2
还不快抢沙发