NtFlushInstructionCache
Invalidates the instruction cache for a region in a target process so freshly written code can be executed.
Prototype
NTSTATUS NtFlushInstructionCache( HANDLE ProcessHandle, PVOID BaseAddress, SIZE_T Length );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | HANDLE | in | Process whose I-cache to flush. NtCurrentProcess() for self. |
| BaseAddress | PVOID | in | Start of the region whose instructions should be invalidated. NULL flushes the entire I-cache. |
| Length | SIZE_T | in | Length of the region in bytes. Ignored when BaseAddress is NULL. |
Syscall IDs by Windows version
| Windows version | Syscall ID | 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 |
Kernel module
Related APIs
Syscall stub
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
Undocumented notes
On x86/x64, the architectural rules already serialise instruction fetches against data writes after a `cpuid`/`iret`/branch on the same logical processor, so the syscall is mostly a no-op for self-flushes on the current core — its real job is to broadcast an IPI to every other processor running the target process so they invalidate their L1 I-cache lines. On ARM64 (where the rules are weaker), it issues the `ic ivau` / `dsb ish` / `isb` sequence the architecture requires after any code modification. The Win32 thin wrapper is `FlushInstructionCache`, which is unconditionally called by `JIT_GenericResolverCommon` in CLR, by V8's `CodeRange::CommitPages`, by Chakra, and by every reflective loader worth its salt.
Common malware usage
Mandatory step in **every** reflective shellcode or PE loader that writes executable bytes at runtime. The classic sequence is `NtAllocateVirtualMemory(MEM_COMMIT, PAGE_READWRITE)` → memcpy payload → `NtProtectVirtualMemory(PAGE_EXECUTE_READ)` → `NtFlushInstructionCache`. Skipping the flush is the #1 reason brand-new loaders crash on ARM64 Windows but work on x64 — and even on x64, omitting it produces hard-to-reproduce intermittent crashes when the target page happens to be touched by a sibling SMT thread. ScareCrow, Donut-generated stagers, Stephen Fewer's classic reflective DLL injection, MemoryModule, and virtually every Cobalt Strike user-defined reflective loader call it. Direct-syscall loaders that resolve the SSN dynamically (Hell's Gate / Halo's Gate / Tartarus' Gate) usually hardcode this one too because it's so cheap.
Detection opportunities
Per call, almost no signal in legitimate software because JITs call it constantly. The interesting telemetry is the *combination*: a process that has neither a managed runtime (CLR, V8, JVM, Chakra) nor a browser/Electron content host calling `NtFlushInstructionCache` against a region that was recently `PAGE_EXECUTE_READWRITE` or has no `MEMORY_BASIC_INFORMATION::AllocationBase` mapping to an on-disk image is a textbook reflective-loader IOC. ETW `Microsoft-Windows-Threat-Intelligence` (Defender ETW-TI) emits a `VirtualProtect`-like event for the protection transition that *immediately precedes* the flush in nearly every loader — pair the two.
Direct syscall examples
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);
}MITRE ATT&CK mappings
Last verified: 2026-05-20