> Windows Syscalls
ntoskrnl.exeT1055T1055.002T1620

NtProtectVirtualMemory

Modifie la protection d'une région de mémoire virtuelle validée dans un processus cible.

Prototype

NTSTATUS NtProtectVirtualMemory(
  HANDLE  ProcessHandle,
  PVOID  *BaseAddress,
  PSIZE_T NumberOfBytesToProtect,
  ULONG   NewAccessProtection,
  PULONG  OldAccessProtection
);

Arguments

NameTypeDirDescription
ProcessHandleHANDLEinHandle vers le processus cible. Utilisez NtCurrentProcess() ((HANDLE)-1) pour soi-même.
BaseAddressPVOID*in/outPointeur vers l'adresse de base de la région. Aligné vers le bas sur une page au retour.
NumberOfBytesToProtectPSIZE_Tin/outPointeur vers la taille en octets. Arrondi à la page supérieure au retour.
NewAccessProtectionULONGinNouvelle constante de protection mémoire, ex. PAGE_EXECUTE_READ, PAGE_READWRITE.
OldAccessProtectionPULONGoutReçoit la valeur de protection précédente de la première page de la région.

IDs de syscalls par version de Windows

Version de WindowsID de syscallBuild
Win10 15070x50win10-1507
Win10 16070x50win10-1607
Win10 17030x50win10-1703
Win10 17090x50win10-1709
Win10 18030x50win10-1803
Win10 18090x50win10-1809
Win10 19030x50win10-1903
Win10 19090x50win10-1909
Win10 20040x50win10-2004
Win10 20H20x50win10-20h2
Win10 21H10x50win10-21h1
Win10 21H20x50win10-21h2
Win10 22H20x50win10-22h2
Win11 21H20x50win11-21h2
Win11 22H20x50win11-22h2
Win11 23H20x50win11-23h2
Win11 24H20x50win11-24h2
Server 20160x50winserver-2016
Server 20190x50winserver-2019
Server 20220x50winserver-2022
Server 20250x50winserver-2025

Module noyau

ntoskrnl.exeNtProtectVirtualMemory

APIs liées

VirtualProtectVirtualProtectExNtAllocateVirtualMemoryNtWriteVirtualMemoryZwProtectVirtualMemory

Stub du syscall

4C 8B D1            mov r10, rcx
B8 50 00 00 00      mov eax, 0x50
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

NtProtectVirtualMemory conserve le SSN `0x50` de Windows 10 1507 jusqu'à Windows 11 24H2 — l'un des numéros les plus stables de la plateforme. C'est le point d'entrée noyau derrière `VirtualProtect`/`VirtualProtectEx` qui aboutit à `MiProtectVirtualMemory`. La fonction arrondit silencieusement `BaseAddress` vers le bas et `NumberOfBytesToProtect` vers le haut sur les bornes de page, donc une requête touchant un seul octet d'une page adjacente reprotégera la page entière.

Usage courant par les malwares

C'est la seconde moitié du schéma canonique de loader : la région est allouée en `PAGE_READWRITE`, le payload est écrit, puis `NtProtectVirtualMemory` la bascule en `PAGE_EXECUTE_READ` (ou `PAGE_EXECUTE_READWRITE` pour des stubs auto-modifiants). Passer de RW à RX après l'écriture est la façon classique de déjouer les scanners qui ne cherchent que du RWX tout en obtenant du code exécutable. Utilisé dans les loaders de beacon Cobalt Strike, les runners shellcode Sliver, Brute Ratel et la plupart des implémentations de module stomping ou de thread stack spoofing.

Opportunités de détection

Les transitions RX ou RWX sur de la mémoire privée (non-image) constituent un signal fort. Sysmon n'a pas d'événement dédié, mais ETW Threat Intelligence (`Microsoft-Windows-Threat-Intelligence`) émet `EtwTiLogProtectExecVm` dès qu'une protection exécutable est appliquée à de la mémoire privée — c'est exactement l'événement auquel les EDR souscrivent. Les hooks userland sur `NtProtectVirtualMemory` dans ntdll sont contournés par les syscalls directs ou indirects, mais la télémétrie ETW-TI côté noyau, elle, reste active. PE-Sieve et Moneta détectent en post-mortem les régions RX privées non adossées à une image.

Exemples de syscalls directs

asmx64 direct stub

; Direct syscall stub for NtProtectVirtualMemory (SSN 0x50, stable across all Win10/11)
NtProtectVirtualMemory PROC
    mov  r10, rcx          ; syscall convention
    mov  eax, 50h          ; SSN
    syscall
    ret
NtProtectVirtualMemory ENDP

cRW -> RX flip after shellcode copy

// Classic two-phase loader: allocate RW, write, flip to RX.
SIZE_T size = payload_len;
PVOID  base = NULL;
NtAllocateVirtualMemory(NtCurrentProcess(), &base, 0, &size,
                        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
memcpy(base, payload, payload_len);

ULONG old = 0;
NTSTATUS st = NtProtectVirtualMemory(NtCurrentProcess(), &base, &size,
                                     PAGE_EXECUTE_READ, &old);
if (NT_SUCCESS(st)) ((void(*)())base)();

cHell's Gate dynamic lookup

// Resolve SSN at runtime from a non-hooked ntdll copy.
typedef NTSTATUS (NTAPI *pNtProtect)(HANDLE, PVOID*, PSIZE_T, ULONG, PULONG);

DWORD ssn = GetSyscallNumber(GetProcAddress(GetModuleHandleA("ntdll.dll"),
                                            "NtProtectVirtualMemory"));
// hand SSN to your indirect-syscall trampoline; ssn is 0x50 on every supported build
set_ssn(ssn);
indirect_syscall_invoke(/* args */);

Mappings MITRE ATT&CK

Last verified: 2026-05-20