NtAlpcOpenSenderProcess
Helper côté serveur qui ouvre un HANDLE vers le processus ayant envoyé un message ALPC donné.
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 | Reçoit un HANDLE vers l'objet EPROCESS de l'expéditeur, ouvert avec DesiredAccess. |
| PortHandle | HANDLE | in | Handle vers le port de communication ALPC du serveur ayant reçu le message. |
| PortMessage | PPORT_MESSAGE | in | Le PORT_MESSAGE renvoyé par NtAlpcSendWaitReceivePort ; son ClientId.UniqueProcess identifie l'expéditeur. |
| Flags | ULONG | in | Drapeaux réservés / contextuels. Typiquement 0 ; les appelants kernel-mode peuvent passer ALPC_OPENSENDER_KERNEL_MODE. |
| DesiredAccess | ACCESS_MASK | in | Masque d'accès pour le handle retourné (ex. PROCESS_QUERY_LIMITED_INFORMATION pour une vérification d'identité, PROCESS_DUP_HANDLE pour smuggling de handle). |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Bloc d'attributs objet (généralement uniquement des flags d'héritage). Peut être NULL. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | 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 |
Module noyau
APIs liées
Stub du syscall
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
Notes non documentées
`NtAlpcOpenSenderProcess` est le **hook d'identité client autoritatif** du serveur. Après que `NtAlpcSendWaitReceivePort` retourne un `PORT_MESSAGE`, le serveur ne tient que l'en-tête de message — entièrement contrôlé par l'attaquant dans tous les champs *sauf* `ClientId`, que le noyau estampille depuis l'EPROCESS de l'expéditeur à l'envoi. Ce syscall prend ce `ClientId.UniqueProcess` et retourne un vrai `HANDLE` vers l'objet EPROCESS, ouvert avec l'accès demandé. Sert à : (a) vérifier le chemin d'image (`QueryFullProcessImageName`), (b) vérifier le token (`NtOpenProcessToken`), (c) vérifier le niveau de protection (PsProtectedSignerSystem, etc., via `NtQueryInformationProcess(ProcessProtectionInformation)`). L'implémentation noyau `AlpcpOpenSender` valide que le message est bien arrivé sur le port fourni et que l'expéditeur existe encore — les pids ne sont pas réutilisés tant que le message en détient une référence, donc les courses de PID-recycle ne sont pas possibles *via cette primitive*.
Usage courant par les malwares
La primitive en elle-même est bénigne ; les **bugs chez ses appelants** ne le sont pas. Trois classes notoires : 1. **Vérification d'identité oubliée** — un serveur ALPC privilégié (TaskHost, Spooler, DiagTrack) fait confiance à `PORT_MESSAGE.ClientId` pour une décision privilégiée *sans* appeler `NtAlpcOpenSenderProcess` pour canonicaliser. Un attaquant spoofe `ClientId` dans un message forgé et déclenche une action privilégiée. **CVE-2018-8440** (LPE ALPC de SandboxEscaper, Task Scheduler) et plusieurs bugs proches de PrintNightmare étaient des variantes. 2. **Mauvais DesiredAccess** — le serveur ouvre avec `PROCESS_DUP_HANDLE` 'au cas où' puis fuite le handle au client ; le client l'utilise pour injecter dans le serveur. Bug réel dans le service WinHTTP, vers 2020. 3. **TOCTOU d'identité de l'expéditeur** — le serveur ouvre l'expéditeur, lit son chemin d'image puis agit ; entretemps l'expéditeur exec un binaire privilégié via `NtCreateUserProcess` en réutilisant le même pid (rare ; nécessite PROCESS_CREATE_PROCESS). Outillage offensif : la lignée **rpcview** / **alpc-fuzzing** (Clément Rouault, James Forshaw) exerce exactement ce chemin pour cartographier les frontières de confiance côté serveur.
Opportunités de détection
En dehors de `lsass`, `services.exe`, `spoolsv.exe`, du Task Scheduler et d'une poignée de serveurs RPC bien connus, les appels à `NtAlpcOpenSenderProcess` sont rares. Le provider ETW `Microsoft-Windows-Threat-Intelligence` (Defender uniquement) trace les ouvertures de handle avec origine ; `Microsoft-Windows-RPC` montre l'appel RPC correspondant si le serveur utilise LRPC. Chasser les **serveurs privilégiés inattendus** qui ouvrent leurs expéditeurs — un implant user-context prétendant être un serveur RPC émettra ce syscall sur les connexions entrantes d'autres implants. Coupler avec `NtOpenProcessToken` sur le même handle pour attraper le pattern complet de vérification d'identité, puis l'impersonation token aval (`NtAlpcImpersonateClientOfPort` ou `ImpersonateLoggedOnUser`) — ce triplet est la frontière de confiance ALPC classique, et tout appelant hors allowlist Microsoft mérite triage.
Exemples de syscalls directs
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) }
}Mappings MITRE ATT&CK
Last verified: 2026-05-20