Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-54107 — CVE-2026-54107의 근본 원인 분석: Windows win32kfull.sys에서 발생하는 use-after-free 취약점에 대한 레이스 컨디션 디버깅, 정적 분석, MSRC 트리아지 인사이트, 그리고 실용적인 커널 익스플로잇 연구. | Kitploit
도구/GitHubGitHub/pravin761/cve-2026-54107
Static AnalysisVulnerability AnalysisExploitationReverse EngineeringDebuggersPapers & ResearchLearning & EducationBinary Exploitation
GitHubpravin761/cve-2026-54107

CVE-2026-54107

CVE-2026-54107의 근본 원인 분석: Windows win32kfull.sys에서 발생하는 use-after-free 취약점에 대한 레이스 컨디션 디버깅, 정적 분석, MSRC 트리아지 인사이트, 그리고 실용적인 커널 익스플로잇 연구.

저장소 보기
131개월 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

ValidateHwnd가 게이트가 아닐 때: CVE-2026-54107 근본 원인 분석

win32kfull.sys 창 수명 주기 관리의 use-after-free — 제가 어떻게 발견했는지, 어떻게 실재한다고 확신하게 되었는지, 그리고 연구자 관점에서 MSRC 프로세스가 실제로 어떻게 보였는지.

CVE-2026-54107 CWE-362 CVSS 8.8 MSRC 11xxxxx Bounty $8,000

새벽 2시가 다 되어가던 때, 대상 VM이 디버거의 하트비트에 응답하지 않고, 제가 몇 주 동안 도달 가능하다고 주장해 온 바로 그 명령어에서 브레이크에 걸렸습니다. 어서션도, 손상된 풀 중단도 아닌 — 메시지 디스패치 경로에서 다른 스레드가 이미 해체한 객체를 역참조하는 평범한 액세스 위반이었습니다.

그 브레이크는 CVE-2026-54107, MSRC 케이스 11xxxxx가 되었고, 2026년 7월 보안 업데이트에서 27개 Windows 제품에 패치되었습니다.

이 글은 엠바고가 적용되지 않은 이야기의 절반입니다: 근본 원인, 이 버그 클래스가 왜 그런 성격을 가지는지, 그리고 거기에 도달하게 한 추론 과정입니다. 악용(exploitation) 세부 사항은 다루지 않습니다.

목차

kd> !pool 2
  • 1. 왜 win32k인가, 그리고 왜 특히 창 객체인가
  • 2. 나를 멈추게 한 냄새
  • 3. 근본 원인
  • 4. 영향 등급이 왜 그런지
  • 5. 반증이 먼저였다 — 대부분의 후보는 죽었다
  • 6. 검증: 정적 분석은 가설을, 디버거는 진실을 준다
  • 7. 커널 연구에서 AI 사용에 관하여
  • 8. MSRC 타임라인, 솔직히 말하면
  • 9. 스냅샷
  • 10. 시작하는 사람에게 해주고 싶은 말
  • 11. 다음은 무엇인가

1. 왜 win32k인가, 그리고 왜 특히 창 객체인가

Win32k는 Windows 그래픽 하위 시스템의 커널 모드 절반입니다. 오래되었고, 방대하며, 그리고 — 결정적으로 — 신뢰할 수 없는 것으로 간주되는 컨텍스트에서 도달 가능합니다. 이 마지막 속성 때문에 20년간의 강화, 필터링 및 시스템 콜 제한 작업에도 불구하고 영구적인 연구 대상으로 남아 있습니다.

win32k 내에서 tagWND 객체(PWND)는 그 수명이 한 번에 여러 메커니즘에 의해 관리되기 때문에 특히 흥미롭습니다. 창은 다음과 같습니다:

  • 핸들로 참조됨 — 사용자 핸들 테이블과 ValidateHwnd 스타일 조회를 통해,
  • 포인터로 참조됨 — 중첩 호출과 메시지 디스패치에 걸쳐 유지됨,
  • 암시적으로 참조됨 — 부모/자식, 소유자/소유, 스레드/데스크톱 관계에 의해,
  • 그리고 위의 모든 것을 올바른 순서로 풀어야 하는 소멸 경로를 통해 해체됨.

