Guides

Code-Cave Padding

Prepending an inert filler for the wider buffer a payload is dropped into.

Code-Cave Padding

A payload is rarely executed from a buffer that is exactly its own size. It is dropped into a code cave — a gap between sections, a fixed-size slot, unused alignment space — and everything before the payload's first byte executes first.

ShellSNIP can generate those leading bytes. They must be inert: the payload has to behave identically whether the filler is there or not.

Using it

# 64 bytes of inert filler in front of the payload
./generate.py --os posix -o padded.raw --pad-size 64 --pad-with inert exec "id"

# The classic NOP sled, as a control
./generate.py --os posix -o sledded.raw --pad-size 64 --pad-with nop exec "id"
[+] Padding with 'inert' strategy (64 byte(s))...
[+] Padding applied: 64 byte(s) of 'inert' filler before a 81 byte payload

Padding is opt-in. Without --pad-size, output is byte-for-byte unchanged — existing invocations are unaffected.

The step runs after encoding and before validation, so the filler sits outside the decoder stub, and the bad-character report covers the whole written artifact:

create  ->  compile  ->  encode (optional)  ->  pad (optional)  ->  validate  ->  write

Strategies

StrategyFillerNotes
nonenothingThe default. "Do not pad" is a declared choice, not an absence of one.
nop0x90 repeatedRejected loudly when 0x90 is in bad_chars.
inertno-op instructions, never 0x90Whole instructions only, exact size, bad-char filtered.

What inert guarantees

  • Whole instructions only. The filler is a concatenation of complete instructions, so no partial instruction is left at the boundary. Any whole-unit prefix is itself valid filler.
  • Never 0x90. The pool is curated 0x90-free by construction — nothing is filtered afterwards, which could break a unit boundary. This matters because a NOP sled is the single most recognisable padding signature there is.
  • bad_chars is a hard constraint. Units containing a forbidden byte are discarded, then the exact size is solved against what remains. A size the surviving units cannot reach is refused, naming the strategy and the offending set — never a short filler, never a forbidden byte.
  • Deterministic. The same (size, arch, bad_chars) always yields byte-identical filler, so builds are reproducible. Instructions are emitted in rotation rather than repeating one pattern, which would be as recognisable as the sled this strategy exists to avoid.

The pool is assembled from register self-moves (mov rax, rax, mov r8, r8, …) and paired push/pop (push rax/pop rax), in 2- and 3-byte encodings.

Two facts worth knowing:

  • mov eax, eax is not inert in 64-bit. It encodes to the same 89 c0 as in 32-bit mode, but writing a 32-bit register zeroes the upper half of the 64-bit one. It appears only in the 32-bit pool.
  • xchg rax, rax is 48 90 — it contains 0x90, so it cannot be in the pool at all.

Sizes that are refused

The shortest inert instruction is two bytes: the only one-byte no-op is 0x90 itself, which inert must never emit. So --pad-size 1 --pad-with inert has no whole-instruction decomposition and is refused by design, rather than filled with a 0x90.

Zero is not an error: --pad-size 0 produces no filler.

Python

from app.shellSnip import ShellSnip

bot = ShellSnip(os_="posix", arch="64")
bot.create("exec", {"cmd": "exit 7", "visible": False})
bot.compile()
bot.pad(64, "inert", bad_chars=r"\x00")   # after compile(); after encode() if encoding
bot.validate(r"\x00")                     # analyses filler + payload
bot.write("padded.raw")                   # filler, then payload

ShellSnip.pad() returns the filler it applied. The padded bytes are what output(), print() and write() expose. Calling pad() before compile(), or padding twice, raises SystemError — neither silently no-ops.

MCP

list_padding_strategies()
pad_shellcode(input_data="31c0b03c0f05", pad_size=64, pad_with="inert")

pad_shellcode accepts hex, base64 or ASM input. An unknown strategy, a negative size, an unreachable size, or nop with 0x90 forbidden all return a readable error rather than a bad payload.

How inertness is proven

The structural tests assert the filler's shape: exact size, the 0x90-free invariant, the bad-char filter, and — through the disassembler — that the bytes really are whole no-op instructions rather than matching a hard-coded pattern.

Shape is not behaviour, though. The opt-in execution suite runs a payload behind a 512-byte inert filler through the Linux loader and asserts the exit status is unchanged, including behind an XOR-encoded payload. To show those assertions can fail, one test runs a deliberately non-inert filler (pop rsp, 1 byte, repeated): it must change the result. A test that cannot fail proves nothing.

SHELLSNIP_RUN_EXECUTION=1 PYTHONPATH=$PWD poetry run pytest tests/e2e -v

See Bad Characters for the notation bad_chars accepts, and Encoding for how padding composes with the encoders.

Copyright © 2026