foo/suid/README.NO.md
2026-09-29 09:39:24 +02:00

18 KiB

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:

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 foosds vulnerable_handler() er derfor et overløp inne i en root-prosess.

Diagnostiser det selv når daemonen kjører:

$ ./foosd ...            # se logglinjen den skriver ut ved start
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process

og fra exploitet:

$ ./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

$ 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:

$ ./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:

$ 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:

$ 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

    $ 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:

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)

foosds handler gir et read() 512 bytes tillit, mens den rekker den et 64-byte stack-buffer:

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 foosds 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 foosds 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.

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

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 foosds 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. foosds 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)

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: fooscs 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.
$ make stop
$ make unsetuid