Initial commit
This commit is contained in:
commit
394e3be54d
41 changed files with 16315 additions and 0 deletions
853
food.c
Normal file
853
food.c
Normal file
|
|
@ -0,0 +1,853 @@
|
|||
/*
|
||||
* ============================================================================
|
||||
* food.c -- "food": an INTENTIONALLY VULNERABLE network daemon
|
||||
* ============================================================================
|
||||
*
|
||||
* PURPOSE
|
||||
* -------
|
||||
* This is a deliberately broken TCP daemon used as a *target* for the
|
||||
* companion exploit `fooc`. It exists so you can learn, hands-on, what a
|
||||
* stack buffer overflow actually is, how it is abused to get remote code
|
||||
* execution (RCE), and -- most importantly -- how each of its bugs maps onto
|
||||
* a concrete, well-known defence that a real program should use instead.
|
||||
*
|
||||
* NOTHING HERE IS SAFE. Every "vulnerability" in this file is a real,
|
||||
* long-documented class of C bug:
|
||||
*
|
||||
* Bug #1 Unbounded read() into a fixed stack buffer .... CWE-120
|
||||
* Bug #2 Unchecked format string from the network ..... CWE-134
|
||||
* Bug #3 Use of attacker-controlled data as a path ... CWE-22
|
||||
* Bug #4 Stack canary would have caught Bug #1 ......... CWE-121
|
||||
* Bug #5 NX bit would have stopped shellcode ........... CWE-94
|
||||
* Bug #6 PIE/ASLR would have randomised the targets .... CWE-829
|
||||
*
|
||||
* The comments next to each bug name the fix. That mapping is the entire
|
||||
* point of the exercise.
|
||||
*
|
||||
* SAFETY RAILS (please keep them in place while you experiment)
|
||||
* ------------------------------------------------------------
|
||||
* * It binds to 127.0.0.1 (loopback) by default, so the deliberately
|
||||
* exploitable service is NOT reachable from your network.
|
||||
* * It runs in the foreground with a banner so you can watch it die.
|
||||
* * Each connection is handled in a forked child, so one crash does not
|
||||
* take the daemon down.
|
||||
*
|
||||
* Build: make food
|
||||
*
|
||||
* THE BUILD IS THE POINT, PART 1
|
||||
* ------------------------------
|
||||
* `make food` compiles this file with three flags that a sane project would
|
||||
* never use, each switched off on purpose so the lab behaves the same way on
|
||||
* every machine:
|
||||
*
|
||||
* -fno-stack-protector no stack canary
|
||||
* -no-pie fixed load address, so win() is a constant
|
||||
* -z execstack executable stack, so shellcode can run
|
||||
*
|
||||
* Drop any one of them and the corresponding technique stops working. That is
|
||||
* not a flaw in the exploit; that is the defence being demonstrated. The
|
||||
* Makefile's `make hardened` target builds the same source WITHOUT all three,
|
||||
* and `make test-hardened` shows you which techniques it kills.
|
||||
*
|
||||
* Note that -z execstack is the reason `./fooc -t shellcode` works at all. A
|
||||
* stock Linux stack is not executable (`rw-p` in /proc/PID/maps, and `RWE` in
|
||||
* the ELF program headers only when this flag is present), and the shellcode
|
||||
* technique dies with SIGSEGV at RIP = the address of the payload. Everything
|
||||
* in README.md's mitigation table explains why that flag matters to you.
|
||||
*
|
||||
* Usage: ./food [-h HOST] [-p PORT] [-d]
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
/* Ask glibc for the extra declarations we need (dprintf, etc.). */
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <arpa/inet.h> /* inet_pton(), to turn "127.0.0.1" into bytes. */
|
||||
#include <errno.h> /* errno and the strerror() family. */
|
||||
#include <fcntl.h> /* dup2(), used to hand the socket to the shell. */
|
||||
#include <netinet/in.h>/* struct sockaddr_in, htons(), the TCP address. */
|
||||
#include <signal.h> /* signal(), SIGPIPE / SIGCHLD handling. */
|
||||
#include <stdarg.h> /* va_list, needed by our own tiny printf wrapper. */
|
||||
#include <stdint.h> /* uint16_t, the fixed-width type htons() returns. */
|
||||
#include <stdio.h> /* printf, dprintf, fputs. */
|
||||
#include <stdlib.h> /* exec*, _exit, atoi. */
|
||||
#include <string.h> /* memset, strncpy, strlen, memchr. */
|
||||
#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: the saved CPU registers. */
|
||||
#include <sys/wait.h> /* waitpid(), for reaping children. */
|
||||
#include <unistd.h> /* read, write, close, dup2, getpid, fork, chdir,
|
||||
* getopt -- the POSIX workhorses. */
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Configuration constants */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Default TCP port. Not privileged (>1024), so no root is required. */
|
||||
#define FOOD_PORT 2342
|
||||
|
||||
/* Loopback only, on purpose. Change with -h if you really know better. */
|
||||
#define FOOD_HOST "127.0.0.1"
|
||||
|
||||
/* Size of the stack buffer in vulnerable_handler(). This is the value the
|
||||
* exploit has to fill *plus* 8 bytes of saved frame pointer before it can
|
||||
* reach the return address. Do not change it without re-running the exploit's
|
||||
* automatic offset detection, which reads it from this binary. */
|
||||
#define FOOD_BUFSZ 64
|
||||
|
||||
/* How many bytes the vulnerable read() is willing to accept. This is much
|
||||
* larger than FOOD_BUFSZ on purpose -- that mismatch IS the vulnerability. */
|
||||
#define FOOD_READMAX 512
|
||||
|
||||
/* Size of the (also broken) log line buffer used by the format-string demo. */
|
||||
#define FOOD_LOGSZ 128
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Tiny helpers */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* g_logfd -- the descriptor logmsg() writes to.
|
||||
*
|
||||
* It begins life as a duplicate of the real stdout, taken *before*
|
||||
* prepare_client_fds() replaces fd 1 with the client's socket. The point is
|
||||
* that the server's log must never travel to the attacker.
|
||||
*
|
||||
* This is not cosmetic. If the daemon had kept logging to fd 1, then the
|
||||
* moment a client connected, every log line -- including file paths, internal
|
||||
* hostnames, and in a real system any credential that ever reached a log --
|
||||
* would be delivered to whoever happened to be on the other end of the socket.
|
||||
* Keeping diagnostics on a separate, trusted descriptor is a genuine security
|
||||
* practice, and a lab about exploiting a daemon would be a poor place to
|
||||
* accidentally teach the alternative.
|
||||
*/
|
||||
static int g_logfd = -1;
|
||||
|
||||
/*
|
||||
* logmsg() -- print one timestamped line to the log descriptor.
|
||||
*
|
||||
* (Deliberately NOT named `logf`: that collides with the libm builtin
|
||||
* `float logf(float)`, and GCC warns about it. Naming your own helpers after
|
||||
* standard library functions is a surprisingly common source of pain.)
|
||||
*
|
||||
* We use dprintf() rather than printf() because we need to write to a specific
|
||||
* descriptor, and because it is close to atomic: one write() call means two
|
||||
* forked children cannot interleave halfway through a line.
|
||||
*/
|
||||
static void logmsg(const char *fmt, ...)
|
||||
{
|
||||
char line[1024]; /* Compose the whole message in one buffer. */
|
||||
va_list ap; /* The argument list of this variadic call. */
|
||||
int n; /* Bytes composed. */
|
||||
|
||||
/*
|
||||
* va_start MUST be called before the va_list is used. It initialises `ap`
|
||||
* to point just past `fmt` in the argument area. Passing an uninitialised
|
||||
* va_list to vsnprintf makes it walk wild stack memory and crash -- which
|
||||
* is exactly what happened the first time this function was written.
|
||||
*
|
||||
* va_end is mandatory once va_start has been called, even on error paths.
|
||||
*/
|
||||
va_start(ap, fmt);
|
||||
|
||||
/* Format the body first, into the tail of the buffer, leaving room for
|
||||
* the "[food 1234] " prefix and the trailing newline. */
|
||||
n = vsnprintf(line, sizeof(line) - 32, fmt, ap);
|
||||
va_end(ap); /* Always pair va_start with va_end. */
|
||||
if (n < 0)
|
||||
return;
|
||||
|
||||
/* Prepend the pid. Knowing which forked child did what is what makes the
|
||||
* per-connection log readable. */
|
||||
if (g_logfd >= 0)
|
||||
dprintf(g_logfd, "[food %d] %s\n", (int)getpid(), line);
|
||||
}
|
||||
|
||||
/*
|
||||
* read_exact() -- read exactly n bytes, looping until we have them all.
|
||||
*
|
||||
* This helper is *correct*. The bug in this program is not here.
|
||||
*
|
||||
* It is included deliberately, so you can compare it against vulnerable_handler()
|
||||
* below. The difference between the two functions is, essentially, the whole
|
||||
* lesson: this one asks for `n` bytes and checks it got them; the other asks
|
||||
* for far more than its buffer can hold and never checks.
|
||||
*
|
||||
* Why it matters: read() on a socket is allowed to return a short count (it
|
||||
* is a stream, not a message queue). A correct program must loop. The
|
||||
* vulnerable function below deliberately does not do this correctly either.
|
||||
*/
|
||||
__attribute__((unused)) /* Referenced in comments only, so silence the
|
||||
* -Wunused-function warning deliberately. */
|
||||
static ssize_t read_exact(int fd, void *buf, size_t n)
|
||||
{
|
||||
size_t got = 0; /* Bytes received so far. */
|
||||
while (got < n) { /* Keep going until the full request. */
|
||||
ssize_t r = read(fd, (char *)buf + got, n - got);
|
||||
if (r < 0) { /* r < 0 means an error occurred. */
|
||||
if (errno == EINTR) /* Interrupted by a signal: just retry. */
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
if (r == 0) /* Peer closed the connection. */
|
||||
break;
|
||||
got += (size_t)r; /* Otherwise bank the bytes. */
|
||||
}
|
||||
return (ssize_t)got; /* Return total bytes actually read. */
|
||||
}
|
||||
|
||||
/*
|
||||
* write_all() -- write a whole buffer, looping over short writes.
|
||||
* Also correct. Exists so the exploit's I/O is not the flaky part of the lab.
|
||||
*/
|
||||
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 "backdoor" function that ret2win aims at.
|
||||
*
|
||||
* A ret2win exploit works by overwriting the saved return address with the
|
||||
* address of a function that (a) is already in the binary and (b) does
|
||||
* something useful to the attacker. Here, that is "hand me a shell".
|
||||
*
|
||||
* The *presence* of win() is not itself the vulnerability -- shipping a hidden
|
||||
* "debug backdoor" like this is a real and sadly common self-inflicted wound
|
||||
* (it is exactly the CVE class "undocumented backdoor", e.g. the Juniper
|
||||
* ScreenOS backdoors). But in this lab it exists purely as an easy, reliable
|
||||
* first target so you can prove code execution before reaching for shellcode.
|
||||
*
|
||||
* noinline: the compiler must not inline this away, or the exploit would have
|
||||
* no address to jump to.
|
||||
* used: keeps the function alive even though we never call it in C.
|
||||
*/
|
||||
__attribute__((noinline, used))
|
||||
static void win(void)
|
||||
{
|
||||
pid_t pid; /* Child's PID after the fork below. */
|
||||
|
||||
logmsg("win() reached -- executing /bin/sh");
|
||||
|
||||
/*
|
||||
* Note the signature: win() deliberately takes NO arguments, and that is
|
||||
* the whole point rather than an oversight.
|
||||
*
|
||||
* A ret2win exploit overwrites only the saved *return address*. Every
|
||||
* other register holds whatever the vulnerable function happened to leave
|
||||
* behind, and the attacker has no control over any of them. An earlier
|
||||
* revision of this function took an `int fd` parameter, and gdb showed the
|
||||
* exploit apparently landing while actually passing garbage in rdi
|
||||
* (-11073), which made every dup2() fail and the "shell" go nowhere.
|
||||
* Depending on an incoming argument is the most common reason a
|
||||
* ret2win-style exploit looks like it works and then silently does nothing.
|
||||
*
|
||||
* We do not need the argument: prepare_client_fds() has already made
|
||||
* fds 0, 1 and 2 refer to the client's socket, so the shell can simply
|
||||
* use the standard descriptors.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Fork before exec so the parent can reap the child and go away, while
|
||||
* the child keeps talking to the client. Without this the daemon would
|
||||
* stay busy until the shell exits.
|
||||
*/
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
logmsg("win(): fork() failed: %s", strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
if (pid > 0) { /* Parent: reap the child, then bail out. */
|
||||
waitpid(pid, NULL, 0);
|
||||
/*
|
||||
* _exit, NOT return. Returning would execute `ret` a second time,
|
||||
* popping the next 8 bytes of attacker payload as a new RIP and
|
||||
* crashing immediately. Exiting is the only safe way out of a
|
||||
* function that was entered by hijacking a return address.
|
||||
*/
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Child. stdin/stdout/stderr already point at the socket (see
|
||||
* prepare_client_fds), so there is nothing to rewire here.
|
||||
*
|
||||
* Dropping privileges is deliberately absent: in a real system this is
|
||||
* exactly where you would setuid()/setgid() to an unprivileged user
|
||||
* before exec. A backdoor that hands out a root shell is what turns an
|
||||
* ordinary memory-safety bug into a full compromise.
|
||||
*/
|
||||
execl("/bin/sh", "sh", (char *)NULL); /* Does not return on success. */
|
||||
_exit(127); /* Only if exec failed. */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* The vulnerable handler -- Bug #1 and Bug #2 live here */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* noinline: mandatory for a stack-overflow lab. If the compiler inlines this
|
||||
* into its caller, the stack frame the exploit is aiming at changes
|
||||
* and the whole exercise stops making sense.
|
||||
* used: do not let the optimiser delete it.
|
||||
*/
|
||||
__attribute__((noinline, used))
|
||||
static void vulnerable_handler(int fd)
|
||||
{
|
||||
char buf[FOOD_BUFSZ]; /* 64 bytes of stack. The whole ballgame. */
|
||||
char line[FOOD_LOGSZ]; /* Second, larger buffer for the format bug. */
|
||||
ssize_t n; /* Byte count returned by read(). */
|
||||
|
||||
/*
|
||||
* ------------------------------------------------------------------
|
||||
* The buffer-address leak, done on purpose, inside this function.
|
||||
* ------------------------------------------------------------------
|
||||
* We hand the client the exact address of `buf` *before* it overflows
|
||||
* anything. Without this the client is shooting blind at a randomised
|
||||
* stack, and you would need either a lucky guess or a "ret sled"
|
||||
* thousands of ret-instructions wide.
|
||||
*
|
||||
* Where do real-world leaks of this kind come from? All of these are
|
||||
* genuine, frequently-seen CWE-200 / CWE-497 bugs:
|
||||
*
|
||||
* * a format string bug printing %p (our Bug #2 above does this),
|
||||
* * returning or serialising a pointer that was never initialised
|
||||
* (CWE-457, use of uninitialised variable -- a very common way to
|
||||
* turn a mere crash into a full info leak),
|
||||
* * a verbose crash handler or core dump served to the client,
|
||||
* * a debug endpoint left enabled, or /proc/self/maps over HTTP,
|
||||
* * a non-randomised fixed-address mmap(), or a non-PIE binary,
|
||||
* which is precisely why the Makefile here builds with -no-pie.
|
||||
*
|
||||
* The real lesson: address-space layout randomisation is only a
|
||||
* *speed bump*. It raises the cost of an exploit; it is not a fix. The
|
||||
* fix is not having the memory corruption in the first place.
|
||||
*
|
||||
* FIX: do not disclose addresses to untrusted clients, and initialise
|
||||
* every pointer before you might print it.
|
||||
*/
|
||||
dprintf(fd, "BUF=%p\n", (void *)buf);
|
||||
|
||||
/*
|
||||
* ====================================================================
|
||||
* BUG #1 -- UNBOUNDED COPY INTO A FIXED STACK BUFFER (CWE-120)
|
||||
* ====================================================================
|
||||
*
|
||||
* This single read() call is the entire exploit surface:
|
||||
*
|
||||
* we have FOOD_BUFSZ = 64 bytes of room
|
||||
* we accept FOOD_READMAX = 512 bytes from the network
|
||||
*
|
||||
* The attacker therefore gets to write 448 bytes more than they should,
|
||||
* and everything laid out on the stack above `buf` gets clobbered.
|
||||
*
|
||||
* In a compiled x86-64 function the stack grows *downwards*, so memory
|
||||
* looks like this, with rbp pointing at the saved frame pointer:
|
||||
*
|
||||
* high addresses
|
||||
* +------------------------+ <- rbp + 16 : caller locals
|
||||
* | ... |
|
||||
* +------------------------+ <- rbp + 8 : SAVED RETURN ADDRESS <-- RIP
|
||||
* | saved rbp (8 bytes) |
|
||||
* +------------------------+ <- rbp : our frame pointer
|
||||
* | line[128] | (second buffer, padding)
|
||||
* | buf[64] | <- rsp: what read() will fill
|
||||
* +------------------------+
|
||||
* low addresses
|
||||
*
|
||||
* So the attacker writes 64 bytes of junk to fill `buf`, another 8 bytes
|
||||
* to fill the saved rbp, and the *next* 8 bytes become the return address
|
||||
* that the `ret` instruction pops into RIP. From that moment the attacker
|
||||
* decides where the CPU executes next.
|
||||
*
|
||||
* FIXES, in increasing order of strength:
|
||||
* 1. Bound every read by the true size of the destination:
|
||||
* n = read(fd, buf, sizeof(buf) - 1); <-- the real fix
|
||||
* 2. Compile with -fstack-protector-strong so a *canary* sits between
|
||||
* the buffers and the return address; `ret` then aborts first.
|
||||
* 3. Compile with -fstack-protector-all (covers locals that a plain
|
||||
* -O2 might have kept in registers).
|
||||
* 4. Real root cause: do not use fixed-size stack arrays for input at
|
||||
* all. Use heap allocation sized from a checked length, or a
|
||||
* stdio-style bounded reader.
|
||||
*
|
||||
* NOTE: modern GCC detects *this exact shape* at compile time and warns
|
||||
* ("writing 512 bytes into a region of size 64"). Never suppress that
|
||||
* warning in real code -- it is free security.
|
||||
*/
|
||||
n = read(fd, buf, FOOD_READMAX); /* <-- CWE-120, THE bug. */
|
||||
if (n <= 0)
|
||||
return; /* Nothing to do. */
|
||||
|
||||
/*
|
||||
* Echo back what we received, truncated to the buffer's real size so that
|
||||
* *this* line is safe. It is here purely so you can watch the overflow
|
||||
* happen live in the log. Truncating for display does NOT undo the
|
||||
* overwrite that already happened above.
|
||||
*/
|
||||
{
|
||||
ssize_t show = n < FOOD_BUFSZ ? n : FOOD_BUFSZ; /* clamp for log. */
|
||||
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)
|
||||
* ====================================================================
|
||||
*
|
||||
* `buf` is fully attacker-controlled. Passing it to printf() as the
|
||||
* *format* rather than as a %s *argument* lets the attacker supply their
|
||||
* own conversion specifiers: %x to read stack words, %n to *write* to
|
||||
* memory, %s to walk arbitrary pointers. A %n here is a write-what-where
|
||||
* primitive, which is another way to build an exploit.
|
||||
*
|
||||
* FIX: never pass untrusted data as the format string. Use
|
||||
* printf("%s", buf); or better, fwrite()/write() of a length.
|
||||
*
|
||||
* We keep this as a *demonstration only* -- it runs on a copy in `line`
|
||||
* so the crash it causes is obviously separate from Bug #1. The payload
|
||||
* you send by default is plain text with no '%' characters, so this line
|
||||
* is a no-op unless you explicitly ask for the format-string demo with
|
||||
* the `fooc --fmt` mode.
|
||||
*/
|
||||
if (memchr(buf, '%', (size_t)n) != NULL) {
|
||||
snprintf(line, sizeof(line), "%.*s", (int)FOOD_LOGSZ - 1, buf);
|
||||
logmsg("vulnerable_handler: payload contains '%%', echoing it raw");
|
||||
(void)write_all(fd, line, strlen(line)); /* safe echo of raw text. */
|
||||
}
|
||||
|
||||
/*
|
||||
* When this function returns, the CPU pops the (attacker-controlled)
|
||||
* saved return address into RIP and jumps wherever the attacker chose.
|
||||
* The `leave` + `ret` pair in the generated assembly is the exact
|
||||
* instruction that hands over control.
|
||||
*/
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* fd handling */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* on_sigsegv() -- print exactly where the CPU was trying to go.
|
||||
*
|
||||
* This handler exists purely to make the exploit *visible*. When the overflow
|
||||
* lands, the CPU jumps to an address we chose; that address is almost always
|
||||
* unmapped, the CPU raises SIGSEGV, and this handler runs.
|
||||
*
|
||||
* Two facts come out of the kernel for free:
|
||||
*
|
||||
* * ucontext->uc_mcontext.gregs[REG_RIP] is the address of the instruction
|
||||
* the CPU was executing, i.e. the value of the instruction pointer at the
|
||||
* moment of the fault. If we clobbered the return address, THIS is our
|
||||
* eight bytes. It is the most direct possible proof that the attacker
|
||||
* controls RIP.
|
||||
* * siginfo->si_addr is the bad address the access was aimed at.
|
||||
*
|
||||
* Both are read out of the ucontext_t that the kernel hands us, which is why
|
||||
* this needs <sys/ucontext.h> and _GNU_SOURCE.
|
||||
*
|
||||
* A production server absolutely should catch SIGSEGV like this -- not to keep
|
||||
* serving, but to log the fault address so that a crash *tells you it was
|
||||
* malicious*. Crashing silently is what makes these bugs survive for years.
|
||||
*/
|
||||
static void on_sigsegv(int sig, siginfo_t *si, void *ucv)
|
||||
{
|
||||
ucontext_t *uc = (ucontext_t *)ucv; /* The CPU's saved register state. */
|
||||
unsigned long rip = 0;
|
||||
unsigned long 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, rsp);
|
||||
logmsg("SIGSEGV: RIP is the return address the client supplied. "
|
||||
"If it is 0x4141414141414141, that is our 'A' padding. "
|
||||
"If it looks like a real code or libc address, we were hijacked.");
|
||||
|
||||
/*
|
||||
* Re-raise with the default disposition so the process still dies with the
|
||||
* correct status and still dumps core. A handler that swallowed the
|
||||
* signal and returned would re-execute the faulting instruction forever,
|
||||
* because the bad address has not been fixed -- an easy way to turn one
|
||||
* crash into an unkillable hang.
|
||||
*/
|
||||
signal(sig, SIG_DFL);
|
||||
raise(sig);
|
||||
}
|
||||
|
||||
/*
|
||||
* install_crash_reporter() -- attach on_sigsegv() to this process.
|
||||
*
|
||||
* Called in the forked child, so installing it is cheap and affects only the
|
||||
* process serving one connection. The parent keeps its default dispositions
|
||||
* and is therefore not slowed by signal handling.
|
||||
*/
|
||||
static void install_crash_reporter(void)
|
||||
{
|
||||
struct sigaction sa; /* The action structure sigaction() wants. */
|
||||
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_sigaction = on_sigsegv; /* The extended handler form. */
|
||||
sa.sa_flags = SA_SIGINFO; /* "...and pass me the siginfo_t." */
|
||||
|
||||
/* An empty sigset means "block nothing extra while in the handler". */
|
||||
sigemptyset(&sa.sa_mask);
|
||||
|
||||
if (sigaction(SIGSEGV, &sa, NULL) < 0)
|
||||
logmsg("sigaction(SIGSEGV) failed: %s", strerror(errno));
|
||||
if (sigaction(SIGBUS, &sa, NULL) < 0) /* Misaligned access, same idea. */
|
||||
logmsg("sigaction(SIGBUS) failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
/*
|
||||
* prepare_client_fds() -- point fds 0, 1 and 2 at the accepted socket.
|
||||
*
|
||||
* Doing this once, up front, is what lets every exploitation technique in
|
||||
* `fooc` work identically:
|
||||
*
|
||||
* * ret2win -> win() execs /bin/sh with fds 0-2 already on the socket.
|
||||
* * ret2libc -> system("/bin/sh") likewise inherits the socket.
|
||||
* * shellcode -> execve("/bin/sh") likewise inherits the socket.
|
||||
*
|
||||
* so whichever payload lands, the resulting shell talks straight back to the
|
||||
* attacker 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 -- Bug #3 lives here (this one is a feature in the lab)*/
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* send_leaks() -- hand the attacker two pointers, on purpose.
|
||||
*
|
||||
* This models two *real* vulnerability classes, and it is what makes the
|
||||
* "hard" techniques (ret2libc, shellcode) deterministic instead of a
|
||||
* probability game:
|
||||
*
|
||||
* Leak A: a STACK address (the address of a local variable).
|
||||
* Real-world analogue: CWE-200 / CWE-497 "exposure of sensitive
|
||||
* information to an unauthorized actor" -- a debug endpoint, a verbose
|
||||
* error page, a crash dump, a /proc/self/maps file served over HTTP.
|
||||
* With it, the attacker learns exactly where their shellcode landed.
|
||||
*
|
||||
* Leak B: a LIBC address (the real address of `read`, resolved by the PLT
|
||||
* trampoline into libc).
|
||||
* Real-world analogue: the same, plus a classic function-pointer leak.
|
||||
* With it, the attacker computes the load address of libc and therefore
|
||||
* the addresses of `system` and of the "/bin/sh" string inside it.
|
||||
*
|
||||
* FIX for the daemon: do not print addresses to untrusted clients, and do
|
||||
* not leave debug endpoints enabled in production builds.
|
||||
*
|
||||
* The text format is deliberately simple so the exploit can parse it with a
|
||||
* one-line sscanf():
|
||||
*
|
||||
* "FOOD 1.0 leak stack=0x<hex> libc=0x<hex>\n"
|
||||
*/
|
||||
static void send_leaks(int fd)
|
||||
{
|
||||
long stack_marker = 0; /* A local; its address reveals the stack base.*/
|
||||
/*
|
||||
* The *exact* type of `read` as declared in <unistd.h>. Getting this
|
||||
* signature wrong is a compile error in C (and a far worse bug in C++),
|
||||
* which is a nice reminder that the type system is a security tool:
|
||||
*
|
||||
* ssize_t read(int fd, void *buf, size_t nbytes);
|
||||
*
|
||||
* We store it in a variable only so we can print its value as a leak.
|
||||
*/
|
||||
ssize_t (*libc_read)(int, void *, size_t); /* Real libc `read` fn ptr. */
|
||||
|
||||
/*
|
||||
* Taking the address of a local is the leak. Compilers must honour this
|
||||
* (it is observable behaviour), so it cannot be optimised away.
|
||||
*/
|
||||
stack_marker = 0x4141414141414141L; /* Make it obvious in a debugger.*/
|
||||
|
||||
/*
|
||||
* `&read` is not the PLT stub once the dynamic linker has run: the GOT
|
||||
* holds the true address inside libc, so this yields a genuine libc
|
||||
* pointer. That is what makes leak B useful for ret2libc.
|
||||
*/
|
||||
libc_read = &read;
|
||||
|
||||
/*
|
||||
* 0644 octal = "rw-r--r--", the conventional permission bits for a file.
|
||||
* %p prints a pointer in the implementation-defined but universally
|
||||
* "0x..." form on glibc/x86-64.
|
||||
*/
|
||||
dprintf(fd, "FOOD 1.0 leak stack=%p libc=%p\n",
|
||||
(void *)&stack_marker, (void *)libc_read);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Per-connection handling */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* handle_client() -- do everything for one connected attacker.
|
||||
*
|
||||
* Runs in the forked child. Its job:
|
||||
* 1. Send a banner and the two leaks.
|
||||
* 2. Call the vulnerable function, which will be overflowed.
|
||||
* 3. Never return to the accept loop: if the overflow missed, exit cleanly;
|
||||
* if it hit, we never come back at all -- the CPU is somewhere else now.
|
||||
*/
|
||||
static void handle_client(int fd)
|
||||
{
|
||||
static const char banner[] =
|
||||
"FOOD 1.0 - deliberately vulnerable service\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 the crash reporter *after* the dup2 dance, so its log lines
|
||||
* (which go to g_logfd, not to the socket) cannot be seen by the client.
|
||||
*/
|
||||
install_crash_reporter();
|
||||
|
||||
logmsg("client connected (fd %d)", fd);
|
||||
|
||||
/* The banner and the leaks are two separate writes so the client can
|
||||
* read them incrementally without needing a length-prefixed protocol. */
|
||||
(void)write_all(STDOUT_FILENO, banner, sizeof(banner) - 1);
|
||||
send_leaks(STDOUT_FILENO);
|
||||
|
||||
/* Hand control to the vulnerable code. Nothing after this line is
|
||||
* guaranteed to run. */
|
||||
vulnerable_handler(STDOUT_FILENO);
|
||||
|
||||
/*
|
||||
* We only get here if the exploit *missed* its target, or if no exploit
|
||||
* was sent. Say goodbye politely so the exploit can tell the difference
|
||||
* between "failed" and "succeeded".
|
||||
*/
|
||||
logmsg("vulnerable_handler returned normally -- payload did not hijack RIP");
|
||||
(void)write_all(STDOUT_FILENO, "OK: no hijack, disconnecting.\n", 29);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* The server loop */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* make_listener() -- create the listening socket.
|
||||
*
|
||||
* A correct, boring, textbook implementation: it would be the same code in
|
||||
* production. Returns the fd, or -1 on failure.
|
||||
*/
|
||||
static int make_listener(const char *host, int port)
|
||||
{
|
||||
struct sockaddr_in addr; /* The TCP address we will bind to. */
|
||||
int fd; /* The socket descriptor. */
|
||||
int one = 1; /* Value for setsockopt(). */
|
||||
|
||||
fd = socket(AF_INET, SOCK_STREAM, 0); /* IPv4, TCP. */
|
||||
if (fd < 0) {
|
||||
logmsg("socket() failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* SO_REUSEADDR: let us restart quickly without TIME_WAIT blocking us. */
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0)
|
||||
logmsg("setsockopt(SO_REUSEADDR) failed: %s", strerror(errno));
|
||||
|
||||
/*
|
||||
* Zero the whole structure first. Leaving uninitialised padding bytes is
|
||||
* a real bug (CWE-457) that leaks stack memory to the kernel -- harmless
|
||||
* here, but habit-forming in a bad way, so we do it properly.
|
||||
*/
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET; /* IPv4. */
|
||||
addr.sin_port = htons((uint16_t)port);/* Network byte order. */
|
||||
|
||||
/*
|
||||
* inet_pton() parses the dotted-quad text form "127.0.0.1" into the
|
||||
* network-byte-order struct in_addr. It returns 1 on success, 0 on a
|
||||
* malformed address, -1 on error. This is the correct way to turn a
|
||||
* config string into an address -- strtoul() would happily accept things
|
||||
* like "0x7f000001" and hide bugs.
|
||||
*/
|
||||
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) {
|
||||
logmsg("bad bind address: %s", host);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* int -> unsigned short is a narrowing cast, so range-check the port
|
||||
* before htons() can silently truncate a value like 70000 to 4464. */
|
||||
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) { /* Backlog of 16 connections. */
|
||||
logmsg("listen() failed: %s", strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Tiny main() */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static void usage(const char *argv0)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"usage: %s [-h HOST] [-p PORT] [-d]\n"
|
||||
"\n"
|
||||
" -h HOST address to bind (default %s -- keep it on loopback!)\n"
|
||||
" -p PORT TCP port to listen on (default %d)\n"
|
||||
" -d daemonise: fork into the background\n"
|
||||
"\n"
|
||||
"WARNING: this program is intentionally exploitable. Do not run it\n"
|
||||
"on any host that matters, and do not bind it to 0.0.0.0.\n",
|
||||
argv0, FOOD_HOST, FOOD_PORT);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *host = FOOD_HOST; /* Bind address, overridable with -h. */
|
||||
int port = FOOD_PORT; /* Bind port, overridable with -p. */
|
||||
int daemonise = 0; /* Set by -d. */
|
||||
int lfd; /* Listening socket fd. */
|
||||
int i; /* getopt()'s index. */
|
||||
|
||||
/* getopt() parses the command line. ":h:p:d" = h/p take args, d does not,
|
||||
* leading ':' means "report missing argument as ':'". */
|
||||
while ((i = getopt(argc, argv, ":h:p:d")) != -1) {
|
||||
switch (i) {
|
||||
case 'h': host = optarg; break; /* host argument. */
|
||||
case 'p': port = atoi(optarg); break; /* port argument. */
|
||||
case 'd': daemonise = 1; break; /* background flag. */
|
||||
case ':': fprintf(stderr, "missing argument to -%c\n", optopt);
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
default: usage(argv[0]); /* unknown flag. */
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ignore SIGPIPE so a client disconnecting mid-write cannot kill us. */
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
/* Reap dead children automatically instead of accumulating zombies. */
|
||||
signal(SIGCHLD, SIG_IGN);
|
||||
|
||||
/*
|
||||
* Claim a private copy of stdout for logging, BEFORE any accept() can
|
||||
* dup2 a client socket over fd 1. Everything logged afterwards goes to
|
||||
* the real terminal or wherever stdout was pointed, never to a client.
|
||||
* g_logfd is 0 or 1 only if dup() failed, in which case we fall back to
|
||||
* the original stdout in logmsg().
|
||||
*/
|
||||
g_logfd = dup(STDOUT_FILENO);
|
||||
if (g_logfd < 0) {
|
||||
g_logfd = STDOUT_FILENO;
|
||||
fprintf(stderr, "food: warning: could not reserve a log descriptor\n");
|
||||
}
|
||||
|
||||
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 daemonisation so we cannot acquire a
|
||||
* controlling terminal. Parent exits, intermediate exits, we survive. */
|
||||
pid_t p1 = fork();
|
||||
if (p1 < 0) { perror("fork"); return 1; }
|
||||
if (p1 > 0) _exit(0); /* Original parent: go away. */
|
||||
if (setsid() < 0) perror("setsid");
|
||||
pid_t p2 = fork();
|
||||
if (p2 < 0) { perror("fork"); return 1; }
|
||||
if (p2 > 0) _exit(0); /* Session leader: also go away. */
|
||||
if (chdir("/") < 0) perror("chdir");
|
||||
umask(022); /* New files default to 0644. */
|
||||
}
|
||||
|
||||
/* ---- The accept loop. Runs forever. ---------------------------------- */
|
||||
for (;;) {
|
||||
struct sockaddr_in peer; /* Who connected. */
|
||||
socklen_t plen = sizeof(peer);
|
||||
int cfd; /* Client socket. */
|
||||
pid_t pid; /* Child pid. */
|
||||
|
||||
/*
|
||||
* accept() blocks until a client arrives, then returns a *new* fd
|
||||
* connected to that client. The listening fd stays open.
|
||||
*/
|
||||
cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
|
||||
if (cfd < 0) {
|
||||
if (errno == EINTR || errno == ECONNABORTED)
|
||||
continue; /* Transient: just try again. */
|
||||
logmsg("accept() failed: %s", strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fork per connection. Reason 1: isolation -- a segfault in the
|
||||
* exploit's payload kills only the child, so the daemon survives.
|
||||
* Reason 2: the child can _exit() without taking the server down.
|
||||
*/
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
logmsg("fork() failed: %s", strerror(errno));
|
||||
close(cfd);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
/* ---- Child: serve exactly one client, then die. -------------- */
|
||||
close(lfd); /* Release our copy of the listening socket. */
|
||||
handle_client(cfd);
|
||||
/* If the exploit worked, we never get here. If it did not, exit. */
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
/* ---- Parent: close our copy of the client socket and go around. -- */
|
||||
close(cfd);
|
||||
}
|
||||
|
||||
/* Not reached: the accept loop is infinite. */
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue