Sniffing Authentication References on macOS

details of a privilege-escalation vulnerability (CVE-2017-7170)

🩹🍎 This bug has been patched for several years (as CVE-2017-7170). …however, it remains one of my favorite discoveries on macOS! I’ve been meaning to blog about it now for several years …so well, finally(!) here goes.

Background

At DefCon 25, I presented a talk titled: “Death By 1000 Installers":

In this talk, I highlighted flaws in a myriad of (3rd-party) installers …flaws that allowed local attackers to escalate their privileges to root.

The general finding(s) of my research was that installers (that run with elevated privileges) often invoke insecure APIs or perform insecure actions:

"Death By 1000 Installers". 📝 Interested in more details? Slides for the talk are online here:

One of the insecure APIs that I discussed was the widely used AuthorizationExecuteWithPrivileges function. In a nutshell, this API takes a path to a binary (in the pathToTool argument) that will be executed with elevated privileges, once the user has authenticated:

Apple clearly notes that this API is deprecated and should not be used. Why? Because the API does not validate the binary that will be executed (as root!) …meaning a local unprivileged attacker or piece of malware could surreptitiously tamper or replace it in order to escalate their privileges to root (as well):

Many (myself) included, reasoned that if this API was invoked with a path to a (SIP) protected binary, this issue would be thwarted (as in such a case, unprivileged code could not subvert the binary):

1 int reboot () { 2 3 ... 4 5 AuthorizationExecuteWithPrivileges(authRef, " /sbin/reboot " , 6 kAuthorizationFlagDefaults, ( char * * )args, NULL); 7 }

After my talk, I dug into the API more and uncovered a systematic flaw …a flaw that made any invocation of the AuthorizationExecuteWithPrivileges vulnerable to a reliable local privilege escalation attack!

AuthorizationExecuteWithPrivileges

To understand the flaw in Apple’s implementation of this widely used authentication API, we need to understand how it works. Though this was covered in my DefCon talk, we’ll briefly cover it here as we well.

First, let’s take a look at some code that invokes AuthorizationExecuteWithPrivileges to execute a binary as root. As noted, such code was (is?) common in many (3rd-party) installers:

1 //run binary as root 2 BOOL runAsRoot ( char * path) 3 { 4 //return/status var 5 BOOL bRet = NO; 6 7 //authorization ref 8 AuthorizationRef authorizatioRef = { 0 }; 9 10 //args 11 char * args[] = {NULL}; 12 13 //flag creation of ref 14 BOOL authRefCreated = NO; 15 16 //status code 17 OSStatus osStatus = - 1 ; 18 19 //create authorization ref 20 osStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, 21 kAuthorizationFlagDefaults, & authorizatioRef); 22 if (errAuthorizationSuccess ! = osStatus) 23 { 24 //err msg 25 NSLog( @" AuthorizationCreate() failed with %d " , osStatus); 26 27 //bail 28 goto bail; 29 } 30 31 //set flag indicating auth ref was created 32 authRefCreated = YES; 33 34 //run cmd as root 35 // will ask user for password... 36 osStatus = AuthorizationExecuteWithPrivileges(authorizatioRef, path, 0 , args, NULL); 37 if (errAuthorizationSuccess ! = osStatus) 38 { 39 //err msg 40 NSLog( @" AuthorizationExecuteWithPrivileges() failed with %d " , osStatus); 41 42 //bail 43 goto bail; 44 } 45 46 //no errors 47 bRet = YES; 48 49 bail: 50 51 //free auth ref 52 if (YES = = authRefCreated) 53 { 54 //free 55 AuthorizationFree(authorizatioRef, kAuthorizationFlagDefaults); 56 } 57 58 return bRet; 59 }

After creating an AuthorizationRef authorization reference (via the AuthorizationCreate API), the example code invokes AuthorizationExecuteWithPrivileges , which will trigger an authentication dialog:

…assuming the user provides sufficient credentials, the binary (passed into the function via the path parameter) will be executed with elevated privileges!

Let’s dive a little deeper to understand what happens behind the scenes, as this will ultimately lead to the flaw in the API’s implementation.

Here, we have an overview of what goes on when program (i.e. an installer) invokes the AuthorizationExecuteWithPrivileges API:

As shown in the above image, when an installer (or anybody else) wants to perform privileged action via AuthorizationExecuteWithPrivileges :

