foo/wosuid/README.NL.md
2026-09-29 09:39:24 +02:00

311 lines
No EOL
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Het `wosuid`-lab — root-RCE **zonder** setuid-bit
```
foowosd een bewust kwetsbare daemon die root is omdat hij als *GESTART* als
root (poort 2344, standaard alleen loopback)
foowosc het exploit: verandert een stack-overloop in een **root**-shell door
shellcode uit te voeren — dezelfde 23 bytes die de user-level-
`food`-daemon in het hoofdlab kraakten
```
Dit is het derde lab in de serie. Dezelfde exploit-werktuigketen, dezelfde stijl,
één fundamenteel verschil:
| Lab | hoe het doelproces root wordt | `uid=0(root)`-shell? |
|----------|------------------------------------------------|----------------------|
| food/fooc | nooit — het is een gewone user-daemon | nee |
| foosd/foosc | de SUID-bit (`chmod u+s`) — euid 0, ruid 1000 | ja (vereist `setreuid` in de shellcode, want bash reset euid→ruid) |
| **foowosd/foowosc** | **geen — root *start* de daemon** (sudo / systemd `User=root`) | **ja (gewone `execve`-shellcode)** |
De setuid-bit is een *transportmiddel* voor privileges — niet de privileges
zelf. Een daemon die door root is gestart, heeft reële, effectieve en
opgeslagen uid allemaal 0. Voor de kernel is dat root, punt uit; hij kan en wil
niet weten of het proces daar via `+s` op een bestand of via `sudo ./foowosd`
is gekomen. De overloop in een root-gestarte daemon is dus een root-exploit — *"ik
heb geen SUID-binaries" is niet hetzelfde als "ik ben niet exploiteerbaar".*
Dat is de hele les van dit lab. Al het andere hieronder is het mechanisme.
---
## Snelle start (wat de gebruiker vroeg)
```
cd wosuid
make # bouwt de daemon, het exploit en de test-harness
```
### Het echte werk — draai de daemon als **root**
```
sudo make run-root # start foowosd als uid 0 (proces, geen bestandstoestand)
make test-root # elke techniek moet nu uid=0(root) geven
```
### Geen sudo? Het identieke kernelpad via een user-namespace
```
make run-root-ns # uid 0 in een user-namespace — geen wachtwoord nodig
make test-root # dezelfde oordelen; gebruikt door CI en iedereen zonder sudo
```
### Baseline — daemon als je normale gebruiker (nergens root)
```
make run # foowosd draait met jouw uids
make test # exploits landen shells, maar `root` wordt verwacht als MISSING
```
### Opruimritueel (altijd: dit is een root-shell-lab)
```
make stop
```
`foowosd` is *niet* setuid, en niets in deze map doet ooit `chmod +s` — dat is
het punt. De gevaarlijke toestand is **het proces**, niet het bestand.
---
## Wanneer je wordt gezegd de SUID-bit te zetten
Dat zul je **niet** worden. Dit lab heeft bewust geen SUID-bit:
- `foowosd` wordt gebouwd, is eigendom van jou en heeft gewone toestanden als
elk ander programma.
- Hij wordt root zoals echte daemons dat doen — door *gestart* te worden door
root.
- `make run-root` gebruikt `sudo` voor precies dat, en `make run-root-ns`
regelt een echte uid-0-proces zonder ook maar iets daarvan.
De Suid-bit hoort bij het *zuster*-lab (`foosd`). Het contrast tussen de twee
is de leerstof:
1. SUID-lab: de bit geeft **euid 0, maar ruid 1000** → `execve("/bin/sh")`
wordt gedegradeerd door de wacht van bash (`euid != ruid` → reset) → de
shellcode moet eerst `setreuid(0,0)` aanroepen (32-byte-payload).
2. Dit lab: root **start** het proces → **ruid == euid == 0** → de wacht heeft
niets om te resetten → de gewone 23-byte `execve`-shellcode houdt root.
Dezelfde overloop. Dezelfde techniek. Andere *oorsprong* van privileges, andere
payload-vorm. Dat is de les in het klein.
---
## Het protocol
Welke client er ook verbindt, foowosd begroet hem met:
```
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (banner + leaks)
BUF=0x7ffd… (het bufferadres)
```
`ids=euid/ruid` is het *"ben ik root?"*-kanaal. foowosc print een luide
waarschuwing wanneer euid niet 0 is (dus je startte de daemon als gewone
gebruiker): de payload landt nog steeds, maar de shell wordt een
gebruiker-shell, en het exploit "kapot" noemen zou fout zijn — hij escaleert
alleen niet.
> Spellingsnoot: `ids=`, niet `euid=`/`ruid=`. De test-harness bewijst een
> levende `id` door de letterlijke vorm `uid=NNN(` te matchen, dus het banner
> mag nooit een deelstring bevatten die zelf aan de controle voldoet. (In het
> SUID-lab produceerde precies die val een spectaculaire fout-positief.)
---
## De exploit-technieken (`foowosc -t …`)
Alle vier de exploit-paden hieronder werken tegen foowosd. Wanneer de daemon
root is, geeft **alles dat iets spawnt root** — in tegenstelling tot het
SUID-lab, waar ret2win/ret2libc stilletjes door de wacht van bash naar uid 1000
werden gedegradeerd. Hier is er geen mismatch om te bewaken.
| `-t` | wat er gebeurt | wanneer de daemon root is |
|---------------|---------------------------------------------------------------------|---------------------|
| `shellcode` | 23-byte `execve("/bin/sh", NULL, NULL)` draait op de stack. | **root-shell** (standaard) |
| `ret2win` | sprong naar `win()` → `execl("/bin/sh")` | **root-shell** |
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root-shell** |
| `demo` | alleen rommel-overloop — verwacht een SIGSEGV in de daemonlog | crash, volgens ontwerp |
| `leak` | print alleen leaks, stuurt geen payload | n.v.t. |
```
./foowosc -t shellcode # interactief; standaardtarget 127.0.0.1:2344
./foowosc -t shellcode -n # sturen en rapporteren, geen interactieve sessie
```
Een succesvolle interactieve sessie schakelt je terminal door naar de shell *op
het slachtoffer* — er is precies één shell in het beeld, en dat is `/bin/sh`
die als root binnenin foowosd draait. Typ `id` om `uid=0(root)` te zien.
### Waarom er hier geen `ret2win-root`-techniek is
foosc had er één — die sprong naar een `win_root()` die `setreuid(0,0)` aanriep
vóór exec, omdat een setuid-proces draaide met een reële uid die nog 1000 zei.
Een root-*gestart* proces heeft de reële uid al 0; er valt niets op te ruimen,
dus de extra functie en techniek zouden niets leren. Verwijderd.
---
## Wat foowosc doet, stap voor stap
1. **Statische analyse** — `objdump -d` van `./foowosd`. Vindt
`vulnerable_handler`, `win()`, de `lea -0x50(%rbp)` die `buf` adresseert,
en de eerste kale `ret`. Uit de verplaatsing berekent het
`rip_off = 80 + 8 = 88`. Niets is hardcoded; het overleeft een herbouw.
2. **Zelf-introspectie** — leest zijn eigen `/proc/self/maps` en `dlsym()`t
`system`/`read` om de libc-*offsets* te leren. De libc-base van het
doelwit is `leaked_read − off_read`, daarna `system = base + off_system`,
enz. Die delta-rekenkunde is de reden dat exploits libc-versies overleven.
3. **Verbind** — leest het banner/leaks (`ids=`, `stack=`, `libc=`, `BUF=`).
4. **Bouw de payload** — voor `shellcode`: 23 bytes machinecode, padding tot
`rip_off`, daarna de opgeslagen RIP = `buf` (zodat de `ret` op de code
springt). Voor `ret2win`/`ret2libc`: adressen berekend uit de analyse —
geen stackuitvoering nodig.
5. **De uitlijnfix** — een gekaapte kale `ret` geeft de callee
`rsp ≡ 8 (mod 16)`, en glibc's SSE2-code faalt met `movaps` op een
niet-uitgelijnde stack (de crash-reporter logt `si_addr=(nil)` — de hint).
foowosc plaatst één extra `ret`-gadget vóór het echte doelwit en herstelt de
invariant. Een zijdenhandschoentechniek in een shellcode-lab, maar het is
het verschil tussen een payload die "soms werkt" en een die altijd werkt.
6. **Stuur, daarna schakel door** — het doelproces *is* de shell; dit proces
splist alleen bytes. Geen lokale shell, geen andere lezer — de
single-read-cursor-bug (één opgegeten byte per chunk) is gedocumenteerd in
`become_shell()`.
---
## De bewuste bugs van de daemon (allemaal in `foowosd.c`, allemaal echte CWE-klassen)
| # | bug | CWE | notitie |
|---|-----|-----|------|
| 1 | `read(fd, buf, 512)` in een 64-byte stack-buffer | CWE-120 | de overloop: 448 bytes voorbij `buf`, opgeslagen RIP op +88 |
| 2 | alleen `snprintf(line, …, "%.*s", …)`; maar aanvaller-`%` in het echo-pad | CWE-134 | het lek is hier de echte payload; een `%n` in een *root*-proces zou write-what-where als root zijn |
| 3 | kinderen behouden root terwijl ze onbetrouwbare input afhandelen | CWE-271 | de correcte `drop_privs()` (setgroups→setgid→setuid, in die volgorde, met verificatie) staat in het bestand, gecommentarieerd, *bewust nooit aangeroepen* |
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` onthuld aan elke client | CWE-200 | zonder deze leaks konden de shellcode- en ret2libc-technieken geen adressen berekenen (ASLR zou ze verslaan) |
De handler heeft precies dezelfde `buf[64]`/`read(512)`-vorm als de twee andere
labs, dus de gedeelde objdump-gebaseerde herkenningspipeline werkt ongewijzigd.
---
## Zo inspecteer je de daemon (leerpad)
```
make status # draait hij? als welke uid? bestandstoestand wordt getoond
make run-root # of run / run-root-ns
./foowosc -t leak # zie het banner en leaks, stuur niets
./foowosc -t demo # rommel-overloop -> SIGSEGV, gelogd met RIP/rsp
./foowosc -t shellcode # de interactieve root-shell
make test-root # volledige matrix, alle technieken, --must-root
```
Crash-reporter: Bij SIGSEGV logt de daemon het foutadres, RIP en RSP. Een `ret`
in een niet-canonieke `0x4141…` faalt bij de `ret` zelf (RIP als `0x4028xx`,
`si_addr=(nil)`) — goed om te weten vóór je een logregel als NULL-dereferentie
verkeerd leest.
---
## Waarom de shellcode 23 bytes is, niet 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
```
Het SUID-lab heeft `setreuid(0,0)` vóór dit nodig. Dit lab niet, om de reden
die overal herhaald wordt: **ruid is al 0**, omdat root het proces startte.
`make verify` bewijst dat de bytes in `foowosc.c` byte-voor-byte zijn wat
`shellcode.S` assembleren tot.
---
## Tegenmaatregelen — wat `make hardened` verandert
Geharde build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
| techniek | kwetsbare `foowosd` | geharde `foowosd_hardened` |
|-----------|----------------------|------------------------------|
| `shellcode` | root-shell (uitvoerbare stack) | SIGSEGV bij de canary-controle / NX |
| `ret2win` / `ret2libc` | root-shell | canary aborted de `ret` — maar merk: een *PIE*-build maakt deze adressen ook willekeurig |
| `demo` | SIGSEGV, gelogd | SIGSEGV, gelogd |
`make test-hardened` demonstreert het live. De belangrijke observatie is niet
alleen dat de tegenmaatregelen de technieken doodden — het is dat ze de daemon
**niet** "niet-root" maakten. Een geharde build die nog steeds als root
*gestart* wordt, is nog steeds een root-daemon; de tegenmaatregel verhoogt
alleen de lat voor de aanvaller. Minste privilege (`drop_privs()`) en
geheugenveiligheid zijn twee verschillende bugs, en een daemon die geen root
nodig heeft, zou het niet moeten hebben.
---
## Veiligheidsleuningen (zelfde beleid als het SUID-lab)
- **Alleen loopback.** foowosd weigert iets anders dan `127.0.0.1` /
`localhost` / `::1` te binden, tenzij je `-L` geeft. Een root-daemon op een
echte interface is een *externe* root-dienst. `-L` bestaat alleen om de
wacht te tonen; gebruik het niet op iets dat ertoe doet.
- **De toestand wordt luid gelogd.** Bij de start print hij `ruid/euid` en of
dit een root-proces is, zodat je altijd weet welk exploit-resultaat je moet
verwachten.
- **Oordelen komen uit exit-status** in `make test*` (de retourcode van de
pty-harness), nooit uit het greppen van zijn stdout — grepbare output liegt.
- **De pty moet cooked + ECHO off draaien**, anders echoët de harness zijn
eigen commandoregel en vervalst de markering. De harness zet ECHO uit en
houdt ECHONL aan.
- **Opruimritueel:** `make stop` na elke sessie. Als de daemon root-bezeten
is, zegt `stop` je `sudo pkill -x foowosd` te draaien.
- Draai dit nooit op een host waar je om geeft. Het bestaat om `uid=0`-shells
over de loopback-interface uit te delen.
---
## Oefeningen
1. Draai `make run` (user-daemon), daarna `./foowosc -t shellcode`. Waarom is
de shell niet root? (Check `ids=` in het banner — foowosc zegt het je vóór
je überhaupt verbindt.)
2. `make stop && sudo make run-root && make test-root`. Verklaar aan de hand
van de bannerregel waarom alle vier de technieken nu `uid=0(root)` geven.
3. Vind in `foowosd.c` `drop_privs()` en lees *waarom de volgorde* van
`setgroups → setgid → setuid` ertoe doet. Bepaal waar in `main()` hij thuis
zou horen, en wat het aanvalsoppervlak van het lab wordt wanneer hij
daadwerkelijk wordt aangeroepen.
4. Bereken `rip_off` met de hand uit `objdump -d foowosd`: vind de
`lea -0xNN(%rbp)` van `buf` binnenin `vulnerable_handler`, daarna `NN + 8`.
foowosc doet precies dat; controleer zijn rekensom tegen de jouwe.
5. `make hardened && make test-hardened`. Welke techniek valt voor de canary,
en welke voor NX? Waarom verandert harden niet wat `make status` over *het
proces* rapporteert?
6. Vergelijk de shellcodes van de twee labs: 23 bytes hier, 32 voor foosd. Wat
doen de extra 9 bytes, en waarom zijn ze alleen in het SUID-geval nodig?
7. Lees de commentaar van `become_shell()` over de enkele lees-cursor. Maak de
faaltoestand mentaal na: twee lezers op één socket betekent dat de
login-shell één byte per chunk eet — "uid=1000…" arriveert als "id=1000…".
Waarom kan een relay-proces deze bug nooit hebben?
---
## Bestanden
```
foowosd.c de kwetsbare root-daemon (elke regel gecommentarieerd)
foowosc.c het exploit (elke regel gecommentarieerd)
shellcode.S referentie-assembly voor de 23-byte-payload
tests/pty_wosuid_test.c de pty-harness (markering + strikte id-vorm-controle)
Makefile build / run / run-root / run-root-ns / test /
test-root / verify / hardened / clean …
```
Zuster-labs: `../food.c`/`../fooc.c` (user-level-baseline, poort 2342) en
`../suid/` (SUID-root-daemon `foosd`/`foosc`, poort 2343). De poorten zijn
bewust verschillend — je kunt alle drie tegelijk draaien en hun `ids=`-regels
in het banner kruislings controleren.