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

image1

  • CVE-2024-54489 Analysis Series

Recap

안녕하세요, ji9umi입니다! Part 1에서는 CVE-2024-54489가 어떤 취약점인지 살펴보고 다양한 접근 방법을 시도하였는데요, 진행했던 내용은 다음과 같습니다:

  1. CVE-2024-54489는 Apple에서 발표한 보안 업데이트 기준 Disk Utility에서 발생한 취약점으로 분류됨
  2. 이에 macOS의 시스템 유틸리티인 Disk Utility.app의 diffing 진행
    → 취약점 패치와 관련된 변경 사항은 확인되지 않음
  3. 전체 파일 시스템을 비교하기 위해 IPSW 파일의 diffing 진행
    → Disk Utility에 관한 직접적인 변경 내역은 존재하지 않지만 연관된 프레임워크 및 다른 요소에서 변경 사항이 존재함을 확인
  4. 정적 분석을 통한 root cause 분석 진행 중

Part 1에서는 정적 분석 중 애플리케이션 내에서 호출되는 흐름을 중점적으로 분석하였으며 mount 요청을 처리하는 두 가지 흐름 모두 Dyld Shared Cache 내에 구현된 메서드를 참조하는 부분까지 확인하였습니다.

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

1. Analyze DSC

1.1. extract shared cache

추가 분석이 필요한 두 메서드는 프레임워크 내에 구현되어 있는 것으로 파일시스템 내에서 프레임워크와 관련된 위치는 총 세 곳이 존재합니다:

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

확인이 필요한 항목은 StorageKit과 DiskImages 프레임워크로 이들은 /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

우선 StorageKit.framework 폴더의 내부를 확인하면 실제 분석을 위한 바이너리 파일이 식별되지 않는 점을 확인할 수 있습니다.

╭─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

이는 macOS Big Sur부터 시스템 부팅 시 퍼포먼스 최적화를 위해 모든 내장 라이브러리를 하나로 통합한 캐시 파일로 만들어 배포했기 때문입니다. 이 파일의 명칭은 dyld shared cache로 해당 파일은 /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/ 경로에서 확인할 수 있습니다. 또한 x86_64와 arm64e를 지원하기 위한 별개의 파일이 존재하는 것을 확인할 수 있습니다.

╭─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

만약 분석하고자 하는 파일이 현재 설치된 OS인 경우 해당 위치의 파일을 디컴파일러 도구 등에 불러와서 진행할 수 있으나 다른 버전을 분석하고자 하는 경우 펌웨어 파일로부터 추출이 필요합니다. 이를 위해 diffing을 위해 사용했던 ipsw 도구를 활용할 수 있습니다.

ipsw 명령은 diff 옵션 외에도 파일 추출을 위한 extract를 지원하는데 여기에는 대상에 따른 추가적인 옵션이 존재합니다.

╭─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

추출하고자 하는 대상은 파일 시스템 내에 존재하기 때문에 --files--pattern을 조합하여도 가능하지만 더 편리한 --dyld--dyld-arch를 활용하면 아키텍처를 정의해주는 것으로 빠르게 추출이 가능합니다.

추출된 파일을 확인하면 대상으로 한 파일 외에도 2개의 파일이 추가로 추출된 점을 확인할 수 있습니다.

╭─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

각 파일의 용도는 다음과 같습니다:

  1. dyld_shared_cache_arm64e
    1. 메인 파일로 아키텍처에 따라 arm64e 대신 x86_64의 이름을 가진 파일도 존재할 수 있음
  2. dyld_shared_cache_arm64e.01
    1. 메인 파일의 분할된 버전으로 기능적으로는 메인 파일의 확장 역할로 사용됨
    2. 분석을 진행한 펌웨어 기준 arm64e의 분할 파일은 하나만 존재하여 .01이지만 x86_64는 .05까지 넘버링이 존재하였음
  3. dyld_shared_cache_arm64e.map
    1. 읽을 수 있는 문자열로 작성된 text map으로 메인 파일의 구성 요소에 대한 매핑 주소 등이 기록되어 있음

1.2. decompile shared cache

추출이 완료된 파일은 디컴파일러에 올려 분석을 진행할 수 있습니다. 일반 파일과 다른 점이라면 어떤 영역을 분석할 것인지 선택하는 과정이 요구됩니다.

image.png

위 화면은 바이너리 닌자에서 지원하는 Dyld Shared Cache Triage view로 내부에 존재하는 항목을 확인할 수 있습니다. 추가로 주소와 이름 외에 Loaded 항목이 존재하는데 이는 선택하는 항목에 한정하여 디컴파일러가 분석을 진행할 수 있도록 적재할 수 있음을 의미합니다.

image.png

검색을 통해 키워드로 필터링 하는 것 또한 가능하며 원하는 항목을 우클릭할 경우 선택한 항목에 대해서만 불러오는 것과 이와 연관된 파일까지 불러오는 두 가지 옵션이 존재합니다.

연관된 항목을 포함하도록 선택할 경우 대상과 관련된 dylib 및 프레임워크가 같이 적재됩니다.

