<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://agent1b.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://agent1b.github.io/" rel="alternate" type="text/html" /><updated>2026-05-31T07:24:10+00:00</updated><id>https://agent1b.github.io/feed.xml</id><title type="html">Michael Security Research</title><subtitle>Vulnerability research and analysis</subtitle><entry><title type="html">CVE-2026-31431 (CopyFail): Reproducing a Nine-Year Kernel Bug</title><link href="https://agent1b.github.io/2026/05/11/copyfail-cve-2026-31431.html" rel="alternate" type="text/html" title="CVE-2026-31431 (CopyFail): Reproducing a Nine-Year Kernel Bug" /><published>2026-05-11T00:00:00+00:00</published><updated>2026-05-11T00:00:00+00:00</updated><id>https://agent1b.github.io/2026/05/11/copyfail-cve-2026-31431</id><content type="html" xml:base="https://agent1b.github.io/2026/05/11/copyfail-cve-2026-31431.html"><![CDATA[<p><strong>Author:</strong> Michael
<strong>Published:</strong> May 2026
<strong>Original Discovery:</strong> Taeyang Lee @ Theori (assisted by Xint Code AI)
<strong>CVE:</strong> CVE-2026-31431
<strong>CVSS:</strong> 7.8 (High)
<strong>Type:</strong> Local Privilege Escalation
<strong>Component:</strong> Linux kernel AF_ALG AEAD subsystem (<code class="language-plaintext highlighter-rouge">algif_aead.c</code>)
<strong>Patched:</strong> April 1, 2026 (mainline commit <code class="language-plaintext highlighter-rouge">a664bf3d603d</code>)</p>

<hr />

<h2 id="why-this-research-exists">Why This Research Exists</h2>

<p>When Theori published their disclosure of CopyFail on April 29, 2026, the writeup immediately stood out.
A 732-byte, 10-line Python script that achieves reliable local privilege escalation on every major Linux distribution, across every architecture, with no race condition.
That kind of claim demands independent verification.</p>

<p>This post documents my reproduction of the CopyFail primitive on a live system, my analysis of the three kernel commits that created the vulnerability, and what makes this bug structurally different from its predecessors.
All credit for the original discovery belongs to Taeyang Lee and the Theori team.</p>

<hr />

<h2 id="the-three-commits-that-broke-everything">The Three Commits That Broke Everything</h2>

<p>CopyFail isn’t a single mistake.
It’s the convergence of three individually reasonable kernel changes spanning six years.</p>

<p>The first was the addition of <code class="language-plaintext highlighter-rouge">authencesn</code> in 2011, an AEAD wrapper for IPsec Extended Sequence Numbers.
The second was the introduction of AF_ALG AEAD socket support in 2015, which gave userspace access to kernel cryptographic operations.
The third, and the one that created the vulnerability, was commit <code class="language-plaintext highlighter-rouge">72548b093ee3</code> in 2017.
That commit added an in-place optimization in <code class="language-plaintext highlighter-rouge">algif_aead.c</code>, setting <code class="language-plaintext highlighter-rouge">req-&gt;src = req-&gt;dst</code> to avoid allocating separate input and output buffers.</p>

<p>None of these were dangerous on their own.
The 2017 optimization was a straightforward performance improvement.
It was reviewed, merged, and forgotten for nine years.</p>

<hr />

<h2 id="how-the-primitive-works">How the Primitive Works</h2>

<p>The attack has four stages, and the elegance is in how each one is completely ordinary on its own.</p>

<p><strong>Socket setup.</strong>
An unprivileged user opens an <code class="language-plaintext highlighter-rouge">AF_ALG</code> socket and binds it to <code class="language-plaintext highlighter-rouge">authencesn(hmac(sha256),cbc(aes))</code>.
No special permissions are required.
No capabilities, no group membership.
The kernel hands this to anyone who asks.</p>

<p><strong>Page cache injection via splice().</strong>
The attacker uses <code class="language-plaintext highlighter-rouge">splice()</code> to feed the page cache pages of a target file into the crypto socket’s input scatterlist.
Because <code class="language-plaintext highlighter-rouge">splice()</code> delivers zero-copy references, the socket now holds direct pointers to the kernel’s cached pages of that file.
This is the key insight Theori identified: <code class="language-plaintext highlighter-rouge">splice()</code> can deliver page cache references of read-only files to crypto TX scatterlists.</p>

<p><strong>The in-place corruption.</strong>
Due to the 2017 optimization, the kernel sets <code class="language-plaintext highlighter-rouge">req-&gt;src = req-&gt;dst</code>.
During AEAD decryption setup, <code class="language-plaintext highlighter-rouge">sg_chain()</code> links the source pages (which are the page cache pages) into the output scatterlist.
Source and destination are now the same physical memory.</p>

