Metasploit Deep Dive Part 3: Payload Architecture and What msfvenom Actually Builds

“Generate me a payload” sounds like one operation. It’s actually a small assembly pipeline, and msfvenom is the front end for it. Reading modules/payloads/ and the msfvenom script itself in the real framework clone shows three distinct payload shapes, not one, and understanding the difference explains a lot about why some payloads are 200 bytes and others are 200 kilobytes.

Singles, stagers, and stages

The real clone’s modules/payloads/ directory has four top-level folders: singles, stagers, stages, and adapters.

A single is self-contained. windows/x64/exec or linux/x64/shell_reverse_tcp do everything in one blob: open the connection, spawn the shell or run the command, done. No second download, no handshake beyond the initial connect. Singles are simple and reliable, but every capability has to fit in whatever space the exploit’s vulnerability allows for shellcode, which for a lot of memory corruption bugs is small.

A stager is deliberately minimal: its only job is to establish a connection back to the attacker and pull down something bigger. windows/x64/meterpreter/reverse_tcp is the classic example, a stager just big enough to open a TCP socket, read a length-prefixed blob from it, and jump execution into that blob. The stager itself never contains meterpreter. It’s a bootloader.

A stage is what the stager downloads: the actual meterpreter DLL, in meterpreter’s case, loaded reflectively into memory without ever touching disk. Stages can be large, because by the time they’re transferring, the constrained-shellcode-space problem the exploit imposed is already solved; the stager did its job and got out of the way.

adapters is a smaller, newer category: wrappers that adapt a payload’s output format for a specific delivery mechanism (Python, PowerShell, and similar) without changing the payload logic itself.

The split exists because exploit space is scarce and payload capability isn’t free. A buffer overflow with 400 bytes of usable shellcode space can carry a single, or a stager, but never the full meterpreter stage directly. Splitting stager from stage is what lets a 400-byte primitive turn into a full interactive session.

What msfvenom actually is

msfvenom isn’t a separate tool bolted onto the framework. It’s a thin Ruby script (msfvenom in the repo root) that loads the framework’s own libraries, require 'rex', require 'msf/core/payload_generator', and drives Msf::PayloadGenerator, the same class the interactive msfconsole uses internally when you run generate inside a payload module. msfvenom exists so you don’t have to open msfconsole, select a payload module, set options, and run generate by hand every time; it collapses that into one command-line invocation with the same underlying engine.

PayloadGenerator#generate_payload and #generate_raw_payload (in lib/msf/core/payload_generator.rb) do the real work: resolve the requested payload path, apply datastore options (LHOST, LPORT, and payload-specific ones), optionally run it through one or more encoders, optionally wrap it in a specific output format (exe, elf, python, raw, and dozens more via msfvenom -f), and hand back bytes. Everything msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=... -f exe does is that same pipeline, just invoked from the command line instead of msfconsole.

Encoders, real list

The modules/encoders/ directory groups by target architecture: x86, x64, mipsbe, mipsle, ppc, sparc, riscv32le, riscv64le, plus format-specific ones for cmd, php, and ruby payloads. Inside x86 alone, 22 distinct encoders exist, from simple ones like alpha_mixed (restricts output to alphanumeric bytes, useful when the injection point only accepts printable characters) to shikata_ga_nai, described in its own source as:

“This encoder implements a polymorphic XOR additive feedback encoder. The decoder stub is generated based on dynamic instruction substitution and dynamic block ordering. Registers are also selected dynamically.”

That’s the real docstring, and it’s an accurate description of what it does: XOR the payload against a rolling key, then generate a decoder stub whose instruction order and register choices vary on every run, so the same payload never produces the exact same bytes twice. What it does not do, and this is the subject of Part 9, is reliably beat modern detection anymore. It changes the byte signature. It does very little about the behavioral pattern an EDR is actually watching for once the decoder runs and the payload executes in memory.

A 2026 architecture change worth flagging here

Metasploit 6.5, the release this series is working against, made a change specifically to the payload-generation layer that belongs in this post rather than Part 9: the block API used by nearly all of Metasploit’s 32-bit and 64-bit shellcode, the piece that lets shellcode call Windows API functions by a 32-bit hash of the function and module name instead of a hardcoded address, now has its hashes randomized on every generation, not just its instruction order. Previously, the block API’s instructions were already shuffled per Rapid7’s own account (a change made over five years ago), but the 32-bit API hashes themselves stayed static and became signature fodder in their own right. As of 6.5, both the instructions and the hashes are regenerated each time a payload is built. This is architecture, not evasion tuning: it changes what msfvenom outputs at the bytecode level for essentially every Windows shellcode payload in the framework, whether or not an encoder is layered on top.

Why staged vs. stageless still matters in 2026

The stager/stage split isn’t just an artifact of exploit-space constraints. It’s also a network behavior choice, covered properly in Part 5, but worth flagging here: a stageless payload (windows/x64/meterpreter_reverse_tcp, no slash between meterpreter and the transport) bundles everything into one blob and sends it in a single connection. A staged payload (windows/x64/meterpreter/reverse_tcp, slash present) makes two: a tiny initial connection, then a second transfer for the stage. That second transfer is exactly the kind of fixed-size, fixed-pattern network event that shows up in Part 10’s detection discussion. The architectural decision made here, in payload generation, has direct downstream consequences for what’s visible on the wire.

Part 4 follows that stage download all the way to where it lands: meterpreter’s own TLV wire protocol, read from the real packet-parsing source rather than from a packet capture, since this part of the series doesn’t yet have a live target to capture against.