적재 이후 분석은 일반적인 바이너리 분석 방법을 통해 진행할 수 있습니다. 다만 모든 영역을 적재하지 않은 경우 일부 참조를 자동으로 찾지 못할 수 있기 때문에 연관된 맥락을 분석할 수 있는 능력이 요구됩니다.

실제 분석 화면에서는 해당 주소를 포함하는 항목에 대한 적재 또한 가능하지만 이에 대한 판단이 필요한 것이 일반 바이너리와 다른 점이라고 볼 수 있습니다.

2. Analyze Suspicious Method

2.1. StorageKit.framework

애플리케이션에서 호출한 [obj1 mountWithCompletionBlock:]은 DSC 내에서 -[SKDisk mountWithCompletionBlock:]으로 찾을 수 있으며 내부 호출 순서는 다음과 같이 진행됩니다:

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

정적 분석을 진행하는 경우 -[SKHelperClient mountDisk:options:blocking:completionBlock:] 메서드에서 내부적으로 호출되는 [obj_9 mountDisk:obj_6 options:obj_1 withCompletionUUID:obj_5] 메서드에 관한 구현을 확인할 수 없었습니다.

이를 확인하기 위해서는 obj_9이 정의되는 곳의 분석이 필요한데 해당 값은 [[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];

내부 구현을 분석하려면 -[SKHelperClient remoteObjectWithUUID:errorHandler:] 메서드의 확인이 필요하며 주요 동작은 XPC 연결 과정을 담당합니다.

    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는 경량화된 내부 프로세스 간 통신을 제공하는 역할로 XPC 서비스를 통해 서로 다른 애플리케이션이 데이터를 주고받을 수 있도록 합니다.

위 메서드에서는 XPC 연결에 관한 내용만 존재하기 때문에 어떤 대상과 연결되는지 식별하기 위해서는 다른 메서드의 분석이 필요하였습니다. 여기서는 -[SKHelperClient createXPCConnection] 메서드에서 확인 가능하였습니다.

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

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

전달되는 service name을 통해 실행되는 서비스를 식별할 수 있으며 아래 명령어를 통해 실제로 호출되는 실행 파일을 확인할 수 있습니다.

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

본 취약점에 대한 분석을 진행하며 전체적인 시스템 구성을 학습하기 위해 공개된 다른 취약점의 정보 수집을 진행하였습니다. 이 과정에서 storagekitd와 관련된 취약점 분석 내용이 존재하였고 이는 패치 노트 상에 StorageKit으로 표기된 점을 확인할 수 있었습니다.

해당 정보를 알게된 취약점은 CVE-2024-44243으로 마이크로소프트에서 분석한 별도의 글 또한 존재하였습니다.

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/

현재 분석 중인 취약점이 Disk Utility에서 패치된 점을 고려할 때 이는 범위 외로 판단되어 다른 벡터에 대한 검증을 진행하였습니다.

2.2. DiskImages.framework

애플리케이션에 의해 호출된 _DIHLDiskImageAttach() 함수는 DSC 내에서 DiskImages.framework를 적재하여 확인할 수 있습니다.

내부 동작을 요약하면 호출 당시 넘겨받은 파라미터를 바탕으로 각종 옵션값을 지정하여 딕셔너리를 생성합니다. 이후 이 딕셔너리는 [obj_2 performOperationReturning:&theDict_1]; 호출 시 참조됩니다.

여기서 obj_2는 아래와 같이 생성됩니다.

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

if (_DIGetDebugLevel())
    _NSLog();

int32_t rbx_3 = [obj_2 performOperationReturning:&theDict_1];

즉, 호출된 메서드의 정보는 -[DIHelperProxy performOperationReturning:]에 해당됩니다. 해당 메서드에서는 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;

detachNewThreadSelector 메서드 동작 방식을 확인하면 인자로 전달하는 selector를 실행한다는 점을 알 수 있었고 -[DIHelperProxy workerThread:] 메서드 또한 구현되어 있는 점을 확인할 수 있었습니다.

workerThread: 메서드는 내부적으로 threadSetupServer를 호출하여 연결을 위한 서버 인스턴스를 실행합니다.

    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;
						// ...
}

정상적으로 연결에 성공한 경우 threadLaunchToolAuthenticated: 흐름으로 진행되며 실행에 실패한 경우 로그를 기록하고 에러 넘버를 세팅합니다. 연결을 시도하는 XPC 대상은 다음과 같습니다.

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

3. Other Executable File

Application 분석을 진행하던 중 diskutilhdiutil에서도 mount 옵션을 지원하는 것을 확인하였습니다. 두 도구 모두 Command Line에서 사용 가능한 도구로 각 역할과 위치는 다음과 같습니다:

  • diskutil
    • /usr/sbin/diskutil
    • 로컬 디스크와 볼륨을 관리하기 위한 유틸리티
  • hdiutil
    • /usr/bin/hdiutil
    • 디스크 이미지를 다루기 위한 유틸리티

3.1. diskutil

diskutil에서 지원하는 마운트는 mountmountDisk 총 두 가지 방식으로 전자의 경우 단일 볼륨을 대상으로 한다면 후자는 해당 디스크의 마운트 가능한 모든 볼륨을 대상으로 합니다.

