Code Injection Attacks

A web server parses a malformed HTTP header. Moments later, an attacker has a root shell. No password was cracked, no credential stolen. The program simply executed data it should never have trusted. This is the essence of code injection: an attacker smuggles executable instructions into a process's memory, then hijacks control flow to run them. It converts a memory-safety bug, often a humble stack buffer overflow, into full arbitrary code execution. Code injection attacks matter because they collapse the distinction between "data the program reads" and "code the program runs." Real systems — operating system kernels, browsers, network daemons, embedded firmware — are written largely in memory-unsafe languages like C and C++. In these languages, a program has no built-in way to distinguish a byte that is data from a byte that is an instruction. If an attacker can place bytes anywhere in memory, and later redirect the instruction pointer to that location, the CPU will happily decode and execute those bytes as machine code. This happens regardless of the programmer's intent. The big picture is a two-stage attack: (1) injection — get attacker-controlled bytes (the "shellcode") into the address space of the victim process, typically via an overflow that writes past the bounds of a buffer, and (2) redirection — corrupt a code pointer (a return address, a function pointer, a saved instruction pointer) so that control flow jumps to the injected bytes. Historically these two steps happened in the same buffer: the classic "smash the stack and jump into it" attack. Understanding this mechanism precisely is the prerequisite for understanding why modern defenses exist at all. This article gives a rigorous, step-by-step account of the classical stack-based code injection attack. It covers how shellcode is constructed and placed, and how the return address is overwritten to point at it. It also explains why the attack succeeds with a given probability, and where it fails. It closes by foreshadowing the defenses — ASLR, DEP, and Stack Canaries — that were built specifically to break each link in this chain. It also introduces the more advanced technique, Return-Oriented Programming, that attackers developed in response.

Key takeaways
  • Code injection places attacker bytes (shellcode) into a process's memory and then redirects execution to them.
  • The classic vector is a stack buffer overflow that overwrites a saved return address.
  • Shellcode is small, position-aware machine code, often placed in a NOP sled to tolerate address guessing.
  • Success requires (a) an executable memory region and (b) a return address the attacker can predict or brute-force.
  • Modern defenses — DEP/W^X, ASLR, and stack canaries — each block one specific step of this pipeline.
  • When direct code injection is blocked, attackers pivot to code-reuse attacks like Return-Oriented Programming.
The core insight

Code injection turns a memory-corruption bug into arbitrary code execution by writing executable attacker data into memory and then hijacking a code pointer to jump to it.

The idea

A code injection attack exploits a program's failure to validate the size or content of an input, using that failure to place attacker-chosen machine code (shellcode) into the process's address space, and then diverting the CPU's control flow (via a corrupted return address, function pointer, or saved instruction pointer) to execute it. It is distinct from code-reuse attacks — such as Return-Oriented Programming — in one crucial respect: in classical injection, the attacker's own bytes are the executed instructions; nothing pre-existing in the binary is reused. The prerequisite vulnerability is almost always a memory-safety violation. The paradigm case is the stack buffer overflow: a fixed-size local buffer is written using an unchecked-length operation (`strcpy`, `gets`, `sprintf` without a width specifier, manual loops without bounds checks), and the write proceeds past the buffer's allocated end into adjacent stack memory — most dangerously, into the saved return address that tells the CPU where to resume execution when the current function returns. Two conditions must jointly hold for classical code injection to succeed. First, the region into which the shellcode is written must be executable at the time of the jump — historically true of the stack and heap on most systems, before DEP/W^X (Data Execution Prevention / "Write XOR Execute") policies made writable memory non-executable by default. Second, the attacker must know, or be able to guess with workable probability, the absolute address of the injected shellcode, since the corrupted return address must point precisely there — a requirement that ASLR (Address Space Layout Randomization) was designed to defeat by randomizing that address on every run.

How it works

The attack proceeds through a fixed sequence of stages, each of which the defender can, in principle, individually block.

