| `-t demo` | sendet `rip_off + 8` Bytes `0x41`, sodass `RIP` zu `0x4141...` wird und der Daemon stirbt. Beweist den Bug ganz ohne Adresswissen |
| `-t sled` | ein Ret-Sled, bewusst als **fehlschlagendes** Beispiel behalten. Ohne ein Leak würdest du ASLR brute-forcen, indem du den Puffer mit der Adresse eines `ret` füllst. Hier kann es nicht funktionieren: `food` akzeptiert 512 Bytes, der Sled hat also ~53 Slots gegen ~28 Bit Entropie. Implementiert, damit du zusehen kannst, wie es scheitert, und bestätigst, dass der Mechanismus wirklich „die CPU folgt einer Kette von rets" ist |
---
## Die Tabelle der Gegenmaßnahmen
Das ist der Teil, den man sich merken sollte. Jede Zeile ist eine echte
Verteidigung, und die rechte Spalte zeigt, was sie tatsächlich mit der
Ereigniskette macht.
| Gegenmaßnahme | So aktivierst du sie | Was sie stoppt | Was sie *nicht* stoppt |
|---|---|---|---|
| **Read begrenzen** | `n = read(fd, buf, sizeof buf - 1);` | **Alles.** Der Bug existiert nicht, also ist nichts nachgelagert relevant | Nichts — das ist der einzige vollständige Fix |
| **Stack-Canary** | `-fstack-protector-strong` (gcc-Standard) | Das `ret`: Der Canary wird beim Funktionsende geprüft, der Einschlag wird also erkannt und der Prozess bricht ab, bevor `RIP` gepoppt wird | Ein Bug in einer Funktion *ohne* Array (nichts zu schützen); ein Overflow, der unter dem Canary bleibt; alles, was nicht normal zurückkehrt |
| **NX / W^X** | `-z noexecstack` (der Standard) | Shellcode. Die eigenen Befehle des Payloads können nicht geholt werden | ret2win und ret2libc vollständig. Sie sind der *Grund*, warum ROP existiert |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (beides Standard) | ret2wins fest verdrahtete Adressen. Alles bewegt sich bei jedem Lauf | Alles, wo der Angreifer ein Leak hat. ASLR erhöht die Kosten eines Exploits; es ist kein Fix. Beachte, dass Stack, Heap und mmap randomisiert sind, der *Inhalt* des Haupt-Binärprogramms jedoch nicht — das ist es, was ROP-Ketten verwenden |
| **Nicht leaken** | kein `printf("%p")` an Clients; vor dem Drucken initialisieren | Der Informations-Leak, der ASLR von „teuer" zu „gratis" macht | — |
| **Kein `printf(user_data)`** | `printf("%s", buf)` statt `printf(buf)` | Format-String-Bugs: `%x`-Stack-Reads, `%n`-beliebige Schreibzugriffe — ein *zweiter* Weg zu RCE | — |
| **CET / Shadow Stack** | `-fcf-protection=full`, Kernel- und CPU-Unterstützung | Das `ret` selbst: Der Shadow Stack merkt sich die *echte* Rücksprungadresse und fault bei einem Mismatch. Fängt ROP-Ketten ab, die Hardware-`ret` verwenden | Angriffe, die nie `ret` ausführen (call-oriented, oder das Ziel eines Funktionspointers mit einer Gadget-Kette überschreiben, die keine Rückkehr braucht) |
| **Sichere Sprachen** | Rust, Go, C# für neuen Code | Die ganze Klasse. Bounds-Checks werden zur Laufzeit geprüft, nicht beim Review erhofft | — |
### Selbst ausprobieren
```sh
make run # angreifbarer Daemon
make test # alle drei Techniken funktionieren
make test-hardened # gleicher Quellcode, Gegenmaßnahmen an
```
`test-hardened` baut `food_hardened` mit `-fstack-protector-strong -fPIE -pie
-z noexecstack`, tauscht es ein, führt alle drei erneut aus und legt danach
das angreifbare wieder zurück. Du wirst sehen:
```
### 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)
```
Und im Log des gehärteten Daemons das Auslösen des Canarys:
```
*** stack smashing detected ***: terminated
```
Lies das genau, denn es ist die wichtigste Zeile des ganzen Labors: **der
Canary hat ret2win erwischt, nicht PIE.** Alle drei Techniken sterben am
Canary, weil alle drei durch dasselbe `read()` gehen und denselben Frame
zerstören. NX stoppt nur zusätzlich den *Code* des Shellcodes; PIE bricht nur
zusätzlich die hart verdrahtete Adresse. Schalte sie einzeln an, und du wirst
feststellen, dass dich die meisten einzelnen Gegenmaßnahmen irgendetwas
ausgesetzt lassen.
---
## Dateien
| Datei | Zweck |
|---|---|
| `food.c` | der angreifbare Daemon. 6 nummerierte Bugs, jeder mit seinem Fix im Kommentar |
| `shellcode.S` | die 23 Shellcode-Bytes als Assembly, damit man sie lesen und verifizieren kann. `fooc` trägt sie inline und braucht das zur Laufzeit nicht |
| `Makefile` | baut, testet und liefert den gehärteten Vergleich |
| `tests/pty_test.c` | treibt `fooc` über ein Pseudo-Terminal und prüft auf echte Shell-Ausgabe |
| `tests/sock_test.c` | unabhängiger Verifizierer über einen rohen Socket, damit das Ergebnis nicht von `fooc` abhängt |
| `food.log` | das Log des Daemons. Dein Beweis, was passiert ist |
---
## Zwei Bugs in diesem Labor, die es wert sind, verstanden zu werden
Das sind nicht die Bugs des Zielprogramms. Es sind Bugs im Exploit und in
seiner Test-Harness, und beide haben überzeugende Lügen produziert. Sie sind im
Quellcode an ihrem Ort dokumentiert; hier stehen sie, weil die Ausfallmuster
lehrreich sind.
### Stack-Ausrichtung: der Absturz, der kein NULL-Deref ist
**Symptom.** Die Übernahme landet korrekt — `gdb` zeigt dich in `win()` — und
dann stirbt das allererste, was `win()` tut, ein `dprintf()`. Der
`SIGSEGV`-Handler meldet `RIP` tief im glibc-Formatter und eine Fehleradresse
von `(nil)`, was exakt wie ein korrupter Pointer aussieht.
**Ursache.** Die System-V-AMD64-ABI verlangt 16-Byte-Stack-Ausrichtung. Ein
normales `ret` stellt `%rsp` exakt auf das wieder her, was das zugehörige
`call` gespeichert hat, sodass die Invariante gratis erhalten bleibt. Unser
nacktes `ret` tut das nicht: Danach gilt `%rsp = buf + rip_off`. Hier ist `buf`
16-Byte-ausgerichtet und `rip_off` ist 88, der Callee bekommt also einen Stack,
der 8 mod 16 ist. glibc ist mit SSE2 kompiliert, und `movaps`**fault** bei
einem nicht ausgerichteten Operanden. Auf x86 löst das `#GP` aus, nicht `#PF`,
der Kernel hat also keine Fehleradresse und meldet `si_addr = 0`. Dieses NULL
ist der Hinweis: ein Ausrichtungsfehler, verkleidet als NULL-Deref.
**Fix.** Ein `ret`-Gadget *bei Offset `rip_off`*, das das echte Ziel um 8 Bytes
nach oben verschiebt, denn jedes `ret` addiert exakt 8 auf `%rsp`. Die
Reihenfolge ist entscheidend: Eine frühere Version hängte das `ret`*hinter*
das Ziel an und erzeugte `[ padding | target | ret ]`, wo das abschließende
`ret` nie erreicht wird und der Fix still nichts tut. Ein versprengtes `ret`,
das wie ein Fehler aussieht, ist fast immer Absicht.
### Ein Socket, zwei Leser: das verschwundene Byte
**Symptom.** Shellcode wurde als funktionierend gemeldet. Dann wurde die
pty-Harness strenger gemacht (Abschalten von `ECHO`, sodass das Terminal seine
eigene Befehlszeile nicht mehr zurückspiegelte) und die Technik begann zu
scheitern. Im Kern ließ jede Technik exakt ein Byte vom Anfang jedes
Ausgabeblocks fallen: `uid=1000(hanez)` wurde zu `id=1000(hanez)` gedruckt,
`PWNED-OK` zu `WNED-OK`, `Linux 7.2.7` zu `inux 7.2.7`.
**Ursache.** `fooc` pflegte den Socket per `dup2()` auf sein eigenes
stdin/stdout zu legen und eine *lokale*`/bin/sh` per `execv()` zu starten,
während ein geforktes Relay-Kind denselben Socket ebenfalls las, um die Ausgabe
zum Terminal zu befördern. Dem Kernel ist egal, dass die beiden kooperieren. Ein
Stream-Socket hat **einen** Read-Cursor, und jeder Leser bewegt ihn, sodass
Bytes unvorhersehbar zwischen ihnen aufgeteilt werden. Die lokale Shell las als
interaktive Login-Shell exakt ein Byte und verwarf es — bei jedem einzelnen
Mal. `strace -f` zeigte es sofort:
```
read(0, "u", 1) <- die lokale Shell, frisst ein Byte
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- das Relay, 1 Byte zu wenig
```
**Fix.** Auf dieser Seite gibt es überhaupt keine Shell. Es gibt genau eine
Shell im gesamten Bild, und sie ist auf dem Opfer, im übernommenen Prozess, mit
der TCP-Verbindung als stdin/stdout. Diese Seite bewegt nur Bytes. Wenn du je
zwei Konsumenten eines Streams brauchst, braucht dieser Stream einen einzigen
Leser, der ihn bewusst demultiplexiert.
**Die Meta-Lektion.** Das erste „funktionierende" Ergebnis war ein
Fehlpositiv, das dadurch entstand, dass die pty die eigene Befehlszeile der
Harness zurückwarf, und der Fix für dieses Fehlpositiv ist es, der den echten
Bug bloßlegte. Tests, die nicht scheitern können, sind schlimmer als keine
Tests, weil sie „ich weiß es nicht" in „es funktioniert" verwandeln. Eine
Test-Harness verdient denselben Argwohn wie der Code, den sie testet.
---
## Daran herumexperimentieren
Dinge, die einen Versuch wert sind, ungefähr in der Reihenfolge, in der man
mehr lernt:
1.**Ändere `FOOD_BUFSZ` auf 128.** Führe `fooc` erneut aus. Es sollte ohne
jede Änderung weiter funktionieren, weil es das Offset aus der Disassembly
liest. Brich es dann von Hand — härt 88 ein — und sieh zu, wie es abstürzt.
Füge dann zwischen `buf` und den gespeicherten Registern ein zweites Array
ein und beobachte, wie die automatische Erkennung es verkraftet.
2. **Füge `-Wformat-security` hinzu und schau, was der Format-String-Pfad
tut.** Sende `%p %p %p %n` und beobachte, wie `food` den Stack leakt.
3.**Nutze gdb.**`make debug`, dann:
```gdb
(gdb) break food.c:393 # das read(), das überläuft
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # beachte, wo die Rücksprungadresse liegt
(gdb) c # in einem anderen Terminal: ./fooc -t ret2win
```
Der `SIGSEGV`-Handler loggt `REG_RIP` und `REG_RSP`, sodass dir `food.log`
sagt, ob die Übernahme gelandet ist, selbst wenn das Kind stirbt, bevor du
dich anhängen kannst.
4.**Lösche den Ausrichtungs-Fix** in `fooc.c` und beobachte den `#GP`-Fault
mit der `si_addr = 0`-Signatur. Lies dann
`/proc/sys/kernel/randomize_va_space` und denke darüber nach, was ASLR
randomisiert und was nicht.
5.**Brich die libc-Symbolauflösung** und beobachte, wie `fooc` sich anpasst.
Der ganze Sinn des `/proc/self/maps`-Ansatzes ist, dass kein Offset hart
verdrahtet ist.
6.**Schreibe eine vierte Technik.** Eine `ret2csu`-artige Kette, wenn du
`__libc_csu_init` findest, oder eine SROP-Kette (`sigreturn`-Frames lassen
dich alle Register gleichzeitig kontrollieren). Beides ist reines ROP und
braucht keinen ausführbaren Speicher.
7.**Fixe `food.c` richtig**, Bug für Bug, und führe den Exploit nach jedem
Fix erneut aus. Die Reihenfolge in der Tabelle am Anfang von `food.c` ist
ungefähr die richtige Reihenfolge zum Nachdenken: begrenze zuerst das read,
denn nichts anderes zählt, bis der Bug weg ist.
---
## Aufräumen
```sh
make stop # stoppt food
make clean # entfernt Build-Produkte; lässt food.log in Ruhe
pkill -x sh # nur, wenn du streunende Shells aus einem schiefgelaufenen Test hast
```
Beachte: `pkill -x food` matcht den Prozess**namen** exakt. Verwende nicht
`pkill -f ./food` — dieses Muster matcht auch die Shell, in die du es getippt
hast, und tötet deine eigene Session. Das ist keine Hypothese; es ist beim Bau