x64页表映射

x64页表映射

微软设计了一种页表自映射的方式,提高了cpu工作的效率。正常情况下一个线性地址需要进行4次拆分.

image-20230525191034126

而在表映射的作用下,可实现3次拆分即可。

原理

微软在PML4表中的某一项保存着表地址(实际上也是CR3),假设这个地址在PML4+0x100的位置,那么根据下图所示:

image-20230525190600016

有这么一个关系:![cr3+i*8]=cr3。由于一个PML4E可以管理的内存大小为512G,因此0x100位置的PML4E可以管理的内存范围如下:

1
0xFFFF8000`00000000 ~ 0xFFFF807F`FFFFF000

按照 9-9-9-9-12 分页方式去拆分上述边界物理地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 起始地址
0x8000`00000000
1 0000 0000 0x100
0 0000 0000 0x0
0 0000 0000 0x0
0 0000 0000 0x0
0000 0000 0000 0x0

// 结束地址
0x807F`FFFFF000
1 0000 0000 0x100
1 1111 1111 0x1FF
1 1111 1111 0x1FF
1 1111 1111 0x1FF
0000 0000 0000 0x0

常规查询流程:

1
2
3
4
5
6
7
8
9
10
11
// 起始地址
![Cr3 + 0x100 * 8] = PDPTE
![PDPTE + 0x0 * 8] = PDE
![PDE + 0x0 * 8] = PTE
![PTE + 0x0 * 8] = 物理页面

// 结束地址
![Cr3 + 0x100 * 8] = PDPTE
![PDPTE + 0x1FF * 8] = PDE
![PDE + 0x1FF * 8] = PTE
![PTE + 0x0 * 8] = 物理页面

根据上述等式,![Cr3 + 0x100 * 8] = Cr3,所以查询流程变为:

1
2
3
4
5
6
7
8
9
// 起始地址
![Cr3 + 0x0 * 8] = PDE
![PDE + 0x0 * 8] = PTE
![PTE + 0x0 * 8] = 物理页面

// 结束地址
![Cr3 + 0x1FF * 8] = PDE
![PDE + 0x1FF * 8] = PTE
![PTE + 0x0 * 8] = 物理页面

很神奇,查询页表操作由四次变成了三次,效率大大提升。而且只是使用了8字节的物理地址空间来保存 Cr3 。下图展示了优化后的查询过程:

image-20230525191009000

推导PML4基地址

PML4 页表基址有两个特点:

  • 属于虚拟地址
  • 虚拟地址的内容是Cr3

cr3=[pml4_base]

物理寻址如下:

1
2
3
4
5
6
//!为读物理地址
cr3 =![x + pte]
=![x + ![y + pde]]
=![x + ![y + ![z + pdpte]]]
=![x + ![y + ![z + ![r + pml4]]]]
=![x + ![y + ![z + ![r + ![n + cr3]]]]]

其中cr3=pml4_base

  • x:page_offset,页内偏移
  • y:pti
  • z:pdi
  • r:pdti
  • n:自映射的索引。

当x=y=z=r=0时,可得:

1
cr3 = ![![![n + cr3]]]

由此可知在cr3中的某项保存的物理地址指向了自身,因此可以将cr3映射到虚拟地址后,通过遍历来获取自身。

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
#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);
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表头
{
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;
}

image-20230525185152023

有了PML4基址,其他基址就可以推出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ULONG64 GetPdptBase(ULONG64 ulPml4Base)
{
return (ulPml4Base >> 21) << 21;
}

ULONG64 GetPdBase(ULONG64 ulPml4Base)
{
return (ulPml4Base >> 30) << 30;
}

ULONG64 GetPtBase(ULONG64 ulPml4Base)
{
return (ulPml4Base >> 39) << 39;
}

image-20230525191636201

参考

[原创]四级分页下的页表自映射与基址随机化原理介绍https://bbs.kanxue.com/thread-274152.htm

[关于WIndows内核自映射方案的通俗解释]]https://www.cnblogs.com/SivilTaram/p/WindowsKernelMapping.html