> Windows Syscalls
ntoskrnl.exeT1027.011T1027T1106

NtUnlockVirtualMemory

Releases a working-set lock previously taken by NtLockVirtualMemory.

Prototype

NTSTATUS NtUnlockVirtualMemory(
  HANDLE  ProcessHandle,
  PVOID  *BaseAddress,
  PSIZE_T RegionSize,
  ULONG   MapType
);

Arguments

NameTypeDirDescription
ProcessHandleHANDLEinTarget process handle. Almost always NtCurrentProcess() ((HANDLE)-1).
BaseAddressPVOID*in/outPointer to the base of the region to unlock. Page-aligned on return.
RegionSizePSIZE_Tin/outPointer to the size in bytes. Rounded up to a multiple of the page size on return.
MapTypeULONGinMust match the MapType used when locking — MAP_PROCESS (1) for the VirtualUnlock semantic, MAP_SYSTEM (2) for kernel-pinned pages.

Syscall IDs by Windows version

Windows versionSyscall IDBuild
Win10 15070x1AEwin10-1507
Win10 16070x1B7win10-1607
Win10 17030x1BDwin10-1703
Win10 17090x1C1win10-1709
Win10 18030x1C3win10-1803
Win10 18090x1C4win10-1809
Win10 19030x1C5win10-1903
Win10 19090x1C5win10-1909
Win10 20040x1CBwin10-2004
Win10 20H20x1CBwin10-20h2
Win10 21H10x1CBwin10-21h1
Win10 21H20x1CDwin10-21h2
Win10 22H20x1CDwin10-22h2
Win11 21H20x1D7win11-21h2
Win11 22H20x1DBwin11-22h2
Win11 23H20x1DBwin11-23h2
Win11 24H20x1DEwin11-24h2
Server 20160x1B7winserver-2016
Server 20190x1C4winserver-2019
Server 20220x1D3winserver-2022
Server 20250x1DEwinserver-2025

Kernel module

ntoskrnl.exeNtUnlockVirtualMemory

Related APIs

VirtualUnlockVirtualLockNtLockVirtualMemoryNtFreeVirtualMemoryNtProtectVirtualMemory

Syscall stub

4C 8B D1            mov r10, rcx
B8 DE 01 00 00      mov eax, 0x1DE
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 strict counterpart to NtLockVirtualMemory. The SSN drifts on every major release (`0x1C5` Win10 1903, `0x1CB` 2004, `0x1DE` Win11 24H2 / Server 2025). Calling it on a non-locked region returns `STATUS_NOT_LOCKED` (0xC000002A) which is benign; calling it with a mismatched `MapType` returns `STATUS_INVALID_PARAMETER`. The kernel implementation lowers the process's working-set minimum back down, but does not actively page the region — it merely permits the trim. Practically every legitimate use is paired one-to-one with a prior NtLockVirtualMemory.

Common malware usage

The wake-side of the sleep-mask pattern documented under NtLockVirtualMemory. Ekko, Foliage, and the Cobalt Strike `Sleep_Mask` family use a `Lock → Encrypt → Sleep → Decrypt → Unlock` cycle every beacon callback interval; the matching `Unlock` releases the working-set quota so subsequent allocations don't get crowded out. Some packers also unlock the OEP region after a watchdog timeout to reduce their visible working-set footprint. By itself, NtUnlockVirtualMemory is essentially never the locus of malicious behavior — its presence is most useful as a *pairing* signal next to a same-thread NtLockVirtualMemory on the same range earlier in the call trace.

Detection opportunities

Detect the *pair*, not the single call. The classic high-signal sequence within one thread: `NtAllocateVirtualMemory(RWX)` → `NtLockVirtualMemory(MAP_PROCESS)` → `NtDelayExecution(long)` → `NtUnlockVirtualMemory` → execute. Hook the user-mode VirtualUnlock thunk and correlate with the prior VirtualLock site and the allocation's protection bits. Memory scanners that snapshot working-set membership across time can sometimes spot the lock-then-unlock pulse — though many EDRs do not retain that history. As with NtLockVirtualMemory, there is no first-class ETW event; behavior chaining is the path.

Direct syscall examples

asmx64 stub (Win11 24H2 SSN 0x1DE)

; Direct syscall stub for NtUnlockVirtualMemory
NtUnlockVirtualMemory PROC
    mov  r10, rcx          ; syscall convention
    mov  eax, 1DEh         ; SSN (Win11 24H2 / Server 2025)
    syscall
    ret
NtUnlockVirtualMemory ENDP

cWake side of an Ekko-style sleep

// Symmetric counterpart to the SleepWithLockedRegion example: after the
// caller wakes and decrypts in place, unlock so the working-set quota is
// returned and subsequent allocations aren't crowded.
#include <windows.h>

typedef NTSTATUS (NTAPI *pNtUnlockVirtualMemory)(HANDLE, PVOID*, PSIZE_T, ULONG);
#define MAP_PROCESS 1

NTSTATUS UnlockRegion(PVOID base, SIZE_T size) {
    pNtUnlockVirtualMemory NtUnlockVirtualMemory = (pNtUnlockVirtualMemory)
        GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnlockVirtualMemory");
    PVOID  b = base;
    SIZE_T s = size;
    return NtUnlockVirtualMemory((HANDLE)-1, &b, &s, MAP_PROCESS);
}

rustRAII wrapper that pairs Lock / Unlock

// Cargo: windows-sys = "0.59" (Win32_System_Memory)
use windows_sys::Win32::System::Memory::{VirtualLock, VirtualUnlock};

pub struct Pinned { ptr: *mut u8, len: usize }

impl Pinned {
    pub fn new(p: *mut u8, len: usize) -> std::io::Result<Self> {
        if unsafe { VirtualLock(p as _, len) } == 0 {
            return Err(std::io::Error::last_os_error());
        }
        Ok(Self { ptr: p, len })
    }
}

impl Drop for Pinned {
    fn drop(&mut self) {
        // VirtualUnlock — underneath, NtUnlockVirtualMemory(MAP_PROCESS).
        unsafe { VirtualUnlock(self.ptr as _, self.len); }
    }
}

MITRE ATT&CK mappings

Last verified: 2026-05-20