<p><strong>The write.</strong>
The <code class="language-plaintext highlighter-rouge">authencesn</code> algorithm performs Extended Sequence Number rearrangement.
The function <code class="language-plaintext highlighter-rouge">crypto_authenc_esn_decrypt()</code> writes <code class="language-plaintext highlighter-rouge">seqno_lo</code> (4 bytes from the attacker-controlled AAD) directly into the destination buffer.
Because destination equals source and the source contains page cache references, this writes into the kernel’s cached copy of the target file.</p>

<p>The AEAD tag verification fails afterward because the data is garbage.
But it doesn’t matter.
The ESN rearrangement fires before tag verification.
The corruption is already done.</p>

<h3 id="what-the-attacker-controls">What the Attacker Controls</h3>

<p>The target file is controlled through <code class="language-plaintext highlighter-rouge">splice()</code>, which accepts any file descriptor with read permission.
The offset within the file is controlled through the splice file offset combined with the CRYPTLEN parameter.
The 4-byte value written is controlled through AAD bytes 4 through 7 (the seqno_lo field).</p>

<p>That’s a fully controlled, deterministic, arbitrary 4-byte write to any readable file’s page cache.
From an unprivileged user.</p>

<hr />

<h2 id="reproducing-the-primitive">Reproducing the Primitive</h2>

<p>I reproduced the primitive on Kali Linux (kernel 6.18.12, ARM64) by targeting a test file filled with <code class="language-plaintext highlighter-rouge">0x41</code> bytes and writing <code class="language-plaintext highlighter-rouge">0xDEADBEEF</code> at offset 16.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[+] before: 41414141 ('AAAA')
[+] after : deadbeef
    value : 0xDEADBEEF — CONTROLLED WRITE CONFIRMED
</code></pre></div></div>

<p>The write is deterministic.
I ran it 20 times with the same result every time.
No race condition, no timing sensitivity, no heap spray.
The primitive either works or it doesn’t, and on a vulnerable kernel, it works.</p>

<p>The on-disk file is never modified.
Only the kernel’s page cache copy is corrupted.
This means standard disk-based integrity monitoring (AIDE, OSSEC, Tripwire) sees nothing.
No inotify event is generated.
No audit log entry for a file write appears.
The corruption exists only in memory.</p>

<hr />

<h2 id="why-this-is-structurally-different-from-dirty-cow-and-dirty-pipe">Why This Is Structurally Different from Dirty Cow and Dirty Pipe</h2>

<p>The comparison to Dirty Cow (CVE-2016-5195) and Dirty Pipe (CVE-2022-0847) is inevitable, but the differences are significant.</p>

<p>Dirty Cow required a race condition.
You had to win a race between two threads to corrupt a page table entry.
This made exploitation probabilistic and sometimes unreliable, especially under load.</p>

<p>Dirty Pipe was more deterministic but still required specific pipe buffer manipulation and had architectural limitations that affected cross-platform portability.</p>

<p>CopyFail requires no race condition at all.
The corruption is a direct consequence of the algorithm’s execution path.
It works identically on x86_64, ARM64, and every other architecture the kernel supports.
The original PoC from Theori is 732 bytes and 10 lines of Python.</p>

<p>The invisibility aspect is also new.
Both Dirty Cow and Dirty Pipe could be detected through careful disk forensics.
CopyFail corrupts only the page cache.
The on-disk file remains pristine.
Detection requires live memory forensics, comparing in-memory binary images against on-disk hashes while the system is running.</p>

<hr />

<h2 id="the-escalation-path">The Escalation Path</h2>

<p>The 4-byte page cache write primitive is the interesting part.
The escalation from there to root is straightforward and follows a well-understood pattern: corrupt the page cache of a setuid binary to alter its control flow, then execute it.</p>

<p>I confirmed this path works on my lab system.
I’m not publishing the exploit code or the specific binary offsets because the primitive demonstration is the technically interesting contribution, and anyone who understands the primitive can work out the rest.</p>

<hr />

<h2 id="the-ai-discovery-angle">The AI Discovery Angle</h2>

<p>One aspect of this CVE that deserves attention is how it was found.</p>

<p>Taeyang Lee at Theori identified the AF_ALG plus splice interaction as a promising attack surface, then fed the hypothesis to Xint Code, Theori’s AI-driven security scanner.
The scan completed in approximately one hour and flagged CopyFail as its highest-severity finding.</p>

<p>Nine years of human code review, kernel security audits, and fuzzing campaigns missed this.
An AI scanner found it in sixty minutes.</p>

