[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:

  1. Download the firmware files to be compared.
  2. Install the tool for IPSW analysis. Refer to Github for installation instructions.
  3. Run ipsw diff <source_firmware_1> <source_firmware_2> --output <path> --markdown
    1. If the -output option is not specified, the comparison results are output to the terminal by default.

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



본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.