Skip to content
pwnsy
exploitationadvanced#shellcode#exploitation#payloads#memory-corruption#binary-exploitation

Shellcode Explained: Payloads, Staging & Why NX Matters

What shellcode is, how staged and stageless payloads differ, and why DEP and ASLR make injection harder. Conceptual, no payloads.

An exploit has two halves. The first finds a bug and uses it to seize control of a program's execution. The second decides what to do with that control. Shellcode is the second half: the compact block of machine code that runs once the hijack succeeds. This guide explains what shellcode is, how it is structured, and why the mitigations of the last two decades made simply injecting and running it so much harder. It stays conceptual throughout. There are no working payloads here, only the ideas behind them.

What the word means

The name is historical. The earliest and most iconic goal of an exploit against a system service was to launch a command shell, giving the attacker interactive control of the machine. The little piece of code that made the system call to spawn that shell was the shellcode.

The name stuck even as the goals broadened. Today shellcode is any small, self-contained machine-code payload that an exploit runs after gaining control. It might open a shell, add a user, connect back to the attacker, load a larger tool into memory, or simply prove that code execution was achieved. What unites all of it is the role: it is the payload the code-execution vulnerability (CWE-94) delivers you to.

Why shellcode is written the way it is

Shellcode runs under conditions ordinary programs never face. A normal program is placed in memory by the operating system loader, which resolves its addresses, links its libraries, and sets up its environment. Shellcode gets none of that. It is copied into some buffer by the exploit and jumped to directly. Two constraints follow.

It must be position-independent. The shellcode cannot assume it sits at any particular address, because it does not control where the vulnerable program copied it. It cannot use hardcoded absolute addresses for its own data or code. Instead it computes locations relative to wherever it happens to be running. Everything it needs must be reachable without knowing the absolute address in advance.

It must be self-contained. There is no loader to link it against libraries, so the shellcode has to reach the functionality it needs on its own, typically by locating system call interfaces or library functions already present in the process at runtime. It carries or discovers everything it depends on.

There is often a third, more tactical constraint. Shellcode frequently travels through the vulnerable program as if it were ordinary data, passed through string-copy routines that stop at a null byte or transformations that mangle certain characters. So shellcode is often written to avoid those bytes, encoding itself so that the raw payload contains only values that survive the trip and then unpacking itself in memory before running. This is a delivery concern, not a capability one.

These constraints together explain why shellcode looks nothing like ordinary compiled output. A normal program can be large, can reference absolute addresses freely, and can contain any byte value, because the loader and the linker smooth all of that over. Shellcode gives up those luxuries in exchange for being able to run in a hostile, half-built environment with no support. The discipline that produces it, compact, relocatable, dependency-free, and byte-clean, is the same discipline that makes a small change to the target environment enough to break a payload that worked a moment before.

Shellcode is one stage of an exploit

It helps to keep the layers separate. The vulnerability is the flaw. The exploit is the technique that turns the flaw into control of execution. The shellcode is the payload that runs once control is taken. A single shellcode can be reused across many different exploits, and a single vulnerability can deliver many different payloads. Mitigations like DEP and ASLR mostly attack the handoff between exploit and shellcode, which is why that seam matters so much.

Staged versus stageless payloads

Payloads come in two structural styles, and the difference shapes how an intrusion looks on the wire and in memory.

A stageless payload is a single self-contained blob. Everything the attacker wants to run is present in that one piece of code. It is simple and reliable, because once it lands there are no further dependencies. The cost is size and exposure: the whole payload has to fit through the vulnerability in one shot, and the full capability sits in the delivered bytes where defenders can inspect it.

A staged payload splits the work. The exploit first delivers a very small piece of code, the stager, whose only job is to establish a channel, usually a network connection back to the attacker, and pull down the larger real payload (the stage) into memory. The stage then runs.

