Padding API
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 descriptionfill(size, arch, bad_chars)— returns the filler, exactlysizebytes longlist_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.
| Method | Description |
|---|---|
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
ValueErrorwhen0x90is inbad_chars, naming theinertalternative — 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:
| Unit | Bytes | Length |
|---|---|---|
mov rax, rax | 48 89 c0 | 3 |
mov r8, r8 … mov r15, r15 | 4d 89 c0 … | 3 |
push rax / pop rax | 50 58 | 2 |
mov ax, ax (32-bit pool) | 66 89 c0 | 3 |
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.