> Windows Syscalls
ntoskrnl.exeT1055T1055.002T1620

NtProtectVirtualMemory

Cambia la protección de una región de memoria virtual confirmada en un proceso objetivo.

Prototipo

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

Argumentos

NameTypeDirDescription
ProcessHandleHANDLEinHandle al proceso objetivo. Use NtCurrentProcess() ((HANDLE)-1) para sí mismo.
BaseAddressPVOID*in/outPuntero a la dirección base de la región. Alineado hacia abajo a una página al retornar.
NumberOfBytesToProtectPSIZE_Tin/outPuntero al tamaño en bytes. Redondeado al límite de página al retornar.
NewAccessProtectionULONGinNueva constante de protección de memoria, p. ej. PAGE_EXECUTE_READ, PAGE_READWRITE.
OldAccessProtectionPULONGoutRecibe el valor de protección anterior de la primera página de la región.

IDs de syscalls por versión de Windows

Versión 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

Módulo del kernel

ntoskrnl.exeNtProtectVirtualMemory

APIs relacionadas

VirtualProtectVirtualProtectExNtAllocateVirtualMemoryNtWriteVirtualMemoryZwProtectVirtualMemory

Stub del 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

Notas no documentadas

NtProtectVirtualMemory mantiene el SSN `0x50` desde Windows 10 1507 hasta Windows 11 24H2 — uno de los números más estables de la plataforma. Es el punto de entrada del kernel detrás de `VirtualProtect`/`VirtualProtectEx` y termina llamando a `MiProtectVirtualMemory`. La función redondea silenciosamente `BaseAddress` hacia abajo y `NumberOfBytesToProtect` hacia arriba a la frontera de página, por lo que tocar un solo byte de una página adyacente reprotegerá esa página completa.

Uso común por malware

Es la segunda mitad del patrón canónico de loader: la región se asigna como `PAGE_READWRITE`, se escribe el payload y luego `NtProtectVirtualMemory` la cambia a `PAGE_EXECUTE_READ` (o `PAGE_EXECUTE_READWRITE` para stubs auto-modificables). Pasar de RW a RX tras la escritura es la forma clásica de eludir escáneres que solo buscan RWX y aún así terminar con código ejecutable. Usado en loaders de beacon de Cobalt Strike, runners de shellcode de Sliver, Brute Ratel y la mayoría de las implementaciones de module stomping y thread stack spoofing.

Oportunidades de detección

Las transiciones a RX o RWX sobre memoria privada (no imagen) son un indicador de alta señal. Sysmon no tiene un evento dedicado, pero ETW Threat Intelligence (`Microsoft-Windows-Threat-Intelligence`) emite `EtwTiLogProtectExecVm` siempre que se aplica protección ejecutable a memoria privada — exactamente el evento al que se suscriben los EDR. Los hooks de userland sobre `NtProtectVirtualMemory` en ntdll se eluden con syscalls directos o indirectos, pero la telemetría ETW-TI del kernel sigue disparándose. PE-Sieve y Moneta detectan post-mortem regiones RX privadas sin respaldo de imagen.

Ejemplos de syscalls directos

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 */);

Mapeos MITRE ATT&CK

Last verified: 2026-05-20