research
Research·Advanced·15 min·2026-09-27

Root Cause Analysis of CVE-2026-69834: Windows ALPC Use-After-Free in AlpcpImpersonateMessage

An independent root cause analysis of CVE-2026-69834; a Use-After-Free in Windows ALPC's AlpcpImpersonateMessage caused by a missing reference on the _KALPC_SECURITY_DATA struct across the impersonation read window.

by Penguin

Introduction

CVE-2026-69834 is a Use-After-Free (UAF) vulnerability in the Windows Advanced Local Procedure Call (ALPC) subsystem, patched in Microsoft's September 2026 Patch Tuesday. Successful exploitation allows a low-privilege attacker to gain SYSTEM-level privileges by winning a race condition in the kernel's ALPC impersonation path.

CVE

CVE-2026-69834

CWE

CWE-416 (Use After Free)

Severity

Important (CVSS 7.0)

Attack Complexity

High

Impact

Elevation of Privilege

Exploited in Wild

No

This article presents an independent root cause analysis of the vulnerability derived from binary diffing pre-patch (10.0.17763.9121 ) and post-patch (10.0.17763.9245 ) ntoskrnl.exe builds and live kernel debugging on an isolated VM.

Background — Windows ALPC

Advanced Local Procedure Call (ALPC) is a high-performance message-passing mechanism in Windows that enables efficient inter-process communication (IPC) between client and server processes. ALPC replaced the older Local Procedure Call (LPC) mechanism and provides optimized communication channels.

At its core, ALPC is a messaging protocol, one process acts as a server exposing a port, and another acts as a client connecting to it and exchanging messages.

image10.png
Source: https://www.tutorialspoint.com/article/advanced-local-procedure-call-alpc

One notable feature ALPC provides beyond simple message passing is client impersonation, which lets a server act on behalf of a client. When the server receives a message, it can call NtAlpcImpersonateClientOfPort with that message, and the kernel assigns an impersonation token representing the client to the server thread. Access checks that use the thread's token, such as opening a file or registry key, are then evaluated against the client's identity instead of the server's. How far the server can go with that identity is limited by the impersonation level the client allowed when it connected.

ALPC Overview

Server — Creating the Port

The server creates a named port using NtAlpcCreatePort, providing a port name and attributes.

This port becomes the endpoint clients connect to:

RtlInitUnicodeString(&usPortName, L"\\RPC Control\\BeaconBytes");
InitializeObjectAttributes(&objPort, &usPortName, 0, 0, 0);

RtlSecureZeroMemory(&serverPortAttr, sizeof(serverPortAttr));
serverPortAttr.MaxMessageLength = MAX_MSG_LEN;

ntRet = NtAlpcCreatePort(&hPort, &objPort, &serverPortAttr);

Server — Waiting for a Client

After creating the port, the server calls NtAlpcSendWaitReceivePort with no outgoing message. This blocks the server thread until a client connects or sends a message. When a client arrives, the kernel wakes the server and fills in pmReceive with the incoming PORT_MESSAGE:

PORT_MESSAGE            pmReceive;
NTSTATUS                ntRet;

nLen = sizeof(pmReceive);
ntRet = NtAlpcSendWaitReceivePort(hPort, 0, NULL, NULL, &pmReceive, &nLen, NULL, NULL);
  • PORT_MESSAGE Struct [CLICK TO OPEN]

    //0x28 bytes (sizeof)
    struct _PORT_MESSAGE
    {
        union
        {
            struct
            {
                SHORT DataLength;                                               //0x0
                SHORT TotalLength;                                              //0x2
            } s1;                                                               //0x0
            ULONG Length;                                                       //0x0
        } u1;                                                                   //0x0
        union
        {
            struct
            {
                SHORT Type;                                                     //0x4
                SHORT DataInfoOffset;                                           //0x6
            } s2;                                                               //0x4
            ULONG ZeroInit;                                                     //0x4
        } u2;                                                                   //0x4
        union
        {
            struct _CLIENT_ID ClientId;                                         //0x8
            double DoNotUseThisField;                                           //0x8
        };
        ULONG MessageId;                                                        //0x18
        union
        {
            ULONGLONG ClientViewSize;                                           //0x20
            ULONG CallbackId;                                                   //0x20
        };
    }; 
    
    //SOURCE: https://www.vergiliusproject.com/kernels/x64/windows-10/1809/_PORT_MESSAGE

