foo/suid/README.DA.md

18 KiB

SUID-root-RCE-laboratorium — foosd (daemon) + foosc (exploit)

En ledsager til det overordnede laboratorium (food / fooc, en almindelig daemon, hvor et bufferoverløb giver dig en bruger-shell). Dette tilføjer den farligste en-tegns-ændring i Unix: setuid-bitten.

chmod u+s forvandler "angriberen kan køre kode på denne host" til "angriberen kan køre kode som root på denne host".

Den sætning er hele laboratoriet. Alt herunder er mekanismen under den, skrevet ned, så du, når du skriver din egen software, præcist ved, hvilke to eller tre filsystem-attributter og compiler-flag der afgør, om en hukommelsessikkerhedsfejl i din kode er en gene eller en root-shell.

Den endelige demo, når foosd er setuid-root, er en root-shell, der åbnes over netværket ved at udføre 32 bytes håndskrevet shellcode.


1. Hvad setuid-bitten rent faktisk gør

Hver proces på Linux bærer tre user-ID'er, og setuid-bitten piller ved forholdet mellem dem:

ID Navn Betydning
ruid reelle user-ID kontoen, der startede processen
euid effektive user-ID det, kernen tjekker, når den håndhæver adgang
(saved) gemte set-user-ID en "slot", en privilegeret proces må vende tilbage til senere

Et normalt program har ruid == euid. Når du udfører en binærfil med setuid-bitten sat, ejet af root:

ruid = dig        (fx. 1000, "hanez")
euid = ejeren     (fx. 0, "root")

Processen har derfor roots autoritet, selvom brugeren, der startede den, er helt almindelig. Hvert tjek, kernen udfører — kan denne proces læse /etc/shadow? skrive en fil? dræbe en anden proces? — besvares med euid, dvs. "ja, den er root".

foosd er en netværksdaemon. Den binder en port og fork()er derefter et barn per forbindelse. En fork arver euid'en, så hvert barn, der håndterer en forbindelse, også er root. Overløbet i foosds vulnerable_handler() er derfor et overløb inde i en root-proces.

Diagnosticér det selv, når daemonen kører:

$ ./foosd ...            # se den loglinje, den printer 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 et øjekast

