x64 分页管理(二)页表自映射
😄
1 遍历页表
1.1 使用物理地址遍历页表
有 2 种方法使用物理地址来遍历页表结构:
使用如下图方法。
![68.png]()
使用 CR3 逐级遍历。具体过程见上一篇文章 2.5 章节。
1.2 使用虚拟地址遍历页表
实际在代码中,由于分页模式的存在,我们无法直接操作物理地址。但是 Windows 会将对应的页表结构映射到虚拟地址中,如 x86 下将PTE 页表基址映射到 0xC0000000,将 PDE 映射到 0xC0300000。Drunkmars 在文章《x64下隐藏可执行内存》指出在 Windows 7 x64 PTE_BASE = fffff68000000000 固定映射,但是从 Windows 10 1607 开始页表采用随机地址映射。
这一块主要涉及两种获取 PTE_BASE 的方法和解析 PTE 的方式:
获取 PTE_BASE 两种常用方法:
- 动态获取
PTE_BASE对应的虚拟地址,可以通过特征码搜索。 - 利用页表自映射(Self-Referencing Page Tables)首先获取 PLM4T 的虚拟基地址,然后再获取
PTE_BASE。
解析 PTE 的方式的方式:
PTE 表在物理内存中是一块 4KB 大小的物理页,但是所有物理页面的 PTE 映射到虚拟地址后,这些虚拟地址都是连续的。这就有一个比较有趣的现象,表示 PTE 的虚拟地址是连续的(不止4KB)。
示例1:
先拿到一个虚拟地址,地址
0x0。1
2
3
4
5
6
7
8
9
10
11
12
13
14kd> !process 0 0 notepad.exe
PROCESS ffff9386ffaa4080
SessionId: 1 Cid: 0858 Peb: 555dab6000 ParentCid: 0c5c
DirBase: 3cb63002 ObjectTable: ffffe68e661f4d80 HandleCount: 239.
Image: notepad.exe
kd> .process /i ffff9386ffaa4080
kd> !pte 0
VA 0000000000000000
PXE at FFFF8FC7E3F1F000 PPE at FFFF8FC7E3E00000 PDE at FFFF8FC7C0000000 PTE at FFFF8F8000000000
contains 8A0000001FE61867 contains 0000000000000000
pfn 1fe61 ---DA--UW-V contains 0000000000000000
not valid可以看到,线性地址
0x0对应的 PTE 为0xFFFF8F8000000000。解析地址
0x20DEAC52A40对应的 PTE。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16kd> dq FFFF8F8000000000+((0x20D`EAC52A40>>0xC)<<3)
ffff8f81`06f56290 81000000`3d28a867 00002000`45bb6880
ffff8f81`06f562a0 81000000`6cb79847 00002000`1bd78880
ffff8f81`06f562b0 81000000`24545805 0005a817`00002094
kd> !vtop 3cb63000 0x20DEAC52A40
Amd64VtoP: Virt 0000020deac52a40, pagedir 000000003cb63000
Amd64VtoP: PML4E 000000003cb63020
Amd64VtoP: PDPE 0000000035a051b8
Amd64VtoP: PDE 0000000025786ab0
Amd64VtoP: PTE 0000000070e29290
Amd64VtoP: Mapped phys 000000003d28aa40
Virtual address 20deac52a40 translates to physical address 3d28aa40.
kd> !du 3d28aa40
#3d28aa40 "20221110"可以看到地址
0x20DEAC52A40对应的 PTE 为ffff8f81'06f56290。
从上面这个示例可以总结几点:
为什么选定
0x0的 PTE 作为转换其他线性地址对应的基础地址,也就是PTE_BASE?是因为在线性地址中,所有的 PTE 都是从0x0对应的 PTE 进行线性排列的。- 先右移 12 位,目的就是拿到 PTi 索引。每一项为 8 字节,所以还要左移 3 位。
- 然后加上基地址
FFFF8F8000000000即可得到目标地址对应的 PTE。
![76.png]()
计算 PTE 的公式:
PTE = PTE_BASE + PLM4i*512GB + PDPTi*1GB + PDi*2MB + PTi*8,实际上右移 12 位后低 9 位就是 PTi,左移 3 位就是*8,只不过使用移位更方便一些。
2 页表自映射
微软管理页表时,使用了页表自映射的技术(Self-Referencing Page Tables,或 Self-ref entry)。
其核心就是:在某一级的页表中存在一个条目,该条目指向当前级页表基址。
- 32-bit paging、PAE 分页下,该条目位于 PDE 中。
- 4-level 分页下,该条目位于 PLM4T 中。
注意:Cr3 寄存器中存储了最高级页表(PML4)的表基物理地址。
在 4-level 分页下:
该自映射的条目位于 PLM4T 中,同时该条目指向 PLM4T 表。因为 CR3 也指向 PLM4T 表,所以:这个 PLM4E entry 保存的物理地址 = CR3,这种自映射也叫做最高级页表自映射。
参考以下文章,对 Microsoft 的页表自映射进行分析:
- 四级分页下的页表自映射与基址随机化原理介绍
- 逆向TesSafe.sys有感:鹅厂是如何定位随机化的PTE_BASE
- Exploiting CVE-2018-1038 - Total Meltdown
- Getting Physical: Extreme abuse of Intel based Paging Systems - Part 2 - Windows
2.1 推导自映射页表条目
假设 PML4 表中索引位置 0x100 的元素用于内存管理且满足自映射,那么此时用于内存管理的虚拟地址空间为:
1 | 0xFFFF8000`00000000 ~ 0xFFFF807F`FFFFF000 |
查询流程:
1 | // 起始地址 |
推指向 PLM4T 表的虚拟基址。假设在 PLM4T 中,这个自映射页表条目的虚拟地址对应的 4 级分页索引分别为:$x,y,z,r$。页表解析规则:
1
2
3
4![CR3 + x * 8] = PDPTE = Cr3
![PDPTE + y * 8] = PDE
![PDE + z * 8] = PTE
![PTE + r * 8] = PLM4T基址 = Cr3当且仅当 $x = y = z = r$ 时,满足上述条件。所以指向 PLM4T 表基址的虚拟地址的特点为:PML4i == PDTi == PDi == PTi == Index。
推导指向 PDPT 表的虚拟基址。假设这个虚拟地址对应的 4 级分页索引分别为:$a,b,c,d$。
1
2
3
4![CR3 + a * 8] = PDPTE = Cr3
![PDPTE + b * 8] = PDE
![PDE + c * 8] = PTE
![PTE + d * 8] = X注意
![PTE + d * 8] != Cr3,不再指向 PLM4T 表,而是d = 0。
经过最后的推导,所有指向每级页表的虚拟地址有如下特征:
- PML4 表的虚拟地址:PML4i == PDTi == PDi == PTi == Index,页内偏移为0。
- PDPT 表的虚拟地址:PML4i == PDTi == PDi == Index && PTi == 0,页内偏移为0。
- PDT 表的虚拟地址:PML4i == PDTi == Index && PDi == 0 && PTi == 0,页内偏移为0。
- PTT 表的虚拟地址:PML4i == Index && PDTi == 0 && PDi == 0 && PTi == 0,页内偏移为0。
这个 Index 就是自映射表项在 PML4 表中的索引。
所以只要得到 PLM4T 表的虚拟地址,就得到了Index,就可以推导PDPTE、PDT、PTT:
1 | PDPT_BASE = (PLM4T_BASE >> (9+12)) << 21 PML4i == PDTi == PDi == Index && PTi == 0; |
使用PLM4T_BASE推导PTE_BASE
利用页表自映射的一些结论来获取 PML4 表基址:
PML4 表基址的内容为 Cr3 的值,并且位于 PML4 表所在的页面内。因为 Cr3 里保存了 PML4 的表基物理地址,所以可以通过映射 Cr3 物理地址的虚拟地址(该地址指向PLM4T),遍历这个虚拟地址页面的 512 个 8 字节的地址,哪个地址符合上述条件,哪个地址就是 PML4 表基址。下面给出驱动代码实现:
1 | ULONG64 GetPml4Base() |
注意:虽然一开始就得到了Cr3映射的虚拟地址 VirtualAddress_1,该地址虽然指向 PLM4T 表,但不是自映射条目的虚拟地址。 循环中找到的虚拟地址 VirtualAddress_2 也指向PLM4T表,此时的虚拟地址才是自映射条目的虚拟地址。即数值上:
VirtualAddress_1 != VirtualAddress_2,但是两个虚拟地址指向的内容相等,都是 PLM4T 表的基址。
得到 PLM4T 表的虚拟地址,就得到了Index,就可以推导PDPTE、PDT、PTT:
1 | PDPT_BASE = (PLM4T_BASE >> (9+12)) << 21 PML4i == PDTi == PDi == Index && PTi == 0; |
如上面实验的 0x0 地址,其 4 级页表的虚拟地址满足上述的特征:
1 | kd> !pte 0 |
2.2 推导PTE公式
为什么推导 PTE 虚拟地址要使用 PTE = PTE_BASE + (PLM4i*2^(9*3) + PDPTi*2^(9*2) + PDi*2^(9) + PTi)*8 ?
是因为 $PLM4i*2^{9*3} + PDPTi*2^{9*2} + PDi*2^{9} + PTi$ 是基于 PTE_BASE 的偏移量,是一个偏移量。
同理:
1 | PTE = PTE_BASE + (PLM4i*2^(9*3) + PDPTi*2^(9*2) + PDi*2^(9) + PTi)*8 |
即:
1 | PLM4E = ((VA & 0x0000FFFF`FFFFFFFF) >> 39) << 3 + PLM4E_BASE |
使用 !pte 0 可以得到自 0 地址的 PLM4T、PDPT、PDT、PTT 表虚拟地址基址。 同时可以根据得到的虚拟地址解析出自映射的 Index。
是因为每级页表起始虚拟地址地总都是从 0 映射的位置开始的。如地址 0 的 PTE = 0xFFFF0808'00000000开始,所以所有虚拟地址的 PTE 都是从 0xFFFF0808'00000000 开始。
2.3 推导公式总结
根据页表自映射机制,获取 PLM4T/PTT 基址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20ULONG64 GetPml4Base()
{
PHYSICAL_ADDRESS pCr3 = { 0 };
pCr3.QuadPart = __readcr3() & 0xfffffffffffff000;
PULONG64 Plm4t_Base = MmGetVirtualForPhysical(pCr3); //得到CR3映射的虚拟地址
ULONG64 index = 0;
while ((Plm4t_Base[index] & 0xFFFFFFFFF000) != pCr3.QuadPart)//注意需要屏蔽掉高16位属性,否则是错误的
{
index++;
if (index >= 512)
{
return -1;
}
}
// ULONG64 Pte_Base = 0;
// Pte_Base = ((index + 0x1fffe00) << 39);
// return Pte_Base;
return ((ULONG64)Plm4t_Base & 0xFFFFFFFFFFFFF000);
}根据 PLM4T_BASE、页表自映射原理,推导各级页表基址。
1
2
3PDPT_BASE = (PLM4T_BASE >> (9+12)) << 21 PML4i == PDPi == PDi == Index && PTi == 0;
PDT_BASE = (PLM4T_BASE >> (9*2+12)) << 30 PML4i == PDPi == Index && PDi == 0 && PTi == 0;
PTT_BASE = (PLM4T_BASE >> (9*3+12) << 39) PML4i == Index && PDPi == 0 && PDi == 0 && PTi == 0;根据 VA、各级页表基址,推导该 VA 对应的页表条目。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
参考《处理器虚拟化技术 6.1.5 p422》
IA-32e 4 级分页模式下,每级页表占 1 页(4KB)大小,每个页表条目为 8 bytes,则每级页表共有 512 个条目。
1、每个 PLM4E 管理 512-GByte 物理内存,[bits 47:39] 是 512-GByte Page 的 Page Number。则 [bits 47:39] = PLM4i = PageFrameNumber。
2、每个 PDPTE 管理 1-GByte 物理内存,[bits 47:30] 是 1-GByte Page 的 Page Number。
也就是将物理内存看成是 1-GByte 分页。则 [bits 47:30] = PDPi = PageFrameNumber。
3、每个 PDE 管理 2-MByte 物理内存,[bits 47:21] 是 2-MByte Page的 Page Number。
也就是将物理内存看成是 2-MByte 分页。则 [bits 47:21] = PDi = PageFrameNumber。
4、每个 PTE 管理 4-KByte 物理内存,[bits 47:12] 是 4-KByte Page的 Page Number。
也就是将物理内存看成是 4-KByte 分页。则 [bits 47:12] = PTi = PageFrameNumber。
*/
PLM4E = PLM4E_BASE + ((VA & 0x0000FFFF`FFFFFFFF) >> 39) << 3;
PDPTE = PDPTE_BASE + ((VA & 0x0000FFFF`FFFFFFFF) >> 30) << 3;
PDE = PDE_BASE + ((VA & 0x0000FFFF`FFFFFFFF) >> 21) << 3;
PTE = PTE_BASE + ((VA & 0x0000FFFF`FFFFFFFF) >> 12) << 3;注意:
1
PTE = PTE_BASE + ((VA & 0x0000FFFF`FFFFFFFF) >> 12) << 3;
这里的
PTE_BASE是虚拟地址,不是物理地址,该公式等价于如下:1
Phy(PTE) = Phy(PTE_BASE) + ((VA >> 12) & 0x1FF) << 3;
根据 PTE 反推 PDE、PDPTE、PLM4E。
另外一个疑问,已知一个地址的 PTE(虚拟地址),如何得到该虚拟地址对应的 PDE、PDPTE、PLM4E 对应的虚拟地址。
答案:只需要将当前得到的 PTE 虚拟地址当做一个普通地址,然后解析该地址的 PTE。因为新的 PTE 指向的物理地址保存的内容就是原来的 PTE。
原理:先获取虚拟地址 VA 的 PTE ,该 PTE 指向该地址。如果将这个 PTE 当成是一个普通地址 VAx,通过这个 PTE 得到的 PTEx,即为这个 PTE 的 PDE。
1
2
3
4
5
6
7// 已知 PTE_BASE
// 已知公式:PTE = PTE_BASE + ((VA & 0x0000FFFF`FFFFFFFF) >> 12) << 3;
// 有如下关系:
PTE = PTE_BASE + ((VA & 0x0000FFFF`FFFFFFFF) >> 12) << 3;
PDE = PTE_BASE + (PTE & 0x0000FFFF`FFFFFFFF >> 0xC) << 3;
PDPTE = PTE_BASE + (PDE & 0x0000FFFF`FFFFFFFF >> 0xC) << 3;
PLM4E = PTE_BASE + (PDPTE & 0x0000FFFF`FFFFFFFF >> 0xC) << 3;举例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// VA = 0xFFFF020D`EAC52A40
// PTE_BASE = FFFF8F8000000000, 求得 PTE = 0xFFFF8f81`06f56290
kd> dq FFFF8F8000000000+((0x20D`EAC52A40>>0xC)<<3)
ffff8f81`06f56290 81000000`3d28a867 00002000`45bb6880
ffff8f81`06f562a0 81000000`6cb79847 00002000`1bd78880
ffff8f81`06f562b0 81000000`24545805 0005a817`00002094
// 将 PTE(0xFFFF8f81`06f56290) 当成一个 VA,然后求这个 VA 的 PTE
// 求得 0xffff8fc7`c0837ab0 即为这个 VA 的 PTEx
// 即:VA = 0xFFFF020D`EAC52A40, PTE = 0xFFFF8f81`06f56290, PDE = 0xffff8fc7`c0837ab0
// 然后还可以利用该 PDE 继续求得 PDPTE、PLM4E。
kd> dq FFFF8F8000000000+((0x8f8106f56290>>0xC)<<3)
ffff8fc7`c0837ab0 0a000000`70e29867 0a000000`182ba867
ffff8fc7`c0837ac0 0a000000`13925867 00000000`00000000
ffff8fc7`c0837ad0 00000000`00000000 00000000`00000000
3 分析 MmIsAddressValidEx
该函数对输入的参数 VirtualAddress 进行校验是否是一个有效的线性地址。该函数中给出了PTE_BASE 后推导该虚拟地址 PDE_BASE、PDPTE_BASE、PLM4E_BASE 的方法:
1 | PTE = ((VA & 0x0000FFFF`FFFFFFFF) >> 12) << 3 + PTE_BASE |
pCmpArr = FFFFA8542A150000 pCr3.QuadPart = 1AD000
1 | .text:00000001402EDF10 // =============== S U B R O U T I N E ======================================= |
3.1 获取PLM4E
1 |
|
输出如下:
1 | MmIsAddressValid_Addr = FFFFF80066F44CC0 |
查看一下地址 FFFFF80066F44CC0 是否是函数 MmIsAddressValid:
1 | kd> u FFFFF80066F44CC0 |

