>pwndojo
Vulnerability Management··16 min read

0x0 — A Bug Is Not an Exploit (Yet)

Two of the most famous memory bugs in computing history had no attacker at all: a wrapped counter in a 1980 arcade cabinet, and a 16-bit conversion that destroyed a rocket in 1996. This is the lens that separates a bug from an exploit — the three primitives every attack chases, the seven root causes behind every memory vulnerability in C, and why thirty-five years of defenses moved the war without ending it.

Memory SafetyBinary ExploitationLinuxCFundamentals

"Smashing the stack is not a crime. Getting caught is." — nobody important, probably

A wrecked arcade cabinet half-buried in desert sand, still lit, the right half of its screen dissolved into garbage tiles.
Level 256. One byte was never going to be enough.

In 1980, Namco shipped a maze game with a level counter one byte wide. One byte holds 256 values, which was 255 more than anyone at Namco expected a human being to need. Reach level 256 and the counter rolls over to zero. The routine that draws the little fruit at the bottom of the screen — one per level cleared — asks how many to draw, gets an answer it was never designed to receive, and starts writing fruit past the end of the space reserved for it and straight into the maze. Half the screen turns to garbage. The level cannot be finished. The people who got there called it the kill screen, and treated it as the machine's way of saying it had nothing left to offer them.

Sixteen years later, on 4 June 1996, Ariane 5 lifted off from Kourou for the first time. Thirty-nine seconds after that, it was debris over the mangrove.

The inertial reference system carried a routine that converted a 64-bit floating-point number — a measure of horizontal velocity — into a 16-bit signed integer. On Ariane 4, that number had never grown large enough to be a problem. Ariane 5 flew faster. The value did not fit, the conversion overflowed, and the software did exactly what it had been instructed to do with an operand it could not handle: it stopped, and reported the failure. The backup unit, running identical code on identical inputs, had stopped for the identical reason a fraction of a second earlier. So the guidance computer received a diagnostic bit pattern where it expected flight data, read it as flight data, concluded the rocket was catastrophically off course, and swung the nozzles hard over to correct a deviation that was not happening. The vehicle broke up under the load, and the range safety system finished what the arithmetic had started. Roughly $370 million, in less time than it takes to read this paragraph.

The detail that elevates this from tragedy to something you should carry with you for the rest of your career: the code that destroyed the rocket had no reason to be running at all. It was an alignment routine for the inertial platform, useful only before liftoff, left running for forty seconds into flight as a convenience inherited from Ariane 4 — where it let engineers restart a held countdown without redoing the alignment from scratch. Ariane 5 could not restart a countdown that way. The function was computing a number that nobody would ever read, on a rocket that was already flying, and it took the entire vehicle with it.

Two bugs, sixteen years apart, on wildly different hardware, in service of wildly different goals. Same root cause: a number got bigger than the box someone put it in. And here is the thing worth stopping on — neither of them had an attacker.

Nobody was attacking Pac-Man. Nobody wanted that rocket in the water. Both systems corrupted their own memory, on their own, with no adversary anywhere in the picture, and the worst either managed was a ruined screen and a fireball. That is what a memory bug does when left to its own devices: whatever the machine happens to do next.

Now add one person who wants something.

Put that same wrapped number in a C program, where the value that overflowed is a length, and feed it to malloc. You get a buffer smaller than every line of code that follows believes it to be. The write that comes next goes somewhere it was never supposed to reach — and if the person supplying the input chose it carefully, "somewhere" is a place they picked in advance. That is not a crash. That is an appointment.

That gap — between a machine breaking and a machine being steered — is the subject of this series, and the claim it makes is this: in thirty-five years we have become dramatically better at making the second step expensive, and barely better at preventing the first one. The list of mistakes has not meaningfully changed since the eighties. The defenses have changed beyond recognition. That gap is not an accident, and by 0x9 you will know exactly whose decision it was.

0x0.1 · A crash is an exploit that didn't get its act together

Here is the reframe that everything else in this series depends on, and it trips up nearly everyone at the start: memory corruption and exploitation are not the same thing.

Corruption is writing where you shouldn't, reading what you shouldn't, or using memory after you promised you were finished with it. That's the bug. On its own it usually makes the program misbehave, most often by crashing, because you scribbled over something it needed and it tripped over the mess a moment later. Pac-Man and Ariane 5 are corruption with the volume turned all the way up and nobody at the mixing desk.

Exploitation is the craft of arranging that same corruption so the program does something useful to you instead of dying. Same bug, opposite outcome, and the entire difference lives in the attacker's preparation.