PropertyStagelessStaged
StructureOne self-contained blobSmall stager plus a larger downloaded stage
Initial sizeLarger, must fit through the bug at onceTiny, easier to fit through a small overflow
Network dependencyNone after deliveryRequires a callback channel to fetch the stage
Detection surfaceWhole capability visible in delivered bytesInitial delivery is small; stage often stays in memory only
ReliabilityHigh once deliveredDepends on the network channel holding

The trade is straightforward. Staging buys a smaller initial footprint and flexibility, at the cost of a network dependency and more moving parts. Stageless buys simplicity and reliability, at the cost of size and a larger visible payload. Which one an attacker chooses depends on how much room the vulnerability gives them and how much they want to keep off disk.

Why NX and ASLR complicate all of it

Everything above describes the payload. The hard part on a modern system is getting the CPU to run it at all, and that is where the mitigations bite.

DEP and NX enforce that data pages are not executable. Shellcode injected into a buffer on the stack or heap sits on exactly such a page. When the exploit redirects execution into it, the CPU faults instead of running it. The payload is intact and dead. To revive it, the attacker has to first make its memory executable, and the standard way to do that under DEP is a return-oriented programming chain that calls the system function to change page permissions, then jumps into the now-executable shellcode. In other words, DEP inserts a whole extra technique between the exploit and the shellcode. Our ROP guide covers that step in detail.

ASLR attacks the other requirement. To jump into the shellcode, the exploit needs the shellcode's address, and to run a ROP chain it needs gadget addresses. ASLR randomizes those every run. So the modern exploit typically needs a separate information-leak bug to disclose one real address, from which the rest are computed. The interplay of DEP and ASLR, and why they force a two-bug chain, is the subject of our ASLR and DEP guide.

The result is that the romantic image of shellcode, a buffer full of bytes that the CPU simply runs, describes a world that DEP and ASLR ended. On current systems the payload is often the easy part. The work is in the exploit steps that clear a path to it.

Heap spraying enters here

When the landing address is unpredictable, one attacker response is to make the payload land somewhere predictable by flooding memory with copies of it, a technique called heap spraying. It raises the odds that a semi-controlled jump lands inside attacker data. Spraying and shellcode delivery are closely linked in browser and document exploits. See our heap spraying guide for how that works and how it is countered.

A conceptual walkthrough of a modern payload path

To see why the payload has become the easy part, follow the abstract shape of an exploit against a hardened target. This stays at the level of ideas, with no working steps, because the point is the sequence of obstacles rather than any recipe.

  1. A memory-corruption bug gives partial control. Some flaw, a buffer overflow or an out-of-bounds write, lets the attacker overwrite memory they should not reach, including a value the CPU will later use to decide where to execute next.
  2. ASLR hides the map. Before anything can be pointed at, the attacker needs a real address. Randomization means the shellcode's landing spot, the library functions, and the code fragments a reuse chain would need are all at unknown locations. So the first practical requirement is an address.
  3. An information leak supplies one address. A second flaw, often an out-of-bounds read or a pointer printed somewhere it should not be, discloses one genuine runtime address. Because each module is randomized as a block, that single leak reveals the module's base, and from the base every internal offset becomes computable again.
  4. DEP blocks the direct route. With addresses in hand, the attacker would like to jump straight into a buffer full of shellcode. DEP refuses to execute data pages, so that buffer is inert. The direct path is closed.
  5. A code-reuse chain reopens execution. To get around DEP, the attacker assembles a return-oriented programming chain from fragments of existing executable code, using the addresses recovered in step three. The chain's job is narrow: call the system function that changes memory permissions, marking the region holding the shellcode as executable.
  6. The shellcode finally runs. Once its page is executable, control transfers into the shellcode, and only now does the payload described earlier in this guide actually execute.

The striking thing about the sequence is how much work happens before the payload runs. Steps two through five exist entirely to defeat mitigations. The shellcode itself, the part that opens a shell or connects back, is often the least difficult component to write. This is the concrete meaning of the claim that on modern systems the payload is the easy part and the path to it is the hard part.

Detection signals

