15 KiB
Das wosuid-Labor — Root-RCE ohne Setuid-Bit
foowosd ein absichtlich angreifbarer Daemon, der root ist, weil er als
root *GESTARTET* wurde (Port 2344, standardmäßig nur Loopback)
foowosc der Exploit: verwandelt einen Stack-Overflow in eine **root**-Shell,
indem er Shellcode ausführt — dieselben 23 Bytes, mit denen der
User-Level-`food`-Daemon im übergeordneten Labor geknackt wurde
Das ist das dritte Labor der Reihe. Gleiche Exploit-Toolchain, gleicher Stil, ein fundamentaler Unterschied:
| Labor | wie der Zielprozess root wird | uid=0(root)-Shell? |
|---|---|---|
| food/fooc | nie — es ist ein gewöhnlicher User-Daemon | nein |
| foosd/foosc | das SUID-Bit (chmod u+s) — euid 0, ruid 1000 |
ja (braucht setreuid im Shellcode, weil bash euid→ruid zurücksetzt) |
| foowosd/foowosc | keins — root startet den Daemon (sudo / systemd User=root) |
ja (schlichter execve-Shellcode) |
Das Setuid-Bit ist ein Transportmittel für Privilegien — nicht die
Privilegien selbst. Ein von root gestarteter Daemon hat reale, effektive und
gespeicherte uid alle gleich 0. Für den Kernel ist das root, Punkt; es kann und
will nicht wissen, ob der Prozess über +s an einer Datei dorthin kam oder
über sudo ./foowosd. Der Overflow in einem von root gestarteten Daemon ist
also ein Root-Exploit — „Ich habe keine SUID-Binärprogramme" ist nicht
dasselbe wie „Ich bin nicht ausnutzbar".
Das ist die ganze Lektion dieses Labors. Alles darunter ist die Maschinerie.
Schnellstart (was der Benutzer angefordert hat)
cd wosuid
make # baut den Daemon, den Exploit und die Test-Harness
Das Echte — den Daemon als root ausführen
sudo make run-root # startet foowosd als uid 0 (Prozess, nicht Dateimodus)
make test-root # jede Technik muss jetzt uid=0(root) ergeben
Kein sudo? Derselbe Kernelpfad über einen User-Namespace
make run-root-ns # uid 0 in einem User-Namespace — kein Passwort nötig
make test-root # gleiche Urteile; für CI und alle ohne sudo
Baseline — Daemon als dein normaler Benutzer (kein root irgendwo)
make run # foowosd läuft mit deinen uids
make test # Exploits landen Shells, aber `root` wird als MISSING erwartet
Aufräumritual (immer: dies ist ein Root-Shell-Labor)
make stop
foowosd ist nicht setuid, und nichts in diesem Verzeichnis macht je
chmod +s — das ist der Punkt. Der gefährliche Zustand ist der Prozess,
nicht die Datei.
Wenn dir gesagt wird, das SUID-Bit zu setzen
Du wirst es nicht tun. Dieses Labor hat bewusst kein SUID-Bit:
foowosdwird gebaut, gehört dir und hat reguläre Modi wie jedes andere Programm.- Es wird so root, wie es echte Daemons tun — indem es von root gestartet wird.
make run-rootnutzt dafürsudo, undmake run-root-nsbekommt einen echten uid-0-Prozess ganz ohne das.
Das Suid-Bit gehört dem Schwester-Labor (foosd). Der Kontrast zwischen den
beiden ist der Lehrplan:
- SUID-Labor: Das Bit gibt euid 0, aber ruid 1000 →
execve("/bin/sh")wird vom Wächter der bash herabgestuft (euid != ruid→ Reset) → der Shellcode muss zuerstsetreuid(0,0)aufrufen (32-Byte-Payload). - Dieses Labor: root startet den Prozess → ruid == euid == 0 → der
Wächter hat nichts zurückzusetzen → der schlichte 23-Byte-
execve-Shellcode behält root.
Gleicher Overflow. Gleiche Technik. Anderer Ursprung der Privilegien, andere Payload-Form. Das ist die Lektion in Miniatur.
Das Protokoll
Egal welche Art Client sich verbindet, foowosd begrüßt ihn mit:
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (das Banner + die Leaks)
BUF=0x7ffd… (die Pufferadresse)
ids=euid/ruid ist der „bin ich root?"-Seitenkanal. foowosc druckt eine
laute Warnung, wenn euid nicht 0 ist (d. h., du hast den Daemon als normalen
Benutzer gestartet): Der Payload landet trotzdem, aber die Shell wird eine
Benutzer-Shell sein, und den Exploit „kaputt" zu nennen wäre falsch — er
eskaliert nur eben nicht.
Schreibweise-Hinweis:
ids=, nichteuid=/ruid=. Die Test-Harness beweist ein lebendesid, indem sie die wörtliche Formuid=NNN(matcht, daher darf das Banner nie einen Teilstring enthalten, der selbst die Prüfung erfüllt. (Im SUID-Labor erzeugte genau diese Falle ein spektakuläres Fehlpositiv.)
Die Exploit-Techniken (foowosc -t …)
Alle vier Exploit-Pfade unten funktionieren gegen foowosd. Wenn der Daemon root ist, ergeben alle, die irgendetwas spawne, root — anders als im SUID-Labor, wo ret2win/ret2libc vom bash-Wächter still auf uid 1000 herabgestuft wurden. Hier gibt es keinen Mismatch, gegen den es zu wachen gälte.
-t |
was passiert | wenn der Daemon root ist |
|---|---|---|
shellcode |
23-Byte-execve("/bin/sh", NULL, NULL) läuft auf dem Stack. |
root-Shell (Standard) |
ret2win |
Sprung zu win() → execl("/bin/sh") |
root-Shell |
ret2libc |
ROP: pop rdi; ret → "/bin/sh" → system() |
root-Shell |
demo |
nur Müll-Overflow — erwarte ein SIGSEGV im Daemon-Log | Absturz, per Design |
leak |
druckt nur die Leaks, sendet keinen Payload | n/a |
./foowosc -t shellcode # interaktiv; Standardziel 127.0.0.1:2344
./foowosc -t shellcode -n # senden und berichten, keine interaktive Session
Eine erfolgreiche interaktive Session leitet dein Terminal an die Shell auf
dem Opfer weiter — es gibt genau eine Shell im Bild, und es ist /bin/sh,
das als root in foowosd läuft. Tippe id, um uid=0(root) zu sehen.
Warum es hier keine ret2win-root-Technik gibt
foosc hatte eine — sie sprang zu einem win_root(), das vor dem exec
setreuid(0,0) aufrief, weil ein Setuid-Prozess mit einer realen uid lief, die
immer noch 1000 sagte. Ein von root gestarteter Prozess hat die reale uid
schon 0; es gibt nichts zu leeren, also würden die zusätzliche Funktion und
Technik nichts lehren. Entfernt.
Was foowosc Schritt für Schritt tut
- Statische Analyse —
objdump -dvon./foowosd. Findetvulnerable_handler,win(), daslea -0x50(%rbp), dasbufadressiert, und das erste nackteret. Aus der Verschiebung berechnet esrip_off = 80 + 8 = 88. Nichts ist hart verdrahtet; das überlebt einen Rebuild. - Selbst-Introspektion — liest sein eigenes
/proc/self/mapsund ruft perdlsym()system/readauf, um die libc-Offsets zu lernen. Die libc-Basis des Ziels istleaked_read − off_read, dann istsystem = base + off_system, usw. Diese Delta-Arithmetik ist der Grund, warum Exploits über libc-Versionen hinweg am Leben bleiben. - Verbinden — liest das Banner/die Leaks (
ids=,stack=,libc=,BUF=). - Payload bauen — für
shellcode: 23 Bytes Maschinencode, Padding bisrip_off, dann die gespeicherte RIP =buf(sodassretauf den Code springt). Fürret2win/ret2libc: aus der Analyse berechnete Adressen — keine Ausführung des Stacks nötig. - Der Ausrichtungs-Fix — ein gekapertes nacktes
retübergibt dem Calleersp ≡ 8 (mod 16), und glibcs SSE2-Codemovaps-fault auf einem nicht ausgerichteten Stack (der Crash-Reporter loggtsi_addr=(nil)— der Hinweis). foowosc fügt vor dem echten Ziel ein zusätzlichesret-Gadget ein und stellt die Invariante wieder her. Samthandschuh-Ingenieurkunst in einem Shellcode-Labor, aber es ist der Unterschied zwischen einem Payload, der „manchmal funktioniert", und einem, der immer funktioniert. - Senden, dann weiterleiten — der Opferprozess ist die Shell; dieser
Prozess nur splißt Bytes. Keine lokale Shell, kein zweiter Leser — der
Single-Read-Cursor-Fehler (ein gefressenes Byte pro Block) ist in
become_shell()dokumentiert.
Die absichtlichen Bugs des Daemons (alle in foowosd.c, alle echte CWE-Klassen)
| # | Bug | CWE | Hinweis |
|---|---|---|---|
| 1 | read(fd, buf, 512) in einen 64-Byte-Stack-Puffer |
CWE-120 | der Overflow: 448 Bytes über buf hinaus, gespeicherte RIP bei +88 |
| 2 | nur snprintf(line, …, "%.*s", …); aber Angreifer-% im Echo-Pfad |
CWE-134 | das Leak ist hier der eigentliche Payload; ein %n in einem root-Prozess wäre write-what-where als root |
| 3 | Kinder behalten root, während sie unvertrauenswürdige Eingaben behandeln | CWE-271 | das korrekte drop_privs() (setgroups→setgid→setuid, in dieser Reihenfolge, mit Verifikation) steht in der Datei, kommentiert, bewusst nie aufgerufen |
| 4 | ids=, stack=, libc=, BUF= an jeden Client offengelegt |
CWE-200 | ohne diese Leaks könnten Shellcode- und ret2libc-Techniken keine Adressen berechnen (ASLR würde sie schlagen) |
Der Handler hat exakt dieselbe buf[64]/read(512)-Form wie die beiden
anderen Labore, sodass die geteilte objdump-basierte Erkennungspipeline
unverändert funktioniert.
So inspizierst du den Daemon (Lernpfad)
make status # läuft er? als welche uid? Dateimodus wird gezeigt
make run-root # oder run / run-root-ns
./foowosc -t leak # sieh das Banner und die Leaks, sende nichts
./foowosc -t demo # Müll-Overflow -> SIGSEGV, geloggt mit RIP/rsp
./foowosc -t shellcode # die interaktive Root-Shell
make test-root # volle Matrix, alle Techniken, --must-root
Crash-Reporter: Bei SIGSEGV loggt der Daemon die Fehleradresse, RIP und RSP.
Ein ret in eine nicht-kanonische 0x4141… fault am ret selbst (RIP wie
0x4028xx, si_addr=(nil)) — wissenswert, bevor du eine Log-Zeile als
NULL-Deref fehlliest.
Warum der Shellcode 23 Bytes sind, nicht 32
31 f6 xor esi, esi ; argv = NULL
31 d2 xor edx, edx ; envp = NULL
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
57 push rdi
48 89 e7 mov rdi, rsp
6a 3b push 0x3b ; 59 = execve
58 pop rax
0f 05 syscall
Das SUID-Labor braucht setreuid(0,0) davor. Dieses Labor nicht, aus dem
Grund, der überall wiederholt wird: ruid ist bereits 0, weil root den
Prozess gestartet hat. make verify beweist, dass die Bytes in foowosc.c
Byte für Byte das sind, was shellcode.S assembliert.
Gegenmaßnahmen — was make hardened ändert
Gehärteter Build (-fstack-protector-strong -fPIE -pie -z noexecstack):
| Technik | angreifbares foowosd |
gehärtetes foowosd_hardened |
|---|---|---|
shellcode |
root-Shell (ausführbarer Stack) | SIGSEGV bei der Canary-Prüfung / NX |
ret2win / ret2libc |
root-Shell | Canary bricht ret ab — aber Achtung: ein PIE-Build macht diese Adressen auch zufällig |
demo |
SIGSEGV, geloggt | SIGSEGV, geloggt |
make test-hardened demonstriert das live. Die wichtige Beobachtung ist nicht
nur, dass die Gegenmaßnahmen die Techniken getötet haben — es ist, dass sie
den Daemon nicht „nicht-root" gemacht haben. Ein gehärteter Build, der
immer noch als root gestartet wird, ist immer noch ein Root-Daemon; die
Gegenmaßnahme erhöht nur die Latte für den Angreifer. Least Privilege
(drop_privs()) und Speichersicherheit sind zwei verschiedene Bugs, und ein
Daemon, der root nicht braucht, sollte es nicht haben.
Sicherheitsleitplanken (gleiche Politik wie das SUID-Labor)
- Nur Loopback. foowosd weigert sich, etwas anderes als
127.0.0.1/localhost/::1zu binden, sofern du nicht-Lübergibst. Ein Root-Daemon auf einer echten Schnittstelle ist ein entfernter Root-Dienst.-Ldient nur dazu, den Wächter zu zeigen; verwende es nicht auf irgendetwas, das wichtig ist. - Der Zustand wird laut geloggt. Beim Start druckt es
ruid/euidund ob dies ein Root-Prozess ist, sodass du immer weißt, welches Exploit-Ergebnis zu erwarten ist. - Urteile kommen aus Exit-Status in
make test*(dem Rückgabecode der pty-Harness), nie aus dem Greppen ihrer stdout-Ausgabe — greppbare Ausgabe lügt. - pty muss cooked + ECHO off laufen, sonst spiegelt die Harness ihre eigene Befehlszeile zurück und fälscht den Marker. Die Harness schaltet ECHO aus und lässt ECHONL an.
- Aufräumritual:
make stopnach jeder Session. Wenn der Daemon root-gehörig ist, sagt dirstop, dass dusudo pkill -x foowosdausführen sollst. - Führe dies nie auf einem Host aus, der dir wichtig ist. Es existiert, um
uid=0-Shells über die Loopback-Schnittstelle auszugeben.
Übungen
- Führe
make run(User-Daemon) aus, dann./foowosc -t shellcode. Warum ist die Shell nicht root? (Prüfeids=im Banner — foowosc sagt es dir, bevor du dich überhaupt verbindest.) make stop && sudo make run-root && make test-root. Erkläre anhand der Banner-Zeile, warum alle vier Techniken jetztuid=0(root)ergeben.- Finde in
foowosd.cdrop_privs()und lies, warum die Reihenfolge vonsetgroups → setgid → setuidzählt. Entscheide, wo inmain()es hingehören würde, und was aus der Angriffsfläche des Labors wird, sobald es tatsächlich aufgerufen wird. - Berechne
rip_offvon Hand ausobjdump -d foowosd: findebufslea -0xNN(%rbp)invulnerable_handler, dannNN + 8. foowosc macht genau das; prüfe seine Rechnung gegen deine eigene. make hardened && make test-hardened. Welche Technik erliegt dem Canary und welche NX? Warum ändert Härten nicht, wasmake statusüber den Prozess meldet?- Vergleiche die Shellcodes der beiden Labore: 23 Bytes hier, 32 für foosd. Was tun die zusätzlichen 9 Bytes, und warum werden sie nur im SUID-Fall gebraucht?
- Lies
become_shell()s Kommentar über den einzelnen Read-Cursor. Stelle den Ausfallmodus gedanklich nach: zwei Leser an einem Socket bedeuten, dass die Login-Shell ein Byte pro Block frisst — „uid=1000…" kommt als „id=1000…" an. Warum kann ein Relay-Prozess diesen Bug nie haben?
Dateien
foowosd.c der angreifbare Root-Daemon (jede Zeile kommentiert)
foowosc.c der Exploit (jede Zeile kommentiert)
shellcode.S Referenz-Assembly für den 23-Byte-Payload
tests/pty_wosuid_test.c die pty-Harness (Marker + strenge id-Form-Prüfungen)
Makefile build / run / run-root / run-root-ns / test /
test-root / verify / hardened / clean …
Schwester-Labore: ../food.c/../fooc.c (User-Level-Baseline, Port 2342) und
../suid/ (SUID-Root-Daemon foosd/foosc, Port 2343). Die Ports sind
bewusst verschieden — du kannst alle drei gleichzeitig laufen lassen und ihre
ids=-Zeilen im Banner gegeneinander prüfen.