Windows内存机制
1 基本原理
Cr3寄存器
描述:
在所有的寄存器中,只有Cr3存储的是物理地址,其它寄存器存的都是线性地址
Cr3所存储的物理地址指向了一个页目录表(PDT-Page Directory Table)
在Windows中,一个页的大小通常为4KB,即一个页可以存储1024个页目录表项(PDE)
PD/PDE(Page Directory Table/Entry 页目录表/项)
描述:
页目录表(PDT)的每一项元素称为页目录表项(PDE)
每个页目录表项指向一个页表(PTT)
每个页表的大小为4KB,即一个页表可以存储1024个页表项(PTE)
PT/PTE(Page Table/Entry 页表/项)
描述:
页表(PTT)的每一个元素称为页表项(PTE)
页表项(PTE)所指向的才是真正的物理页
PDP (Page Directory Pointer)
PAE中增加了Page Directory Pointer Table这一级:
PAE
PAE 就是物理地址扩展。我们常规的寻址方式是之前的将虚拟地址化为10 10 12的方式来寻址页目录,页表,页偏移,但是在开始PAE之后的寻址方式发生了改变,将32位的虚拟地址转化成 2 9 9 12的方式来寻址。
高2位指向Page Directory Pointer Table
次高9位指向页目录 共512项 每项64位8字节,4K(正好一页,与未启用PAE一致)
次次高9位指向页表 共512项 每项64位8字节,4K(正好一页,与未启用PAE一致)
低12位作4K的页内偏移。
2 X64特性
PML4
x64的分页使用4个级别的表,将物理页面映射到虚拟页面,它们分别是PML4(也就是PXE)、PDPT、PD和PT。
控制寄存器CR3包含当前进程PML4表的(物理)内存基地址。
示例-虚拟地址转化为物理地址
图为x64系统中,从虚拟地址到物理地址遍历过程的概览
在这里,假设我们需要遍历虚拟地址0x71000000000的分页表。
首先,我们对其进行分解:
地址结构
虚拟地址最低的12位是页偏移量,接下来的9位是PT索引,后面的9位是PD索引,再然后的9位是PDPT索引,最后的9位是PML4索引。这里可以看到64位的电脑,实际地址为48位。
我会使用下面的结构来完成这项工作:
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)
针对这个例子(VA:0x71000000000),我们得到了PML4_index = 0x0E,PDPT_index = 0x40,PD_index = 0,PT_index = 0,Offset = 0。
现在,我们就得到了虚拟地址的PML4条目:38a0000040653867
这实际上是一个被称为MMPTE的8字节结构,我们需要从中提取页帧号(Page Frame Number):
现在我们将PFN(0x40653),乘以页大小(0x1000),其得到的结果(0x40653000)作为下一个分页表(PDPT)的基地址。并且,PDPT_index(0x40)将作为该分页表的索引:
然后,再将下一个PFN(0x41cd7),乘以页大小(0x1000),其得到的结果(0x41cd7000)作为下一个分页表(PD)的基地址。并且,PD_index(0x00)将作为该分页表的索引:
同样,对于最后一级的PFN(0x3e7d8),乘以页大小(0x1000),其得到的结果(0x3e7d8000)作为下一个分页表(PT)的基地址。并且PT_index(0x00)作为该表的索引:
最后剩下需要做的,就是把这个PFN(0x3d7d9)乘以页大小(0x1000),再加上page_offset(0x000)。
现在,我们就知道了,虚拟地址0x71000000000实际上是映射物理地址0x3d7d9000。
漏洞
漏洞-自引用
X64由于48位物理地址的限制,实际上有效的虚拟地址属于下面这两个范围:0至7FFF’FFFFFFFF(512GB256)或FFFF8000’00000000至FFFFFFFF’FFFFFFFF(512GB256)。微软采取了一种称为self-ref entry(自引用条目)的技术,在最高级别的页表中有一个条目指向自己。在64位系统中,任意自引用条目使用的物理地址应该指向PML4基地址的物理地址,与CR3寄存器所指向的地址相同。
例如,如果我们在PML4表中创建一个索引0x100处的新条目,并且该条目指向PML4表的物理地址,那么我们就有所谓的“自引用条目”。
那么为什么会有人这样做呢?这实际上给了我们一组虚拟地址,我们可以在虚拟地址空间中引用和修改任何页表。
例如,如果我们想要为我们的过程修改PML4表,我们可以简单地引用虚拟地址0x804020100000,该虚拟地址转换为:
0b 1000 0000 0 — 100 0000 00 — 10 0000 000 — 1 0000 0000 — 0000 0000 0000
- PML4索引0x100 - PML4的物理地址
- PDPT索引0x100 - 同样是PML4的物理地址
- PD索引0x100 - 同样是PML4的物理地址
- PT索引0x100 - 再次… PML4的物理地址
它最终会返回PML4的内存。
修补
微软已经在Anniversary Update中针对该方法推出了缓解机制。这并不意味着Windows 10不再依赖自引用,Windows只是随机化处理了PML4中自引用条目的索引,这样最终导致PML4及PET虚拟地址的随机化。