| `-t demo` | stuurt `rip_off + 8` bytes `0x41`, zodat `RIP``0x4141...` wordt en de daemon sterft. Bewijst de bug zonder enige adreskennis |
| `-t sled` | een ret-sled, bewust bewaard als **fout** voorbeeld. Zonder lek zou je ASLR brute-forcen door de buffer te vullen met het adres van een `ret`. Dat kan hier niet werken: `food` accepteert 512 bytes, dus de sled heeft ~53 slots tegenover ~28 bits entropie. Zo geïmplementeerd dat je het kunt zien falen en kunt bevestigen dat het mechanisme echt "de CPU volgt een keten van rets" is |
---
## De tabel met tegenmaatregelen
Dit is het deel om te onthouden. Elke rij is een echte verdediging, en de
rechterkolom laat zien wat die daadwerkelijk met de gebeurtenisketen doet.
| Tegenmaatregel | Zo activeer je | Wat hij stopt | Wat hij *niet* stopt |
|---|---|---|---|
| **Beperk read** | `n = read(fd, buf, sizeof buf - 1);` | **Alles.** De bug bestaat niet, dus niets stroomafwaarts doet ertoe | Niets — dit is de enige volledige fix |
| **Stack-canary** | `-fstack-protector-strong` (gcc-standaard) | De `ret`: de canary wordt aan het einde van de functie gecontroleerd, dus de beschadiging wordt gedetecteerd en het proces aborted vóórdat `RIP` wordt gepopt | Een bug in een functie *zonder* array (niets om te beschermen); een overloop die onder de canary blijft; alles wat niet normaal return't |
| **NX / W^X** | `-z noexecstack` (de standaard) | Shellcode. De eigen instructies van de payload kunnen niet worden opgehaald | ret2win en ret2libc volledig. Die zijn *de reden* dat ROP bestaat |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (beide standaard) | De hardcoded adressen van ret2win. Alles verschuift bij elke run | Alles waar de aanvaller een lek heeft. ASLR verhoogt de prijs van een exploit; het is geen fix. Merk op dat stack, heap en mmap worden gerandomiseerd, maar de *inhoud* van de hoofd-binary niet — dat is wat ROP-ketens gebruiken |
| **Lek niets** | geen `printf("%p")` naar clients; initialiseer vóór je print | Het informatielek dat ASLR van "duur" naar "gratis" verandert | — |
| **Gebruik geen `printf(user_data)`** | `printf("%s", buf)` in plaats van `printf(buf)` | Format-string-bugs: `%x`-stack-reads, `%n`-willekeurige schrijfbewerkingen — een *andere* weg naar RCE | — |
| **Gebruik geen onbetrouwbare paden** | valideer en `openat()` onder een vaste map | Pad-traversal ([CWE-22](https://cwe.mitre.org/data/definitions/22.html)) | — |
| **CET / shadow stack** | `-fcf-protection=full`, kernel- en CPU-ondersteuning | De `ret` zelf: de shadow stack onthoudt het *echte* retouradres en faalt bij een mismatch. Vangt ROP-ketens die hardware-`ret` gebruiken | Aanvallen die nooit `ret`-en (call-oriented, of het doel van een functiepointer overschrijven met een gadget-keten die geen retour nodig heeft) |
| **Veilige talen** | Rust, Go, C# voor nieuwe code | De hele klasse. Bounds-checks worden tijdens de uitvoering afgedwongen, niet vertrouwd bij review | — |
### Zie het zelf
```sh
make run # kwetsbare daemon
make test # alle drie de technieken werken
make test-hardened # dezelfde broncode, tegenmaatregelen aan
```
`test-hardened` bouwt `food_hardened` met `-fstack-protector-strong -fPIE -pie
-z noexecstack`, wisselt hem in, draait alle drie opnieuw en legt daarna de
kwetsbare terug. Je ziet:
```
### 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)
```
En in de log van de geharde daemon, de canary die afgaat:
```
*** stack smashing detected ***: terminated
```
Lees dat goed, want het is de belangrijkste regel van het hele lab: **de canary
ving ret2win, niet PIE.** Alle drie de technieken sterven bij de canary, omdat
alle drie door dezelfde `read()` gaan en hetzelfde frame beschadigen. NX stopt
alleen nog de *code* van de shellcode; PIE breekt alleen nog het hardcoded
adres. Zet ze één voor één aan, en je ontdekt dat de meeste enkele
tegenmaatregelen je ergens kwetsbaar achterlaten.
---
## Bestanden
| Bestand | Doel |
|---|---|
| `food.c` | de kwetsbare daemon. 6 genummerde bugs, elk met zijn fix in de commentaar |
| `shellcode.S` | de 23 shellcode-bytes als assembly, zodat ze leesbaar en verifieerbaar zijn. `fooc` draagt ze inline en heeft dit tijdens het draaien niet nodig |
| `Makefile` | bouwt, test en de harde vergelijking |
| `tests/pty_test.c` | drijft `fooc` door een pseudo-terminal en controleert op echte shell-output |
| `tests/sock_test.c` | onafhankelijke verificateur over een rauwe socket, zodat het resultaat niet van `fooc` afhangt |
| `food.log` | de log van de daemon. Jouw bewijs van wat er gebeurde |
---
## Twee bugs in dit lab die de moeite van het begrijpen waard zijn
Dit zijn niet de bugs van het doelprogramma. Het zijn bugs in het exploit en in
zijn test-harness, en beide produceerden overtuigende leugens. Ze zijn
gedocumenteerd in de bron waar ze wonen; hier staan ze omdat de faalpatronen
leerzaam zijn.
### Stack-uitlijning: de crash die geen NULL-dereferentie is
**Symptoom.** De overname landt correct — `gdb` laat je in `win()` zien — en
dan sterft het allereerste wat `win()` doet, een `dprintf()`. De
SIGSEGV-handler rapporteert `RIP` diep in glibc's formatter en een foutadres
van `(nil)`, wat er precies uitziet als een corrupte pointer.
**Oorzaak.** De System V AMD64-ABI vereist 16-byte stack-uitlijning. Een
normale `ret` herstelt `%rsp` precies zoals de bijbehorende `call` het
opsloeg, dus de invariant blijft gratis behouden. Onze kale `ret` doet dat
niet: daarna geldt `%rsp = buf + rip_off`. Hier is `buf` 16-byte uitgelijnd en
is `rip_off` 88, dus de callee krijgt een stack die 8 mod 16 is. glibc is met
SSE2 gecompileerd, en `movaps`**faalt** op een verkeerd uitgelijnde operand.
Op x86 werpt dat `#GP` op, niet `#PF`, dus de kernel heeft geen foutadres en
rapporteert `si_addr = 0`. Die NULL is de hint: een uitlijnfout vermomd als
NULL-dereferentie.
**Fix.** Eén `ret`-gadget *op offset `rip_off`*, dat het echte doelwit 8 bytes
omhoog schuift, omdat elke `ret` precies 8 bij `%rsp` optelt. De volgorde is
kritiek: een eerdere versie plakte de `ret`*achter* het doelwit en produceerde
`[ padding | target | ret ]`, waarbij de afsluitende `ret` nooit wordt bereikt
en de fix stilletjes niets doet. Een verdwaalde `ret` die op een bug lijkt, is
bijna altijd bewust.
### Eén socket, twee lezers: de byte die verdween
**Symptoom.** Shellcode werd gerapporteerd als werkend. Toen werd de
pty-harness strenger gemaakt (`ECHO` uitzetten, zodat de terminal ophield de
eigen commandoregel van de harness naar zichzelf terug te echoën), en de
techniek begon te falen. Dieper ging elke techniek precies één byte van het
begin van elke uitvoer-chunk kwijt: `uid=1000(hanez)` werd geprint als
`id=1000(hanez)`, `PWNED-OK` als `WNED-OK`, `Linux 7.2.7` als `inux 7.2.7`.
**Oorzaak.** `fooc` deed vroeger de socket `dup2()`en naar zijn eigen
stdin/stdout en een *lokale*`/bin/sh``execv()`en, terwijl een geforkt
relay-kind dezelfde socket ook las om output naar de terminal te verplaatsen.
De kernel kan het niets schelen dat die twee samenwerken. Een streamsocket heeft
**één** lees-cursor, en elke lezer verplaatst hem, dus bytes worden
onvoorspelbaar tussen hen verdeeld. De lokale shell — een interactieve
login-shell — las precies één byte en gooide het weg, elke keer weer.
`strace -f` liet het onmiddellijk zien:
```
read(0, "u", 1) <- de lokale shell, eet een byte op
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- het relay, 1 byte te kort
```
**Fix.** Er is hier helemaal geen shell aan deze kant. Er is precies één shell
in het hele plaatje, en die zit op het slachtoffer, in het gekaapte proces,
met de TCP-verbinding als zijn stdin/stdout. Deze kant verplaatst alleen bytes.
Als je ooit twee consumenten van een stream nodig hebt, heeft die stream één
enkele lezer nodig die hem bewust demultiplext.
**De meta-les.** Het eerste "werkende" resultaat was een fout-positief,
geproduceerd doordat de pty de eigen commandoregel van de harness terug naar
zichzelf echoëde, en de fix voor dat fout-positief is wat de echte bug
onthulde. Tests die niet kunnen falen, zijn erger dan geen tests, omdat ze "ik
weet het niet" veranderen in "het werkt". Een test-harness verdient hetzelfde
wantrouwen als de code die hij test.
---
## Ermee spelen
Dingen die de moeite waard zijn om te proberen, ongeveer in de volgorde waarin
je er het meest van leert:
1.**Verander `FOOD_BUFSZ` naar 128.** Draai `fooc` opnieuw. Het zou nog
steeds moeten werken zonder wijzigingen, omdat het de offset uit de
disassembly leest. Breek het dan met de hand — hardcode 88 — en zie het
crashen. Voeg daarna een tweede array toe tussen `buf` en de opgeslagen
registers, en zie hoe de automatische herkenning het afhandelt.
2.**Voeg `-Wformat-security` toe en kijk wat het format-string-pad doet.**
Stuur `%p %p %p %n` en zie `food` de stack lekken.
3.**Gebruik gdb.**`make debug`, daarna:
```gdb
(gdb) break food.c:393 # de read() die overloopt
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # merk op waar het retouradres ligt
(gdb) c # in een andere terminal: ./fooc -t ret2win
```
De SIGSEGV-handler logt `REG_RIP` en `REG_RSP`, dus `food.log` vertelt je of
de overname geland is, zelfs wanneer het kind sterft vóór je kunt aanhaken.
4.**Verwijder de uitlijnfix** in `fooc.c` en zie de `#GP`-fout met de
`si_addr = 0`-handtekening. Lees dan `/proc/sys/kernel/randomize_va_space`
en denk na over wat ASLR randomiseert en wat niet.
5.**Breek de libc-symbolresolutie** en zie hoe `fooc` zich aanpast. Het hele
punt van de `/proc/self/maps`-benadering is dat geen enkele offset
hardcoded is.
6.**Schrijf een vierde techniek.** Een `ret2csu`-achtige keten als je
`__libc_csu_init` kunt vinden, of een SROP-keten (`sigreturn`-frames laten
je alle registers tegelijk controleren). Beide zijn puur ROP en hebben geen
uitvoerbaar geheugen nodig.
7.**Fix `food.c` goed**, één bug tegelijk, en draai het exploit opnieuw na
elke fix. De volgorde in de tabel bovenaan `food.c` is ongeveer de juiste om
in te denken: beperk eerst de read, want niets anders doet ertoe voordat de
bug weg is.
---
## Opruiming
```sh
make stop # stopt food
make clean # verwijdert bouwproducten; laat food.log met rust
pkill -x sh # alleen als je losse shells hebt van een test die misging
```
Merk op: `pkill -x food` matcht de proces**naam** precies. Gebruik geen
`pkill -f ./food` — dat patroon matcht ook de shell waarin je het typt en doodt
je eigen sessie. Dat is geen hypothese; het gebeurde terwijl dit lab werd