Fil Rolle
foosd.c Den bevidst sårbare daemon (ejer fejlene). Kør som setuid-root-binærfil til root-shell-demoen.
foosc.c Exploitet. Bruger som standard den 32-byte setreuid + execve-shellcode-teknik.
shellcode.S Reference-shellcoden; make verify diff'er den mod byte-arrayet i foosc.c.
tests/pty_suid_test.c Test-harness. Driver foosc gennem et pseudo-terminal og beviser både "en shell kørte" og "den var root" (uid=0().
Makefile Build, setuid/unsetuid-hjælpere, testmatrix.
README.md Denne fil.

Hvorfor en pty? Exploitets sidste handling er at videresende din terminal til shellen, der udfører på offeret. En pipe eller her-doc lander i den forkerte ende af den videresendelse; en ægte terminal er påkrævet.


3. Hurtig start

$ make                       # byg alt, som din normale bruger
$ make setuid                # én gang, spørger om sudo: chown root + chmod u+s
$ make run                   # start foosd på 127.0.0.1:2343
$ make test-suid             # fuld matrix; shellcode + ret2win-root skal give root

Interaktiv rygeprø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 færdig:

$ make stop
$ make unsetuid             # hygiejne: efterlad aldrig en root-SUID-binærfil

4. Hvornår skal jeg sætte SUID-bitten? — svaret, du bad om

Præcis én gang, efter bygningen, før du starter daemonen til root-shell-demoerne — og kun på en maskine, der er din, velegnet til at smide væk og frakoblet netværket:

$ make            # kompilér foosd, foosc, tests
$ make setuid     # <-- ØJEBLIKKET. sudo chown root:root foosd && sudo chmod u+s foosd
$ make run        # start EFTER at have sat bitten

To regler, der betyder mere end det præcise tidspunkt:

  1. Sæt den kun, når binærfilen er færdig. Hvis du genbygger (make / make clean), efter du har sat bitten, rammer du et "Permission denied", når du skriver root-ejede outputfiler — og hvis du tvinger genbygningen, genskaber værktøjskæden filen uden s-en og fortryder stille og roligt opsætningen. Den kanoniske rækkefølge ved enhver genbygning er derfor

    $ make unsetuid && make && make setuid
    
  2. Fjern den, når du er færdig. make unsetuid. En levende, root-ejet setuid-binærfil med en udnyttelig fejl i dit træ er ikke et læremiddel, det er et root-hul med en kompileringsfejl mellem sig og ingenting. På en delt eller produktionsmaskine: lav ikke noget af dette. Daemonen nægter desuden som standard at binde andet end loopback (se §7).

Hvis du kører exploitet uden nogensinde at sætte bitten, går intet i stykker — payloaden lander stadig, og du får stadig en shell. Forskellen er i ét tal, og exploitet siger det højt:

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

Det "virker, men ikke root"-resultat er selv en del af laboratoriet. Husk det til næste afsnit.


5. Mekanismen — og drejningen, der gør SUID interessant

5.1 Overløbet (identisk med food)

foosds handler giver et read() 512 bytes tillid, mens den rækker 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 nedad. Exploitet skriver 64 bytes junk for at fylde buf, 8 for at fylde den gemte framepointer og 8 mere for at erstatte den gemte returadresse. Når vulnerable_handler udfører ret, popper CPU'en angriberens værdi ind i RIP — angriberkontrolleret kodeudførelse. Exploitet finder den præcise afstand (88 bytes for denne build) ved at parse objdump-output i stedet for at hardkode det, så tallet overlever genbygninger.

5.2 Drejningen: shellen nægter at være root

Her er det, hvor at tænke "SUID-fejl → spawn /bin/sh → root" ville gå galt, og hvorfor dette laboratorium har præcis den form, det har.

Når et setuid-root-program kører, er dets ruid stadig den startende bruger, og dets euid er root. Hvis programmet — eller angriberen — nu starter en shell:

  • execve("/bin/sh") ændrer ikke uiderne; den nye proces arver (ruid=1000, euid=0).
  • bash (og dash) tjekker præcis den tilstand 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 kigger på sig selv og dropper root — et forsvar, som shell-forfatterne byggede præcis mod dette angreb (den historiske begrundelse var setuid-shell-/setuid-script-problemet). Resultatet er "virker, men ikke root"-tilfældene:

Teknik Hvad den udfører Resulterende uid
ret2win foosds win() → execl("/bin/sh") 1000 — shell landede, root nulstillet af bash
ret2libc system("/bin/sh") → frisk sh -c '/bin/sh' 1000 — samme nulstilling, et niveau nede
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, der når root, adskiller sig fra de to, der ikke gør, med præcis én idé: de rydder den reelle uid, ikke kun den effektive.

setuid(0)          /* sætter euid til 0, men ruid forbliver 1000:
                      bash ser stadig euid != ruid og nulstiller STADIG.  */
setreuid(0, 0)     /* sætter BEGGE: ruid = euid = 0.
                      bash ser lige uider og beholder root.              */

Det er derfor, den klassiske /bin/sh-shellcode, du finder overalt på internettet, starter med et uid-ryddende syscall — og det er grunden til, at shellcoden her er 32 bytes i stedet for 23: de første fem instruktioner er

xor    edi, edi      ; ruid = 0
xor    esi, esi      ; euid = 0
push   0x71          ; 113 = __NR_setreuid
pop    rax
syscall

5.3 Så hvad er exploitet, ende til ende?

  1. foosc læser foosds banner over socket'en. Det får:
    • ids=0/1000 — euid/ruid (SUID-selvdiagnosen)
    • stack=… og libc=… — pointers (ASLR-leaksene)
    • BUF=… — den nøjagtige adresse på det buffer, den er ved at løbe over
  2. Fra target-binærfilen (via objdump) lærer det rip_off og adresserne på win() / win_root().
  3. Fra sin egen libc (via /proc/self/maps + dlsym + et hukommelsesscan) måler det offsets for system, read, /bin/sh og et pop rdi; ret-gadget — intet er hardkodet.
  4. Det samler payloaden. For -t shellcode er det: [32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf].
  5. foosds read() løber over; ret lander på shellcoden; kernen udfører setreuid(0,0) (fint: euid 0 er privilegeret) og derefter execve af /bin/sh. bash starter med ruid == euid == 0 og forbliver root.
  6. foosc videresender din terminal til den root-shell, indtil du skriver exit.

Én bekvemmelighedsdetalje, der koster folk meget tid, hvis den overses: exploitet tester hver uid-ryddende adfærd uden først at have brug for setuid-bitten. Kør make test før make setuid, og du vil se hver teknik lande en shell, mens ROOT=MISSING står; kør make test-suid efter make setuid, og ROOT=SEEN dukker op ved de to teknikker, der rydder den reelle uid. Det A/B er hele lektionen, udførligt på ti sekunder.


6. De gamle one-liners — og hvorfor de fleste af dem er døde

Har du læst om SUID, har du læst om PATH-kapring, LD_PRELOAD og setuid-shells. Alle tre er klassiske, og alle tre fejler på et moderne system mod dette program. Det er værd at vide præcis hvorfor, fordi grundene er de forsvar, du får gratis:

Angrebsklasse Gamle påstand Hvorfor den fejler på en moderne maskine
LD_PRELOAD af et ondsindet bibliotek "Setuid-programmet loader min .so og kører min kode som root." Kernen markerer en setuid-binærfil som AT_SECURE; glibc ignorerer derefter LD_PRELOAD, LD_LIBRARY_PATH, LD_DEBUG og venner. Miljøet behandles som utroverdig input. LD_PRELOAD mod en setuid-binærfil er en no-op.
PATH-kapring (system("ls") med en forgiftet PATH) "Peg PATH mod et bibliotek med min falske ls; root-programmet kører den." Et andet ansigt af samme forsvar: en AT_SECURE-proces får en saneret PATH (en sikker standard, nogenlunde /usr/local/bin:/usr/bin:/bin) til system()/execvp, så det forgiftede bibliotek aldrig konsulteres.
Setuid-system()-kommandoinjektion "Den injicerede kommando kører med euid 0." system() kører kommandoen i en frisk /bin/sh, og den shell — §5.2 — nulstiller euid = ruid ved start. Den injicerede kommando udføres med den reelle uid. (Det er stadig en fejl; den eskalerer bare ikke længere gennem /bin/sh.)
Setuid-root-shell på disken (cp /bin/sh /tmp; chmod u+s) "Kør den, få root." Præcis forsvaret ovenfor, og det er grunden til, at moderne distroer ikke leverer nogen setuid-root-shell. Selv når det lykkes at lave én, nægter bash at beholde euid 0, medmindre den startes med -p.

Hvad der forbliver i live, og det er dette laboratorium: programmet er allerede root, når det kører. Du behøver ikke miljøet eller system(); du har brug for, at programmet udfører din kode (via en hukommelseskorruptionsfejl), mens det er privilegeret, og din kode skal være omhyggelig nok til selv at rette uid-mismatchet — setreuid(0,0) — før den overrækker dig en shell. Hukommelseskorruption + SUID er kombinationen, der stadig ender i uid=0, hvilket er præcis hvorfor hukommelsessikre sprog, canaries og no-execute-stacks ikke er en modebeslutning.


7. De sikkerhedsgelændere, der er bygget ind i daemonen

foosd er bevidst det dårligste stykke software i dette repository, så det bærer også flest gelændere:

  1. Kun loopback, håndhævet. foosd nægter enhver bind-adresse ud over loopback, medmindre du giver -L. En setuid-root-listener på en rigtig grænseflade er en fjern root-tjeneste; afslaget er standarden, så den farlige tilstand skal skrives bevidst ind.
  2. Selvdiagnose. Ved start logger den ruid/euid og om den kører som root, så konsollen viser den tilstand, exploitet afhænger af.
  3. Loggen når aldrig klienten. Daemonen reserverer en privat log-descriptor, før sockets erstatter fd 1, så crash-reporter-output og interne stier ikke kan læses tilbage over ledningen af angriberen.
  4. Crash-reporter. En SIGSEGV-handler logger RIP/RSP — den værdi, angriberen skrev ind i returadressen — så en vellykket kapring er synlig i foosd.log i stedet for at være en stille død.
  5. make unsetuid. Fjernelse af bitten er scriptet, fordi at efterlade den sat er den fiaskotilstand, folk rent faktisk har.

8. Modforanstaltninger — hvad hver stopper, og hvad den ikke stopper

Anvendt på foosd via make hardened, én ad gangen eller sammen:

Modforanstaltning Hvad den stopper Hvad den ikke stopper
-fstack-protector-strong (canary) Overløbet: ret opdager en smadret canary og abort'er, før angriberens adresse bruges. Stopper her alle fire teknikker — de deler det ene sårbare read(). Intet ved designet: binærfilen er stadig setuid-root; en anden fejl (format-string-%n, heap-overflow, use-after-free) har ingen canary at udløse.
-fPIE -pie (ASLR for binærfilen) Brug af forudsigelige win()/win_root()-adresser (ret2win-teknikkerne). Shellcode-teknikken, hvis en stack-adresse stadig lækker (BUF=-linjen).
-z noexecstack (NX / W^X) Shellcoden: CPU'en nægter at hente instruktioner fra en data-only-side, så et hop til buf er et SIGSEGV. ROP — at køre kode, der allerede findes (ret2libc).
Alle tre sammen En svær-at-overløbe, randomiseret binærfil med ikke-eksekverbar stack. Sådan ser en normal hærdet build ud. Setuid-bitten. En hærdet SUID-binærfil er stadig en SUID-binærfil. Hvis nogen nåbar hukommelsessikkerhedsfejl overlever, er det stadig "fejl i en root-proces".

Konsolbeviset er make test-hardened, som bytter den hærdede build ind og viser alle teknikker dø ved canaryen, mens foosd_hardened.log optager *** stack smashing detected ***.

To designniveau-modforanstaltninger, som intet compiler-flag leverer, og som det overordnede laboratorium (food) også bruger:

  • Least privilege. En daemon til en uprivilegeret port (2343 > 1024) har intet legitimt behov for root. En korrekt foosd ville binde og derefter setgroups/setgid/setuid til en uprivilegeret konto og bekræfte, at det holdt (den korrekte version står i kilden som drop_privs(), aldrig kaldt — ikke-kaldelsen er laboratoriets fejl nr. 3).
  • Begræns read'et. n = read(fd, buf, sizeof(buf) - 1). Én korrekt linje overgår hvert compiler-flag i tabellen.

9. Wire-protokollen (så du kan læse 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 printes som euid=/ruid=, fordi test-harnessen beviser en shell ved at greppe efter det bogstavelige uid=, og banneret må ikke indeholde det (en sonde, der deler signatur med svaret, er en klassisk falsk-positiv-fælde; se kommentaren i foosd.c). Harnessen kræver desuden den strenge id-outputform — uid=NNN(...) — så intet, daemonen eller exploitet printer, kan opfylde tjekket ved et tilfælde: fooscs eget "target euid=… ruid=…" indeholder uid= som delstreng, hvilket engang fik en hærdet test til at melde en shell, der aldrig havde kørt.
  • stack=, libc=, BUF= — ASLR-leaksene: lader shellcode og ret2libc beregne eksakte adresser.

10. Øvelser

  1. Betragt ikke-root-nedgraderingen. Kør make test før make setuid, og derefter igen bagefter. Forklar ROOT=SEEN-ændringen med ruid/euid-historien i §5.2.
  2. Læs nedbruddet. Kør ./foosc -t demo -n og læs derefter foosd.log. Linjen RIP=0x4141414141414141 er angriberens padding — beviset på, at overløbet, ikke uheld, kontrollerer udførelsen.
  3. Tilføj canaryen. make hardened og ændr selv test-hardened-løkken; loglinjen *** stack smashing detected *** er forsvaret, der virker.
  4. Deaktiver leaket. Kommentér BUF=-linjen i foosd.c ud, genbyg, og se -t shellcode gå fra deterministisk til et gættespil. Den ene linje er grunden til, at ægte ASLR-bypasses er et helt felt.
  5. -p-eksperimentet. Ændr i en kopi af win() execl("/bin/sh", "sh", NULL) til execl("/bin/sh", "sh", "-p", NULL) og observer root. -p er den dokumenterede nødudgang fra shellens vagt — og grunden til, at rådet "spawn bare en shell" fra gamle write-ups er ufuldstændigt.
  6. Hvorfor ikke setuid(0)? Omskriv shellcoden til at kalde setuid(0) i stedet for setreuid(0,0) (syscall 105). Shellen lander stadig — og falder stadig til uid=1000. Det er det mest lærerige en-linjes-eksperiment i hele repositoryet.

11. Sikkerhed og oprydning

  • Kun loopback, som standard og efter design; -L binder længere, og kun en velegnet-til-at-smid-væk-VM bør overhovedet overveje det.
  • Dette er et root-shell-laboratorium. Kør det ikke på en maskine, der betyder noget, og peg ikke foosc -h mod noget, du ikke ejer.
  • Oprydningsritual: make stop og derefter make unsetuid, og hvis du vil have træet pletfrit igen: sudo make clean.
$ make stop
$ make unsetuid