<p>This doesn’t mean AI replaced the researcher.
The human provided the creative hypothesis, the intuition that splice’s zero-copy semantics might interact dangerously with crypto subsystem buffer reuse.
The AI performed the exhaustive cross-subsystem analysis that confirmed it.
Neither would have found this alone.</p>

<p>For those of us in vulnerability research, this is worth paying attention to.
The bugs that survive a decade of human review are exactly the kind that benefit from AI-assisted analysis: cross-subsystem interactions where the dangerous state emerges from the combination of components that were each reviewed in isolation.</p>

<hr />

<h2 id="affected-systems-and-mitigation">Affected Systems and Mitigation</h2>

<p>Any Linux kernel from 4.14 onward with <code class="language-plaintext highlighter-rouge">CONFIG_CRYPTO_USER_API_AEAD</code> enabled is vulnerable.
This is the default on virtually all major distributions.</p>

<p>The upstream fix (mainline commit <code class="language-plaintext highlighter-rouge">a664bf3d603d</code>, April 1, 2026) reverts <code class="language-plaintext highlighter-rouge">algif_aead.c</code> to out-of-place operation.
Source and destination are separate scatterlists.
Page cache pages never appear in the writable destination.</p>

<p>If you can’t patch immediately, disabling the vulnerable module is the fastest mitigation:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"install algif_aead /bin/false"</span> <span class="o">&gt;</span> /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2&gt;/dev/null
</code></pre></div></div>

<p>Be aware this may break applications using kernel crypto acceleration, including cryptsetup, Firefox, and IPsec implementations.</p>

<p>For container environments, deploying seccomp policies that block AF_ALG socket creation is an effective hardening measure, since container isolation alone does not protect against kernel-level page cache corruption.</p>

<hr />

<h2 id="detection">Detection</h2>

<p>Standard disk forensics cannot detect exploitation.
The on-disk file is never modified.</p>

<p>What works is runtime monitoring.
Falco or Sysdig rules can detect suspicious AF_ALG SEQPACKET socket creation from unexpected processes.
Live memory forensics can compare in-memory binary images against on-disk hashes.
Monitoring for unexpected privilege transitions following kernel crypto syscalls is another viable detection path.</p>

<p>What doesn’t work is anything that relies on the file system.
Hash checks, inotify, fanotify, and VFS audit logs all see a clean disk.</p>

<hr />

<h2 id="timeline">Timeline</h2>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2017</td>
      <td>In-place optimization commit <code class="language-plaintext highlighter-rouge">72548b093ee3</code> introduces the bug</td>
    </tr>
    <tr>
      <td>April 1, 2026</td>
      <td>Upstream fix committed</td>
    </tr>
    <tr>
      <td>April 29, 2026</td>
      <td>Public disclosure by Theori</td>
    </tr>
    <tr>
      <td>April 30, 2026</td>
      <td>CISA adds to Known Exploited Vulnerabilities catalog</td>
    </tr>
    <tr>
      <td>May 11, 2026</td>
      <td>This independent reproduction and analysis</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="conclusion">Conclusion</h2>

<p>CopyFail is a reminder that the most dangerous vulnerabilities aren’t always the most complex.
Three reasonable commits, spanning six years, created a primitive that gives any unprivileged user a deterministic 4-byte write to the page cache of any readable file.
The fix is a one-line change reverting to out-of-place operation.</p>

<p>What makes it worth studying is the structural lesson.
Each component was safe in isolation.
The danger emerged from their interaction, specifically from the assumption that buffer reuse is safe when the buffers might contain zero-copy references to shared kernel data structures.
That assumption was invisible to anyone reviewing a single subsystem.</p>

<p>Patch your systems.
If you run containers, deploy the seccomp mitigation.
And if you’re in vulnerability research, spend some time thinking about what other zero-copy plus in-place-operation interactions might be hiding in the kernel.
CopyFail probably isn’t the last bug in this class.</p>

<hr />

<h2 id="acknowledgments">Acknowledgments</h2>

<p>All credit for the original discovery goes to Taeyang Lee and the team at Theori.
Their disclosure and the Xint Code AI-assisted scanning approach represent an important development in how vulnerabilities are found.
This post is an independent reproduction and analysis, not an original finding.</p>

<hr />

<h2 id="references">References</h2>

<p>Theori/Xint, “Copy Fail: 732 Bytes to Root on Every Major Linux Distribution.”
Help Net Security, “Nine-year-old Linux kernel flaw enables reliable local privilege escalation.”
Tenable, “Copy Fail CVE-2026-31431 FAQ.”
The Register, “Linux cryptographic code flaw offers fast route to root.”
CERT-EU, Security Advisory 2026-005.
Sysdig, “CVE-2026-31431 Copy Fail analysis.”</p>