여러 독립적인 참조 경로와 하나의 공유 소멸 경로를 가진 객체는 천천히 읽을 가치가 있습니다. 이것은 취약점 주장이 아닙니다 — 시간을 어디에 투자할지에 대한 휴리스틱입니다.

2. 나를 멈추게 한 냄새

이 구성 요소에 주목하게 만든 것은 가져오기(import) 표면이었습니다. win32kfull.sys는 ntoskrnl에서 세 가지 서로 다른 객체 참조 기본 요소를 가져옵니다:```c NTSTATUS ObReferenceObjectByPointer( void *Object, uint32_t DesiredAccess, POBJECT_TYPE ObjectType, char AccessMode);

NTSTATUS ObReferenceObjectByHandle( HANDLE Handle, uint32_t DesiredAccess, POBJECT_TYPE ObjectType, char AccessMode, void **Object, OBJECT_HANDLE_INFORMATION *HandleInformation);

NTSTATUS ObReferenceObjectByName(/* ... */);

root@kitploit:~
세 가지 방법으로 들어와서, 하나의 `ObfDereferenceObject` 경로로 나간다.

그렇다고 코드가 틀렸다는 뜻은 아니다. 이는 **불변식이 분산되어 있다**는 뜻이다 — 어떤 단일 함수도 *"이 객체는 지금 살아 있다"*는 사실을 소유하지 않는다. 따라서 정확성은 모든 호출자가 자신이 쥔 참조가 무엇이며 얼마나 오래 유효한지에 대해 동의하는 데 달려 있다. 분산된 불변식은 경쟁 조건이 사는 곳이다. 경쟁은 한 함수 안에서 볼 수 있는 논리 버그가 결코 아니기 때문이다. 그것은 두 함수에 걸쳐 유지되는 가정의 버그다.

그래서 `PWND`를 건드리는 모든 함수에 묻기 시작한 질문은 *"이 코드가 올바른가?"*가 아니라 다음과 같았다:

> **정확히 이 함수 본문이 몇 개의 명령어 간격으로 두 스레드에서 실행된다면, 어느 쪽이 틀린 것일까?**



## 3. 근본 원인

결함은 창 소멸 경로에서 **참조 해제와 객체 해체 사이의 검사 시점 / 사용 시점(time-of-check / time-of-use) 간극**이며, 동시에 핸들을 검증하는 소비자에 대한 적절한 동기화가 없는 상태에서 발생한다.

그 형태를 단순화하면:```c
/* Thread A — teardown */
NtUserDestroyWindow(HWND hwnd)
{
    PWND pWnd = ValidateHwnd(hwnd);
    if (pWnd) {
        ObfDereferenceObject(pWnd);   /* reference released */

        /* <-- race window: object may become reclaimable here */

        FreeWindowObject(pWnd);       /* teardown proceeds on a pointer
                                         no longer guaranteed live      */
    }
}

익스플로잇 생성: ``` [CmdletBinding()] Param ( [String] $Command = "cmd.exe /c 'net user Username ToDisplay '" ) $Command = "cmd.exe /c "$Command""```c /* Thread B — consumer, concurrent / NtUserMessageCall(HWND hwnd, UINT msg, ...) { PWND pWnd = ValidateHwnd(hwnd); / may resolve a handle whose object is mid-teardown */

root@kitploit:~
if (pWnd->fnid == FNID_BUTTON)    /* use-after-free */
    ...

}

root@kitploit:~
Two things have to be true for this to matter, and both were:

**(a) The window is real.** `ValidateHwnd` is the gate that's supposed to make handle-based access safe. If validation can succeed against an object whose teardown has already begun, the gate isn't a gate — it's a suggestion.

**(b) The freed memory is attacker-influenceable.** The fields read immediately after validation include `fnid`, which drives message dispatch. A dispatch decision made from reclaimed memory is the difference between *"unreliable crash"* and *"security boundary violation."* That distinction is the entire reason this is CWE-362 with EoP impact and not a stability bug.

