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