309 lines
14 KiB
Markdown
309 lines
14 KiB
Markdown
# `wosuid`-laboratoriet — root-RCE **uten** setuid-bit
|
||
|
||
```
|
||
foowosd en bevisst sårbar daemon som er root fordi den ble *STARTET* som
|
||
root (port 2344, bare loopback som standard)
|
||
foowosc exploitet: forvandler et stack-overløp til en **root**-shell ved å
|
||
utføre shellcode — de samme 23 bytene som knekte user-level-
|
||
`food`-daemonen i hovedlaboratoriet
|
||
```
|
||
|
||
Det er det tredje laboratoriet i serien. Samme exploit-verktøykjede, samme
|
||
stil, én fundamental forskjell:
|
||
|
||
| Laboratorium | hvordan målprosessen blir root | `uid=0(root)`-shell? |
|
||
|----------|------------------------------------------------|----------------------|
|
||
| food/fooc| aldri — det er en vanlig user-daemon | nei |
|
||
| foosd/foosc | SUID-biten (`chmod u+s`) — euid 0, ruid 1000 | ja (krever `setreuid` i shellcoden, fordi bash nullstiller euid→ruid) |
|
||
| **foowosd/foowosc** | **ingen — root *starter* daemonen** (sudo / systemd `User=root`) | **ja (vanlig `execve`-shellcode)** |
|
||
|
||
Setuid-biten er et *transportmiddel* for privilegier — ikke privilegiene selv.
|
||
En daemon startet av root har reell, effektiv og lagret uid alle lik 0. For
|
||
kjernen er det root, punktum; den kan og vil ikke vite om prosessen kom dit via
|
||
`+s` på en fil eller via `sudo ./foowosd`. Overløpet i en root-startet daemon er
|
||
altså et root-exploit — *«jeg har ingen SUID-binærfiler» er ikke det samme som
|
||
«jeg er ikke utnyttbar».*
|
||
|
||
Det er hele leksjonen i dette laboratoriet. Alt nedenfor er maskineriet.
|
||
|
||
---
|
||
|
||
## Rask start (det brukeren ba om)
|
||
|
||
```
|
||
cd wosuid
|
||
make # bygger daemonen, exploitet og test-harnessen
|
||
```
|
||
|
||
### Det ekte — kjør daemonen som **root**
|
||
|
||
```
|
||
sudo make run-root # starter foowosd som uid 0 (prosess, ikke filtillstand)
|
||
make test-root # hver teknikk skal nå gi uid=0(root)
|
||
```
|
||
|
||
### Ikke sudo? Den identiske kjernesti via en user-namespace
|
||
|
||
```
|
||
make run-root-ns # uid 0 i en user-namespace — ingen passord nødvendig
|
||
make test-root # samme dommer; brukes av CI og alle uten sudo
|
||
```
|
||
|
||
### Baseline — daemon som din vanlige bruker (intet root noen steder)
|
||
|
||
```
|
||
make run # foowosd kjører med dine uid-er
|
||
make test # exploits lander shells, men `root` forventes MISSING
|
||
```
|
||
|
||
### Oppryddingsritual (alltid: dette er et root-shell-laboratorium)
|
||
|
||
```
|
||
make stop
|
||
```
|
||
|
||
`foowosd` er *ikke* setuid, og intet i dette biblioteket gjør noen gang
|
||
`chmod +s` — det er poenget. Den farlige tilstanden er **prosessen**, ikke
|
||
filen.
|
||
|
||
---
|
||
|
||
## Når du blir bedt om å sette SUID-biten
|
||
|
||
Det blir du **ikke**. Dette laboratoriet har bevisst ingen SUID-bit:
|
||
|
||
- `foowosd` bygges, eies og har vanlige tillstander som ethvert annet program.
|
||
- Det blir root, som ekte daemoner gjør — ved å bli *startet* av root.
|
||
- `make run-root` bruker `sudo` til nøyaktig det, og `make run-root-ns` skaffer
|
||
en ekte uid-0-prosess helt uten noe av det.
|
||
|
||
Suid-biten tilhører *søster*-laboratoriet (`foosd`). Kontrasten mellom de to
|
||
er pensum:
|
||
|
||
1. SUID-laboratoriet: biten gir **euid 0, men ruid 1000** →
|
||
`execve("/bin/sh")` nedgraderes av bashs vakt (`euid != ruid` → reset) →
|
||
shellcoden må først kalle `setreuid(0,0)` (32-byte-payload).
|
||
2. Dette laboratoriet: root **starter** prosessen → **ruid == euid == 0** →
|
||
vakten har ingenting å nullstille → den vanlige 23-byte `execve`-shellcoden
|
||
beholder root.
|
||
|
||
Samme overløp. Samme teknikk. Forskjellig *opprinnelse* av privilegier,
|
||
annerledes payload-form. Det er leksjonen i miniatyr.
|
||
|
||
---
|
||
|
||
## Protokollen
|
||
|
||
Uansett hvilken slags klient som kobler til, hilser foowosd på den med:
|
||
|
||
```
|
||
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (banneret + leaks)
|
||
BUF=0x7ffd… (bufferadressen)
|
||
```
|
||
|
||
`ids=euid/ruid` er *«er jeg root?»*-sidekanalen. foowosc skriver ut en høy
|
||
advarsel når euid ikke er 0 (dvs. du startet daemonen som en vanlig bruker):
|
||
payloaden lander fortsatt, men shellen blir en bruker-shell, og å kalle
|
||
exploitet «i stykker» ville vært feil — det eskalerer bare ikke.
|
||
|
||
> Staveform-merknad: `ids=`, ikke `euid=`/`ruid=`. Test-harnessen beviser en
|
||
> levende `id` ved å matche den bokstavelige formen `uid=NNN(`, så banneret må
|
||
> aldri inneholde en delstreng som selv oppfyller sjekken. (I SUID-laboratoriet
|
||
> produserte nøyaktig den fellen et spektakulært falskt positivt.)
|
||
|
||
---
|
||
|
||
## Exploit-teknikkene (`foowosc -t …`)
|
||
|
||
Alle fire exploit-stiene nedenfor virker mot foowosd. Når daemonen er root,
|
||
gir **alle som spawner noe, root** — i motsetning til SUID-laboratoriet der
|
||
ret2win/ret2libc stille og rolig ble nedgradert til uid 1000 av bashs vakt. Her
|
||
er det intet mismatch å vokte mot.
|
||
|
||
| `-t` | hva som skjer | når daemonen er root |
|
||
|---------------|---------------------------------------------------------------------|---------------------|
|
||
| `shellcode` | 23-byte-`execve("/bin/sh", NULL, NULL)` kjører på stacken. | **root-shell** (standard) |
|
||
| `ret2win` | hopp til `win()` → `execl("/bin/sh")` | **root-shell** |
|
||
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root-shell** |
|
||
| `demo` | bare søppel-overløp — forvent et SIGSEGV i daemonloggen | krasj, etter design |
|
||
| `leak` | skriver bare ut leaks, sender ingen payload | n/a |
|
||
|
||
```
|
||
./foowosc -t shellcode # interaktiv; standard-target 127.0.0.1:2344
|
||
./foowosc -t shellcode -n # send og rapporter, ingen interaktiv økt
|
||
```
|
||
|
||
En vellykket interaktiv økt videresender terminalen din til shellen *på
|
||
offeret* — det er nøyaktig én shell i bildet, og det er `/bin/sh` som kjører
|
||
som root inne i foowosd. Skriv `id` for å se `uid=0(root)`.
|
||
|
||
### Hvorfor det ikke finnes noen `ret2win-root`-teknikk her
|
||
|
||
foosc hadde én — den hoppet til et `win_root()` som kalte `setreuid(0,0)` før
|
||
exec, fordi en setuid-prosess kjørte med en reell uid som fortsatt sa 1000. En
|
||
root-*startet* prosess har den reelle uid-en allerede 0; det er ingenting å
|
||
rydde, så den ekstra funksjonen og teknikken ville ikke lært noe. Fjernet.
|
||
|
||
---
|
||
|
||
## Hva foowosc gjør, steg for steg
|
||
|
||
1. **Statisk analyse** — `objdump -d` av `./foowosd`. Finner
|
||
`vulnerable_handler`, `win()`, det `lea -0x50(%rbp)` som adresserer `buf`,
|
||
og det første nakne `ret`. Ut fra forskyvningen beregner det
|
||
`rip_off = 80 + 8 = 88`. Ingenting er hardkodet; det overlever en ombygging.
|
||
2. **Selv-introspeksjon** — leser sitt eget `/proc/self/maps` og `dlsym()`er
|
||
`system`/`read` for å lære libc-*offsets*. Targetets libc-base er
|
||
`leaked_read − off_read`, deretter `system = base + off_system`, osv. Denne
|
||
delta-aritmetikken er grunnen til at exploits overlever libc-versjoner.
|
||
3. **Koble til** — leser banneret/leaks (`ids=`, `stack=`, `libc=`, `BUF=`).
|
||
4. **Bygg payloaden** — for `shellcode`: 23 bytes maskinkode, padding til
|
||
`rip_off`, deretter den lagrede RIP = `buf` (så `ret` hopper inn på koden).
|
||
For `ret2win`/`ret2libc`: adresser beregnet ut fra analysen — ingen
|
||
utførelse av stacken nødvendig.
|
||
5. **Justeringsfixen** — et kapret nakent `ret` gir callee-en
|
||
`rsp ≡ 8 (mod 16)`, og glibcs SSE2-kode `movaps`-feiler på en ikke-justert
|
||
stack (crash-reporteren logger `si_addr=(nil)` — fingerpeket). foowosc
|
||
setter inn ett ekstra `ret`-gadget før det ekte målet og gjenoppretter
|
||
invarianten. Silkehansketeknikk i et shellcode-laboratorium, men det er
|
||
forskjellen mellom en payload som «noen ganger virker» og en som alltid
|
||
virker.
|
||
6. **Send, deretter videresend** — offerprosessen *er* shellen; denne prosessen
|
||
splisser bare bytes. Ingen lokal shell, ingen annen leser —
|
||
single-read-cursor-feilen (ett spist byte per chunk) er dokumentert i
|
||
`become_shell()`.
|
||
|
||
---
|
||
|
||
## Daemonens bevisste feil (alle i `foowosd.c`, alle ekte CWE-klasser)
|
||
|
||
| # | feil | CWE | notat |
|
||
|---|-----|-----|------|
|
||
| 1 | `read(fd, buf, 512)` inn i et 64-byte stack-buffer | [CWE-120](https://cwe.mitre.org/data/definitions/120.html) | overløpet: 448 bytes forbi `buf`, lagret RIP ved +88 |
|
||
| 2 | bare `snprintf(line, …, "%.*s", …)`; men angriper-`%` i ekko-stien | [CWE-134](https://cwe.mitre.org/data/definitions/134.html) | leaket er her den reelle payloaden; et `%n` i en *root*-prosess ville vært write-what-where som root |
|
||
| 3 | barn beholder root mens de håndterer upålitelige inputs | [CWE-271](https://cwe.mitre.org/data/definitions/271.html) | det korrekte `drop_privs()` (setgroups→setgid→setuid, i den rekkefølgen, med verifikasjon) står i filen, kommentert, *bevisst aldri kalt* |
|
||
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` avslørt til enhver klient | [CWE-120](https://cwe.mitre.org/data/definitions/120.html) | uten disse leaks kunne ikke shellcode- og ret2libc-teknikkene beregne adresser (ASLR ville beseiret dem) |
|
||
|
||
Handleren har nøyaktig samme `buf[64]`/`read(512)`-form som de to andre
|
||
laboratoriene, så den felles objdump-baserte oppdagelsespipelinen virker
|
||
uendret.
|
||
|
||
---
|
||
|
||
## Slik inspiserer du daemonen (læringssti)
|
||
|
||
```
|
||
make status # kjører den? som hvilken uid? filtillstand vises
|
||
make run-root # eller run / run-root-ns
|
||
./foowosc -t leak # se banneret og leaks, send ingenting
|
||
./foowosc -t demo # søppel-overløp -> SIGSEGV, logget med RIP/rsp
|
||
./foowosc -t shellcode # den interaktive root-shellen
|
||
make test-root # full matrise, alle teknikkene, --must-root
|
||
```
|
||
|
||
Crash-reporter: Ved SIGSEGV logger daemonen feiladressen, RIP og RSP. Et `ret`
|
||
inn i en ikke-kanonisk `0x4141…` feiler ved `ret`-et selv (RIP som `0x4028xx`,
|
||
`si_addr=(nil)`) — verdt å vite før du feilleser en logglinje som en
|
||
NULL-dereferanse.
|
||
|
||
---
|
||
|
||
## Hvorfor shellcoden er 23 bytes, ikke 32
|
||
|
||
```
|
||
31 f6 xor esi, esi ; argv = NULL
|
||
31 d2 xor edx, edx ; envp = NULL
|
||
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\\0"
|
||
57 push rdi
|
||
48 89 e7 mov rdi, rsp
|
||
6a 3b push 0x3b ; 59 = execve
|
||
58 pop rax
|
||
0f 05 syscall
|
||
```
|
||
|
||
SUID-laboratoriet trenger `setreuid(0,0)` foran dette. Dette laboratoriet ikke,
|
||
av den grunnen som gjentas overalt: **ruid er allerede 0**, fordi root startet
|
||
prosessen. `make verify` beviser at bytene i `foowosc.c` er byte-for-byte det
|
||
`shellcode.S` assemblerer til.
|
||
|
||
---
|
||
|
||
## Mottiltak — hva `make hardened` endrer
|
||
|
||
Hardet build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
|
||
|
||
| teknikk | sårbar `foowosd` | hardet `foowosd_hardened` |
|
||
|-----------|----------------------|------------------------------|
|
||
| `shellcode` | root-shell (kjørbar stack) | SIGSEGV ved canary-sjekken / NX |
|
||
| `ret2win` / `ret2libc` | root-shell | canary aborter `ret` — men merk: en *PIE*-build gjør også disse adressene tilfeldige |
|
||
| `demo` | SIGSEGV, logget | SIGSEGV, logget |
|
||
|
||
`make test-hardened` demonstrerer det live. Den viktige observasjonen er ikke
|
||
bare at mottiltakene drepte teknikkene — det er at de **ikke** gjorde daemonen
|
||
til «ikke-root». En hardet build som fortsatt *startes* som root, er fortsatt en
|
||
root-daemon; mottiltaket hever bare lista for angriperen. Minste privilegium
|
||
(`drop_privs()`) og minnesikkerhet er to forskjellige feil, og en daemon som
|
||
ikke trenger root, bør ikke ha det.
|
||
|
||
---
|
||
|
||
## Sikkerhetsgelendere (samme politikk som SUID-laboratoriet)
|
||
|
||
- **Bare loopback.** foowosd nekter å binde noe annet enn `127.0.0.1` /
|
||
`localhost` / `::1`, med mindre du gir `-L`. En root-daemon på et ekte
|
||
grensesnitt er en *fjern* root-tjeneste. `-L` finnes bare for å vise vakten;
|
||
ikke bruk den på noe som betyr noe.
|
||
- **Tilstanden logges høyt.** Ved start skriver den ut `ruid/euid` og om dette
|
||
er en root-prosess, så du alltid vet hvilket exploit-resultat du skal vente.
|
||
- **Dommer kommer fra exit-status** i `make test*` (pty-harnessens
|
||
returkode), aldri fra å greppe stdout-en dens — grepbar utdata lyver.
|
||
- **pty-en må kjøre cooked + ECHO off**, ellers ekkoer harnessen sin egen
|
||
kommandolinje og forfalsker markøren. Harnessen slår av ECHO og beholder
|
||
ECHONL på.
|
||
- **Oppryddingsritual:** `make stop` etter hver økt. Hvis daemonen er
|
||
root-eid, sier `stop` deg å kjøre `sudo pkill -x foowosd`.
|
||
- Kjør aldri dette på en vert du bryr deg om. Det finnes for å dele ut
|
||
`uid=0`-shells over loopback-grensesnittet.
|
||
|
||
---
|
||
|
||
## Øvelser
|
||
|
||
1. Kjør `make run` (user-daemon), deretter `./foowosc -t shellcode`. Hvorfor er
|
||
shellen ikke root? (Sjekk `ids=` i banneret — foowosc sier deg det før du i
|
||
det hele tatt kobler til.)
|
||
2. `make stop && sudo make run-root && make test-root`. Forklar ut fra
|
||
bannerlinjen hvorfor alle fire teknikkene nå gir `uid=0(root)`.
|
||
3. Finn i `foowosd.c` `drop_privs()` og les *hvorfor rekkefølgen* av
|
||
`setgroups → setgid → setuid` betyr noe. Bestem hvor i `main()` den ville
|
||
hørt hjemme, og hva laboratoriets angrepsflate blir når den faktisk kalles.
|
||
4. Beregn `rip_off` for hånd ut fra `objdump -d foowosd`: finn `buf`s
|
||
`lea -0xNN(%rbp)` inne i `vulnerable_handler`, deretter `NN + 8`. foowosc
|
||
gjør nøyaktig det; sjekk regnestykket dens mot ditt eget.
|
||
5. `make hardened && make test-hardened`. Hvilken teknikk faller for canaryen,
|
||
og hvilken for NX? Hvorfor endrer ikke hærdning hva `make status` rapporterer
|
||
om *prosessen*?
|
||
6. Sammenlign de to laboratorienes shellcodes: 23 bytes her, 32 for foosd. Hva
|
||
gjør de ekstra 9 bytene, og hvorfor trengs de bare i SUID-tilfellet?
|
||
7. Les `become_shell()`s kommentar om den enkelte lese-cursoren. Gjenskap
|
||
feiltilstanden mentalt: to lesere på én socket betyr at login-shellen spiser
|
||
ett byte per chunk — «uid=1000…» ankommer som «id=1000…». Hvorfor kan en
|
||
relay-prosess aldri ha denne feilen?
|
||
|
||
---
|
||
|
||
## Filer
|
||
|
||
```
|
||
foowosd.c den sårbare root-daemonen (hver linje kommentert)
|
||
foowosc.c exploitet (hver linje kommentert)
|
||
shellcode.S referanse-assembly for 23-byte-payloaden
|
||
tests/pty_wosuid_test.c pty-harnessen (markør + streng id-form-sjekk)
|
||
Makefile build / run / run-root / run-root-ns / test /
|
||
test-root / verify / hardened / clean …
|
||
```
|
||
|
||
Søster-laboratorier: `../food.c`/`../fooc.c` (user-level-baseline, port 2342)
|
||
og `../suid/` (SUID-root-daemon `foosd`/`foosc`, port 2343). Portene er bevisst
|
||
forskjellige — du kan kjøre alle tre samtidig og kryssjekke `ids=`-linjene
|
||
deres i banneret.
|