╭─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))
# ...

우선 툴 실행 이후 옵션 파싱을 진행하는 로직까지 분석을 목표로 진행하였으며 흐름은 다음과 같습니다:

  1. -[Main init]
  2. -[Main run]
  3. -[CommandLineInterface run]
    • 인자가 존재하는지 확인
  4. 존재하는 경우 -[CommandLineInterface parseVerbAndDispatch:]

인자가 존재하는 경우 기본적으로 -[CommandLineInterface parseVerbAndDispatch:]까지 도달하게 되며 해당 메서드에서 내부적으로 옵션의 유효성 검증과 이에 따른 처리가 진행됩니다.

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

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

위 코드 스니펫은 mountmountDisk의 처리 과정이며 동일하게 -[DiskMount mount:entireDisk:] 메서드를 호출하는 점을 확인할 수 있습니다.

분석을 진행한 Binary Ninja의 Pesudo Objective-C의 오류로 entireDisk: 필드에 대한 값이 누락되었으나 assembly 혹은 LLIL에서 확인하면 mount를 통한 호출 시 이를 ‘0’으로 세팅하며 mountDisk로 호출 시 ‘1’로 세팅합니다.

  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

진행 흐름을 요약하면 119번 줄에서 mount 문자열을 불러와 비교를 진행합니다. 123번 줄의 분기문에서 일치하는 경우 124번으로 이동하며 그렇지 않은 경우 128번으로 이동합니다.

124번의 진행 흐름을 따라가면 필요한 인스턴스를 초기화 후 메서드 호출을 위한 셀렉터 정보를 가져옵니다. 이후 72번 줄로 분기합니다.

72번 줄에서는 필요한 인자를 세팅하는 과정을 가지는데 objc_msgSend()의 calling convention은 다음과 같습니다:

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

여기서 호출되는 메서드는 두 개의 인자를 받기 때문에 entireDisk에 해당하는 레지스터 ecx에 0으로 세팅되는 로직을 74번 줄에서 확인할 수 있습니다.

-[DiskMount mount:entireDisk:] 메서드의 내부 동작을 분석하다보면 익숙한 이름의 호출을 확인할 수 있습니다.

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

해당 코드가 실행되는 시점을 확인하면 전달된 인자를 정상적으로 파싱한 이후 싱글 볼륨을 마운트 하는 상황으로 obj_57SKDisk 인스턴스로 전체 함수명은 -[SKDisk mountWithOptionsDictionary:error:]가 됩니다.

이전 StorageKit.framework 분석 단계에서 비슷한 이름의 -[SKDisk mountWithOptionsDictionary:withCompletionBlock:] 메서드에 관한 언급이 존재하였는데 내부 동작 또한 유사한 형태를 지니고 있었습니다.

만약 mount가 아닌 mountDisk를 통해 호출된 경우에도 추가적인 호출이 존재하지만 실제 마운트 과정은 -[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];

이후 동작은 DSC 내에 구현되어 있기 때문에 우선 hdiutil의 분석을 진행하였습니다.

3.2. hdiutil

hdiutil에서는 mount, mountvolattach 옵션이 관련이 있을 것으로 판단되어 이를 중점적으로 초기 분석을 진행하였습니다.

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()

diskutil과 다르게 _start() 함수에서 시작하는 구조로 진행되었습니다. 마찬가지로 추가적인 옵션이 존재하는지 확인하기 위해 실행 시 전달된 인자의 개수를 확인하였고 이에 따라 분기하는 로직이 존재하였습니다.

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

이때 참조되는 data_100041020 주소를 접근하면 아래와 같은 테이블 구조를 확인할 수 있었습니다:

  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
// ...

사용 가능한 옵션 이름과 두 개의 함수 포인터가 반복되는 구조였으며 각 옵션을 처리하는 핸들러 역할을 수행함을 예측할 수 있습니다.

mount 옵션을 예시로 확인하면 이는 sub_100010b70sub_10000278f 함수 주소를 가지고 있습니다.

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

sub_100010b70() 함수를 확인하면 해당 옵션을 사용하기 위한 도움말 내용을 출력하는 로직이 구현되어 있습니다.

    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)
// ...

sub_10000278f() 함수의 경우 옵션 호출 시 이를 처리하기 위한 로직이 구현되어 있습니다.

    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);
// ...

해당 명령어와 함께 전달된 옵션을 추출하여 딕셔너리 형태로 저장한 뒤 최종적으로 _DIHLDiskImageAttach()를 호출하는 방식으로 처리됩니다.

4. To Do

현재까지 분석한 내용을 종합하면 주요 동작의 경우 StorageKit과 DiskImages 프레임워크 기능을 활용하며 이들은 DSC 내에 구현된 것을 확인하였습니다.

DSC에 정의된 메서드는 다시 XPC, Proxy 등을 이용하여 별도의 실행 파일과 연계되는 과정이 존재하였는데, 추가 분석을 진행하기에 앞서 취약한 버전과 패치된 버전의 전체적인 흐름을 비교하여 변경된 점이 존재하는지 확인할 예정입니다.


References



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