436 lines
21 KiB
Markdown
436 lines
21 KiB
Markdown
|
|
# food / fooc — ein Stack-Pufferüberlauf, von beiden Seiten
|
||
|
|
|
||
|
|
Ein C99-Sicherheitslabor in zwei Hälften:
|
||
|
|
|
||
|
|
- **`food.c`** — ein absichtlich angreifbarer TCP-Daemon. Er enthält einen
|
||
|
|
echten, lehrbuchreifen Stack-Pufferüberlauf (CWE-120) und nebenbei noch ein
|
||
|
|
paar weitere Bugs.
|
||
|
|
- **`fooc.c`** — ein Exploit dafür. Er berechnet das Overflow-Offset, indem er
|
||
|
|
das Zielprogramm zur Laufzeit disassembliert, liest Adress-Leaks vom Daemon
|
||
|
|
und erhält eine Shell auf dem „Opfer", indem er eine gespeicherte
|
||
|
|
Rücksprungadresse überschreibt.
|
||
|
|
|
||
|
|
Es geht nicht um die Shell. Es geht darum, dass man Ende-zu-Ende mitverfolgen
|
||
|
|
kann, wie aus einem Speichersicherheitsfehler eine beliebige Codeausführung
|
||
|
|
wird — und dann genau sieht, welche Gegenmaßnahmen welchen Schritt dieser
|
||
|
|
Kette stoppen. Jede Zeile beider Programme ist kommentiert, denn der Mechanismus
|
||
|
|
ist die Lektion.
|
||
|
|
|
||
|
|
```
|
||
|
|
dein Terminal
|
||
|
|
|
|
||
|
|
./fooc (Exploit)
|
||
|
|
|
|
||
|
|
TCP 127.0.0.1:2342
|
||
|
|
|
|
||
|
|
./food (angreifbarer Daemon)
|
||
|
|
|
|
||
|
|
fork() -> vulnerable_handler() -> Overflow -> ret -> dein Code
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ⚠️ Bitte zuerst lesen
|
||
|
|
|
||
|
|
**`food` ist ein absichtlich kaputter Netzwerkdienst. Er bindet ausschließlich
|
||
|
|
an `127.0.0.1`, und dieser Standard ist Absicht — bitte lass ihn so.**
|
||
|
|
|
||
|
|
- Führe ihn **nicht** auf einer Maschine aus, die dir wichtig ist, oder auf
|
||
|
|
irgendetwas mit Daten darauf.
|
||
|
|
- Binde ihn **nicht** an `0.0.0.0` oder eine echte Netzwerkschnittstelle. Er
|
||
|
|
ist bewusst remote ausnutzbar.
|
||
|
|
- Ein `fooc` gegen einen Host zu richten, der dir nicht gehört bzw. für den du
|
||
|
|
keine schriftliche Testgenehmigung hast, ist in den meisten Rechtsordnungen
|
||
|
|
ein Computersabotage-Straftatbestand — auch nach dem UK Computer Misuse Act
|
||
|
|
und dem US Computer Fraud and Abuse Act.
|
||
|
|
- Er bindet einen unprivilegierten Port (>1024), du brauchst also kein root.
|
||
|
|
„Verbessere" ihn nicht, indem du Capabilities hinzufügst oder ihn als
|
||
|
|
Systemdienst laufen lässt.
|
||
|
|
- Jede Verbindung wird in einem per `fork()` erzeugten Kindprozess behandelt,
|
||
|
|
und `food` reaped ihn, sodass sich keine Abstürze ansammeln. Falls du danach
|
||
|
|
dutzende streunende `sh`-Prozesse vorfindest, ist `pkill -x sh` die
|
||
|
|
Aufräumlösung.
|
||
|
|
|
||
|
|
Im Zweifel: Dieses Labor ist für eine virtuelle Maschine oder einen Container
|
||
|
|
gedacht, in einem Netzwerk, das du kontrollierst, auf einer Maschine, auf der
|
||
|
|
dir nichts fehlen würde.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Schnellstart
|
||
|
|
|
||
|
|
```sh
|
||
|
|
make # baut food, fooc und die Test-Harnesses
|
||
|
|
make run # startet food auf 127.0.0.1:2342, abgelöst im Hintergrund
|
||
|
|
make test # führt alle drei Exploit-Techniken aus
|
||
|
|
make stop # stoppt den Daemon
|
||
|
|
```
|
||
|
|
|
||
|
|
Danach von Hand:
|
||
|
|
|
||
|
|
```sh
|
||
|
|
./fooc -t leak # sieh dir die Adress-Leaks an, die food ausgibt
|
||
|
|
./fooc -t demo -v # sende Datenmüll; beobachte, wie food mit SIGSEGV stirbt
|
||
|
|
./fooc -t ret2win -i # springe zu einer Funktion, die bereits existiert -> Shell
|
||
|
|
```
|
||
|
|
|
||
|
|
### Voraussetzungen
|
||
|
|
|
||
|
|
| Werkzeug | Wofür | Hinweise |
|
||
|
|
|---|---|---|
|
||
|
|
| `gcc` (oder clang) | Bauen | C99. Getestet mit gcc 16.2 |
|
||
|
|
| `objdump` | `fooc` | binutils. `fooc` ruft es zur Laufzeit auf |
|
||
|
|
| `nasm` | `make verify` | nur zum Gegenprüfen des Shellcodes; wird übersprungen, wenn nicht vorhanden |
|
||
|
|
| `gdb` | `make debug` | optional |
|
||
|
|
| Linux, x86-64 | beides | Payload und Gadget-Suche sind architekturspezifisch |
|
||
|
|
|
||
|
|
`fooc` benötigt außerdem `-ldl` für `dlsym()`; das erledigt das Makefile.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Der Bug
|
||
|
|
|
||
|
|
Eine Zeile in `food.c` ist die gesamte Angriffsfläche:
|
||
|
|
|
||
|
|
```c
|
||
|
|
char buf[FOOD_BUFSZ]; /* 64 Bytes */
|
||
|
|
n = read(fd, buf, FOOD_READMAX); /* bis zu 512 Bytes aus dem Netzwerk */
|
||
|
|
```
|
||
|
|
|
||
|
|
64 Bytes Ziel, 512 Bytes akzeptiert. Der Angreifer überschreibt 448 Bytes über
|
||
|
|
das Ende des Puffers hinaus, und weil der Stack nach unten wächst, bedeutet
|
||
|
|
„über das Ende hinaus" „in den darüberliegenden Frame hinein" — und genau dort
|
||
|
|
liegen der gespeicherte Frame-Pointer und die **gespeicherte
|
||
|
|
Rücksprungadresse**.
|
||
|
|
|
||
|
|
In einer kompilierten x86-64-Funktion bei `-O0`:
|
||
|
|
|
||
|
|
```
|
||
|
|
hohe Adressen
|
||
|
|
+------------------------+ rbp + 16 : Locals des Aufrufers
|
||
|
|
| ... |
|
||
|
|
+------------------------+ rbp + 8 : GESPEICHERTE RÜCKSPRUNGSADRESSE <-- wird zu RIP
|
||
|
|
| saved rbp (8 Bytes) |
|
||
|
|
+------------------------+ rbp : unser Frame-Pointer
|
||
|
|
| line[128] |
|
||
|
|
| buf[64] | <- rsp: das, was read() füllt
|
||
|
|
+------------------------+
|
||
|
|
niedrige Adressen
|
||
|
|
```
|
||
|
|
|
||
|
|
Wenn die Funktion zurückkehrt, poppt `leave; ret` diese 8 Bytes in `RIP`, und
|
||
|
|
die CPU springt dorthin, wo der Angreifer es bestimmt hat. Alles andere in
|
||
|
|
diesem Labor ist Arithmetik darüber, wohin gedeutet werden soll.
|
||
|
|
|
||
|
|
Für diesen Build sind die Zahlen: `buf` ist 64 Bytes, das gespeicherte `rbp`
|
||
|
|
ist 8, die Rücksprungadresse liegt also bei Offset **88** vom Anfang von `buf`.
|
||
|
|
`fooc` härtet das nicht ein — es disassembliert `food` und findet das
|
||
|
|
`lea -0x50(%rbp)` vor dem `call read@plt`, sodass es weiter funktioniert, wenn
|
||
|
|
du `FOOD_BUFSZ` änderst.
|
||
|
|
|
||
|
|
> gcc weist bereits darauf hin. Das Bauen von `food` druckt:
|
||
|
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||
|
|
> destination [-Wstringop-overflow=]`. Unterdrücke diese Warnung in echtem Code
|
||
|
|
> nie. Sie ist geschenkte Sicherheit.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Die drei Techniken
|
||
|
|
|
||
|
|
`fooc -t <technique>`. Sie stehen in der Reihenfolge, in der ein echter
|
||
|
|
Angreifer sie durcharbeiten würde, denn jede braucht, was die vorherige dich
|
||
|
|
gelehrt hat.
|
||
|
|
|
||
|
|
### 1. `ret2win` — den Befehlszeiger kontrollieren
|
||
|
|
|
||
|
|
```
|
||
|
|
[ 88 Bytes Müll ][ Adresse von food's win() ]
|
||
|
|
^ saved rbp
|
||
|
|
^ wird zu RIP
|
||
|
|
```
|
||
|
|
|
||
|
|
`win()` ist eine Funktion im Zielprogramm, die `/bin/sh` ausführt. Das
|
||
|
|
Überschreiben der Rücksprungadresse mit ihrer Adresse ist der gesamte Exploit.
|
||
|
|
|
||
|
|
**Was es lehrt:** Du hast beliebige Kontrolle über den Befehlszeiger. Es
|
||
|
|
braucht außerdem kein Leak, weil das Binärprogramm `-no-pie` gebaut ist, sodass
|
||
|
|
`win()` für immer an einer festen Adresse sitzt.
|
||
|
|
|
||
|
|
**Das reale Äquivalent** ist nicht „Angriffe sind einfach", sondern „verschiffe
|
||
|
|
keine undokumentierten Hintertüren in Netzwerk-Binärprogrammen". Wenn eine
|
||
|
|
Funktion wie `win()` in deinem Binärprogramm existiert, wird ein
|
||
|
|
Pufferüberlauf sie finden. Das ist wörtlich die Hintertür-Klasse von Juniper
|
||
|
|
ScreenOS (CVE).
|
||
|
|
|
||
|
|
**Verteidigung:** `-fPIE` (oder ASLR) randomisiert die Ladeadresse, sodass der
|
||
|
|
Angreifer die Adresse kennen muss — was meistens bedeutet, dass er zuerst ein
|
||
|
|
Leak braucht. Deshalb schlägt `ret2win` gegen `food_hardened` fehl.
|
||
|
|
|
||
|
|
### 2. `ret2libc` — Beliebiges aufrufen, beim Namen
|
||
|
|
|
||
|
|
```
|
||
|
|
[ Müll ][ pop rdi; ret ][ Adresse von "/bin/sh" ][ Adresse von system() ]
|
||
|
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||
|
|
setzt rdi der zu übergebende String die aufzurufende Funktion
|
||
|
|
```
|
||
|
|
|
||
|
|
Zur Ausführungszeit: `ret` poppt `pop rdi; ret` in `RIP`; das poppt den
|
||
|
|
`"/bin/sh"`-Pointer in `RDI`; dessen `ret` poppt `system()` in `RIP`, wobei
|
||
|
|
`RDI` weiterhin den String hält. `system("/bin/sh")` läuft.
|
||
|
|
|
||
|
|
Die Gadgets (`pop rdi; ret`) stecken nicht in `food` — diese glibc hat kein
|
||
|
|
`__libc_csu_init` — daher findet sie `fooc`, indem es den Live-libc-Speicher
|
||
|
|
nach dem Bytepaar `5f c3` durchsucht. Es lokalisiert libc über
|
||
|
|
`/proc/self/maps`, findet die Offsets von `system` und `"/bin/sh"` mit
|
||
|
|
`dlsym()` und berechnet die Basis aus dem Leak, das `food` veröffentlicht.
|
||
|
|
Nichts ist fest verdrahtet, sodass der Exploit ein libc-Update überlebt.
|
||
|
|
|
||
|
|
**Was es lehrt:** Wenn du einmal `RIP` kontrollierst, kannst du *vorhandene*
|
||
|
|
Befehle aneinanderreihen. Das ist Return-Oriented Programming, und es ist, wie
|
||
|
|
fast alle echten Exploits aussehen, weil es keinen vom Angreifer gelieferten
|
||
|
|
ausführbaren Speicher braucht.
|
||
|
|
|
||
|
|
**Verteidigung:** Keine der Compiler-Flags stoppt das allein. Es funktioniert
|
||
|
|
gegen ein PIE-Binärprogramm, mit NX, mit Canary — solange der Angreifer ein
|
||
|
|
Leak hat. Die Verteidigungen sind „habe den Overflow nicht" und „leake keine
|
||
|
|
Adressen". Siehe Tabelle unten.
|
||
|
|
|
||
|
|
### 3. `shellcode` — eigenen Maschinencode ausführen
|
||
|
|
|
||
|
|
23 Bytes, platziert am Anfang des Puffers, mit `RIP`, das auf sie zeigt:
|
||
|
|
|
||
|
|
```asm
|
||
|
|
xor esi, esi ; envp = NULL
|
||
|
|
xor edx, edx ; argv = NULL
|
||
|
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" als 8 rohe Bytes
|
||
|
|
push rdi ; lege den String auf den Stack
|
||
|
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||
|
|
push 0x3b ; 59 = __NR_execve
|
||
|
|
pop rax
|
||
|
|
syscall ; wir sind jetzt eine Shell
|
||
|
|
```
|
||
|
|
|
||
|
|
Das ist die reinste Form des Bugs: Der Angreifer liefert die *Befehle*, nicht
|
||
|
|
nur die Adresse von Befehlen, die bereits existieren. Keine libc-Offsets nötig,
|
||
|
|
also funktioniert es im Prinzip gegen ein statisch gelinktes, vollständig
|
||
|
|
randomisiertes Ziel.
|
||
|
|
|
||
|
|
`make verify` assembliert `shellcode.S` und vergleicht es mit dem Byte-Array,
|
||
|
|
das in `fooc.c` eingebettet ist, sodass die beiden nicht auseinanderlaufen
|
||
|
|
können.
|
||
|
|
|
||
|
|
**Verteidigung:** **NX** (auch W^X, „no execute"). Wenn der Stack als
|
||
|
|
nicht-ausführbar markiert ist, weigert sich die Hardware, Befehle von ihm zu
|
||
|
|
holen, und das `ret` landet auf einer Seite, die nicht ausführbar ist. Deshalb
|
||
|
|
übergibt `make food` die Flag `-z execstack`: Ein Standard-Linux-Stack ist
|
||
|
|
`rw-p`, nicht `rwx`, und die Technik stirbt mit SIGSEGV bei `RIP = die Adresse
|
||
|
|
des Payloads`. Die mit Abstand wichtigste Lektion des Labors ist, dass jedes
|
||
|
|
dieser Bytes nur funktioniert, weil dem Compiler gesagt wurde, den Stack
|
||
|
|
ausführbar zu lassen. Diese Flag ist für niemandes Wohl eingeschaltet.
|
||
|
|
|
||
|
|
### Außerdem enthalten
|
||
|
|
|
||
|
|
| Modus | Was es tut |
|
||
|
|
|---|---|
|
||
|
|
| `-t leak` | verbindet, druckt die Leaks, sendet nichts |
|
||
|
|
| `-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 | — |
|
||
|
|
| **Keine unvertrauenswürdigen Pfade** | validieren und `openat()` unter einem festen Verzeichnis | Pfad-Traversal (CWE-22) | — |
|
||
|
|
| **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 |
|
||
|
|
| `fooc.c` | der Exploit. objdump-basierte Offset-Erkennung, `/proc`-basierte libc-Erkennung, 4 Payload-Builder |
|
||
|
|
| `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
|
||
|
|
dieses Labors passiert.
|