pmReceive is the user-mode visible portion of the kernel's internal KALPC_MESSAGE structure.

nLen serves two roles:

  1. On Input: The caller sets it to sizeof(pmReceive),tells the kernel the maximum buffer size available. If the incoming message is larger than nLen on input, the kernel returns STATUS_BUFFER_TOO_SMALL and the message is not delivered.

  2. On output: Kernel overwrites it with the actual received message length, server uses this to know how many valid bytes are in pmReceive.

When NtAlpcSendWaitReceivePort returns successfully, pmReceive is populated and ready.

Client — Connecting to the Port

The client connects using NtAlpcConnectPort, specifying the port name and a set of port attributes. This is where the client decides how far the server is allowed to impersonate it.

Two things in ALPC_PORT_ATTRIBUTES control this (how far the server is allowed to impersonate it) :

Flag 0x10000 — ALPC_PORTFLG_ALLOWIMPERSONATION:

Explicitly grants the server permission to impersonate this client at all. Without this flag set, any impersonation attempt by the server will be denied by the kernel.

SecurityQos — SECURITY_QUALITY_OF_SERVICE:

Defines the limits of that impersonation, specifically the ImpersonationLevel, which controls how far the server can go with the client's identity:

ALPC_PORT_ATTRIBUTES clientPortAttr;
RtlSecureZeroMemory(&clientPortAttr, sizeof(clientPortAttr));

clientPortAttr.Flags = 0x10000;   // ALPC_PORTFLG_ALLOWIMPERSONATION
clientPortAttr.SecurityQos.Length = sizeof(SECURITY_QUALITY_OF_SERVICE);
clientPortAttr.SecurityQos.ImpersonationLevel = SecurityImpersonation;
clientPortAttr.SecurityQos.ContextTrackingMode = SECURITY_STATIC_TRACKING;
clientPortAttr.SecurityQos.EffectiveOnly = FALSE;
clientPortAttr.MaxMessageLength = MSG_LEN;

ntRet = NtAlpcConnectPort(
    &hPort,
    &usPortName,       // port name to connect to
    NULL,              // ObjectAttributes — not needed
    &clientPortAttr,   // QoS and impersonation flags live here
    0, NULL, NULL, NULL, NULL, NULL, NULL);

Client - Sends a message

Connection alone is not enough, the client must also send a message with a security attribute attached. Interestingly, the client uses the same NtAlpcSendWaitReceivePort call the server uses for listening, the difference is that the client populates the send parameters (lpMem and pSendAttr) instead of the receive parameters.

Before calling NtAlpcSendWaitReceivePort, the client needs to set up a security attribute to attach to the outgoing message. Instead of calling NtAlpcCreateSecurityContext first, the client can pass ContextHandle = -2, a sentinel value the kernel recognises as an instruction to create the security context inline during the send, without a separate creation call.

Before setting up the security attribute, the client first needs to define the SECURITY_QUALITY_OF_SERVICE, this tells the kernel what impersonation level and tracking mode to use when creating the security context inline:

SECURITY_QUALITY_OF_SERVICE sqos;
RtlSecureZeroMemory(&sqos, sizeof(sqos));

sqos.Length              = sizeof(sqos);
sqos.ImpersonationLevel  = SecurityImpersonation;
sqos.ContextTrackingMode = SECURITY_STATIC_TRACKING;
sqos.EffectiveOnly       = FALSE;
  • SECURITY_QUALITY_OF_SERVICE Struct [CLICK TO OPEN]

    //0xc bytes (sizeof)
    struct _SECURITY_QUALITY_OF_SERVICE
    {
        ULONG Length;                                                           //0x0
        enum _SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;                  //0x4
        UCHAR ContextTrackingMode;                                              //0x8
        UCHAR EffectiveOnly;                                                    //0x9
    }; 
    
    //SOURCE: https://www.vergiliusproject.com/kernels/x64/windows-10/1809/_SECURITY_QUALITY_OF_SERVICE