<hr />

<p><em>This research was conducted in an isolated lab environment.
All testing was performed on systems owned and operated by the researcher.</em></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Author: Michael Published: May 2026 Original Discovery: Taeyang Lee @ Theori (assisted by Xint Code AI) CVE: CVE-2026-31431 CVSS: 7.8 (High) Type: Local Privilege Escalation Component: Linux kernel AF_ALG AEAD subsystem (algif_aead.c) Patched: April 1, 2026 (mainline commit a664bf3d603d)]]></summary></entry><entry><title type="html">CVE-2026-23669: A Deep Dive into a Windows Print Spooler Heap Overflow</title><link href="https://agent1b.github.io/2026/05/01/cve-2026-23669.html" rel="alternate" type="text/html" title="CVE-2026-23669: A Deep Dive into a Windows Print Spooler Heap Overflow" /><published>2026-05-01T00:00:00+00:00</published><updated>2026-05-01T00:00:00+00:00</updated><id>https://agent1b.github.io/2026/05/01/cve-2026-23669</id><content type="html" xml:base="https://agent1b.github.io/2026/05/01/cve-2026-23669.html"><![CDATA[<p><strong>Author:</strong> Michael
<strong>Published:</strong> May 2026
<strong>Severity:</strong> CVSS 8.8 (High), AV:N / AC:L / PR:L / UI:N
<strong>Affected:</strong> Windows 10, Windows 11, Windows Server 2012 through 2025
<strong>Patched:</strong> March 11, 2026, KB5079473</p>

<hr />

<h2 id="abstract">Abstract</h2>

<p>This paper presents an analysis of CVE-2026-23669, a heap buffer overflow in Microsoft’s RPC runtime library (<code class="language-plaintext highlighter-rouge">rpcrt4.dll</code>).
Although the vulnerability is publicly categorized under the Print Spooler umbrella, our research shows the underlying bug sits in the endpoint mapper (epmap) client code within the RPC runtime itself.
The Print Spooler serves as an attack vector, but the flaw is not exclusive to it.
Any Windows service that performs remote endpoint resolution through <code class="language-plaintext highlighter-rouge">rpcrt4.dll</code> could, in principle, be affected.</p>

<p>Through binary diffing, Ghidra decompilation, and controlled testing on a pre-patch Windows 11 system, we confirmed a reliable remote denial-of-service condition and identified a privilege escalation path from any domain user to domain administrator via NTLM coercion.
We also examine why direct remote code execution from this primitive alone is effectively blocked by several overlapping mitigations in modern Windows, offering a practical look at how Microsoft’s defense-in-depth strategy holds up in the real world.</p>

<hr />

<h2 id="1-introduction">1. Introduction</h2>

<p>Microsoft’s March 2026 Patch Tuesday addressed 67 vulnerabilities, among them CVE-2026-23669, a heap overflow in the Windows Print Spooler rated at CVSS 8.8.
The vulnerability was originally identified by security researcher Andrea Pierini, who noted publicly that the bug is not confined to the print spooler but resides in the endpoint mapper protocol within <code class="language-plaintext highlighter-rouge">rpcrt4.dll</code>.</p>

<p>That observation turned out to be the key to understanding this vulnerability.
Unlike PrintNightmare and its successors, CVE-2026-23669 does not exploit a flaw in spooler-specific logic.
Instead, it targets a fundamental weakness in how the RPC runtime resolves remote endpoints, a mechanism that underpins dozens of Windows services beyond printing.</p>

<p>Our research spanned 18 days and involved binary diffing of six patched DLLs, full Ghidra decompilation of <code class="language-plaintext highlighter-rouge">rpcrt4.dll</code>, <code class="language-plaintext highlighter-rouge">ntdll.dll</code>, and <code class="language-plaintext highlighter-rouge">spoolsv.exe</code>, combined with controlled exploitation in an isolated lab environment.
What follows is a detailed account of our findings, including what works, what doesn’t, and why.</p>

<hr />

<h2 id="2-vulnerability-mechanics">2. Vulnerability Mechanics</h2>

<h3 id="21-the-endpoint-mapper-protocol">2.1 The Endpoint Mapper Protocol</h3>

<p>When a Windows service needs to reach a remote RPC endpoint, it queries the target machine’s Endpoint Mapper on port 135 using the <code class="language-plaintext highlighter-rouge">ept_map</code> RPC call (opnum 3).
The response carries an array of “towers”, binary structures that describe available network endpoints in terms of protocol, address, and port.</p>

