
CVE-2026-54107 का मूल-कारण विश्लेषण: रेस कंडीशन डिबगिंग, स्टैटिक विश्लेषण, MSRC ट्राइएज अंतर्दृष्टि और व्यावहारिक कर्नेल एक्सप्लॉइटेशन शोध के साथ Windows win32kfull.sys में use-after-free।
ValidateHwnd कोई गेट नहीं है: CVE-2026-54107 का मूल कारण
win32kfull.sysविंडो जीवनचक्र प्रबंधन में एक use-after-free — यह कैसे मिला, कैसे मैंने खुद को विश्वास दिलाया कि यह वास्तविक है, और शोधकर्ता के नज़रिए से MSRC प्रक्रिया वास्तव में कैसी दिखी।
लगभग सुबह के 2 बज रहे थे जब टार्गेट VM डीबगर के हार्टबीट का जवाब देना बंद करके ठीक उसी निर्देश पर ब्रेक में गिर गया जिसे मैं हफ्तों से बहस करते हुए सुलभ बता रहा था। न कोई assertion, न corrupted-pool stop — बल्कि message-dispatch पथ पर एक साधारण access violation, किसी ऐसी ऑब्जेक्ट को dereference करते हुए जिसे दूसरे थ्रेड ने पहले ही तोड़ दिया था।
वह ब्रेक CVE-2026-54107, MSRC केस 11xxxxx, में 27 Windows उत्पादों में पैच किया गया, बन गया।
kd> !pool 2यह पोस्ट कहानी का गैर-एम्बार्गो वाला आधा हिस्सा है: मूल कारण, यह बग श्रेणी क्यों ऐसी है, और वह तर्क जिसने मुझे वहाँ पहुँचाया। एक्सप्लॉइटेशन से जुड़े विवरण शामिल नहीं हैं।
Win32k, Windows ग्राफ़िकल सबसिस्टम का kernel-mode भाग है। यह पुराना है, बहुत बड़ा है, और — महत्वपूर्ण रूप से — यह उन संदर्भों से पहुँच योग्य है जिन्हें अविश्वसनीय माना जाता है। यह अंतिम गुण ही कारण है कि बीस साल के hardening, filtering और syscall प्रतिबंध कार्यों के बावजूद यह स्थायी शोध लक्ष्य बना हुआ है।
win32k के भीतर, tagWND ऑब्जेक्ट (PWND) असामान्य रूप से दिलचस्प है क्योंकि इसका जीवनकाल एक साथ एक से अधिक तंत्रों द्वारा प्रबंधित होता है। एक window:
ValidateHwnd-शैली लुकअप के माध्यम से,कई स्वतंत्र संदर्भ पथों और एक साझा विनाश पथ वाली कोई भी ऑब्जेक्ट धीरे-धीरे पढ़ने लायक है। यह कोई vulnerability दावा नहीं है — यह एक heuristic है कि समय कहाँ बिताना है।
इस कंपोनेंट पर मुझे रोकने वाली चीज़ import surface थी। win32kfull.sys ntoskrnl से तीन अलग-अलग ऑब्जेक्ट संदर्भ primitives खींचता है:```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(/* ... */);
अंदर जाने के तीन पथ, बाहर निकलने का एक `ObfDereferenceObject` पथ।
इसका मतलब यह नहीं है कि कोड गलत है। इसका मतलब है कि **इनवेरिएंट वितरित है** — कोई एक फ़ंक्शन *"यह ऑब्जेक्ट अभी जीवित है"* का स्वामी नहीं होता, इसलिए शुद्धता इस पर निर्भर करती है कि हर कॉलर इस बात पर सहमत हो कि वे कौन सा रेफ़रेंस पकड़े हुए हैं और वह कितने समय तक मान्य है। वितरित इनवेरिएंट्स ही वे स्थान हैं जहाँ रेस कंडीशंस पनपती हैं, क्योंकि रेस कभी भी कोई लॉजिक बग नहीं होती जिसे आप किसी एक फ़ंक्शन में देख सकें। यह एक ऐसी धारणा में बग है जो दो के बीच साझा होती है।
तो जो सवाल मैंने हर उस फ़ंक्शन से पूछना शुरू किया जो `PWND` को छूता था, वह *"क्या यह कोड सही है?"* नहीं था, बल्कि:
> **यदि यही फ़ंक्शन बॉडी दो थ्रेड्स पर कुछ ही निर्देशों के अंतराल पर निष्पादित होती है, तो उनमें से कौन सा गलत है?**
## 3. मूल कारण
यह दोष window destruction path में **रेफ़रेंस रिलीज़ और ऑब्जेक्ट टियरडाउन के बीच time-of-check / time-of-use अंतराल** है, जिसमें समवर्ती handle-validating consumer के विरुद्ध पर्याप्त synchronization नहीं है।
इसके मूल स्वरूप में:```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 */
}
}
No input content was provided after "INPUT:". Please provide the chunk text to translate.```c /* Thread B — consumer, concurrent / NtUserMessageCall(HWND hwnd, UINT msg, ...) { PWND pWnd = ValidateHwnd(hwnd); / may resolve a handle whose object is mid-teardown */
if (pWnd->fnid == FNID_BUTTON) /* use-after-free */
...
}
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
Combined with pool tag filtering, this is what turns "intermittent bugcheck under load" into a reproducible, attributable fault.
If you take one thing from this post: enable special pool before you start, not after you're stuck.
Once you have a fault, the question is whether you're looking at corruption or at a lifetime bug. Those need different reports. The pool metadata answers it:``` kd> !pool
एक ब्लॉक जो *आवंटित* (allocated) है और उसमें एक संभावित टैग और कचरा कंटेंट है, **भ्रष्टाचार** (corruption) की ओर इशारा करता है। एक ब्लॉक जो *मुक्त* (freed) है, या किसी विशेष-पूल नो-एक्सेस पेज पर रखा हुआ है, **लाइफ़टाइम बग** की ओर इशारा करता है — यानी किसी पॉइंटर को ऑब्जेक्ट की मृत्यु के बाद भी पकड़े रखना। यही *"हमलावर ने यहाँ लिखा"* और *"यह ऑब्जेक्ट पहुँच योग्य नहीं होना चाहिए था"* के बीच का अंतर है, और यह हीप-भ्रष्टाचार रिपोर्ट और CWE-362 रिपोर्ट के बीच का अंतर है।
किसी निष्कर्ष पर पहुँचने से पहले ऑब्जेक्ट प्रकार को क्रॉस-चेक करें। एक `PWND` की पहचान योग्य आकृति होती है; यदि जिस मेमोरी पर आपको फॉल्ट मिला है, उसमें अभी भी उसके अवशेष मौजूद हैं, तो आप लगभग निश्चित रूप से एक विंडो-लाइफ़टाइम समस्या में हैं, न कि किसी यादृच्छिक ओवरराइट में।
### इंटरलीविंग पर लाइव कर्नेल डिबगिंग
स्पेशल पूल के साथ भी, एक रेस शेड्यूलिंग की समस्या है, और **डीबगर शेड्यूलिंग को बदल देता है।** रेस कार्य की यही केंद्रीय निराशा है: उपकरण उस चीज़ को विक्षुब्ध करता है जिसे वह मापता है। टियरडाउन पथ पर एक ब्रेकपॉइंट उन दो थ्रेड्स को सीरियलाइज़ कर देता है जिन्हें आप ओवरलैप करने की कोशिश कर रहे हैं, और बग विनम्रता से गायब हो जाता है।
इससे निकलने का रास्ता यह है कि ब्रेकपॉइंट से रेस को पकड़ने की कोशिश करना बंद करें और इसके बजाय:
- **विंडो को कृत्रिम रूप से चौड़ा करें** — जो भी रेफरेंस रिलीज़ और टियरडाउन के बीच के अंतराल को लंबा करता है, वह सामान्य शेड्यूलिंग दरों पर टक्कर को पहुँच योग्य बना देता है,
- **सटीकता के बजाय टक्कर के प्रयास बढ़ाएँ** — दोनों पथों को लगातार चलाएँ और संभावना को काम करने दें,
- **कंडीशनल और वन-शॉट ब्रेकपॉइंट्स का उपयोग करें** जो केवल तब सक्रिय होते हैं जब रुचिकर स्थिति मौजूद होती है, हर प्रविष्टि पर ब्रेक करने के बजाय,
- **फॉल्ट के बाद इंटरलीविंग की पुष्टि करें**, थ्रेड स्थिति और स्टैक्स से, लाइव इसे देखने की कोशिश करने के बजाय।
फॉल्ट स्वयं, एक बार मिल जाने पर, बहुत साधारण होता है — मैसेज-डिस्पैच पथ पर एक `PWND` का डीरेफ़रेंस जहाँ ऑब्जेक्ट पहले ही किसी अन्य थ्रेड पर टियरडाउन से गुज़र चुका है, और `!pool` यह पुष्टि करता है कि ब्लॉक ओवरराइट होने के बजाय रिलीज़ किया गया था:```
kd> !analyze -v
EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - Access violation
FAULTING_MODULE: win32kfull
(ऑफ़सेट, पते और पुनरुत्पादन विवरण समन्वित प्रकटीकरण के अंतर्गत रोके गए हैं।)
प्रभाव क्रैश से सिद्ध नहीं होता। यह क्रैश कौन पैदा कर सकता है से सिद्ध होता है। हर ट्रिगर रन लक्ष्य पर एक मानक, गैर-प्रशासक उपयोगकर्ता खाते से निष्पादित किया गया था, क्योंकि एक कर्नेल फ़ॉल्ट जो केवल पहले से विशेषाधिकार प्राप्त संदर्भ से पहुँच योग्य है, स्थिरता बग है, सुरक्षा बग नहीं। टक्कर चलाने वाली प्रक्रिया के इंटीग्रिटी स्तर की जाँच एक तीस सेकंड का कदम है जो तय करता है कि आपके पास बाउंटी मामला है या Windows Feedback Hub प्रविष्टि।
मुख्य अनुशासन: मैंने किसी एक क्रैश पर विश्वास नहीं किया। रेस पर एक अकेला क्रैश शोर है। जिसने इसे रिपोर्ट योग्य बनाया वह नियंत्रित समय के अंतर्गत पुनरावृत्ति थी — "ये दो पथ, यह क्रम, यह विंडो" कहने और वही फ़ॉल्ट वापस पाने में सक्षम होना। यही एक ऐसी रिपोर्ट के बीच का अंतर है जिस पर MSRC कार्रवाई कर सकता है और उस रिपोर्ट के बीच जिसे वे not-reproducible के रूप में बंद कर देते हैं।
मैंने डीकंपाइल आउटपुट से तेज़ी से गुज़रने के लिए AI-सहायित ट्राइएज का उपयोग किया, और मैं स्पष्ट रूप से बताऊँगा कि यह किस लिए अच्छा था और किस लिए नहीं।
अच्छा इसके लिए: सतह क्षेत्र कवर करना। बड़ी मात्रा में HLIL पढ़ना और "ये फ़ंक्शन बिना किसी दृश्य सिंक्रोनाइज़ेशन के एक साझा ऑब्जेक्ट को छूते हैं" को फ़्लैग करना पैटर्न-मिलान है, और बड़ी मात्रा में पैटर्न-मिलान ठीक वही है जो ये उपकरण अच्छा करते हैं। इसने सरसरी पढ़ाई के हफ़्तों को दिनों में समेट दिया।
बेकार इसके लिए: निर्णय। यह आत्मविश्वास से एक हमले की श्रृंखला सुनाएगा जो मौजूद नहीं है, पहुँच क्षमता का दावा करेगा जो उसने स्थापित नहीं की है, और एक ऐसे बग के लिए सुंदर ढंग से संरचित राइटअप तैयार करेगा जो वहाँ है ही नहीं। हर एक निष्कर्ष को रिपोर्ट के पास कहीं भी जाने से पहले WinDbg में मैन्युअल सत्यापन से गुज़रना पड़ा।
जिस विफलता-मोड से डरना चाहिए वह उपकरण का गलत होना नहीं है। यह उपकरण का गलत होते हुए भी धाराप्रवाह होना है, रात के 2 बजे, जब आप चाहते हैं कि यह सही हो।
MSRC को भेजा गया एक गढ़ा हुआ निष्कर्ष उनके इंजीनियरों का वास्तविक समय लेता है और आपकी उस प्रतिष्ठा की कीमत चुकाता है जिसे आप जल्दी दोबारा नहीं बना सकते।
| दिनांक | घटना |
|---|---|
| May 7, 2026 | सबमिट किया गया — VULN-186460 |
| May 7, 2026 | केस खोला गया — MSRC केस 11xxxxx |
| Jun 11, 2026 | Microsoft द्वारा व्यवहार की पुष्टि की गई; बाउंटी समीक्षा खोली गई |
| Jun 27, 2026 | जुलाई रिलीज़ के लिए फिक्स निर्धारित; CVE-2026-54107 आवंटित (प्री-रिलीज़) |
| Jun 30, 2026 | बाउंटी प्रदान की गई — US$8,000, Windows Insider Preview Bounty Program |
| Jul 14, 2026 | पैच जारी; CVE प्रकाशित |
सबमिशन और पुष्टि के बीच पाँच सप्ताह का मौन। यही वह हिस्सा है जो आपकी परीक्षा लेता है। आपने किसी और के कर्नेल के बारे में एक दावा लिख दिया है और आपको अभी तक पता नहीं है कि आपका तर्क सही है या नहीं, यह डुप्लिकेट है या नहीं, या यह उनके बिल्ड पर पुनरुत्पादित होता भी है या नहीं। पुष्टि ईमेल वह क्षण है जब यह आपके पास मौजूद एक सिद्धांत नहीं रह जाता और एक विद्यमान भेद्यता बन जाता है।
एक छोटी सी बात जिसने मुझे खुद पर हँसा दिया: 14 जुलाई को मेरे समय में, मैंने केस को पिंग करके पूछा कि CVE प्रकाशित क्यों नहीं हुआ। उत्तर, विनम्रता से: सिएटल में 13 जुलाई है। Microsoft का रिलीज़ कैलेंडर प्रशांत समय पर चलता है। अब मुझे पता है।
| फ़ील्ड | विवरण |
|---|---|
| CVE | CVE-2026-54107 |
| MSRC केस | 11xxxxx (VULN-186460) |
| घटक | win32kfull.sys — विंडो ऑब्जेक्ट लाइफसाइकिल |
| वर्ग | रेस कंडीशन → use-after-free |
| CWE | CWE-362 |
| प्रभाव | विशेषाधिकार उन्नयन |
| गंभीरता | महत्वपूर्ण (MSRC) |
| CVSS v3.1 | 8.8 (उच्च) |
| वेक्टर | स्थानीय, प्रमाणित |
| कार्यक्रम | Windows Insider Preview Bounty Program |
| पुरस्कार | US$8,000 |
| फिक्स | जुलाई 2026 सुरक्षा अद्यतन |
इनवेरिएंट के लिए पढ़ें, बग के लिए नहीं। "यह कोड कहाँ कुछ ऐसा मान लेता है जिसे वह लागू नहीं करता?" "ओवरफ़्लो कहाँ है?" से अधिक खोजता है — विशेष रूप से परिपक्व, व्यापक रूप से ऑडिट किए गए घटकों में जहाँ आसान श्रेणियाँ खत्म हो चुकी हैं।
रेस एक अंतर-प्रक्रियात्मक (interprocedural) दावा है। आप इसे एक अकेले फ़ंक्शन से स्थापित या खंडित नहीं कर सकते। यदि आपका विश्लेषण फ़ंक्शन की सीमा पर रुक जाता है, तो आप ऐसे उम्मीदवार उत्पन्न करेंगे जिन्हें आप कभी बंद नहीं कर पाएँगे।
आपका डिबगिंग वातावरण ही काम है। मैंने अस्थिर सीरियल COM लिंक पर वास्तविक खोज की तुलना में अधिक घंटे खोए, और एक टूटी हुई पाइपलाइन झूठे नकारात्मक पैदा करती है जो "यहाँ कुछ नहीं है" के समान दिखते हैं। मैंने लगभग एक COM पोर्ट सेटिंग के कारण इस लक्ष्य को छोड़ दिया था।
कारण रिपोर्ट करें, क्रैश नहीं। MSRC को क्रैश मिलते हैं। जो किसी मामले को आगे बढ़ाता है वह एक सुसंगत कहानी है कि कौन सा इनवेरिएंट टूटा और प्रवर्तन वहाँ क्यों नहीं था।
पुष्टि का मतलब पूर्णता नहीं है। पुष्टि और जारी किए गए पैच के बीच पुनरुत्पादनीयता अनुवर्ती, कैनरी व्यवहार और समीक्षा होती है। जुड़े रहें।
वही पद्धति, अलग सतहें — tcpip.sys, afd.sys, clfs.sys। मैं एक भाग्यशाली खोज के बजाय कार्यों के एक समूह के लिए जाना जाना पसंद करूँगा, और ऐसा होने का एकमात्र तरीका यह है कि मैं उम्मीदवारों को उत्पन्न करने की तुलना में तेज़ी से खत्म करता रहूँ।
यदि आप वहाँ हैं जहाँ मैं एक साल पहले था — वेब बाउंटी से आते हुए, कर्नेल कार्य के बारे में उत्सुक, अनिश्चित कि क्या आप उस तरह के व्यक्ति हैं जो यह कर सकता है — तो आप इसे करके ही पता लगाते हैं।
एक ड्राइवर चुनें। डिबगर अटैच करें। धीरे-धीरे पढ़ें। पूछते रहें कि यदि यह दो बार चलता है तो क्या होता है।
वास्तव में यही वह जगह है जहाँ यह शुरू होता है।
लेखक: Pravin Choudhary (@pr4v1nx) — स्वतंत्र आक्रामक सुरक्षा शोधकर्ता। समन्वित प्रकटीकरण के अंतर्गत Microsoft को प्रकट किया गया। शोषण विवरण, ऑफ़सेट और पुनरुत्पादन कोड जानबूझकर रोके गए हैं।