由于在研究内核漏洞的时候,在漏洞的利用方式上有时候,时常会用到Physical Address
的概念,最先是在研究SMBleeding CVE-2020-0796、CVE-2020-1206
(记录文档丢失)的RCE利用的时候接触到这一方式。后续在研究CVE-2018-1038
时,再次接触到这一利用方式,有感于这种利用方式的威力,思路的新颖,特此学习记录。
Windows 自从Windows Vista以来在安全方面做了很多功课,除了在用户层我们熟知的NX/DEP/ALSR/SafeSEH等缓解措施,Windows 在内核层也做了很多漏洞缓解措施。
在kernel mode下也区分data
和code
,通过PROTECTED MODE MEMORY SEGMENTATION
实现内存的属性标记。
与ALSR类似,在kernel mode下,将Windows 的模块基地址随机化,提高RCE利用难度(需要一个泄漏内核模块地址的信息泄漏漏洞)。而对LPE是没有影响的,因为在Local System下,可以通过NtQuerySystemInfomation
获取模块信息。
即可信级别
,高风险的程序(典型的浏览器)可信级别低,将不被允许执行敏感操作(系统调用),如NtQuerySystemInfomation
,以此避免这类程序完成权限提升。
全称Supervisor Mode Excution Protect
,该保护措施严格区分Kernel Space
和User Spcae
,即不允许以SYSTEM
权限运行User Space
的代码。
以上种种措施,使得exploit更难实现利用,很多情况下是拥有一定的权限(常见的任意地址写),但是无法转为LPE
或者RCE
1KB = 0x400B
1MB = 1KB * 0x400
1GB = 1MB * 0x400
1TB = 1GB * 0x400
在32bits机器上,cpu需要访问的虚拟内存空间达到4G
,为了达到这个目的,采用了PAGE DIRECTORY
和PAGE TABLE
两级寻址机制。
在Window 上CR3寄存器指向当前的PAGE DIRECTORY
的Physical Address
,每个PAGE DIRECTORY有1024
个ENTRY
(简称PDE
),每个PDE指向一个PAGE TABLE的Physical Address。每个PAGE TABLE有1024
个ENTRY(简称PTE
),每个PTE指向一块4KB
物理地址,即将4KB物理地址映射到4KB的虚拟地址。
一个虚拟地址即可以用下面的方式映射到物理地址(也可以反过来由物理地址得到虚拟地址)
每个ENTRY都是4bytes,其中低位用于标记相应地址的属性。
其中,PDE在一种特殊情况下可以不指向1024个PAGE TABLE,而是指向1个Large Page
(拥有4MB大小),即将PS
(Page size)标记。
需要注意到的是,Windows上的每个进程都拥有4GB的虚拟内存,而且互相之间不可以访问,这种实现方式必然要求每个进程的指向Page Directory的Physical Address不同!
即Physical Address Extension
,引入了更高的一层寻址机制Page Directory Pointer Table(PDPT)。将物理地址的表示方式由32bits增加到了36bits,也就意味着此时的寻址空间达到了64GB。
PDPT中保存着Page Directory的Entry(简称PDPTE
),每个Entry指向一个Page Directory,表示1GB大小,因为此时每个entry表示需要8bytes,所以每个Table只有512个Entry,也就是512 PDE * 512 PTE * 4KB
。
此外PAE引入了NX bit,用于完成data和code的区分。
在64bits 系统中,此时CPU可访问的物理地址有48bits,而虚拟地址达到了64bits。明显是不足的。这时提出了一个概念CANONICAL ADDRESS
,提出真实的虚拟地址空间也只有48bits,不过虚拟内存做了一个区分,即我们熟知的两部分:0 - 0x7FFF FFFF FFFF
和0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF
。
为了满足48bits寻址需求,在PAE基础上引入了四级页表寻址的概念:PML4、PDPT、PAGE DIRECTORY、PAGE TABLE.
此时的寻址计算式512PML4Es * 512PDPTEs * 512PDEs * 512PTEs * 4KB = 256TB
到目前为止,Windows Page机制基本介绍完了,但是这里有个问题,上面都是在介绍Windows物理地址如何映射到整个虚拟地址空间,但是为了管理虚拟地址,Windows有必要完成逆过程,即 将任意一个虚拟地址定位到某一物理地址。这一过程会存在一些问题。
1、应用程序需要分配一块虚拟地址 VirtualAlloc 得到 0x402000
2、为了管理虚拟地址,Windows需要建立PAGE TABLE 分配物理地址映射 0x4002000,这物理地址假设 0x1000
3、为了管理新建的PAGE TABLE,Windows需要另一个Virtual Address来存储,假设0x80001000
4、同样的,又需要一个PAGE TABLE将 0x80001000 到映射物理地址 0x8000+0x1000
5、有一个PAGE TABLE.....
这个过程显然并不理想,因此,Windows Page不止于此....
该技术 将最高级别的页表中的某一entry指向该页表自身。在32bits下,self-entry存在于PAGE DIRECTORY,64bits下,self-entry存在于PML4。
以64bits下为例,self-entry的两种特殊情况
self-ref entry在PML4的entry 0
self-ref entry在PML4的entry 0x1FF
可以看到,PML4的512个entry都可以作为self-ref entry,随机的。而事实上,self-ref entry可以存在于在四级页表的每个级别上,没必要只允许在最高级别的页表上(Windows没有采用)
该技术解决了上面的虚拟内存分配时遇到的问题,因为此时已经将Page Table预先分配好了(通过Self-ref entry指定了Table的地址)
在Self-ref机制中,一个Page Table会有3个entry是有特殊的作用,分别用来指定User Space
、Kernel Space
、Self-ref
,我们作如下假设:
entry 0x00 ==> User Space; entry 0x100 ==> self-ref; entry 0x1ff ==> Kernel Space
在此基础上,在64bits系统上相应的虚拟内存的分布就是“User Space:0 ~ 0x7f ffff ffff
(512G entry 0 + 512G),Memory Management:0xffff 8000 0000 0000 ~ 0xffff 8000 0000 0000
(canonical_address+ 512G entry 0x100 + 512G)、Kernel Space: 0xffff ff80 0000 0000 ~ 0xffff ffff ffff ffff
”(canonical_address+ 512G * entry 0x1ff + 512G)
通过上面的描述,我们可以计算出系统中PML4对应的虚拟地址:Canonical_Address + (512G + 1G + 2MB + 4KB) * 0x100 = 0xffff 8040 2010 0000
,这就意味着,当需要访问用户空间地址的时候,一定会访问0xffff 8040 2010 0000 + 0x00*8
;当需要访问内和空间地址的时候,一定会访问0xffff 8040 2010 0000 + 0x1ff*8
。
同时,根据这个规律,我们可以将任何一个虚拟地址的Page Table entry计算出来(虚拟地址对应Page Table每个level的量值,bits对应 12.9.9.9)
def calc_physical_64(virtual_addr): entry_size = 0x8 shift_address = virtual_addr >> 12 # 4kb pte_offset = shift_address & 0x1ff shift_address = shift_address >> 9 # 512 pde_offset = shift_address & 0x1ff shift_address = shift_address >> 9 # 512 pdpt_offset = shift_address & 0x1ff shift_address = shift_address >> 9 pml_offset = shift_address & 0x1ff print("entry: PML4: 0x%x PDPT: 0x%x PD: 0x%x PT: 0x%x" % (pml_offset, pdpt_offset, pde_offset, pte_offset)) print("offset: PML4: 0x%x PDPT: 0x%x PD: 0x%x PT: 0x%x" % (pml_offset * entry_size, pdpt_offset * entry_size, pde_offset * entry_size, pte_offset * entry_size)) def calc_physical_32(virtual_addr): entry_size = 0x8 shift_address = virtual_addr >> 12 # 4kb pte_offset = shift_address & 0x1ff shift_address = shift_address >> 9 # 512 pde_offset = shift_address & 0x1ff print("entry: PD: 0x%x PT: 0x%x" % (pde_offset, pte_offset)) print("offset: PD: 0x%x PT: 0x%x" % (pde_offset * entry_size, pte_offset * entry_size))
Self-Ref机制的缺点是很明显的,就是上面展示的,由于self-entry的位置并不是绝对随机化的,对于给定的虚拟地址,我们是有可能计算出用来管理该虚拟地址的各级Page Table的。
Windows在32bits和64bits中都采用了Self-Ref机制,以64bits系统为例,前256 PML4 entries用作USER SPACE,后256 PML4 entries用作KERNEL SPACE。PML4的self-ref entry是0x1ED(在内核空间)相应的虚拟地址空间(512G + 1G + 2M + 4K)*0x1ED =0xf6fb 7dbe d000
,加上canonical address就是0xffff f6fb 7dbe d000
。
事实上,Windows上为所有运行的进程使用 固定的 PML4 self-ref entry,这使得攻击者可以计算出Page Table/entries(无论使用的那些物理地址)。通过这种方式,攻击者可以修改或者添加entries。这种方式既可以用在本地也可以远程攻击中,windows2000 或者win10 都受影响。使得KALSR受到冲击,这种攻击方式在Windows SMEP bypass
中有所体现。
Windows采用了随机化Self-Ref Entry的措施。
Windows由于KALSR的存在,每个模块基地址都是随机的。但是我们不由得问道“第一个加载的模块呢?”
Windows每次开机时第一次加载的模块是HAL.dll
(Hardware Abstraction Layer),该模块运行在Kernel Mode下,用于抽象基础硬件。通过这种方式Windows可以通过Hal.dll的导出函数与各种硬件交互。
HAL.DLL在运行时同样需要Stack,Heap空间,但是最有趣的是HEAP,HAL.DLL的HEAP地址在启动时由HAL创建,该地址总是映射在同一个虚拟地址空间(自从win2000),这个攻击因子也被用来Bypass KALSR。这个固定的虚拟地址是:Windows 32 bits =>0xffd0 0000
,Windows => 0xffff ffff ffd0 0000
。下面是一张64bits 下的该地址的物理地址及虚拟地址查询表
为了验证上面探讨的Windows Page机制,我分别在Windows 7 32bits和 Windows 10 64bits上做了下面的实验。
测试代码:
#include <Windows.h> #include <wchar.h> int main() { PVOID addr = (PVOID)0x1000000; //Allocate Memory addr = VirtualAlloc(addr, 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); wprintf(L"address = 0x%llx\n", addr); // Setting Memory memset(addr, 0x41, 0x1000); // Debug __debugbreak(); }
运行到断点时,可以发现已经成功地分配到虚拟地址,并在其中写入了我们的数据
查看对应的虚拟地址的Page Table状态
可以发现32bits上只有PDE和PTE,而且我们发现PTE用物理地址0x61cd5000(低12bits是标志位) 映射 虚拟地址0x1000000。这说明虚拟地址在解析到具体的物理地址的时候依赖PTE的值。
例如,我们修改PTE的值,使其指向别的物理地址。
可以看到,此时eax指向虚拟地址没有变化,但是内容已经变化,这就意味着我们修改某一虚拟地址对应的PTE到某一物理地址,就能实现修改虚拟地址的内容的目的,也可以达到对该物理地址实际对应的虚拟地址的重写的目的。
再举个例子,在Win7上的任意地址写利用时,时常会用到的nt!HalDispatchTable
偏移0x4
位置的指针hal!HaliQuerySystemInformation
。我们尝试修改可控的虚拟地址指向和该地址同一个物理地址的位置,看看会发生什么。
查看HalDispatchTable
我们将该物理地址03f6f000+3fc
写到虚拟地址eax=0x1000000
的PTE中,对虚拟地址eax写入看看会怎样
可以看到,我们通过将两个虚拟地址的PTE修改为一致的,可以通过写入可控的虚拟地址来修改到不可控的虚拟地址!(偏移在虚拟地址和物理地址是一样的)
除此之外,我们可以验证下由虚拟地址获取Page Table地址
由上面给的脚本计算出的PTE值
可以看到偏移是完全正确的,缺的是一个PTE的基地址。
另外,值得注意的是,在测试过程中发现,Window 7中,0x1000000虚拟地址对应的物理地址是随机的,但是PDE和PTE始终保持不变,这也就意味着Self-Ref是没有开启随机化的。
同样的运行测试的代码,断下后的状态
可以看到,Win10 x64上的四级页表寻址机制,其中PTE保存的依然是虚拟地址对应的物理地址内容。
修改PTE的效果和Win7是一样的(可以自己动手实验下)。
测试上述脚本根据虚拟地址得到Page Table的情况,可以看到偏移是一致的。
而且,在Windows 10及以上,存在nt!MiGetPteAddress
,Pte基地址编码在固定的偏移0x13
,类似的也有MiGetPdeAddress
,偏移0xc
这种情况下,根据虚拟地址得到PTE是相对容易的。如果我们拥有任意地址写,是可以用用户可控的虚拟地址的PTE修改物理地址到一内核地址,实现利用的。
Windows10测试中,PTE基地址是随机的。