[Research] CVE-2024-54489 Analysis from Security Updates(EN)
Prologue
Hello, I’m ji9umi, writing about the CVE-2024-54489 vulnerability discovered in macOS Disk Utility!
This vulnerability has been patched in macOS Ventura 13.7.2, Sonoma 14.7.2, and Sequoia 15.2. As of the time of writing (2025-08-25), no additional information beyond the Apple Security Update has been publicly disclosed.
This article will cover related topics based on the publicly available patch information, ranging from analyzing the vulnerability’s root cause to developing an exploit.
1. Background
1.1. About Disk Utility
Before delving into the vulnerabilities, we briefly reviewed what Disk Utility is. Disk Utility is a system utility provided by macOS that performs tasks such as disk partition management, checking, and mounting/unmounting, similar to Windows Disk Management.
One feature that distinguishes macOS from Windows is the inclusion of additional functionality for handling .dmg
files.
1.2. Patch Analysis
Reviewing the security updates released by Apple allows you to gather general information about the vulnerability through its impact and description. For this vulnerability, the details are as follows:
Impact: Running a mount command may unexpectedly execute arbitrary code
Description: A path handling issue was addressed with improved validation.
By combining the disclosed information, it appears there was an inadequacy in the logic handling paths during the user’s disk mounting process. Exploiting this could potentially lead to arbitrary code execution. Therefore, triggering the vulnerability is expected to be possible either when the user manually performs the mount or when macOS detects a new storage device.
When briefly reviewing Disk Utility information, we touched on .dmg
files. These are often encountered when receiving third-party applications distributed externally, outside the App Store.
% file Notion-4.18.0-arm64.dmg
Notion-4.18.0-arm64.dmg: lzfse encoded, lzvn compressed
After downloading the file and executing it, it automatically mounts to the volume and proceeds with the application installation in an appropriate manner based on the internal implementation. If a vulnerability occurs at this point, an attacker could distribute malicious installation files, potentially enabling remote code execution.
2. Root Cause Analysis
While the general cause of the vulnerability can be identified, the actual events triggering the mount operation can occur in multiple scenarios. As mentioned earlier, this includes not only when external storage devices are connected or when disk images are handled for application installation, but also situations where new disk images are created and automatically mounted upon completion, among other related actions that may invoke the operation.
Since analyzing all components would be time-consuming, we conducted a patch comparison analysis as a method to narrow the scope of analysis.
2.1. Patch Diffing
While various combinations exist for analysis, this article utilized the Binary Ninja + BinExport + BinDiff combination.
Since the vulnerability location identified through the patch is Disk Utility, analysis of that application was prioritized. The executable file can be obtained from the path /System/Applications/Utilities/Disk\\ Utility.app/Contents/MacOS/Disk\\ Utility
. For comparative analysis, the same file was extracted from both the pre-patch version 13.7.1 and the post-patch version 13.7.2.
% file /System/Applications/Utilities/Disk\ Utility.app/Contents/MacOS/Disk\ Utility
/System/Applications/Utilities/Disk Utility.app/Contents/MacOS/Disk Utility: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e]
/System/Applications/Utilities/Disk Utility.app/Contents/MacOS/Disk Utility (for architecture x86_64): Mach-O 64-bit executable x86_64
/System/Applications/Utilities/Disk Utility.app/Contents/MacOS/Disk Utility (for architecture arm64e): Mach-O 64-bit executable arm64e
After loading the target executable file into Binary Ninja, each file extracted via BinExport was compared using BinDiff.
The image above shows the BinDiff execution results, confirming that all functions have a similarity score of 1.0, classified as Matched Functions. This indicates that no changes within the executable file can be identified. Even in the case of CVE-2025-31200 also shows that while the patch history lists CoreAudio, the actual mitigation was implemented in AudioToolBox, a subset of CoreAudio.
Therefore, for this vulnerability, we conducted additional analysis considering the possibility that the vulnerability could originate from components related to Disk Utility. Revisiting the analysis process for CVE-2025-31200, we utilized the results of the ipsw diff
command to compare patch histories. However, the current test environments built on 13.7.1 and 13.7.2 are not distributed in IPSW format.
IPSW is a format long used for iPhone, iPad, etc., but MacBooks using Intel CPUs did not support it; support began only after the Apple Silicon chipset. The reason IPSW files for versions 13.7.1 and 13.7.2 could not be found is confirmed to be because these versions are not supported on Apple Silicon.
Another notable feature is that applications installed by default on macOS are configured as Mach-O universal binaries to support both x86_64 and arm64e architectures.
% file /System/Applications/Utilities/Disk\ Utility.app/Contents/MacOS/Disk\ Utility
/System/Applications/Utilities/Disk Utility.app/Contents/MacOS/Disk Utility: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e]
/System/Applications/Utilities/Disk Utility.app/Contents/MacOS/Disk Utility (for architecture x86_64): Mach-O 64-bit executable x86_64
/System/Applications/Utilities/Disk Utility.app/Contents/MacOS/Disk Utility (for architecture arm64e): Mach-O 64-bit executable arm64e
Considering the above information collectively, we can conclude that the firmware supported by ipsw diff
for Apple Silicon can be utilized, as the changes resulting from vulnerability patches will not impact the architecture.
2.2. ipsw Diffing
Comparative analysis using ipsw diff
is performed as follows:
- Download the firmware files to be compared.
- Install the tool for IPSW analysis. Refer to Github for installation instructions.
- Run
ipsw diff <source_firmware_1> <source_firmware_2> --output <path> --markdown
- If the
-output
option is not specified, the comparison results are output to the terminal by default.
- If the
Below are the results generated from performing the comparison analysis.
% ls -l 15_1_1VS15_2/15_1_1_24B91__vs_15_2_24C101/
total 640
drwxr-x--- 1382 root staff 44224 Aug 23 20:33 DYLIBS
drwxr-x--- 89 root staff 2848 Aug 23 20:33 KEXTS
drwxr-x--- 1064 root staff 34048 Aug 23 20:33 MACHOS
-rw-r--r-- 1 root staff 324941 Aug 23 20:33 README.md
Three subfolders—DYLIBS, KEXTS, and MACHOS—are created, along with a README.md file summarizing the overall results. The README file categorizes changes as NEW, UPDATED, or REMOVED based on the modifications. Referencing the linked files provides more detailed change information.
However, given the sheer volume of changes and the difficulty in arbitrarily classifying their relationships, we conducted an application analysis to understand how it actually behaves during the mounting process.
2.3. Attack Vector
In addition to mounting directly from an application, you can also mount a new disk device when it is connected by running the hdiutil attach
command in Terminal. Initially, we focused on analyzing the process of mounting through an application.
On macOS, a toolbar related to the currently selected application is supported by default in the upper-right corner of the screen. For Disk Utility, this toolbar helps you utilize functions such as creating or loading new disk image files. Some functions may be implemented in both the application and the toolbar.
Referring to the image above, you can see that the mount/unmount function is available both in the toolbar and within the application UI.
2.4. Static Analysis
Within the executable file, functionality related to the toolbar was implemented through the SUToolbarController
class.
100034ef6 id -[SUToolbarController toolbarItemWithName:label:image:action:](struct SUToolbarController* self, SEL sel, id toolbarItemWithName, id label, id image, SEL action)
// ...
1000356dd if (![obj_3 isEqualToString:strRef_Sidebar_Toolbar_Button])
1000356dd {
100035bc3 obj_93 = obj_104;
100035bc9 id obj_2;
100035bc9 id obj_95;
100035bc9
100035bc9 if (![obj_3 isEqualToString:strRef_Image_Toolbar_Button])
100035bc9 {
100036453
100036453 if ([obj_3 isEqualToString:strRef_Mount_Toolbar_Button])
10003645b {
10003647f id obj_107 = [[clsRef_NSBundle mainBundle] retain];
100036484 obj_2 = obj_107;
1000364af id obj_108 = [[obj_107 localizedStringForKey:@"Mount" value: // If disk can be mounted
1000364af &cfstr_ table:0] retain];
1000364bf id obj_109 = obj_108;
1000364d3 id obj_90 = [[clsRef_NSBundle mainBundle] retain];
1000364f4 id obj_91 = [[obj_90 localizedStringForKey:@"Unmount" value: // if disk can be unmounted
1000364f4 &cfstr_ table:0] retain];
1000364fc id obj_99 = obj_91;
10003651d int64_t obj_92 = [[clsRef_NSArray arrayWithObjects:
10003651d &obj_109 count:2] retain];
100036533 [var_78 _setAllPossibleLabelsToFit:obj_92];
100036540 obj_93 = obj_104;
100036544 [obj_92 release];
10003654a [obj_91 release];
100036550 [obj_90 release];
100036553 obj_95 = obj_108;
10003655a self_1 = self;
10003655a goto label_10003655e;
10003645b }
10003645b
10003656a self_1 = self;
100035bc9 }
// ...
Analyzing the -[SUToolbarController toolbarItemWithName:label:image:action:]
method reveals that code is implemented to change the displayed string based on the current state of the selected disk.
The caller of this method is the -[SUToolbarController toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:]
method, which subsequently passes the value of the action parameter to the called method as @selector(mountOrUnmountClicked:)
.
100034584 id -[SUToolbarController toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:](struct SUToolbarController* self, SEL sel, id toolbar, id itemForItemIdentifier, char willBeInsertedIntoToolbar)
// ...
100034d4f else
100034d4f {
100034d55 int64_t strRef_Mount_Toolbar_Button_1 = strRef_Mount_Toolbar_Button;
100034d76 id obj_10 = [[clsRef_NSBundle mainBundle] retain];
100034d9e int64_t obj_11 = [[obj_10 localizedStringForKey:@"Mount" value:
100034d9e &cfstr_ table:0] retain];
100034dc0 int64_t obj_12 =
100034dc0 [[clsRef_NSImage _imageWithSystemSymbolName:@"mount"] retain];
100034ded r14 = [[self toolbarItemWithName:strRef_Mount_Toolbar_Button_1 label:
100034ded obj_11 image:obj_12 action:@selector(mountOrUnmountClicked:)] retain]; // <-- Set action selector
100034dfa [obj_12 release];
100034dff [obj_11 release];
100034e04 [obj_10 release];
100034e20 id obj_13 = [[clsRef_NSBundle mainBundle] retain];
100034e4f int64_t obj_14 = [[obj_13 localizedStringForKey:@"Mount/Unmount" value:
100034e4f &cfstr_ table:0] retain];
100034e64 [r14 setPaletteLabel:obj_14];
100034737 [obj_14 release];
10003473c [obj_13 release];
100034d4f }
// ...
In Objective-C, a selector is an identifier used to identify a specific method, making it searchable within decompiled method names. Searching the method list revealed the methods -[SUISidebarController mountOrUnmountClicked:]
and -[SUSharedActionController mountOrUnmountClicked:]
. However, since the current target under analysis pertains to toolbar functionality rather than sidebar functionality, we proceeded based on -[SUSharedActionController mountOrUnmountClicked:]
.
10004d2f6 void -[SUSharedActionController mountOrUnmountClicked:](struct SUSharedActionController* self, SEL sel, id mountOrUnmountClicked)
10004d2f6 {
10004d2f6 id rax_1 = [[self representedDisk] retain];
10004d32c [self performMountOrUnmount:rax_1];
10004d33c /* tailcall */
10004d33c return [rax_1 release];
10004d2f6 }
This method receives the selected disk information and passes it as an argument to -[SUSharedActionController performMountOrUnmount]
. At this point, it is not yet determined whether the mount or unmount action will be performed; this decision is made in the method called subsequently.
10004d22c void -[SUSharedActionController performMountOrUnmount:](struct SUSharedActionController* self, SEL sel, id performMountOrUnmount)
10004d22c {
10004d22c id rax = [performMountOrUnmount retain];
10004d269 uint8_t** const rcx = &selRef_performUnmount:; // <-- Set selector as Unmount
10004d269
10004d270 if (![self _diskCanBeUnmounted:rax])
10004d270 rcx = &selRef_performMountOrUnlock:; // <-- Set selector as Mount or Unlock
10004d270
10004d27d _objc_msgSend(self, *(uint64_t*)rcx);
10004d28b /* tailcall */
10004d28b return [rax release];
10004d22c }
When examining the -[SUSharedActionController performMountOrUnmount:]
method, if the disk passed as an argument is not in a state where it can be unmounted—that is, if it is not yet mounted—the action to perform next is specified as performMountOrUnlock:
. Conversely, if the disk is already mounted, the action is specified as performUnmount:
.
10004cff4 void -[SUSharedActionController performMountOrUnlock:](struct SUSharedActionController* self, SEL sel, id performMountOrUnlock)
10004cff4 {
10004cff4 int64_t rax = *(uint64_t*)___stack_chk_guard;
10004d020 id obj = [performMountOrUnlock retain];
10004d040 id obj_1 = [[obj type] retain];
10004d05c char rax_2 = [obj_1 isEqualToString:*(uint64_t*)_kSKDiskTypeAPFSContainer];
10004d065 [obj_1 release];
10004d065
10004d06e if (!rax_2)
10004d06e {
10004d1d2 char* cmd_1;
10004d1d2
10004d1da if (![obj isLocked])
10004d1e5 cmd_1 = @selector(performMount:); // <-- Do Mount
10004d1da else
10004d1dc cmd_1 = @selector(performUnlock:); // <-- Do Unlock
10004d1dc
10004d1f6 _objc_msgSend(self, cmd_1);
// ...
Ultimately, it branches to performMount:
and performUnlock:
to execute each action. For the -[SUSharedActionController performMount:]
method, it creates an NSConcreteStackBlock as shown below and calls the mountWithCompletionBlock:
method, passing this block as the completion block argument.
10004d38e void -[SUSharedActionController performMount:](struct SUSharedActionController* self, SEL sel, id performMount)
10004d38e {
10004d38e id obj = [performMount retain];
10004d3b3 struct Block_literal_10004d3b3 stack_block_var_48;
10004d3b3 stack_block_var_48.isa = __NSConcreteStackBlock;
10004d3bb stack_block_var_48.flags = 0xc2000000;
10004d3bb stack_block_var_48.reserved = 0;
10004d3c6 stack_block_var_48.invoke = sub_10004d415_block_invoke;
10004d3d1 stack_block_var_48.descriptor = &block_descriptor_1000f5670;
10004d3d5 stack_block_var_48.strong_ptr_20 = obj;
10004d3e3 id obj_1 = [obj retain];
10004d3f2 [obj_1 mountWithCompletionBlock:&stack_block_var_48];
10004d403 [stack_block_var_48.strong_ptr_20 release];
10004d408 [obj_1 release];
10004d38e }
Unlike the code seen so far, the instance referenced during method calls is obj_1
. The first location where this argument is passed is the -[SUSharedActionController mountButtonClicked:]
method, which retrieves the representedDisk
member of the SUSharedActionController
structure via a getter.
struct SUSharedActionController
{
char _volumeGroupRepresented;
char _lockControls;
SKDisk* _representedDisk;
// ...
}
This is a pointer value to the SKDisk structure, and its actual implementation can be found in StorageKit.framework. Therefore, the [obj_1 mountWithCompletionBlock:]
method can perform additional analysis within StorageKit.
Alternatively, you can mount it via the toolbar’s “File → Open Disk Image” menu in the toolbar. In this case, the sequence proceeds as follows: -[SUSharedActionController openDmg:]
→ sub_10004ca6b_block_invoke()
→ sub_10004cb4e_block_invoke()
→ +[SUUtilities mountDiskImageAtPath:visible:readOnly:]
.
1000645df id +[SUUtilities mountDiskImageAtPath:visible:readOnly:](struct SUUtilities* self, SEL sel, id mountDiskImageAtPath, char visible, char only)
1000645df {
1000645df struct objc_class_t* clsRef_NSDictionary_1 = clsRef_NSDictionary;
10006461c id obj = [[clsRef_NSURL fileURLWithPath:mountDiskImageAtPath] retain];
100064641 id obj_1 = [[clsRef_NSNumber numberWithBool:1] retain];
10006465f id obj_2 = [[clsRef_NSNumber numberWithBool:1] retain];
10006467a id obj_3 = [[clsRef_NSNumber numberWithBool:(uint64_t)only] retain];
100064698 id obj_4 = [[clsRef_NSNumber numberWithBool:0] retain];
1000647de // ...
1000647de if (!_DIHLDiskImageAttach(obj_14, 0, 0, &var_60)) // <-- Attach here!
// ...
Upon examining the code, it retrieves the location of the file to be mounted and ultimately calls the _DIHLDiskImageAttach()
method. Since the implementation of this method also resides within DiskImages.framework, analysis requires examining the dyld_shared_cache.
Next up
The following article will focus on analyzing dyld_shared_cache and identifying the actual root cause. Given the nature of Apple’s publicly released patch notes, similar cases are likely to be numerous. Therefore, if the opportunity arises, I also plan to include details about the trial-and-error process involved.
Reference
- Prologue
- Background
- patch analysis
- Root Cause Analysis
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.