It invokes the “authentication API” (i.e. AuthorizationExecuteWithPrivileges ) which generates an XPC message to an “Authorization Daemon” ( authd ). The daemon consults the authorization database and can decide “Ok - but need you to (re)authenticate first”, which results in another XPC message sent to the “ Security Agent ” This “ Security Agent ” displays the actual authentication dialog to the user. Assuming valid authentication credentials are provided, the privileged action is allowed.

Let’s now take a closer look at the steps relevant to understand the flaw.

When the AuthorizationExecuteWithPrivileges function is invoked, looking at its source code (see: libsecurity_authorization/lib/trampolineClient.cpp), we can see it first “externalizes” the authorization reference, via a call to the AuthorizationMakeExternalForm function:

In a debugger ( lldb ), stepping over the AuthorizationMakeExternalForm call, we can dump the “externalized” authentication reference (variable: extForm , type: AuthorizationExternalForm ):

$ lldb installer (lldb) target create "installer" Current executable set to 'installer' (x86_64). frame #0: 0x00007fff7c909dee Security`AuthorizationExecuteWithPrivileges + 48 Security`AuthorizationExecuteWithPrivileges: -> 0x7fff7c909dee : callq 0x7fff7c908e0a ; AuthorizationMakeExternalForm ... (lldb) reg read $rsi rsi = 0x7fff5fbffab0 (lldb) x/20xb $0x7fff5fbffab0 0x7fff5fbffab0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fff5fbffab8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fff5fbffac0: 0x00 0x00 0x00 0x00 (lldb) ni (lldb) x/20xb 0x7fff5fbffab0 0x7fff5fbffab0: 0xdb 0x49 0x27 0xe3 0x87 0x27 0x4a 0x61 0x7fff5fbffab8: 0xa6 0x86 0x01 0x00 0x00 0x00 0x00 0x00 0x7fff5fbffac0: 0x00 0x00 0x00 0x00

…as others have noted “the external form is basically just a random 12-byte handle associated with a token inside the authd service”.

More specifically, it’s an AuthorizationBlob (see: authd_private.h )

1 typedef struct AuthorizationBlob { 2 uint32_t data[ 2 ]; 3 } AuthorizationBlob; 4 5 typedef struct AuthorizationExternalBlob { 6 AuthorizationBlob blob; 7 int32_t session; 8 } AuthorizationExternalBlob;

The AuthorizationExecuteWithPrivileges function then invokes AuthorizationExecuteWithPrivilegesExternalForm passing in (amongst other parameters), the initialized AuthorizationExternalForm structure:

If we set a debugger breakpoint on the AuthorizationExecuteWithPrivilegesExternalForm function, we can confirm this flow of execution, by examining the callstack (via the bt debugger command):

$ lldb installer (lldb) target create "installer" Current executable set to 'installer' (x86_64). (lldb) b AuthorizationExecuteWithPrivilegesExternalForm Breakpoint 1: where = Security`AuthorizationExecuteWithPrivilegesExternalForm (lldb) r Process 485 launched: '/Users/user/Desktop/installer' (x86_64) Process 485 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 Security`AuthorizationExecuteWithPrivilegesExternalForm: -> 0x7fff7c909e26 : pushq %rbp 0x7fff7c909e27 : movq %rsp, %rbp 0x7fff7c909e2a : pushq %r15 0x7fff7c909e2c : pushq %r14 (lldb) bt * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 * frame #0: 0x00007fff7c909e26 Security`AuthorizationExecuteWithPrivilegesExternalForm frame #1: 0x00007fff7c909e0c Security`AuthorizationExecuteWithPrivileges + 78 frame #2: 0x0000000100000e48 installer`runAsRoot + 168 frame #3: 0x0000000100000d8b installer`main + 43

As shown in the graphic below, the AuthorizationExecuteWithPrivilegesExternalForm function then calls execv to execute a setuid system binary named security_authtrampoline :

$ ls -lart /usr/libexec/security_authtrampoline -rws--x--x 1 root wheel 36368 /usr/libexec/security_authtrampoline

The security_authtrampoline process invokes the AuthorizationCopyRights function which generates an XPC message to authd :

Once authd has ascertained that the request can continue, as noted, it sends a request to the “ Security Agent ” to display the authentication dialog to the user and capture and validate necessary credentials.

