foo/Makefile

305 lines
14 KiB
Makefile
Raw Normal View History

2026-09-29 09:39:24 +02:00
# ============================================================================
# Makefile -- builds the lab: the vulnerable daemon and its exploit
# ============================================================================
#
# make build food, fooc and the test harnesses
# make run start food in the background, on loopback
# make test run the full technique matrix (needs `make run` first)
# make verify prove the shellcode in fooc.c matches shellcode.S
# make hardened rebuild food with every mitigation ENABLED
# make test-hardened run the matrix against the hardened build
# make stop stop the daemon
# make clean remove build products
#
# ---------------------------------------------------------------------------
# WHY THESE FLAGS -- the single most important thing in this file
# ---------------------------------------------------------------------------
#
# `food` is built with three protections switched OFF, deliberately:
#
# -fno-stack-protector no stack canary
# -no-pie fixed load address, so win() is a constant
# -z execstack executable stack, so shellcode can run
#
# Each one corresponds to a real defence that a real program gets for free, and
# `make test-hardened` turns them all back on so you can watch the techniques
# fail. That contrast is the entire lesson. Do not copy these flags into
# anything you actually ship.
#
# The exploit (`fooc`) is built with the protections ON. There is no reason for
# an attacker to disable them, and leaving them on is a useful reminder that
# the tool works fine in a hardened process.
#
# ---------------------------------------------------------------------------
# WHY -O0 -g
# ---------------------------------------------------------------------------
#
# -O0 the compiler does not reorder, inline, or elide the code. At -O2 the
# stack layout the exploit reasons about can change between builds, and
# variables you were told exist may be gone. For a lab you have to be
# able to read the disassembly and find the thing the comment promised.
# -g symbols and line numbers, so gdb is actually usable. `make debug`
# goes further and stops at the vulnerable read().
# ============================================================================
CC ?= gcc
CSTD := -std=c99
# Warnings we always want, even on the vulnerable build. Note that we do NOT
# use -Werror: food.c's deliberate overflow triggers -Wstringop-overflow, and
# that warning is *supposed* to fire (see the comment at the read() call).
WARN := -Wall -Wextra
# Debug info and no optimisation: see above.
DBG := -O0 -g
# --- the vulnerable build -----------------------------------------------------
# These are the flags we are trying to defeat. See the header comment.
VULN := -fno-stack-protector -no-pie -z execstack
# --- the hardened build -------------------------------------------------------
# What a modern project actually does. Note that -fstack-protector-strong is
# gcc's DEFAULT on many distros, and -fPIE is too, so the hardened build is
# really just "stop overriding the defaults". `make test-hardened` shows the
# exploits failing, which is the point.
HARDEN := -fstack-protector-strong -fPIE -pie -z noexecstack
# Shellcode needs a terminal, and the test harness is the only thing that
# provides one. It is a normal POSIX program, not part of the exploit.
TESTCFLAGS := $(CSTD) $(DBG) $(WARN)
all: food fooc tests/pty_test tests/sock_test
# -----------------------------------------------------------------------------
# The vulnerable daemon.
# -----------------------------------------------------------------------------
food: food.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $<
# -----------------------------------------------------------------------------
# The exploit. -ldl is needed for dlsym(), which is how it locates libc's
# system() and "/bin/sh" at runtime instead of hardcoding offsets that would
# break the next time glibc is updated.
#
# It gets the mitigations ON, unlike the target.
# -----------------------------------------------------------------------------
fooc: fooc.c
$(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl
# -----------------------------------------------------------------------------
# Test harnesses. These exist because the exploit's last act is to hand its
# process over to a shell; verifying that needs a real terminal, which a pipe
# or a here-doc is not.
# -----------------------------------------------------------------------------
tests/pty_test: tests/pty_test.c
$(CC) $(TESTCFLAGS) -o $@ $<
tests/sock_test: tests/sock_test.c
$(CC) $(TESTCFLAGS) -o $@ $<
# -----------------------------------------------------------------------------
# The hardened daemon: same source, protections on. Build it, then run
# `make test-hardened` to see which techniques it survives.
# -----------------------------------------------------------------------------
hardened: food.c
$(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o food_hardened $<
@echo
@echo "=== food_hardened built with the mitigations ON."
@echo "=== Stack segment permissions ('RWE' would mean executable; you"
@echo "=== want 'RW', i.e. no-execute):"
@readelf -W -l food_hardened | grep GNU_STACK
@echo "=== Now run: make test-hardened"
# -----------------------------------------------------------------------------
# verify-shellcode: prove the bytes in fooc.c are what nasm produces from
# shellcode.S. This is the check that keeps the inline byte array honest --
# a hand-maintained hex dump and a disassembler are both easy to get wrong, and
# a single wrong byte means a payload that crashes instead of running.
# -----------------------------------------------------------------------------
verify verify-shellcode: shellcode.S fooc.c
@command -v nasm >/dev/null 2>&1 || { \
echo "verify-shellcode: nasm is not installed; skipping."; \
echo " (Arch: pacman -S nasm)"; exit 0; }
@echo "=== Assembling shellcode.S ..."
@nasm -f bin -o shellcode.bin shellcode.S
@echo "=== nasm output:"
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
-e 's/[[:space:]]*$$//'
@echo
@# Pull the byte list out of the C array. `sed s,/*.**/,` first strips the
@# trailing /* ... */ annotations, so a hex constant mentioned inside a
@# comment (there is one: "push 0x3b (execve)") is not counted as data.
@# Stripping comments before grepping is the whole trick here.
@sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' fooc.c \
| sed -e 's,/\*.*\*,,' \
| grep -o '0x[0-9a-fA-F][0-9a-fA-F]' \
| tr 'A-F' 'a-f' | tr '\n' ' ' | sed -e 's/^ //' -e 's/[[:space:]]*$$//' \
> .sc_c_raw.txt
@echo "=== bytes declared in fooc.c's SHELLCODE[] array:"
@cat .sc_c_raw.txt
@echo
@echo "=== comparing ..."
@# Both sides reduced to the same plain "31 f6 31 d2 ..." form, so the
@# comparison is on VALUES and not on how each tool happens to print them.
@sed -e 's/0x//g' .sc_c_raw.txt > .sc_c.txt
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
-e 's/[[:space:]]*$$//' > .sc_asm.txt
@if cmp -s .sc_c.txt .sc_asm.txt; then \
n=$$(wc -c < shellcode.bin); \
echo "MATCH: the $$n bytes in fooc.c are byte-for-byte what"; \
echo " shellcode.S assembles to."; \
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; \
else \
echo "MISMATCH -- the two differ:"; \
diff .sc_c.txt .sc_asm.txt || true; \
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; exit 1; \
fi
# -----------------------------------------------------------------------------
# run: start the daemon in the background.
#
# setsid + nohup + </dev/null are all needed. Without setsid the daemon dies
# when the invoking shell exits; without </dev/null it inherits your terminal
# and competes with you for it; without nohup it gets SIGHUP.
#
# It listens on 127.0.0.1 only. Please keep it that way.
# -----------------------------------------------------------------------------
PORT ?= 2342
run: food
@echo "=== starting food on 127.0.0.1:$(PORT)"
@setsid nohup ./food -p $(PORT) > food.log 2>&1 </dev/null & \
disown 2>/dev/null || true
@sleep 1
@if pgrep -x food >/dev/null; then \
echo "=== food is running (pid $$(pgrep -x food | head -1))"; \
echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \
grep '\[stack\]' /proc/$$(pgrep -x food | head -1)/maps; \
else \
echo "=== food failed to start; see food.log"; exit 1; \
fi
# -----------------------------------------------------------------------------
# test: the technique matrix. Every technique must print both SEEN.
#
# Note this runs against whatever ./food currently is. If you last ran
# `make hardened`, you are testing the hardened build -- which is what
# test-hardened is for.
# -----------------------------------------------------------------------------
#
# Note on the redirection below. The verdict is the "[pty_test] ..." line the
# harness prints to STDERR, and its EXIT STATUS, so stderr is sent to the
# terminal and the shell's chatter (stdout) is discarded. Piping the two
# together and tailing is what hid a real failure during development: the pty's
# echo of our own command line contains the marker string, so a loose grep on
# the transcript was always going to pass.
test: tests/pty_test
@fail=0; \
for t in ret2win ret2libc shellcode; do \
echo "=================== $$t"; \
if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \
:; \
else \
fail=1; \
fi; \
done; \
echo; \
if [ $$fail -eq 0 ]; then \
echo "=== all three techniques gave a working shell"; \
else \
echo "=== at least one technique did NOT work."; \
echo "=== If food was built with `make hardened`, that is the"; \
echo "=== mitigations doing their job. See README.md."; \
fi; \
exit $$fail
# -----------------------------------------------------------------------------
# test-hardened: swap in the hardened daemon, prove the mitigations hold, then
# put the vulnerable one back. Leaves your tree exactly as it found it.
# -----------------------------------------------------------------------------
#
# Two things this target has to get right, both of which bit during development:
#
# * `pgrep -x` matches the process NAME, and the hardened binary is
# food_hardened, not food. Using the wrong name silently inspects nothing.
# * The verdict is pty_test's EXIT STATUS (0 = both markers seen), not the
# presence of its output line. Grepping for a line that is also printed on
# failure reports success for a run that crashed.
test-hardened: hardened tests/pty_test
@if ! pgrep -x food >/dev/null; then \
echo "=== start the daemon first: make run"; exit 1; \
fi
@echo "### stopping the vulnerable daemon"
@$(MAKE) --no-print-directory stop
@echo "### starting food_hardened instead"
@setsid nohup ./food_hardened -p $(PORT) > food_hardened.log 2>&1 \
</dev/null & disown 2>/dev/null || true
@sleep 1
@if ! pgrep -x food_hardened >/dev/null; then \
echo "!!! food_hardened did not start; see food_hardened.log"; \
$(MAKE) --no-print-directory stop; exit 1; \
fi
@echo "### stack segment: 'rw-p' (NOT executable) is what you want to see"
@grep '\[stack\]' /proc/$$(pgrep -x food_hardened | head -1)/maps || true
@echo
@for t in ret2win ret2libc shellcode; do \
echo "=================== $$t"; \
if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \
echo "!!! $$t STILL WORKED against the hardened build"; \
else \
echo "--- $$t was stopped by the mitigations (as expected)"; \
fi; \
done; \
echo
@$(MAKE) --no-print-directory stop
@echo "### restoring the vulnerable daemon"
@setsid nohup ./food -p $(PORT) > food.log 2>&1 </dev/null \
& disown 2>/dev/null || true
@sleep 1
@echo
@echo "=== mitigation comparison is above."
@echo "=== Read the table in README.md to see which flag stopped what,"
@echo "=== and note which mitigations are NOT enough on their own."
# -----------------------------------------------------------------------------
# debug: build food and run it under gdb, stopping at the vulnerable read() so
# you can watch the stack frame get overwritten.
# -----------------------------------------------------------------------------
debug: food.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o food $<
@echo "=== built ./food for gdb. Try:"
@echo " gdb -q ./food"
@echo " (gdb) break food.c:393 # the read() that overflows"
@echo " (gdb) run -p 2342"
@echo " (gdb) info registers rsp rbp"
@echo " (gdb) x/24gx \$rsp # watch the return address"
# -----------------------------------------------------------------------------
# stop: kill the daemon.
#
# `pkill -x food` matches the process NAME exactly. Do NOT use
# `pkill -f ./food` -- that pattern also matches the shell you typed it into,
# so it kills your own session. This is not a theoretical risk; it happened
# while building this lab.
# -----------------------------------------------------------------------------
stop:
@if pgrep -x food >/dev/null; then \
pkill -x food; sleep 0.5; \
echo "=== food stopped"; \
else \
echo "=== food was not running"; \
fi
@# The hardened binary has a different process name, so it needs its own
@# pkill. A leftover food_hardened keeps port 2342 bound and makes the
@# next `make run` fail with "Address already in use".
@if pgrep -x food_hardened >/dev/null; then \
pkill -x food_hardened; sleep 0.5; \
echo "=== food_hardened stopped"; \
fi
clean:
rm -f food fooc food.hardened shellcode.bin
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt
rm -f tests/pty_test tests/sock_test
@echo "=== cleaned. (food.log is left alone; it is your evidence.)"
.PHONY: all run stop test test-hardened verify verify-shellcode hardened debug clean