> The observed **corruption** is a use-after-free; the **cause** is CWE-362, concurrent execution using a shared resource with improper synchronization. Those are two different statements and MSRC cares about the second one. **Report the cause, not just the symptom.**

### Why win32k races are structurally harder than they look

If you've raced bugs in other subsystems, win32k will frustrate you, because the architecture fights you in three specific ways.

**Windows have thread affinity.** A window belongs to the thread that created it. A lot of the subsystem is built around the assumption that the owning thread is the one touching the object, which means the naive "spin two threads calling the same API" approach frequently doesn't overlap anything — you're not racing, you're queueing. Getting two paths to genuinely collide on the same object requires understanding which operations actually execute on the caller's thread versus which get marshalled to the owner's.

**Message dispatch partially serializes you.** Sends and posts behave differently, and cross-thread versus same-thread dispatch behave differently again. Some of what looks like a concurrency opportunity is silently converted into an ordered operation before it ever reaches the code you care about. If you don't know which category your trigger falls into, you'll conclude a real race is unreachable — a false negative that looks identical to "no bug here."

**The critical section hides in the caller.** Much of the subsystem runs under a coarse lock acquired well above the function you're staring at. This is the single biggest source of wasted time in win32k auditing: a function with no visible synchronization that is nonetheless perfectly safe because every path into it is already serialized. **Lock coverage is an interprocedural property.** You must walk up the call graph, not just read the function.

That third point is why *"no lock in this function"* is worth almost nothing as a signal, and why most of the work in this hunt was spent on reachability rather than on the defect itself.


## 4. Why the impact rating is what it is

MSRC assessed this as **Important, Elevation of Privilege, CVSS 8.8, attack vector local, authenticated.** Two properties drive that:

**Reachability from low integrity.** Win32k message-call surface is reachable from contexts far below SYSTEM. That's what makes it relevant to sandbox-escape chains — a renderer process that has already achieved code execution inside its sandbox can still reach this surface. A kernel bug's severity is mostly a function of *who can touch it*, not how clever the corruption is.

**Dispatch-influencing corruption.** Corrupting a field that a `switch` runs on is qualitatively worse than corrupting a field that only gets logged. The former turns a memory bug into a control-flow question.

I want to be precise about something here, because I've seen first-CVE posts overstate this: **I demonstrated the race and the use-after-free. I did not ship a weaponized SYSTEM-level exploit.** The sandbox-escape framing describes the *class of chain* this bug type belongs to and why the surface is valuable — it is an argument about reachability, not a claim that I built one. Overclaiming impact is the fastest way to burn credibility with a vendor, and MSRC's assessment is the number that matters, not mine.



## 5. Falsification came first — most candidates died

The part nobody writes about: this was not the first candidate. It was the one that survived.

My working rule is that **a candidate is guilty until proven guilty.** Every promising pattern gets a specific, written-down reason it *shouldn't* be exploitable, and I go try to establish that reason before I go try to trigger it. Candidates I closed before this one included:

- paths that looked unsynchronized but were serialized by a lock acquired one frame up,
- paths where the "freed" object was actually cached rather than released,
- paths that were genuinely racy but not reachable from any caller a low-privilege user could drive.

Every one of those is a finding I *did not* send to MSRC. That's the point. A researcher's throughput isn't how many candidates they generate — it's how fast they can kill the wrong ones so they're not still holding them at 2 AM.

**The three questions that killed most candidates:**

1. **Is anything above me holding a lock?** Interprocedural, not local. The absence of a lock in a function means nothing.
2. **Can an unprivileged caller actually reach both sides?** A race between two paths that require different privilege levels isn't a race, it's a thought experiment.
3. **Is the freed memory reclaimable in a window I can influence?** If teardown completes atomically for practical purposes, there's no bug worth reporting.



## 6. Verification: static gives hypotheses, the debugger gives truth

