If you are here, I assume you have read "Windows Internals You Need To Know Before Kernel Exploitation" and "The Kernel Attack Surface: How Windows Internals Enable Exploitation" already. If you haven't, I recommend you go through them first.
The series has walked three legs of the kernel attack surface so far. The ThrottleStop article walked the physical-memory leg — a signed driver handing MmMapIoSpace to any caller. The CVE-2024-30088 article walked the virtual-memory leg — a pointer race inside ntoskrnl.exe's own copyout path. The CVE-2026-21241 article walked the kernel-pool leg — a use-after-free in afd.sys, reclaimed with a spray and pivoted through kCFG-legal calls into a single bit flipped in the token. This article walks the fourth leg: the Object Manager — Section 4 of the attack-surface post, the section that documented token overwrites, privilege bitmasks, and handle-table manipulation as the endgame every write primitive aims at. There is a reason this leg comes last. Every exploit in this series has ended by writing to the token. This bug begins there.
The setting is what makes this bug interesting. CVE-2025-62215 is an actively exploited, CISA KEV-listed zero-day that lived in ntoskrnl.exe — no vulnerable driver, no IOCTL, no BYOVD. It is a race condition that produces a double free, and the object being double-freed is part of the token's own bookkeeping: the structure the kernel uses to decide which token can share which memory with which other token. The attack surface aimed at its own endgame. And the patch tells a story the series has not seen yet: the CVE-2024-30088 fix removed the racy access with a staging buffer, and the afd.sys class of fixes serialized the racy window — but you cannot stage a counter. A reference count is shared data, and the only fix for shared data racing is to make every touch of it atomic. The patch is a lock, and the patch being a lock is the whole lesson.
#The Object Manager as an Attack Surface
In Section 4 of The Kernel Attack Surface, the Object Manager was mapped through what it enforces: access checks on handles, the token as the prize, _EPROCESS.Token as the one-pointer-write endgame, the privilege bitmaps, handle-table forging, PPL stripping. Every technique in that section assumed you already had a write primitive and asked what to aim it at. This article walks the section from the other side: what the Object Manager is, mechanically, and where it breaks on its own.
The Object Manager gives every kernel resource a uniform identity: an _OBJECT_HEADER with a type, a name, a security descriptor, and a reference count. The reference count is the lifetime law of the entire subsystem. ObReferenceObject increments it; ObDereferenceObject decrements it; when it reaches zero, the object dies. Handles are indirect references — each open handle pins the underlying object. Tokens, the objects this bug orbits, are managed the same way, and so are their sub-objects. The law is simple:
reference count > 0 → the object exists, and touching it is safe
reference count == 0 → the object is freed, and touching it is a bugThe entire security model of the Object Manager — the access checks, the handle tables, the protected processes — sits on top of this arithmetic. The count is not protected by the MMU, not covered by PatchGuard, not watched by any mitigation. It is a plain integer in a pool allocation, incremented and decremented by Interlocked operations. Interlocked means atomic — one InterlockedDecrement cannot tear mid-flight. It does not mean serialized — nothing stops a decrement, a read, and an increment from interleaving around each other. An atomic counter races exactly as freely as a non-atomic one; it just never produces a torn intermediate value. The race CVE-2025-62215 exploits lives in the space between those two properties.
#The SID Values Block
The most technically specific public analysis locates the bug in a small optimization most people have never heard of, and the optimization deserves a section of its own — because it is a textbook example of how attack surface grows in the quiet corners of a subsystem.
A Windows access token carries the user's SID, group SIDs, and the other identity claims — SEP_TOKEN_PRIVILEGES, the mandatory label, the whole _TOKEN structure Section 4 walked through. That is a lot of per-token memory, and much of it is identical across related tokens. When a token is duplicated — NtDuplicateToken, the syscall underneath DuplicateTokenEx, LogonUser, impersonation setups, half of the Win32 security API — the naive implementation copies all of that identity data into a fresh allocation.
The SepTokenSidSharingEnabled optimization skips the copy. Instead of duplicating the SID data, the kernel shares it: the duplicated token and the original point at one shared, read-only SID Values Block, and a ReferenceCount field inside the block tracks how many tokens are sharing it. When the last sharer releases, the block is freed. On _TOKEN (Windows 11 24H2 offsets, per the public PoC's debugger work — treat as build-specific):
// The sharing structure (layout from the public PoC's WinDbg analysis —
// offsets are build-specific and must be re-derived per target):
// _TOKEN + 0x468 → SepSidValues : pointer to the SID Values Block
//
// SID Values Block:
// +0x00 ULONG Length // size of the block
// +0x08 LONGLONG ReferenceCount // number of tokens sharing this block
typedef struct _SEP_SID_VALUES_BLOCK { // name illustrative
ULONG Length;
LONGLONG ReferenceCount; // the counter at the center of the race
// ... SID data follows, read-only while shared ...
} SEP_SID_VALUES_BLOCK, *PSEP_SID_VALUES_BLOCK;The contract the Object Manager promises with this block is the same contract it promises everywhere:
// The share path — one token hands the block to another:
// (reached from NtDuplicateToken when the optimization is active)
VOID SepReferenceSidValuesBlock(PSEP_SID_VALUES_BLOCK block) {
InterlockedIncrement64(&block->ReferenceCount); // block stays alive: +1 sharer
}
// The release path — one sharer lets go:
VOID SepDereferenceSidValuesBlock(PSEP_SID_VALUES_BLOCK block) {
if (InterlockedDecrement64(&block->ReferenceCount) == 0) {
ExFreePoolWithTag(block, 'kcoB'); // tag per the public PoC — last sharer frees
}
}Both operations are atomic. Neither path takes a lock around the decision. The decrement returns zero, and the free follows it as a separate step. That gap — between "the counter says zero" and "the memory is actually gone" — is where the bug lives, because the increment path is reading and writing the same counter concurrently, and nothing about atomicity prevents this interleaving:
Thread A (duplicating a token) Thread B (releasing a token)
───────────────────────────────── ─────────────────────────────────
InterlockedDecrement → count 1→0
→ "I hold the last reference" → free path armed
InterlockedIncrement → count 0→1
→ "the block is shared with the new
duplicate token, alive and well"
ExFreePoolWithTag(block) ← FIRST FREE
...duplicate token proceeds, pointing
at a freed block...
SepDereferenceSidValuesBlock(duplicate)
→ ExFreePoolWithTag(block) ← SECOND FREE (double free)Read the middle three lines again. Thread B's decrement produced zero — from its point of view, correctly, because the duplicate's increment had not landed yet. Thread A's increment then resurrected the counter — from its point of view, correctly, because the block was referenced when A looked at it. Both threads are individually correct. The composition is fatal: the block is freed while a live token points at it, and when that token eventually releases, it frees the same memory again. That is the double free Microsoft's CWE classification names, produced by the race its CWE-362 classification names.
#CVE-2025-62215 Overview
CVE-2025-62215 is a Windows Kernel elevation-of-privilege vulnerability — a race condition producing a double free — in ntoskrnl.exe, patched in the November 11, 2025 Patch Tuesday and added to CISA's Known Exploited Vulnerabilities catalog the next day. This is the series' first KEV-listed, actively exploited bug — a sharp departure from CVE-2024-30088, a Pwn2Own research demo that never appeared in the wild. Attackers were using this one before anyone outside MSTIC knew it existed.
| Field | Value |
|---|---|
| CVE | CVE-2025-62215 |
| Class | Race condition (CWE-362) → double free (CWE-415) |
| Component | Object Manager / token bookkeeping (ntoskrnl.exe) — per the most specific public analysis, the SID Values Block reference count behind SepTokenSidSharingEnabled, reached via NtDuplicateToken |
| CVSS | 7.0 High (CVSS 3.1) — AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H (Attack Complexity: High — it is a race); Microsoft's own severity label is "Important" |
| Affected builds | All supported editions: Windows 10 1809–22H2 (incl. ESU), Windows 11 22H2–25H2, Server 2019–2025. Example fixed builds per public writeups: 10.0.22631.6199 (Win11 23H2, KB5068865), 10.0.19045.6575 (Win10 22H2, KB5068781) — verify per configuration |
| Fixed in | November 11, 2025 cumulative updates |
| Discovery | MSTIC + MSRC, during investigation of active exploitation — not proactive auditing |
| CISA KEV | Added 2025-11-12; federal remediation due 2025-12-03 |
| Status | Exploited in the wild — a zero-day, undisclosed before the November 2025 patch. No functional public exploit exists; the public PoCs are a driver-assisted demo and a fake |
| EPSS / ATT&CK | Top-tier exploited cohort; T1068 (Exploitation for Privilege Escalation) |
Two rows deserve emphasis against the series' prior CVEs. First, discovery: CVE-2024-30088 was found by a security researcher and demoed at Pwn2Own Vancouver 2024. CVE-2025-62215 was found because malware was already using it — the same relationship the ThrottleStop article documented for BYOVD: the offensive ecosystem is ahead of the defensive one. Second, attack complexity: AC:H in the CVSS vector, same as CVE-2024-30088 — a race you must win, not a check you must fool. The in-the-wild record proves the race is winnable in practice, repeatedly, by commodity actors.
#The Root Cause: A Reference Count Is Data, and Data Races
Place this bug next to the two races the series has already walked, because the three together sketch the full taxonomy of kernel lifetime bugs:
- CVE-2024-30088 — the kernel trusted memory it does not own (a user-mode buffer) between a write and a read-back. The fix removed the trust: stage the data in kernel memory, stop reading user memory.
- CVE-2026-21241 — two kernel paths disagreed about a window around a spinlock release, and the attacker widened the window with a second thread. The fix serialized the window.
- CVE-2025-62215 — two kernel paths disagree about a counter. Not a pointer into user memory, not a window around a lock — a plain integer whose value each path reads, acts on, and writes back through atomic operations that were never a substitute for serialization.
The third shape is the most general of the three, because a reference count is the load-bearing wall of the Object Manager — and of every allocator layered above it. The pattern to audit for is any code that makes a lifetime decision from a counter without holding a lock across the decision:
// The vulnerable shape, generalized. Every line is individually correct.
// The composition is the bug.
// RELEASE side:
if (InterlockedDecrement(&obj->RefCount) == 0) {
Free(obj); // decision made from the counter's value...
}
// ...but between the decrement returning 0 and Free() completing, an
// interlocked INCREMENT on another core can resurrect the same object.
// ACQUIRE side:
InterlockedIncrement(&obj->RefCount); // "I have a reference now" — except the
// object may already be in Free() on
// the other core. The +1 lands on a corpse.The error is a category error about what Interlocked buys you. InterlockedIncrement guarantees that two concurrent increments produce 2, not 1 — the operation is atomic. It guarantees nothing about the ordering of decisions made from the values it returns. "I saw zero, therefore I free" is a decision, and decisions need locks, not atomic instructions. Every kernel-side lifetime bug from the double-free in this CVE to the classic refcount UAFs in file systems and I/O is this same category error wearing different structure layouts.
#Triggering the Bug
The reachability question for the SID Values Block narrative has a short answer: token duplication is one of the most ordinary operations in the Win32 security API. NtDuplicateToken is the syscall underneath DuplicateTokenEx, and duplication happens constantly — service logons, impersonation, restricted-token creation, UAC elevation plumbing. The attacker's own process can duplicate its own token at will. No driver, no special privilege, no handle to somebody else's anything.
The gating condition is the optimization itself: the sharing path (and therefore the racy refcount) is only active when the kernel enables SepTokenSidSharingEnabled — per the public PoC's analysis, on low-memory systems. On a machine where the optimization is off, NtDuplicateToken copies the SID data and no shared block exists to race. On a machine where it is on, every duplication is a fresh attempt:
// Trigger harness (reconstruction) — a token-duplication storm.
// Every duplication that takes the sharing path is one attempt at the
// interleaving. The attacker supplies volume and scheduling; the kernel
// supplies both racing sides.
// Pin the duplication loop and a release loop to separate cores so the
// increment and decrement paths run concurrently, not time-sliced.
SetThreadAffinityMask(hDuplicatorThread, 1 << 0);
SetThreadAffinityMask(hReleaserThread, 1 << 1);
DWORD WINAPI DuplicatorThread(LPVOID param) {
HANDLE hToken;
OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, &hToken);
while (g_running) {
HANDLE hDup = NULL;
// NtDuplicateToken → SepReferenceSidValuesBlock (+1) —
// the ACQUIRE side of the race, in a tight loop.
DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL,
SecurityImpersonation, TokenImpersonation, &hDup);
g_dupHandles[g_dupCount++ % MAX_DUPS] = hDup; // stash; release later
CloseHandle(hDup); // release → SepDereferenceSidValuesBlock (-1)
// — the RELEASE side, same loop
}
return 0;
}One honest caveat, in the spirit of the series' sourcing rules: the public PoC uses a helper driver to place a controlled block and force the racing free on demand — a demonstration tool, because the researcher needed deterministic wins to study the crash. The in-the-wild exploit, whatever it actually was, needed no driver (the victims were ordinary users), and MSTIC never published its trigger. The harness above is the user-mode shape of that trigger — duplication and release churn, pinned cores, sustained volume — reconstructed from what the bug class requires. What is certain is the arithmetic: every attempt is a coin flip on the interleaving, the loop flips the coin thousands of times per second, and the attackers only needed it to land once per machine.
#The Exploitation: From Double Free to a Token Flip
A double free is a corruption of the allocator's free list, and on the modern Windows kernel pool that corruption is worth exactly as much as the attacker can make it. The chain below follows the same design discipline the afd.sys article used: the bug gives a corrupted free list; everything after that is pool craft the series has already walked.
#Stage 1 — Win the Interleaving
The race must produce the fatal order: a release-side decrement to zero, then an acquire-side increment on the freed block, then the release-side free, then the duplicate token's eventual release freeing the same slot again. The window is instruction-scale, but the attacker controls the volume of attempts and the core layout — the same math as the I/O Ring race and the AFD spinlock race. Losing attempts are not crashes; the decrement that does not race simply frees normally, and the loop tries again. The race is free to lose and only needs to win once.
#Stage 2 — Understand What the Double Free Gives You
The second ExFreePoolWithTag hands the allocator a block it already believes is free. The exact consequence depends on the pool's free-list state at that instant, but the family of outcomes is the same in every variant:
- The same slot enters the free list twice. Two free-list entries now point at one physical allocation.
- Two subsequent allocations are served the same memory. Allocation A (attacker-controlled spray data) and allocation B (a real kernel object the kernel is about to use) land on the same address.
- The result is a type confusion by construction — the kernel operates on object B's fields while attacker-chosen bytes from allocation A are sitting in them. If the kernel writes object B through this slot, it corrupts A's controlled data; if it reads or calls through it, it consumes attacker-chosen values. This is the overlapping-allocations technique Section 5 of the attack-surface post documented, arrived at through a race instead of an overflow.
This is also the moment to repeat the fake-PoC lesson from the sourcing callout, because it is the single most common giveaway in public PoCs for this class: user-mode VirtualAlloc cannot spray kernel pool. The spray must be made of user-mode-reachable kernel allocations — objects the attacker can create by the thousands with attacker-influenced contents. The working set, per the series' prior posts:
// Same-bucket spray (pattern from Section 5 of the attack-surface post;
// worked in full in the AFD article). The SID Values Block is a small
// allocation — match the freed block's bucket and flood it.
HANDLE spray[8192];
for (int i = 0; i < 8192; i++) {
// Fixed-size kernel allocations, attacker-influenced contents, no
// driver and no special privilege — NtAllocateReserveObject with
// IoCompletionReserve, named-pipe attribute buffers, WNF state objects.
pNtAllocateReserveObject(&spray[i], NULL, 1 /* IoCompletionReserve */);
}
// One of these lands in the double-freed slot — twice, in fact, because
// the slot is on the free list twice. Two spray allocations may receive
// the same address; the kernel's next same-size object allocation can
// receive it too. The overlap is the primitive.#Stage 3 — Weaponize the Overlap
What the attacker does with an overlapping allocation depends on which real kernel object lands on the sprayed slot — the attacker does not fully control the victim, only the bucket. The series-standard options, in ascending order of value:
- Overlapping spray objects — controlled data where the kernel expects another spray object: usually worthless, both sides are attacker data.
- Overlapping a kernel object with function-pointer-bearing fields — the double-fetch/overwrite game, constrained by kCFG: the call target must be a valid CFG entry, which is why the afd.sys exploit pivoted through legitimate
nt!functions. Same rules apply here. - Overlapping a data object the attacker wants to modify — and here the bug's home territory becomes an advantage. The corrupted structure is token bookkeeping. The objects living in these buckets include the tokens themselves and their sub-structures. An attacker who lands a real
_TOKEN-family allocation on the sprayed slot is one write away from the fields Section 4 of the attack-surface post has been cataloguing all along.
#Stage 4 — The Endgame: the Token, Again
The endgame is the series' oldest friend, and the irony is the closing point of the whole article: the bug starts in the token's lifetime machinery and ends in the token's privilege fields. Whichever write the overlap yields, the target selection follows the template Section 4 documented and the two prior exploits executed — and the afd.sys post's reasoning for which token target carries over verbatim:
// Endgame (data-only, KDP-evading — the afd article's Stage 4 template):
// Flip SeDebugPrivilege in the attacker's OWN token. One bit, no
// SID change, no token-pointer swap, no control flow.
// SE_DEBUG_PRIVILEGE = bit 20 in the privilege bitmaps:
privs.PrivilegesPresent |= (1ULL << 20); // present
privs.PrivilegesEnabled |= (1ULL << 20); // enabled
// The process can now OpenProcess(PROCESS_ALL_ACCESS) on any process
// in the system — lsass included — and the standard toolbox follows.Why the bit flip and not the classic _EPROCESS.Token steal from Section 4's first technique? The same reasons the afd.sys exploit chose it: it is the smallest possible ask of a constrained primitive (one bit, not one pointer), it changes no user SID (nothing trips identity-comparing monitoring), and it is a pure data write — CET's shadow stack, kCFG's dispatch table, and HVCI's page permissions watch nothing that happens. The stolen-token path remains available when the primitive is a full arbitrary write; the bit flip is the correct spend for a corruption the attacker only partially controls.
#The Patch
The November 2025 fix serializes the reference counting — per the public patch analyses, the Object Manager's refcount operations around this path were placed under proper locking, so that the decrement-to-zero-and-free decision and any concurrent increment cannot interleave. A concurrent acquire either completes before the release decision (the free correctly does not happen) or blocks until after it (the block is correctly gone and the acquire fails).
Notice what this fix is, against the series' two prior patches: it is a lock. The CVE-2024-30088 fix was a staging buffer — stop trusting the pointer. The CVE-2026-21241 fix serialized a window. This fix makes the access atomic as a unit — decision and action together. The pattern across the three is not three random remedies; it is the correct answer chosen per bug shape:
The audit corollary: every InterlockedDecrement(...) == 0 in the kernel followed by a free is a candidate for this bug class, and the audit is as mechanical as the call-site audit that found CVE-2024-30088. The pattern to grep for is not the counter — it is the decision made from the counter without a lock spanning it. The SID Values Block is where that audit found a winner this cycle. It is not where the pattern ends.
#Why It Bypasses the Mitigation Stack
CVE-2025-62215 is a data-only lifetime race, and the mitigation audit lands the way it landed for the two prior data-only attacks in this series — with one addition: this one has a KEV entry proving it worked in the wild.
| Mitigation | Does it stop CVE-2025-62215? | Why |
|---|---|---|
| SMEP | No | No user-mode code is executed in kernel context — the attacker never queues code, only token-duplication syscalls |
| SMAP | No | Every address the racing paths touch is kernel pool; no user page is dereferenced by the kernel |
| CET / shadow stack | No | No return address is overwritten; no control flow is hijacked anywhere in the chain |
| kCFG | No | No indirect call is redirected; the chain ends in a data write, never a call through corrupted state |
| HVCI | No | No new executable page is created — the entire chain is pool arithmetic and data writes |
| KASLR | Partial | The trigger needs no kernel addresses; the spray needs the bucket, not the base. The endgame needs token-field offsets, resolvable by the same object-address queries the prior articles used |
| KDP | No | KDP protects static kernel data in EPT. The SID Values Block, the free-list state, and the token bitmap are dynamic pool allocations — none is on any KDP protection list |
The KDP row is the same honest note the CVE-2024-30088 article made: KDP is the mitigation built for data-only attacks, and it does not stop this one — not because it was defeated, but because the targets were never on its list. A reference count in a pool allocation is the definition of dynamic data. KDP's list cannot grow to cover every counter in the kernel; the fix for this bug class is serialized code, not protected pages.
And the row the table cannot express: the attack-complexity row of the CVSS vector (AC:H — a race) is the only mitigation-adjacent property that ever slowed this bug down, and the KEV entry is the empirical proof that it did not slow it down enough. Fully patched machines, every mitigation on, commodity actors, SYSTEM.
#Detection
The race and its endgame leave behavioral footprints, and because this bug has an in-the-wild record, these indicators are worth tuning with unusual care:
- Token-API storms from a low-privilege process. The trigger is a duplication/release churn —
NtDuplicateTokenandOpenProcessTokenat volumes no legitimate process produces. A standard-user process issuing thousands of token duplications per second is not a workload Windows generates. This is the same syscall-storm signature the CVE-2024-30088 detection section documented, one subsystem over. - Handle-operation bursts. The churn's handle side shows up as Event ID 4656/4658 storms against the attacker's own process token — individually legal operations, anomalous in aggregate rate. Baseline per-process token-API rates and alert on variance, not on any single event.
- Failed-attempt bugchecks. Mistimed attempts do not always vanish — a lost race can free a block still in active use and bugcheck the machine.
BAD_POOL_HEADER(0x19) andPAGE_FAULT_IN_NONPAGED_AREA(0x50) on machines running pre-November-2025 builds, with the crashing access in token/duplication call paths, are the fingerprint of someone probing this exact bug — and a machine that crashes this way has been interacted with, which makes the crash a detection event, not just an outage. - The endgame's signature, regardless of the entry bug. The series' recurring tripwire: a process whose
_TOKENprivilege bitmaps change without a corresponding Event ID 4672 (special privileges assigned) or 4624 (logon) lineage. EDRs that snapshot token privilege state between observations catch exactly this diff — and they catch it whether the attacker arrived through this CVE, a BYOVD write, or anything else. - Patch-compliance is the primary control. A KEV entry with a due date means this is not a detection problem anymore; it is a hygiene problem. Pre-November-2025 cumulative updates on any multi-user or internet-exposed host, checked against the November 2025 CU per configuration, is the control CISA's BOD 22-01 actually mandates — and for once, the compliance answer and the security answer are the same.
#Key Takeaways
-
The Object Manager's lifetime law is arithmetic, and arithmetic races. Every object in the subsystem lives and dies by a reference count — a plain integer that no mitigation watches. CVE-2025-62215 is two correct kernel paths disagreeing about one counter, and the disagreement being fatal is not a memory-safety bug, a probe bug, or a control-flow bug. It is a serialization bug.
-
Atomic is not serialized.
InterlockedIncrementguarantees the counter never tears — it guarantees nothing about the decisions made from the values it returns. "I saw zero, therefore I free" is a decision, and decisions need locks. EveryInterlockedDecrement == 0followed by a free, without a lock spanning decision and action, is a candidate for this bug class — and that grep is mechanical. -
Optimizations behind hardware gates are untested attack surface. The racy SID sharing only activates on low-memory systems — a fraction of the install base, a fraction of the testing, and none of the adversarial review that always-on paths collect. Audit the branches gated on feature flags, velocity gates, and hardware conditions; the least-trodden path is where the bugs live.
-
The bug class produced the double free, and the double free is a type-confusion engine. A slot freed twice enters the free list twice; two allocations receive one address; a kernel object lands on attacker-controlled bytes. The exploitation after that is the pool craft the series walked in the afd.sys article — same buckets, same sprays, same endgame.
-
The token was the endgame of every exploit in this series — and this time it was the entry point too. The bug lives in the token's own sharing machinery (
NtDuplicateToken, the SID Values Block) and ends in the token's privilege bitmaps. Section 4 of the attack-surface post mapped the token as the target; this CVE is the map's proof that the target is also a door. -
The patch is a lock, and that is the correct fix for a counter. You cannot stage a shared counter the way you stage a pointer (CVE-2024-30088's buffer) or close a handoff window (the afd class). When the racy object is data that all parties must agree on, the only remedy is mutual exclusion. Three patches in this series, three different shapes, three correct remedies — bug shape dictates fix shape.
-
KEV is the difference between a lesson and an incident. CVE-2024-30088 was a research demo; CVE-2025-62215 was found because malware was already using it, on fully patched machines, with every mitigation on. The bug class this series keeps arriving at — logic races producing data-only writes — is not the future of kernel exploitation. It is the present, with a catalog entry and a remediation deadline.
-
Public PoCs lie sometimes. The loudest PoC for this CVE is fake — nonexistent API, impossible spray. After a KEV listing, the repo ecosystem fills with noise written for the attention, not the vulnerability. Verify the syscalls exist and the spray primitives actually allocate in kernel space before you spend a snapshot on anyone's exploit.
Further reading:
- Microsoft Security Update Guide — CVE-2025-62215 — the official advisory and affected-configuration list
- NVD — CVE-2025-62215 and the CISA KEV catalog entry (added 2025-11-12, due 2025-12-03)
- Help Net Security — Patch Tuesday coverage of CVE-2025-62215 — the exploited-in-the-wild context and analyst commentary
- KernelSight — CVE-2025-62215 case study — the Object-Manager cleanup-race analysis and PoC references
- CVEReports — "Race for the Kernel: Anatomy of the Windows Handle Double-Free" — the reconstructed exploit walkthrough (note: AI-generated, validate before relying on it)
- gowonisgood/CVE-2025-62215-POC — the most technically specific public analysis: the SID Values Block refcount race,
_TOKEN+0x468, and the driver-assisted demonstration harness - uky007/CVE-2025-62215_analysis — the analysis that documented the fake
abrewer251PoC, and whyVirtualAlloccannot spray kernel pool - The companion posts in this series: Windows Internals You Need To Know Before Kernel Exploitation, The Kernel Attack Surface, the CVE-2024-30088 TOCTOU race, and the CVE-2026-21241 afd.sys UAF
- Windows Internals, Part 1 (Yosifovich, Ionescu, Russinovich, Solomon) — object manager reference counting and the token object