Create your own
Lesson illustration

Classifying Crashes by Exploitation Impact

Hello again. In our last session, we built a solid workflow for tracing attacker-controlled input from its entry point all the way to a corrupted memory location. By combining Ghidra's static analysis with GDB's dynamic verification using breakpoints and watchpoints, you learned how to prove that corruption happens and where.

Now, we move from proving the cause to understanding the effect. A program crash is not just a failure; it's a symptom that tells a story. This lesson is about learning to read those symptoms. By analyzing the state of the program at the moment of the crash—the instruction pointer, registers, and memory—you can classify the vulnerability's outcome. This outcome is your "exploitation primitive": the fundamental capability the bug grants you. Is it the power to redirect execution, to alter program data, or to read secrets? Answering this question is the bridge between finding a bug and planning a successful exploit.


What is an Exploitation Primitive?

In the context of binary exploitation, a primitive is the basic building block of an exploit—the simplest capability an attacker gains from a vulnerability. Think of it as discovering what tools you have at your disposal. Did the bug give you a key, a pen, or a sledgehammer?

  • A key might let you open one specific door (e.g., changing is_admin = 0 to is_admin = 1).
  • A pen might let you write a new message anywhere you want (an arbitrary write).
  • A sledgehammer might let you demolish a wall to go wherever you please (instruction-pointer control).

Our goal is to look at the wreckage of a crash and determine which tool we've just been handed. The most common primitives you'll encounter at this stage are:

  • Instruction-Pointer Control: The ability to decide what code the CPU executes next. This is the classic goal of many exploits.
  • Data Modification: The ability to overwrite data in memory. This can range from changing a single, specific variable to writing arbitrary data to an arbitrary location (a "write-what-where" primitive).
  • Information Disclosure: The ability to read memory that should be secret, such as passwords, encryption keys, or, more commonly, memory addresses that help defeat ASLR.
  • Denial of Service (DoS): The ability to crash the program reliably, but not control it. This is often the default outcome of a bug before it's refined into a more useful primitive.

Primitive #1: Instruction-Pointer Control

This is often considered the jackpot. You've gone beyond just corrupting data and can now seize control of the program's execution flow.

The Symptom: After providing your input, the program crashes with a Segmentation fault. When you inspect the crash in GDB, the instruction pointer register (RIP on x86-64) contains a value made of your input, like 0x4141414141414141 (the ASCII for 'AAAAAAAA').

The Cause: This is the classic outcome of a stack buffer overflow. Your oversized input has written past the end of its intended buffer and overwritten the saved return address stored on the stack. When the function finishes and executes the ret instruction, it pops this corrupted address off the stack and into RIP, attempting to "return" to a location of your choosing.

This diagram shows how overflowing a buffer (`char foo`) in `function_1`'s stack frame can overwrite the `saved return pointer` pushed by its caller, `main`. When `function_1` returns, it will use the attacker-controlled value as the next instruction address.

The pwn.college video "Smashing the Stack" provides an excellent walkthrough of this entire process. Pay close attention to how a simple, vulnerable C program is analyzed and how a long string input leads directly to hijacking the return address.

pwn.college - Memory Errors - Smashing the Stack

This video from pwn.college demonstrates the canonical stack buffer overflow.

Watch these key segments: The explanation of how an overflow corrupts the stack, visually connecting the sprintf vulnerability to the overwritten return address. The demonstration in GDB, showing a crash with a controlled RIP. This reinforces what you see in the debugger. Finally, the short explanation of how this crash can be turned into a "win" condition by replacing the garbage 'A's with a valid address.

A Note on 64-bit Crashes: Canonical Addresses

On x86-64, you might not see RIP become 0x4141414141414141. Instead, the program just crashes. This is due to the canonical address requirement. 64-bit CPUs don't actually use all 64 bits for addressing. Valid addresses must have their upper 16 bits all be the same (either all 0s or all 1s). An address like 0x4141... is non-canonical, and the CPU will raise a fault before it even attempts the jump.

In GDB, you'll still get a SIGSEGV, but RIP might point to the instruction after the ret. The real clue is on the stack. Check the value at the top of the stack (x/gx $rsp). You will find your 0x4141... pattern there, confirming you control the value that would have been loaded into RIP.

The article "64-bit Stack-based Buffer Overflow" from ired.team walks through this exact scenario.

64-bit Stack-based Buffer Overflow

This article provides a step-by-step guide to achieving RIP control on x86-64, paying special attention to the canonical address issue.

Follow the author's process: In the section "Getting Control of RIP", read how an initial overflow with 'A's causes a crash but doesn't immediately control RIP. The next section, "Why is RIP not overflowed?", explains the canonical address requirement. Under "Finding RIP Offset", see how a cyclic pattern is used to find the exact offset to the return address. Finally, "RIP is Under Control" shows the successful hijack of RIP with a carefully crafted payload.


Primitive #2: Attacker-Controlled Data Modification

Sometimes your overflow doesn't reach the return address, or you're dealing with a different kind of bug entirely. Instead of controlling RIP, you might find you can overwrite other variables.

The Symptom: The program doesn't crash, but its logic changes. Or it crashes later, when it tries to use data you corrupted (e.g., mov rax, [rcx] where rcx now holds 0x41414141).

The Cause: Your input overwrote a local variable, a function pointer, or a data pointer stored in memory.

There are two main flavors:

  1. Limited Data Modification: You overwrite a specific variable adjacent to your buffer. Common CTF examples include overwriting a boolean is_admin flag from false to true, a user ID, or a filename string to trick the program into opening /etc/passwd instead of user.txt.
  2. Arbitrary Write (Write-What-Where): This is a far more powerful primitive. Here, a vulnerability lets you control both a destination address (the "where") and the data written to it (the "what"). For example, you might overflow a buffer to corrupt a pointer variable, and then a later part of the code writes data using that now-malicious pointer.