ImpersonationLevel controls how far the server can go with the client's identity. SecurityImpersonation allows the server to impersonate locally. ContextTrackingMode = SECURITY_STATIC_TRACKING captures the client's security context at connection time and holds it fixed. EffectiveOnly = FALSE allows the server to see all of the client's privileges, not just the currently enabled subset.

With the QoS defined, the client then sets up the security attribute with ContextHandle = -2:

  1. Find out how large the buffer needs to be:

SIZE_T attrBufSize = 0;
AlpcInitializeMessageAttribute(ALPC_MESSAGE_SECURITY_ATTRIBUTE,
    NULL, 0, &attrBufSize);
// attrBufSize now holds the required allocation size
  1. Initialize the buffer:

PALPC_MESSAGE_ATTRIBUTES pSendAttr =
    (PALPC_MESSAGE_ATTRIBUTES)HeapAlloc(
        GetProcessHeap(), HEAP_ZERO_MEMORY, attrBufSize); 
        
AlpcInitializeMessageAttribute(ALPC_MESSAGE_SECURITY_ATTRIBUTE,
    pSendAttr, attrBufSize, &attrBufSize);
  1. Stamp the security context handle into the buffer's security slot

PALPC_SECURITY_ATTR pSecSlot =
    (PALPC_SECURITY_ATTR)AlpcGetMessageAttribute(
        pSendAttr, ALPC_MESSAGE_SECURITY_ATTRIBUTE);

pSecSlot->Flags         = 0;
pSecSlot->QoS           = &sqos;   
pSecSlot->ContextHandle = (ALPC_HANDLE)-2;  // ← sentinel stamped here

pSendAttr->ValidAttributes |= ALPC_MESSAGE_SECURITY_ATTRIBUTE;

When this pSendAttr is passed to NtAlpcSendWaitReceivePort, the kernel sees ContextHandle = -2, creates the security context blob inline:

ntRet = NtAlpcSendWaitReceivePort(hPort, 0, (PPORT_MESSAGE)lpMem, pSendAttr, NULL, NULL, NULL, NULL);

pSendAttr is an ALPC_MESSAGE_ATTRIBUTES buffer, a variable-length structure the kernel uses to carry message attributes on a send call. It is not a single flat struct but a container with a header followed by slots for each attribute type. AlpcInitializeMessageAttribute sets up that container for the ALPC_MESSAGE_SECURITY_ATTRIBUTE slot specifically.

pSecSlot is not a separate allocation, it is a typed pointer directly into pSendAttr's memory, returned by AlpcGetMessageAttribute at the offset where the security attribute slot lives inside the container. Writing to pSecSlot->ContextHandle is writing directly into pSendAttr's buffer at that offset. The QoS pointer is also stamped here, pSecSlot->QoS = &sqos tells the kernel which impersonation constraints to use when creating the security context inline via the -2 path.

Server — Impersonating the Client

After receiving the client's message, the server calls NtAlpcImpersonateClientOfPort to temporarily assume the client's identity:

ntRet = NtAlpcImpersonateClientOfPort(hConnectedPort, &pmReceive, 0);

Internally NtAlpcImpersonateClientOfPort does two things in sequence:

  1. Resolves the message:

AlpcpCaptureIdMessage(Message, local_res20, local_38);
// safely reads message ID from user-mode PORT_MESSAGE into kernel stack

iVar2 = AlpcpLookupMessage(local_30, local_res20[0], local_38[0], local_28);
// searches port's message queue for matching _KALPC_MESSAGE*
// writes _KALPC_MESSAGE* → local_28[0]

