手把手教你用C语言实现Windows页表操作:从获取PTE到拷贝物理页的完整流程

张开发
2026/4/21 9:49:26 15 分钟阅读

分享文章

手把手教你用C语言实现Windows页表操作:从获取PTE到拷贝物理页的完整流程
深入探索x64 Windows页表操作从虚拟地址到物理页拷贝的完整实践指南在操作系统底层开发中内存管理是最核心也是最复杂的部分之一。理解x64架构下的分页机制掌握直接操作页表的技术是每一位追求技术深度的开发者必备的技能。本文将带你从CPU的MMU单元开始逐步深入Windows内存管理的核心机制最终实现通过代码直接操作页表、拷贝物理页的完整流程。1. x64分页机制基础解析现代x64处理器采用四级页表结构PXE-PPE-PDE-PTE来管理虚拟地址到物理地址的转换。这种分层设计既解决了64位地址空间的管理问题又通过多级索引提高了查找效率。1.1 虚拟地址结构分解x64架构下虽然理论上是64位地址空间但实际上目前只使用48位地址256TB的虚拟地址空间。一个标准的虚拟地址按照9-9-9-9-12的结构被分解为63-48位符号扩展位全0或全1 47-39位PXEPage Table Level 4索引 38-30位PPEPage Table Level 3索引 29-21位PDEPage Table Level 2索引 20-12位PTEPage Table Level 1索引 11-0位页内偏移量这种结构意味着每个页表项PTE/PDE/PPE/PXE占用8字节每页4KB大小可容纳512个页表项4096/8因此每个索引字段都是9位宽2^95121.2 CR3寄存器与页表基址CR3寄存器在x64下称为Page Directory Pointer Table Register存储着当前进程的顶级页表PXT的物理地址。这是整个地址转换过程的起点UINT64 GetCR3() { return __readcr3(); }在Windows内核中系统维护了一个全局的页表基址可以通过特定方式获取UINT64 GetPTEBase() { PUCHAR BaseAddr (PUCHAR)MmGetVirtualForPhysical; return *(PUINT64)(BaseAddr 0x22); }这个基址非常重要因为它允许我们将物理页表项转换为内核虚拟地址从而可以直接读写页表内容。2. 页表遍历与地址转换理解了基础结构后我们需要实现从虚拟地址到各级页表项的转换函数。这是操作页表的核心能力。2.1 获取各级页表项地址给定一个虚拟地址和页表基址我们可以计算出对应的PTE、PDE、PPE和PXE的虚拟地址UINT64 GetXXXAddress(UINT64 VirtualAddress, UINT64 PTEBase) { return ((VirtualAddress 0x0000FFFFFFFFF000) 12) * 8 PTEBase; }这个函数的原理是屏蔽掉页内偏移低12位右移12位得到页帧号乘以8每个页表项8字节得到页表项偏移加上页表基址得到完整虚拟地址2.2 完整的地址转换流程一个完整的虚拟到物理地址转换过程如下从CR3获取PXT物理地址使用PXE索引在PXT中找到PPE物理地址使用PPE索引在PPE中找到PDE物理地址使用PDE索引在PDE中找到PTE物理地址使用PTE索引在PTE中找到物理页地址加上页内偏移得到最终物理地址这个过程可以通过Windbg验证!pte 0xfffff80001234567 // 查看指定虚拟地址的页表项 !vtop cr3_value va_value // 手动转换虚拟地址到物理地址3. 物理页操作技术直接操作物理页是内存管理的高级技术需要特别小心以避免系统不稳定。3.1 物理页分配与释放Windows内核提供了专门的函数来分配连续的物理内存PVOID AllocatePhysicalPage() { PHYSICAL_ADDRESS Low { 0 }; PHYSICAL_ADDRESS High { MAXULONG64 }; PHYSICAL_ADDRESS Boundary { 0 }; return MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, Low, High, Boundary, MmCached); } VOID FreePhysicalPage(PVOID Page) { MmFreeContiguousMemory(Page); }3.2 物理页拷贝实现拷贝物理页的关键在于临时修改页表项使我们可以访问源物理页的内容VOID CopyPhysicalPage(PVOID DestPage, UINT64 SourcePagePTE) { // 分配临时缓冲区 PVOID TempPage AllocatePhysicalPage(); // 获取临时页的PTE地址 UINT64 PTEAddress GetXXXAddress((UINT64)TempPage, GetPTEBase()); // 保存原始PTE UINT64 OldPTE *(PUINT64)PTEAddress; // 修改PTE指向源物理页 *(PULONG64)PTEAddress SourcePagePTE; // 执行拷贝 RtlCopyMemory(DestPage, TempPage, PAGE_SIZE); // 恢复原始PTE *(PUINT64)PTEAddress OldPTE; // 释放临时页 FreePhysicalPage(TempPage); }这个过程展示了如何通过临时重映射来访问特定的物理内存是许多高级内存操作的基础。4. 页表隔离与Hook技术通过操作页表实现函数Hook而不影响其他进程是一种精细化的Hook技术。4.1 页表隔离原理页表隔离的核心思想是拷贝原始函数所在物理页拷贝相关的PTE/PDE/PPE/PXE页构建独立的页表结构修改目标进程的CR3指向新的页表这样目标进程访问该函数时会使用独立的物理页而其他进程继续使用原始页。4.2 关键数据结构我们需要一个结构体来管理所有相关的页typedef enum { PhyPage, // 函数所在物理页 PT, // PTE页 PDT, // PDE页 PPT, // PPE页 PXT // PXE页 } PAGE_TYPE; typedef struct _PAGE_INFO { UINT64 PteBase; // 页表基址 UINT32 PXEIndex; // PXE索引 UINT64 Pxe; // 原始PXE值 PVOID pPageArray[5]; // 各页的虚拟地址 } PAGE_INFO, *PPAGE_INFO;4.3 初始化Hook页初始化过程包括分配所有需要的页并建立正确的链接关系BOOLEAN InitHookPage(UINT64 VirtualAddress, PPAGE_INFO pPageInfo, CHAR ShellCode[], UINT32 Length) { // 解析虚拟地址各字段 pPageInfo-PXEIndex (VirtualAddress 0x27) 0x1FF; UINT32 PPEIndex (VirtualAddress 0x1E) 0x1FF; UINT32 PDEIndex (VirtualAddress 0x15) 0x1FF; UINT32 PTEIndex (VirtualAddress 0xC) 0x1FF; UINT32 FuncOffset VirtualAddress 0xFFF; // 获取页表基址和各层级页表项 pPageInfo-PteBase GetPTEBase(); UINT64 PteAddr GetXXXAddress(VirtualAddress, pPageInfo-PteBase); UINT64 PdeAddr GetXXXAddress(PteAddr, pPageInfo-PteBase); UINT64 PpeAddr GetXXXAddress(PdeAddr, pPageInfo-PteBase); UINT64 PxeAddr GetXXXAddress(PpeAddr, pPageInfo-PteBase); // 保存原始页表项值 UINT64 Pte *(PUINT64)PteAddr; UINT64 Pde *(PUINT64)PdeAddr; UINT64 Ppe *(PUINT64)PpeAddr; pPageInfo-Pxe *(PUINT64)PxeAddr; // 分配所有需要的物理页 for (int i 0; i 5; i) { pPageInfo-pPageArray[i] AllocatePhysicalPage(); if (!pPageInfo-pPageArray[i]) return FALSE; RtlZeroMemory(pPageInfo-pPageArray[i], PAGE_SIZE); } // 拷贝原始页内容 CopyPhysicalPage(pPageInfo-pPageArray[PhyPage], Pte); CopyPhysicalPage(pPageInfo-pPageArray[PT], Pde); CopyPhysicalPage(pPageInfo-pPageArray[PDT], Ppe); CopyPhysicalPage(pPageInfo-pPageArray[PPT], pPageInfo-Pxe); // 建立页表层级关系 LinkPhysicalPages(pPageInfo-pPageArray[PhyPage], pPageInfo-pPageArray[PT], PTEIndex, Pte); LinkPhysicalPages(pPageInfo-pPageArray[PT], pPageInfo-pPageArray[PDT], PDEIndex, Pde); LinkPhysicalPages(pPageInfo-pPageArray[PDT], pPageInfo-pPageArray[PPT], PPEIndex, Ppe); // 应用Hook代码 if (ShellCode Length 0) { for (UINT32 i 0; i Length; i) { *(PCHAR)((UINT64)pPageInfo-pPageArray[PhyPage] FuncOffset i) ShellCode[i]; } } return TRUE; }4.4 应用Hook到目标进程最后一步是修改目标进程的页表使其指向我们创建的独立页表结构VOID SetHookPage(UINT64 DirectoryTableBase, PAGE_INFO PageInfo) { // 修改目标进程的CR3指向我们的PXT页 DirectoryTableBase DirectoryTableBase (~0xFFF) | 0x063; UINT64 PtePXTAddress GetXXXAddress((UINT64)PageInfo.pPageArray[PXT], PageInfo.PteBase); UINT64 PtePXT *(PUINT64)PtePXTAddress; *(PUINT64)PtePXTAddress DirectoryTableBase; LinkPhysicalPages(PageInfo.pPageArray[PPT], PageInfo.pPageArray[PXT], PageInfo.PXEIndex, PageInfo.Pxe); *(PUINT64)PtePXTAddress PtePXT; }在实际应用中通常会在进程创建回调PsSetCreateProcessNotifyRoutine中检测特定进程并应用Hook。

更多文章