foo/README.DK.md
2026-09-29 09:39:24 +02:00

414 lines
No EOL
19 KiB
Markdown

# food / fooc — et stack-bufferoverløb, fra begge sider
Et C99-sikkerhedslaboratorium i to halvdele:
- **`food.c`** — en bevidst sårbar TCP-daemon. Den har et ægte,
lærebogsagtigt stack-bufferoverløb (CWE-120) og et par fejl oveni.
- **`fooc.c`** — et exploit til den. Det beregner overflow-offsettet ved at
disassemblere target-programmet ved kørsel, læser adresse-leaks fra daemonen
og får en shell på "offeret" ved at overskrive en gemt returadresse.
Pointen er ikke shellen. Pointen er, at du kan følge med hele vejen, hvordan
en hukommelsessikkerhedsfejl bliver til vilkårlig kodeudførelse — og derefter
se præcist, hvilke modforanstaltninger der stopper hvert trin i den kæde. Hver
linje i begge programmer er kommenteret, fordi mekanismen er lektionen.
```
din terminal
|
./fooc (exploit)
|
TCP 127.0.0.1:2342
|
./food (sårbar daemon)
|
fork() -> vulnerable_handler() -> overflow -> ret -> din kode
```
---
## ⚠️ Læs dette først
**`food` er en bevidst ødelagt netværkstjeneste. Den binder kun til
`127.0.0.1`, og den standard er bevidst — lad den være der.**
- Kør den **ikke** på en maskine, du holder af, eller på noget med data på.
- Bind den **ikke** til `0.0.0.0` eller en rigtig netværksgrænseflade. Den er
bevidst eksternt udnyttelig.
- At rette `fooc` mod en host, du ikke ejer eller ikke har skriftlig tilladelse
til at teste, er en computerindbrudsforseelse i de fleste jurisdiktioner —
også efter UK Computer Misuse Act og US Computer Fraud and Abuse Act.
- Den binder til en uprivilegeret port (>1024), så du behøver ikke root. Forbedr
den ikke ved at tilføje capabilities eller køre den som systemtjeneste.
- Hver forbindelse håndteres i et `fork()`et barn, og `food` reaper det, så
nedbrud hober sig ikke op. Hvis du bagefter finder dusinvis af strejfende
`sh`-processer, er `pkill -x sh` oprydningen.
I tvivlstilfælde: Dette laboratorium er til en virtuel maskine eller container,
på et netværk du kontrollerer, på en maskine uden noget, du ville savne.
---
## Hurtig start
```sh
make # bygger food, fooc og test-harnessene
make run # starter food på 127.0.0.1:2342, frakoblet i baggrunden
make test # kører alle tre exploit-teknikker
make stop # stopper daemonen
```
Derefter i hånden:
```sh
./fooc -t leak # se de adresse-leaks, food udleverer
./fooc -t demo -v # send junk; se food dø med SIGSEGV
./fooc -t ret2win -i # hop til en funktion, der allerede findes -> shell
```
### Krav
| Værktøj | Hvortil | Bemærkninger |
|---|---|---|
| `gcc` (eller clang) | bygning | C99. Testet med gcc 16.2 |
| `objdump` | `fooc` | binutils. `fooc` kalder det ved kørsel |
| `nasm` | `make verify` | kun til at krydstjekke shellcoden; springes over, hvis ikke til stede |
| `gdb` | `make debug` | valgfrit |
| Linux, x86-64 | begge | payload og gadget-jagt er arkitekturafhængige |
`fooc` har også brug for `-ldl` til `dlsym()`; Makefile'et klarer det.
---
## Fejlen
Én linje i `food.c` er hele angrebsfladen:
```c
char buf[FOOD_BUFSZ]; /* 64 bytes */
n = read(fd, buf, FOOD_READMAX); /* op til 512 bytes fra netværket */
```
64 bytes destination, 512 bytes accepteret. Angriberen overskriver 448 bytes
forbi enden af bufferen, og fordi stacken vokser nedad, betyder "forbi enden"
"ind i det ovenstående frame" — og det er præcis der, den gemte
framepointer og den **gemte returadresse** ligger.
I en kompileret x86-64-funktion ved `-O0`:
```
høje adresser
+------------------------+ rbp + 16 : callerens lokale
| ... |
+------------------------+ rbp + 8 : GEMT RETURADRESSE <-- bliver til RIP
| saved rbp (8 bytes) |
+------------------------+ rbp : vores framepointer
| line[128] |
| buf[64] | <- rsp: det, read() fylder
+------------------------+
lave adresser
```
Når funktionen returnerer, popper `leave; ret` de 8 bytes ind i `RIP`, og CPU'en
hopper, hvor angriberen har bestemt. Alt andet i dette laboratorium er
aritmetik om, hvorhen der skal peges.
For denne build er tallene: `buf` er 64 bytes, det gemte `rbp` er 8, så
returadressen ligger på offset **88** fra starten af `buf`. `fooc` hardkoder
ikke det — det disassemblerer `food` og finder `lea -0x50(%rbp)` foran
`call read@plt`, så det fortsat virker, hvis du ændrer `FOOD_BUFSZ`.
> gcc fortæller dig allerede om det. At bygge `food` printer:
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
> destination [-Wstringop-overflow=]`. Undertryk aldrig den advarsel i ægte
> kode. Den er gratis sikkerhed.
---
## De tre teknikker
`fooc -t <technique>`. De står i den rækkefølge, en ægte angriber ville
arbejde sig igennem dem, fordi hver enkelt har brug for det, den forrige lærte
dig.
### 1. `ret2win` — kontrollér instruktionsmarkøren
```
[ 88 bytes junk ][ adressen på food's win() ]
^ saved rbp
^ bliver til RIP
```
`win()` er en funktion i target-programmet, der exec'er `/bin/sh`. At
overskrive returadressen med dens adresse er hele exploitet.
**Hvad det lærer:** du har vilkårlig kontrol over instruktionsmarkøren. Det
kræver heller ikke noget leak, fordi binærfilen er bygget `-no-pie`, så `win()`
sidder på en fast adresse for evigt.
**Den virkelige verdens ækvivalent** er ikke "angreb er nemme", men "skib ikke
udokumenterede bagdøre i netværks-binærfiler". Hvis en funktion som `win()`
findes i din binærfil, vil et bufferoverløb finde den. Det er bogstaveligt talt
Juniper ScreenOS-bagdør-CVE-klassen.
**Forsvar:** `-fPIE` (eller ASLR) randomiserer load-adressen, så angriberen må
kende adressen — hvilket som regel betyder, at de først har brug for et leak.
Derfor fejler `ret2win` mod `food_hardened`.
### 2. `ret2libc` — kald hvad som helst, ved navn
```
[ junk ][ pop rdi; ret ][ adressen på "/bin/sh" ][ adressen på system() ]
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
sætter rdi strengen at sende funktionen at kalde
```
Ved udførelse: `ret` popper `pop rdi; ret` ind i RIP; det popper
`"/bin/sh"`-pointeren ind i `RDI`; dets `ret` popper `system()` ind i RIP, mens
`RDI` stadig holder strengen. `system("/bin/sh")` kører.
Gadgets (`pop rdi; ret`) er ikke i `food` — denne glibc har intet
`__libc_csu_init` — så `fooc` finder dem ved at scanne live libc-hukommelse
efter byteparret `5f c3`. Det lokaliserer libc via `/proc/self/maps`, finder
offsets for `system` og `"/bin/sh"` med `dlsym()` og beregner basen ud fra det
leak, `food` offentliggør. Intet er hardkodet, så det overlever en
libc-opdatering.
**Hvad det lærer:** når du først kan kontrollere `RIP`, kan du kæde
*eksisterende* instruktioner sammen. Det er return-oriented programming, og det
er sådan næsten alle virkelige exploits ser ud, fordi det ikke kræver
angriberleveret eksekverbar hukommelse.
**Forsvar:** ingen af compiler-flagene stopper det alene. Det virker mod en
PIE-binærfil, med NX, med canary — så længe angriberen har et leak.
Forsvarene er "hav ikke overløbet" og "læk ikke adresser". Se tabellen nedenfor.
### 3. `shellcode` — kør din egen maskinkode
23 bytes, placeret i starten af bufferen, med `RIP` pegende på dem:
```asm
xor esi, esi ; envp = NULL
xor edx, edx ; argv = NULL
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" som 8 rå bytes
push rdi ; læg strengen på stacken
mov rdi, rsp ; rdi = &"/bin/sh"
push 0x3b ; 59 = __NR_execve
pop rax
syscall ; vi er nu en shell
```
Det er den reneste form for fejlen: angriberen leverer *instruktionerne*, ikke
bare adressen på instruktioner, der allerede findes. Ingen libc-offsets
nødvendige, så det virker i princippet mod et statisk linket, fuldt
randomiseret target.
`make verify` assemblerer `shellcode.S` og diff'er det mod byte-arrayet, der er
indlejret i `fooc.c`, så de to ikke kan drive fra hinanden.
**Forsvar:** **NX** (også kaldet W^X, "no execute"). At markere stacken som
ikke-eksekverbar får hardwaren til at nægte at hente instruktioner fra den, og
`ret`-et lander på en side, der ikke kan køre. Det er derfor `make food`
giver `-z execstack`: en normal Linux-stack er `rw-p`, ikke `rwx`, og teknikken
dør med SIGSEGV ved `RIP = payloadens adresse`. Den absolut vigtigste lektion i
laboratoriet er, at hver eneste af disse bytes kun virker, fordi compileren fik
besked på at lade stacken være eksekverbar. Det flag er tændt til gavn for
ingen.
### Også inkluderet
| Tilstand | Hvad den gør |
|---|---|
| `-t leak` | forbinder, printer leaks, sender intet |
| `-t demo` | sender `rip_off + 8` bytes `0x41`, så `RIP` bliver `0x4141...` og daemonen dør. Beviser fejlen helt uden adresseviden |
| `-t sled` | et ret-sled, bevidst beholdt som et **fejlende** eksempel. Uden et leak ville du brute-force ASLR ved at fylde bufferen med adressen på et `ret`. Det kan ikke virke her: `food` accepterer 512 bytes, så sleden har ~53 slots mod ~28 bit entropi. Implementeret, så du kan se det fejle og bekræfte, at mekanismen virkelig er "CPU'en følger en kæde af rets" |
---
## Tabellen over modforanstaltninger
Det er den del, man skal huske. Hver række er et ægte forsvar, og højre kolonne
viser, hvad den rent faktisk gør ved begivenhedskæden.
| Modforanstaltning | Sådan aktiveres | Hvad den stopper | Hvad den *ikke* stopper |
|---|---|---|---|
| **Begræns read** | `n = read(fd, buf, sizeof buf - 1);` | **Alt.** Fejlen findes ikke, så intet nedstrøms betyder noget | Intet — det er den eneste fuldstændige fix |
| **Stack-canary** | `-fstack-protector-strong` (gccs standard) | `ret`-et: canaryen tjekkes ved funktionens afslutning, så smadringen opdages, og processen abort'er, før `RIP` poppes | En fejl i en funktion *uden* array (intet at beskytte); et overflow, der holder sig under canaryen; alt, der ikke returnerer normalt |
| **NX / W^X** | `-z noexecstack` (standarden) | Shellcode. Payloadens egne instruktioner kan ikke hentes | ret2win og ret2libc fuldstændigt. De er *grunden* til, at ROP findes |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (begge standard) | ret2wins hardkodede adresser. Alt flytter sig ved hver kørsel | Alt, hvor angriberen har et leak. ASLR hæver prisen på et exploit; det er ikke en fix. Bemærk, at stack, heap og mmap randomiseres, men hoved-binærfilens *indhold* gør ikke — det er det, ROP-kæder bruger |
| **Læk ikke** | ingen `printf("%p")` til klienter; initialisér før du printer | Det informationsleak, der gør ASLR til "gratis" i stedet for "dyrt" | — |
| **Brug ikke `printf(user_data)`** | `printf("%s", buf)` i stedet for `printf(buf)` | Format-string-fejl: `%x`-stack-reads, `%n`-vilkårlige skrivninger, hvilket er en *anden* vej til RCE | — |
| **Brug ikke utroverdige stier** | validér og `openat()` under en fast mappe | Sti-traversal (CWE-22) | — |
| **CET / shadow stack** | `-fcf-protection=full`, kernel- og CPU-understøttelse | `ret`-et selv: shadow stacken husker den *rigtige* returadresse og fault'er ved mismatch. Fanger ROP-kæder, der bruger hardware-`ret` | Angreb, der aldrig `ret` (call-oriented, eller at overskrive en funktionspegers mål med en gadget-kæde, der ikke behøver en retur) |
| **Sikre sprog** | Rust, Go, C# til ny kode | Hele klassen. Bounds-tjek udføres ved kørsel, ikke håbet på ved review | — |
### Se det selv
```sh
make run # sårbar daemon
make test # alle tre teknikker virker
make test-hardened # samme kildekode, modforanstaltninger på
```
`test-hardened` bygger `food_hardened` med `-fstack-protector-strong -fPIE -pie
-z noexecstack`, bytter den ind, kører alle tre igen og lægger derefter den
sårbare tilbage. Du vil se:
```
### 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)
```
Og i den hærdede daemons log, canaryen der udløses:
```
*** stack smashing detected ***: terminated
```
Læs det grundigt, for det er den vigtigste linje i hele laboratoriet: **canaryen
fangede ret2win, ikke PIE.** Alle tre teknikker dør ved canaryen, fordi alle tre
går gennem det samme `read()` og smadrer den samme frame. NX stopper kun
yderligere shellcodens *kode*; PIE bryder kun yderligere den hardkodede adresse.
Tænd dem enkeltvis, og du vil opdage, at de fleste enkelte modforanstaltninger
efterlader dig udsat over for noget.
---
## Filer
| Fil | Formål |
|---|---|
| `food.c` | den sårbare daemon. 6 nummererede fejl, hver med sin fix i kommentaren |
| `fooc.c` | exploitet. objdump-baseret offseterkendelse, `/proc`-baseret libc-erkendelse, 4 payload-buildere |
| `shellcode.S` | de 23 shellcode-bytes som assembly, så de kan læses og verificeres. `fooc` bærer dem inline og behøver ikke dette ved kørsel |
| `Makefile` | bygger, tester og den hærdede sammenligning |
| `tests/pty_test.c` | driver `fooc` gennem et pseudo-terminal og tjekker for ægte shell-output |
| `tests/sock_test.c` | uafhængig verifikator over en rå socket, så resultatet ikke afhænger af `fooc` |
| `food.log` | daemonens log. Dit bevis på, hvad der skete |
---
## To fejl i dette laboratorium, der er værd at forstå
Det er ikke target-programmets fejl. Det er fejl i exploitet og i dets
test-harness, og begge producerede overbevisende løgne. De er dokumenteret i
kilden, hvor de bor; her står de, fordi fiaskomønstrene er lærerige.
### Stack-justering: nedbruddet, der ikke er en NULL-dereference
**Symptom.** Kapringen lander korrekt — `gdb` viser dig i `win()` — og så dør
det allerførste, `win()` gør, et `dprintf()`. `SIGSEGV`-handleren rapporterer
`RIP` dybt inde i glibcs formatter og en fejladresse på `(nil)`, hvilket ser
præcis ud som en korrupt pointer.
**Årsag.** System V AMD64-ABI'en kræver 16-byte stack-justering. Et normalt
`ret` genskaber `%rsp` præcis som det tilsvarende `call` gemte det, så
invarianten bevares gratis. Vores nøgne `ret` gør ikke: efter det gælder
`%rsp = buf + rip_off`. Her er `buf` 16-byte justeret og `rip_off` er 88, så
callee'en får en stack, der er 8 mod 16. glibc er kompileret med SSE2, og
`movaps` **fault'er** ved et fejljusteret operand. På x86 rejser det `#GP`, ikke
`#PF`, så kernen har ingen fejladresse og rapporterer `si_addr = 0`. Det NULL
er fingerpeg: en justeringsfejl forklædt som en NULL-dereference.
**Fix.** Et `ret`-gadget *ved offset `rip_off`*, der flytter det rigtige target
8 bytes op, fordi hvert `ret` lægger præcis 8 til `%rsp`. Rækkefølgen er
kritisk: en tidligere version hæftede `ret`-et *efter* target og producerede
`[ padding | target | ret ]`, hvor det afsluttende `ret` aldrig nås, og fixen
stille og roligt intet gør. Et vildfarent `ret`, der ligner en fejl, er næsten
altid bevidst.
### Én socket, to læsere: det byte, der forsvandt
**Symptom.** Shellcode blev rapporteret som fungerende. Derefter blev
pty-harnessen gjort strengere (slukning af `ECHO`, så terminalen holdt op med at
ekko harnessens egen kommandolinje tilbage til sig selv), og teknikken begyndte
at fejle. Dybere set tabte hver teknik præcis ét byte fra starten af hver
udgangschunk: `uid=1000(hanez)` blev printet som `id=1000(hanez)`, `PWNED-OK`
som `WNED-OK`, `Linux 7.2.7` som `inux 7.2.7`.
**Årsag.** `fooc` plejede at `dup2()`e socket'en på sit eget stdin/stdout og
`execv()`e en *lokal* `/bin/sh`, mens et forket relay-barn også læste den samme
socket for at flytte output til terminalen. Kernen er ligeglad med, at de to
samarbejder. En streamsocket har **én** læse-cursor, og hver læser flytter den,
så bytes deles uforudsigeligt mellem dem. Den lokale shell, der er en
interaktiv login-shell, læste præcis ét byte og smed det væk — hver eneste
gang. `strace -f` viste det øjeblikkeligt:
```
read(0, "u", 1) <- den lokale shell, æder et byte
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- relay'et, 1 byte for kort
```
**Fix.** Der er slet ingen shell på denne side. Der er præcis én shell i hele
billedet, og den er på offeret, inde i den kaprede proces, med
TCP-forbindelsen som dens stdin/stdout. Denne side flytter kun bytes. Hvis du
nogensinde har brug for to forbrugere af en stream, skal den stream have én
eneste læser, der bevidst demultiplekser den.
**Meta-lektionen.** Det første "fungerende" resultat var et falsk positivt,
produceret af at pty'en ekkoede harnessens egen kommandolinje tilbage til den,
og fixen for det falske positive er det, der afslørede den rigtige fejl. Tests,
der ikke kan fejle, er værre end ingen tests, fordi de forvandler "jeg ved
ikke" til "det virker". En test-harness fortjener samme mistænksomhed som den
kode, den tester.
---
## At pille ved det
Ting, der er værd at prøve, nogenlunde i den rækkefølge, du lærer mest af dem:
1. **Ændr `FOOD_BUFSZ` til 128.** Kør `fooc` igen. Det burde stadig virke uden
ændringer, fordi det læser offset ud af disassembly'en. Bræk det så i
hånden — hardkod 88 — og se det crashe. Tilføj derefter et andet array
mellem `buf` og de gemte registre, og se den automatiske erkendelse klare
det.
2. **Tilføj `-Wformat-security` og se, hvad format-string-stien gør.** Send
`%p %p %p %n` og se `food` lække stacken.
3. **Brug gdb.** `make debug`, derefter:
```gdb
(gdb) break food.c:393 # det read(), der løber over
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # bemærk, hvor returadressen ligger
(gdb) c # i et andet terminal: ./fooc -t ret2win
```
`SIGSEGV`-handleren logger `REG_RIP` og `REG_RSP`, så `food.log` fortæller
dig, om kapringen landede, selv når barnet dør, før du kan koble på.
4. **Slet justeringsfixen** i `fooc.c` og se `#GP`-fejlen med
`si_addr = 0`-signaturen. Læs derefter `/proc/sys/kernel/randomize_va_space`
og tænk over, hvad ASLR randomiserer, og hvad det ikke gør.
5. **Bræk libc-symbolopløsningen** og se `fooc` tilpasse sig. Hele pointen med
`/proc/self/maps`-tilgangen er, at intet offset er hardkodet.
6. **Skriv en fjerde teknik.** En `ret2csu`-lignende kæde, hvis du kan finde
`__libc_csu_init`, eller en SROP-kæde (`sigreturn`-frames lader dig
kontrollere alle registre på én gang). Begge er rent ROP og behøver ingen
eksekverbar hukommelse.
7. **Fix `food.c` ordentligt**, én fejl ad gangen, og kør exploitet igen efter
hver fix. Rækkefølgen i tabellen øverst i `food.c` er nogenlunde den rigtige
rækkefølge at tænke i: begræns først read'et, for intet andet betyder noget,
før fejlen er væk.
---
## Oprydning
```sh
make stop # stopper food
make clean # fjerner build-produkter; lader food.log være i fred
pkill -x sh # kun hvis du har strejfende shells fra en test, der gik skævt
```
Bemærk: `pkill -x food` matcher proces**navnet** præcist. Brug ikke
`pkill -f ./food` — det mønster matcher også den shell, du har skrevet det i,
og dræber din egen session. Det er ikke en hypotese; det skete, mens dette
laboratorium blev bygget.