由于微软需要管理内存,内存由cr3维护,但r3又不能访问物理内存,因此微软设计了一种页表基址的基址;x64下分有pml4、pdpt、pde、pte四个表,每个表的表头地址用一个固定的虚拟地址进行保存,这个地址就被成为页表基址
页表自映射原理
使用windbg命令!pte查看0地址数据。
这里的pxe、ppe、pde、pte地址为虚拟地址(pxe和ppe下边统称为pml4、pdpt),同时也被称为对应项的页表基址。
对pml4、pdpt、pde、pte四项的页表基址进行拆分得到如下(去除页内偏移和高16位):
Text1 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; 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++) { 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位,继续往下分析。
再继续一次相同的计算,式子有点长这里直接展开:
Text1 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