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+sforvandler «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:
-
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 utens-en og angrer stille og rolig oppsettet. Den kanoniske rekkefølgen ved enhver ombygging er derfor$ make unsetuid && make && make setuid -
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?
fooscleserfoosds banner over socketen. Det får:ids=0/1000— euid/ruid (SUID-selvdiagnosen)stack=…oglibc=…— pekere (ASLR-leaksene)BUF=…— den nøyaktige adressen på bufferen den er i ferd med å renne over
- Fra målbinærfilen (via
objdump) lærer detrip_offog adressene tilwin()/win_root(). - Fra sin egen libc (via
/proc/self/maps+dlsym+ et minnesøk) måler det offsetene forsystem,read,/bin/shog etpop rdi; ret-gadget — ingenting er hardkodet. - Det setter sammen payloaden. For
-t shellcodeer det:[32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf]. foosdsread()renner over;retlander på shellcoden; kjernen utførersetreuid(0,0)(helt greit: euid 0 er privilegert) og deretterexecveav/bin/sh. bash starter medruid == euid == 0og forblir root.fooscvideresender terminalen din til den root-shellen, til du skriverexit.
É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:
- Bare loopback, håndhevet.
foosdnekter 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. - Selvdiagnose. Ved start logger den
ruid/euidog om den kjører som root, så konsollen viser den tilstanden exploitet avhenger av. - 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.
- Crash-reporter. En SIGSEGV-handler logger
RIP/RSP— den verdien angriperen skrev inn i returadressen — så en vellykket kapring er synlig ifoosd.logi stedet for å være en stille død. 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
foosdville binde og derettersetgroups/setgid/setuidtil en uprivilegert konto og bekrefte at det holdt (den korrekte versjonen står i kilden somdrop_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 someuid=/ruid=, fordi test-harnessen beviser en shell ved å greppe etter det bokstaveligeuid=, og banneret må ikke inneholde det (en sonde som deler signatur med svaret, er en klassisk falsk-positiv-felle; se kommentaren ifoosd.c). Harnessen krever dessuten den strengeid-utdataformen —uid=NNN(...)— så ingenting daemonen eller exploitet skriver ut kan oppfylle sjekken ved en tilfeldighet:fooscs eget «target euid=… ruid=…» inneholderuid=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
- Betrakt ikke-root-nedgraderingen. Kjør
make testførmake setuid, og deretter igjen etterpå. ForklarROOT=SEEN-endringen med ruid/euid-historien i §5.2. - Les krasjet. Kjør
./foosc -t demo -nog les deretterfoosd.log. LinjenRIP=0x4141414141414141er angriperens padding — beviset på at overløpet, ikke uhell, kontrollerer utførelsen. - Legg til canaryen.
make hardenedog endre selvtest-hardened-løkken; logglinjen*** stack smashing detected ***er forsvaret som virker. - Deaktiver leaket. Kommentér
BUF=-linjen ifoosd.cut, bygg om, og se-t shellcodegå fra deterministisk til et gjettespill. Den ene linjen er grunnen til at ekte ASLR-bypasser er et helt felt. -p-eksperimentet. Endre i en kopi avwin()execl("/bin/sh", "sh", NULL)tilexecl("/bin/sh", "sh", "-p", NULL)og observer root.-per den dokumenterte nødutgangen fra shellens vakt — og grunnen til at rådet «spawn bare en shell» fra gamle write-ups er ufullstendig.- Hvorfor ikke
setuid(0)? Omskriv shellcoden til å kallesetuid(0)i stedet forsetreuid(0,0)(syscall 105). Shellen lander fortsatt — og faller fortsatt tiluid=1000. Det er det mest lærerike én-linjes-eksperimentet i hele repositoriet.
11. Sikkerhet og opprydding
- Bare loopback, som standard og etter design;
-Lbinder 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 -hmot noe du ikke eier. - Oppryddingsritual:
make stopog derettermake unsetuid, og hvis du vil ha treet plettfritt igjen:sudo make clean.
$ make stop
$ make unsetuid