Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
305 lines (285 loc) · 13.7 KB
/
Copy pathMakefile
File metadata and controls
305 lines (285 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# ============================================================================
# Makefile -- builds the lab: the vulnerable daemon and its exploit
# ============================================================================
#
# make build food, fooc and the test harnesses
# make run start food in the background, on loopback
# make test run the full technique matrix (needs `make run` first)
# make verify prove the shellcode in fooc.c matches shellcode.S
# make hardened rebuild food with every mitigation ENABLED
# make test-hardened run the matrix against the hardened build
# make stop stop the daemon
# make clean remove build products
#
# ---------------------------------------------------------------------------
# WHY THESE FLAGS -- the single most important thing in this file
# ---------------------------------------------------------------------------
#
# `food` is built with three protections switched OFF, deliberately:
#
# -fno-stack-protector no stack canary
# -no-pie fixed load address, so win() is a constant
# -z execstack executable stack, so shellcode can run
#
# Each one corresponds to a real defence that a real program gets for free, and
# `make test-hardened` turns them all back on so you can watch the techniques
# fail. That contrast is the entire lesson. Do not copy these flags into
# anything you actually ship.
#
# The exploit (`fooc`) is built with the protections ON. There is no reason for
# an attacker to disable them, and leaving them on is a useful reminder that
# the tool works fine in a hardened process.
#
# ---------------------------------------------------------------------------
# WHY -O0 -g
# ---------------------------------------------------------------------------
#
# -O0 the compiler does not reorder, inline, or elide the code. At -O2 the
# stack layout the exploit reasons about can change between builds, and
# variables you were told exist may be gone. For a lab you have to be
# able to read the disassembly and find the thing the comment promised.
# -g symbols and line numbers, so gdb is actually usable. `make debug`
# goes further and stops at the vulnerable read().
# ============================================================================
CC ?= gcc
CSTD := -std=c99
# Warnings we always want, even on the vulnerable build. Note that we do NOT
# use -Werror: food.c's deliberate overflow triggers -Wstringop-overflow, and
# that warning is *supposed* to fire (see the comment at the read() call).
WARN := -Wall -Wextra
# Debug info and no optimisation: see above.
DBG := -O0 -g
# --- the vulnerable build -----------------------------------------------------
# These are the flags we are trying to defeat. See the header comment.
VULN := -fno-stack-protector -no-pie -z execstack
# --- the hardened build -------------------------------------------------------
# What a modern project actually does. Note that -fstack-protector-strong is
# gcc's DEFAULT on many distros, and -fPIE is too, so the hardened build is
# really just "stop overriding the defaults". `make test-hardened` shows the
# exploits failing, which is the point.
HARDEN := -fstack-protector-strong -fPIE -pie -z noexecstack
# Shellcode needs a terminal, and the test harness is the only thing that
# provides one. It is a normal POSIX program, not part of the exploit.
TESTCFLAGS := $(CSTD) $(DBG) $(WARN)
all: food fooc tests/pty_test tests/sock_test
# -----------------------------------------------------------------------------
# The vulnerable daemon.
# -----------------------------------------------------------------------------
food: food.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o $@ $<
# -----------------------------------------------------------------------------
# The exploit. -ldl is needed for dlsym(), which is how it locates libc's
# system() and "/bin/sh" at runtime instead of hardcoding offsets that would
# break the next time glibc is updated.
#
# It gets the mitigations ON, unlike the target.
# -----------------------------------------------------------------------------
fooc: fooc.c
$(CC) $(CSTD) $(DBG) $(WARN) -fstack-protector-strong -o $@ $< -ldl
# -----------------------------------------------------------------------------
# Test harnesses. These exist because the exploit's last act is to hand its
# process over to a shell; verifying that needs a real terminal, which a pipe
# or a here-doc is not.
# -----------------------------------------------------------------------------
tests/pty_test: tests/pty_test.c
$(CC) $(TESTCFLAGS) -o $@ $<
tests/sock_test: tests/sock_test.c
$(CC) $(TESTCFLAGS) -o $@ $<
# -----------------------------------------------------------------------------
# The hardened daemon: same source, protections on. Build it, then run
# `make test-hardened` to see which techniques it survives.
# -----------------------------------------------------------------------------
hardened: food.c
$(CC) $(CSTD) $(DBG) $(WARN) $(HARDEN) -o food_hardened $<
@echo
@echo "=== food_hardened built with the mitigations ON."
@echo "=== Stack segment permissions ('RWE' would mean executable; you"
@echo "=== want 'RW', i.e. no-execute):"
@readelf -W -l food_hardened | grep GNU_STACK
@echo "=== Now run: make test-hardened"
# -----------------------------------------------------------------------------
# verify-shellcode: prove the bytes in fooc.c are what nasm produces from
# shellcode.S. This is the check that keeps the inline byte array honest --
# a hand-maintained hex dump and a disassembler are both easy to get wrong, and
# a single wrong byte means a payload that crashes instead of running.
# -----------------------------------------------------------------------------
verify verify-shellcode: shellcode.S fooc.c
@command -v nasm >/dev/null 2>&1 || { \
echo "verify-shellcode: nasm is not installed; skipping."; \
echo " (Arch: pacman -S nasm)"; exit 0; }
@echo "=== Assembling shellcode.S ..."
@nasm -f bin -o shellcode.bin shellcode.S
@echo "=== nasm output:"
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
-e 's/[[:space:]]*$$//'
@echo
@# Pull the byte list out of the C array. `sed s,/*.**/,` first strips the
@# trailing /* ... */ annotations, so a hex constant mentioned inside a
@# comment (there is one: "push 0x3b (execve)") is not counted as data.
@# Stripping comments before grepping is the whole trick here.
@sed -n '/^static const unsigned char SHELLCODE\[\] = {/,/^};/p' fooc.c \
| sed -e 's,/\*.*\*,,' \
| grep -o '0x[0-9a-fA-F][0-9a-fA-F]' \
| tr 'A-F' 'a-f' | tr '\n' ' ' | sed -e 's/^ //' -e 's/[[:space:]]*$$//' \
> .sc_c_raw.txt
@echo "=== bytes declared in fooc.c's SHELLCODE[] array:"
@cat .sc_c_raw.txt
@echo
@echo "=== comparing ..."
@# Both sides reduced to the same plain "31 f6 31 d2 ..." form, so the
@# comparison is on VALUES and not on how each tool happens to print them.
@sed -e 's/0x//g' .sc_c_raw.txt > .sc_c.txt
@od -An -tx1 -v shellcode.bin | tr -s ' \n' ' ' | sed -e 's/^ //' \
-e 's/[[:space:]]*$$//' > .sc_asm.txt
@if cmp -s .sc_c.txt .sc_asm.txt; then \
n=$$(wc -c < shellcode.bin); \
echo "MATCH: the $$n bytes in fooc.c are byte-for-byte what"; \
echo " shellcode.S assembles to."; \
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; \
else \
echo "MISMATCH -- the two differ:"; \
diff .sc_c.txt .sc_asm.txt || true; \
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt; exit 1; \
fi
# -----------------------------------------------------------------------------
# run: start the daemon in the background.
#
# setsid + nohup + </dev/null are all needed. Without setsid the daemon dies
# when the invoking shell exits; without </dev/null it inherits your terminal
# and competes with you for it; without nohup it gets SIGHUP.
#
# It listens on 127.0.0.1 only. Please keep it that way.
# -----------------------------------------------------------------------------
PORT ?= 2342
run: food
@echo "=== starting food on 127.0.0.1:$(PORT)"
@setsid nohup ./food -p $(PORT) > food.log 2>&1 </dev/null & \
disown 2>/dev/null || true
@sleep 1
@if pgrep -x food >/dev/null; then \
echo "=== food is running (pid $$(pgrep -x food | head -1))"; \
echo "=== stack segment -- 'rwxp' means executable (needed for shellcode):"; \
grep '\[stack\]' /proc/$$(pgrep -x food | head -1)/maps; \
else \
echo "=== food failed to start; see food.log"; exit 1; \
fi
# -----------------------------------------------------------------------------
# test: the technique matrix. Every technique must print both SEEN.
#
# Note this runs against whatever ./food currently is. If you last ran
# `make hardened`, you are testing the hardened build -- which is what
# test-hardened is for.
# -----------------------------------------------------------------------------
#
# Note on the redirection below. The verdict is the "[pty_test] ..." line the
# harness prints to STDERR, and its EXIT STATUS, so stderr is sent to the
# terminal and the shell's chatter (stdout) is discarded. Piping the two
# together and tailing is what hid a real failure during development: the pty's
# echo of our own command line contains the marker string, so a loose grep on
# the transcript was always going to pass.
test: tests/pty_test
@fail=0; \
for t in ret2win ret2libc shellcode; do \
echo "=================== $$t"; \
if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \
:; \
else \
fail=1; \
fi; \
done; \
echo; \
if [ $$fail -eq 0 ]; then \
echo "=== all three techniques gave a working shell"; \
else \
echo "=== at least one technique did NOT work."; \
echo "=== If food was built with `make hardened`, that is the"; \
echo "=== mitigations doing their job. See README.md."; \
fi; \
exit $$fail
# -----------------------------------------------------------------------------
# test-hardened: swap in the hardened daemon, prove the mitigations hold, then
# put the vulnerable one back. Leaves your tree exactly as it found it.
# -----------------------------------------------------------------------------
#
# Two things this target has to get right, both of which bit during development:
#
# * `pgrep -x` matches the process NAME, and the hardened binary is
# food_hardened, not food. Using the wrong name silently inspects nothing.
# * The verdict is pty_test's EXIT STATUS (0 = both markers seen), not the
# presence of its output line. Grepping for a line that is also printed on
# failure reports success for a run that crashed.
test-hardened: hardened tests/pty_test
@if ! pgrep -x food >/dev/null; then \
echo "=== start the daemon first: make run"; exit 1; \
fi
@echo "### stopping the vulnerable daemon"
@$(MAKE) --no-print-directory stop
@echo "### starting food_hardened instead"
@setsid nohup ./food_hardened -p $(PORT) > food_hardened.log 2>&1 \
</dev/null & disown 2>/dev/null || true
@sleep 1
@if ! pgrep -x food_hardened >/dev/null; then \
echo "!!! food_hardened did not start; see food_hardened.log"; \
$(MAKE) --no-print-directory stop; exit 1; \
fi
@echo "### stack segment: 'rw-p' (NOT executable) is what you want to see"
@grep '\[stack\]' /proc/$$(pgrep -x food_hardened | head -1)/maps || true
@echo
@for t in ret2win ret2libc shellcode; do \
echo "=================== $$t"; \
if ./tests/pty_test -t $$t 2>&1 >/dev/null; then \
echo "!!! $$t STILL WORKED against the hardened build"; \
else \
echo "--- $$t was stopped by the mitigations (as expected)"; \
fi; \
done; \
echo
@$(MAKE) --no-print-directory stop
@echo "### restoring the vulnerable daemon"
@setsid nohup ./food -p $(PORT) > food.log 2>&1 </dev/null \
& disown 2>/dev/null || true
@sleep 1
@echo
@echo "=== mitigation comparison is above."
@echo "=== Read the table in README.md to see which flag stopped what,"
@echo "=== and note which mitigations are NOT enough on their own."
# -----------------------------------------------------------------------------
# debug: build food and run it under gdb, stopping at the vulnerable read() so
# you can watch the stack frame get overwritten.
# -----------------------------------------------------------------------------
debug: food.c
$(CC) $(CSTD) $(DBG) $(WARN) $(VULN) -o food $<
@echo "=== built ./food for gdb. Try:"
@echo " gdb -q ./food"
@echo " (gdb) break food.c:393 # the read() that overflows"
@echo " (gdb) run -p 2342"
@echo " (gdb) info registers rsp rbp"
@echo " (gdb) x/24gx \$rsp # watch the return address"
# -----------------------------------------------------------------------------
# stop: kill the daemon.
#
# `pkill -x food` matches the process NAME exactly. Do NOT use
# `pkill -f ./food` -- that pattern also matches the shell you typed it into,
# so it kills your own session. This is not a theoretical risk; it happened
# while building this lab.
# -----------------------------------------------------------------------------
stop:
@if pgrep -x food >/dev/null; then \
pkill -x food; sleep 0.5; \
echo "=== food stopped"; \
else \
echo "=== food was not running"; \
fi
@# The hardened binary has a different process name, so it needs its own
@# pkill. A leftover food_hardened keeps port 2342 bound and makes the
@# next `make run` fail with "Address already in use".
@if pgrep -x food_hardened >/dev/null; then \
pkill -x food_hardened; sleep 0.5; \
echo "=== food_hardened stopped"; \
fi
clean:
rm -f food fooc food.hardened shellcode.bin
rm -f .sc_c_raw.txt .sc_c.txt .sc_asm.txt
rm -f tests/pty_test tests/sock_test
@echo "=== cleaned. (food.log is left alone; it is your evidence.)"
.PHONY: all run stop test test-hardened verify verify-shellcode hardened debug clean