The user-mode PORT_MESSAGE pointer is never used directly in the kernel. AlpcpCaptureIdMessage safely extracts the message ID from it into a kernel stack variable, and AlpcpLookupMessage uses that ID to find and return the corresponding _KALPC_MESSAGE* from the kernel's internal message table, this kernel object is what gets passed to AlpcpImpersonateMessage, not the original user-supplied PORT_MESSAGE.

  • KALPC_MESSAGE Struct [CLICK TO OPEN]

    //0x118 bytes (sizeof)
    struct _KALPC_MESSAGE
    {
        struct _LIST_ENTRY Entry;                                               //0x0
        struct _ALPC_PORT* PortQueue;                                           //0x10
        struct _ALPC_PORT* OwnerPort;                                           //0x18
        struct _ETHREAD* WaitingThread;                                         //0x20
        union
        {
            struct
            {
                ULONG QueueType:3;                                              //0x28
                ULONG QueuePortType:4;                                          //0x28
                ULONG Canceled:1;                                               //0x28
                ULONG Ready:1;                                                  //0x28
                ULONG ReleaseMessage:1;                                         //0x28
                ULONG SharedQuota:1;                                            //0x28
                ULONG ReplyWaitReply:1;                                         //0x28
                ULONG OwnerPortReference:1;                                     //0x28
                ULONG ReceiverReference:1;                                      //0x28
                ULONG ViewAttributeRetrieved:1;                                 //0x28
                ULONG InDispatch:1;                                             //0x28
            } s1;                                                               //0x28
            ULONG State;                                                        //0x28
        } u1;                                                                   //0x28
        LONG SequenceNo;                                                        //0x2c
        union
        {
            struct _EPROCESS* QuotaProcess;                                     //0x30
            VOID* QuotaBlock;                                                   //0x30
        };
        struct _ALPC_PORT* CancelSequencePort;                                  //0x38
        struct _ALPC_PORT* CancelQueuePort;                                     //0x40
        LONG CancelSequenceNo;                                                  //0x48
        struct _LIST_ENTRY CancelListEntry;                                     //0x50
        struct _KALPC_RESERVE* Reserve;                                         //0x60
        struct _KALPC_MESSAGE_ATTRIBUTES MessageAttributes;                     //0x68
        VOID* DataUserVa;                                                       //0xb0
        struct _ALPC_COMMUNICATION_INFO* CommunicationInfo;                     //0xb8
        struct _ALPC_PORT* ConnectionPort;                                      //0xc0
        struct _ETHREAD* ServerThread;                                          //0xc8
        VOID* WakeReference;                                                    //0xd0
        VOID* WakeReference2;                                                   //0xd8
        VOID* ExtensionBuffer;                                                  //0xe0
        ULONGLONG ExtensionBufferSize;                                          //0xe8
        struct _PORT_MESSAGE PortMessage;                                       //0xf0
    }; 
    
    //SOURCE: https://www.vergiliusproject.com/kernels/x64/windows-10/1809/_KALPC_MESSAGE
  1. Impersonates using the security context:

 AlpcpImpersonateMessage(local_30,local_28[0],(uint)Flags & 1,
                             (uint)((Flags & ((ulonglong)(RequiredLevel * 4) | 2)) != 0),
                             RequiredLevel);

This is where the vulnerability lives. AlpcpImpersonateMessage is the kernel-internal function that handles all ALPC impersonation, every call to NtAlpcImpersonateClientOfPort ultimately ends up here.

AlpcpImpersonateMessage — Vulnerable Function

AlpcpImpersonateMessage receives the _KALPC_PORT* and _KALPC_MESSAGE* resolved by the lookup and is responsible for reading the client's captured security context and attaching it to the server thread via PsImpersonateClient.

AlpcpImpersonateMessage Function Signature:

NTSTATUS AlpcpImpersonateMessage(
    longlong Port,              // _KALPC_PORT*
    longlong Message,           // _KALPC_MESSAGE*
    int      Flags,             
    int      CheckRequiredLevel,
    int      RequiredLevel
)

