x64页表自映射

由于微软需要管理内存,内存由cr3维护,但r3又不能访问物理内存,因此微软设计了一种页表基址的基址;x64下分有pml4、pdpt、pde、pte四个表,每个表的表头地址用一个固定的虚拟地址进行保存,这个地址就被成为页表基址

页表自映射原理

使用windbg命令!pte查看0地址数据。

这里的pxe、ppe、pde、pte地址为虚拟地址(pxe和ppe下边统称为pml4、pdpt),同时也被称为对应项的页表基址。

对pml4、pdpt、pde、pte四项的页表基址进行拆分得到如下(去除页内偏移高16位):

Text
1
2
3
4
pte_base:   111010111 000000000 000000000 000000000(FEB8000000)
pde_base: 111010111 111010111 000000000 000000000(FEBF5C0000)
pdpte_base: 111010111 111010111 111010111 000000000(FEBF5FAE00)
pml4_base: 111010111 111010111 111010111 111010111(FEBF5FAFD7)

其中发现pml4_base拆分的每项都是111010111,使用windbg查看数据,发现读取第一项时,得到的仍然为本身!

这个就是微软设置的页表自映射,通过这种巧妙的地址索引构造,使得读取pml4_base时会连续查询四次都是得到cr3本身(cr3实际上就是pml4表头),通过这个机制很容易推导出pml4_base。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<ntifs.h>
#include <intrin.h>

VOID UnloadDrv(PDRIVER_OBJECT pDrv)
{
return;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDrv, PUNICODE_STRING pReg)
{
NTSTATUS status = STATUS_SUCCESS;
do
{
pDrv->DriverUnload = UnloadDrv;
//获取CR3的虚地址
PHYSICAL_ADDRESS pCr3 = {0};
pCr3.QuadPart = __readcr3();
PULONG64 tmp = MmGetVirtualForPhysical(pCr3); // 为cr3映射虚拟地址
if (tmp == NULL)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "get cr3 virtual address failed!\r\n");
break;
}
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "cr3 virtual address = %p\r\n", tmp);

//遍历
ULONG_PTR pml4_base = NULL;
for (int i = 0; i < 512; i++) //512是因为pml4表项有512个,cr3=pml4表头
{
// 如果读取到的地址 == cr3,则该虚拟地址为pml4的页基址
if (pCr3.QuadPart == (tmp[i] & 0xFFFFFFFFF000)) //去除属性
{
pml4_base = tmp[i] & 0xFFFFFFFFF000;
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "&tmp[i] = %p\rpml4_base = %p\r\n",&tmp[i], pml4_base);
break;
}
}

} while (FALSE);
return status;
}

实际上有了pml4_base,其他三个的基址也是可以推导得到。通过观察0地址的规律,可得如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
uintptr_t get_pdpt_base(uintptr_t pml4_base)
{
return (pml4_base >> 21) << 21;
}

uintptr_t get_pdpt_base(uintptr_t pml4_base)
{
return (pml4_base >> 30) << 30;
}

uintptr_t get_pte_base(uintptr_t pml4_base)
{
return (pml4_base >> 39) << 39;
}

假如说要获取某地址的某个表项,可通过如下表达式获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uinptr_t get_pte_address (uintptr_t addr)
{
return (((addr & 0xffffffffffff) >> 12 ) << 3) + g_pte_base;
}

uinptr_t get_pde_address (uintptr_t addr)
{
return (((addr & 0xffffffffffff) >> 21 ) << 3) + g_pde_base;
}

uinptr_t get_pdpt_address (uintptr_t addr)
{
return (((addr & 0xffffffffffff) >> 30 ) << 3) + g_pdpt_base;
}
uinptr_t get_pml4_address(uintptr_t addr)
{
return (((addr & 0xffffffffffff) >> 39 ) << 3) + g_pml4_base;
}

这里使用get_pte_address为例,对其中的表达式进行原理解释。这个和页表自映射有关,也是理解自映射最好的方式。回顾上边对0地址使用!pte得到的pte_base数据的拆分结果111010111 000000000 000000000 000000000。按照正常查询流程,左往右的第一项为pml4,但此时比较特殊,这里暂称为fake_pml4,原因为该项仅作为自映射,不参与实际地址的逻辑运算(该处就是通过自映射的方式填充第一项让cpu在拆分地址读的时候少读一项,使得只读到pte)。因此真正的pml4应该位于第二项中,然后紧接着为pdpt、pde,pte。呃,pte存放哪?别忘了我们这个是去除了后12位,pte就是存在后12位中,因此在页表映射的基础下,完整的拆分应该为下:

1
2
fake_pml4  pml4       pdpt       pde        pte 
111010111 000000000 000000000 000000000 000000000 000

其中后三位保持为0,因为要保持8字节对齐;因此这里重新看一下get_pte_address的表达式:

1、addr & 0xffffffffffff为去除高16位。

2、>> 12为去除page offset

3、上边两个步骤之后,原先的addr仅剩下了pml4-pdpt-pde-pte组合,此时为了保持对齐,因此进行<<3,最后加上g_pte_base,即可得到对应的pte地址。

其余的pdpt、pde、pml4也是相同道理,实际上就是个填空题,缺哪项填哪项!

总结:实际上就是利用自映射的方式,让cpu满足5次查询的情况下,只查询到我们需要的项。

MmIsAddressValidEx分析

这里仅分析微软计算pml4、pdpt、pde、pte地址的方式。

环境:win10 1903

ida打开ntoskrnl.exe,跳转到函数MmIsAddressValidEx。

其中0xFFFFF68000000000为PTE_base,每次重启都不相同。该处通过windbg查看可验证。

这里简单分析一下前半段内容:

其中由上边自映射原理可知,该处实际上为计算参数地址的pte地址保存至[rsp]中。这里计算的方式与上边表达式有差异但实际上相同,0x7FFFFFFFF8为去除高16位后再去除后3位,继续往下分析。

再继续一次相同的计算,式子有点长这里直接展开:

Text
1
2
pte_base = FFFFF68000000000 + (arg_addr >> 9) & 7FFFFFFFF8
rdx = FFFFF68000000000 + (pte_base >> 9) & 7FFFFFFFF8

这里(pte_base >> 9) & 7FFFFFFFF8后实际上只保留了pml4-pdpt-pde的组合,因此这里获取的是pde地址保存至[rsp+8]中,再往下还有两次相同的计算,实际上就是算pdpt地址和pml4地址。

因此另一种推导页表基址的方式为,通过定位MmIsAddressValidEx获取pte_base,然后通过规律计算出其他三项的页表基址

设地址为0,pte_base = 0xFFFF868000000000,则pde_base、pdpt_base、pml4_base为如下:

  • pde_base = pte_base + (pte_base >> 9) & 7FFFFFFFF8 = 0xFFFF86C340000000

  • pdpt_base = pte_base + (pde_base >> 9) & 7FFFFFFFF8 = 0xFFFF86C361A00000

  • pml4 = pte_base + (pdpt_base >> 9) & 7FFFFFFFF8 = 0xFFFF86C361B0D000