Create your own
Lesson illustration

Mitigation-Aware Exploitation Planning from Confirmed Vulnerabilities

Good to see you again. You now have the pieces that usually precede an exploit: ELF triage, an input-to-crash trace, a classification of the resulting primitive, and a pwntools harness that can reproduce observations locally under GDB.

This lesson is the decision point between “the program crashes” and “I know what exploit I am building.” You will learn to write a mitigation-aware exploitation plan: a short, evidence-based design that states your objective, viable targets, required disclosures, payload stages, and tests. The goal is not to guess a payload early; it is to eliminate exploit paths that the binary’s actual mitigations make impossible.


A plan is a testable hypothesis

A confirmed primitive tells you what capability the vulnerability gives you. For example:

  • “The second input overwrites the saved return address after 40 bytes” gives instruction-pointer control.
  • “The first input is passed as the format argument to printf” gives a potential disclosure primitive.
  • “An index lets me replace one pointer-sized field in a global object” gives a targeted write primitive.

None of these statements alone says what to do next. An exploitation plan connects the primitive to a realistic program target while accounting for the protections in the way.

A useful plan answers six questions:

  1. What is verified?
    State the exact primitive, input path, offset or relevant field, and any input constraints.

  2. What is the objective?
    A direct win()-style success function, reading a flag, or obtaining a shell are different goals with different requirements.

  3. What code and data targets exist?
    List useful functions, PLT entries, ROP gadgets, GOT entries, embedded strings, writable regions, and any known library file.

  4. Which mitigations block the shortest route?
    Be specific: NX blocks injected code execution; PIE blocks static binary addresses; a canary blocks a normal return-address overwrite.

  5. What runtime facts must be obtained?
    Typical facts include a canary, PIE base, libc base, stack address, or the address stored in a GOT entry.

  6. How will each assumption be checked?
    Your plan should say what you will inspect in GDB/GEF, what output you expect, and what condition would make you abandon the route.

The distinction between verified, inferred, and assumed is worth maintaining in your notes.

LabelMeaningExample
VerifiedDirectly observed in code, debugger, or outputread(buf, 0x100) writes into a 32-byte stack buffer
InferredFollows from verified factsRIP should be controllable if no canary intervenes
AssumedPlausible but untestedThe remote service uses the supplied libc

An exploit should be built on verified facts, not on an attractive assumption.


A concrete planning mindset

The following walkthrough starts with a familiar pwn shape: a stack overflow, NX enabled, no stack canary, no PIE, and full RELRO. Notice that full RELRO does not make the Global Offset Table useless. It prevents overwriting GOT slots, but a resolved GOT entry can still be a valuable read target for a libc leak.

BOF + ROP + libc leak + system('/bin/sh') - Cyber Apocalypse 2023 - pwn/pandora

Watch “BOF + ROP + libc leak + system('/bin/sh')” by SloppyJoePirates CTF Writeups for a compact example of moving from binary triage to a two-stage plan.

Watch the triage to see how the overflow and mitigation profile establish the initial constraints. Then watch the first stage, focusing on why a readable GOT entry and an existing output function can disclose a libc address even under full RELRO. Finish with the second stage, which explains how a known libc base makes library-resident targets addressable.

The video’s payload construction belongs mostly to the next module. For this lesson, extract its planning logic:

  • The overflow supplies control of the return sequence.
  • NX rules out returning to shellcode stored on the stack.
  • No PIE means binary code, PLT, GOT, and gadget addresses are stable.
  • ASLR still randomizes libc, so a hard-coded system address is not viable.
  • Full RELRO rules out a GOT overwrite, but not a GOT leak.
  • Returning to the vulnerable routine after the leak creates a second opportunity to send input.
  • The final route uses a libc base calculated from a specific, identified leaked libc function.

This is much stronger than the vague statement, “I will use ret2libc.” A plan must identify how you get the base address, which callable path emits it, and whether the program gives you another input stage.

A conceptual Linux ELF process layout: executable code and static data occupy lower mappings, shared libraries have separate mappings, the heap grows upward, and the stack grows downward. Exploit planning must distinguish file-relative ELF offsets from the randomized runtime addresses of these mappings.

