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.