Stage 1 — Locate the vulnerability. The attacker finds a code path where attacker-controlled input is copied into a fixed-size stack buffer without a length check, and determines the exact offset (in bytes) from the start of the buffer to the saved return address on the stack.

Stage 2 — Construct the shellcode. The attacker writes a short sequence of machine instructions that perform the desired action — spawn a shell, open a reverse network connection, add a user — using position-independent code with no embedded null bytes if the overflow is triggered through a C-string function (since `\x00` would terminate the copy).

Stage 3 — Build the padded payload. Because the exact stack address where the shellcode will land is hard to predict precisely, the payload is typically: ``` [ NOP sled ][ shellcode ][ repeated guessed return address ] ``` A NOP sled is a long run of no-op instructions (`0x90` on x86). If the corrupted return address lands anywhere inside the sled, execution slides forward harmlessly until it reaches the shellcode — turning an exact-address guess into a much easier "land somewhere in this wide window" guess.

Stage 4 — Overwrite the return address. The overflow writes past the buffer boundary, through saved registers (e.g., a saved base pointer), and finally overwrites the return address slot with the attacker's guessed address — a pointer into the sled/shellcode region.

Stage 5 — Redirect control flow. When the vulnerable function executes its `return` (or `ret`) instruction, the CPU pops the corrupted value off the stack into the instruction pointer and jumps there — into the NOP sled, and from there into the shellcode.

Stage 6 — Shellcode executes. Now running with the privileges of the victim process, the shellcode performs its payload — classically an `execve("/bin/sh", ...)` syscall sequence, granting the attacker a shell. ``` ALGORITHM ClassicStackInjection(vulnerable_function, target_process) // Attacker-side construction Input: buf_offset // bytes from buffer start to saved return address shellcode // machine code bytes, no embedded 0x00 if via strcpy-like copy guessed_addr // predicted address inside the NOP sled sled_len // length of NOP sled, chosen to widen the address guess window 1. sled <- REPEAT(0x90, sled_len) // NOP sled 2. payload <- sled || shellcode 3. pad_len <- buf_offset - LENGTH(payload) if pad_len < 0: payload <- TRUNCATE_OR_RESIZE(payload, buf_offset) // must fit before ret addr pad_len <- 0 4. filler <- REPEAT(0x41, pad_len) // arbitrary padding to reach saved regs 5. ret_bytes <- ENCODE_ADDRESS(guessed_addr) // little-endian address bytes 6. exploit <- payload || filler || ret_bytes 7. SEND(target_process, exploit) // e.g. as a network request or argv // Victim-side execution (inside the vulnerable function) 8. vulnerable_function receives `exploit` as input 9. UNCHECKED_COPY(local_buffer, exploit) // e.g. strcpy — overflow occurs here 10. // stack memory beyond local_buffer, including saved // frame pointer and return address, is now overwritten 11. RETURN // pops corrupted return address into IP 12. CPU jumps to guessed_addr -> lands in NOP sled 13. execution slides forward through sled -> reaches shellcode 14. shellcode executes with victim's privileges ```

Figure
Anatomy of a stack-smashing code injection
Stack frame before and after overflow BEFORE (legit call) local_buffer[64] (attacker input) saved frame pointer saved return address caller's stack frame... AFTER (overflow) NOP sled 0x90 0x90 ... + shellcode 0x41414141 (filler overwrite) return addr -> points INTO sled caller's stack frame... Execution path on RET 1. RET pops corrupted address into instruction pointer 2. CPU jumps to address -> lands inside NOP sled (wide target) 3. Instruction stream executes NOPs, "sliding" forward with no effect 4. Control reaches shellcode entry point exactly 5. Shellcode executes with victim process privileges (e.g. execve("/bin/sh") -> attacker-controlled shell) Green = attacker-written bytes. Red = hijacked control-flow pointer. Yellow/gray = original, untouched stack contents (before overflow).

Complexity & correctness