The image also highlights a source of frequent confusion: .text, .rodata, .data, and .bss describe content in the executable file and its mapped image; the stack, heap, and shared libraries are runtime regions. Under ASLR and PIE, the target’s offset may be known while its live address is not.


Translate mitigations into requirements

Mitigations are not simply a “difficulty score.” Each one creates a specific missing requirement in your plan.

MitigationWhat it blocksPlanning consequence
NXExecuting bytes placed in stack or heap memoryReuse existing executable code, such as a function, PLT stub, ROP gadget, or syscall sequence
ASLRPredictable shared-library, stack, heap, and loader addressesObtain a suitable runtime disclosure before using randomized addresses
PIEPredictable addresses inside the main executableObtain a binary pointer and calculate the PIE base, or find a route requiring only another known base
Stack canaryA normal stack overwrite reaching saved control dataPreserve the exact current canary, avoid crossing it, or use another primitive
Partial RELROReliable pre-resolution GOT overwrite in many common casesTreat GOT overwrites cautiously; determine whether the desired slot remains writable and when
Full RELROGOT overwrites after startup relocationDo not plan a GOT overwrite; GOT reads and PLT calls may still be useful

Two rules prevent many bad plans:

  • A mitigation is only relevant if your chosen route encounters it. If you have a format-string arbitrary write to a non-GOT target, full RELRO is not necessarily the central obstacle.
  • A leak is only useful when you identify what it leaks. Seeing a hexadecimal value beginning with 0x7f is a clue, not proof that it is the address of puts, a libc base, or even a code pointer.

For a PIE pointer to a known binary location:

For an exact function address leaked from libc:

Here, is a runtime leak and is a stable file-relative offset. The subtraction is valid only when you have verified the identity of the leaked pointer and the correct binary or libc file.

Once a base is known, a target runtime address is computed as:

Do not subtract the offset of puts from an arbitrary pointer that merely falls inside libc. It may point to an internal libc structure, a return site, or a different mapping. Use GDB’s memory map and disassembly to classify it first.


Inventory targets before choosing a route

Before writing a ROP chain, build a small capability inventory. Ghidra, readelf, pwntools’ ELF, and GDB each contribute different evidence.

Code targets

Look for:

  • A direct success function such as win, print_flag, or a function that already opens and prints the target file.
  • A useful program loop or input routine to re-enter after a first-stage leak.
  • Imported functions reachable through the PLT, especially output functions such as puts, printf, or write.
  • Calling-convention gadgets, particularly pop rdi ; ret for a one-argument AMD64 function call.
  • A plain ret gadget, which may be needed for 16-byte stack alignment.
  • Existing wrappers that call interesting functionality with favorable arguments.

A direct success function is almost always preferable to a shell-oriented route. It has fewer dependencies: perhaps only control-flow redirection and a binary base.

Data targets

Record:

  • GOT entries for functions that have already been called or can be safely resolved.
  • Static strings in .rodata, including command strings or file names.
  • Writable .bss or .data regions if the route requires storing later-stage data.
  • A supplied libc.so.6 and loader, including their build identity and offsets.
  • Runtime pointers visible on the stack, heap, or output.

A target is not automatically usable because it appears in Ghidra. Ask three questions:

  1. Can I reach it? Do I have RIP control, a writable function pointer, a format-string write, or another matching primitive?
  2. Do I know its live address? If PIE or ASLR applies, what leak establishes its base?
  3. Can I satisfy its inputs? On x86-64 Linux, a normal first argument is placed in , not pushed as a stack argument.

The following resource gives a short mitigation summary and then uses an example containing both a format-string disclosure and a protected overflow. Read it as a planning case study, not as a universal recipe: the format-string argument positions and leaked values are target-specific.

PWN - ROP: bypass NX, ASLR, PIE and Canary — IRONHACKERS

Read the “Protections” and “Analysis” sections of this IRONHACKERS article to connect each mitigation to the capability that must be recovered before a protected stack overwrite becomes viable.

