[Research] CVE-2024-54489 Analysis from Security Updates Part 2(EN)

image

  • CVE-2024-54489 Analysis Series
    • Part 1
    • Part 2 [Current Post]

Recap

Hi, I’m ji9umi! In the previous post, we analyzed CVE-2024-54489 and tested several approach methods. Here is summary of what we covered:

  1. CVE-2024-54489 was identified by Apple as a vulnerability residing in Disk Utility.
  2. I performed a diffing analysis on Disk Utility.app

    → but found no specific changes related to patch.

  3. By diffing the IPSW files to compare the full system, I confirmed that the changes exist in associated in frameworks and other elements rather than in Disk Utility itself.

  4. I am currently conducting static analysis to determine the root cause.

In Part 1, I focused the static analysis on identifying the execution flow within the application. I confirmed that both flows for processing mount requests reference methods implemented inside the Dyld Shared Cache.

  • [obj_1 mountWithCompletionBlock:] from StorageKit.framework
  • _DIHLDiskImageAttach() from DiskImages.framework

1. Analyze DSC

1.1. extract shared cache

The two methods requiring further analysis are implemented within frameworks, which can be found in three different locations within the file system:

  • /System/Library/Frameworks/
  • /System/Library/PrivateFrameworks/
  • /Library/Frameworks/

The items requiring verification are the StorageKit and DiskImages frameworks, which can be found in the following path: /System/Library/PrivateFrameworks/

╭─ji9umi@Junyoungs-MacBook-Pro in ~ 
╰$ file /System/Library/PrivateFrameworks/StorageKit.framework 
/System/Library/PrivateFrameworks/StorageKit.framework: directory
╭─ji9umi@Junyoungs-MacBook-Pro in ~ 
╰$ file /System/Library/PrivateFrameworks/DiskImages.framework 
/System/Library/PrivateFrameworks/DiskImages.framework: directory

First, upon inspecting the StorageKit.framework directory, you will notice that the actual binary file required for analysis is missing.

╭─ji9umi@Junyoungs-MacBook-Pro in /System/Library/PrivateFrameworks/StorageKit.framework 
╰$ tree .
.
├── Resources -> Versions/Current/Resources
├── StorageKit -> Versions/Current/StorageKit
└── Versions
    ├── A
    │   ├── _CodeSignature
    │   │   └── CodeResources
    │   └── Resources
    │       ├── ar.lproj
    │       ├── BootCampAssistant.icns
    │       ├── ca.lproj
    │       ├── cs.lproj
    │       ├── da.lproj
    │       ├── de.lproj
    │       ├── el.lproj
    │       ├── en_AU.lproj
    │       ├── en_GB.lproj
    │       ├── en.lproj
    │       ├── es_419.lproj
    │       ├── es_US.lproj
    │       ├── es.lproj
    │       ├── fi.lproj
    │       ├── fr_CA.lproj
    │       ├── fr.lproj
    │       ├── he.lproj
    │       ├── hi.lproj
    │       ├── hr.lproj
    │       ├── hu.lproj
    │       ├── id.lproj
    │       ├── Info.plist
    │       ├── InfoPlist.loctable
    │       ├── it.lproj
    │       ├── ja.lproj
    │       ├── ko.lproj
    │       ├── Localizable.loctable
    │       ├── ms.lproj
    │       ├── nl.lproj
    │       ├── no.lproj
    │       ├── pl.lproj
    │       ├── pt_BR.lproj
    │       ├── pt_PT.lproj
    │       ├── ro.lproj
    │       ├── ru.lproj
    │       ├── sk.lproj
    │       ├── SKError.loctable
    │       ├── sl.lproj
    │       ├── sv.lproj
    │       ├── th.lproj
    │       ├── tr.lproj
    │       ├── uk.lproj
    │       ├── UserPictureNotFound.jpg
    │       ├── version.plist
    │       ├── vi.lproj
    │       ├── zh_CN.lproj
    │       ├── zh_HK.lproj
    │       └── zh_TW.lproj
    └── Current -> A

48 directories, 9 files

This is because, starting with macOS Big Sur, all built-in libraries into a single integrated cache file to optimize system boot performance. This file is known as the dyld shared cache, and it can be found at the following path: /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/. Additionally, you can see that separate files exist to support both x86_64 and arm64e architectures.

