本帖最后由 零五零八 于 2020-10-13 19:46 编辑
开门见山,本文的核心思路就是通过填充页表项,将一块连续的虚拟地址映射到新的地址,同时将需要修改的只读内存对应页表项的Dirty位置位。在Windows操作系统下,写保护是通过保护特定虚拟地址实现的,若不建立新映射,则即使将Dirty位置位,尝试写只读内存照样会触发BugCheck,若建立了新映射但不置位Dirty则触发PAGE_FAULT的BugCheck,两个步骤缺一不可。填充页表项首先需要动态定位PTEBase,国际惯例是采用大表哥的页表自映射法,代码如下:
- ULONG_PTR PTEBase = 0;
- BOOLEAN hzqstGetPTEBase()
- {
- BOOLEAN Result = FALSE;
- ULONG_PTR PXEPA = __readcr3() & 0xFFFFFFFFF000;
- PHYSICAL_ADDRESS PXEPAParam;
- PXEPAParam.QuadPart = (LONGLONG)PXEPA;
- ULONG_PTR PXEVA = (ULONG_PTR)MmGetVirtualForPhysical(PXEPAParam);
- if (PXEVA)
- {
- ULONG_PTR PXEOffset = 0;
- do
- {
- if ((*(PULONGLONG)(PXEVA + PXEOffset) & 0xFFFFFFFFF000) == PXEPA)
- {
- PTEBase = (PXEOffset + 0xFFFF000) << 36;
- Result = TRUE;
- break;
- }
- PXEOffset += 8;
- } while (PXEOffset < PAGE_SIZE);
- }
- return Result;
- }
复制代码
这里我顺便也给出另一种获取方案,基本思路是通过NT导出函数
- NTKERNELAPI ULONG NTAPI KeCapturePersistentThreadState(PCONTEXT Context, PKTHREAD Thread, ULONG BugCheckCode, ULONG_PTR BugCheckParameter1, ULONG_PTR BugCheckParameter2, ULONG_PTR BugCheckParameter3, ULONG_PTR BugCheckParameter4, PVOID VirtualAddress);
复制代码
Dump下0x40000大小的数据,里面就存放有MmPfnDataBase,MmPfnDataBase是一个以物理地址页帧号为索引的内存信息数组,其中就有物理页对应的PTE项的地址PteAddress,我们传一个有效的物理地址进去(如当前CR3:_readcr3()&0xFFFFFFFFF000)取出其中的PteAddress,由于PTEBase必定是0x8000000000的倍数,因此可由PteAddress直接算出PTEBase。另外,从Win10RS1周年预览版开始,KeCapturePersistentThreadState开始受全局变量ForceDumpDisabled的控制,若注册表“HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\CrashControl”中有关子项满足条件,此变量会在开机启动时置为1,导致KeCapturePersistentThreadState调用失败。综上所述我们得到第二种获取PTEBase的代码如下:
- //若在Win10上测试,需要在这里加上"#define _WIN10 1"
-
- #ifdef _WIN10
- #define OFFSET_PTEADDRESS 0x8
- #elif
- #define OFFSET_PTEADDRESS 0x10
- #endif
- ULONG_PTR PTEBase = 0;
-
- PBOOLEAN LocateForceDumpDisabledInRange(ULONG_PTR StartAddress, ULONG MaximumBytesToSearch)
- {
- PBOOLEAN Result = 0;
- ULONG_PTR p = StartAddress;
- ULONG_PTR pEnd = p + MaximumBytesToSearch;
- do
- {
- //cmp cs:ForceDumpDisabled, al
- //jnz ...
- if ((*(PULONGLONG)p & 0xFFFF00000000FFFF) == 0x850F000000000538)
- {
- Result = p + 6 + *(PLONG)(p + 2);
- break;
- }
- p++;
- } while (p < pEnd);
- return Result;
- }
-
- BOOLEAN GetPTEBase()
- {
- BOOLEAN Result = FALSE;
- CONTEXT Context = { 0 };
- Context.Rcx = (ULONG64)&Context;
- PUCHAR DumpData = ExAllocatePool(NonPagedPool, 0x40000);
- if (DumpData)
- {
- PBOOLEAN pForceDumpDisabled = LocateForceDumpDisabledInRange((ULONG_PTR)KeCapturePersistentThreadState, 0x300);
- if (pForceDumpDisabled) *pForceDumpDisabled = FALSE;
- if (KeCapturePersistentThreadState(&Context, 0, 0, 0, 0, 0, 0, DumpData) == 0x40000)
- {
- ULONG_PTR MmPfnDataBase = *(PULONG_PTR)(DumpData + 0x18);
- PTEBase = *(PULONG_PTR)(MmPfnDataBase + OFFSET_PTEADDRESS + (((ULONG_PTR)(__readcr3() & 0xFFFFFFFFF000) >> 8) * 3)) & 0xFFFFFF8000000000;
- Result = TRUE;
- }
- ExFreePool(DumpData);
- }
- return Result;
- }
复制代码
获取到PTEBase后,现在进入正题,下面代码的主要思路是:首先从某个有效的按512G对齐的内核虚拟地址开始,一直到0xFFFFFFFFFFFFFFFF,查找未被占用的PML4T(下文统称PXE)子项,即PXE的Valid位为0的子项。
对于有效起始内核虚拟地址,一开始笔者选用的是MmSystemRangeStart,虚拟机测试发现对于8.1/10映射成功了,而Vista/7/8下CPU并没有承认映射地址的有效性触发了BugCheck,调试器发现Vista/7/8下MmSystemRangeStart=0xFFFF080000000000,而8.1/10下MmSystemRangeStart=0xFFFF800000000000,并且Vista/7/8下映射地址范围为[0xFFFF080000000000, 0xFFFF800000000000)时,调试器的CrashDump提示为Noncanonical Virtual Address。查阅了Intel手册后,笔者发现当前的IntelCPU支持的虚拟地址寻址最大位数限制为48位,对于64位Windows来说,第47位被用来区分用户层虚拟地址和内核层虚拟地址,即内核层地址实际上只有47位的有效位,于是得出有效起始内核虚拟地址为0xFFFF800000000000。当然严谨起见,可以使用CPUID的0x80000008号功能,此时eax寄存器的ah即为处理器支持的虚拟地址最大有效位数,设其为x,则对64位地址,只要将最高的(65-x)位全部置1,剩余的(x-1)位全部置0,即得有效起始内核虚拟地址。
找到未使用的PXE子项后,申请一段连续的物理内存,初始大小为PXE子项以及PXE子项的512个PPE项所描述的页面大小,即(1 + 0x200) * PAGE_SIZE,若申请失败,则将申请的PPE项减半,以此类推……由于是连续物理内存,因此最好的方案是通过MmAllocateContiguousMemory申请,若使用ExAllocatePool,则当申请的页面不是2M大页面时,其虚拟地址对应的物理地址很可能不是连续的,这会给我们后续填充512个PPE项的物理页帧号徒增不少麻烦。申请到连续物理地址后,第一个页面填充至目标PXE子项,第2到513个页面的物理页帧号按顺序填充到PXE子项页面描述的512个PPE项中。随后给定任一个需要映射的虚拟地址,我们先将它按0x8000000000(512G)进行对齐,再依次检索其PXE、PPE、PDE项,若PXE项Valid为0或LargePage为1则不映射,否则开始依次检索PPE和PDE。若PPE项Valid为0,则把映射地址对应的PPE页面清零。若Valid为1则分别处理是否LargePage的情形,若为1G大页面,则将其等分成512个2M大页面,再把对应的物理页帧号按顺序填充到PPE描述的512个PDE项中;若不是大页面,则将被映射地址的PPE项对应的一页全部复制到映射地址对应的PPE页面中。从这里开始,基本的地址映射已经完成,接下来对欲修改的页面只要把其对应的PTE或大页面PDE或大页面PPE项的Dirty位置1即可。
- ULONG_PTR AllocateSinglePXEDirectory(OUT PULONG_PTR BaseAddress, OUT PULONG SizeOfPPEPages)
- {
- ULONG_PTR Result = 0;
- ULONG_PTR PteBase = PTEBase;
- ULONG_PTR OffsetMask = 0x7FFFFFFFF8;
- ULONG_PTR PXEVA = PteBase + (((PteBase + (((PteBase + ((PteBase >> 9) & OffsetMask)) >> 9) & OffsetMask)) >> 9) & OffsetMask) + 0x800; //有效起始内核虚拟地址0xFFFF800000000000对应的PXE子项
- ULONG_PTR PXEVAEnd = PXEVA + 0x800; //0x800 + 0x800 == 0x1000 == PAGE_SIZE
- do
- {
- if (!(*(PULONGLONG)PXEVA & 0xFFFFFFFFF001))
- {
- PHYSICAL_ADDRESS Alloc;
- Alloc.QuadPart = MAXULONG64;
- ULONG TotalSizeOfValidPPEPages = 0x200 * PAGE_SIZE;
- while (TotalSizeOfValidPPEPages >= PAGE_SIZE && !(Result = (ULONG_PTR)MmAllocateContiguousMemory(TotalSizeOfValidPPEPages + PAGE_SIZE, Alloc)))
- TotalSizeOfValidPPEPages >>= 1;
- if (Result)
- {
- if (SizeOfPPEPages) *SizeOfPPEPages = TotalSizeOfValidPPEPages;
- ULONG64 OringinalIRQL = __readcr8();
- __writecr8(DISPATCH_LEVEL);
- if (BaseAddress) *BaseAddress = ((PXEVA & 0xFF8) + 0xFFFF000) << 36;
- ULONG_PTR PTEVA = PteBase + ((Result >> 9) & OffsetMask);
- ULONG_PTR PDEVA = PteBase + ((PTEVA >> 9) & OffsetMask);
- ULONG_PTR PPEVA = PteBase + ((PDEVA >> 9) & OffsetMask);
- ULONGLONG StartValue = *(PULONGLONG)PPEVA;
- if (StartValue & 0x80)
- {
- StartValue &= ~0x80;
- StartValue += (Result & 0x3FFFF000);
- }
- else
- {
- StartValue = *(PULONGLONG)PDEVA;
- if (StartValue & 0x80)
- {
- StartValue &= ~0x80;
- StartValue += (Result & 0x1FF000);
- }
- else StartValue = *(PULONGLONG)PTEVA;
- }
- *(PULONGLONG)PXEVA = StartValue;
- ULONG PPEOffset = 0;
- ULONG PPEOffsetEnd = TotalSizeOfValidPPEPages >> 9;
- ULONG_PTR PPEBase = Result;
- do
- {
- *(PULONGLONG)(PPEBase + PPEOffset) = StartValue + ((PPEOffset + 8) << 9);
- PPEOffset += 8;
- } while (PPEOffset < PPEOffsetEnd);
- RtlZeroMemory((PVOID)(PPEBase + PPEOffset), PAGE_SIZE - PPEOffset);
- __writecr8(OringinalIRQL);
- }
- break;
- }
- PXEVA += 8;
- } while (PXEVA < PXEVAEnd);
- return Result;
- }
-
- ULONG_PTR FillPDEArrayForAllValidPPEs(ULONG_PTR PagePointer, ULONG_PTR BaseAddress, ULONG SizeOfPPEPages, ULONG_PTR VirtualAddress)
- {
- ULONG_PTR Result = 0;
- ULONG_PTR PteBase = PTEBase;
- ULONG_PTR OffsetMask = 0x7FFFFFFFF8;
- ULONG_PTR PDEVA = PteBase + (((PteBase + (((VirtualAddress & 0xFFFFFF8000000000) >> 9) & OffsetMask)) >> 9) & OffsetMask);
- ULONG_PTR PPEVA = PteBase + ((PDEVA >> 9) & OffsetMask);
- ULONG_PTR PXEVA = PteBase + ((PPEVA >> 9) & OffsetMask);
- if ((*(PUCHAR)PXEVA & 0x81) == 1) //不支持512G超大页面
- {
- if (SizeOfPPEPages >= PAGE_SIZE)
- {
- ULONG_PTR NewPDEVA = PagePointer + PAGE_SIZE;
- ULONG_PTR NewPDEVAEnd = NewPDEVA + SizeOfPPEPages;
- ULONG64 OringinalIRQL = __readcr8();
- __writecr8(DISPATCH_LEVEL);
- do
- {
- UCHAR ByteFlag = *(PUCHAR)PPEVA;
- if (ByteFlag & 1)
- {
- if ((CHAR)ByteFlag < 0) //为了不改变原先填充的PPE项,将1G大页面等分成512个2M大页面
- {
- ULONGLONG PDEStartValue = *(PULONGLONG)PPEVA;
- ULONG PDEOffset = 0;
- do
- {
- *(PULONGLONG)(NewPDEVA + PDEOffset) = PDEStartValue + (PDEOffset << 9);
- PDEOffset += 8;
- } while (PDEOffset < PAGE_SIZE);
- }
- else memcpy((PVOID)NewPDEVA, (PVOID)PDEVA, PAGE_SIZE);
- }
- else RtlZeroMemory((PVOID)NewPDEVA, PAGE_SIZE);
- PDEVA += PAGE_SIZE;
- PPEVA += 8;
- NewPDEVA += PAGE_SIZE;
- } while (NewPDEVA < NewPDEVAEnd);
- __writecr8(OringinalIRQL);
- __writecr3(__readcr3());
- Result = BaseAddress + (VirtualAddress & 0x7FFFFFFFFF);
- }
- }
- return Result;
- }
-
- BOOLEAN MakeDirtyPage(ULONG_PTR VirtualAddress)
- {
- BOOLEAN Result = FALSE;
- ULONG_PTR PteBase = PTEBase;
- ULONG_PTR OffsetMask = 0x7FFFFFFFF8;
- ULONG_PTR PTEVA = PteBase + ((VirtualAddress >> 9) & OffsetMask);
- ULONG_PTR PDEVA = PteBase + ((PTEVA >> 9) & OffsetMask);
- ULONG_PTR PPEVA = PteBase + ((PDEVA >> 9) & OffsetMask);
- ULONG_PTR PXEVA = PteBase + ((PPEVA >> 9) & OffsetMask);
- if ((*(PUCHAR)PXEVA & 0x81) == 1) //不支持512G超大页面
- {
- UCHAR ByteFlag = *(PUCHAR)PPEVA;
- if (ByteFlag & 1)
- {
- if ((CHAR)ByteFlag < 0)
- {
- *(PUCHAR)PPEVA |= 0x42; //Dirty1 & Dirty
- Result = TRUE;
- }
- else
- {
- ByteFlag = *(PUCHAR)PDEVA;
- if (ByteFlag & 1)
- {
- if ((CHAR)ByteFlag < 0)
- {
- *(PUCHAR)PDEVA |= 0x42;
- Result = TRUE;
- }
- else
- {
- if (_bittest((PLONG)PTEVA, 0))
- {
- *(PUCHAR)PTEVA |= 0x42;
- Result = TRUE;
- }
- }
- }
- }
- }
- }
- __invlpg((PVOID)VirtualAddress);
- return Result;
- }
-
- void FreeSinglePXEDirectory(ULONG_PTR PagePointer, ULONG_PTR BaseAddress)
- {
- ULONG_PTR PteBase = PTEBase;
- ULONG_PTR OffsetMask = 0x7FFFFFFFF8;
- *(PULONGLONG)(PteBase + (((PteBase + (((PteBase + (((PteBase + ((BaseAddress >> 9) & OffsetMask)) >> 9) & OffsetMask)) >> 9) & OffsetMask)) >> 9) & OffsetMask)) = 0;
- MmFreeContiguousMemory((PVOID)PagePointer);
- __writecr3(__readcr3());
- }
复制代码
在Win10 18362.207 x64上的测试代码与结果:
- NTKERNELAPI NTSTATUS ObReferenceObjectByName(IN PUNICODE_STRING ObjectName, IN ULONG Attributes, IN PACCESS_STATE PassedAccessState OPTIONAL, IN ACCESS_MASK DesiredAccess OPTIONAL, IN POBJECT_TYPE ObjectType, IN KPROCESSOR_MODE AccessMode, IN OUT PVOID ParseContext OPTIONAL, OUT PVOID *Object);
- extern POBJECT_TYPE *IoDriverObjectType;
-
- NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegistryString)
- {
- ULONG_PTR PagePointer = 0;
- ULONG_PTR BaseAddress = 0;
- ULONG SizeOfValidPPEPages = 0;
- if (GetPTEBase())
- {
- UNICODE_STRING UDrvName;
- PDRIVER_OBJECT DrvObj = 0;
- if (NT_SUCCESS(RtlInitUnicodeString(&UDrvName, L"\\Driver\\kbdclass")) && NT_SUCCESS(ObReferenceObjectByName(&UDrvName, OBJ_CASE_INSENSITIVE, 0, 0, *IoDriverObjectType, KernelMode, 0, (PVOID*)&DrvObj)))
- {
- ObDereferenceObject(DrvObj);
- PagePointer = AllocateSinglePXEDirectory(&BaseAddress, &SizeOfValidPPEPages);
- ULONG_PTR MappedKbdClassBase = FillPDEArrayForAllValidPPEs(PagePointer, BaseAddress, SizeOfValidPPEPages, (ULONG_PTR)DrvObj->DriverStart);
- if (MakeDirtyPage(MappedKbdClassBase)) *(PULONG)(MappedKbdClassBase + 4) = 0x78563412;
- FreeSinglePXEDirectory(PagePointer, BaseAddress);
- }
- }
- return STATUS_UNSUCCESSFUL;
- }
复制代码
另外附上Win10的_MMPTE_HARDWARE以供参考
- typedef struct _MMPTE_HARDWARE
- {
- ULONGLONG Valid : 1;
- ULONGLONG Dirty1 : 1;
- ULONGLONG Owner : 1;
- ULONGLONG WriteThrough : 1;
- ULONGLONG CacheDisable : 1;
- ULONGLONG Accessed : 1;
- ULONGLONG Dirty : 1;
- ULONGLONG LargePage : 1;
- ULONGLONG Global : 1;
- ULONGLONG CopyOnWrite : 1;
- ULONGLONG Unused : 1;
- ULONGLONG Write : 1;
- ULONGLONG PageFrameNumber : 36; //Vista SP0为28,Vista SP1--10通用36
- ULONGLONG ReservedForHardware : 4;
- ULONGLONG ReservedForSoftware : 4;
- ULONGLONG WsleAge : 4;
- ULONGLONG WsleProtection : 3;
- ULONGLONG NoExecute : 1;
- } MMPTE_HARDWARE, *PMMPTE_HARDWARE;
复制代码
总结一下,以上实现的代码大概类似弟中弟中弟版MmBuildMdlForNonPagedPool和MmMapLockedPagesSpecifyCache,不同的是本文的代码更多地相当于一份512G的虚拟地址空间物理内存的分布快照,这其中可能存在部分分页内存在快照过程中被放入物理内存而快照后再次被分回硬盘的情况,因此在映射地址空间使用MmIsAddressValid远远不如在原地址空间使用靠谱。
测试发现刷TLB的确不需要KeFlushTb,只要对要访问或修改的映射页面invlpg刷新单个PTE对应的TLB项即可。以上代码在Vista SP0至Win10 18363均测试有效。 |