Format string vulnerabilities are a classic source of arbitrary write primitives. The %n format specifier is designed to write the number of characters printed so far into a pointer provided as an argument. If you control the format string, you can make it read an address you've placed on the stack and use %n to write to it.

LiveOverflow's video on format string exploits is a masterclass in demonstrating this.

A simple Format String exploit example - bin 0x11

LiveOverflow's video explains how format string bugs can be turned into powerful write primitives.

First, watch the segment on the %n specifier to understand the mechanism. He explains how %n tells printf to write the number of characters printed so far to a memory location pointed to by an argument on the stack. Next, see how this is exploited. The demonstration shows the full process: finding the target variable's address, placing that address into the input so it ends up on the stack, and then using %n to write to that target address, thus achieving an arbitrary write.


Primitive #3: Information Disclosure

Before you can successfully exploit a modern program with ASLR and stack canaries, you often need to leak information from it first. An information disclosure (or "infoleak") primitive allows you to do just that.

The Symptom: The program's output includes data that looks out of place: sequences of hex digits that are clearly memory addresses, random-looking data, or parts of secrets.

The Cause: The vulnerability causes the program to read from and print memory it shouldn't.

  • Reading past a null terminator: You overflow a buffer and overwrite its null terminator. A subsequent puts(buffer) will keep printing bytes from the stack until it hits another null byte, leaking whatever was in between.
  • Uninitialized variables: A function uses a variable without first initializing it, potentially leaking data left on the stack by a previous function call.
  • Format String Vulnerability: As seen before, if you provide format specifiers like %p or %x without corresponding arguments, printf will simply pull values sequentially off the stack and print them. This is a powerful and direct way to leak stack content, including saved addresses and canaries.

The same LiveOverflow video also demonstrates this primitive perfectly.

A simple Format String exploit example - bin 0x11

This segment of the format string video focuses on the information disclosure aspect.

Watch the part where he provides format specifiers like %x as input. You can see how this causes the program to leak values directly from the stack. He specifically mentions how this can be used to defeat ASLR by leaking stack addresses or to bypass stack protectors by leaking the canary value.


A Systematic Approach to Classification

To reliably classify a crash, you need a methodical process. Fuzzing with a simple pattern is your first diagnostic tool.

  1. Trigger the crash with a long, non-repeating input, like one generated by pwn.cyclic().
  2. Analyze the crash in GDB/GEF:
    • Check RIP and GEF's context. GEF will often tell you RIP: 0x61616170 ('paaa'). This is a clear sign of direct instruction-pointer control. Calculate the offset with cyclic_find('paaa').
    • Check for canary failure. If the program exits with *** stack smashing detected ***, your primitive is "stack canary overwrite." You've proven you can reach the canary; the next step (for a future lesson) would be to find a leak to defeat it.
    • Examine the faulting instruction. If RIP isn't controlled, what instruction failed? If it's something like mov rax, [rcx] and rcx contains part of your cyclic pattern, you likely have a data modification primitive that corrupted a pointer.
  3. Use core dumps. For complex crashes or for automation, letting the program generate a core file can be very efficient. pwntools has excellent support for parsing these files. You can inspect registers, memory, and signals without needing an interactive GDB session.

This pwntools documentation shows a powerful example of using a core dump to automate finding a buffer overflow offset.

pwnlib.elf.corefile — Core Files — pwntools 4.15.0 documentation

This documentation demonstrates how to use pwntools to analyze core dumps programmatically, which is a key skill for CTF players.

First, look at the main example under "Using Corefiles to Automate Exploitation". It shows a complete script that crashes a program and uses the core dump to find the value of EIP at the time of the crash, confirming control. Then, scroll down and note the useful properties you can access, such as fault_addr and signal. These help you programmatically determine why the program crashed, which is the essence of classification.

This table summarizes the diagnostic process:

Crash Symptom & GDB/GEF StateLikely PrimitiveImmediate Next Step
RIP contains bytes from your cyclic pattern.Instruction-Pointer ControlCalculate the exact offset and plan a code reuse attack (e.g., ROP).
Program aborts with *** stack smashing detected ***.Stack Canary OverwriteFind an information disclosure vulnerability to leak the canary.
Crash on mov rax, [rcx] where rcx contains your pattern.Corrupted Data PointerVerify control over the pointer's value and what's accessed.
Program output includes stack/libc addresses.Information DisclosureUse the leaked address to calculate base addresses and defeat ASLR.
Program logic changes (e.g., grants admin access) but doesn't crash.Limited Data ModificationConfirm which variable was modified and leverage the new state.

Key Takeaways

  • A crash is a source of information. Your first goal after finding a crash is to classify the exploitation primitive it provides.
  • The three most common primitives from memory corruption are instruction-pointer control, data modification, and information disclosure.
  • Instruction-pointer control is identified by RIP being overwritten with attacker data. On x86-64, be mindful of the canonical address requirement.
  • Data modification is identified by changes in program behavior or crashes when using corrupted pointers. This can range from flipping a single flag to a full arbitrary write.
  • Information disclosure is identified by the program printing memory contents like addresses or canaries, often through format string bugs or over-reading buffers.
  • Use a systematic approach: crash with a cyclic pattern, analyze the registers and faulting instruction in GDB, and leverage tools like pwntools with core dumps to diagnose the outcome.

In our next module, we'll dive deep into dynamic linking and ret2libc. You'll learn how to combine an information disclosure primitive with instruction-pointer control to bypass NX protection—a foundational technique for modern pwn challenges.

Can't find a good explanation? Sign up and we'll make it for you

Sign up