Initial commit
This commit is contained in:
commit
394e3be54d
41 changed files with 16315 additions and 0 deletions
436
README.DE.md
Normal file
436
README.DE.md
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue