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+sforvandler "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:
-
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 udens-en og fortryder stille og roligt opsætningen. Den kanoniske rækkefølge ved enhver genbygning er derfor$ make unsetuid && make && make setuid -
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?
foosclæserfoosds banner over socket'en. Det får:ids=0/1000— euid/ruid (SUID-selvdiagnosen)stack=…oglibc=…— pointers (ASLR-leaksene)BUF=…— den nøjagtige adresse på det buffer, den er ved at løbe over
- Fra target-binærfilen (via
objdump) lærer detrip_offog adresserne påwin()/win_root(). - Fra sin egen libc (via
/proc/self/maps+dlsym+ et hukommelsesscan) måler det offsets forsystem,read,/bin/shog etpop rdi; ret-gadget — intet er hardkodet. - Det samler payloaden. For
-t shellcodeer det:[32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf]. foosdsread()løber over;retlander på shellcoden; kernen udførersetreuid(0,0)(fint: euid 0 er privilegeret) og derefterexecveaf/bin/sh. bash starter medruid == euid == 0og forbliver root.fooscvideresender din terminal til den root-shell, indtil du skriverexit.
É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:
- Kun loopback, håndhævet.
foosdnæ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. - Selvdiagnose. Ved start logger den
ruid/euidog om den kører som root, så konsollen viser den tilstand, exploitet afhænger af. - 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.
- Crash-reporter. En SIGSEGV-handler logger
RIP/RSP— den værdi, angriberen skrev ind i returadressen — så en vellykket kapring er synlig ifoosd.logi stedet for at være en stille død. 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
foosdville binde og dereftersetgroups/setgid/setuidtil en uprivilegeret konto og bekræfte, at det holdt (den korrekte version står i kilden somdrop_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 someuid=/ruid=, fordi test-harnessen beviser en shell ved at greppe efter det bogstaveligeuid=, og banneret må ikke indeholde det (en sonde, der deler signatur med svaret, er en klassisk falsk-positiv-fælde; se kommentaren ifoosd.c). Harnessen kræver desuden den strengeid-outputform —uid=NNN(...)— så intet, daemonen eller exploitet printer, kan opfylde tjekket ved et tilfælde:fooscs eget "target euid=… ruid=…" indeholderuid=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
- Betragt ikke-root-nedgraderingen. Kør
make testførmake setuid, og derefter igen bagefter. ForklarROOT=SEEN-ændringen med ruid/euid-historien i §5.2. - Læs nedbruddet. Kør
./foosc -t demo -nog læs derefterfoosd.log. LinjenRIP=0x4141414141414141er angriberens padding — beviset på, at overløbet, ikke uheld, kontrollerer udførelsen. - Tilføj canaryen.
make hardenedog ændr selvtest-hardened-løkken; loglinjen*** stack smashing detected ***er forsvaret, der virker. - Deaktiver leaket. Kommentér
BUF=-linjen ifoosd.cud, genbyg, og se-t shellcodegå fra deterministisk til et gættespil. Den ene linje er grunden til, at ægte ASLR-bypasses er et helt felt. -p-eksperimentet. Ændr i en kopi afwin()execl("/bin/sh", "sh", NULL)tilexecl("/bin/sh", "sh", "-p", NULL)og observer root.-per den dokumenterede nødudgang fra shellens vagt — og grunden til, at rådet "spawn bare en shell" fra gamle write-ups er ufuldstændigt.- Hvorfor ikke
setuid(0)? Omskriv shellcoden til at kaldesetuid(0)i stedet forsetreuid(0,0)(syscall 105). Shellen lander stadig — og falder stadig tiluid=1000. Det er det mest lærerige en-linjes-eksperiment i hele repositoryet.
11. Sikkerhed og oprydning
- Kun loopback, som standard og efter design;
-Lbinder 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 -hmod noget, du ikke ejer. - Oprydningsritual:
make stopog dereftermake unsetuid, og hvis du vil have træet pletfrit igen:sudo make clean.
$ make stop
$ make unsetuid