Assuming such credentials are provided, when this logic returns, the externalized AuthorizationRef (passed in via the AuthorizationExecuteWithPrivileges function) will now be fully initialized and “authorized”. As such, the security_auth_trampoline continues and invokes execv to execute the privileged action (i.e. the path of the binary that installer passed in to the AuthorizationExecuteWithPrivileges function):

Sniffing Authorization References

Though the AuthorizationExecuteWithPrivileges function, as an programming interface is simple, it abstracts away a ton of complexity (such as interactions between various processes, daemons, and agents). Ultimately this was its undoing.

Recall the very first action performed by the AuthorizationExecuteWithPrivileges function is to “externalize” the authorization reference via a call to the the AuthorizationMakeExternalForm function. Why? So it can pass the authorization reference to the security_authtrampoline process.

The implementation details of the “externalization” conversion are irrelevant, however, what is important to note is that this externalized form must not be disclosed, since, as Apple sternly notes: “any process can use this external authorization reference to access the authorization reference.”

This warning is reiterated in the Authorization.h file:

“SECURITY NOTE:

Applications should take care to not disclose the AuthorizationExternalForm to potential attackers since it would authorize rights to them.”

This warning piqued my interest, as if we (as an unprivileged user) can find a way to “sniff” or capture any “externalized” authorization references we would be able to (re)utilize them to perform arbitrary privileged actions!

Back to the AuthorizationExecuteWithPrivilegesExternalForm function, let’s take a peek at how it passes the “externalized” authorization reference to the security_authtrampoline process. From the Apple’s libsecurity_authorization/lib/trampolineClient.cpp):