The Race Condition

Before explaining how the vulnerable path is reached, it is important to understand what the function does wrong, because the bug is visible in the structure of the code itself.

The function takes the pushlock on the _KALPC_SECURITY_DATA struct, the same security context the client attached to the message via NtAlpcSendWaitReceivePort with ContextHandle = -2, now sitting at Message+0x88 inside AlpcpImpersonateMessage as lVar3, checks and sets the flags inside it, then releases the pushlock before reading the security context.

Message+0x88 is not a direct field of _KALPC_MESSAGE, it is reached through two nested structs. _KALPC_MESSAGE contains a _KALPC_MESSAGE_ATTRIBUTES field at +0x68, and inside that struct SecurityData sits at +0x20, making the security context pointer reachable at +0x68 + 0x20 = +0x88:

_KALPC_MESSAGE:
    +0x68  MessageAttributes  (_KALPC_MESSAGE_ATTRIBUTES)
               │
               └── +0x20  SecurityData  (_KALPC_SECURITY_DATA*)
                               │
                               └── this is lVar3 in the decompile
                                   = Message+0x88
                                   
 
 
 
 //0x70 bytes (sizeof)
struct _KALPC_SECURITY_DATA
{
    struct _ALPC_HANDLE_TABLE* HandleTable;                                 //0x0
    VOID* ContextHandle;                                                    //0x8
    struct _EPROCESS* OwningProcess;                                        //0x10
    struct _ALPC_PORT* OwnerPort;                                           //0x18
    struct _SECURITY_CLIENT_CONTEXT DynamicSecurity;                        //0x20
    union
    {
        struct
        {
            ULONG Revoked:1;                                                //0x68
            ULONG Impersonated:1;                                           //0x68
        } s1;                                                               //0x68
    } u1;                                                                   //0x68
}; 

With this layout in mind, the vulnerable execution path becomes clear:

lVar3 = *(longlong *)(Message + 0x88);
// _KALPC_MESSAGE.MessageAttributes.SecurityData
// = _KALPC_SECURITY_DATA* created inline during send

ExAcquirePushLockExclusiveEx(lVar3 - 0x10, 0);
// ← pushlock TAKEN at blob-0x10
//   serializes access to flags at blob+0x68

*(uint *)(lVar3 + 0x68) |= 2;
// set Impersonated flag (bit 1)
// still inside pushlock — flag write is safe

KeAbPostRelease(lVar3 - 0x10);
// ← pushlock RELEASED
//   access lock gone
//   blob-0x18 (refcount) NEVER TOUCHED
//   nothing keeping blob alive from this point

// ── everything below has no protection on lVar3 ──────────────────

puVar7 = (undefined4 *)(lVar3 + 0x20);
// reads DynamicSecurity from blob+0x20
// pushlock already gone
// no reference held on blob-0x18

PsImpersonateClient(..., *(puVar7 + 4), ...);
// ← ClientToken read from lVar3+0x30
//   puVar7+4 = DynamicSecurity+0x10 = blob+0x30
//   no reference held on the blob
//   no lock protecting the read

After setting the Impersonated flag the pushlock on _KALPC_SECURITY_DATA is released and from that point forward the function holds absolutely nothing on the struct. Yet execution continues reading directly from it — puVar7 = lVar3+0x20 sets up a pointer into the same struct whose lock was just dropped, and PsImpersonateClient is called, taking in data from the same struct whose pushlock was just released.

How the Vulnerable Path is Reached

For the race window to be reachable, five conditions must all pass. Not all of them are equal in importance, Gate 4 is the critical one that determines whether the vulnerable path is reachable at all, and it is directly controlled by what the client did before the server called NtAlpcImpersonateClientOfPort.

  1. Gate 4 — Security context must be present (the critical fork):

lVar3 = *(longlong *)(Message + 0x88);
// _KALPC_MESSAGE.MessageAttributes.SecurityData
// = _KALPC_SECURITY_DATA*

