foo/wosuid/README.DK.md

309 lines
14 KiB
Markdown
Raw Normal View History

2026-09-29 09:39:24 +02:00
# `wosuid`-laboratoriet — root-RCE **uden** setuid-bit
```
foowosd en bevidst sårbar daemon, der er root, fordi den blev *STARTET* som
root (port 2344, kun loopback som standard)
foowosc exploitet: forvandler et stack-overløb til en **root**-shell ved at
udføre shellcode — de samme 23 bytes, der knækkede user-level-
`food`-daemonen i det overordnede laboratorium
```
Det er det tredje laboratorium i serien. Samme exploit-værktøjskæde, samme
stil, én fundamental forskel:
| Laboratorium | hvordan target-processen bliver root | `uid=0(root)`-shell? |
|----------|------------------------------------------------|----------------------|
| food/fooc| aldrig — det er en almindelig user-daemon | nej |
| foosd/foosc | SUID-bitten (`chmod u+s`) — euid 0, ruid 1000 | ja (kræver `setreuid` i shellcoden, fordi bash nulstiller euid→ruid) |
| **foowosd/foowosc** | **ingen — root *starter* daemonen** (sudo / systemd `User=root`) | **ja (almindelig `execve`-shellcode)** |
Setuid-bitten er et *transportmiddel* for privilegier — ikke privilegierne
selv. En daemon startet af root har reelle, effektive og gemte uid alle lig 0.
For kernen er det root, punktum; den kan og vil ikke vide, om processen kom
derhen via `+s` på en fil eller via `sudo ./foowosd`. Overløbet i en
root-started daemon er altså et root-exploit — *"jeg har ingen
SUID-binærfiler" er ikke det samme som "jeg er ikke udnyttelig".*
Det er hele lektionen i dette laboratorium. Alt herunder er maskineriet.
---
## Hurtig start (hvad brugeren bad om)
```
cd wosuid
make # bygger daemonen, exploitet og test-harnessen
```
### Det ægte — kør daemonen som **root**
```
sudo make run-root # starter foowosd som uid 0 (proces, ikke filtilstand)
make test-root # hver teknik skal nu give uid=0(root)
```
### Ikke sudo? Den identiske kernel-sti via en user-namespace
```
make run-root-ns # uid 0 i en user-namespace — ingen adgangskode nødvendig
make test-root # samme domme; bruges af CI og alle uden sudo
```
### Baseline — daemon som din normale bruger (intet root nogen steder)
```
make run # foowosd kører med dine uider
make test # exploits lander shells, men `root` forventes MISSING
```
### Oprydningsritual (altid: dette er et root-shell-laboratorium)
```
make stop
```
`foowosd` er *ikke* setuid, og intet i dette bibliotek laver nogensinde
`chmod +s` — det er pointen. Den farlige tilstand er **processen**, ikke
filen.
---
## Når du bliver bedt om at sætte SUID-bitten
Det kommer du **ikke** til. Dette laboratorium har bevidst ingen SUID-bit:
- `foowosd` bygges, ejes og har almindelige tilstande som ethvert andet program.
- Det bliver root, som rigtige daemoner gør — ved at blive *startet* af root.
- `make run-root` bruger `sudo` til præcis det, og `make run-root-ns` får en
ægte uid-0-proces helt uden noget af det.
Suid-bitten tilhører *søster*-laboratoriet (`foosd`). Kontrasten mellem de to
er pensum:
1. SUID-laboratoriet: bitten giver **euid 0, men ruid 1000** →
`execve("/bin/sh")` nedgraderes af bashs vagt (`euid != ruid` → reset) →
shellcoden må først kalde `setreuid(0,0)` (32-byte-payload).
2. Dette laboratorium: root **starter** processen → **ruid == euid == 0** →
vagten har intet at nulstille → den almindelige 23-byte `execve`-shellcode
beholder root.
Samme overløb. Samme teknik. Forskellig *oprindelse* af privilegier, anderledes
payload-form. Det er lektionen i miniature.
---
## Protokollen
Uanset hvilken slags klient der forbinder, 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 printer en høj
advarsel, når euid ikke er 0 (dvs. du startede daemonen som en almindelig
bruger): payloaden lander stadig, men shellen bliver en bruger-shell, og at
kalde exploitet "i stykker" ville være forkert — det eskalerer bare ikke.
> Staveform-bemærkning: `ids=`, ikke `euid=`/`ruid=`. Test-harnessen beviser et
> levende `id` ved at matche den bogstavelige form `uid=NNN(`, så banneret må
> aldrig indeholde en delstreng, der selv opfylder tjekket. (I SUID-laboratoriet
> producerede præcis den fælde et spektakulært falsk positivt.)
---
## Exploit-teknikkerne (`foowosc -t …`)
Alle fire exploit-stier nedenfor virker mod foowosd. Når daemonen er root,
giver **alle, der spawner noget, root** — i modsætning til SUID-laboratoriet,
hvor ret2win/ret2libc stille og roligt blev nedgraderet til uid 1000 af bashs
vagt. Her er der intet mismatch at vogte mod.
| `-t` | hvad der sker | når daemonen er root |
|---------------|---------------------------------------------------------------------|---------------------|
| `shellcode` | 23-byte-`execve("/bin/sh", NULL, NULL)` kører på stacken. | **root-shell** (standard) |
| `ret2win` | hop til `win()` → `execl("/bin/sh")` | **root-shell** |
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root-shell** |
| `demo` | kun junk-overløb — forvent et SIGSEGV i daemon-loggen | nedbrud, efter design |
| `leak` | printer bare 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 session
```
En vellykket interaktiv session videresender din terminal til shellen *på
offeret* — der er præcis én shell i billedet, og det er `/bin/sh`, der kører
som root inde i foowosd. Skriv `id` for at se `uid=0(root)`.
### Hvorfor der ikke er nogen `ret2win-root`-teknik her
foosc havde én — den hoppede til et `win_root()`, der kaldte `setreuid(0,0)`
før exec, fordi en setuid-proces kørte med en reelle uid, der stadig sagde
1000. En root-*startet* proces har den reelle uid allerede 0; der er intet at
rydde, så den ekstra funktion og teknik ville ikke lære noget. Fjernet.
---
## Hvad foowosc gør, trin for trin
1. **Statisk analyse** — `objdump -d` af `./foowosd`. Finder
`vulnerable_handler`, `win()`, det `lea -0x50(%rbp)`, der adresserer `buf`,
og det første nøgne `ret`. Ud fra forskydningen beregner det
`rip_off = 80 + 8 = 88`. Intet er hardkodet; det overlever en genbygning.
2. **Selv-introspektion** — læser sit eget `/proc/self/maps` og `dlsym()`er
`system`/`read` for at lære libc-*offsets*. Targetets libc-base er
`leaked_read − off_read`, derefter `system = base + off_system`, osv. Denne
delta-aritmetik er grunden til, at exploits overlever libc-versioner.
3. **Forbind** — læser banneret/leaks (`ids=`, `stack=`, `libc=`, `BUF=`).
4. **Byg payloaden** — for `shellcode`: 23 bytes maskinkode, padding til
`rip_off`, derefter den gemte RIP = `buf` (så `ret` hopper ind på koden). For
`ret2win`/`ret2libc`: adresser beregnet ud fra analysen — ingen udførelse af
stacken nødvendig.
5. **Justeringsfixen** — et kapret nøgent `ret` giver callee'en
`rsp ≡ 8 (mod 16)`, og glibcs SSE2-kode `movaps`-fault'er på en ikke-justeret
stack (crash-reporteren logger `si_addr=(nil)` — fingerpeget). foowosc
indsætter ét ekstra `ret`-gadget før det rigtige target og genopretter
invarianten. Silkehandsketeknik i et shellcode-laboratorium, men det er
forskellen mellem en payload, der "nogle gange virker", og en, der altid
virker.
6. **Send, derefter videresend** — offerprocessen *er* shellen; denne proces
splisser kun bytes. Ingen lokal shell, ingen anden læser — single-read-
cursor-fejlen (ét spist byte per chunk) er dokumenteret i `become_shell()`.
---
## Daemonens bevidste fejl (alle i `foowosd.c`, alle ægte CWE-klasser)
| # | fejl | CWE | note |
|---|-----|-----|------|
2026-09-29 10:04:38 +02:00
| 1 | `read(fd, buf, 512)` ind i et 64-byte stack-buffer | [CWE-120](https://cwe.mitre.org/data/definitions/120.html) | overløbet: 448 bytes forbi `buf`, gemt RIP ved +88 |
| 2 | kun `snprintf(line, …, "%.*s", …)`; men angriber-`%` i echo-stien | [CWE-134](https://cwe.mitre.org/data/definitions/134.html) | leaket er her den reelle payload; et `%n` i en *root*-proces ville være write-what-where som root |
| 3 | børn beholder root, mens de håndterer utroverdige inputs | [CWE-271](https://cwe.mitre.org/data/definitions/271.html) | det korrekte `drop_privs()` (setgroups→setgid→setuid, i den rækkefølge, med verifikation) står i filen, kommenteret, *bevidst aldrig kaldt* |
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` afsløret til enhver klient | [CWE-200](https://cwe.mitre.org/data/definitions/200.html) | uden disse leaks kunne shellcode- og ret2libc-teknikkerne ikke beregne adresser (ASLR ville besejre dem) |
2026-09-29 09:39:24 +02:00
Handleren har præcis samme `buf[64]`/`read(512)`-form som de to andre
laboratorier, så den fælles objdump-baserede erkendelsespipeline virker
uændret.
---
## Sådan inspicerer du daemonen (læringssti)
```
make status # kører den? som hvilken uid? filtilsand vises
make run-root # eller run / run-root-ns
./foowosc -t leak # se banneret og leaks, send intet
./foowosc -t demo # junk-overløb -> SIGSEGV, logget med RIP/rsp
./foowosc -t shellcode # den interaktive root-shell
make test-root # fuld matrix, alle teknikker, --must-root
```
Crash-reporter: Ved SIGSEGV logger daemonen fejladressen, RIP og RSP. Et `ret`
ind i en ikke-kanonisk `0x4141…` fault'er ved `ret`-et selv (RIP som `0x4028xx`,
`si_addr=(nil)`) — værd at vide, før du fejllæser en loglinje som en
NULL-dereference.
---
## 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 har brug for `setreuid(0,0)` foran dette. Dette
laboratorium ikke, af den grund der gentages overalt: **ruid er allerede 0**,
fordi root startede processen. `make verify` beviser, at bytes i `foowosc.c`
er byte-for-byte det, `shellcode.S` assemblerer til.
---
## Modforanstaltninger — hvad `make hardened` ændrer
Hærdet build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
| teknik | sårbar `foowosd` | hærdet `foowosd_hardened` |
|-----------|----------------------|------------------------------|
| `shellcode` | root-shell (eksekverbar stack) | SIGSEGV ved canary-tjekket / NX |
| `ret2win` / `ret2libc` | root-shell | canary abort'er `ret` — men bemærk: en *PIE*-build gør også disse adresser tilfældige |
| `demo` | SIGSEGV, logget | SIGSEGV, logget |
`make test-hardened` demonstrerer det live. Den vigtige observation er ikke
bare, at modforanstaltningerne dræbte teknikkerne — det er, at de **ikke**
gjorde daemonen til "ikke-root". En hærdet build, der stadig *startes* som
root, er stadig en root-daemon; modforanstaltningen hæver kun barren for
angriberen. Least privilege (`drop_privs()`) og hukommelsessikkerhed er to
forskellige fejl, og en daemon, der ikke behøver root, bør ikke have det.
---
## Sikkerhedsgelændere (samme politik som SUID-laboratoriet)
- **Kun loopback.** foowosd nægter at binde andet end `127.0.0.1` /
`localhost` / `::1`, medmindre du giver `-L`. En root-daemon på en rigtig
grænseflade er en *fjern* root-tjeneste. `-L` findes kun for at vise vagten;
brug den ikke på noget, der betyder noget.
- **Tilstanden logges højt.** Ved start printer den `ruid/euid` og om dette er
en root-proces, så du altid ved, hvilket exploit-resultat du skal forvente.
- **Domme kommer fra exit-status** i `make test*` (pty-harnessens
returkode), aldrig fra at greppe dens stdout — grebbart output lyver.
- **pty'en skal køre cooked + ECHO off**, ellers ekkoer harnessen sin egen
kommandolinje og forfalsker markøren. Harnessen slår ECHO fra og beholder
ECHONL på.
- **Oprydningsritual:** `make stop` efter hver session. Hvis daemonen er
root-ejet, siger `stop` dig at køre `sudo pkill -x foowosd`.
- Kør aldrig dette på en host, du holder af. Det findes for at uddele
`uid=0`-shells over loopback-grænsefladen.
---
## Øvelser
1. Kør `make run` (user-daemon), derefter `./foowosc -t shellcode`. Hvorfor er
shellen ikke root? (Tjek `ids=` i banneret — foowosc siger dig det, før du
overhovedet forbinder.)
2. `make stop && sudo make run-root && make test-root`. Forklar ud fra
bannerlinjen, hvorfor alle fire teknikker nu giver `uid=0(root)`.
3. Find i `foowosd.c` `drop_privs()` og læs, *hvorfor rækkefølgen* af
`setgroups → setgid → setuid` betyder noget. Beslut, hvor i `main()` den
ville høre hjemme, og hvad laboratoriets angrebsflade bliver, når den
faktisk kaldes.
4. Beregn `rip_off` i hånden ud fra `objdump -d foowosd`: find `buf`s
`lea -0xNN(%rbp)` inde i `vulnerable_handler`, derefter `NN + 8`. foowosc
gør præcis det; tjek dens regnestykke mod dit eget.
5. `make hardened && make test-hardened`. Hvilken teknik falder for canaryen,
og hvilken for NX? Hvorfor ændrer hærdning ikke, hvad `make status`
rapporterer om *processen*?
6. Sammenlign de to laboratoriers shellcodes: 23 bytes her, 32 for foosd. Hvad
gør de ekstra 9 bytes, og hvorfor behøves de kun i SUID-tilfældet?
7. Læs `become_shell()`s kommentar om den enkelte læse-cursor. Genskab
fiaskotilstanden mentalt: to læsere på én socket betyder, at login-shellen
æder ét byte per chunk — "uid=1000…" ankommer som "id=1000…". Hvorfor kan en
relay-proces aldrig have denne fejl?
---
## Filer
```
foowosd.c den sårbare root-daemon (hver linje kommenteret)
foowosc.c exploitet (hver linje kommenteret)
shellcode.S reference-assembly for 23-byte-payloaden
tests/pty_wosuid_test.c pty-harnessen (markør + strenge id-form-tjek)
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 bevidst
forskellige — du kan køre alle tre på én gang og krydstjekke deres `ids=`-linjer
2026-09-29 10:04:38 +02:00
i banneret.