1 define TRAMPOLINE " /usr/libexec/security_authtrampoline " 2 3 OSStatus AuthorizationExecuteWithPrivilegesExternalForm( 4 const AuthorizationExternalForm * extForm, 5 const char * pathToTool, 6 AuthorizationFlags flags, 7 char * const * arguments, 8 FILE * * communicationsPipe) 9 { 10 ... 11 12 // create the mailbox file 13 FILE * mbox = tmpfile(); 14 if ( ! mbox) 15 return errAuthorizationInternal; 16 if (fwrite(extForm, sizeof ( * extForm), 1 , mbox) ! = 1 ) { 17 fclose(mbox); 18 return errAuthorizationInternal; 19 } 20 fflush(mbox); 21 22 ... 23 24 // make text representation of the temp-file descriptor 25 char mboxFdText[ 20 ]; 26 snprintf(mboxFdText, sizeof (mboxFdText), " auth %d " , fileno(mbox)); 27 28 const char * * argv = argVector(trampoline, pathToTool, mboxFdText, arguments); 29 30 .... 31 const char * trampoline = TRAMPOLINE; 32 execv(trampoline, ( char * const * )argv);

Looking at the above code, observe that AuthorizationExecuteWithPrivilegesExternalForm takes the “externalized” authorization reference (passed in as extForm ), creates a temporary file, stores the “externalized” authorization reference in said file, then passes the file’s descriptor ( mboxFdText ) to security_authtrampoline via commandline arguments ( argv ):

As expected, security_authtrampoline (see source code here) reads in the “externalized” authorization reference and “internalizes” it back into a authorization reference (type: AuthorizationRef ) via a call to the AuthorizationCreateFromExternalForm function:

1 // 2 // Main program entry point. 3 // 4 // Arguments: 5 // argv[0] = my name 6 // argv[1] = path to user tool 7 // argv[2] = "auth n", n=file descriptor of mailbox temp file 8 // argv[3..n] = arguments to pass on 9 // 10 // File descriptors (set by fork/exec code in client): 11 // 0 -> communications pipe (perhaps /dev/null) 12 // 1 -> notify pipe write end 13 // 2 and above -> unchanged from original client 14 // 15 int main ( int argc, const char * argv[]) 16 { 17 ... 18 19 20 // read the external form 21 AuthorizationExternalForm extForm; 22 int fd; 23 if (sscanf(mboxFdText, " auth %d " , & fd) ! = 1 ) 24 return errAuthorizationInternal; 25 if (lseek(fd, 0 , SEEK_SET) | | 26 read(fd, & extForm, sizeof (extForm)) ! = sizeof (extForm)) { 27 close(fd); 28 return errAuthorizationInternal; 29 } 30 31 // internalize the authorization 32 AuthorizationRef auth; 33 if (OSStatus error = AuthorizationCreateFromExternalForm( & extForm, & auth)) 34 fail(error); 35 secdebug( " authtramp " , " authorization recovered " );

At first glance passing the sensitive “externalized” authorization reference via a temporarily file seems like a horrible idea, though a closer look seems it perhaps was done ‘securely’?! …ultimately though, with a little creativity, it proved to indeed to be massive security faux pas, affording a local non-privileged attacker access to any and all authorization references! 😱

Let’s take a closer look at the code within the AuthorizationExecuteWithPrivilegesExternalForm function responsible for creating and writing out the “externalized” authorization reference:

1 // create the mailbox file 2 FILE * mbox = tmpfile(); 3 if ( ! mbox) 4 return errAuthorizationInternal; 5 if (fwrite(extForm, sizeof ( * extForm), 1 , mbox) ! = 1 ) { 6 fclose(mbox); 7 return errAuthorizationInternal; 8 }

The tmpfile API creates a randomly named temporary file (via mkstemp , in $TMPDIR ) and immediately removes it (via unlink ), though it returns a file handle to the caller:

1 FILE * 2 tmpfile ( void ) 3 { 4 FILE * fp; 5 6 ... 7 8 fd = mkstemp(buf); 9 if (fd ! = - 1 ) 10 ( void )unlink(buf); 11 12 return (fp); 13 }

Since the file is both randomly named and immediately unlinked, its appears that the file cannot be opened any external processes. In other words it seems an external (malicious) process cannot access the contents of this temporary file (which from a security point of view is good, as recall it contains the AuthorizationExternalForm structure).

Of course the process that invoked AuthorizationExecuteWithPrivileges , has a FILE* handle to this this file (returned by tmpfile ), and as security_authtrampoline is spawned as a child this file handle can be shared.

….still, the sensitive AuthorizationExternalForm structure is being written out to a temporary file, which just “feels” like a really bad idea. And turns out it was!

While an unprivileged attacker can’t access (open) the temporary file as it has been unlinked and moreover can’t read the “raw bytes” (the “externalized” authorization reference) of the temporary file directly off the default filesystem (access would be denied), turns out they can read the raw bytes if the temporary file is written out to another filesystem that they have created …such as a ramdisk! 😈

And how does one (as a non-privileged attacker) accomplish this? Rather trivially! Just symlink the user’s temporary directory ( $TMPDIR ) to ramdisk that you’ve created (and thus can read directly from).

Recall our goal (as a local non-privileged attacker), is to ‘sniff’ the AuthorizationExternalForm structures that are written to the temporary file as they are passed to security_authtrampoline

On a vulnerable system (which at the time of this bug discovery was all versions of OSX 10.4 onwards) we can trivially and reliable accomplish this in the following steps:

Create (and format/mount) a ramdisk: hdiutil attach -nobrowse -nomount ram://2048 diskutil erasevolume HFS+ "RamDisk" /dev/disk2 Create a symbolic link from the user’s temporary directory ( $TMPDIR ) to the ramdisk.

This is allowed since while /tmp is owned by root, the user’s temporary directory is, well, owned by the user! $ ls -lart /var/folders/yx/bp25tm5x4l32k5297qwc7wcd4m022r/ drwx------ 140 patrick staff 4760 Aug 28 09:37 T rm -rf /var/folders/yx/bp25tm5x4l32k5297qwc7wcd4m022r/T ln -s /Volumes/RamDisk/ /var/folders/yx/bp25tm5x4l32k5297qwc7wcd4m022r/T $ ls -lart /var/folders/yx/bp25tm5x4l32k5297qwc7wcd4m022r/ lrwxr-xr-x 1 user staff 17 Aug 27 22:37 T -> /Volumes/RamDisk/

Wait till some program (e.g. an installer) invokes the AuthorizationExecuteWithPrivileges API.



…at the time of this bug discovery, pretty much every 3rd-party program that wanted perform any privileged action (install, update, etc) invoked this API. And while some interactive attacker may want r00t right away, it would be easy to leave behind something that persistently runs patiently waiting. Sniff and recover the AuthorizationExternalForm .



Once any program invokes AuthorizationExecuteWithPrivileges (even to perform a ‘secure’ action such as executing /sbin/reboot ), the AuthorizationExternalForm structure will be written out to the (our) ramdisk. As a unprivileged user, we read the raw bytes off this ramdisk to recover it: $ hexdump -s 0x73000 -n 32 -v -e'1/1 "%02x"' /dev/rdisk2 abdf4fe44eb4476ead8601000000000000000000000000000000000000000000

Wait until the caller (user) authenticates.



Though we now have access to the AuthorizationExternalForm structure, if we try use it right away we’ll get an authorization error as it hasn’t actually been authorized (yet) by the user. (Recall they have to enter in their credentials in the authorization dialog).



So, we can just sit in a loop invoking AuthorizationCopyRights (without the kAuthorizationFlagInteractionAllowed as we don’t want to pop the authorization dialog ourselves) until the user authenticates via the prompt that is displayed (as a result of the invocation of AuthorizationExecuteWithPrivileges ). Got Root? Yas



Once the user authenticates via the authorization dialog that was triggered by the legitimate process which invoked AuthorizationExecuteWithPrivileges , the AuthorizationExternalForm (which we have recovered!) is authorized too, and thus can be used (by anybody!) to perform privileged actions as root. In other words, we can now spawn any command or binary that will be executed with root privileges! #GameOver

It should be pointed out that if the legitimate process that invoked AuthorizationExecuteWithPrivileges, calls AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); (as it should), AuthorizationExternalForm is invalidated. However, we can simply send a kill -STOP to this process once we detect security_authtrampoline has been spawned (which is done on-demand). Since security_authtrampoline (by means of the SecurityAgent) is displaying a modal window (the authorization dialog), suspending the process won’t cause any issues. This ensures we have time to use the AuthorizationExternalForm before its invalidated! Of course we nicely call kill -CONT so nothing appears amiss.