╭─ji9umi@Junyoungs-MacBook-Pro in ~ 
╰$ ls -l /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/
total 10850248
-rwxr-xr-x  1 root  admin   844242203 Aug 17 03:44 aot_shared_cache.0
-rwxr-xr-x  1 root  admin   910422299 Aug 17 03:44 aot_shared_cache.1
-rwxr-xr-x  1 root  admin   897691547 Aug 17 03:44 aot_shared_cache.2
-rwxr-xr-x  1 root  admin   787358363 Aug 17 03:44 aot_shared_cache.3
-rwxr-xr-x  1 root  admin   896155931 Aug 17 03:44 aot_shared_cache.4
-rwxr-xr-x  1 root  admin   713302043 Aug 17 03:44 aot_shared_cache.5
-rwxr-xr-x  1 root  admin  2712764416 Aug 17 03:44 dyld_shared_cache_arm64e
-rwxr-xr-x  1 root  admin  2203500544 Aug 17 03:44 dyld_shared_cache_arm64e.01
-rwxr-xr-x  1 root  admin     2023837 Aug 17 03:44 dyld_shared_cache_arm64e.atlas
-rwxr-xr-x  1 root  admin     1140220 Aug 17 03:44 dyld_shared_cache_arm64e.map
-rwxr-xr-x  1 root  admin   877608960 Aug 17 03:44 dyld_shared_cache_x86_64
-rwxr-xr-x  1 root  admin   804831232 Aug 17 03:44 dyld_shared_cache_x86_64.01
-rwxr-xr-x  1 root  admin   789921792 Aug 17 03:44 dyld_shared_cache_x86_64.02
-rwxr-xr-x  1 root  admin   777699328 Aug 17 03:44 dyld_shared_cache_x86_64.03
-rwxr-xr-x  1 root  admin   782598144 Aug 17 03:44 dyld_shared_cache_x86_64.04
-rwxr-xr-x  1 root  admin   850001920 Aug 17 03:44 dyld_shared_cache_x86_64.05
-rwxr-xr-x  1 root  admin     1155495 Aug 17 03:44 dyld_shared_cache_x86_64.atlas
-rwxr-xr-x  1 root  admin      959702 Aug 17 03:44 dyld_shared_cache_x86_64.map

If the target version matches your currently installed OS, you can simply load the files from their local directory into a decompiler. However, if you want to analyze a different version, you must extract them from the firmware. For this process, you can use the ipsw tool, which we previously used for diffing.

In addition to the diff command, the ipsw tool supports an extract command for file extraction, which includes various flags depending on you target.

╭─ji9umi@Junyoungs-MacBook-Pro in ~ 
╰$ ipsw extract -h
Extract kernelcache, dyld_shared_cache or DeviceTree from IPSW/OTA

Usage:
  ipsw extract <IPSW/OTA | URL> [flags]

Aliases:
  extract, e, ex

Examples:
# Extract kernelcache from IPSW
$ ipsw extract --kernel iPhone15,2_16.5_20F66_Restore.ipsw

# Extract dyld_shared_cache for specific architecture
$ ipsw extract --dyld --dyld-arch arm64e iPhone.ipsw

# Extract all files matching a pattern (from the IPSW zip contents)
$ ipsw extract --pattern '.*\.ttf$' iOS.ipsw

# Extract multiple components with custom output directory
$ ipsw extract --kernel --sep --dyld -o /tmp/extracted iPhone.ipsw

# Extract from remote URL
$ ipsw extract --kernel --remote https://updates.cdn-apple.com/iPhone.ipsw

# Extract system version info as JSON
$ ipsw extract --sys-ver iPhone.ipsw

# Extract files from filesystem DMG matching pattern
$ ipsw extract --files --pattern '.*LaunchDaemons.*\.plist$' iPhone.ipsw

# Extract DriverKit dyld_shared_cache
$ ipsw extract --dyld --driverkit macOS.ipsw

Since the target is located within the file system, you can use a combination of the --files and --pattern flags. However, using the more convenient --dyld and --dyld-arch flags allows for a faster extraction by specificially defining the architectures.