<p>Inside <code class="language-plaintext highlighter-rouge">rpcrt4.dll</code>, the relevant data structure is what we call the EPM Registration Context.
It is a 0x388-byte heap object.
At offset +0x2D8, it holds a fixed 4-slot pointer array (32 bytes) meant for up to 4 tower pointers.
The count of active entries lives at offset +0x2F8.</p>

<h3 id="22-the-missing-correlation-check">2.2 The Missing Correlation Check</h3>

<p>The <code class="language-plaintext highlighter-rouge">ept_map</code> response is a conformant varying array, defined in the IDL with <code class="language-plaintext highlighter-rouge">[size_is(max_towers)]</code> constraining the maximum number of entries returned.
On the wire, this translates to a <code class="language-plaintext highlighter-rouge">MaxCount</code> field in the NDR encoding, which should be validated against the caller’s <code class="language-plaintext highlighter-rouge">max_towers</code> parameter, hardcoded to 4.</p>

<p>Our analysis of the client-side NDR unmarshaling code reveals that this correlation validation is skipped because the correlation descriptor pointer is NULL.
This is a compile-time artifact.
The MIDL-generated stub for <code class="language-plaintext highlighter-rouge">ept_map</code> does not emit correlation validation metadata on the client-side path.</p>

<p>The consequence is straightforward.
A malicious EPM server can return any number of towers.
NDR writes all of the corresponding tower pointers into the 4-slot array without complaint, overflowing into adjacent fields of the EPM context structure.</p>

<h3 id="23-triggering-the-overflow">2.3 Triggering the Overflow</h3>

<p>The attack hinges on getting the target’s spooler to issue an EPM query against a machine the attacker controls.
This is accomplished through the notification callback mechanism.</p>

<p>First, the attacker authenticates to the target’s <code class="language-plaintext highlighter-rouge">\pipe\spoolss</code> using <code class="language-plaintext highlighter-rouge">RPC_C_AUTHN_LEVEL_CONNECT</code>, the lowest authentication level, available to any domain user.
Then, the attacker invokes <code class="language-plaintext highlighter-rouge">RpcRemoteFindFirstPrinterChangeNotificationEx</code> (opnum 65) with a callback address pointing to the attacker’s host.
The target’s <code class="language-plaintext highlighter-rouge">win32spl.dll</code> then attempts to resolve the remote spoolss endpoint by querying the attacker’s IP on port 135.
At that point, the attacker’s rogue EPM responds with more than 4 towers.</p>

<p>Because the encoding is valid NDR, with <code class="language-plaintext highlighter-rouge">MaxCount</code> and <code class="language-plaintext highlighter-rouge">ActualCount</code> simply reflecting the actual number of towers, all client-side validation passes.
The overflow is immediate and deterministic.</p>

<h3 id="24-overflow-geometry">2.4 Overflow Geometry</h3>

<p>With 11 towers in the response, the overflow map looks like this:</p>

<table>
  <thead>
    <tr>
      <th>Slot</th>
      <th>Offset</th>
      <th>Field Overwritten</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0–3</td>
      <td>+0x2D8</td>
      <td>In-bounds (valid tower pointers)</td>
    </tr>
    <tr>
      <td>4</td>
      <td>+0x2F8</td>
      <td>Entry count (DWORD)</td>
    </tr>
    <tr>
      <td>5–9</td>
      <td>+0x300</td>
      <td>Internal flags and async state</td>
    </tr>
    <tr>
      <td>10</td>
      <td>+0x328</td>
      <td><code class="language-plaintext highlighter-rouge">RPC_ASYNC_STATE.RuntimeInfo</code></td>
    </tr>
  </tbody>
</table>

<p>Each overflow slot receives a heap pointer to the corresponding tower data allocation, a valid, attacker-influenced heap block.</p>

<hr />

<h2 id="3-self-terminating-cleanup">3. Self-Terminating Cleanup</h2>

<p>One of the more interesting aspects of this vulnerability is the behavior of the cleanup loop that runs after the overflow.</p>

<p>Once <code class="language-plaintext highlighter-rouge">NdrClientCall3</code> returns, the tower processing function iterates over the pointer array, freeing each tower and zeroing the slot.
But here’s the catch.
At iteration 4, the loop reaches the count field itself (offset +0x2F8).
The 8-byte zero-write at that location sets the count to 0, and the loop terminates on the next check.</p>

<p>This self-termination is deterministic.
Slots 5 and beyond are never touched by the cleanup.
They persist as valid heap pointers to attacker-controlled tower data until the object is eventually destroyed.</p>

<p>In practice, the cleanup still crashes roughly 80% of the time.
The count field temporarily holds the low 32 bits of tower[4]’s heap pointer, which is a large value.
The loop tries to free non-heap addresses at the overflow offsets before it gets a chance to zero the count, and the result is a <code class="language-plaintext highlighter-rouge">STATUS_HEAP_CORRUPTION</code> (0xc0000374) crash.</p>

