NtLockVirtualMemory
Pins a virtual memory region in the process's working set so its pages cannot be paged out.
Prototype
NTSTATUS NtLockVirtualMemory( HANDLE ProcessHandle, PVOID *BaseAddress, PSIZE_T RegionSize, ULONG MapType );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | HANDLE | in | Target process handle. Almost always NtCurrentProcess() ((HANDLE)-1); locking another process's memory requires PROCESS_VM_OPERATION. |
| BaseAddress | PVOID* | in/out | Pointer to the base of the region to lock. Adjusted to the actual page-aligned base on return. |
| RegionSize | PSIZE_T | in/out | Pointer to the size in bytes. Rounded up to a multiple of the page size on return. |
| MapType | ULONG | in | Lock type: MAP_PROCESS (1) keeps pages in the working set (the VirtualLock semantic); MAP_SYSTEM (2) requires SeLockMemoryPrivilege and pins pages in physical RAM at the kernel level. |
Syscall IDs by Windows version
| Windows version | Syscall ID | 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 |
Kernel module
Related APIs
Syscall stub
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
Undocumented notes
The kernel implementation behind `VirtualLock` (MAP_PROCESS) and the rarely-used `AllocateUserPhysicalPages` cohort (MAP_SYSTEM). With MAP_PROCESS, the kernel grows the process's working-set minimum so that the locked range stays resident — pages can still be paged out if the working-set quota is later exceeded by other allocations, contrary to common belief. MAP_SYSTEM is what actually pins pages to physical frames, but requires SeLockMemoryPrivilege (which by default only the LocalSystem account holds). The SSN drifts modestly across builds (`0x108` Win10 1903, `0x10D` 2004, `0x117` Win11 24H2 / Server 2025), so Hell's-Gate-style resolution is recommended.
Common malware usage
The headline malware use is in **sleep-mask** designs such as Ekko, Foliage, FOLIAGE-style variants and the Cobalt Strike `Sleep_Mask` user-defined-reflective-loader (UDRL) family. The pattern: encrypt the beacon's `.text`/heap region in place, sleep for N seconds, then decrypt on wake. If the encrypted region is paged out while sleeping, the OS may write ciphertext to the pagefile *and* fault it back in noisily on wake — defeating both stealth and timing. `NtLockVirtualMemory(MAP_PROCESS)` keeps the region in the working set so the encrypt-sleep-decrypt cycle stays predictable and quiet. A secondary use is in custom packers that lock the unpacked OEP region to defeat page-fault-based memory-introspection scanners (e.g. early Pafish-style detection that races EDR scan against pagefault timing). Note: MAP_SYSTEM is essentially unreachable from non-SYSTEM user code, so the malware-relevant path is always MAP_PROCESS.
Detection opportunities
VirtualLock itself is extremely rare in legitimate user-mode software (Adobe Reader uses it for licensing key material, KeePass for secret buffers, OpenSSL FIPS mode for keys, and that is roughly the entire population). An unsigned binary calling NtLockVirtualMemory on a freshly-`NtAllocateVirtualMemory`d RWX region, then `NtDelayExecution`-ing for several seconds, then unlocking — that pattern is far rarer in benign code than it is in Ekko/Foliage sleep masks. ETW Threat Intelligence does *not* expose locking events natively; the most reliable signal is hooking the user-mode `VirtualLock` thunk and correlating with allocation flags and call-site backing module. EDRs that perform periodic working-set scans (CrowdStrike, MDE) effectively defeat the sleep-mask hiding trick because they read the region's bytes regardless of whether they're locked.
Direct syscall examples
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);
}
}
}MITRE ATT&CK mappings
Last verified: 2026-05-20