In the “Protections” section, read the mitigation summary. Focus on the distinct roles of NX, ASLR, PIE, and the canary. Then, in “Analysis,” read from the dual-primitive analysis. Notice the article first identifies what each input does before proposing bypasses; do not assume its exact offsets, library version, or leak indexes apply to another binary.


Choose the shortest viable route

When several routes are conceivable, prefer the one with the fewest unverified dependencies. A practical ordering is:

  1. Direct control transfer to a known success function
  2. Control transfer after one necessary base leak
  3. A two-stage libc-based route
  4. A write-based route requiring a carefully selected writable target
  5. A route that depends on several unverified leaks or fragile environmental guesses

Consider these common cases.

Case 1: RIP control, no PIE, no canary, NX enabled

Suppose a stack overflow reaches RIP at offset 72. checksec shows NX enabled, no PIE, no canary, and full RELRO. Ghidra shows no win() function, but the binary imports puts and read.

A reasonable plan is:

  • Objective: gain code reuse through libc.
  • Stage one: use a fixed-address pop rdi ; ret gadget to place the address of read@got in ; call puts@plt; return to a routine that accepts another input.
  • Required observation: the resulting output must correspond to the actual resolved read address, parsed as binary output rather than assumed to be printable text.
  • Calculation: subtract read’s offset in the supplied libc from the leaked address.
  • Stage two: use the computed libc base to call a chosen libc function with valid arguments.
  • Mitigation notes: NX is handled through code reuse; ASLR is handled by the stage-one libc leak; full RELRO is respected because the GOT is read, not overwritten.

If Ghidra instead reveals a fixed-address win() that prints the flag, the plan becomes simpler: return directly to win() and avoid libc entirely.

Case 2: RIP control, canary, PIE, NX, and a format-string leak

Suppose the first prompt accepts 20 bytes and is used as printf(name). The second prompt overflows a stack buffer. The binary has a canary, PIE, NX, and ASLR.

The plan must recognize that the overflow alone is currently unusable: it would overwrite the canary before it reaches RIP. The format string potentially supplies the missing information.

A disciplined plan is:

  • Objective: invoke a direct binary success function, if one exists.
  • Stage one: use positional format directives to disclose:
    • the current stack canary;
    • a pointer verified to lie in the main binary’s mapped image.
  • Calculation: subtract the statically known offset of that binary pointer to obtain the PIE base.
  • Stage two: send an overflow consisting of padding, the leaked canary in its exact slot, saved-frame padding, and the success function’s calculated runtime address.
  • Mitigation notes: the correct canary preserves the stack-integrity check; the PIE leak makes code addresses usable; NX is irrelevant if the target is existing executable code; a libc leak is unnecessary unless the selected final target lives in libc.
  • Critical feasibility check: confirm that the program accepts the second input in the same process instance after the first disclosure.

This route is viable only after you validate each leaked value. A candidate canary should match the value observed at the function’s canary slot in GDB during the same run. A candidate PIE pointer should fall in the binary mapping and retain the expected file-relative relationship across several local runs.

Case 3: Canary-protected overflow with no leak

If a canary sits between the input buffer and saved RIP and you cannot disclose or preserve it, “overwrite RIP” is not an exploitable plan yet. Do not write “brute force the canary” as a default answer. In a normal local process or a forking remote service with rate limits, that is generally infeasible.

Instead, return to your primitive classification:

  • Can the overflow modify an object or function pointer without crossing the canary?
  • Is there a separate information disclosure?
  • Is a different input parser or code path less protected?
  • Does the apparent crash actually occur before the intended corruption site?

A good plan can legitimately conclude: current route blocked; investigate alternative primitive.


Write the plan your exploit will implement

Use a one-page structure like the following. It should be complete enough that you could implement it in your harness later without reopening every tool just to remember the idea.

# Exploitation plan, version 1

Verified primitive
- Input: ...
- Corruption or disclosure: ...
- Exact boundary or offset: ...
- Interaction constraints: ...

Mitigation profile
- NX: ...
- PIE: ...
- Canary: ...
- RELRO: ...
- Supplied runtime files: ...