The classic stack overflow does not crash because overflowing a buffer is illegal — C is delighted to let you do it, and will not mention it. It crashes because you overwrote a return address with garbage and the CPU dutifully jumped to garbage. Overwrite that same return address with a chosen, valid address and nothing crashes at all. The program continues, politely, somewhere new.

Two concrete rooms side by side: the left one gutted and burning, the right one intact with a chair pulled up to a working console.
Same corruption, same blast radius. The difference is whether anyone was waiting for it.

This did not click for me from a definition. It clicked after I spent three days convinced a one-byte overflow was worthless. One byte. What is anyone supposed to do with one byte? Then I noticed the byte was the low byte of a pointer, and that changing it moved that pointer somewhere I very much wanted it to go. The bug had not changed in three days. My definition of "useful" had.

Note: A crash is a failed exploit. While you're learning, stop reading every segfault as "it's broken" and start reading it as "I had control of something and wasted it — what did I overwrite, and what could I have put there instead?" That single flip is the job.

It is also why "we fixed the bug" and "we made the bug unexploitable" are two different sentences, and why the defenders in this story almost never do the first one. They make exploitation more expensive. The bug stays exactly where it is; the road from bug to shell gets longer, narrower, and meaner. Hold onto that — it is the thesis of everything that follows.

0x0.2 · Every exploit is a hunt for three magic powers

Strip away the vocabulary and every memory exploit ever written is chasing some combination of three capabilities. Think of them as win conditions.

  • Arbitrary read — "tell me what is stored at any address I name." Breaks secrecy. It is how you steal keys, and — far more important in the modern era — how you steal the addresses you need to defeat randomization.
  • Arbitrary write — "put this value at any address I name." Breaks integrity. A function pointer, a return address, a permission flag, a length field.
  • Code execution — "run logic of my choosing." Historically this meant hijacking control flow: making the instruction pointer, RIP, land on instructions you picked.

Nearly every technique in this series is a recipe for upgrading a weak bug into one or more of these, then chaining them. A leak tells you where things live. A write corrupts something load-bearing. That corruption hands you control. The whole arms race is a fight over how cheaply and how reliably an attacker can buy these three powers, and every defense you will meet is an attempt to raise one of those prices.

Three blast doors in a concrete wall, each with a different opening mechanism: a hand crank, a hydraulic ram, a hotwired keypad.
Read, write, execute. Every technique in this series is a way of buying one of these.

The spoiler for where this ends up: in the modern era, attackers increasingly skip the third power altogether. You do not always need to run your own code. If you can read and write the right data, the program's own blessed, signed, control-flow-integrity-approved code will do the work for you. That is 0x8, and it is the reason a decade of defenses aimed at the instruction pointer turned out to be aiming at the wrong thing.

Note — reliability box. Reliability is a property of the exploit, not a footnote about it. An exploit that works every time and one that works once in 4096 attempts and crashes the target on every miss are different weapons, even when the underlying trick is identical. Half of this series is attackers buying reliability and defenders selling them randomness. Whenever you see odds quoted — 1 in 256, 1 in 16, 1 in 4096 — ask the second question immediately: what happens on a miss? Silent retry means the attacker has all day. A crash means someone gets paged.

0x0.3 · Thirty-five years, seven mistakes

Techniques change constantly. The bugs come from a startlingly short list. Learn this taxonomy now and every advisory you read for the rest of your life becomes legible — you will catch yourself thinking "ah, temporal" instead of drowning in the writeup.

  • Spatial safety — reading or writing outside an object's bounds. The buffer overflow and its entire extended family. Root cause: C does no bounds checking, ever.
    char buf[8];
    strcpy(buf, attacker_input);   // 200 bytes? Certainly. C will not stop you.
  • Temporal safety — using memory outside its lifetime. Use-after-free, double-free, dangling pointers. Root cause: lifetimes are managed by hand, and nothing verifies that the object you are touching is still alive.
    free(p);
    *p = 0x41;     // use-after-free: p still points at memory you gave back
    free(p);       // double-free: the allocator's bookkeeping is now poisoned
  • Type safety — treating the bytes of one type as another. Type confusion. Root cause: C and C++ let you reinterpret memory through casts and unions with no runtime check that the bytes are what you claim they are.
  • Initialization — reading memory before it holds a defined value. Root cause: nothing zeroes memory for you, so a fresh allocation can still hold whatever the previous tenant left behind. Frequently that is a pointer, which is a free address leak.
  • Integer / arithmetic — math that wraps in silence. This is the sin that took the rocket down.
    void *p = malloc(count * size);   // count * size wraps to something small...
    // ...and the buffer is now smaller than every line below believes. A spatial bug, pre-loaded.
  • Concurrency — time-of-check versus time-of-use. You verify something, then act on it, and someone rearranges the world in the gap. The kernel chapters live here.
  • Undefined Behavior — the meta-sin that supercharges all six of the above.