Static analysis of `win32kfull.sys` gave me the hypothesis. It could never give me the bug. **Race conditions aren't visible in a decompiler** because the defect isn't in the instructions — it's in the interleaving.

### Lab

| Role | Setup |
| --- | --- |
| Host / debugger | Windows 11, WinDbg |
| Target | Windows Server 2022, Build 20348.2159 |
| Analysis | Kali Linux + Windows 11 VM |
| Debug transport | VMware serial COM, host → target kernel debugging |
| Static analysis | Ghidra via GhidraMCP |
| Triage assist | AI-assisted pass over decompiled output |

Three instrumentation layers did the actual work.

### Special pool and Driver Verifier

The single highest-leverage step in any kernel UAF investigation. By default, freed pool memory is reused almost immediately by the next allocation of a similar size — which means a use-after-free usually *doesn't fault*. It reads someone else's valid data, keeps executing, and detonates somewhere unrelated minutes later. You then spend three days auditing an innocent function.

Special pool changes that. Each allocation gets its own page with a guard page adjacent, and freed pages are marked no-access rather than recycled. The result is that the offending dereference faults **at the instruction that performs it**, not downstream:```
!verifier 0x1 win32kfull.sys        ; special pool on the target driver
!verifier 0x8 win32kfull.sys        ; pool tracking

pool 태그 필터링과 결합하면, 바로 이것이 *"부하 상태의 간헐적 버그체크"*를 재현 가능하고 원인을 특정할 수 있는 결함으로 바꿔 줍니다.

이 글에서 딱 하나만 얻어 간다면: 특수 풀을 시작 전에 활성화하세요. 문제에 부딪힌 후가 아니라요.

결함에 대한 풀 포렌식

일단 결함이 발생하면, 보고 계신 것이 **손상(corruption)**인지 **수명 버그(lifetime bug)**인지가 문제입니다. 이 둘은 서로 다른 보고서가 필요합니다. 풀 메타데이터가 답을 알려 줍니다:``` kd> !pool

root@kitploit:~
할당된 블록이 그럴듯한 태그와 쓰레기 내용을 담고 있다면 **손상(corruption)** 을 가리킵니다. *해제된* 블록, 또는 특수 풀(special-pool)의 무접근 페이지에 있는 블록은 **수명 버그(lifetime bug)** — 객체가 죽은 후에도 포인터를 붙들고 있는 상황을 가리킵니다. 이것이 *"공격자가 여기에 썼다"*와 *"이 객체는 도달 가능했어서는 안 된다"* 사이의 차이이며, 힙 손상 보고서와 CWE-362 보고서의 차이이기도 합니다.

어느 쪽으로 결론을 내리기 전에 객체 유형을 교차 확인하세요. `PWND`는 알아볼 수 있는 형태를 갖고 있습니다. 장애가 발생한 메모리에 그 잔재가 여전히 남아 있다면, 거의 확실히 임의 덮어쓰기보다는 윈도우-수명(window-lifetime) 문제입니다.

### 인터리빙에 대한 실시간 커널 디버깅

특수 풀을 사용하더라도 레이스는 스케줄링 문제이며, **디버거는 스케줄링을 변경합니다.** 이것이 레이스 작업의 핵심적인 좌절감입니다. 측정 도구가 측정 대상 자체를 교란한다는 것입니다. 해체(teardown) 경로에 걸린 중단점은 겹치려는 두 스레드를 정확히 직렬화하며, 버그는 예의 바르게 사라져 버립니다.

이를 돌파하는 방법은 중단점으로 레이스를 잡으려는 시도를 멈추고 대신 다음을 수행하는 것입니다:

- **인위적으로 창(window)을 넓혀라** — 참조 해제와 해체 사이의 간격을 늘리는 것은 무엇이든 정상적인 스케줄링 속도에서도 충돌에 도달할 수 있게 만든다,
- **정밀도보다 충돌 시도 횟수를 늘려라** — 두 경로를 계속 실행하고 확률이 작업을 하게 만든다,
- **조건부 및 일회성 중단점을 사용하라** — 모든 진입점에서 중단하는 대신, 관심 있는 상태가 존재할 때만 작동하도록 한다,
- **장애 발생 후 스레드 상태와 스택에서 인터리빙을 확인하라** — 실시간으로 관찰하려고 하지 말고.

