NtFlushInstructionCache
Invalide le cache d'instructions sur une région d'un processus cible pour que du code fraîchement écrit puisse être exécuté.
Prototype
NTSTATUS NtFlushInstructionCache( HANDLE ProcessHandle, PVOID BaseAddress, SIZE_T Length );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | HANDLE | in | Processus dont le cache d'instructions doit être vidé. NtCurrentProcess() pour soi-même. |
| BaseAddress | PVOID | in | Début de la région dont les instructions doivent être invalidées. NULL vide tout le cache d'instructions. |
| Length | SIZE_T | in | Longueur de la région en octets. Ignorée si BaseAddress est NULL. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | Build |
|---|---|---|
| Win10 1507 | 0xD9 | win10-1507 |
| Win10 1607 | 0xDC | win10-1607 |
| Win10 1703 | 0xDF | win10-1703 |
| Win10 1709 | 0xE0 | win10-1709 |
| Win10 1803 | 0xE1 | win10-1803 |
| Win10 1809 | 0xE2 | win10-1809 |
| Win10 1903 | 0xE3 | win10-1903 |
| Win10 1909 | 0xE3 | win10-1909 |
| Win10 2004 | 0xE8 | win10-2004 |
| Win10 20H2 | 0xE8 | win10-20h2 |
| Win10 21H1 | 0xE8 | win10-21h1 |
| Win10 21H2 | 0xE9 | win10-21h2 |
| Win10 22H2 | 0xE9 | win10-22h2 |
| Win11 21H2 | 0xEE | win11-21h2 |
| Win11 22H2 | 0xEF | win11-22h2 |
| Win11 23H2 | 0xEF | win11-23h2 |
| Win11 24H2 | 0xF1 | win11-24h2 |
| Server 2016 | 0xDC | winserver-2016 |
| Server 2019 | 0xE2 | winserver-2019 |
| Server 2022 | 0xED | winserver-2022 |
| Server 2025 | 0xF1 | winserver-2025 |
Module noyau
APIs liées
Stub du syscall
4C 8B D1 mov r10, rcx B8 F1 00 00 00 mov eax, 0xF1 ; Win11 24H2 SSN 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
Sur x86/x64, les règles architecturales sérialisent déjà le fetch d'instructions vis-à-vis des écritures de données après un `cpuid`/`iret`/branchement sur le même processeur logique, donc le syscall est essentiellement un no-op pour un self-flush sur le core courant — son vrai rôle est de diffuser un IPI à chaque autre processeur exécutant le processus cible pour qu'il invalide ses lignes de cache L1 I. Sur ARM64 (où les règles sont plus faibles), il émet la séquence `ic ivau` / `dsb ish` / `isb` requise par l'architecture après toute modification de code. Le wrapper Win32 mince est `FlushInstructionCache`, inconditionnellement appelé par `JIT_GenericResolverCommon` dans le CLR, par `CodeRange::CommitPages` de V8, par Chakra, et par tout loader reflectif qui se respecte.
Usage courant par les malwares
Étape obligatoire dans **tout** loader reflectif de shellcode ou de PE qui écrit des octets exécutables au runtime. La séquence classique : `NtAllocateVirtualMemory(MEM_COMMIT, PAGE_READWRITE)` → memcpy payload → `NtProtectVirtualMemory(PAGE_EXECUTE_READ)` → `NtFlushInstructionCache`. Sauter le flush est la raison n°1 pour laquelle les nouveaux loaders crashent sur ARM64 Windows mais marchent sur x64 — et même sur x64, l'omettre produit des crashes intermittents difficiles à reproduire quand un thread SMT frère touche la page cible. ScareCrow, les stagers générés par Donut, l'injection reflective de DLL classique de Stephen Fewer, MemoryModule, et pratiquement tout user-defined reflective loader Cobalt Strike l'appellent. Les loaders direct-syscall qui résolvent le SSN dynamiquement (Hell's Gate / Halo's Gate / Tartarus' Gate) codent généralement celui-ci en dur car il est très peu coûteux.
Opportunités de détection
Par appel, quasi aucun signal en logiciel légitime parce que les JIT l'appellent constamment. La télémétrie intéressante est la *combinaison* : un processus sans runtime managé (CLR, V8, JVM, Chakra) ni hôte de contenu navigateur/Electron qui appelle `NtFlushInstructionCache` sur une région récemment `PAGE_EXECUTE_READWRITE` ou dont `MEMORY_BASIC_INFORMATION::AllocationBase` ne mappe aucune image disque est un IOC manuel de loader reflectif. ETW `Microsoft-Windows-Threat-Intelligence` (Defender ETW-TI) émet un event façon `VirtualProtect` pour la transition de protection qui précède *immédiatement* le flush dans presque tous les loaders — coupler les deux.
Exemples de syscalls directs
cReflective loader fix-up
// Final step of a reflective PE loader.
// pImageBase points at freshly relocated bytes; size is the SizeOfImage.
// 1) Flip the .text section to PAGE_EXECUTE_READ.
ULONG oldProt;
NtProtectVirtualMemory(NtCurrentProcess(),
(PVOID*)&pTextBase, &textSize,
PAGE_EXECUTE_READ, &oldProt);
// 2) Invalidate the I-cache across every core that runs us.
NtFlushInstructionCache(NtCurrentProcess(), pTextBase, textSize);
// 3) Safe to call the DLL's entry point.
DllMain_t entry = (DllMain_t)((BYTE*)pImageBase + nt->OptionalHeader.AddressOfEntryPoint);
entry((HMODULE)pImageBase, DLL_PROCESS_ATTACH, NULL);asmx64 direct stub (Win11 24H2)
; NtFlushInstructionCache direct stub — SSN 0xF1 on Win11 24H2
NtFlushInstructionCache PROC
mov r10, rcx
mov eax, 0F1h
syscall
ret
NtFlushInstructionCache ENDPrustSelf-flush after writing shellcode
use ntapi::ntmmapi::NtFlushInstructionCache;
use winapi::shared::ntdef::HANDLE;
unsafe fn flush_self(addr: *mut u8, len: usize) {
let s = NtFlushInstructionCache(
-1isize as HANDLE, // NtCurrentProcess()
addr as _,
len,
);
assert!(s >= 0);
}Mappings MITRE ATT&CK
Last verified: 2026-05-20