Because shellcode can be encoded to slip past byte-pattern signatures, the durable detection opportunities are in behavior and memory state rather than in the payload bytes.

  • Memory that turns executable at runtime. A data region on the stack or heap being remarked as executable is exactly the step a DEP bypass performs. Instrumentation that flags a data page gaining execute permission catches the moment the payload is being revived.
  • Unexpected child processes from a network-facing program. A browser, document reader, or server suddenly spawning a command interpreter is a classic sign that shellcode reached its goal of launching a shell. This maps to the client-execution technique tracked as T1203.
  • New outbound connections from a process that should not make them. A staged payload's stager, or a reverse connection from stageless shellcode, shows up as a callback to an unfamiliar host from a process with no reason to open one.
  • Execution flowing through unusual code sequences. Return-oriented chains produce a distinctive pattern of many short returns into the middle of existing functions, which control-flow monitoring and hardware branch-tracking features can surface.
  • Signs of heap grooming. Large volumes of near-identical allocations, the fingerprint of heap spraying, can precede a payload that needed a predictable landing area.

The unifying idea is to watch the seams the payload cannot avoid. It has to become executable, it has to act once it runs, and those actions are observable even when the bytes are obfuscated.

How shellcode relates to nearby concepts

Shellcode is often confused with the other pieces of an intrusion because they appear together. Separating them clarifies what shellcode actually is.

ConceptWhat it isRelationship to shellcode
VulnerabilityThe underlying flaw, such as a buffer overflowThe precondition; shellcode never runs without a bug first handing over control
ExploitThe technique that turns the flaw into control of executionDelivers and triggers the shellcode; one exploit can carry many payloads
ShellcodeThe compact payload that runs after control is seizedThe stage this guide describes
ROP chainA sequence of existing-code fragments used to bypass DEPRuns before the shellcode, to make its memory executable
MalwareA full, self-sufficient malicious programOften what a stager pulls in after the shellcode establishes a foothold

Reading the table top to bottom traces the order of an intrusion: a vulnerability exists, an exploit weaponizes it, a reuse chain clears the mitigations, the shellcode runs, and larger tooling may follow. Shellcode is one link in that chain, and keeping it distinct from the links around it is what makes the mitigations legible.

Common misconceptions

Shellcode collects a set of outdated mental models, most of them left over from the era before DEP and ASLR.

  • "Shellcode is a virus or malware." It is one stage of an exploit, the payload that runs after control is seized. It is smaller and more specialized than a full malware program, and its job is often just to establish a foothold that pulls in the larger tooling.
  • "Injecting shellcode into a buffer is enough to run it." On any modern system, DEP means bytes sitting in a data buffer will not execute. Getting them to run requires extra work, typically a code-reuse chain to change memory permissions first.
  • "Shellcode must literally launch a shell." The name is historical. Today it may add a user, connect back, load a larger payload, or simply prove code execution. Launching a shell is one option among many.
  • "Bigger and more capable shellcode is always better." Size is a liability. It has to fit through the vulnerability, and a larger payload is more visible to inspection. This constraint is exactly why staged payloads exist, trading a tiny initial footprint for a network fetch.
  • "Signature detection stops shellcode." Payloads are routinely encoded to avoid known byte patterns and to survive string-handling routines, so signatures on the raw bytes are brittle. Behavior-based detection is the more durable approach.

How it evolved

Shellcode techniques and the defenses against them have chased each other for decades, and the arms race explains why payloads look the way they do now.

In the earliest period, memory was both writable and executable, so an exploit could place machine code in a buffer and jump straight into it. Payloads were written to be position-independent and to avoid bytes like the null terminator that string routines would truncate, but otherwise the model was simple: put code in memory, run it.

The introduction of non-executable data pages, DEP and the hardware NX bit, ended that simplicity. Injected bytes on the stack or heap would no longer run. Attackers responded with code reuse: first return-to-libc, calling existing library functions, then the more general return-oriented programming, stitching together short fragments of trusted code. The payload did not go away, but a whole new stage appeared in front of it whose only purpose was to make execution possible again.

