424 lines
20 KiB
Markdown
424 lines
20 KiB
Markdown
|
|
# food / fooc — een stack-bufferoverloop, van beide kanten
|
||
|
|
|
||
|
|
Een C99-beveiligingslab in twee helften:
|
||
|
|
|
||
|
|
- **`food.c`** — een bewust kwetsbare TCP-daemon. Hij heeft een echte,
|
||
|
|
schoolboekachtige stack-bufferoverloop (CWE-120), plus een paar bugs
|
||
|
|
extra.
|
||
|
|
- **`fooc.c`** — een exploit daarvoor. Hij berekent de overflow-offset door
|
||
|
|
het doelprogramma tijdens het draaien te disassembleren, leest
|
||
|
|
adres-leaks van de daemon en krijgt een shell op het "slachtoffer" door
|
||
|
|
een opgeslagen retouradres te overschrijven.
|
||
|
|
|
||
|
|
Het punt is niet de shell. Het punt is dat je van begin tot eind kunt volgen hoe
|
||
|
|
een geheugenveiligheidsbug uitgroeit tot willekeurige code-uitvoering — en
|
||
|
|
daarna precies ziet welke tegenmaatregelen elke stap in die keten stoppen.
|
||
|
|
Elke regel in beide programma's is gecommentarieerd, want het mechanisme is de
|
||
|
|
les.
|
||
|
|
|
||
|
|
```
|
||
|
|
jouw terminal
|
||
|
|
|
|
||
|
|
./fooc (exploit)
|
||
|
|
|
|
||
|
|
TCP 127.0.0.1:2342
|
||
|
|
|
|
||
|
|
./food (kwetsbare daemon)
|
||
|
|
|
|
||
|
|
fork() -> vulnerable_handler() -> overflow -> ret -> jouw code
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚠️ Lees dit eerst
|
||
|
|
|
||
|
|
**`food` is een bewust kapotte netwerkdienst. Hij bindt alleen aan
|
||
|
|
`127.0.0.1`, en die standaard is bewust — laat hem daar.**
|
||
|
|
|
||
|
|
- Draai hem **niet** op een machine waar je om geeft, of op iets met data.
|
||
|
|
- Bind hem **niet** aan `0.0.0.0` of een echte netwerkinterface. Hij is
|
||
|
|
bewust extern exploiteerbaar.
|
||
|
|
- `fooc` richten op een host die je niet bezit of waarvoor je geen schriftelijke
|
||
|
|
toestemming hebt om te testen, is in de meeste rechtsgebieden een
|
||
|
|
computercriminaliteitsovertreding — ook onder de UK Computer Misuse Act en
|
||
|
|
de US Computer Fraud and Abuse Act.
|
||
|
|
- Hij bindt aan een onbevoordeelde poort (>1024), dus je hebt geen root nodig.
|
||
|
|
"Verbeter" hem niet door capabilities toe te voegen of hem als
|
||
|
|
systeemdienst te draaien.
|
||
|
|
- Elke verbinding wordt afgehandeld in een `fork()`-kind, en `food` reapt
|
||
|
|
het, dus crashes stapelen zich niet op. Vind je daarna tientallen losse
|
||
|
|
`sh`-processen, dan is `pkill -x sh` de opruiming.
|
||
|
|
|
||
|
|
Bij twijfel: dit lab is voor een virtuele machine of container, op een netwerk
|
||
|
|
dat jij beheert, op een machine zonder iets dat je zou missen.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Snelle start
|
||
|
|
|
||
|
|
```sh
|
||
|
|
make # bouwt food, fooc en de test-harnesses
|
||
|
|
make run # start food op 127.0.0.1:2342, losgekoppeld op de achtergrond
|
||
|
|
make test # draait alle drie de exploit-technieken
|
||
|
|
make stop # stopt de daemon
|
||
|
|
```
|
||
|
|
|
||
|
|
Daarna met de hand:
|
||
|
|
|
||
|
|
```sh
|
||
|
|
./fooc -t leak # bekijk de adres-leaks die food prijsgeeft
|
||
|
|
./fooc -t demo -v # stuur rommel; zie food sterven met SIGSEGV
|
||
|
|
./fooc -t ret2win -i # spring naar een functie die al bestaat -> shell
|
||
|
|
```
|
||
|
|
|
||
|
|
### Vereisten
|
||
|
|
|
||
|
|
| Hulpmiddel | Waarvoor | Opmerkingen |
|
||
|
|
|---|---|---|
|
||
|
|
| `gcc` (of clang) | bouwen | C99. Getest met gcc 16.2 |
|
||
|
|
| `objdump` | `fooc` | binutils. `fooc` roept het tijdens het draaien aan |
|
||
|
|
| `nasm` | `make verify` | alleen om de shellcode te verifiëren; wordt overgeslagen als het ontbreekt |
|
||
|
|
| `gdb` | `make debug` | optioneel |
|
||
|
|
| Linux, x86-64 | beide | payload en gadget-jacht zijn architectuurafhankelijk |
|
||
|
|
|
||
|
|
`fooc` heeft ook `-ldl` nodig voor `dlsym()`; de Makefile regelt dat.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## De bug
|
||
|
|
|
||
|
|
Eén regel in `food.c` is het hele aanvalsoppervlak:
|
||
|
|
|
||
|
|
```c
|
||
|
|
char buf[FOOD_BUFSZ]; /* 64 bytes */
|
||
|
|
n = read(fd, buf, FOOD_READMAX); /* tot 512 bytes van het netwerk */
|
||
|
|
```
|
||
|
|
|
||
|
|
64 bytes bestemming, 512 bytes geaccepteerd. De aanvaller overschrijft 448
|
||
|
|
bytes voorbij het einde van de buffer, en omdat de stack naar beneden groeit,
|
||
|
|
betekent "voorbij het einde" "in het frame erboven" — en daar liggen precies de
|
||
|
|
opgeslagen framepointer en de **opgeslagen retouradres**.
|
||
|
|
|
||
|
|
In een gecompileerde x86-64-functie bij `-O0`:
|
||
|
|
|
||
|
|
```
|
||
|
|
hoge adressen
|
||
|
|
+------------------------+ rbp + 16 : locals van de aanroeper
|
||
|
|
| ... |
|
||
|
|
+------------------------+ rbp + 8 : OPGESLAGEN RETOURADRES <-- wordt RIP
|
||
|
|
| saved rbp (8 bytes) |
|
||
|
|
+------------------------+ rbp : onze framepointer
|
||
|
|
| line[128] |
|
||
|
|
| buf[64] | <- rsp: wat read() vult
|
||
|
|
+------------------------+
|
||
|
|
lage adressen
|
||
|
|
```
|
||
|
|
|
||
|
|
Wanneer de functie terugkeert, poppen `leave; ret` de 8 bytes in `RIP`, en de
|
||
|
|
CPU springt waar de aanvaller het heeft bepaald. Al het andere in dit lab is
|
||
|
|
rekenkunde over waarheen je moet wijzen.
|
||
|
|
|
||
|
|
Voor deze build zijn de getallen: `buf` is 64 bytes, de opgeslagen `rbp` is 8,
|
||
|
|
dus het retouradres ligt op offset **88** vanaf het begin van `buf`. `fooc`
|
||
|
|
hardcoded dat niet — het disassembleert `food` en vindt `lea -0x50(%rbp)`
|
||
|
|
vóór `call read@plt`, zodat het blijft werken als je `FOOD_BUFSZ` verandert.
|
||
|
|
|
||
|
|
> gcc vertelt je dit al. `food` bouwen print:
|
||
|
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||
|
|
> destination [-Wstringop-overflow=]`. Onderdruk die waarschuwing nooit in
|
||
|
|
> echte code. Het is gratis beveiliging.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## De drie technieken
|
||
|
|
|
||
|
|
`fooc -t <technique>`. Ze staan in de volgorde waarin een echte aanvaller er
|
||
|
|
doorheen zou werken, omdat elke techniek nodig heeft wat de vorige je leerde.
|
||
|
|
|
||
|
|
### 1. `ret2win` — bestuur de instructiepointer
|
||
|
|
|
||
|
|
```
|
||
|
|
[ 88 bytes rommel ][ het adres van food's win() ]
|
||
|
|
^ saved rbp
|
||
|
|
^ wordt RIP
|
||
|
|
```
|
||
|
|
|
||
|
|
`win()` is een functie in het doelprogramma die `/bin/sh` exec't. Het
|
||
|
|
overschrijven van het retouradres met haar adres is het hele exploit.
|
||
|
|
|
||
|
|
**Wat het leert:** je hebt volledige controle over de instructiepointer. Het
|
||
|
|
heeft ook geen leak nodig, want het binaire bestand is gebouwd met `-no-pie`,
|
||
|
|
dus `win()` staat voor altijd op een vast adres.
|
||
|
|
|
||
|
|
**De tegenhanger in de echte wereld** is niet "aanvallen zijn makkelijk",
|
||
|
|
maar "lever geen ongedocumenteerde backdoors in netwerk-binaries". Zit er een
|
||
|
|
functie als `win()` in jouw binaire bestand, dan zal een bufferoverloop haar
|
||
|
|
vinden. Dat is letterlijk de Juniper ScreenOS-backdoor-CVE-klasse.
|
||
|
|
|
||
|
|
**Verdediging:** `-fPIE` (of ASLR) randomiseert het laadadres, dus de aanvaller
|
||
|
|
moet het adres kennen — wat meestal betekent dat ze eerst een lek nodig hebben.
|
||
|
|
Daarom faalt `ret2win` tegen `food_hardened`.
|
||
|
|
|
||
|
|
### 2. `ret2libc` — roep om het even wat aan, bij naam
|
||
|
|
|
||
|
|
```
|
||
|
|
[ rommel ][ pop rdi; ret ][ adres van "/bin/sh" ][ adres van system() ]
|
||
|
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
zet rdi de string om te sturen de functie om aan te roepen
|
||
|
|
```
|
||
|
|
|
||
|
|
Bij uitvoering: `ret` poppt `pop rdi; ret` in RIP; dat poppt de
|
||
|
|
`"/bin/sh"`-pointer in `RDI`; zijn `ret` poppt `system()` in RIP, terwijl `RDI`
|
||
|
|
de string nog vasthoudt. `system("/bin/sh")` draait.
|
||
|
|
|
||
|
|
Gadgets (`pop rdi; ret`) zitten niet in `food` — deze glibc heeft geen
|
||
|
|
`__libc_csu_init` — dus `fooc` vindt ze door live libc-geheugen te scannen op
|
||
|
|
het bytepaar `5f c3`. Het lokaliseert libc via `/proc/self/maps`, vindt de
|
||
|
|
offsets van `system` en `"/bin/sh"` met `dlsym()` en berekent de base uit het
|
||
|
|
lek dat `food` prijsgeeft. Niets is hardcoded, dus het overleeft een
|
||
|
|
libc-update.
|
||
|
|
|
||
|
|
**Wat het leert:** zodra je RIP kunt controleren, kun je *bestaande*
|
||
|
|
instructies aan elkaar rijgen. Dat is return-oriented programming, en zo ziet
|
||
|
|
bijna elk echt exploit eruit, omdat het geen door de aanvaller geleverd
|
||
|
|
uitvoerbaar geheugen nodig heeft.
|
||
|
|
|
||
|
|
**Verdediging:** geen van de compilerflags stopt dit alleen. Het werkt tegen
|
||
|
|
een PIE-binary, met NX, met canary — zolang de aanvaller een lek heeft. De
|
||
|
|
verdedigingen zijn "heb de overloop niet" en "lek geen adressen". Zie de tabel
|
||
|
|
hieronder.
|
||
|
|
|
||
|
|
### 3. `shellcode` — voer je eigen machinecode uit
|
||
|
|
|
||
|
|
23 bytes, geplaatst aan het begin van de buffer, met `RIP` ernaar wijzend:
|
||
|
|
|
||
|
|
```asm
|
||
|
|
xor esi, esi ; envp = NULL
|
||
|
|
xor edx, edx ; argv = NULL
|
||
|
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" als 8 ruwe bytes
|
||
|
|
push rdi ; leg de string op de stack
|
||
|
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||
|
|
push 0x3b ; 59 = __NR_execve
|
||
|
|
pop rax
|
||
|
|
syscall ; we zijn nu een shell
|
||
|
|
```
|
||
|
|
|
||
|
|
Dit is de puurste vorm van de bug: de aanvaller levert de *instructies*, niet
|
||
|
|
alleen het adres van instructies die al bestaan. Geen libc-offsets nodig, dus
|
||
|
|
het werkt in principe tegen een statisch gelinkt, volledig gerandomiseerd
|
||
|
|
doelwit.
|
||
|
|
|
||
|
|
`make verify` assembleert `shellcode.S` en diff't het tegen de byte-array die
|
||
|
|
in `fooc.c` is ingebed, zodat de twee niet uiteen kunnen drijven.
|
||
|
|
|
||
|
|
**Verdediging:** **NX** (ook wel W^X, "no execute" genoemd). De stack als
|
||
|
|
niet-uitvoerbaar markeren zorgt ervoor dat de hardware weigert er instructies
|
||
|
|
uit te halen, en de `ret` landt op een pagina die niet kan draaien. Daarom
|
||
|
|
geeft `make food` `-z execstack`: een normale Linux-stack is `rw-p`, niet
|
||
|
|
`rwx`, en de techniek sterft met SIGSEGV bij `RIP = het adres van de payload`.
|
||
|
|
De allerbelangrijkste les van het lab is dat elk van deze bytes alleen werkt
|
||
|
|
omdat de compiler opdracht kreeg de stack uitvoerbaar te laten. Dat vlaggetje
|
||
|
|
staat aan voor niemands gewin.
|
||
|
|
|
||
|
|
### Ook inbegrepen
|
||
|
|
|
||
|
|
| Modus | Wat hij doet |
|
||
|
|
|---|---|
|
||
|
|
| `-t leak` | verbindt, print leaks, stuurt niets |
|
||
|
|
| `-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) | — |
|
||
|
|
| **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 |
|
||
|
|
| `fooc.c` | het exploit. objdump-gebaseerde offenderkenning, `/proc`-gebaseerde libc-herkenning, 4 payload-bouwers |
|
||
|
|
| `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
|
||
|
|
gebouwd.
|