Time complexity (attack construction). Building the exploit payload is O(n)O(n) in the size of the buffer being overflowed, where nn is the number of bytes written: constructing the sled, shellcode, filler, and return address is a linear scan and concatenation. Finding the correct buffer offset (`buf_offset`) is typically a one-time, small-constant search — done offline via disassembly, source inspection, or a handful of probing test inputs with an incrementing cyclic pattern (e.g., the "De Bruijn pattern" technique), so it does not scale with input size in the online attack.

Space complexity. The payload occupies O(n)O(n) bytes, bounded above by the space available in the vulnerable buffer plus whatever adjacent stack memory the overflow is permitted to reach before hitting a guard page or unrelated memory that would crash the process before the return executes.

Correctness argument (why the attack necessarily works absent defenses). The argument rests on an invariant of the calling convention on architectures like x86: at the moment a function executes its return instruction, it unconditionally trusts the memory word at a fixed stack offset to be the address of the next instruction to execute — there is no CPU-level verification that this word was not modified during the function's execution. Formally, let RR denote the return-address memory cell and let WW be the sequence of writes issued by the (buggy) copy operation. If the vulnerable function contains no bounds check, then WW is determined entirely by attacker input length, and the attacker can choose ∣W∣|W| such that R∈range(W)R \in \text{range}(W). Since the architecture's `ret` semantics are simply "jump to M[R]M[R]" with no authentication of M[R]M[R]'s provenance, whichever value the attacker last wrote to RR becomes the next instruction pointer. This is a direct consequence of the stack being a single, undifferentiated linear memory region shared between control data (return addresses) and program data (buffers) — the root architectural fact that all defenses in this space (canaries, ASLR, DEP) attack from different angles.

Probability of success (address guessing). If the attacker cannot read the target's memory layout and must guess the shellcode's address blindly, the effective success probability per attempt is approximately

p≈L2bp \approx \frac{L}{2^b}

where LL is the width of the NOP sled (the tolerance window) and 2b2^b is the size of the address space region the guess must land in (bb effective bits of entropy in the target address, e.g. from stack randomization). A larger sled linearly increases pp; this is precisely why NOP sleds exist — they trade payload space for probability of a correct landing, converting an exact point-guess into an interval-guess. Without any address randomization (b≈0b \approx 0 in practice, since stack layout is deterministic on non-ASLR systems), p→1p \to 1: the attack is deterministic. This single equation is also why ASLR works as a defense — it inflates bb, driving pp toward zero exponentially.

Honesty about tradeoffs. Classical injection is fragile in ways an attacker must engineer around: null-byte-terminated copy functions restrict which byte values shellcode may contain; stack alignment and canary values (where present) must be preserved or recomputed; and modern non-executable stacks (DEP/W^X) make the entire technique fail outright regardless of address prediction, forcing the pivot to code-reuse techniques such as Return-Oriented Programming that never write new executable code at all.

Worked example

Consider a vulnerable C function on a 32-bit x86 Linux system with no protections enabled (no ASLR, no DEP, no canary — the historical default in the 1990s): ```c void handle_request(char *input) { char buf[64]; strcpy(buf, input); // no length check } ``` The stack layout, low to high addresses, places `buf` first, then the saved frame pointer (4 bytes), then the saved return address (4 bytes).

StepAttacker actionResulting stack state
1Probe with cyclic pattern of length 80Crash occurs; return address = bytes at offset 72–75, i.e. `buf_offset = 72`
2Craft 32-byte shellcode (`execve("/bin/sh")`) with no `\x00` bytesShellcode ready
3Build 40-byte NOP sled (`0x90 × 40`)`sled + shellcode` = 72 bytes total
4Fill remaining 0 bytes to reach offset 72`pad_len = 72 − 72 = 0`
5Guess return address `0xbffff7d0` (stack addresses were near-static pre-ASLR)4 bytes appended, little-endian: `d0 f7 ff bf`
6Send 76-byte payload as `input``strcpy` overflows `buf`, overwrites frame pointer region with sled tail, overwrites return address with `0xbffff7d0`
7`handle_request` executes `ret`CPU jumps to `0xbffff7d0`, which lands inside the 40-byte NOP sled
8CPU decodes NOPsInstruction pointer slides forward ~20 bytes on average until reaching shellcode start
9Shellcode executes`execve("/bin/sh", NULL, NULL)` runs; attacker gets a shell with the process's privileges