Address randomization then removed the fixed addresses that code reuse depends on, which pushed attackers toward pairing every exploit with an information-leak bug to recover one real address. Staged payloads grew more attractive in this environment, since keeping most of the capability off disk and in memory reduced the footprint defenders could inspect. Later layers such as control-flow integrity took aim at the return-oriented step itself. Each defensive move did not eliminate shellcode, it added another obstacle in front of it, which is why a modern payload path is the multi-stage sequence described above rather than a single jump into a buffer.

How to defend

Defending against shellcode means attacking the seams it depends on, not the bytes themselves, since the bytes can be encoded to evade signatures.

  1. Keep DEP and NX fully enabled. This is the single control that stops injected shellcode from running on data pages. Ensure no compatibility opt-out re-enables execution on the stack or heap.
  2. Maximize and complete ASLR. Full position-independence and high entropy deny the attacker the addresses the payload and its ROP setup need. Treat information-leak bugs as high severity, because one of them undoes this.
  3. Add control-flow integrity. CFG on Windows and Intel CET break the ROP step that DEP forces attackers into, cutting the path between exploit and shellcode.
  4. Prevent the underlying bug. Shellcode only ever runs after a memory-corruption or code-injection flaw. Memory-safe languages, bounds checking, and input validation remove the entry point.
  5. Watch for the behavior, not the bytes. Because payloads are encoded and often memory-only, focus detection on what shellcode does after it runs: unexpected child processes, new network callbacks, and memory regions that turn executable at runtime.

For a broader picture of how payload delivery and its mitigations show up across real exploit chains, our Exploit Intelligence dashboard tracks the techniques attackers actually rely on.

Frequently asked questions

Is shellcode always written in assembly? It is usually written or finished in assembly, or generated as raw machine code, because it has to be compact, position-independent, and free of bytes that would break delivery. Higher-level code exists to help produce it, but the payload that lands in memory is machine code with tight constraints that ordinary compiled output does not respect.

Why does shellcode avoid null bytes? Many vulnerabilities deliver the payload through string-handling routines that treat a null byte as the end of the string. A null byte in the middle of the payload would truncate it. Avoiding null bytes, and often other characters a given path mangles, is a delivery concern, so shellcode is frequently encoded to contain only bytes that survive the trip and then unpacks itself in memory.

What is the difference between a stager and a stage? The stager is the small first piece a staged payload delivers. Its only job is to open a channel, usually a connection back to the attacker, and pull down the larger real payload, which is the stage. The split lets the initial delivery fit through a small vulnerability while the full capability arrives afterward, often staying in memory only.

Does DEP make shellcode useless? No, it makes direct injection useless. DEP stops bytes on a data page from executing, so the attacker adds a step, typically a return-oriented programming chain, to mark the payload's memory executable first. DEP raised the cost and changed the technique. The payload still runs once the extra step clears the way.

Why do modern exploits need two bugs? One bug corrupts memory to seize control, and a second, an information leak, defeats address randomization by disclosing a real runtime address. Without the leak, the attacker cannot compute where the shellcode or the reuse-chain fragments live. ASLR turns exploitation into a two-bug problem, which is why leak vulnerabilities are treated as high severity even when they seem to only reveal an address.

Can memory-safe languages eliminate shellcode? They remove the memory-corruption entry point that shellcode depends on. Shellcode only ever runs after a bug hands over control of execution, and languages that prevent out-of-bounds writes and use-after-free bugs close that door for the code written in them. Native dependencies and unsafe blocks can still reintroduce the risk, so the guarantee holds only as far as the memory safety does.

How is shellcode detected if the bytes keep changing? By watching behavior rather than bytes. Encoding changes the payload's appearance but not what it does. Memory that turns executable at runtime, a network-facing process spawning a shell, unexpected outbound connections, and the distinctive control flow of a reuse chain are all observable regardless of how the payload is encoded.

Sources & further reading

Sharetwitterlinkedin

Related guides