NtLockVirtualMemory
Fija una región de memoria virtual en el working set del proceso para que sus páginas no sean paginadas.
Prototipo
NTSTATUS NtLockVirtualMemory( HANDLE ProcessHandle, PVOID *BaseAddress, PSIZE_T RegionSize, ULONG MapType );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | HANDLE | in | Handle al proceso objetivo. Casi siempre NtCurrentProcess() ((HANDLE)-1); bloquear memoria de otro proceso requiere PROCESS_VM_OPERATION. |
| BaseAddress | PVOID* | in/out | Puntero a la base de la región a bloquear. Ajustado a la base alineada a página al retornar. |
| RegionSize | PSIZE_T | in/out | Puntero al tamaño en bytes. Redondeado a un múltiplo del tamaño de página al retornar. |
| MapType | ULONG | in | Tipo de bloqueo: MAP_PROCESS (1) mantiene páginas en el working set (semántica VirtualLock); MAP_SYSTEM (2) requiere SeLockMemoryPrivilege y fija páginas en RAM física a nivel kernel. |
IDs de syscalls por versión de Windows
| Versión de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0xFC | win10-1507 |
| Win10 1607 | 0x101 | win10-1607 |
| Win10 1703 | 0x105 | win10-1703 |
| Win10 1709 | 0x106 | win10-1709 |
| Win10 1803 | 0x107 | win10-1803 |
| Win10 1809 | 0x107 | win10-1809 |
| Win10 1903 | 0x108 | win10-1903 |
| Win10 1909 | 0x108 | win10-1909 |
| Win10 2004 | 0x10D | win10-2004 |
| Win10 20H2 | 0x10D | win10-20h2 |
| Win10 21H1 | 0x10D | win10-21h1 |
| Win10 21H2 | 0x10E | win10-21h2 |
| Win10 22H2 | 0x10E | win10-22h2 |
| Win11 21H2 | 0x114 | win11-21h2 |
| Win11 22H2 | 0x115 | win11-22h2 |
| Win11 23H2 | 0x115 | win11-23h2 |
| Win11 24H2 | 0x117 | win11-24h2 |
| Server 2016 | 0x101 | winserver-2016 |
| Server 2019 | 0x107 | winserver-2019 |
| Server 2022 | 0x113 | winserver-2022 |
| Server 2025 | 0x117 | winserver-2025 |
Módulo del kernel
APIs relacionadas
Stub del syscall
4C 8B D1 mov r10, rcx B8 17 01 00 00 mov eax, 0x117 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
La implementación kernel detrás de `VirtualLock` (MAP_PROCESS) y la cohorte raramente usada `AllocateUserPhysicalPages` (MAP_SYSTEM). Con MAP_PROCESS, el kernel aumenta el mínimo del working set del proceso para que el rango bloqueado permanezca residente — al contrario de la creencia común, las páginas todavía pueden paginarse si la cuota del working set es excedida después por otras asignaciones. MAP_SYSTEM es lo que realmente fija páginas a frames físicos, pero requiere SeLockMemoryPrivilege (que por defecto sólo LocalSystem posee). El SSN deriva modestamente (`0x108` Win10 1903, `0x10D` 2004, `0x117` Win11 24H2 / Server 2025), así que la resolución estilo Hell's Gate es recomendada.
Uso común por malware
El uso malware estrella es en diseños de **sleep-mask** como Ekko, Foliage, variantes tipo FOLIAGE y la familia UDRL `Sleep_Mask` de Cobalt Strike. El patrón: cifrar en sitio la región `.text`/heap del beacon, dormir N segundos, descifrar al despertar. Si la región cifrada se pagina durante el sueño, el OS puede escribir el cifrado al pagefile *y* refaultearlo ruidosamente al despertar — derrotando tanto el sigilo como el timing. `NtLockVirtualMemory(MAP_PROCESS)` mantiene la región en el working set para que el ciclo cifrar-dormir-descifrar quede predecible y silencioso. Uso secundario: packers custom que bloquean la región OEP desempacada para vencer escáneres de introspección de memoria basados en page-fault (p. ej. detección estilo Pafish que carrerea el scan EDR contra el timing de pagefault). Nota: MAP_SYSTEM es esencialmente inalcanzable desde código user no SYSTEM, así que el camino malware-relevante es siempre MAP_PROCESS.
Oportunidades de detección
VirtualLock por sí solo es extremadamente raro en software user-mode legítimo (Adobe Reader para material de clave de licencia, KeePass para buffers de secretos, OpenSSL FIPS para claves, y eso es aproximadamente toda la población). Un binario no firmado que llama a NtLockVirtualMemory sobre una región RWX recién `NtAllocateVirtualMemory`-da, luego `NtDelayExecution`-ea varios segundos, luego desbloquea — ese patrón es mucho más raro en código benigno que en sleep-masks Ekko / Foliage. ETW Threat Intelligence *no* expone eventos de bloqueo nativamente; la señal más confiable es hookear el thunk user-mode `VirtualLock` y correlacionar con flags de asignación y módulo del call-site. Los EDR que hacen scans periódicos del working set (CrowdStrike, MDE) efectivamente derrotan el truco de ocultación del sleep-mask porque leen los bytes de la región sin importar si están bloqueados.
Ejemplos de syscalls directos
asmx64 stub (Win11 24H2 SSN 0x117)
; Direct syscall stub for NtLockVirtualMemory
NtLockVirtualMemory PROC
mov r10, rcx ; syscall convention
mov eax, 117h ; SSN (Win11 24H2 / Server 2025)
syscall
ret
NtLockVirtualMemory ENDPcEkko-style sleep-mask page pin
// Sleep-mask skeleton: lock the encrypted region so the kernel can't
// swap ciphertext to disk while we wait on a timer-queue or APC.
#include <windows.h>
typedef NTSTATUS (NTAPI *pNtLockVirtualMemory)(HANDLE, PVOID*, PSIZE_T, ULONG);
typedef NTSTATUS (NTAPI *pNtUnlockVirtualMemory)(HANDLE, PVOID*, PSIZE_T, ULONG);
#define MAP_PROCESS 1
void SleepWithLockedRegion(PVOID base, SIZE_T size, DWORD ms) {
HMODULE n = GetModuleHandleA("ntdll.dll");
pNtLockVirtualMemory NtLock = (pNtLockVirtualMemory) GetProcAddress(n, "NtLockVirtualMemory");
pNtUnlockVirtualMemory NtUnlock = (pNtUnlockVirtualMemory)GetProcAddress(n, "NtUnlockVirtualMemory");
PVOID b = base;
SIZE_T s = size;
NtLock((HANDLE)-1, &b, &s, MAP_PROCESS);
// ... XOR-encrypt b[0..s] in place ...
Sleep(ms);
// ... XOR-decrypt b[0..s] in place ...
NtUnlock((HANDLE)-1, &b, &s, MAP_PROCESS);
}rustPin a secret buffer (KeePass-style)
// Defensive use: keep a derived key out of the pagefile.
use windows_sys::Win32::System::Memory::{VirtualLock, VirtualUnlock};
pub struct PinnedSecret { ptr: *mut u8, len: usize }
impl PinnedSecret {
pub fn new(buf: &mut [u8]) -> std::io::Result<Self> {
let ptr = buf.as_mut_ptr();
let len = buf.len();
if unsafe { VirtualLock(ptr as _, len) } == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Self { ptr, len })
}
}
impl Drop for PinnedSecret {
fn drop(&mut self) {
unsafe {
std::ptr::write_bytes(self.ptr, 0, self.len);
VirtualUnlock(self.ptr as _, self.len);
}
}
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20