if (lVar3 == 0) → safe connectedPort path
                  reference held on connected port
                  not vulnerable

if (lVar3 != 0) → vulnerable blob path
                  no reference held
                  race window reachable

This value is non-NULL because the client sent the message with ContextHandle = -2 in pSendAttr —
the kernel's inline creation path in AlpcpCaptureSecurityAttributeInternal created the _KALPC_SECURITY_DATA blob and wrote its pointer into Message+0x88.

  1. Gate 5 — Security context must not be revoked:

(*(uint *)(lVar3 + 0x68) & 1) == 0
// _KALPC_SECURITY_DATA.u1.s1.Revoked == 0

The Revoked bit is a one-time-use guard. If set, the function releases the pushlock and returns STATUS_ACCESS_DENIED immediately, never reaching the race window. A freshly created _KALPC_SECURITY_DATA always has this bit clear, so this gate always passes on first impersonation. The vulnerable path is only reachable on the first use of a fresh security context.

  1. Gates 1, 2, 3 — Message validation:

These three gates are standard checks that pass naturally in any normal ALPC request flow:

// Gate 1 — message must be REQUEST type:
(*(undefined4 *)(Message + 0x28) & 7) == 3
// _KALPC_MESSAGE.u1.s1.QueueType:3 == 3
// any normal client request message satisfies this

// Gate 2 — impersonation must not be blocked:
(*(ushort *)(Message + 0xf4) & 0x4000) == 0
// _KALPC_MESSAGE.PortMessage.Type & 0x4000
// kernel only sets this on specific internal message types

// Gate 3 — message must belong to this port:
*(longlong *)(Message + 0x10) == Port
// _KALPC_MESSAGE.PortQueue == Port
// naturally satisfied — AlpcpLookupMessage already
// validated port ownership before calling this function

When all five pass:

Execution falls through to the vulnerable code path. The pushlock is taken, the Impersonated flag is set, the pushlock is released, and from that point the function read _KALPC_SECURITY_DATA.DynamicSecurity and calls PsImpersonateClient with no reference held on the struct. The race window is open.

The Fix

Microsoft's fix for CVE-2026-69834 is contained entirely within AlpcpImpersonateMessage and adds three things, a feature gate check, a reference increment before the pushlock is released, and a matching reference decrement after PsImpersonateClient completes.

Comparing pre-patch and post-patch directly:

Pre-patch — goes straight from Gate 5 to setting the Impersonated flag:

// consumed bit check passes → falls through
*(uint *)(lVar2 + 0x68) = *(uint *)(lVar2 + 0x68) | 2;  // set Impersonated
// ... pushlock released ...
KeAbPostRelease(lVar2 + -0x10);
puVar8 = (undefined4 *)(lVar2 + 0x20);   // dangling — no reference held
PsImpersonateClient(...);
// no AlpcpDereferenceBlobEx

Post-patch — three additions before the same point:

// consumed bit check passes → falls through

// ── ADDITION 1: feature gate ────────────────────────────────────
iVar4 = EvaluateCurrentState(
    &g_Feature_1081839931_62406798_FeatureDescriptorDetails);
if (iVar4 != 0) {

    // ── ADDITION 2: pin the blob alive ──────────────────────────
    lVar5 = AlpcpReferenceBlob(lVar2);
    if (lVar5 == 0) goto LAB_1405c7632;   // blob dying → bail safely
    local_f8 = 1;                          // mark ref taken
}

*(uint *)(lVar2 + 0x68) = *(uint *)(lVar2 + 0x68) | 2;  // set Impersonated
// ... pushlock released ...
KeAbPostRelease(lVar2 + -0x10);
puVar8 = (undefined4 *)(lVar2 + 0x20);   // safe — blob pinned alive

PsImpersonateClient(...);

// ── ADDITION 3: release the reference ───────────────────────────
if (local_f8 != 0) {
    AlpcpDereferenceBlobEx(lVar2, 1);     // drop impersonation ref
}

The three additions explained

  1. Addition 1 — Feature gate (EvaluateCurrentState):

