NtAlpcOpenSenderProcess
Server-side helper that opens a HANDLE to the process that sent a given ALPC message.
Prototype
NTSTATUS NtAlpcOpenSenderProcess( PHANDLE ProcessHandle, HANDLE PortHandle, PPORT_MESSAGE PortMessage, ULONG Flags, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | PHANDLE | out | Receives a HANDLE to the sender's EPROCESS object, opened with DesiredAccess. |
| PortHandle | HANDLE | in | Handle to the server's ALPC communication port that received the message. |
| PortMessage | PPORT_MESSAGE | in | The PORT_MESSAGE just returned from NtAlpcSendWaitReceivePort; its ClientId.UniqueProcess identifies the sender. |
| Flags | ULONG | in | Reserved / context flags. Typically 0; kernel-mode callers may pass ALPC_OPENSENDER_KERNEL_MODE. |
| DesiredAccess | ACCESS_MASK | in | Access mask for the returned handle (e.g. PROCESS_QUERY_LIMITED_INFORMATION for an identity check, PROCESS_DUP_HANDLE for handle smuggling). |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Object-attributes block (usually inheritance flags only). May be NULL. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0x83 | win10-1507 |
| Win10 1607 | 0x83 | win10-1607 |
| Win10 1703 | 0x84 | win10-1703 |
| Win10 1709 | 0x84 | win10-1709 |
| Win10 1803 | 0x85 | win10-1803 |
| Win10 1809 | 0x85 | win10-1809 |
| Win10 1903 | 0x85 | win10-1903 |
| Win10 1909 | 0x85 | win10-1909 |
| Win10 2004 | 0x87 | win10-2004 |
| Win10 20H2 | 0x87 | win10-20h2 |
| Win10 21H1 | 0x87 | win10-21h1 |
| Win10 21H2 | 0x87 | win10-21h2 |
| Win10 22H2 | 0x87 | win10-22h2 |
| Win11 21H2 | 0x87 | win11-21h2 |
| Win11 22H2 | 0x87 | win11-22h2 |
| Win11 23H2 | 0x87 | win11-23h2 |
| Win11 24H2 | 0x89 | win11-24h2 |
| Server 2016 | 0x83 | winserver-2016 |
| Server 2019 | 0x85 | winserver-2019 |
| Server 2022 | 0x87 | winserver-2022 |
| Server 2025 | 0x89 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 89 00 00 00 mov eax, 0x89 F6 04 25 08 03 FE 7F 01 test byte ptr [0x7FFE0308], 1 75 03 jne short +3 0F 05 syscall C3 ret CD 2E int 2Eh C3 ret
Undocumented notes
`NtAlpcOpenSenderProcess` is the server's **authoritative client-identity hook**. After `NtAlpcSendWaitReceivePort` returns a `PORT_MESSAGE`, the server holds only the message header — which is fully attacker-controlled in every field *except* `ClientId`, which the kernel stamps from the sender's EPROCESS at send time. This syscall takes that `ClientId.UniqueProcess` and returns a real `HANDLE` to the EPROCESS object, opened with the requested access. Used to: (a) check the sender's image path (`QueryFullProcessImageName`), (b) check its token (`NtOpenProcessToken`), (c) check its protection level (PsProtectedSignerSystem etc. via `NtQueryInformationProcess(ProcessProtectionInformation)`). The kernel implementation `AlpcpOpenSender` validates that the message actually arrived on the supplied port and that the sender still exists — pids are not reused while the message holds a reference, so PID-recycle races are not possible *through this primitive*.
Common malware usage
The primitive itself is benign; **bugs in its callers** are not. Three notorious classes: 1. **Forgotten identity check** — a privileged ALPC server (TaskHost, Spooler, DiagTrack) trusts `PORT_MESSAGE.ClientId` for a privileged decision *without* calling `NtAlpcOpenSenderProcess` to canonicalise. An attacker spoofs `ClientId` in a crafted message and triggers a privileged action. **CVE-2018-8440** (SandboxEscaper's ALPC LPE, Task Scheduler) and several PrintNightmare-adjacent bugs were variants of this. 2. **Wrong-DesiredAccess** — server opens with `PROCESS_DUP_HANDLE` 'just in case' and then leaks the handle back to the client; client uses it to inject into the server. Real bug in WinHTTP service, ca. 2020. 3. **Sender-identity TOCTOU** — server opens the sender, reads its image path, then performs work; meanwhile the sender execs a privileged binary via `NtCreateUserProcess` re-using the same pid (rare; needs PROCESS_CREATE_PROCESS). Offensive tooling: the **rpcview** / **alpc-fuzzing** lineage (Clément Rouault, James Forshaw) exercises this exact path to map server-side trust boundaries.
Detection opportunities
Outside `lsass`, `services.exe`, `spoolsv.exe`, Task Scheduler, and a handful of well-known RPC servers, calls to `NtAlpcOpenSenderProcess` are uncommon. ETW provider `Microsoft-Windows-Threat-Intelligence` (Defender-only) traces handle opens with origin; `Microsoft-Windows-RPC` shows the corresponding RPC call if the server uses LRPC. Hunt for **unexpected privileged servers** opening senders — a user-context implant pretending to be an RPC server will issue this syscall against incoming connections from other implants. Pair with `NtOpenProcessToken` on the same returned handle to catch the full identity-check pattern, and with token impersonation downstream (`NtAlpcImpersonateClientOfPort` or `ImpersonateLoggedOnUser`) — that triplet is the classic ALPC trust boundary, and any caller not on the Microsoft allowlist warrants triage.
Direct syscall examples
cALPC server identity-check pattern
// Canonical server-side trust gate: never trust PORT_MESSAGE.ClientId for an
// authorisation decision — always re-derive the sender via NtAlpcOpenSenderProcess.
#include <windows.h>
#include <winternl.h>
NTSTATUS NTAPI NtAlpcOpenSenderProcess(
PHANDLE, HANDLE, PPORT_MESSAGE, ULONG, ACCESS_MASK, POBJECT_ATTRIBUTES);
BOOL CallerIsSystem(HANDLE serverPort, PPORT_MESSAGE msg) {
HANDLE hSender = NULL;
if (NtAlpcOpenSenderProcess(
&hSender, serverPort, msg,
0,
PROCESS_QUERY_LIMITED_INFORMATION,
NULL) < 0) {
return FALSE;
}
HANDLE hTok = NULL;
BOOL ok = OpenProcessToken(hSender, TOKEN_QUERY, &hTok);
if (hSender) CloseHandle(hSender);
if (!ok) return FALSE;
BYTE buf[512]; DWORD ret = 0;
BOOL isSystem = FALSE;
if (GetTokenInformation(hTok, TokenUser, buf, sizeof(buf), &ret)) {
PTOKEN_USER tu = (PTOKEN_USER)buf;
// Compare tu->User.Sid against S-1-5-18 (LocalSystem)
SID localSystem = {SID_REVISION, 1, {0,0,0,0,0,5}, {18}};
isSystem = EqualSid(tu->User.Sid, &localSystem);
}
CloseHandle(hTok);
return isSystem;
}asmx64 direct stub (Win11 24H2)
; Direct syscall stub for NtAlpcOpenSenderProcess (SSN 0x89 on Win11 24H2 / Server 2025)
NtAlpcOpenSenderProcess PROC
mov r10, rcx ; ProcessHandle
mov eax, 89h ; SSN
syscall
ret
NtAlpcOpenSenderProcess ENDPrustDynamic resolution + null-OA call
// Cargo: ntapi = "0.4", windows-sys = "0.59"
use std::ptr::null_mut;
use windows_sys::Win32::Foundation::HANDLE;
type FnOpenSender = unsafe extern "system" fn(
*mut HANDLE, HANDLE, *const u8, u32, u32, *const u8,
) -> i32;
pub unsafe fn open_sender_process(
server_port: HANDLE, msg: *const u8, desired: u32,
) -> Option<HANDLE> {
let ntdll = libloading::Library::new("ntdll.dll").ok()?;
let f: libloading::Symbol<FnOpenSender> =
ntdll.get(b"NtAlpcOpenSenderProcess\0").ok()?;
let mut h: HANDLE = 0;
let st = f(&mut h, server_port, msg, 0, desired, std::ptr::null());
if st < 0 { None } else { Some(h) }
}MITRE ATT&CK mappings
Last verified: 2026-05-20