Objective
- Direct success condition: ...

Candidate targets
- Code: ...
- Data: ...
- Required gadget or calling convention: ...

Chosen route
- Stage one purpose: ...
- Leak identity and expected mapping: ...
- Re-entry location: ...
- Base calculation: ...
- Stage two purpose: ...

Validation points
- In GDB: ...
- In program output: ...
- Across repeated ASLR runs: ...

Fallback
- If a leak is not stable or not identified: ...
- If the second input is unavailable: ...

Here is a filled, intentionally generic example.

Verified primitive
- Prompt 1: user bytes are used as the format argument to printf.
- Prompt 2: read writes 0x100 bytes into a 0x20-byte stack buffer.
- GDB confirms the stack canary is between the buffer and saved RIP.
- The second prompt occurs after Prompt 1 in the same process.

Mitigation profile
- NX enabled, PIE enabled, canary enabled, full RELRO.
- No supplied libc is needed if a binary-resident win function is usable.

Objective
- Call win(), which prints the challenge flag.

Candidate targets
- win at a known ELF-relative offset.
- A binary code pointer present in the format-string argument area.
- The current function canary present in the format-string argument area.

Chosen route
- Stage one: leak the verified binary pointer and canary with a short,
  delimiter-separated format payload.
- Compute PIE base from the known binary pointer offset.
- Stage two: overflow through the saved return address while reproducing
  the exact leaked canary, then use PIE base plus win offset.

Validation points
- GDB: leaked canary equals the qword at the canary stack slot.
- GDB: leaked binary pointer belongs to the executable mapping.
- Script: PIE base is page-aligned and calculated win address disassembles
  to the expected function locally.
- Reliability: repeat at least ten times with ASLR enabled.

Fallback
- If the apparent binary pointer changes position or cannot be identified,
  locate another persistent in-binary pointer before attempting the overflow.

Notice what this plan does not contain:

  • a copied runtime address from one GDB run;
  • an unexplained 0x7f... value labeled “libc”;
  • a promise that a local format-string position will match remotely;
  • a payload written before the target’s base and canary requirements are established.

Your pwntools harness from the previous lesson is the implementation environment for this plan. Log the plan’s intermediate facts rather than only the final result:

log.info(f"canary = {canary:#x}")
log.info(f"pie base = {exe.address:#x}")
log.info(f"win runtime address = {exe.sym['win']:#x}")

For each value, add a local GDB check before trusting it. Then run the script repeatedly with ordinary ASLR enabled. Reliability is part of the plan, not polish added after a lucky success.


A short planning pass for any new challenge

Before building a payload, spend ten focused minutes producing these notes:

  1. Copy the exact checksec output.
  2. State the confirmed primitive in one sentence without using the word “probably.”
  3. List one direct code target, one possible re-entry point, and one possible disclosure target.
  4. Mark every address as either static offset, fixed runtime address, or requires runtime base.
  5. Identify the single missing fact that blocks the simplest route.
  6. Write the GDB observation that would confirm that fact.

If you cannot complete step 5, you are not ready to commit to an exploit route. Return to Ghidra and the debugger; the problem is likely target inventory or primitive understanding, not payload syntax.


Key takeaways

A mitigation-aware exploitation plan turns raw vulnerability evidence into an ordered, testable route:

  • Start from the confirmed primitive, not a favorite technique.
  • Inventory callable code, useful data, runtime leaks, and re-entry opportunities.
  • Treat NX, PIE, ASLR, canaries, and RELRO as precise requirements rather than generic obstacles.
  • Use a direct in-binary success target whenever it removes unnecessary libc or shell dependencies.
  • Only compute a base from a leak whose identity and mapping have been verified.
  • Include validation points, repeated-ASLR testing, and a fallback condition in the plan.

You have now completed the first module’s workflow: triage the ELF, understand its mappings and mitigations, trace a vulnerable path, classify the primitive, reproduce it with a harness, and select an evidence-based route. The next module shifts toward systematic reverse engineering: recovering function roles and calling relationships in binaries whose structure is not immediately obvious.

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

Sign up