Reference

Padding API

The padding framework — AbstractPaddingStrategy, PaddingRegistry, none, nop, and inert.

Padding API

app/padding/ mirrors app/encoders/: a pluggable framework with an abstract base, a singleton registry, and self-registering built-ins. Strategies produce the code-cave filler that executes before a payload.

AbstractPaddingStrategy

app/padding/base.py defines the interface every strategy implements:

  • name (property) — short unique identifier, e.g. "inert"
  • description (property) — one-line human description
  • fill(size, arch, bad_chars) — returns the filler, exactly size bytes long
  • list_params() — returns [{name, type, required, default, description}] for the MCP/CLI discovery surface

base.py also provides _validate_size() and _validate_arch() for subclasses to reuse, so a negative size or an unsupported arch fails the same way everywhere.

PaddingRegistry

app/padding/registry.py is a singleton (app/utils/singleton.py) holding every registered strategy.

MethodDescription
register(cls)Instantiate and register a strategy class; raises TypeError if it is not an AbstractPaddingStrategy subclass and ValueError if the name is taken.
get(name)Return a registered strategy by name; raises KeyError if unknown.
list_all()Return {name, description, params} for every strategy (MCP list_padding_strategies / CLI discovery).
names()Return a sorted list of registered names (used for the CLI --pad-with choices).

Built-ins are auto-registered on import of the app.padding package (app/padding/__init__.py).

none — NonePadding

Returns b"" for every size. The default, so "do not pad" is a declared choice rather than an absence of one, and callers never need to special-case it.

nop — NopPadding

A 0x90 sled: exactly size bytes of 0x90.

  • Raises ValueError when 0x90 is in bad_chars, naming the inert alternative — a NOP sled the caller forbade is a mistake worth surfacing, not a silent fallback.

inert — InertPadding

No-op instructions with no 0x90, assembled from a curated pool:

UnitBytesLength
mov rax, rax48 89 c03
mov r8, r8 … mov r15, r154d 89 c0 …3
push rax / pop rax50 582
mov ax, ax (32-bit pool)66 89 c03

fill() concatenates whole instructions, so no unit is ever split at the boundary. bad_chars is a hard filter: units containing a forbidden byte are discarded, then the size is solved exactly against what survives. An unreachable size raises, naming the strategy and the set.

from app.padding import InertPadding

strat = InertPadding()
filler = strat.fill(64, "64", bad_chars=[0x00])   # 64 bytes, no 0x00
strat.explain(64, "64")                          # the exact units, in order

inert additionally exposes plan(size, arch, bad_chars) — the unit decomposition as (mnemonic, bytes) pairs — and units(arch, bad_chars) for the surviving pool. fill() is a thin wrapper over plan(), so the strategy cannot report one decomposition and emit another.

Writing a custom strategy

Subclass AbstractPaddingStrategy, implement the members, then register it:

from app.padding import PaddingRegistry
from app.padding.base import AbstractPaddingStrategy

class Int3Padding(AbstractPaddingStrategy):
    name = "int3"
    description = "Illustrative placeholder filler."
    def fill(self, size, arch, bad_chars, **params):
        return b"\xcc" * size
    def list_params(self):
        return []

PaddingRegistry().register(Int3Padding)

Once registered it is available via --pad-with int3 (CLI), the MCP pad_shellcode tool, and PaddingRegistry().get("int3").

How it composes with the encoders

Order matters: encode, then pad. Encoders re-compile the payload (they return a decoder stub plus the encoded bytes), so a filler applied before encoding would be discarded. generate.py applies padding after the encoding step for exactly that reason, and ShellSnip.pad() documents the constraint.

ShellSnip.pad() prepends the filler to what output(), print() and write() expose, and analyze()/validate() analyse filler plus payload — the byte string the target actually receives.

Copyright © 2026