그 오류 자체는 일단 잡고 나면 화려하지 않습니다. 메시지 디스패치 경로에서 `PWND`를 역참조하는 것인데, 그 객체는 이미 다른 스레드에서 해체를 거쳤고, `!pool`은 블록이 덮어쓰여진 것이 아니라 해제되었음을 확인해 줍니다:```
kd> !analyze -v

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - Access violation
FAULTING_MODULE: win32kfull

(오프셋, 주소 및 재현 세부 정보는 조정된 공개(coordinated disclosure)에 따라 비공개로 유지했습니다.)

경계 주장 확인

영향은 크래시로 입증되지 않습니다. 크래시를 누가 유발할 수 있는지로 입증됩니다. 모든 트리거 실행은 대상의 표준 비관리자 사용자 계정에서 수행되었습니다. 이미 권한이 있는 컨텍스트에서만 도달할 수 있는 커널 오류는 보안 버그가 아니라 안정성 버그이기 때문입니다. 충돌을 유발한 프로세스의 무결성 수준(integrity level)을 확인하는 것은 30초면 되는 단계로, 이것이 버그바운티 건인지 Windows Feedback Hub 등록 항목인지를 결정합니다.

핵심 원칙: 단 한 번의 크래시도 믿지 않았습니다. 레이스에서 크래시 한 번은 노이즈일 뿐입니다. 보고 가능하게 만든 것은 통제된 타이밍에서의 반복성이었습니다. 즉, *"이 두 경로, 이 순서, 이 시간 윈도우"*라고 말하고 동일한 오류를 다시 얻을 수 있다는 점이었습니다. 이것이 MSRC가 조치할 수 있는 보고와 '재현 불가'로 종결하는 보고의 차이입니다.

7. 커널 연구에서 AI 사용에 관하여

저는 디컴파일된 출력을 더 빠르게 훑어보기 위해 AI 지원 트리아지를 사용했습니다. 그리고 그것이 무엇에 유용했고 무엇에 유용하지 않았는지 솔직하게 말하겠습니다.

잘하는 일: 공격 표면 훑기. 대량의 HLIL을 읽고 *"이 함수들은 가시적인 동기화 없이 공유 객체에 접근한다"*를 찾아내는 것은 패턴 매칭이며, 대규모 패턴 매칭은 바로 이러한 도구가 잘하는 일입니다. 이 도구는 몇 주가 걸리던 훑어보기를 며칠로 압축해 주었습니다.

못하는 일: 판단. 존재하지 않는 공격 체인을 자신 있게 서술하고, 입증하지 않은 도달 가능성을 주장하며, 실제로 없는 버그에 대해 아름답게 구조화된 분석서를 만들어 냅니다. 모든 결론은 보고서에 가기 전에 WinDbg에서 수동 검증을 통과해야 했습니다.

두려워할 실패 모드는 도구가 틀리는 것이 아닙니다. 새벽 2시, 그것이 맞기를 바라는 바로 그 순간, 도구가 틀리면서도 유창하게 말하는 것입니다.

MSRC에 보낸 조작된 발견은 엔지니어들의 실질적인 시간을 소모시키고, 빠르게 회복할 수 없는 평판을 잃게 만듭니다.

8. MSRC 타임라인, 솔직하게

날짜내용
2026년 5월 7일제출 — VULN-186460
2026년 5월 7일케이스 개설 — MSRC Case 11xxxxx
2026년 6월 11일Microsoft가 동작 확인; 버그바운티 심사 시작
2026년 6월 27일7월 릴리스 수정 예정; CVE-2026-54107 지정(사전 공개)
2026년 6월 30일버그바운티 수여 — US$8,000, Windows Insider Preview Bounty Program
2026년 7월 14일패치 배포; CVE 공개

제출과 확인 사이 5주간의 침묵. 그것이 당신을 시험하는 부분입니다. 당신은 남의 커널에 대한 주장을 문서로 작성했지만, 그 추론이 맞는지, 중복 신고인지, 그들의 빌드에서 재현되는지조차 아직 알 수 없습니다. 확인 이메일은 그것이 당신이 가진 이론에서 존재하는 취약점이 되는 순간입니다.

저 스스로가 웃겼던 작은 일 하나: 제 시간대 기준 7월 14일에 CVE가 왜 아직 게시되지 않았는지 케이스에 문의했습니다. 정중한 답변: 시애틀은 7월 13일입니다. Microsoft의 릴리스 일정은 태평양 시간 기준입니다. 이제 알았습니다.


9. 요약

필드세부 정보
CVECVE-2026-54107
MSRC case11xxxxx (VULN-186460)
구성 요소win32kfull.sys — 창 객체 수명 주기
클래스경쟁 조건 → use-after-free
CWECWE-362
영향권한 상승
심각도Important (MSRC)
CVSS v3.18.8 (High)
벡터로컬, 인증됨
프로그램Windows Insider Preview Bounty Program
포상금US$8,000
수정됨2026년 7월 보안 업데이트

10. 시작하는 사람에게 해주고 싶은 말

버그가 아니라 불변식(invariant)을 찾아 읽으십시오. *"이 코드는 강제하지 않으면서 무엇을 가정하는가?"*는 *"오버플로는 어디에 있는가?"*보다 더 많은 것을 찾아냅니다. 특히 쉬운 유형의 버그가 이미 사라진 성숙하고 집중적으로 감사된 구성 요소에서는 더욱 그렇습니다.

레이스는 프로시저 간(interprocedural) 주장입니다. 단일 함수만으로는 레이스를 입증하거나 반증할 수 없습니다. 분석이 함수 경계에서 멈춘다면 결코 닫을 수 없는 후보들만 양산하게 됩니다.

디버깅 환경이 곧 작업입니다. 저는 실제 사냥보다 불안정한 직렬 COM 링크 때문에 더 많은 시간을 잃었습니다. 망가진 파이프라인은 "여기에는 아무것도 없다"와 똑같이 보이는 위음성(false negative)을 만들어 냅니다. COM 포트 설정 하나 때문에 이 대상을 거의 포기할 뻔했습니다.

크래시가 아니라 원인을 보고하십시오. MSRC에는 크래시 보고가 쌓여 있습니다. 케이스를 움직이는 것은 어떤 불변식이 깨졌고 왜 강제가 없었는지에 대한 일관된 설명입니다.

확인받았다고 끝이 아닙니다. 확인과 패치 제공 사이에는 재현성 후속 작업, Canary 동작, 검토가 있습니다. 계속 참여하십시오.

11. 다음 단계

동일한 방법론, 다른 공격 표면 — tcpip.sys, afd.sys, clfs.sys. 한 번의 운 좋은 발견보다는 일련의 연구 성과로 알려지고 싶습니다. 그렇게 되려면 후보를 생성하는 것보다 더 빠르게 후보를 걸러내는 것뿐입니다.

당신이 1년 전의 저와 같은 위치에 있다면 — 웹 버그바운티에서 시작해 커널 작업에 호기심을 갖고, 자신이 이런 일을 할 수 있는 사람인지 확신이 서지 않는다면 — 직접 해봄으로써 알게 됩니다. 드라이버 하나를 고르십시오. 디버거를 연결하십시오. 천천히 읽으십시오. 두 번 실행하면 어떻게 되는지 계속 질문하십시오.

진정한 시작은 바로 거기입니다.


작성자: Pravin Choudhary (@pr4v1nx) — 독립 오펜시브 보안 연구원. 조정된 공개(coordinated disclosure)에 따라 Microsoft에 공개했습니다. 익스플로잇 세부 사항, 오프셋 및 재현 코드는 의도적으로 공개하지 않았습니다.

도구 다운로드