The key numerical point: with a 40-byte sled and a stack region that varies by, say, only a few hundred bytes across runs on a non-ASLR system, the guess succeeds essentially every time — matching the p≈L/2bp \approx L/2^b model with bb effectively near zero. Enable ASLR (randomizing the stack base by roughly 20+ bits on modern 64-bit Linux) and the same sled gives p≈40/220≈0.00004p \approx 40 / 2^{20} \approx 0.00004 per attempt — the identical exploit code now almost always crashes the process instead of succeeding, illustrating exactly why address-space randomization is such an effective, low-cost mitigation.

Common pitfalls & traps

PitfallWhy it happensConsequence
Embedding `0x00` bytes in shellcodePayload delivered via `strcpy`/`gets`, which stop copying at the first null bytePayload truncated before reaching the return address; exploit silently fails
Off-by-one on `buf_offset`Miscounting saved-register bytes between buffer end and return addressReturn address is only partially overwritten; program crashes instead of redirecting
Sled too shortUnderestimating address variance across runs/environmentsGuessed address lands outside the sled; segmentation fault, no shell
Assuming stack is always executableForgetting the target may enforce DEP/W^XJump succeeds but CPU refuses to execute injected bytes; immediate fault
Reusing an exploit across environmentsStack addresses, libc versions, and offsets differ by OS/compiler/ASLR stateExploit developed on one machine fails entirely on another
Ignoring stack alignment requirementsSome shellcode assumes a particular stack pointer alignment (e.g. 16-byte on x86-64 calling convention)Subsequent syscalls or library calls inside shellcode crash unexpectedly

Pioneers & history

The technique's canonical public exposition came from the exploit-development and security-research community of the 1990s, most famously formalized in Aleph One's 1996 Phrack article "Smashing the Stack for Fun and Profit," which gave the security field its first widely circulated, precise walkthrough of stack layout, return-address overwriting, and shellcode construction. The underlying insight — that a stored program's data and its instructions occupy the same undifferentiated memory, and thus can be confused — traces conceptually to the von Neumann architecture itself.

Origins · 1 minJohn von Neumann and how this was discovered
JV
John von Neumann
1903–1957 · Mathematician and computer scientist whose stored-program architecture unified code and data in one memory, the structural precondition that makes code injection possible