Upon checking the extracted files, you will notice that two additional files have been extracted along with the target file.

╭─ji9umi@Junyoungs-MacBook-Pro in ~/Mine/001_Security/n-day/CVE-2024-54489/rsc/24B91__MacOS 
╰$ ls -l dyld_shared_cache_arm64e*
-rwxr-xr-x@ 1 ji9umi  staff  2506964992 Sep  6 20:19 dyld_shared_cache_arm64e
-rwxr-xr-x@ 1 ji9umi  staff  2121580544 Sep  6 20:19 dyld_shared_cache_arm64e.01
-rwxr-xr-x@ 1 ji9umi  staff     1106577 Sep  6 20:19 dyld_shared_cache_arm64e.map

The purpose of each file is as follows:

  1. dyld_shared_cache_arm64e
    1. This is main file. Depending on the architecture, it may be named x86_64 instead of arm64e
  2. dyld_shared_cache_arm64e.01
    1. Split version of the main file that functions as an extension.
    2. In the firmware I analyzed, the arm64e version had only one split file (.01), whereas the x86_64 version was numbered up to .05.
  3. dyld_shared_cache_arm64e.map
    1. A text-based map file containing human-readable strings.
    2. It records information such as memory mapping addresses for the main file’s components.

1.2. decompile shared cache

Once the extraction is complete, you can load the files into a decompiler to begin you analysis. The key difference from analyzing standard files is that you are required to select the specific regions or segments you wish to analyze.

image.png

The image above shows the Dyld Shared Cache Triage view in Binary Ninja, where you can inspect the internal components. In addition to the address and name, there is a ‘Loaded’ column. This indicates that you can selectively load only the specific items you wish to analyze into the decompiler.

image.png

You can also filter items using keywords via the search bar. When you right-click on a specific item, two options are available: you can either load only the selected item or load the selected item along with all its related dependencies.

If you choose to include related items, the dylibs and frameworks associated with the target will be loaded together.

Once loaded, you can proceed with the analysis using standard binary analysis techniques. However, keep in mind if you haven’t loaded all regions, the decompiler might fail to resolve some references automatically. Therefore, the ability to perform contextual analysis is required to bridge those gaps.

In the actual analysis view, you can still load specific items containing certain addresses. The need for analyst to make these judgment calls is what distinguishes this process from analyzing a standard binary.

2. Analyze Suspicious Method

2.1. StorageKit.framework

The [obj1 mountWithCompletionBlock:] called by the application can be located within the DSC as -[SKDisk mountWithCompletionBlock:]. The internal execution flow proceeds as follows:

  1. -[SKDisk mountWithCompletionBlock:]
  2. -[SKDisk mountWithOptions:withCompletionBlock:]
  3. -[SKDisk mountWithOptionsDictionary:withCompletionBlock:]
  4. -[SKHelperClient mountDisk:options:completionBlock:]
  5. -[SKHelperClient mountDisk:options:blocking:completionBlock:]

During static analysis, I was unable to find the implementation for the [obj_9 mountDisk:obj_6 options:obj_1 withCompletionUUID:obj_5] method, which is called internally within -[SKHelperClient mountDisk:options:blocking:completionBlock:].

To clarify this, it is necessary to analyze where obj_9 is defined. The value is retrieved via [[self remoteObjectWithUUID:obj_5 errorHandler:&stack_block_var_b8] retain];.

id obj_9 =
    [[self remoteObjectWithUUID:obj_5 errorHandler:&stack_block_var_b8] retain];
obj_6 = [[obj_2 minimalDictionaryRepresentation] retain];
[obj_2 release];
[obj_9 mountDisk:obj_6 options:obj_1 withCompletionUUID:obj_5];

