Windows x64内核学习笔记(四)—— 9-9-9-9-12分页
前言
在学习VT虚拟化技术的EPT物理地址转换时,我们简单提到过9-9-9-9-12分页的概念,即支持48位物理地址转换。
9-9-9-9-12分页
描述:随着计算机技术的发展,64位系统逐渐占据主流地位,那么也就表示CPU的最大寻址范围为64位。但实际上,CPU只使用了其中的48位用于寻址,并使用9-9-9-9-12分页模式。即便如此,在未来较长一段时间里,48位寻址范围也足够大部分人的日常使用了。
9-9-9-9-12分页表示物理地址拥有四级页表,在Intel开发手册中,将这四级页表分别称为PML4E、PDPTE、PDE、PTE;但微软的命名方式略有不同,将这四级页表分别称为PXE、PPE、PDE、PTE,WinDbg中也是如此。
由于之后的大部分操作都是使用WinDbg完成的,因此我们在之后的笔记中都使用微软对四级页表的命名方式。
实验一:线性地址转物理地址
第一步:打开记事本
启动一个64位的notepad.exe,写入字符串“Hello World”。
第二步:定位线性地址
使用CE定位字符串“Hello World”的线性地址。
可以看到这里搜出了很多线性地址,怎么确定是哪一个呢?
可以在字符串后面再写入一些其他的字符,看看哪些线性地址的值会发生变化。
只剩下两个了,MSCTF.dll是微软用于提供文本服务的dll,那么真正的线性地址能确定是21BABF52920了。
第三步:拆分线性地址
将线性地址的二进制位拆分成9-9-9-9-12五组,可以借助WinDbg或计算器得到二进制。
拆分结果如下:
0 0000 0100 //004
0 0110 1110 //06e
1 0101 1111 //15f
1 0101 0010 //152
1001 0010 0000 //920
第四步:定位页表基址
首先我们需要通过notepad.exe进程的Cr3得到物理页的基址,即第一个PXE的地址。
第五步:定位PXE
注意:高位的0a00
和低位的867
是属性位,不包含在物理地址中。
第六步:定位PPE
第七步:定位PDE
第八步:定位PTE
第九步:定位物理页
成功定位到了记事本中的字符串文本,后面的"123"是之前残留的数据。
页表基址
思考:一个进程该如何访问自己的物理页呢?可以通过读取Cr3的值进行访问吗?
答案:不行,Cr3中保存的页表基址是物理地址,程序如果直接访问这个地址,虽然看上去值是一样的,但实际上访问的是一个线性地址,会被虚拟内存管理器解析成另一个地址。
实际上,操作系统会将当前进程的物理页映射在某个线性地址中,以供程序读取自己的页表内容。
在x86系统中,页表基址是固定的,位于0xC0000000,将这个线性地址进行解析,访问其物理页的内容,会发现从这个地址开始,里面保存的数据为当前程序的所有物理页地址。
而在x64系统中,页表基址不再是固定的值,而是每次系统启动后随机生成的。
可以在WinDbg中查看0地址对应的线性地址来确定当前的页表基址。
可以看到,当前系统的页表基址的线性地址为0xFFFF800000000000,注意,只有后48位才是有效地址。
其中,每个物理页占8个字节,例如,第一个物理页地址位于线性地址0xFFFF800000000000,第二个物理页地址位于线性地址0xFFFF800000000008,每个物理页中包含1024个字节的数据。
定位基址
描述:如果系统每次启动时,基址是随机分配的,那么该如何定位基址呢?其实方法有挺多的,大部分方式是通过提取特征码。
以周壑老师提供的方法为例,首先在WinDbg中定位内核模块的地址。
然后在内核模块中搜索与当前页表基址相同的值出现的位置,当前页表基址为0xFFFF800000000000
。
接着,在IDA中定位到数据所在的位置,可以看到是某行代码引用了这个值的硬编码。
在WinDbg中查看这段代码,能够识别到位于CcUnpinFileDataEx函数。
那么,由于系统每次启动时基址是不固定的,因此这些值也不可能是固定的硬编码,肯定是有“人”对这些值进行了修改,在需要使用时,可以通过固定的偏移量提取硬编码,从而得到页表基址,但要注意不同版本的内核文件的偏移量可能是不同的。
PTE to PXE
思考:如果已经得到了页表基址,即PTE的地址,那么,能否再得到PDE、PPE以及PXE的线性地址呢?
答案:可以,因为页表基址本身也是一个线性地址,也需要有人来管理,因此将页表基址本身当做一个线性地址,向右移动12位能够得到PDE基址对应的PTI,再乘以8(每个物理页地址占8个字节)就能够得到PDE基址在页表基址中的偏移量,然后将这个偏移量再加上页表基址就能得到PDE基址所在的线性地址。同理,也能够通过PDE基址得到PPE基址和PXE基址的线性地址。
计算公式:PAE = PTE_BASE + (Address >> 12) << 3;
实验二:通过页表基址定位各级页表的物理页
描述:以gdt基址为例,通过页表基址寻找保存其线性地址的各项页表的物理地址,此时页表基址为0xFFFF800000000000。
第一步:定位PTE的线性地址,并查看物理页的物理地址
第二步:定位PDE的线性地址,并查看PTE的物理地址
第三步:定位PPE的线性地址,并查看PDE的物理地址
第四步:定位PXE所在的线性地址,并查看PPE的物理地址
第五步:使用!pte
指令进行验证
代码实现
#include "ntddk.h"
ULONG64 g_NT_BASE = 0xfffff80348aa4000; // lm命令得到
ULONG64 g_PTE_BASE;
ULONG64 g_PDE_BASE;
ULONG64 g_PPE_BASE;
ULONG64 g_PXE_BASE;
//卸载函数
VOID DriverUnload(PDRIVER_OBJECT driver)
{
DbgPrint("Driver Unload .\r\n");
}
ULONG64 GetPTEAddress(PVOID address)
{
return (PULONG64)(g_PTE_BASE + ((((ULONG64)address & 0xffffffffffff) >> 12) << 3));
}
ULONG64 GetPDEAddress(PVOID address)
{
return (PULONG64)(g_PDE_BASE + ((((ULONG64)address & 0xffffffffffff) >> 21) << 3));
}
ULONG64 GetPPEAddress(PVOID address)
{
return (PULONG64)(g_PPE_BASE + ((((ULONG64)address & 0xffffffffffff) >> 30) << 3));
}
ULONG64 GetPXEAddress(PVOID address)
{
return (PULONG64)(g_PXE_BASE + ((((ULONG64)address & 0xffffffffffff) >> 39) << 3));
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint("Driver Load .\r\n");
DriverObject->DriverUnload = DriverUnload;
ULONG64 offset = 0xa4ad; // 页表基址硬编码相对于内核文件基址的偏移量
g_PTE_BASE = *(PULONG64)(g_NT_BASE + offset);
g_PDE_BASE = GetPTEAddress(g_PTE_BASE);
g_PPE_BASE = GetPTEAddress(g_PDE_BASE);
g_PXE_BASE = GetPTEAddress(g_PPE_BASE);
DbgPrint("g_PTE_BASE: %p \r\n", g_PTE_BASE);
DbgPrint("g_PDE_BASE: %p \r\n", g_PDE_BASE);
DbgPrint("g_PPE_BASE: %p \r\n", g_PPE_BASE);
DbgPrint("g_PXE_BASE: %p \r\n", g_PXE_BASE);
ULONG64 gdtr = 0xfffff8034b290fb0;
DbgPrint("gdtr - PXE: %p \r\n", GetPXEAddress(gdtr));
DbgPrint("gdtr - PPE: %p \r\n", GetPPEAddress(gdtr));
DbgPrint("gdtr - PDE: %p \r\n", GetPDEAddress(gdtr));
DbgPrint("gdtr - PTE: %p \r\n", GetPTEAddress(gdtr));
return STATUS_SUCCESS;
}
运行结果:
使用WinDbg进行验证:
kd> r gdtr
gdtr=fffff8034b290fb0
参考资料
- bilibili周壑:x64内核研究系列教程