Abusing Windows Physical
2020-10-22 10:49:38 Author: xz.aliyun.com(查看原文) 阅读量:304 收藏

Introduction

​ 由于在研究内核漏洞的时候,在漏洞的利用方式上有时候,时常会用到Physical Address的概念,最先是在研究SMBleeding CVE-2020-0796、CVE-2020-1206(记录文档丢失)的RCE利用的时候接触到这一方式。后续在研究CVE-2018-1038时,再次接触到这一利用方式,有感于这种利用方式的威力,思路的新颖,特此学习记录。

Background

​ Windows 自从Windows Vista以来在安全方面做了很多功课,除了在用户层我们熟知的NX/DEP/ALSR/SafeSEH等缓解措施,Windows 在内核层也做了很多漏洞缓解措施。

NX/DEP

​ 在kernel mode下也区分datacode,通过PROTECTED MODE MEMORY SEGMENTATION实现内存的属性标记。

KALSR

​ 与ALSR类似,在kernel mode下,将Windows 的模块基地址随机化,提高RCE利用难度(需要一个泄漏内核模块地址的信息泄漏漏洞)。而对LPE是没有影响的,因为在Local System下,可以通过NtQuerySystemInfomation获取模块信息。

Integrity Levels

​ 即可信级别,高风险的程序(典型的浏览器)可信级别低,将不被允许执行敏感操作(系统调用),如NtQuerySystemInfomation,以此避免这类程序完成权限提升。

SMEP

​ 全称Supervisor Mode Excution Protect,该保护措施严格区分Kernel SpaceUser Spcae,即不允许以SYSTEM权限运行User Space的代码。

​ 以上种种措施,使得exploit更难实现利用,很多情况下是拥有一定的权限(常见的任意地址写),但是无法转为LPE或者RCE

Windows Pages

unit
1KB = 0x400B
    1MB = 1KB * 0x400
    1GB = 1MB * 0x400
    1TB = 1GB * 0x400
Intel Paging x86

​ 在32bits机器上,cpu需要访问的虚拟内存空间达到4G,为了达到这个目的,采用了PAGE DIRECTORYPAGE TABLE两级寻址机制。

在Window 上CR3寄存器指向当前的PAGE DIRECTORYPhysical Address,每个PAGE DIRECTORY有1024ENTRY(简称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不同!

PAE

​ 即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的区分。

Intel Paging x64

​ 在64bits 系统中,此时CPU可访问的物理地址有48bits,而虚拟地址达到了64bits。明显是不足的。这时提出了一个概念CANONICAL ADDRESS,提出真实的虚拟地址空间也只有48bits,不过虚拟内存做了一个区分,即我们熟知的两部分:0 - 0x7FFF FFFF FFFF0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF

​ 为了满足48bits寻址需求,在PAE基础上引入了四级页表寻址的概念:PML4、PDPT、PAGE DIRECTORY、PAGE TABLE.

​ 此时的寻址计算式512PML4Es * 512PDPTEs * 512PDEs * 512PTEs * 4KB = 256TB

Problems

​ 到目前为止,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不止于此....

Self-ref Entry Technique

​ 该技术 将最高级别的页表中的某一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的地址)

Play Self-Ref

​ 在Self-ref机制中,一个Page Table会有3个entry是有特殊的作用,分别用来指定User SpaceKernel SpaceSelf-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 Entry Weakness

​ Self-Ref机制的缺点是很明显的,就是上面展示的,由于self-entry的位置并不是绝对随机化的,对于给定的虚拟地址,我们是有可能计算出用来管理该虚拟地址的各级Page Table的。

Self-ref In Windows

​ 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 version 1607 Update

​ Windows采用了随机化Self-Ref Entry的措施。

Windows Hal Heap

​ 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 下的该地址的物理地址及虚拟地址查询表

Explorer By Windbg

​ 为了验证上面探讨的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();
}
Windows 7 32bits

​ 运行到断点时,可以发现已经成功地分配到虚拟地址,并在其中写入了我们的数据

​ 查看对应的虚拟地址的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是没有开启随机化的。

Windows 10 1903 64bits

​ 同样的运行测试的代码,断下后的状态

​ 可以看到,Win10 x64上的四级页表寻址机制,其中PTE保存的依然是虚拟地址对应的物理地址内容。

修改PTE的效果和Win7是一样的(可以自己动手实验下)。

​ 测试上述脚本根据虚拟地址得到Page Table的情况,可以看到偏移是一致的。

​ 而且,在Windows 10及以上,存在nt!MiGetPteAddress,Pte基地址编码在固定的偏移0x13,类似的也有MiGetPdeAddress,偏移0xc

​ 这种情况下,根据虚拟地址得到PTE是相对容易的。如果我们拥有任意地址写,是可以用用户可控的虚拟地址的PTE修改物理地址到一内核地址,实现利用的。

Windows10测试中,PTE基地址是随机的。

Related Links


文章来源: http://xz.aliyun.com/t/8392
如有侵权请联系:admin#unsafe.sh