To analyze the internal implementation, we need to examine the -[SKHelperClient remoteObjectWithUUID:errorHandler:] method. Its primary function is to handle the XPC connection process.

    id -[SKHelperClient remoteObjectWithUUID:errorHandler:](struct SKHelperClient* self, SEL sel, id remoteObjectWithUUID, id errorHandler, void* arg)

    {
        id obj = [remoteObjectWithUUID retain];
        id obj_1 = [errorHandler retain];
        id obj_4 = [[self xpcQueue] retain];
        struct Block_literal_7ff917e939a3 stack_block_var_70;
        stack_block_var_70.isa = &__NSConcreteStackBlock;
        stack_block_var_70.flags = 0xc2000000;
        stack_block_var_70.reserved = 0;
        stack_block_var_70.invoke =
            ___52-[SKHelperClient re...tWithUUID:errorHandler:]_block_invoke;
        stack_block_var_70.descriptor = &block_descriptor_7ff9426b8a90;
        stack_block_var_70.strong_ptr_20 = self;
        stack_block_var_70.strong_ptr_30 = obj_1;
        stack_block_var_70.strong_ptr_28 = obj;
        id obj_2 = [obj retain];
        id obj_3 = [obj_1 retain];
        j__dispatch_async(obj_4, &stack_block_var_70);
        [obj_4 release];
        obj_4 = [[self xpcConnection] retain];
// ...

XPC provides a lightweight mechanism for inter-process communication (IPC), allowing different applications and services to exchange data seamlessly through XPC services.

Since the previous method only covered the XPC connection itself, I had to analyze other methods to identify the target service being connected. I was able to confirm the connection endpoint within the -[SKHelperClient createXPCConnection]method.

void -[SKHelperClient createXPCConnection](struct SKHelperClient* self, SEL sel)

{
    id rax = [[NSXPCConnection alloc] initWithMachServiceName:@"
        com.apple.storagekitd" options:0x1000];

You can identify the running service through the provided service name, and by using the command below, you can locate the actual executable file being invoked.

╭─ji9umi@Junyoungs-MacBook-Pro in ~ 
╰$ ps -ef | grep storagekitd | grep -v $$
    0 35052     1   0 22Sep25 ??         0:09.17 /usr/libexec/storagekitd

While analyzing this vulnerability, I conducted research on other publicly disclosed bugs to gain a deeper understanding of the overall system architecture. During this process, I discovered a vulnerability analysis related to storagekitd, which confirmed that it is identified as StorageKit in the official patch notes.

The vulnerability that provided this information was CVE-2024-44243, and I also found a separate, in-depth analysis published by Microsoft regarding this issue.

https://support.apple.com/ko-kr/121839
https://www.microsoft.com/en-us/security/blog/2025/01/13/analyzing-cve-2024-44243-a-macos-system-integrity-protection-bypass-through-kernel-extensions/

Considering that the vulnerability currently under analysis was patched in Disk Utility, I determined that this particular path was out of scope and proceeded to verify other potential attack vectors.

2.2. DiskImages.framework

The _DIHLDiskImageAttach() function, called by the application, can be analyzed by loading the DiskImages.framework from the DSC.

To summarize its internal behavior, the function generates a dictionary by configuring various option values based on the parameters passed during the call. This dictionary is subsequently referenced during the execution of [obj_2 performOperationReturning:&theDict_1];.

The object obj_2 is instantiated as follows:

id obj_2 = [[DIHelperProxy alloc] initWithDictionary:obj andStatusProc:arg2 
    andContext:arg3];

if (_DIGetDebugLevel())
    _NSLog();

int32_t rbx_3 = [obj_2 performOperationReturning:&theDict_1];

In summary, the invoked method corresponds to -[DIHelperProxy performOperationReturning:]. Within this method, subsequent operations are handled as separate threads using NSThread.

*(uint64_t*)performOperationReturning_1 = nullptr;
[self->_threadCondLock lock];
[self->_threadCondLock unlockWithCondition:1];

if (_DIGetDebugLevel())
    _NSLog();

[NSThread detachNewThreadSelector:@selector(workerThread:) toTarget:self 
    withObject:@"workerThread:"];
[self->_threadCondLock lockWhenCondition:0];
[self->_threadCondLock unlock];
char rax_7;

The behavior of the detachNewThreadSelector: method confirmed that it executes the selector passed as an argument; consequently, I was able to locate the implementation of the -[DIHelperProxy workerThread:] method.

Internally, the workerThread: method calls threadSetupServer to initialize and run a server instance for the connection.

    if (![self threadSetupServer])
    {
        if (_DIGetVerboseLevel())
            _NSLog();
        
        self->_threadResultsError = 6;
    }
    else
    {
        int32_t rax_6 = [self threadLaunchToolAuthenticated:(uint64_t)(int32_t)self
            ->_withAuthentication];
        self->_threadResultsError = rax_6;
						// ...
}

If the connection is successful, the flow proceeds to threadLaunchToolAuthenticated:. In the event of a failure, it logs the error and sets an error number. The XPC target attempting the connection is as follows:

if (!j__sandbox_check((uint64_t)j____getpid(), "mach-lookup", 2, 
    [@"com.apple.system.hdiejectd.xpc" UTF8String]))
{

3. Other Executable File

During the application analysis, I discovered that diskutil and hdiutil also support mount options. Both are command-line interface (CLI) tools, and their respective roles and locations are as follows:

  • diskutil
    • /usr/sbin/diskutil
    • A utility for managing local disks and volumes
  • hdiutil
    • /usr/bin/hdiutil
    • A utility specifically designed for handling disk images.

3.1. diskutil

diskutil supports two primary mounting methods: mount and mountDisk. While the former targets a single volume, the latter targets all mountable volumes residing on the specified disk.

╭─ji9umi@Junyoungs-MacBook-Pro in ~ 
╰$ diskutil 
Disk Utility Tool
Utility to manage local disks and volumes
Most commands require an administrator or root user

WARNING: Most destructive operations are not prompted

Usage:  diskutil [quiet] <verb> <options>, where <verb> is as follows:

# ...

     u[n]mount            (Unmount a single volume)
     unmountDisk          (Unmount an entire disk (all volumes))
     eject                (Eject a disk)
     mount                (Mount a single volume)
     mountDisk            (Mount an entire disk (all mountable volumes))
# ...

The initial analysis focused on the logic from tool execution to option parsing. The execution flow is as follows:

  1. [Main init]
  2. [Main run]
  3. [CommandLineInterface run]
    • Verifies whether arguments are present.
  4. [CommandLineInterface parseVerbAndDispatch:]

If arguments are provided, the flow naturally reaches -[CommandLineInterface parseVerbAndDispatch:]. This method is responsible for validating the options and executing the corresponding internal processing.

if (![rax_1 caseInsensitiveCompare:@"mount"])
    /* tailcall */
    return [[[DiskMount alloc] init] mount:entireDisk:];

if (![rax_1 caseInsensitiveCompare:@"mountDisk"])
    /* tailcall */
    return [[[DiskMount alloc] init] mount:entireDisk:];

The code snippet above illustrates the processing of mount and mountDisk, confirming that both eventually call the -[DiskMount mount:entireDisk:] method.

Due to a decompiler artifact in Binary Ninja’s Pseudo Objective-C, the value for the entireDisk: field appeared to be missing. However, cross-referencing with the Assembly or LLIL (Low Level Intermediate Language) views reveals that the value is set to 0 when called via mount, and 1 when called via mountDisk.

  72 @   rdi = rax
  73 @   rdx = rbx
  74 @   ecx = 0
  75 @   goto 89 @ 0x1000053c7

// ...

  89 @   rsp = rsp + 8
  90 @   rbx = pop
  91 @   r12 = pop
  92 @   r13 = pop
  93 @   r14 = pop
  94 @   r15 = pop
  95 @   rbp = pop
  96 @   <return> tailcall([_objc_msgSend].q)

// ...
  
 119 @   rdx = &cfstr_mount
 120 @   rdi = r15
 121 @   rsi = r12
 122 @   call([_objc_msgSend].q)
 123 @   if (rax == 0) then 124 @ 0x1000053f0 else 128 @ 0x100004d94

 124 @   rdi = &cls_DiskMount
 125 @   call(_objc_alloc_init)
 126 @   rsi = [&selRef_mount:entireDisk:].q
 127 @   goto 72 @ 0x100005372

To summarize the execution flow: at line 119, the program loads the string mount for comparison. If they match at the branch on line 123, the flow proceeds to line 124; otherwise, it jumps to line 128.

Following the path from line 124, the program initializes the required instances and retrieves the selector information for the subsequent method call. The execution then branches to line 72.

At line 72, the process of setting up the necessary arguments begins. The calling convention for objc_msgSend() is defined as follows:

  • rdi: instance
  • rsi: selector
  • rdx: first argument
  • rcx: second argument

Since the method invoked here takes two arguments, line 74 reveals the logic where the entireDisk parameter is set to 0 in the ecx register.

As I continued to analyze the internal behavior of the -[DiskMount mount:entireDisk:] method, I encountered a familiar call that had appeared earlier in the analysis.

if (!entireDisk)
{
    id obj = nullptr;
    char rax_39 = [obj_57 mountWithOptionsDictionary:obj_3 error:&obj];
    id obj_62 = [obj retain];

At the point of execution, after the arguments have been successfully parsed for a single volume mount, obj_57 is identified as an SKDisk instance. The full method name is -[SKDisk mountWithOptionsDictionary:error:].

This is notably similar to the -[SKDisk mountWithOptionsDictionary:withCompletionBlock:] method mentioned during the previous analysis of StorageKit.framework, and their internal behaviors follow a very similar pattern.

Even when invoked via mountDisk instead of mount, although there are additional intermediate calls, the actual mounting process is ultimately handled by -[SKDisk mountWithOptionsDictionary:withCompletionBlock:].

id obj_63 = [[obj_8 wholeDiskForDisk:obj_57] retain];
obj_53 = obj_63;
id obj_1 = nullptr;
obj_52 = (uint64_t)[obj_63 mountWithOptionsDictionary:obj_3 error:&obj_1];
obj_4 = [obj_1 retain];
obj_56 = [[CommandLineInterface sharedInterface] retain];

Since the subsequent operations are implemented within the DSC, I proceeded with the analysis of hdiutil first to gain further context.

3.2. hdiutil

Considering that the mount, mountvol, and attach options in hdiutil are likely relevant, I focused my initial analysis primarily on these commands.

void _start(int32_t argc, int64_t** argv) __noreturn

    int32_t argc_1 = argc
    int64_t** argv_1 = argv
    uint64_t __size = _strlen(__s: _basename(*argv)) + 1
    char const (* rax_2)[0x8] = _malloc_type_malloc(__size, 0x766447de)
    data_100041008 = rax_2
    int64_t* rdi_3 = *argv
    
    if (rax_2 != 0)
        _strlcpy(__dst: rax_2, __source: _basename(rdi_3), __size)
    else
        data_100041008 = rdi_3
    
    _atexit(sub_100005280)
    int32_t rax_4 = _DIInitialize()

Unlike diskutil, the execution flow of hdiutil originates from the _start() function. Consistent with the previous analysis, the program first checks the number of arguments passed during execution to verify if additional options are present, followed by a branching logic based on that count.

if (argc s< 2)
    _warnx("missing verb")
    rdi_8 = *argv
else
    char const (* r12_1)[0x5] = data_100041020
    char* r15_1 = argv[1]

Upon accessing the address data_100041020, which is referenced during the branching logic, I identified a table structure as shown below. This table appears to serve as a dispatch mechanism, mapping command strings to their respective handler functions.

  char const (* data_100041020)[0x5] = data_1000310c9 {"help"}
  void* data_100041028 = sub_1000152e7
  void* data_100041030 = sub_10001530c

  char const (* data_100041040)[0x7] = data_10002cbb5 {"attach"}
  void* data_100041048 = sub_100010b70
  void* data_100041050 = sub_10000278f

  char const (* data_100041060)[0x7] = data_10002fbca {"detach"}
  void* data_100041068 = sub_100013841
  void* data_100041070 = sub_100006937

  char const (* data_100041080)[0x6] = data_1000310ce {"eject"}
  void* data_100041088 = sub_10001380c
  void* data_100041090 = sub_100006937

  char const (* data_1000410a0)[0x7] = cstr_verify {"verify"}
  void* data_1000410a8 = sub_10001b010
  void* data_1000410b0 = sub_100009c1c
// ...

The table consists of repeating structures containing an option name followed by two function pointers, which presumably serve as handlers for each respective option.

Taking the mount option as an example, it is mapped to two specific function addresses: sub_100010b70 and sub_10000278f.

char const (* data_1000412a0)[0x6] = data_100031149 {"mount"}
void* data_1000412a8 = sub_100010b70
void* data_1000412b0 = sub_10000278f

Upon examining the sub_100010b70() function, it was confirmed to contain the logic for displaying help information (usage instructions) specific to that option.

    int64_t sub_100010b70(int32_t arg1)

        data_100041559 = 1
        data_100041549:5.b = 1
        data_100041549:1.d = 0x1010101
        data_100041551 = 0x1010101
        data_100041555 = 0x101
        FILE* rdi = *___stderrp
        
        if (arg1 != 0)
            _fprintf(rdi, "Usage:\t%s [options] <image>\n", "hdiutil attach", 
                &data_100041548)
            return _fprintf(*___stderrp, "\t%s -help\n", "hdiutil attach") __tailcall
        
        _fprintf(rdi, "%s: attach disk image\n", "hdiutil attach", &data_100041548)
        _fprintf(*___stderrp, "Usage:\t%s <image>\n", "hdiutil attach")
        _fwrite(__ptr: "\tDevice options:\n", __size: 0x11, __nitems: 1, 
            __stream: *___stderrp)
// ...

The sub_10000278f() function is confirmed to be the primary execution handler, containing the core logic required to process the option when invoked.

Unlike the previous help-related function, this handler manages the actual operational flow, including argument validation and the subsequent calls to underlying frameworks for mounting or attaching disk images.

    uint64_t sub_10000278f()

        int64_t rax
        int64_t var_38 = rax
        int64_t rsi
        int32_t rdi
        rsi, rdi = sub_100010dc0(0x10b8)
        int64_t rax_1 = *___stack_chk_guard
        CFAllocatorRef r14 = *_kCFAllocatorDefault
        CFMutableDictionaryRef theDict_4 = _CFDictionaryCreateMutable(allocator: r14, 
            capacity: 0, keyCallBacks: _kCFTypeDictionaryKeyCallBacks, 
            valueCallBacks: _kCFTypeDictionaryValueCallBacks)
        CFDictionaryRef theDict = nullptr
        int32_t r12
        
        if (rdi s<= 2)
            _warnx("attach: missing <image>")
            r12 = 0x16
            goto label_1000028aa
        
        data_100041559 = 1
        data_100041549:1.d = 0x1010101
        data_100041549:5.b = 1
        data_100041551 = 0x1010101
        data_100041555 = 0x101
        r12 = sub_100004133(&data_100041560, &data_100041548)
        
        if (_getenv("com_apple_hdid_debug") == 0)
            if (_getenv("com_apple_hdid_verbose") != 0)
                data_100041564 = 1
                data_1000415b4 = 1
                _DISetVerboseLevel(1)
        else
            data_100041564 = 1
            data_1000415b4 = 1
            data_10004156c = 1
            data_1000415b0 = 1
            _DISetDebugLevel(1)
            _DISetVerboseLevel(1)
// ...
                            if (!_DIGetDebugLevel())
                            {
                            label_100003c86:
                                int64_t var_10e0_1 = 0x40000000;
                                int64_t (* var_10d8_1)(void* arg1, int64_t* arg2) =
                                    sub_100010d82;
                                void* const var_10d0_1 = &data_10003d120;
                                CFMutableDictionaryRef theDict_5 = theDict_4;
                                sub_10001521e();
                                uint64_t (* rsi_16)(int64_t arg1, CFDictionaryRef arg2);
                                
                                if (_DIGetDebugLevel())
                                    rsi_16 = sub_100014888;
                                else
                                    rsi_16 = sub_100004702;
                                
                                r12 = _DIHLDiskImageAttach(theDict_4, rsi_16, 0, &theDict);
// ...

The execution logic extracts the options passed with the command and organizes them into a dictionary format. This processed data is then ultimately passed as an argument to the _DIHLDiskImageAttach() function, which performs the actual attachment and mounting of the disk image.

4. To Do

To summarize the analysis so far, the core operations of these utilities leverage the StorageKit and DiskImages frameworks, both of which are implemented within the DSC.

I have confirmed that the methods defined in the DSC further interact with separate executable services through XPC and Proxy mechanisms. Before diving deeper into these sub-services, I plan to perform a comparative analysis between the vulnerable and patched versions to identify any significant changes in the execution flow or security logic.


References