<hr />

<h2 id="4-patch-analysis">4. Patch Analysis</h2>

<h3 id="41-binary-diff-results">4.1 Binary Diff Results</h3>

<p>We diffed all six DLLs modified by KB5079473:</p>

<table>
  <thead>
    <tr>
      <th>DLL</th>
      <th>Size Change</th>
      <th>Functional Changes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rpcrt4.dll</code></td>
      <td>+8 bytes</td>
      <td>+80 bytes new code (<code class="language-plaintext highlighter-rouge">ShouldCapEndpointArray</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">spoolsv.exe</code></td>
      <td>+4,096 bytes</td>
      <td>+<code class="language-plaintext highlighter-rouge">VerifyRpcValidSecurityLevelForWinspool</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">localspl.dll</code></td>
      <td>-20,480 bytes</td>
      <td>+<code class="language-plaintext highlighter-rouge">EscapePrinterName</code>, +<code class="language-plaintext highlighter-rouge">IncrementPrintJobCount</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">win32spl.dll</code></td>
      <td>-16,384 bytes</td>
      <td>Removed Point-and-Print functions</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">inetpp.dll</code></td>
      <td>-4,096 bytes</td>
      <td>Minor changes</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">spoolss.dll</code></td>
      <td>0 bytes</td>
      <td>No changes</td>
    </tr>
  </tbody>
</table>

<p>The 728KB byte diff in <code class="language-plaintext highlighter-rouge">rpcrt4.dll</code> initially had us wondering whether a second vulnerability was hiding in the update.
After exhaustive analysis, comparing API call counts, searching for new validation functions, and examining every change that wasn’t clearly compiler noise, we’re confident it’s all recompilation artifacts.
Only the 80-byte <code class="language-plaintext highlighter-rouge">ShouldCapEndpointArray</code> function constitutes a security fix.</p>

<h3 id="42-defense-in-depth">4.2 Defense in Depth</h3>

<p>Microsoft’s patch is notably thorough, employing three independent layers.</p>

<p><strong>Layer 1, Authentication Gate (<code class="language-plaintext highlighter-rouge">spoolsv.exe</code>):</strong>
A new function, <code class="language-plaintext highlighter-rouge">VerifyRpcValidSecurityLevelForWinspool</code>, is added to the notification handler opnums (58, 59, 60, 65, 66).
It rejects connections made at <code class="language-plaintext highlighter-rouge">RPC_C_AUTHN_LEVEL_CONNECT</code>, now requiring at least <code class="language-plaintext highlighter-rouge">RPC_C_AUTHN_LEVEL_PKT_PRIVACY</code>.
This cuts off low-privilege users from the vulnerable code path.</p>

<p><strong>Layer 2, Array Bounds Cap (<code class="language-plaintext highlighter-rouge">rpcrt4.dll</code>):</strong>
<code class="language-plaintext highlighter-rouge">ShouldCapEndpointArray</code> is called before the cleanup loop, capping the iteration count to 4 regardless of whatever value sits at +0x2F8.
This defuses the cleanup overflow even if the authentication gate is somehow bypassed.</p>

<p><strong>Layer 3, Attack Surface Reduction (<code class="language-plaintext highlighter-rouge">win32spl.dll</code>):</strong>
Point-and-Print functions are removed entirely, closing off the remote printer driver installation vector.</p>

<hr />

<h2 id="5-exploitation-assessment">5. Exploitation Assessment</h2>

<h3 id="51-denial-of-service">5.1 Denial of Service</h3>

<p>The DoS condition is straightforward.
With an honest NDR encoding containing more than 4 towers, the spooler crashes shortly after receiving the rogue EPM response.
The crash is logged as Event 7031 (Service Control Manager) and generates WER crash reports.
The spooler auto-restarts within about 15 seconds, meaning the attack can be repeated indefinitely.</p>

<h3 id="52-remote-code-execution-why-it-doesnt-work">5.2 Remote Code Execution, Why It Doesn’t Work</h3>

<p>We put considerable effort into trying to convert the heap overflow into remote code execution.
Over 16 iterations of binary analysis and live testing, we systematically ruled out every direct path.
The short version is that modern Windows mitigations work.</p>

<p><strong>Control Flow Guard (CFG)</strong> is enabled on <code class="language-plaintext highlighter-rouge">spoolsv.exe</code>.
Any attempt to call a heap address as a function pointer is caught by <code class="language-plaintext highlighter-rouge">__guard_dispatch_icall</code>.
We scanned thousands of function entry points across loaded DLLs looking for CFG-valid stack pivot gadgets and came up empty.</p>

<p><strong>LFH Bucket Separation</strong> puts the EPM context (0x388 bytes) and Connection objects (0x350 bytes) into different Low Fragmentation Heap buckets.
You can’t corrupt into a neighboring object’s vtable through adjacent heap writes.</p>

<p><strong>Object Lifecycle</strong> is another blocker.
The corrupted EPM context is a transient client-side object.
It’s allocated for the <code class="language-plaintext highlighter-rouge">ept_map</code> call and destroyed immediately after.
There’s no window to reference the corrupted state from a separate code path.</p>

<p><strong>Synchronous Execution</strong> seals it off further.
Network EPM calls go through <code class="language-plaintext highlighter-rouge">NdrClientCall3</code>, which is synchronous.
The async notification callback at +0x338, which would have been the most promising dispatch target for the corrupted fields, never fires for non-<code class="language-plaintext highlighter-rouge">ncalrpc</code> transports.</p>

<p>We did identify one interesting CFG bypass candidate in <code class="language-plaintext highlighter-rouge">ntdll.dll</code> that performs an unguarded indirect call.
But it’s only reachable during exception dispatch, not from an arbitrary vtable call, creating a dependency loop that doesn’t resolve.</p>

<p>The takeaway is that no single one of these mitigations is bulletproof on its own.
Together, they close off every practical RCE path we could identify from this primitive.</p>

<h3 id="53-privilege-escalation-via-ntlm-coercion">5.3 Privilege Escalation via NTLM Coercion</h3>

<p>While direct RCE is off the table, the vulnerability does enable a meaningful escalation path.</p>

<p>The pre-patch authentication gap on the notification opnums allows any domain user to force the Print Spooler, running as SYSTEM, to initiate an outbound SMB connection to an attacker-controlled host.
That connection authenticates as the machine account, yielding an NTLM coercion primitive.</p>

<p>This is the same class of technique as the well-known PrinterBug (MS-RPRN abuse).
When combined with standard NTLM relay methods against Active Directory services, it can lead to full domain compromise in environments that haven’t hardened their relay defenses.
The specific fix in KB5079473 closes this gate by enforcing <code class="language-plaintext highlighter-rouge">PKT_PRIVACY</code> on the affected opnums.</p>

<p>We are intentionally omitting the specific relay chains here, as they are well-documented elsewhere and adding them to a CVE-specific writeup would lower the barrier without adding meaningful defensive insight.</p>

<hr />

<h2 id="6-comparison-with-printnightmare">6. Comparison with PrintNightmare</h2>

<p>CVE-2026-23669 is often mentioned alongside PrintNightmare (CVE-2021-1675 / CVE-2021-34527), but the two are fundamentally different vulnerabilities.</p>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>PrintNightmare</th>
      <th>CVE-2026-23669</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bug location</td>
      <td><code class="language-plaintext highlighter-rouge">spoolsv.exe</code> (driver install)</td>
      <td><code class="language-plaintext highlighter-rouge">rpcrt4.dll</code> (endpoint mapper)</td>
    </tr>
    <tr>
      <td>Bug type</td>
      <td>Improper access control</td>
      <td>Heap buffer overflow</td>
    </tr>
    <tr>
      <td>Attack vector</td>
      <td><code class="language-plaintext highlighter-rouge">RpcAddPrinterDriverEx</code> (opnum 89)</td>
      <td>Rogue EPM response via notification callback</td>
    </tr>
    <tr>
      <td>Authentication</td>
      <td>Any user (pre-patch)</td>
      <td>Any user at CONNECT level (pre-patch)</td>
    </tr>
    <tr>
      <td>Direct RCE</td>
      <td>Yes (DLL load as SYSTEM)</td>
      <td>No (blocked by CFG + LFH + object lifecycle)</td>
    </tr>
    <tr>
      <td>Escalation path</td>
      <td>Direct DLL execution</td>
      <td>NTLM coercion + relay</td>
    </tr>
    <tr>
      <td>Patch mechanism</td>
      <td>Restrict driver install to admins</td>
      <td>Auth level check + array bounds cap</td>
    </tr>
  </tbody>
</table>

<p>Our testing on Windows 11 24H2 (Build 26200) also confirmed that traditional PrintNightmare exploitation is independently blocked on recent builds through UNC path validation in <code class="language-plaintext highlighter-rouge">RpcAddPrinterDriverEx</code>.
Several established implementations all returned <code class="language-plaintext highlighter-rouge">ERROR_INVALID_PARAMETER</code> on this build, regardless of CVE-2026-23669’s patch status.</p>

<hr />

<h2 id="7-detection-and-mitigation">7. Detection and Mitigation</h2>

<h3 id="71-detection-indicators">7.1 Detection Indicators</h3>

<p><strong>Windows Event Logs:</strong></p>

<p>Event 7031 (System) with the message “The Print Spooler service terminated unexpectedly” is the primary indicator.
Repeated spooler crashes with <code class="language-plaintext highlighter-rouge">snmpapi.dll</code> or <code class="language-plaintext highlighter-rouge">ntdll.dll</code> referenced in WER reports should also raise flags.</p>

<p><strong>Network Indicators:</strong></p>

<p>Look for outbound connections from <code class="language-plaintext highlighter-rouge">spoolsv.exe</code> to unexpected external hosts on port 135.
Also watch for <code class="language-plaintext highlighter-rouge">ept_map</code> responses where <code class="language-plaintext highlighter-rouge">MaxCount</code> exceeds 4 in the conformant array header.</p>

<p><strong>WER Crash Signatures:</strong></p>

<p>The most common crash signature is exception code <code class="language-plaintext highlighter-rouge">0xc0000374</code> (STATUS_HEAP_CORRUPTION) with faulting modules <code class="language-plaintext highlighter-rouge">snmpapi.dll</code> or <code class="language-plaintext highlighter-rouge">ntdll.dll</code> in the crash reports.</p>

<h3 id="72-mitigation">7.2 Mitigation</h3>

<p>The primary recommendation is to apply <strong>KB5079473</strong>, the March 2026 cumulative update.</p>

<p>If patching is not immediately possible, the following registry change disables remote RPC endpoint registration for the spooler, preventing the notification callback from triggering the vulnerable EPM query:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>reg add "HKLM\SYSTEM\CurrentControlSet\Control\Print" ^
    /v RegisterSpoolerRemoteRpcEndPoint /t REG_DWORD /d 2 /f
</code></pre></div></div>

<hr />

<h2 id="8-responsible-disclosure-timeline">8. Responsible Disclosure Timeline</h2>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pre-March 2026</td>
      <td>Vulnerability discovered (credited researcher: Andrea Pierini)</td>
    </tr>
    <tr>
      <td>March 11, 2026</td>
      <td>Microsoft releases KB5079473</td>
    </tr>
    <tr>
      <td>March 11, 2026</td>
      <td>CVE-2026-23669 published (NVD, MSRC)</td>
    </tr>
    <tr>
      <td>April 14, 2026</td>
      <td>Our independent research begins</td>
    </tr>
    <tr>
      <td>April 30, 2026</td>
      <td>Deterministic crash confirmed on pre-patch Windows 11</td>
    </tr>
    <tr>
      <td>May 1, 2026</td>
      <td>Full analysis complete, paper published</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="9-conclusion">9. Conclusion</h2>

<p>CVE-2026-23669 is a significant vulnerability in the Windows RPC runtime that extends beyond the Print Spooler.
The heap overflow in the endpoint mapper client code is deterministic and remotely triggerable by any authenticated domain user, providing both a denial-of-service condition and a privilege escalation primitive through NTLM coercion.</p>

<p>Modern Windows mitigations, including CFG, LFH bucket isolation, and synchronous object lifecycles, collectively prevent direct remote code execution from this primitive alone.
But the NTLM coercion path demonstrates that real-world impact can still reach domain compromise when combined with standard Active Directory attack techniques.</p>

<p>Microsoft’s patch is well-engineered, with three independent layers of defense that each address a different aspect of the attack chain.
It’s a good example of how patching done right doesn’t just fix a bug but closes off entire categories of follow-on abuse.</p>

<p>More broadly, this vulnerability reinforces a pattern we see again and again in modern Windows exploitation.
A single primitive rarely gets you code execution on a fully-mitigated system.
The interesting work, and the real defensive value, is in understanding how primitives chain together and where the mitigation layers intersect.</p>

<hr />

<h2 id="acknowledgments">Acknowledgments</h2>

<p>Thanks to Andrea Pierini for the original vulnerability discovery and the critical observation about the epmap component.
Thanks to the Zero Day Initiative for their March 2026 patch analysis.
Thanks to the impacket project for the RPC protocol implementation used in our tooling.
And thanks to the Microsoft Security Response Center for the comprehensive, multi-layered patch.</p>

<hr />

<p><em>This research was conducted in an isolated lab environment with explicit authorization.
All testing was performed on systems owned and operated by the researcher.</em></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Author: Michael Published: May 2026 Severity: CVSS 8.8 (High), AV:N / AC:L / PR:L / UI:N Affected: Windows 10, Windows 11, Windows Server 2012 through 2025 Patched: March 11, 2026, KB5079473]]></summary></entry></feed>