EvaluateCurrentState(&g_Feature_1081839931_62406798_FeatureDescriptorDetails)

This is Windows' feature flag system, it allows Microsoft to ship a fix and enable or disable it remotely without a new build. In the shipped KB5122876 build EvaluateCurrentState returns nonzero, meaning the fix is fully active. The gate also provides a safe rollback mechanism if the fix causes unexpected regressions.

  1. Addition 2 — AlpcpReferenceBlob (the actual fix):

lVar5 = AlpcpReferenceBlob(lVar2);
if (lVar5 == 0) goto LAB_1405c7632;
local_f8 = 1;

This is the fix in its entirety, a single atomic increment of blob-0x18. Called while the pushlock is still held, before KeAbPostRelease, it increments the refcount stored at blob-0x18. From this point the blob cannot be freed during the window regardless of what concurrent operations fire.

The if (lVar5 == 0) check handles the edge case where the blob is already dying, AlpcpReferenceBlob returns 0 if it finds refcount already at 0, confirmed from its decompile:

longlong AlpcpReferenceBlob(longlong param_1) {
    lVar1 = *(longlong *)(param_1 + -0x18);  // read current refcount
    do {
        if (lVar1 < 1) {
            if (lVar1 == 0) return 0;         // already dying → fail
            KeBugCheckEx(0x18, ...);           // negative → corruption
        }
        LOCK();
        // atomic compare-and-swap:
        if (lVar1 == *(param_1-0x18))
            *(param_1-0x18) = lVar1 + 1;      // increment only if unchanged
        UNLOCK();
    } while (changed);                         // retry if concurrent change
    return lVar2 + 1;                          // return new refcount
}

When it returns 0 the code jumps to LAB_1405c7632, the safe early-return path that releases the pushlock and returns STATUS_ACCESS_DENIED.

3. Addition 3 — AlpcpDereferenceBlobEx (the paired release):

if (local_f8 != 0) {
    AlpcpDereferenceBlobEx(lVar2, 1);
}

Called after PsImpersonateClient completes, the read is done, the server thread has its impersonation token, the blob is no longer needed by the impersonation path. AlpcpDereferenceBlobEx atomically decrements blob-0x18 back down. The local_f8 guard ensures this only runs when Addition 2 actually took a reference.

Impact

An attacker who successfully exploits CVE-2026-69834 gains SYSTEM privileges on the affected machine. The attack requires only a low-privilege local account, no administrator rights, no user interaction, and no network access. The attacker connects to a privileged ALPC server that impersonates its clients, attaches a security context to an outgoing message, and races against the server's impersonation window.

Our analysis confirmed why Microsoft rated this AC:H (High attack complexity). Through exhaustive testing of the ALPC syscall surface, every message-capture reference drop path was found to either be blocked by the kernel's InDispatch mechanism during active impersonation, or to simultaneously zero Message+0x88 before the blob can be freed, making the race non-trivial to win via user-mode syscalls alone.

This is based on our own independent analysis of the available code paths and may not represent the complete picture, a more sophisticated technique or a kernel-internal path we did not examine could potentially achieve the timing required. The AC:H rating reflects that exploitation is possible but requires non-trivial effort, consistent with what our testing revealed.

Conclusion

Our analysis was conducted independently from the binary alone using ntoskrnl.exe builds 10.0.17763.9121 (pre-patch) and 10.0.17763.9245 (post-patch, KB5122876), binary diff to identify the changed function, Ghidra decompilation to trace the root cause, and live kernel debugging on an isolated VM to confirm the window and reference behavior. The vulnerability was discovered by Microsoft's own researchers - Amir Kutcher, Edan Zwick, and Tal Tzhori — and was not exploited in the wild. We hope this analysis contributes to the community.

#Windows Kernel#ALPC#CVE-2026-69834#Use-After-Free#UAF#Race Condition#Privilege Escalation#Kernel Debugging#WinDbg#Ghidra#Binary Diffing#BinDiff#Reverse Engineering