NtLockVirtualMemory
Verrouille une région de mémoire virtuelle dans le working set du processus pour qu'aucune page ne soit paginée.
Prototype
NTSTATUS NtLockVirtualMemory( HANDLE ProcessHandle, PVOID *BaseAddress, PSIZE_T RegionSize, ULONG MapType );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | HANDLE | in | Handle du processus cible. Presque toujours NtCurrentProcess() ((HANDLE)-1) ; verrouiller la mémoire d'un autre processus exige PROCESS_VM_OPERATION. |
| BaseAddress | PVOID* | in/out | Pointeur vers la base de la région à verrouiller. Ajusté à la base alignée page au retour. |
| RegionSize | PSIZE_T | in/out | Pointeur vers la taille en octets. Arrondie à un multiple de la taille de page au retour. |
| MapType | ULONG | in | Type de verrou : MAP_PROCESS (1) garde les pages dans le working set (sémantique VirtualLock) ; MAP_SYSTEM (2) requiert SeLockMemoryPrivilege et épingle les pages en RAM physique au niveau noyau. |
IDs de syscalls par version de Windows
| Version 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 |
Module noyau
APIs liées
Stub du 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
Notes non documentées
L'implémentation noyau derrière `VirtualLock` (MAP_PROCESS) et la cohorte rarement utilisée `AllocateUserPhysicalPages` (MAP_SYSTEM). Avec MAP_PROCESS, le noyau augmente le minimum du working set du processus pour que la plage verrouillée reste résidente — contrairement à une croyance répandue, les pages peuvent toujours être paginées si le quota de working set est ensuite dépassé par d'autres allocations. MAP_SYSTEM est ce qui épingle réellement les pages aux frames physiques, mais exige SeLockMemoryPrivilege (que par défaut seul LocalSystem détient). Le SSN dérive modestement (`0x108` Win10 1903, `0x10D` 2004, `0x117` Win11 24H2 / Server 2025), une résolution style Hell's Gate est donc recommandée.
Usage courant par les malwares
L'usage malware phare est dans les conceptions de **sleep-mask** telles qu'Ekko, Foliage, les variantes FOLIAGE-like et la famille UDRL `Sleep_Mask` de Cobalt Strike. Le motif : chiffrer en place la région `.text`/heap du beacon, dormir N secondes, déchiffrer au réveil. Si la région chiffrée est paginée pendant le sommeil, l'OS peut écrire le chiffré dans le pagefile *et* le re-faulter bruyamment au réveil — défaisant à la fois la furtivité et le timing. `NtLockVirtualMemory(MAP_PROCESS)` garde la région dans le working set pour que le cycle chiffrer-dormir-déchiffrer reste prévisible et silencieux. Usage secondaire : des packers custom qui verrouillent la région OEP déballée pour défaire les scanners d'introspection mémoire basés sur les page-faults (ex. détection Pafish-style qui course le scan EDR contre le timing de pagefault). À noter : MAP_SYSTEM est essentiellement inaccessible depuis du code user non SYSTEM, donc le chemin malware-pertinent est toujours MAP_PROCESS.
Opportunités de détection
VirtualLock lui-même est extrêmement rare dans le logiciel user-mode légitime (Adobe Reader pour le matériel de clé de licence, KeePass pour les buffers secrets, OpenSSL FIPS pour les clés, et c'est à peu près toute la population). Un binaire non signé qui appelle NtLockVirtualMemory sur une région RWX fraîchement `NtAllocateVirtualMemory`'d, puis `NtDelayExecution`-e plusieurs secondes, puis déverrouille — ce motif est bien plus rare dans le code bénin que dans les sleep-masks Ekko / Foliage. ETW Threat Intelligence n'expose *pas* nativement les événements de verrouillage ; le signal le plus fiable est de hooker le thunk user-mode `VirtualLock` et de corréler avec les flags d'allocation et le module appelant. Les EDR qui font des scans périodiques du working set (CrowdStrike, MDE) défont effectivement le truc de cache sleep-mask car ils lisent les octets de la région indépendamment du verrouillage.
Exemples de syscalls directs
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);
}
}
}Mappings MITRE ATT&CK
Last verified: 2026-05-20