368 lines
No EOL
17 KiB
Makefile
368 lines
No EOL
17 KiB
Makefile
# ============================================================================
|
|
# 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 |