虽然写是殴打 KASLR,但其实是为了获取 PTEBASE 这个全局变量。(但是如果获取到了,也等价于获取到 KASLR 偏移了)
众所周知,一个导出函数 KeCapturePersistentThreadState
直接可以获取,但这函数兼容性其实不咋地,首先 PTEBASE 在正式版本中是 14393 之后才加入 KdDebuggerDataBlock
,其次 14393 及之后 ForceDumpDisabled
这个全局变量能决定这个函数是否拷贝 KdDebuggerDataBlock
,ForceDumpDisabled
是由注册表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\CrashControl
有关项控制的,只有开机时的 IoInitSystem
流程才会修改它。要保证 KeCapturePersistentThreadState
在所有机子上成功执行,必须先定位 ForceDumpDisabled
并强制给它设为 1。这就又恶心人了。
自引用页表法的核心思想是利用 x 64 处理器分页机制中的一个特性:在最高级别的页表(PML 4)中,可以存在一个特殊的条目(PML 4 E),该条目指向 PML 4 表自身的物理基地址。这个特殊的条目被称为"自引用条目"。
这里有了三套方法去获取,一套来自腾讯,一套来自 vgk,还有一套来自 tandasat 佬。 腾讯一开始也是通过函数+偏移去定位,不过后来也使用了自引用页表法。原始版本我找不到了,于是自己写了一套特征码偏移。
自引用页表法
原理
当 MMU(内存管理单元)翻译这个特制的虚拟地址时,如果 i 是自引用索引,那么 PML 4 E[i] 的内容(去除属性位后)就是 PML 4 表自身的物理基地址,也就是 CR 3 的值。地址转换过程如下:
- 使用 CR 3 作为 PML 4 表基址,用索引
i
查找PML4E[i]
- 如果
i
是自引用索引,PML4E[i]
指向 PML 4 表自身(即 CR 3) - 以 CR 3 作为 PDPT 表基址,用索引
i
查找,实际读取的还是PML4E[i]
- 这个过程递归进行,最终所有级别都指向 CR 3
- 因此
testVA
最终映射到的物理地址就是 CR 3
接着验证:使用 MmGetPhysicalAddress
函数获取 testVA
对应的物理地址。如果返回的物理地址等于 CR 3 寄存器的值(去除低 12 位标志位),则当前索引 i
就是自引用索引。
找到自引用索引后,计算 PTEBASE = (SelfRefIndex << 39) | 0xFFFF000000000000ULL;
vgk. sys 原始版本,来自看雪佬的逆向:
char __fastcall LocatePteBase()
{
unsigned __int64 v0; // rax
__int64 v1; // krB8_8
__int64 i; // krA0_8
__int64 index; // rsi
__int64 v4; // r9
PHYSICAL_ADDRESS v5; // krA0_8
unsigned __int64 v6; // krD8_8
__int64 v7; // krE8_8
unsigned __int64 v8; // kr30_8
unsigned int v9; // eax
char v10; // cf
ULONG v11; // krB0_4
void *v14; // rax
__int64 v15; // r9
unsigned __int64 VA; // [rsp+44h] [rbp+8h]
v0 = __readcr3();
v1 = v0 & 0xFFFFFFFFFF000i64;
for ( i = 1i64; ; i = index + 1 )
{
VA = (i | ((i | ((i | (i << 9)) << 9)) << 9)) << 12;
index = i;
v5 = (*(PHYSICAL_ADDRESS (__stdcall **)(PVOID))pfnMmGetPhysicalAddress_0)((PVOID)VA);
v6 = VA;
v7 = v4;
if ( v5.QuadPart == v1 )
break;
if ( (unsigned __int64)(index + 1) >= 0x200 )
{
SelfAutoIndex = 0i64;
goto LABEL_11;
}
}
if ( (VA >> 47) & 1 != 0 )
v6 = VA | 0xFFFF000000000000ui64;
SelfAutoIndex = index;
if ( !v6
|| (PteBase = (index << 39) | 0xFFFF000000000000ui64,
PdeBase = (index << 30) | (index << 39) | 0xFFFF000000000000ui64,
PpeBase = (index << 21) | PdeBase,
PxeBase = (index << 21) | PdeBase | (index << 12),
PxeBase != v6) )
{
LABEL_11:
v11 = -536870756;
LABEL_12:
DbgPrintEx(6, v11, L"\n", v7);
return 0;
}
return 1;
}
企鹅后来的版本
char __fastcall InitializePteBase(char a1)
{
char v1; // bl
PHYSICAL_ADDRESS pml4t; // rdi
__int64 *pml4t_va; // r11
int slot; // edx
__int64 index; // rcx
__int64 v6; // r8
v1 = 0;
if ( a1 )
{
pml4t.QuadPart = __readcr3();
pml4t_va = (__int64 *)MmMapIoSpace(pml4t, 0x1000ui64, MmCached);
if ( pml4t_va )
{
slot = 0;
index = 0i64;
while ( (pml4t_va[index] & 0xFFFFFFFFF000i64) != pml4t.QuadPart )
{
++index;
++slot;
if ( index >= 512 )
goto LABEL_8;
}
v1 = 1;
v6 = (slot + 0x1FFFE00i64) << 39;
g_pte_base = (slot + 0x1FFFE00i64) << 39;
g_pxe_selfmapping_index = slot;
g_pde_base = v6 + ((__int64)slot << 30);
g_ppe_base = v6 + ((__int64)slot << 30) + ((__int64)slot << 21);
g_pxe_base = (void *)(g_ppe_base + ((__int64)slot << 12));
g_pxe_end = (__int64)g_pxe_base + 4096;
g_pte_end = v6 + 0x8000000000i64;
LABEL_8:
MmUnmapIoSpace(pml4t_va, 0x1000ui64);
}
}
else
{
g_pxe_selfmapping_index = 493i64;
v1 = 1;
g_pte_base = 0xFFFFF68000000000i64;
g_pde_base = 0xFFFFF6FB40000000i64;
g_ppe_base = 0xFFFFF6FB7DA00000i64;
g_pxe_base = (void *)0xFFFFF6FB7DBED000i64;
g_pxe_end = 0xFFFFF6FB7DBEE000i64;
g_pte_end = 0xFFFFF70000000000i64;
}
return v1;
}
综合魔改后的 C++ 版本:
ULONGLONG GetPteBase2()
{
pteTries++;
if (PteBaseCache != 0) {
return PteBaseCache;
}
ULONGLONG cr3 = __readcr3() & ~0xFFFULL; // 清除标志位
// 循环尝试所有可能的自引用索引
for (ULONGLONG i = 0x100; i < 0x200; i++) {
// 构造特殊地址,使所有页表级别使用相同索引
ULONGLONG testVA = (i | (i | (i | i << 9) << 9) << 9) << 12;
// 获取物理地址
PHYSICAL_ADDRESS phys = MmGetPhysicalAddress((PVOID)testVA);
// 修复类型不匹配问题
if (static_cast<ULONGLONG>(phys.QuadPart) == cr3) {
/*
// 规范化地址
if ((testVA >> 47) & 1)
testVA |= 0xFFFF000000000000ULL;
// testVA实际上就是自引用地址,而不是随便计算的PTE基址
// 根据Windows内存管理机制,正确计算PTE基址
ULONG64 pteBase = (i << 39) | 0xFFFF000000000000ULL;
ULONG64 pdeBase = (i << 30) | (i << 39) | 0xFFFF000000000000ULL;
ULONG64 ppeBase = (i << 21) | pdeBase;
ULONG64 pxeBase = (i << 12) | ppeBase;
// 验证计算正确
if (pxeBase != testVA) {
DbgPrint("Warning: Calculated PXE base doesn't match test address\n");
continue; // 或者返回错误
}
*/
ULONGLONG pteBase = i << 39 | 0xFFFF000000000000ULL;
PteBaseCache = pteBase;
return pteBase; // 返回PTE基址
}
}
return 0; // 未找到
}
启发性特征函数+硬编码偏移
这几乎是最好的方法。在 Win 10、Win 11 多个版本验证了可行性。0 运行时开销。用法和原理简单,三岁小孩都看得懂。工作量已经全在我对 ntoskrnl 的分析中了……
ULONGLONG GetPteBase()
{
pteTries++;
if (PteBaseCache != 0) {
return PteBaseCache;
}
UNICODE_STRING routineName = RTL_CONSTANT_STRING(L"MmMapMemoryDumpMdl");
PVOID routine = MmGetSystemRoutineAddress(&routineName);
if (!routine) {
// 如果无法找到函数地址,返回0
return 0;
}
/*
* public MmMapMemoryDumpMdl
* 48 83 EC 28 - sub rsp, 28h (4字节)
* 4C 8B C1 - mov r8, rcx (3字节)
* 48 8B 0D xx xx xx xx - mov rcx, [rip+?] (7字节)
* 48 C1 E1 19 - shl rcx, 19h (4字节)
* 48 B8 ... - mov rax, 0FFFFF68000000000h (10字节,含立即数)
* ...
*/
PBYTE src = static_cast<PBYTE>(routine) + 20; // 跳过前20字节
// 直接读取8字节
ULONGLONG pteBase = *reinterpret_cast<PULONG64>(src);
PteBaseCache = pteBase;
return pteBase;
}
如你所见,核心在于 MmMapMemoryDumpMdl
的结构。
我是如何找到并验证该函数的?
众所周知,PteBase 在 ntoskrnl. exe 到处都是。只需要搜索立即数 0FFFFF68000000000h
,并在几百个结果中以以下规则进行筛选:
- 函数比较小
- 函数是文档的或导出的
- 函数在多个 Windows 版本中都存在且立即数
0FFFFF68000000000h
的偏移量保持稳定。
当然,对单个版本静态筛选,我们可以先确定前两点。MmMapMemoryDumpMdl
几乎是最好的函数。在后来的多版本验证中,MmMapMemoryDumpMdl
函数字节有变化,但完全不影响立即数的偏移。
特征码搜索
来自知名国际友人 tandasat,颇具巧思。没有魔改,不能立刻使用上。
// Get PTE_BASE from MmGetVirtualForPhysical
const auto p_MmGetVirtualForPhysical =
UtilGetSystemProcAddress(L"MmGetVirtualForPhysical");
if (!p_MmGetVirtualForPhysical) {
return STATUS_PROCEDURE_NOT_FOUND;
}
static const UCHAR kPatternWin10x64[] = {
0x48, 0x8b, 0x04, 0xd0, // mov rax, [rax+rdx*8]
0x48, 0xc1, 0xe0, 0x19, // shl rax, 19h
0x48, 0xba, // mov rdx, ????????`???????? ; PTE_BASE
};
auto found = reinterpret_cast<ULONG_PTR>(
UtilMemMem(p_MmGetVirtualForPhysical, 0x30, kPatternWin10x64,
sizeof(kPatternWin10x64)));
if (!found) {
return STATUS_PROCEDURE_NOT_FOUND;
}
found += sizeof(kPatternWin10x64);
HYPERPLATFORM_LOG_DEBUG("Found a hard coded PTE_BASE at %016Ix", found);
const auto pte_base = *reinterpret_cast<ULONG_PTR *>(found);
const auto index = (pte_base >> kUtilpPxiShift) & kUtilpPxiMask;
const auto pde_base = pte_base | (index << kUtilpPpiShift);
const auto ppe_base = pde_base | (index << kUtilpPdiShift);
const auto pxe_base = ppe_base | (index << kUtilpPtiShift);
g_utilp_pxe_base = static_cast<ULONG_PTR>(pxe_base);
g_utilp_ppe_base = static_cast<ULONG_PTR>(ppe_base);
g_utilp_pde_base = static_cast<ULONG_PTR>(pde_base);
g_utilp_pte_base = static_cast<ULONG_PTR>(pte_base);
g_utilp_pxi_shift = kUtilpPxiShift;
g_utilp_ppi_shift = kUtilpPpiShift;
g_utilp_pdi_shift = kUtilpPdiShift;
g_utilp_pti_shift = kUtilpPtiShift;
g_utilp_pxi_mask = kUtilpPxiMask;
g_utilp_ppi_mask = kUtilpPpiMask;
g_utilp_pdi_mask = kUtilpPdiMask;
g_utilp_pti_mask = kUtilpPtiMask;
return status;