> Windows Syscalls
ntoskrnl.exeT1027.011T1027T1106

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

NameTypeDirDescription
ProcessHandleHANDLEinTarget process handle. Almost always NtCurrentProcess() ((HANDLE)-1); locking another process's memory requires PROCESS_VM_OPERATION.
BaseAddressPVOID*in/outPointer to the base of the region to lock. Adjusted to the actual page-aligned base on return.
RegionSizePSIZE_Tin/outPointer to the size in bytes. Rounded up to a multiple of the page size on return.
MapTypeULONGinLock 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 versionSyscall IDBuild
Win10 15070xFCwin10-1507
Win10 16070x101win10-1607
Win10 17030x105win10-1703
Win10 17090x106win10-1709
Win10 18030x107win10-1803
Win10 18090x107win10-1809
Win10 19030x108win10-1903
Win10 19090x108win10-1909
Win10 20040x10Dwin10-2004
Win10 20H20x10Dwin10-20h2
Win10 21H10x10Dwin10-21h1
Win10 21H20x10Ewin10-21h2
Win10 22H20x10Ewin10-22h2
Win11 21H20x114win11-21h2
Win11 22H20x115win11-22h2
Win11 23H20x115win11-23h2
Win11 24H20x117win11-24h2
Server 20160x101winserver-2016
Server 20190x107winserver-2019
Server 20220x113winserver-2022
Server 20250x117winserver-2025

Kernel module

ntoskrnl.exeNtLockVirtualMemory

Related APIs

VirtualLockVirtualUnlockNtUnlockVirtualMemoryNtAllocateVirtualMemoryNtProtectVirtualMemoryAllocateUserPhysicalPages

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 ENDP

cEkko-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