History
1949
— stored-program computers (EDSAC, building on von Neumann's architecture) place code and data in one uniform, addressable memory.
1988
— the Morris Worm exploits a buffer overflow in `fingerd`, one of the earliest large-scale uses of memory corruption for self-propagating code.
1996
— Aleph One publishes "Smashing the Stack for Fun and Profit," codifying stack-overflow shellcode injection for a mass audience.
2000
s — non-executable memory (NX/XD bit, DEP) and stack canaries become mainstream OS and compiler defenses.
2005
onward — widespread ASLR deployment, followed by attacker migration toward Return-Oriented Programming to survive non-executable stacks.

Where this matters

Code injection via memory corruption underlies decades of real-world exploitation: the 1988 Morris Worm, countless CGI and network-daemon compromises through the 1990s and 2000s, and a large fraction of CVEs still reported today in C/C++ codebases — browsers, kernels, VPN clients, IoT firmware, and embedded control systems where memory-safe languages are not (yet) universal. Understanding this classical mechanism is the essential foundation for every mitigation taught afterward: DEP/W^X blocks Stage 6 by refusing to execute writable memory; ASLR blocks Stage 4's precondition by randomizing the target address so the guess in Stage 3 fails with overwhelming probability; stack canaries detect the overwrite in Stage 4 before the corrupted return address is ever used, aborting the process safely. When all three are deployed together, direct shellcode injection becomes impractical, which is exactly why attackers evolved Return-Oriented Programming — reusing existing executable code fragments instead of injecting new ones, sidestepping DEP entirely while still exploiting the same underlying stack buffer overflow primitive. Security-critical software today is routinely audited, fuzzed, and rewritten in memory-safe languages (Rust, Go) precisely to eliminate this attack surface at its root rather than relying solely on runtime mitigations.

Ask the tutor: How does Return-Oriented Programming construct a working exploit once DEP prevents shellcode execution?

What is the difference between code injection and Return-Oriented Programming?

Code injection writes brand-new attacker-authored machine code into memory and jumps to it directly. Return-Oriented Programming instead reuses tiny fragments of the existing executable code already present in the binary or its libraries, chaining them together via a sequence of corrupted stack addresses. ROP was developed specifically to defeat DEP/W^X, which makes newly written memory non-executable but cannot prevent the CPU from executing code that was already marked executable at compile time. In effect, ROP answers the question "what if I can't write executable bytes, only redirect control flow among bytes that already exist?" — a strictly harder but still practical attack.

Does enabling ASLR alone fully prevent code injection?

No. ASLR raises the cost of guessing addresses but does not eliminate the underlying memory-corruption bug. Attackers routinely defeat ASLR using an auxiliary information leak — a separate vulnerability that discloses a single pointer value, from which the randomized base of the stack, heap, or libraries can be recomputed. ASLR is best understood as one layer in defense in depth, meant to be combined with DEP and stack canaries, not a standalone fix. Also, on 32-bit systems the entropy budget is small enough that brute-force guessing across many connection attempts can still succeed against certain daemons.

Why can't the CPU simply detect that a return address has been corrupted?

Because in the classical calling convention, the CPU has no notion of "this value is a code pointer that must originate from a trusted write." The stack is an undifferentiated block of memory: a 4-byte or 8-byte word looks identical whether it was placed there by the function prologue or by an attacker's overflow. The `ret` instruction's semantics are simply "pop a word, jump there," with no cryptographic or provenance check. Stack canaries add an indirect check — a secret value placed before the return address, verified before `ret` executes — but this is a software-level mitigation layered on top of hardware that has no native mechanism to distinguish legitimate from forged control data.

Is code injection still a practical threat given modern defenses?

Yes, though less directly than in the 1990s. Fully patched, hardened systems (DEP + ASLR + stack canaries + compiler hardening) make raw stack-smashing injection rare in practice. However, embedded systems, legacy software, and misconfigured deployments still lack one or more of these protections, and even hardened systems remain vulnerable to code-reuse techniques like Return-Oriented Programming and to injection via other memory regions (heap overflows, format-string bugs, use-after-free) that bypass some mitigations. The underlying root cause — memory-unsafe languages permitting unchecked writes — remains widespread in production software.

Why is the stack specifically such a common injection target rather than the heap?

The stack stores return addresses directly adjacent to local buffers within the same, predictable frame layout, giving attackers a short, well-understood distance between a controllable buffer and a critical code pointer. Heap overflows can also be exploited but typically require corrupting heap metadata or adjacent function pointers/vtables, which is more allocator-dependent and often more fragile. The stack's simplicity — linear growth, fixed calling convention, return address always at a known offset from the frame — made it the first and most reliable target, which is precisely why stack buffer overflows became the canonical teaching example for this entire class of attacks.

What role does the calling convention play in enabling this attack?

The calling convention defines exactly where the return address, saved registers, and parameters sit relative to local variables, and this fixed, publicly documented layout is what lets an attacker precompute the exact offset needed to overwrite the return address without any need to inspect the target's actual runtime memory. Different architectures (x86, x86-64, ARM) have different conventions — for instance, ARM often keeps the return address in a link register rather than always on the stack — which changes the mechanics of exploitation but not the underlying vulnerability: whenever a return/jump target is stored in writable memory alongside attacker-reachable data, the same class of attack applies.

Before this

Where this leads