Below is a simple PoC in action:

The Fix

I reported this bug to Apple in 2017, who acted quickly to mitigate the bug, as well as (eventually) pushing out a more comprehensive fix.

The (short-term) mitigation was to prevent (non-privileged) users from being able to symbolically link the user’s temporary directory to another file / location (e.g. a ramdisk). This was accomplished via System Integrity Protection (note the sunlnk flag):

$ ls -lO@d $TMPDIR drwx------@ 161 patrick staff sunlnk /var/folders/pw/sv96s36d0qgc_6jh4...000gn/T/

This short-term mitigation was silently applied:

user’s $TMPDIR now protected on H. Sierra!? 🤔 -it’s a (short-term) mitigation against an 0day priv-esc affecting all recent vers OSX 😅🤐☠️🍎🤒🤕 pic.twitter.com/i4k3OvL7Ae — patrick wardle (@patrickwardle) November 6, 2017

In early 2018, in macOS 10.13.1 , Apple fixed the underlying issue (eventually assigning it: CVE-2017-7170 ):

The (full) fix was pretty straightforward (see: AuthorizationTrampoline.cpp ). Now, instead of writing the externalized AuthorizationRef to a temporary file and passing the file handle to security_authtrampoline , AuthorizationExecuteWithPrivileges simply passes a handle to a pipe:

1 // make text representation of the pipe handle 2 char pipeFdText[ 20 ]; 3 snprintf(pipeFdText, sizeof (pipeFdText), " auth %d " , dataPipe[READ]); 4 const char * * argv = argVector(trampoline, pathToTool, pipeFdText, arguments); 5 6 ...

…and then writes the externalized AuthorizationRef structure to that pipe:

1 ... 2 switch (fork()) 3 4 // parent 5 default : { 6 7 write(dataPipe[WRITE], extForm, sizeof ( * extForm)) ! = sizeof ( * extForm)); 8 9 ...

This appears to be a secure way to pass the externalized AuthorizationRef to the (child) security_authtrampoline process.

Conclusion

Oftentimes spelunking around OSX/macOS internals yields interesting bugs! Today, we discussed one of my all time favorite discoveries; a reliable local privilege-escalation vulnerability that affected OSX/macOS for approximately 13 years! (AFAIK it was introduced in OSX Tiger).

It’s been a bit since I reported it to Apple who promptly patched it (though initially did so silently & without credit 😠) However, I’ve always wanted to write more about this neat bug, so definitely stoked to share this post today!

The Ugly: for last ~13 years (OSX 10.4+) anybody could locally sniff 'auth tokens' then replay to stealthy & reliably elevate to r00t 🍎🤒☠️ The Bad: reported to Apple -they *silently* patched it (10.13.1) 🤬 The Good: when confronted they finally assigned CVE + updated docs 😋 pic.twitter.com/RlNBT1DBvK — patrick wardle (@patrickwardle) January 16, 2018