Note — vulnerability-class spotlight: the integer bug that manufactures spatial bugs. CWE-190 rarely lands the killing blow itself. It wraps a length, and the corrupted length becomes an out-of-bounds read or write somewhere downstream — the integer bug is the spatial bug's quiet supplier. It wrecked an arcade cabinet in 1980 and a launch vehicle in 1996, and it survived the jump out of C entirely: in 2020 the same class turned up in gorilla/websocket, one of Go's most widely used libraries, as CVE-2020-27813. Memory safety in the language demoted it from "buffer overflow" to "denial of service," which is a real and enormous win, and it did not prevent the mistake. Whenever a size is computed by multiplication or addition immediately before it bounds a loop or an allocation, your eyes should narrow. (Working out which of three hundred files a given advisory refers to is a separate problem entirely, and I pointed Cisco's Antares-1B at that exact CVE to find out how well a small model handles it — the full trace is in Antares: The Machine That Reads Your Bugs.)

The family is not a historical curiosity. In 2019, sudo shipped CVE-2019-14287: on systems where a rule permitted running commands as any user except root, you could pass sudo -u#-1. The user ID -1 fails to resolve to root, so the "except root" check has nothing to object to — and setresuid treats -1 as "leave this ID unchanged." sudo was already running as root. It changed nothing, and obliged.

You could become root by asking to become user number minus one.

Note: Undefined Behavior is not "the program does something weird." It is a licence the compiler holds to assume your bug cannot happen, and to optimize on that assumption. Write a null check that only makes sense if a pointer might be null, but let earlier code dereference that pointer first, and the optimizer is entitled to reason "it cannot be null — that dereference proved it" and delete your check. Real vulnerabilities ship this way. "The compiler removed my security check" sounds like a conspiracy theory; it's a Tuesday.

0x0.4 · Why C hands you the gun, the ammo, and a foot

It is tempting to read all of this as C being broken. It isn't, quite. It is C being honest about a bargain it struck decades ago: maximum performance and control, in exchange for trusting the programmer completely.

No bounds checks at runtime, because checks cost cycles. No lifetime tracking, because you — the obvious genius at the keyboard — surely know when your objects die. No runtime type identity, because types are a compile-time fiction that ought to evaporate before the CPU sees anything. Every "missing" feature is a deliberate refusal to spend performance protecting you from yourself.

A precisely machined engine on a workbench with rusted armour plates crudely welded over it, one already peeling away.
Beautiful engineering, bolted onto a hull that was never designed to be watertight.

That bargain built the world you are standing in — kernels, browsers, runtimes, the firmware in whatever you are reading this on. It also means every defense you are about to meet is a bolt-on: a plate welded onto a hull that was never designed to be watertight. Stack canaries, NX, ASLR, CFI, shadow stacks — beautiful engineering, all of it, all of it working around a language that declines to check its own homework.

Which is why the only entry in this entire series that attacks the root cause rather than a symptom is "write it in a language that does the checking," and why that answer is simultaneously correct and a decade-long migraine. We will earn that conclusion the hard way.

0x0.5 · How to read the next nine parts

Three things evolve in parallel across this series, and the most common mistake — made by textbooks, conference talks, and me for an embarrassingly long time — is blurring them together:

  1. The vulnerability — the bug, the root cause in the source. One of the seven above.
  2. The technique — how that bug becomes read, write, or execute. This changes for the same bug as defenses appear. A heap overflow in 2001 and a heap overflow in 2021 are the same vulnerability and completely different work.
  3. The defense — what forced the technique to evolve.

Keep them in separate columns in your head. Every part from 0x2 onward runs the same rhythm on purpose: the bug, the technique that weaponizes it, the defense deployed in response, and the bypass that routes around the defense and sets up the next chapter.

Everything here is written against one pinned target so the exploits reproduce byte for byte: Ubuntu 24.04 LTS, glibc 2.39, GCC 13, x86-64. Match it and your output will match mine. Where a historical technique behaves differently on modern glibc, that difference is usually the interesting part rather than an inconvenience.

Note — tooling box. Three tools get you through the early parts. checksec tells you which defenses a binary was built with — run it on something in /usr/bin right now and read the row of Yes/No as a summary of everything the next four parts are about. pwntools scripts the interaction, so you stop pasting payloads by hand. gef or pwndbg makes GDB usable by a human. Install all three before 0x1; you will use them in every part after it.

We have spent this part establishing what an attacker wants. Before we can go and take it, you need to know what the ground looks like: where a program's code, its data, and its return addresses live, why one of those sits close enough to touch the others, and where each defense gets installed along the way from source file to running process.

That's 0x1. Bring the debugger.