At execution time: `ret` pops `pop rdi; ret` into RIP; that pops the `"/bin/sh"`
pointer into `RDI`; that `ret` pops `system()` into RIP, with `RDI` still
holding the string. `system("/bin/sh")` runs.
The gadgets (`pop rdi; ret`) are not in `food` — this glibc has no
`__libc_csu_init` — so `fooc` finds them by scanning live libc memory for the
byte pair `5f c3`. It locates libc via `/proc/self/maps`, finds the offsets of
`system` and `"/bin/sh"` with `dlsym()`, and computes the base from the leak
`food` publishes. Nothing is hardcoded, so it survives a libc update.
**What it teaches:** once you can control `RIP`, you can chain *existing*
instructions. This is return-oriented programming, and it is what nearly all
real-world exploitation looks like, because it needs no attacker-supplied
executable memory.
**Defence:** none of the compiler flags stop this on their own. It works
against a PIE binary, with NX, with a canary — as long as the attacker has a
leak. The defences are "do not have the overflow" and "do not leak addresses."
See the table below.
### 3. `shellcode` — run your own machine code
23 bytes, placed at the start of the buffer, with `RIP` pointed at them:
```asm
xor esi, esi ; envp = NULL
xor edx, edx ; argv = NULL
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" as 8 raw bytes
push rdi ; put the string on the stack
mov rdi, rsp ; rdi = &"/bin/sh"
push 0x3b ; 59 = __NR_execve
pop rax
syscall ; we are now a shell
```
This is the purest form of the bug: the attacker supplies the *instructions*,
not just the address of instructions that already exist. No libc offsets needed,
so in principle it works against a statically linked, fully randomised target.
`make verify` assembles `shellcode.S` and diffs it against the byte array
embedded in `fooc.c`, so the two cannot drift apart.
**Defence:** **NX** (a.k.a. W^X, "no execute"). Marking the stack
non-executable makes the hardware refuse to fetch instructions from it, and the
`ret` lands on a page that cannot run. This is why `make food` passes
`-z execstack`: a stock Linux stack is `rw-p`, not `rwx`, and the technique
dies with SIGSEGV at `RIP = the payload's address`. The single most important
lesson in the lab is that every one of these bytes only works because the
compiler was told to leave the stack executable. That flag is on for nobody's
benefit.
### Also included
| Mode | What it does |
|---|---|
| `-t leak` | connects, prints the leaks, sends nothing |
| `-t demo` | sends `rip_off + 8` bytes of `0x41`, so `RIP` becomes `0x4141...` and the daemon dies. Proves the bug with no address knowledge at all |
| `-t sled` | a ret sled, deliberately kept as a **failing** example. Without a leak you would brute-force ASLR by filling the buffer with the address of a `ret`. It cannot work here: `food` accepts 512 bytes, so the sled is ~53 slots against ~28 bits of entropy. Implemented so you can watch it fail, and confirm the mechanism really is "the CPU follows a chain of rets" |
---
## The mitigation table
This is the part to remember. Each row is a real defence, and the right-hand
column is what it actually does to the chain of events.
| Mitigation | How to enable | What it stops | What it does *not* stop |
|---|---|---|---|
| **Bound the read** | `n = read(fd, buf, sizeof buf - 1);` | **Everything.** The bug does not exist, so nothing downstream matters | Nothing — this is the only complete fix |
| **Stack canary** | `-fstack-protector-strong` (gcc's default) | The `ret`: the canary is checked on function exit, so the smash is detected and the process aborts before `RIP` is popped | A bug in a function with *no* array (nothing to protect); an overflow that stays under the canary; anything that does not return normally |
| **NX / W^X** | `-z noexecstack` (the default) | Shellcode. The payload's own instructions cannot be fetched | ret2win and ret2libc entirely. These are the *reason* ROP exists |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (both default) | ret2win's hardcoded addresses. Everything moves each run | Anything where the attacker has a leak. ASLR raises the cost of an exploit; it is not a fix. Note that stack, heap and mmap are randomised but the main binary's *contents* are not — that is what ROP chains use |
| **Don't leak** | don't `printf("%p")` to clients; initialise before printing | The information leak that turns ASLR from "expensive" into "free" | — |
| **Don't use `printf(user_data)`** | `printf("%s", buf)` instead of `printf(buf)` | Format-string bugs: `%x` stack reads, `%n` arbitrary writes, which is a *second* way to get RCE | — |
| **Don't use untrusted paths** | validate and `openat()` under a fixed dir | Path traversal ([CWE-22](https://cwe.mitre.org/data/definitions/22.html)) | — |
| **CET / shadow stack** | `-fcf-protection=full`, kernel + CPU support | The `ret` itself: the shadow stack remembers the *real* return address and faults on a mismatch. Catches ROP chains that use hardware `ret` | Attacks that never `ret` (call-oriented, or overwriting a function pointer's target with a gadget chain that does not need a return) |
| **Safe languages** | Rust, Go, C# for new code | The whole class. Bounds checks are checked at runtime, not hoped for at review time | — |
### Seeing it for yourself
```sh
make run # vulnerable daemon
make test # all three techniques work
make test-hardened # same source, mitigations on
```
`test-hardened` builds `food_hardened` with `-fstack-protector-strong -fPIE
-pie -z noexecstack`, swaps it in, re-runs all three, then puts the vulnerable
one back. You will see:
```
### stack segment: 'rw-p' (NOT executable) is what you want to see
--- ret2win was stopped by the mitigations (as expected)
--- ret2libc was stopped by the mitigations (as expected)
--- shellcode was stopped by the mitigations (as expected)
```
And in the hardened daemon's log, the canary firing:
```
*** stack smashing detected ***: terminated
```
Read that carefully, because it is the most important line in the whole lab:
**the canary caught ret2win, not PIE.** All three techniques die at the canary,
because all three go through the same `read()` and smash the same frame. NX only
separately stops shellcode's *code*; PIE only separately breaks the hardcoded
address. Turn them on individually and you will find that most single
mitigations leave you exposed to something.
---
## Files
| File | Purpose |
|---|---|
| `food.c` | the vulnerable daemon. 6 numbered bugs, each with its fix in the comment |