A 24-byte execve("/bin/sh") stub is easy until you notice how it reaches
memory. Through strcpy, a single null byte truncates it. Through scanf("%s"),
whitespace cuts it. Through an isalnum() filter, almost every opcode is rejected.
The shellcode has to survive the channel, which means choosing each instruction for
the bytes it encodes to, not only what it does.
- arch
- amd64-64-little
- channels
- strcpy (no 0x00) / scanf %s (no whitespace) / isalnum() (only [A-Za-z0-9])
- NX
- off (classic shellcode target)
null-free: pick instructions that don't encode 0x00#
The usual offenders are zero immediates and certain register encodings. Replace them with self-zeroing and small-immediate forms:
|
The recurring tricks: xor reg,reg instead of mov reg,0; push 0x3b; pop rax
instead of mov rax,0x3b; and /bin//sh so the eight string bytes contain no
terminator of their own. Verify before you ship it:
alphanumeric: a decoder you can spell#
When the filter is isalnum(), only a sliver of x86 opcodes are even legal —
some push/pops, xor, imul, inc/dec, the [A-Za-z0-9] range. You can't
write syscall (0f 05) directly in that alphabet. The standard answer is a
self-decoding stub: a tiny loader written entirely in legal bytes that
reconstructs your real shellcode in memory and jumps to it.
- a legal prologue finds its own address (the alphanumeric equivalent of a
get-eip), - then a legal arithmetic loop writes the forbidden bytes (
0f 05, etc.) into a buffer just ahead of it by adding/subtracting alphanumeric constants, - then control falls into the decoded payload.
Hand-rolling that is a puzzle; in practice you generate it and inspect it:
the payload the filter let through
Pure [A-Za-z0-9] going in, a shell coming out — the decoder rebuilt the
syscall the filter would never have passed:
segfault{i_spelled_a_shell_in_letters}
the mindset#
- Know your alphabet before you write a byte. The channel (
strcpy,%s,isalnum, sometimes "printable ASCII only") defines the legal set; design to it. - Encoding ≠ effect. Two instructions with the same effect can have wildly different bytes; shellcoding is choosing the encoding that fits the channel.
- Decoders buy you everything. If the legal alphabet can express a loop and a write, it can express a decoder, and a decoder can reconstruct any payload. That's why even a brutal filter rarely means "no shellcode."
NX makes all of this moot on modern targets — you ROP instead. But on the boxes where shellcode still runs (kernels, embedded, JIT pages, CTF), getting bytes past a hostile parser is its own small discipline, and it's a satisfying one.