Featured image of post 大厂怎么在 Windows 内核殴打 KASLR?

大厂怎么在 Windows 内核殴打 KASLR?

虽然写是殴打 KASLR,但其实是为了获取 PTEBASE 这个全局变量。(但是如果获取到了,也等价于获取到 KASLR 偏移了)

众所周知,一个导出函数 KeCapturePersistentThreadState 直接可以获取,但这函数兼容性其实不咋地,首先 PTEBASE 在正式版本中是 14393 之后才加入 KdDebuggerDataBlock,其次 14393 及之后 ForceDumpDisabled 这个全局变量能决定这个函数是否拷贝 KdDebuggerDataBlockForceDumpDisabled 是由注册表 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 的值。地址转换过程如下:

  1. 使用 CR 3 作为 PML 4 表基址,用索引 i 查找 PML4E[i]
  2. 如果 i 是自引用索引,PML4E[i] 指向 PML 4 表自身(即 CR 3)
  3. 以 CR 3 作为 PDPT 表基址,用索引 i 查找,实际读取的还是 PML4E[i]
  4. 这个过程递归进行,最终所有级别都指向 CR 3
  5. 因此 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,并在几百个结果中以以下规则进行筛选:

  1. 函数比较小
  2. 函数是文档的或导出的
  3. 函数在多个 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;
Built with Hugo
Theme Stack designed by Jimmy