/* * ============================================================================ * foowosd.c -- "foowosd": an INTENTIONALLY VULNERABLE daemon that becomes * root the honest way: by being STARTED as root. * ============================================================================ * * PURPOSE * ------- * This is the "no setuid bit" companion to the other two labs: * * food / fooc a plain daemon: the overflow gives you a user shell * foosd / foosc a SETUID-root daemon: root arrives via the +s bit * foowosd/ foowosc THIS one: no +s bit anywhere. Root arrives because * somebody STARTED the process as root. * * The setuid bit is not the only way a process ends up privileged. Any * daemon launched by root -- a `sudo ./foowosd`, a systemd unit with * `User=root`, an init script -- has real uid 0, effective uid 0, and saved * uid 0. To the kernel and to every access-control check it makes, that * process IS root, indistinguishable from one that arrived there via +s. * And an overflow in a root process is a root exploit, filesystem * attributes notwithstanding. * * THAT is the lesson of this file: the setuid bit is a *transfer vehicle* * for privilege, not the privilege itself. "I don't have SUID binaries" is * NOT the same as "I am not vulnerable to privilege escalation". If your * daemon runs as root and it has a reachable memory-safety bug, you have a * root-exploit -- with or without the letter 's' in anyone's file mode. * * WHY THE EXPLOIT HERE IS DIFFERENT FROM THE SUID LAB -- ruid * ---------------------------------------------------------- * A setuid-root binary gives the process euid 0 but LEAVES ruid at the * launching user's id (1000). bash and dash notice `euid != ruid` at * startup and reset euid = ruid -- the shell's own guard against this * attack -- which is why foosc's shellcode had to call setreuid(0,0) first. * * A daemon *started* as root has ruid == euid == 0. There is no mismatch * for the shell's guard to notice, so a plain `execve("/bin/sh")` keeps * root -- no setreuid needed. The same 23 bytes that pwnd `food` in the * parent lab, byte for byte, open a *root* shell against this daemon, * because the process they run in is already fully root. The shellcode * chosen for foowosc therefore does not contain a setreuid prefix. * * SAFETY RAILS (identical policy to the SUID lab -- a root daemon is no * less dangerous because it got there without +s) * ------------------------------------------------- * * Binds 127.0.0.1 by default and REFUSES a non-loopback bind unless you * pass -L. A root daemon on a real interface is a remote root service. * * Logs at startup whether it is running as root or as a normal user, so * you always know which exploit outcome to expect. * * Same deliberate bugs as food/foosd, so the whole toolchain * (objdump-based offset discovery, leak parsing, alignment fix, pty * harness) carries over unchanged. * * Build: make foowosd * make run-root (needs sudo; starts the daemon as real root) * make run-root-ns (no sudo: user-namespace root, for verification) * make run (baseline: starts it as your normal user) * * HOW TO BECOME ROOT HERE -- and how NOT to * ----------------------------------------- * START AS ROOT: sudo make run-root -> ruid=0 euid=0 * START AS ROOT (ns): make run-root-ns -> namespaced 0/0 (test-only) * PLAIN USER: make run -> ruid=1000 euid=1000 * * The exploit behaves the same in all three cases -- it just yields a root * shell in the first two. That "the agency, not the attribute, is what * matters" property is the whole point of this lab. * * THE BUILD FLAGS (same deliberate removals as the other two labs) * ---------------------------------------------------------------- * -fno-stack-protector no canary: the overflow is not detected * -no-pie fixed addresses: win() is a constant * -z execstack executable stack: shellcode can run * * `make hardened` re-enables all three; the maliciously shareable lesson is * that those flags do nothing about the "running as root" design decision. * * Usage: ./foowosd [-h HOST] [-p PORT] [-d] [-L] * ============================================================================ */ /* Request the gnu decls we need (dprintf, etc.). */ #define _GNU_SOURCE #include /* inet_pton(): parse "127.0.0.1" into bytes. */ #include /* errno, strerror(). */ #include /* dup2() -- hand the accepted socket to the shell. */ #include /* setgroups(): part of the (never-called) privilege * drop -- supplementary groups must go first. */ #include /* struct sockaddr_in, htons(). */ #include /* signal(), sigaction(). */ #include /* va_list for our log wrapper. */ #include /* uint16_t. */ #include /* dprintf, snprintf. */ #include /* atoi, _exit. */ #include /* memset, strncmp, memchr, strlen. */ #include /* socket, bind, listen, accept. */ #include /* umask. */ #include /* ssize_t, pid_t. */ #include /* ucontext_t: REG_RIP etc. for the crash reporter. */ #include /* waitpid(). */ #include /* read, write, dup2, fork, getpid, setsid, chdir. */ /* ------------------------------------------------------------------------- */ /* Configuration constants */ /* ------------------------------------------------------------------------- */ /* Port. 2344 keeps this lab clear of food (2342) and foosd (2343). It is * above 1024 on purpose: binding it needs NO privilege, so root here is * pure design smell -- a correct daemon would drop privileges after bind, * and the lab's whole point is what happens when it does not. */ #define FOOWOSD_PORT 2344 /* Loopback is the ONLY default. -L is required to go further. */ #define FOOWOSD_HOST "127.0.0.1" /* Size of the overflowed buffer. Same shape as food/foosd so the shared * objdump-based offset detection works unchanged. */ #define FOOWOSD_BUFSZ 64 /* How much read() accepts. The mismatch with FOOWOSD_BUFSZ IS the bug. */ #define FOOWOSD_READMAX 512 /* Size of the second (format-string demo) buffer. */ #define FOOWOSD_LOGSZ 128 /* ------------------------------------------------------------------------- */ /* Logging (same design as the other labs: the log never reaches the attacker)*/ /* ------------------------------------------------------------------------- */ /* g_logfd -- a private copy of stdout taken BEFORE the socket is dup2()'d * over fd 1. Every logmsg() line goes here, so a client that overwrites our * memory or crashes a child never learns internal paths or addresses from * logs (and never mixes its own bytes with ours). */ static int g_logfd = -1; /* logmsg() -- timestamped, pid-prefixed line to the log descriptor. One * write() per line, so forked children cannot interleave mid-line. */ static void logmsg(const char *fmt, ...) { char line[1024]; /* Whole-message scratch. */ va_list ap; /* Variadic argument cursor. */ int n; /* Bytes formatted. */ /* va_start MUST precede any use of ap. An uninitialised va_list makes * vsnprintf walk wild stack memory -- a real bug that was hit in the * earlier food.c, hence the comment. */ va_start(ap, fmt); n = vsnprintf(line, sizeof(line) - 32, fmt, ap); va_end(ap); /* Always pair va_start with va_end. */ if (n < 0) return; if (g_logfd >= 0) dprintf(g_logfd, "[foowosd %d] %s\n", (int)getpid(), line); } /* read_exact() / write_all() -- the CORRECT I/O helpers, present so you can * hold them next to the deliberately broken read() in vulnerable_handler() * and see the difference: these loop until done and check every result. */ __attribute__((unused)) static ssize_t read_exact(int fd, void *buf, size_t n) { size_t got = 0; while (got < n) { ssize_t r = read(fd, (char *)buf + got, n - got); if (r < 0) { if (errno == EINTR) continue; return -1; } if (r == 0) break; got += (size_t)r; } return (ssize_t)got; } 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 "easy" backdoor. The same function as in food.c and foosd.c, * and the difference between this lab and the SUID lab is contained in it. * * In the SUID lab this exact code produced a NON-root shell, because foosd * had euid 0 but ruid 1000, and bash reset euid = ruid at startup. * * Here the daemon is STARTED as root, so at this instant ruid == euid == 0. * fork() inherits both ids, execve() changes neither, and bash starts with * equal uid 0s -- its guard has nothing to reset, so execve("/bin/sh") keeps * root. "spawn a shell" works against a genuinely-root process; it only * fails against the half-root (euid-only) state the setuid bit produces. * That asymmetry -- why one lab needs setreuid and this one does not -- is * the entire technical heart of the two labs side by side. */ __attribute__((noinline, used)) static void win(void) { pid_t pid; logmsg("win() reached -- exec'ing /bin/sh (ruid==euid here, so the shell " "stays root; contrast with foosd where ruid stayed 1000)"); /* Fork so the daemon's accept-loop child can be reaped and return. */ pid = fork(); if (pid < 0) { logmsg("win(): fork() failed: %s", strerror(errno)); _exit(1); } if (pid > 0) { waitpid(pid, NULL, 0); /* Must NOT return: that would pop attacker bytes as the next RIP. */ _exit(0); } /* Child. prepare_client_fds() already made fds 0/1/2 the socket. */ execl("/bin/sh", "sh", (char *)NULL); _exit(127); /* Only reached if exec failed. */ } /* ------------------------------------------------------------------------- */ /* The vulnerable handler -- Bug #1 and Bug #2 live here */ /* ------------------------------------------------------------------------- */ __attribute__((noinline, used)) static void vulnerable_handler(int fd) { char buf[FOOWOSD_BUFSZ]; /* 64 stack bytes. The whole ballgame. */ char line[FOOWOSD_LOGSZ]; /* Second buffer, for the format-string demo. */ ssize_t n; /* Bytes actually read. */ /* * The BUF= leak -- the same deliberate CWE-200 disclosure as the other * labs. The stack is ASLR-randomised; without this the shellcode could * not find itself. Real-world leaks of this kind come from %p format * bugs, crash dumps, debug endpoints, or serialised uninitialised * pointers. * * FIX: never print addresses to untrusted clients. */ dprintf(fd, "BUF=%p\n", (void *)buf); /* * ==================================================================== * BUG #1 -- UNBOUNDED COPY INTO A FIXED STACK BUFFER (CWE-120) * ==================================================================== * Identical to the other labs: 512 bytes are accepted into a 64-byte * array, so the attacker writes 448 bytes past the end, overwriting the * saved frame pointer and — 8 bytes later — the saved return address. * On return, `ret` jumps wherever the attacker said: * * [ 64 bytes buf ][ 8 bytes saved rbp ][ 8 bytes RETURN ADDRESS ] * * The ONLY difference from food is *what that means*: here the hijacked * process has real-and-effective uid 0 (it was started as root), so * "attacker controls RIP" becomes "attacker controls root's RIP" -- * with no setuid bit anywhere on this filesystem. * * FIXES (in increasing order of strength): * 1. n = read(fd, buf, sizeof(buf) - 1); <-- the real fix * 2. -fstack-protector-strong (canary aborts `ret`) * 3. do not take network input into fixed stack buffers at all * And SEPARATELY: never run this daemon as root; and if you must, drop * privileges the moment you are done binding (see drop_privs()). Memory * safety and least privilege are two different bugs; fix both. */ n = read(fd, buf, FOOWOSD_READMAX); /* <-- CWE-120, THE bug. */ if (n <= 0) return; /* Echo back a truncated copy so you can watch the overflow in the log. * Clamping for display does not undo the overwrite that already happened. */ { ssize_t show = n < FOOWOSD_BUFSZ ? n : FOOWOSD_BUFSZ; 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) * ==================================================================== * Same as the other labs: attacker '%'-specifiers in `buf` could read * stack words with %x or write memory with %n. Here the process is * root, so a %n is a write-what-where primitive IN A ROOT PROCESS. It * runs only on a copy in `line`, and only if the payload contains '%'. * * FIX: printf("%s", buf), never printf(buf). */ if (memchr(buf, '%', (size_t)n) != NULL) { snprintf(line, sizeof(line), "%.*s", (int)FOOWOSD_LOGSZ - 1, buf); logmsg("vulnerable_handler: payload contains '%%', echoing it raw"); (void)write_all(fd, line, strlen(line)); } /* On return the (attacker-controlled) saved return address becomes RIP. */ } /* ------------------------------------------------------------------------- */ /* Crash reporter (same rationale as the other labs: a crash should tell you */ /* it was malicious; the fault address is the return address the client */ /* supplied). */ /* ------------------------------------------------------------------------- */ static void on_sigsegv(int sig, siginfo_t *si, void *ucv) { ucontext_t *uc = (ucontext_t *)ucv; unsigned long rip = 0, 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 is the address the client " "supplied)", rip, rsp); logmsg("SIGSEGV: if RIP is a real address the attacker jumped there; " "if it is an address INSIDE vulnerable_handler itself it IS the " "`ret` instruction: a ret into a non-canonical address (e.g. " "0x4141414141414141) faults at the ret, not at the target."); /* Re-raise with the default disposition so the process still dies, with * the correct status, rather than re-executing the faulting instruction * forever (returning from this handler would do exactly that). */ signal(sig, SIG_DFL); raise(sig); } static void install_crash_reporter(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = on_sigsegv; /* Extended two-argument handler. */ sa.sa_flags = SA_SIGINFO; sigemptyset(&sa.sa_mask); if (sigaction(SIGSEGV, &sa, NULL) < 0) logmsg("sigaction(SIGSEGV) failed: %s", strerror(errno)); if (sigaction(SIGBUS, &sa, NULL) < 0) logmsg("sigaction(SIGBUS) failed: %s", strerror(errno)); } /* ------------------------------------------------------------------------- */ /* fd handling */ /* ------------------------------------------------------------------------- */ /* prepare_client_fds() -- put the accepted socket onto fds 0/1/2 so that * every technique (ret2win, ret2libc, shellcode) produces a shell that * automatically speaks 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 */ /* ------------------------------------------------------------------------- */ /* * send_leaks() -- tell the attacker: * * ids= this process's euid/ruid. THE "AM I ROOT ?" CHECK. * foowosc prints a loud warning when euid is not 0, * because without a root daemon there is no root shell * and the user would otherwise think the exploit broke. * leak stack=... an address on the stack, for the shellcode * leak libc=... the real address of read() inside libc, for ret2libc * * The `ids=` spelling (rather than "euid="/"ruid=") is deliberate: the test * harness proves a live shell by grepping the session transcript for the * strict `id`-output shape "uid=NNN(", and a banner containing "uid=" as * part of "euid="/"ruid=" would itself satisfy a careless grep. This kind of * "the probe and the answer must not share a signature" thinking is what you * do when you write real assertions about untrusted output. */ static void send_leaks(int fd) { long stack_marker = 0x4141414141414141L; /* Obvious in a debugger. */ ssize_t (*libc_read)(int, void *, size_t);/* Real address of read(). */ libc_read = &read; /* &read resolves through the GOT to libc. */ dprintf(fd, "FOOWOSD 1.0 ids=%d/%d leak stack=%p libc=%p\n", (int)geteuid(), (int)getuid(), (void *)&stack_marker, (void *)libc_read); } /* THE CORRECT DESIGN, PRESENT BUT NEVER CALLED * ------------------------------------------- * drop_privs() -- what a well-written daemon would do the moment it no * longer needs root. Two mistakes to notice, both immune to every compiler * mitigation: * * * ORDER: setgroups() before setgid() before setuid(), and ONLY AFTER * binding the port and opening any root-only files. Drop first and the * whole point of root is gone. * * PERMANENCE: setuid() to a nonzero value and check it stuck (a root * process may later regain privileges via the saved id otherwise). * * Port 2344 needs no privilege, so the correct design would call this right * after the listen() succeeds. In this lab it is deliberately absent from * main(), because the lab NEEDS the accept-loop children to stay root. The * commented function is your diff: the two missing calls at the point marked * "*** see drop_privs() ***" below are the entire exploit surface (Bug #3, * CWE-271: privilege not dropped before handling untrusted input). */ __attribute__((unused)) static void drop_privs(void) { /* Order matters: setgroups() first (a non-root user may not), then * setgid(), then setuid(). Never the reverse. */ (void)setgroups(0, NULL); /* Remove all supplementary groups. */ (void)setgid(1000); /* Lose group privileges. */ if (setuid(1000) < 0) /* Any non-zero uid is fine here. */ _exit(1); /* If we cannot drop, FAIL CLOSED. */ /* Verify. getuid()/geteuid() are cheap; a privileged program whose drop * failed silently is a root hole wearing a costume. */ if (getuid() != 1000 || geteuid() != 1000) _exit(1); } /* ------------------------------------------------------------------------- */ /* Per-connection handling */ /* ------------------------------------------------------------------------- */ static void handle_client(int fd) { static const char banner[] = "FOOWOSD 1.0 - deliberately vulnerable daemon (no setuid bit: root is\n" "here because this process was started as root).\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_crash_reporter(); /* Log (g_logfd) lines, not to the socket. */ logmsg("client connected (uid=%d euid=%d)", (int)getuid(), (int)geteuid()); (void)write_all(STDOUT_FILENO, banner, sizeof(banner) - 1); send_leaks(STDOUT_FILENO); vulnerable_handler(STDOUT_FILENO); /* Only reached when the payload did NOT hijack RIP. */ logmsg("vulnerable_handler returned normally -- payload did not hijack RIP"); (void)write_all(STDOUT_FILENO, "OK: no hijack, disconnecting.\n", 29); } /* ------------------------------------------------------------------------- */ /* The server loop */ /* ------------------------------------------------------------------------- */ static int make_listener(const char *host, int port) { struct sockaddr_in addr; int fd; int one = 1; fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { logmsg("socket() failed: %s", strerror(errno)); return -1; } if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) < 0) logmsg("setsockopt(SO_REUSEADDR) failed: %s", strerror(errno)); memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons((uint16_t)port); if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { logmsg("bad bind address: %s", host); close(fd); return -1; } 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) { logmsg("listen() failed: %s", strerror(errno)); close(fd); return -1; } return fd; } static void usage(const char *argv0) { fprintf(stderr, "usage: %s [-h HOST] [-p PORT] [-d] [-L]\n" "\n" " -h HOST address to bind (default %s -- loopback only!\n" " -L is required to bind anywhere else)\n" " -p PORT TCP port to listen on (default %d)\n" " -d daemonise: fork into the background\n" " -L ALLOW binding to a non-loopback address (dangerous:\n" " this daemon exists to be exploited as ROOT)\n" "\n" "This lab gives you a ROOT shell only when the daemon was STARTED\n" "as root (sudo make run-root). There is no setuid bit anywhere.\n" "Do not run it on any host that matters, never bind it beyond\n" "loopback, and do not leave it running as root.\n", argv0, FOOWOSD_HOST, FOOWOSD_PORT); } int main(int argc, char **argv) { const char *host = FOOWOSD_HOST; /* Bind address. */ int port = FOOWOSD_PORT; /* Bind port. */ int daemonise = 0; /* -d. */ int allow_nonloopback = 0; /* -L. The root-daemon safety guard. */ int lfd; /* Listening socket. */ int i; /* getopt() index. */ while ((i = getopt(argc, argv, ":h:p:dL")) != -1) { switch (i) { case 'h': host = optarg; break; case 'p': port = atoi(optarg); break; case 'd': daemonise = 1; break; case 'L': allow_nonloopback = 1; break; case ':': fprintf(stderr, "missing argument to -%c\n", optopt); usage(argv[0]); return 2; default: usage(argv[0]); return 2; } } /* * THE ROOT-DAEMON SAFETY GUARD. * * A daemon that is running as root (it was started as root -- there is * no +s bit here, so this state is easy to forget) and listens on a * non-loopback interface is a remote root service. Refuse by default, * document the exception, fail loudly. */ if (!allow_nonloopback && (strcmp(host, "127.0.0.1") != 0 && strcmp(host, "localhost") != 0 && strcmp(host, "::1") != 0)) { fprintf(stderr, "foowosd: refusing to bind %s: this daemon may be running as\n" " root. Loopback is the only permitted default. If you\n" " really know what you are doing, pass -L.\n", host); return 1; } signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN); /* Auto-reap forked children. */ /* Reserve a private log descriptor BEFORE sockets are dup2'd over fd 1. */ g_logfd = dup(STDOUT_FILENO); if (g_logfd < 0) { g_logfd = STDOUT_FILENO; fprintf(stderr, "foowosd: warning: could not reserve a log descriptor\n"); } /* * SELF-DIAGNOSIS OF THE "AM I ROOT ?" STATE -- printed once, to the log. * * ruid==euid==0 -> started as root: the exploit gives root * ruid==euid!=0 -> started as a normal user: baseline only * * foowosc reads euid over the socket and can warn too; this log line is * for you at the console. */ logmsg("startup: ruid=%d euid=%d %s", (int)getuid(), (int)geteuid(), (geteuid() == 0) ? "-> ROOT process" : "-> NOT root (start as root: make run-root)"); if (geteuid() == 0) logmsg("startup: WARNING: this daemon is running as root -- no setuid " "bit involved, just a root-started process. Port %d does not " "need root; see drop_privs().", port); 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 so we cannot acquire a controlling terminal. */ pid_t p1 = fork(); if (p1 < 0) { perror("fork"); return 1; } if (p1 > 0) _exit(0); if (setsid() < 0) perror("setsid"); pid_t p2 = fork(); if (p2 < 0) { perror("fork"); return 1; } if (p2 > 0) _exit(0); if (chdir("/") < 0) perror("chdir"); umask(022); } /* ---- The accept loop. Each child serves one connection. The children * stay root because the parent was started as root. ---- */ for (;;) { struct sockaddr_in peer; socklen_t plen = sizeof(peer); int cfd; pid_t pid; cfd = accept(lfd, (struct sockaddr *)&peer, &plen); if (cfd < 0) { if (errno == EINTR || errno == ECONNABORTED) continue; logmsg("accept() failed: %s", strerror(errno)); continue; } /* * Fork per connection. The child KEEPS the root privileges -- that * is Bug #3 in this lab, "no privilege drop before handling * untrusted input" (CWE-271). See drop_privs() above for the exact * calls a well-written daemon would make at this point, and why the * order of those three calls is security-critical. */ pid = fork(); if (pid < 0) { logmsg("fork() failed: %s", strerror(errno)); close(cfd); continue; } if (pid == 0) { close(lfd); handle_client(cfd); _exit(0); } close(cfd); } }