Malwarebytes | Airo AV |
…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.
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:
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):
1int 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!
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
2BOOL 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
49bail:
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 <+48>: 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
)
1typedef struct AuthorizationBlob {
2 uint32_t data[2];
3} AuthorizationBlob;
4
5typedef 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 <+0>: pushq %rbp 0x7fff7c909e27 <+1>: movq %rsp, %rbp 0x7fff7c909e2a <+4>: pushq %r15 0x7fff7c909e2c <+6>: 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):
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):
1define TRAMPOLINE "/usr/libexec/security_authtrampoline"
2
3OSStatus 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//
15int 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
2FILE *mbox = tmpfile();
3if (!mbox)
4 return errAuthorizationInternal;
5if (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:
1FILE *
2tmpfile(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:
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
2char pipeFdText[20];
3snprintf(pipeFdText, sizeof(pipeFdText), "auth %d", dataPipe[READ]);
4const char **argv = argVector(trampoline, pathToTool, pipeFdText, arguments);
5
6...
…and then writes the externalized AuthorizationRef
structure to that pipe:
1...
2switch (fork())
3
4// parent
5default: {
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.
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
You can support them via my Patreon page!