开门见山,本文的核心思路就是通过填充页表项,将一块连续的虚拟地址映射到新的地址,同时将需要修改的只读内存对应页表项的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。另外,从Win10 RS1周年预览版开始,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手册后,笔者发现当前的Intel CPU支持的虚拟地址寻址最大位数限制为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即可。
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
最后于 2020-10-7 23:22
被hhkqqs编辑
,原因: