# SUID-root-RCE-laboratorium — `foosd` (daemon) + `foosc` (exploit) En ledsager til hovedlaboratoriet (`food` / `fooc`, en vanlig daemon der et bufferoverløp gir deg en *bruker*-shell). Dette legger til den farligste én-tegns-endringen i Unix: **setuid-biten**. > `chmod u+s` forvandler «angriperen kan kjøre kode på denne verten» til > «angriperen kan kjøre kode som **root** på denne verten». Den setningen er hele laboratoriet. Alt nedenfor er mekanismen under den, skrevet ned, slik at du når du skriver din egen programvare, vet nøyaktig hvilke to eller tre filsystem-attributter og kompilator-flag som avgjør om en minnesikkerhetsfeil i koden din er en irritasjon eller en root-shell. Den endelige demoen, når `foosd` er setuid-root, er en **root-shell** som åpnes over nettverket ved å utføre 32 bytes håndskrevet shellcode. --- ## 1. Hva setuid-biten faktisk gjør Hver prosess på Linux bærer tre user-ID-er, og setuid-biten tukler med forholdet mellom dem: | ID | Navn | Betydning | |----|------|---------| | `ruid` | reell user-ID | kontoen som *startet* prosessen | | `euid` | effektiv user-ID | det kjernen sjekker når den håndhever tilgang | | (saved) | lagret set-user-ID | en «sporplass» en privilegert prosess kan vende tilbake til senere | Et vanlig program har `ruid == euid`. Når du kjører en binærfil med setuid-biten satt, eid av root: ```text ruid = deg (f.eks. 1000, «hanez») euid = eieren (f.eks. 0, «root») ``` Prosessen har derfor **roots autoritet**, selv om brukeren som startet den er helt vanlig. Hvert sjekkpunkt kjernen utfører — kan denne prosessen lese `/etc/shadow`? skrive en fil? drepe en annen prosess? — besvares med `euid`, altså «ja, den er root». `foosd` er en nettverksdaemon. Den binder en port og `fork()`er deretter et barn per tilkobling. En fork *arver* euid-en, så hvert barn som håndterer en tilkobling er også root. Overløpet i `foosd`s `vulnerable_handler()` er derfor et overløp *inne i en root-prosess*. **Diagnostiser det selv når daemonen kjører:** ```console $ ./foosd ... # se logglinjen den skriver ut ved start [foosd 1234] startup: ruid=1000 euid=0 -> ROOT process ``` og fra exploitet: ```console $ ./foosc -t leak foosc: target euid=0 ruid=1000 ``` --- ## 2. Laboratoriet ved første øyekast | Fil | Rolle | |------|------| | `foosd.c` | Den bevisst sårbare daemonen (eier feilene). Kjør som *setuid-root*-binærfil for root-shell-demoen. | | `foosc.c` | Exploitet. Bruker som standard den 32-byte `setreuid + execve`-shellcode-teknikken. | | `shellcode.S` | Referanse-shellcoden; `make verify` diff'er den mot byte-arrayen i `foosc.c`. | | `tests/pty_suid_test.c` | Test-harness. Driver `foosc` gjennom et pseudo-terminal og beviser både «en shell kjørte» *og* «den var root» (`uid=0(`). | | `Makefile` | Bygg, `setuid`/`unsetuid`-hjelpere, testmatrise. | | `README.md` | Denne filen. | > **Hvorfor en pty?** Exploitets siste handling er å videresende terminalen din > til shellen som kjører på offeret. En pipe eller her-doc havner i feil ende av > den videresendingen; en ekte terminal er påkrevd. --- ## 3. Rask start ```console $ make # bygg alt, som din vanlige bruker $ make setuid # én gang, spør om sudo: chown root + chmod u+s $ make run # start foosd på 127.0.0.1:2343 $ make test-suid # full matrise; shellcode + ret2win-root skal gi root ``` Interaktiv røykprøve: ```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 er root, på offeret # exit ``` Når du er ferdig: ```console $ make stop $ make unsetuid # hygiene: etterlat aldri en root-SUID-binærfil ``` --- ## 4. *Når skal jeg sette SUID-biten?* — svaret du ba om Nøyaktig **én gang, etter byggingen, før du starter daemonen for root-shell-demoene** — og bare på en maskin som er din, egnet til å kastes og frakoblet nettverket: ```console $ make # kompiler foosd, foosc, tester $ make setuid # <-- ØYEBLIKKET. sudo chown root:root foosd && sudo chmod u+s foosd $ make run # start ETTER at du har satt biten ``` To regler som betyr mer enn det nøyaktige tidspunktet: 1. **Sett den bare når binærfilen er ferdig.** Hvis du bygger om (`make` / `make clean`) etter at du har satt biten, treffer du «Permission denied» når du skriver root-eide utdatafiler — og hvis du tvinger ombyggingen, gjenskaper verktøykjeden filen **uten** `s`-en og angrer stille og rolig oppsettet. Den kanoniske rekkefølgen ved enhver ombygging er derfor ```console $ make unsetuid && make && make setuid ``` 2. **Fjern den når du er ferdig.** `make unsetuid`. En levende, root-eid setuid-binærfil med en utnyttbar feil i treet ditt er ikke et læremiddel, det er et root-hull med en kompileringsfeil mellom seg og ingenting. På en delt eller produksjonsmaskin: **ikke gjør noe av dette.** Daemonen nekter dessuten som standard å binde noe annet enn loopback (se §7). Hvis du kjører exploitet *uten* noen gang å sette biten, går ingenting i stykker — payloaden lander fortsatt, og du får fortsatt en shell. Forskjellen er i ett tall, og exploitet sier det høyt: ```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 ``` «Virket, men ikke root»-resultatet er selv en del av laboratoriet. Husk det til neste avsnitt. --- ## 5. Mekanismen — og vrien som gjør SUID interessant ### 5.1 Overløpet (identisk med `food`) `foosd`s handler gir et `read()` 512 bytes tillit, mens den rekker den et 64-byte stack-buffer: ```c char buf[64]; n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over kanten */ ``` På x86-64 vokser stacken nedover. Exploitet skriver 64 bytes søppel for å fylle `buf`, 8 for å fylle den lagrede rammepekeren og 8 til for å erstatte den **lagrede returadressen**. Når `vulnerable_handler` utfører `ret`, popper CPU-en angriperens verdi inn i `RIP` — angriperkontrollert kodeutførelse. Exploitet finner den nøyaktige avstanden (88 bytes for denne builden) ved å parse `objdump`-utdata i stedet for å hardkode den, så tallet overlever ombygginger. ### 5.2 Vrien: shellen nekter å være root Her er det der å tenke «SUID-feil → spawn /bin/sh → root» ville gått galt, og hvorfor dette laboratoriet har nøyaktig den formen det har. Når et setuid-root-program kjører, er `ruid` fortsatt den startende brukeren, og `euid` er root. Hvis programmet — eller angriperen — nå starter en shell: * `execve("/bin/sh")` endrer **ikke** u-id-ene; den nye prosessen arver `(ruid=1000, euid=0)`. * bash (og dash) **sjekker nøyaktig den tilstanden ved start**. Fra bash-manualen: *«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.»* Så shellen ser på seg selv og *dropper root* — et forsvar shell-forfatterne bygde nøyaktig mot dette angrepet (den historiske begrunnelsen var setuid-shell-/setuid-skript-problemet). Resultatet er «virket, men ikke root»-tilfellene: | Teknikk | Hva den utfører | Resulterende uid | |-----------|------------------|---------------| | `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — shell landet, root nullstilt av bash | | `ret2libc` | `system("/bin/sh")` → fersk `sh -c '/bin/sh'` | **1000** — samme nullstilling, ett nivå ned | | `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid ryddet fra C | | `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid ryddet fra maskinkode | De to som når root, skiller seg fra de to som ikke gjør det, med nøyaktig én idé: **de rydder den *reelle* uid-en, ikke bare den effektive.** ```c setuid(0) /* setter euid til 0, men ruid forblir 1000: bash ser fortsatt euid != ruid og nullstiller FORTSATT. */ setreuid(0, 0) /* setter BEGGE: ruid = euid = 0. bash ser like uid-er og beholder root. */ ``` Det er derfor den klassiske `/bin/sh`-shellcoden du finner overalt på nettet, starter med et uid-ryddende syscall — og grunnen til at shellcoden her er 32 bytes i stedet for 23: de første fem instruksjonene er ```asm xor edi, edi ; ruid = 0 xor esi, esi ; euid = 0 push 0x71 ; 113 = __NR_setreuid pop rax syscall ``` ### 5.3 Så hva er exploitet, fra ende til annen? 1. `foosc` leser `foosd`s banner over socketen. Det får: - `ids=0/1000` — euid/ruid (SUID-selvdiagnosen) - `stack=…` og `libc=…` — pekere (ASLR-leaksene) - `BUF=…` — den nøyaktige adressen på bufferen den er i ferd med å renne over 2. Fra målbinærfilen (via `objdump`) lærer det `rip_off` og adressene til `win()` / `win_root()`. 3. Fra *sin egen* libc (via `/proc/self/maps` + `dlsym` + et minnesøk) måler det offsetene for `system`, `read`, `/bin/sh` og et `pop rdi; ret`-gadget — ingenting er hardkodet. 4. Det setter sammen payloaden. For `-t shellcode` er det: `[32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf]`. 5. `foosd`s `read()` renner over; `ret` lander på shellcoden; kjernen utfører `setreuid(0,0)` (helt greit: euid 0 er privilegert) og deretter `execve` av `/bin/sh`. bash starter med `ruid == euid == 0` og forblir root. 6. `foosc` videresender terminalen din til den root-shellen, til du skriver `exit`. Én bekvemmelighetsdetalj som koster folk mye tid hvis den overses: exploitet tester hver uid-ryddende adferd **uten** først å trenge setuid-biten. Kjør `make test` før `make setuid`, så ser du hver teknikk lande en shell mens `ROOT=MISSING` står; kjør `make test-suid` etter `make setuid`, så dukker `ROOT=SEEN` opp ved de to teknikkene som rydder den reelle uid-en. Den A/B-en er hele leksjonen, utført på ti sekunder. --- ## 6. De gamle one-linerne — og hvorfor de fleste av dem er døde Har du lest om SUID, har du lest om `PATH`-kapring, `LD_PRELOAD` og setuid-shells. Alle tre er klassiske, og alle tre feiler på et moderne system mot *dette programmet*. Det er verdt å vite nøyaktig hvorfor, fordi grunnene er forsvarene du får gratis: | Angrepsklasse | Gammel påstand | Hvorfor den feiler på en moderne maskin | |--------------|-----------|------------------------------| | `LD_PRELOAD` av et ondsinnet bibliotek | «Setuid-programmet laster min `.so` og kjører koden min som root.» | Kjernen merker en setuid-binærfil som **AT_SECURE**; glibc ignorerer deretter `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` og venner. Miljøet behandles som *upålitelig input*. `LD_PRELOAD` mot en setuid-binærfil er en no-op. | | `PATH`-kapring (`system("ls")` med en forgiftet PATH) | «Pek PATH mot en mappe med min falske `ls`; root-programmet kjører den.» | Et annet ansikt av samme forsvar: en AT_SECURE-prosess får en **sanert `PATH`** (en sikker standard, omtrent `/usr/local/bin:/usr/bin:/bin`) for `system()`/`execvp`, så den forgiftede mappen konsulteres aldri. | | Setuid-`system()`-kommandoinjeksjon | «Den injiserte kommandoen kjører med euid 0.» | `system()` kjører kommandoen i en fersk `/bin/sh`, og den shellen — §5.2 — nullstiller `euid = ruid` ved start. Den injiserte kommandoen utføres med den *reelle* uid-en. (Det er fortsatt en feil; den eskalerer bare ikke lenger gjennom `/bin/sh`.) | | Setuid-root-shell på disken (`cp /bin/sh /tmp; chmod u+s`) | «Kjør den, få root.» | Nøyaktig forsvaret ovenfor, og det er grunnen til at moderne distroer ikke leverer noen setuid-root-shell. Selv når du lykkes med å lage én, nekter bash å beholde euid 0 med mindre den startes med `-p`. | Det som forblir i live, og det er dette laboratoriet: **programmet er *allerede* root når det kjører.** Du trenger ikke miljøet eller `system()`; du trenger at programmet utfører *din* kode (via en minnekorrupsjonsfeil) mens det er privilegert, og at koden din er omhyggelig nok til selv å rette opp uid-mismatchen — `setreuid(0,0)` — før den overrekker deg en shell. Minnekorrupsjon + SUID er kombinasjonen som fortsatt ender i `uid=0`, noe som er nøyaktig hvorfor minnesikre språk, canaries og no-execute-stacker ikke er en moteavgjørelse. --- ## 7. Sikkerhetsgelenderne som er bygget inn i daemonen `foosd` er bevisst det *dårligste* stykket programvare i dette repositoriet, så det bærer også flest gelendere: 1. **Bare loopback, håndhevet.** `foosd` nekter enhver bind-adresse utenom loopback, med mindre du gir `-L`. En setuid-root-listener på et ekte grensesnitt er en fjern root-tjeneste; avslaget er standarden, så den farlige tilstanden må skrives inn bevisst. 2. **Selvdiagnose.** Ved start logger den `ruid`/`euid` og om den kjører som root, så konsollen viser den tilstanden exploitet avhenger av. 3. **Loggen når aldri klienten.** Daemonen reserverer en privat logg-descriptor før sockets erstatter fd 1, så crash-reporter-utdata og interne stier ikke kan leses tilbake over ledningen av angriperen. 4. **Crash-reporter.** En SIGSEGV-handler logger `RIP`/`RSP` — den verdien angriperen skrev inn i returadressen — så en vellykket kapring er synlig i `foosd.log` i stedet for å være en stille død. 5. **`make unsetuid`.** Fjerning av biten er scriptet, fordi å etterlate den satt er feiltilstanden folk faktisk har. --- ## 8. Mottiltak — hva hvert stopper, og hva det *ikke* stopper Anvendt på `foosd` via `make hardened`, én om gangen eller sammen: | Mottiltak | Hva det stopper | Hva det *ikke* stopper | |------------|---------------|-------------------------| | `-fstack-protector-strong` (canary) | Overløpet: `ret` oppdager en smadret canary og aborter, før angriperens adresse brukes. Stopper her **alle fire** teknikkene — de deler det ene sårbare `read()`-et. | Ingenting ved *designet*: binærfilen er fortsatt setuid-root; en annen feil (format-streng-`%n`, heap-overflow, use-after-free) har ingen canary å utløse. | | `-fPIE -pie` (ASLR for binærfilen) | Bruk av forutsigbare `win()`/`win_root()`-adresser (ret2win-teknikkene). | Shellcode-teknikken, hvis en stack-adresse fortsatt lekker (`BUF=`-linjen). | | `-z noexecstack` (NX / W^X) | Shellcoden: CPU-en nekter å hente instruksjoner fra en data-only-side, så et hopp til `buf` er et SIGSEGV. | ROP — å kjøre kode som allerede finnes (`ret2libc`). | | Alle tre sammen | En vanskelig-å-renne-over, randomisert binærfil med ikke-kjørbar stack. Slik ser en normal hardet build ut. | Setuid-biten. **En hardet SUID-binærfil er fortsatt en SUID-binærfil.** Hvis noen nåbar minnesikkerhetsfeil overlever, er det fortsatt «feil i en root-prosess». | Konsollbeviset er `make test-hardened`, som bytter den hardnede builden inn og viser alle teknikkene dø ved canaryen, mens `foosd_hardened.log` fanger `*** stack smashing detected ***`. To mottiltak på designnivå som ingen kompilator-flag leverer, og som hovedlaboratoriet (`food`) også bruker: - **Minste privilegium.** En daemon for en uprivilegert port (2343 > 1024) har intet legitimt behov for root. En korrekt `foosd` ville binde og deretter `setgroups`/`setgid`/`setuid` til en uprivilegert konto og *bekrefte at det holdt* (den korrekte versjonen står i kilden som `drop_privs()`, aldri kalt — ikke-kallingen er laboratoriets feil nr. 3). - **Begrens read-et.** `n = read(fd, buf, sizeof(buf) - 1)`. Én korrekt linje overgår hvert kompilator-flag i tabellen. --- ## 9. Wire-protokollen (så du kan lese daemonen med netcat) ```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` — kunne ikke skrives ut som `euid=`/`ruid=`, fordi test-harnessen beviser en shell ved å greppe etter det bokstavelige `uid=`, og banneret må ikke inneholde det (en sonde som deler signatur med svaret, er en klassisk falsk-positiv-felle; se kommentaren i `foosd.c`). Harnessen krever dessuten den strenge `id`-utdataformen — `uid=NNN(...)` — så ingenting daemonen eller exploitet skriver ut kan oppfylle sjekken ved en tilfeldighet: `foosc`s eget «target euid=… ruid=…» inneholder `uid=` som delstreng, noe som en gang fikk en hardnet test til å melde en shell som aldri hadde kjørt. * `stack=`, `libc=`, `BUF=` — ASLR-leaksene: lar shellcode og ret2libc beregne eksakte adresser. --- ## 10. Øvelser 1. **Betrakt ikke-root-nedgraderingen.** Kjør `make test` *før* `make setuid`, og deretter igjen etterpå. Forklar `ROOT=SEEN`-endringen med ruid/euid-historien i §5.2. 2. **Les krasjet.** Kjør `./foosc -t demo -n` og les deretter `foosd.log`. Linjen `RIP=0x4141414141414141` er angriperens padding — beviset på at overløpet, ikke uhell, kontrollerer utførelsen. 3. **Legg til canaryen.** `make hardened` og endre selv `test-hardened`-løkken; logglinjen `*** stack smashing detected ***` er forsvaret som virker. 4. **Deaktiver leaket.** Kommentér `BUF=`-linjen i `foosd.c` ut, bygg om, og se `-t shellcode` gå fra deterministisk til et gjettespill. Den ene linjen er grunnen til at ekte ASLR-bypasser er et helt felt. 5. **`-p`-eksperimentet.** Endre i en kopi av `win()` `execl("/bin/sh", "sh", NULL)` til `execl("/bin/sh", "sh", "-p", NULL)` og observer root. `-p` er den dokumenterte nødutgangen fra shellens vakt — og grunnen til at rådet «spawn bare en shell» fra gamle write-ups er ufullstendig. 6. **Hvorfor ikke `setuid(0)`?** Omskriv shellcoden til å kalle `setuid(0)` i stedet for `setreuid(0,0)` (syscall 105). Shellen lander fortsatt — og faller fortsatt til `uid=1000`. Det er det mest lærerike én-linjes-eksperimentet i hele repositoriet. --- ## 11. Sikkerhet og opprydding - Bare loopback, som standard og etter design; `-L` binder lenger, og bare en egnet-til-å-kastes VM bør i det hele tatt vurdere det. - Dette er et root-shell-laboratorium. Ikke kjør det på en maskin som betyr noe, og pek ikke `foosc -h` mot noe du ikke eier. - Oppryddingsritual: `make stop` og deretter `make unsetuid`, og hvis du vil ha treet plettfritt igjen: `sudo make clean`. ```console $ make stop $ make unsetuid ```