# SUID-Root-RCE-Labor — `foosd` (Daemon) + `foosc` (Exploit) Ein Begleiter zum übergeordneten Labor (`food` / `fooc`, ein gewöhnlicher Daemon, bei dem ein Pufferüberlauf eine *Benutzer*-Shell liefert). Dieses fügt die gefährlichste Ein-Zeichen-Änderung in Unix hinzu: das **Setuid-Bit**. > `chmod u+s` verwandelt „der Angreifer kann Code auf diesem Host ausführen" in > „der Angreifer kann auf diesem Host Code als **root** ausführen". Dieser Satz ist das gesamte Labor. Alles darunter ist der Mechanismus darunter, aufgeschrieben, damit du beim Schreiben eigener Software genau weißt, welche zwei oder drei Dateisystem-Attribute und Compiler-Flags entscheiden, ob ein Speichersicherheitsbug in deinem Code eine Belästigung oder eine Root-Shell ist. Die finale Demo, wenn `foosd` setuid-root ist, ist eine **Root-Shell**, die über das Netzwerk geöffnet wird, indem 32 Bytes handgeschriebener Shellcode ausgeführt werden. --- ## 1. Was das Setuid-Bit tatsächlich tut Jeder Prozess unter Linux trägt drei User-IDs, und das Setuid-Bit bastelt an der Beziehung zwischen ihnen: | ID | Name | Bedeutung | |----|------|---------| | `ruid` | reale User-ID | das Konto, das den Prozess *gestartet* hat | | `euid` | effektive User-ID | was der Kernel bei der Durchsetzung von Zugriffsrechten prüft | | (saved) | gespeicherte Set-User-ID | ein „Slot", in den ein privilegierter Prozess später zurückkehren darf | Ein normales Programm hat `ruid == euid`. Wenn du ein Binärprogramm mit gesetztem Setuid-Bit ausführst, das root gehört: ```text ruid = du (z. B. 1000, "hanez") euid = der Besitzer (z. B. 0, "root") ``` Der Prozess hat also **roots Autorität**, obwohl der Benutzer, der ihn gestartet hat, völlig gewöhnlich ist. Jede Prüfung, die der Kernel durchführt — kann dieser Prozess `/etc/shadow` lesen? eine Datei schreiben? einen anderen Prozess töten? — wird mit `euid` beantwortet, d. h. „ja, es ist root". `foosd` ist ein Netzwerk-Daemon. Er bindet einen Port und `fork()`t dann pro Verbindung ein Kind. Ein Fork *erbt* die euid, also ist auch jedes Kind, das eine Verbindung behandelt, root. Der Overflow in `foosd`s `vulnerable_handler()` ist daher ein Overflow *in einem Root-Prozess*. **Diagnostiziere es selbst, sobald der Daemon läuft:** ```console $ ./foosd ... # siehe die Log-Zeile beim Start [foosd 1234] startup: ruid=1000 euid=0 -> ROOT process ``` und vom Exploit aus: ```console $ ./foosc -t leak foosc: target euid=0 ruid=1000 ``` --- ## 2. Das Labor auf einen Blick | Datei | Rolle | |------|------| | `foosd.c` | Der absichtlich angreifbare Daemon (besitzt die Bugs). Für die Root-Shell-Demo als *setuid-root*-Binärprogramm ausführen. | | `foosc.c` | Der Exploit. Standard: die 32-Byte-`setreuid + execve`-Shellcode-Technik. | | `shellcode.S` | Der Referenz-Shellcode; `make verify` vergleicht ihn mit dem Byte-Array in `foosc.c`. | | `tests/pty_suid_test.c` | Test-Harness. Treibt `foosc` durch ein Pseudo-Terminal und beweist sowohl „eine Shell lief" als auch „sie war root" (`uid=0(`). | | `Makefile` | Build, `setuid`/`unsetuid`-Helfer, Test-Matrix. | | `README.md` | Diese Datei. | > **Warum eine pty?** Die letzte Aktion des Exploits ist es, dein Terminal an > die Shell weiterzuleiten, die auf dem Opfer läuft. Eine Pipe oder ein > Here-Doc landet am falschen Ende dieser Weiterleitung; ein echtes Terminal > ist erforderlich. --- ## 3. Schnellstart ```console $ make # alles bauen, als dein normaler Benutzer $ make setuid # einmalig, fragt nach sudo: chown root + chmod u+s $ make run # startet foosd auf 127.0.0.1:2343 $ make test-suid # volle Matrix; shellcode + ret2win-root müssen root ergeben ``` Interaktiver Smoke-Test: ```console $ ./foosc -t shellcode ... foosc: target euid=0 ruid=1000 foosc: shell is on the victim (root if foosd is SUID); relaying # id uid=0(root) gid=0(root) groups=0(root) <-- du bist root, auf dem Opfer # exit ``` Wenn du fertig bist: ```console $ make stop $ make unsetuid # Hygiene: nie ein Root-SUID-Binärprogramm liegen lassen ``` --- ## 4. *Wann sollte ich das SUID-Bit setzen?* — die Antwort, die du wolltest Genau **einmal, nach dem Bauen, vor dem Start des Daemons für die Root-Shell-Demos** — und nur auf einer Maschine, die dir gehört, wegwerfbar und vom Netzwerk getrennt ist: ```console $ make # kompiliere foosd, foosc, tests $ make setuid # <-- DER Moment. sudo chown root:root foosd && sudo chmod u+s foosd $ make run # starte NACH dem Setzen des Bits ``` Zwei Regeln, die wichtiger sind als der exakte Zeitpunkt: 1. **Setze es erst, wenn das Binärprogramm final ist.** Wenn du das Bit setzt und danach neu baust (`make` / `make clean`), bekommst du beim Schreiben der root-gehörigen Ausgabedatei ein „Permission denied" — und wenn du den Rebuild erzwingst, erstellt die Toolchain die Datei **ohne** das `s` neu, womit die Einrichtung still rückgängig gemacht wird. Die kanonische Reihenfolge bei jedem Rebuild ist daher ```console $ make unsetuid && make && make setuid ``` 2. **Entferne es, wenn du fertig bist.** `make unsetuid`. Ein lebendes, root-gehöriges Setuid-Binärprogramm mit einem ausnutzbaren Bug in deinem Baum ist kein Lernmittel, sondern ein Root-Loch mit einem Compilefehler zwischen ihm und nirgendwo. Auf einer geteilten oder Produktionsmaschine: **mach davon nichts.** Der Daemon weigert sich außerdem standardmäßig, etwas anderes als Loopback zu binden (siehe §7). Wenn du den Exploit *ohne* je gesetztes Bit ausführst, bricht nichts — der Payload landet trotzdem und du bekommst trotzdem eine Shell. Der Unterschied steckt in einer Zahl, und der Exploit sagt sie laut: ```console foosc: WARNING: the daemon is NOT running with euid 0. The payload will still land, but the shell will be a plain user shell, not root. Fix: sudo make setuid ``` Dieses „funktioniert, aber nicht root" ist selbst Teil des Labors. Behalte es für den nächsten Abschnitt im Kopf. --- ## 5. Der Mechanismus — und die Wendung, die SUID interessant macht ### 5.1 Der Overflow (identisch zu `food`) `foosd`s Handler gibt einem `read()` 512 Bytes Vertrauen, während er ihm einen 64-Byte-Stack-Puffer reicht: ```c char buf[64]; n = read(fd, buf, 512); /* <- CWE-120: 448 Bytes über die Kante */ ``` Auf x86-64 wächst der Stack nach unten. Der Exploit schreibt 64 Bytes Müll, um `buf` zu füllen, 8, um den gespeicherten Frame-Pointer zu füllen, und 8 mehr, um die **gespeicherte Rücksprungadresse** zu ersetzen. Wenn `vulnerable_handler` das `ret` ausführt, poppt die CPU den Wert des Angreifers in `RIP` — vom Angreifer kontrollierte Codeausführung. Der Exploit ermittelt den exakten Abstand (88 Bytes für diesen Build), indem er die `objdump`-Ausgabe parst, statt ihn hart zu verdrahten, sodass die Zahl Rebuilds überlebt. ### 5.2 Die Wendung: Die Shell weigert sich, root zu sein Hier geht „SUID-Bug → /bin/sh spawne → root" fehl, und das ist der Grund, warum dieses Labor genau diese Form hat. Wenn ein setuid-root-Programm läuft, ist sein `ruid` immer noch der startende Benutzer und sein `euid` ist root. Wenn das Programm — oder der Angreifer — jetzt eine Shell startet: * `execve("/bin/sh")` ändert die uids **nicht**; der neue Prozess erbt `(ruid=1000, euid=0)`. * bash (und dash) **prüfen genau diese Bedingung beim Start**. Aus dem bash-Handbuch: *„If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, … the effective user id is set to the real user id."* Die Shell wirft also einen Blick auf sich selbst und *lässt root fallen* — eine Verteidigung, die die Shell-Autoren genau gegen diesen Angriff gebaut haben (die historische Rechtfertigung war das Setuid-Shell-/Setuid-Skript-Problem). Das Ergebnis sind die „funktioniert, aber nicht root"-Fälle: | Technik | Was sie ausführt | Resultierende uid | |-----------|------------------|---------------| | `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — Shell gelandet, root von bash zurückgesetzt | | `ret2libc` | `system("/bin/sh")` → frisches `sh -c '/bin/sh'` | **1000** — gleiches Zurücksetzen, eine Ebene tiefer | | `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid aus C heraus geleert | | `shellcode` | 32 Bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid aus Maschinencode geleert | Die beiden, die root erreichen, unterscheiden sich von den beiden, die es nicht tun, um genau eine Idee: **sie leeren die *reale* uid, nicht nur die effektive.** ```c setuid(0) /* setzt euid auf 0, aber ruid bleibt 1000: bash sieht weiterhin euid != ruid und setzt IMMER NOCH zurück. */ setreuid(0, 0) /* setzt BEIDE: ruid = euid = 0. bash sieht gleiche uids und behält root. */ ``` Deshalb beginnt der klassische `/bin/sh`-Shellcode, den du überall im Internet findest, mit einem uid-leerenden Syscall — und deshalb ist der Shellcode hier 32 Bytes statt 23: Die ersten fünf Anweisungen sind ```asm xor edi, edi ; ruid = 0 xor esi, esi ; euid = 0 push 0x71 ; 113 = __NR_setreuid pop rax syscall ``` ### 5.3 Also, was ist der Exploit Ende-zu-Ende? 1. `foosc` liest `foosd`s Banner über den Socket. Es bekommt: - `ids=0/1000` — euid/ruid (die SUID-Selbstdiagnose) - `stack=…` und `libc=…` — Pointer (die ASLR-Leaks) - `BUF=…` — die exakte Adresse des Puffers, den es gleich überlaufen lässt 2. Aus dem Ziel-Binärprogramm (via `objdump`) lernt es `rip_off` und die Adressen von `win()` / `win_root()`. 3. Aus *seiner eigenen* libc (via `/proc/self/maps` + `dlsym` + einen Speicherscan) misst es die Offsets von `system`, `read`, `/bin/sh` und eines `pop rdi; ret`-Gadgets — nichts ist hart verdrahtet. 4. Es setzt den Payload zusammen. Für `-t shellcode` ist das: `[32-Byte-setreuid+execve-Code][Padding bis RIP][ret-Fix][Adresse von buf]`. 5. `foosd`s `read()` läuft über; `ret` landet auf dem Shellcode; der Kernel führt `setreuid(0,0)` aus (ok: euid 0 ist privilegiert) und danach `execve` von `/bin/sh`. bash startet mit `ruid == euid == 0` und bleibt root. 6. `foosc` leitet dein Terminal an diese Root-Shell weiter, bis du `exit` tippst. Ein Details zur Absicherung, das Leute viel Zeit kostet, wenn es übersehen wird: Der Exploit testet jedes uid-leerende Verhalten **ohne** das benötigte Setuid-Bit zuerst. Führe `make test` vor `make setuid` aus, und du siehst jede Technik eine Shell landen, während `ROOT=MISSING` dasteht; führe `make test-suid` nach `make setuid` aus, und `ROOT=SEEN` erscheint bei den zwei Techniken, die die reale uid leeren. Dieses A/B ist die ganze Lektion, ausführbar in zehn Sekunden. --- ## 6. Die alten Einzeiler — und warum die meisten von ihnen tot sind Wenn du über SUID gelesen hast, hast du über `PATH`-Hijacking, `LD_PRELOAD` und Setuid-Shells gelesen. Alle drei sind klassisch, und alle drei scheitern auf einem modernen System gegen *dieses Programm*. Es lohnt sich, genau zu wissen, warum, denn die Gründe sind die Verteidigungen, die du gratis bekommst: | Angriffsklasse | Alte Behauptung | Warum sie auf einem modernen Rechner scheitert | |--------------|-----------|------------------------------| | `LD_PRELOAD` einer bösartigen Bibliothek | „Das Setuid-Programm lädt meine `.so` und führt meinen Code als root aus." | Der Kernel markiert ein Setuid-Binärprogramm als **AT_SECURE**; glibc ignoriert daraufhin `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` und Verwandtes. Die Umgebung wird als *unvertrauenswürdige Eingabe* behandelt. `LD_PRELOAD` gegen ein Setuid-Binärprogramm ist eine No-Operation. | | `PATH`-Hijack (`system("ls")` mit vergiftetem PATH) | „Zeige PATH auf ein Verzeichnis mit meinem falschen `ls`; das Root-Programm führt es aus." | Ein zweites Gesicht derselben Verteidigung: Ein AT_SECURE-Prozess bekommt einen **bereinigten `PATH`** (einen sicheren Standard, in etwa `/usr/local/bin:/usr/bin:/bin`) für `system()`/`execvp`, sodass das vergiftete Verzeichnis nie konsultiert wird. | | Setuid-`system()`-Befehlsinjektion | „Der injizierte Befehl läuft mit euid 0." | `system()` führt den Befehl in einer frischen `/bin/sh` aus, und diese Shell — §5.2 — setzt `euid = ruid` beim Start zurück. Der injizierte Befehl läuft mit der *realen* uid. (Es ist immer noch ein Bug; er eskaliert nur nicht mehr über `/bin/sh`.) | | Setuid-Root-Shell auf der Platte (`cp /bin/sh /tmp; chmod u+s`) | „Führe sie aus, bekomme root." | Genau die Verteidigung oben, und das ist der Grund, warum moderne Distributionen keine Setuid-Root-Shell ausliefern. Selbst wenn dir eine gelingt, weigert sich bash, euid 0 zu behalten, sofern es nicht mit `-p` gestartet wird. | Was lebendig bleibt, und das ist dieses Labor: **Das Programm ist beim Laufen *bereits* root.** Du brauchst weder die Umgebung noch `system()`; du brauchst, dass das Programm *deinen* Code (über einen Memory-Corruption-Bug) ausführt, solange es privilegiert ist, und dein Code muss vorsichtig genug sein, den uid-Mismatch selbst zu beheben — `setreuid(0,0)` — bevor er dir eine Shell übergibt. Memory Corruption + SUID ist die Kombination, die immer noch in `uid=0` endet, und genau deshalb sind speichersichere Sprachen, Canaries und No-Execute-Stacks keine Modeentscheidung. --- ## 7. Die in den Daemon eingebauten Sicherheitsleitplanken `foosd` ist absichtlich das *schlechteste* Stück Software in diesem Repository, also trägt es auch die meisten Leitplanken: 1. **Nur Loopback, erzwungen.** `foosd` weigert sich, eine andere Adresse als Loopback zu binden, sofern du nicht `-L` übergibst. Ein Setuid-Root-Listener auf einer echten Schnittstelle ist ein entfernter Root-Dienst; die Weigerung ist der Standard, damit der gefährliche Zustand bewusst eingetippt werden muss. 2. **Selbstdiagnose.** Beim Start loggt es `ruid`/`euid` und ob es als root läuft, sodass die Konsole den Zustand zeigt, von dem der Exploit abhängt. 3. **Das Log erreicht den Client nie.** Der Daemon reserviert einen privaten Log-Deskriptor, bevor Sockets fd 1 ersetzen, sodass Crash-Reporter-Ausgabe und interne Pfade vom Angreifer nicht über die Leitung zurückgelesen werden können. 4. **Crash-Reporter.** Ein SIGSEGV-Handler loggt `RIP`/`RSP` — den Wert, den der Angreifer in die Rücksprungadresse geschrieben hat — sodass eine erfolgreiche Übernahme in `foosd.log` sichtbar ist, statt ein stiller Tod zu sein. 5. **`make unsetuid`.** Das Entfernen des Bits ist skriptiert, denn es gesetzt zu lassen ist der Ausfallmodus, den Leute tatsächlich haben. --- ## 8. Gegenmaßnahmen — was jede stoppt und was nicht Angewendet auf `foosd` via `make hardened`, einzeln oder zusammen: | Gegenmaßnahme | Was sie stoppt | Was sie *nicht* stoppt | |------------|---------------|-------------------------| | `-fstack-protector-strong` (Canary) | Den Overflow: `ret` erkennt einen zerstörten Canary und bricht ab, bevor die Adresse des Angreifers verwendet wird. Stoppt hier **alle vier** Techniken — sie teilen sich das eine angreifbare `read()`. | Nichts am *Design*: Das Binärprogramm ist immer noch setuid-root; ein anderer Bug (Format-String-`%n`, Heap-Overflow, Use-after-Free) hat keinen Canary zum Auslösen. | | `-fPIE -pie` (ASLR für das Binärprogramm) | Nutzung vorhersagbarer `win()`/`win_root()`-Adressen (die ret2win-Techniken). | Die Shellcode-Technik, wenn weiterhin eine Stack-Adresse leakt (die `BUF=`-Zeile). | | `-z noexecstack` (NX / W^X) | Den Shellcode: Die CPU weigert sich, Befehle von einer daten-only-Seite zu holen, sodass ein Sprung auf `buf` ein SIGSEGV ist. | ROP — das Ausführen vorhandenen Codes (`ret2libc`). | | Alle drei zusammen | Ein schwer zu überlaufendes, randomisiertes Binärprogramm mit nicht-ausführbarem Stack. So sieht ein normaler gehärteter Build aus. | Das Setuid-Bit. **Ein gehärtetes SUID-Binärprogramm ist immer noch ein SUID-Binärprogramm.** Wenn irgendein erreichbarer Speichersicherheitsbug überlebt, ist es immer noch „Bug in einem Root-Prozess". | Der Konsolenbeweis ist `make test-hardened`, das den gehärteten Build eintauscht und zeigt, wie alle Techniken am Canary sterben, während `foosd_hardened.log` `*** stack smashing detected ***` aufzeichnet. Zwei Designebenen-Gegenmaßnahmen, die keine Compiler-Flag liefert und die auch das übergeordnete Labor (`food`) nutzt: - **Least Privilege.** Ein Daemon für einen unprivilegierten Port (2343 > 1024) hat keinen legitimen Bedarf an root. Ein korrektes `foosd` würde binden und dann `setgroups`/`setgid`/`setuid` auf ein unprivilegiertes Konto ausführen und *verifizieren, dass es hielt* (die korrekte Version steht im Quellcode als `drop_privs()`, nie aufgerufen — die Nicht-Aufrufung ist Bug #3 des Labors). - **Das read begrenzen.** `n = read(fd, buf, sizeof(buf) - 1)`. Eine korrekte Zeile schlägt jede Compiler-Flag in der Tabelle. --- ## 9. Das Wire-Protokoll (damit du den Daemon mit netcat lesen kannst) ```text FOOSD 1.0 - deliberately vulnerable SUID service Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512. FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f... BUF=0x7ffd... ``` * `ids=euid/ruid` — durfte nicht als `euid=`/`ruid=` gedruckt werden, weil die Test-Harness eine Shell anhand des wörtlichen `uid=` beweist und das Banner es nicht enthalten darf (eine Sonde, die die Signatur mit der Antwort teilt, ist eine klassische Fehlpositiv-Falle; siehe den Kommentar in `foosd.c`). Die Harness verlangt außerdem die strenge `id`-Ausgabeform — `uid=NNN(...)` — sodass nichts, was der Daemon oder der Exploit druckt, die Prüfung zufällig erfüllen kann: `foosc`s eigenes „target euid=… ruid=…" enthält `uid=` als Teilstring, was einmal einen gehärteten Test eine nie gelaufene Shell melden ließ. * `stack=`, `libc=`, `BUF=` — die ASLR-Leaks: erlauben Shellcode und ret2libc, exakte Adressen zu berechnen. --- ## 10. Übungen 1. **Beobachte die Nicht-Root-Abstufung.** Führe `make test` *vor* `make setuid` aus, dann danach erneut. Erkläre die `ROOT=SEEN`-Änderung mit der ruid/euid-Geschichte aus §5.2. 2. **Lies den Absturz.** Führe `./foosc -t demo -n` aus und lies dann `foosd.log`. Die Zeile `RIP=0x4141414141414141` ist das Padding des Angreifers — der Beweis, dass der Overflow, nicht Pech, die Ausführung kontrolliert. 3. **Füge den Canary hinzu.** `make hardened` und ändere die `test-hardened`-Schleife selbst; die Log-Zeile `*** stack smashing detected ***` ist die arbeitende Verteidigung. 4. **Deaktiviere das Leak.** Kommentiere die `BUF=`-Zeile in `foosd.c` aus, baue neu und beobachte, wie `-t shellcode` von deterministisch zu einem Ratespiel wird. Diese eine Zeile ist der Grund, warum echte ASLR-Bypasses ein ganzes Feld sind. 5. **Das `-p`-Experiment.** Ändere in einer Kopie von `win()` `execl("/bin/sh", "sh", NULL)` zu `execl("/bin/sh", "sh", "-p", NULL)` und beobachte root. `-p` ist die dokumentierte Notluke aus dem Wächter der Shell — und der Grund, warum der Rat „spawne einfach eine Shell" aus alten Write-ups unvollständig ist. 6. **Warum nicht `setuid(0)`?** Schreibe den Shellcode so um, dass er `setuid(0)` statt `setreuid(0,0)` aufruft (Syscall 105). Die Shell landet trotzdem — und fällt trotzdem auf `uid=1000`. Das ist das lehrreichste Ein-Zeilen-Experiment im gesamten Repository. --- ## 11. Sicherheit und Aufräumen - Nur Loopback, standardmäßig und per Design; `-L` bindet weiter, und nur eine Wegwerf-VM sollte es überhaupt in Betracht ziehen. - Dies ist ein Root-Shell-Labor. Führe es nicht auf einer Maschine aus, die wichtig ist, und richte `foosc -h` nicht auf etwas, das dir nicht gehört. - Aufräumritual: `make stop` und dann `make unsetuid`, und wenn du den Baum wieder makellos willst: `sudo make clean`. ```console $ make stop $ make unsetuid ```