IOKit Drivers -- Kernel Attack Surface
IOKit drivers are the largest attack surface for iOS kernel exploitation. Each driver exposes external methods via IOUserClient that userspace can call – similar to ioctl() on Linux.
IOKit Architecture
Driver Hierarchy
IOService (abstract base)
└── IORegistryEntry (device tree node)
└── Concrete driver class (e.g., IOSurface)
└── IOUserClient subclass (e.g., IOSurfaceRootUserClient)
↑
Userspace calls IOConnectCallMethod() here
IOUserClient – The Interface
IOUserClient is the bridge between userspace and the kernel driver:
// Kernel side
class IOSurfaceRootUserClient : public IOUserClient {
// External method dispatch table
static const IOExternalMethodDispatch sMethods[] = {
// selector 0
{ &IOSurfaceRootUserClient::s_create_surface,
0, // scalar input count
kIOUCVariableStructureSize, // struct input size
0, // scalar output count
0 }, // struct output size
// selector 1
{ &IOSurfaceRootUserClient::s_release_surface,
1, 0, 0, 0 },
// ... more methods
};
IOReturn externalMethod(uint32_t selector,
IOExternalMethodArguments *args,
IOExternalMethodDispatch *dispatch,
OSObject *target,
void *reference) override;
};
// Userspace side
io_connect_t conn;
IOServiceOpen(service, mach_task_self(), 0, &conn);
IOConnectCallMethod(conn,
selector, // Method index
scalarInput, scalarInputCount,
structInput, structInputSize,
scalarOutput, &scalarOutputCount,
structOutput, &structOutputSize);
External Method Arguments
struct IOExternalMethodArguments {
uint32_t version;
uint32_t selector; // Method selector
// Scalar (register-like) arguments
const uint64_t *scalarInput;
uint32_t scalarInputCount;
uint64_t *scalarOutput;
uint32_t scalarOutputCount;
// Struct (buffer) arguments
const void *structureInput;
uint32_t structureInputSize;
void *structureOutput;
uint32_t structureOutputSize;
// Memory descriptor
IOMemoryDescriptor *structureInputDescriptor;
IOMemoryDescriptor *structureOutputDescriptor;
};
High-Value Targets
IOSurface / IOSurfaceRootUserClient
- Role: Manages shared memory surfaces for graphics
- Why important: Reachable from app sandbox, complex functionality
- Exploitation history: Many CVEs, used in most modern exploit chains
- Key methods: create_surface, set_value, get_value, release_surface
- iOS 16+: IOSurfaceRootUserClient is restricted (duplicate table with fewer methods)
IOSurface kread/kwrite pattern:
1. Create IOSurface
2. Set serialized property values (controlled data in kernel)
3. Trigger vulnerability → overwrite IOSurface internal pointer
4. Get property value → reads from corrupted pointer address (kread)
5. Set property value → writes to corrupted pointer address (kwrite)
AGXAccelerator / IOGPU
- Role: GPU driver
- Attack surface: Command buffer parsing, shader compilation
- iOS 16+: IOGPUDeviceUserClient restricted
AppleAVE2 / AppleAVE2UserClient
- Role: Video encoding hardware
- Characteristics: iOS-only, symbols stripped, harder to analyze
- Exploitation: Accessible before iOS 16 restrictions
AppleSPU
- Role: Signal Processing Unit
- Newer target: Less analyzed, potentially less hardened
Reverse Engineering IOKit Drivers
1. Find UserClient Classes
# From kernelcache in IDA/Ghidra:
# Search for strings containing "UserClient"
# Or search for ::externalMethod references
# Using jtool2
jtool2 -d __DATA.__objc_classlist kernelcache | grep UserClient
2. Find the External Method Table
In IDA:
1. Find the class's externalMethod() implementation
2. Look for a dispatch table reference (usually in __DATA_CONST.__const)
3. Table entries are IOExternalMethodDispatch structs:
struct IOExternalMethodDispatch {
IOExternalMethodAction function; // 8 bytes (function pointer)
uint32_t checkScalarInputCount; // 4 bytes
uint32_t checkStructureInputSize; // 4 bytes
uint32_t checkScalarOutputCount; // 4 bytes
uint32_t checkStructureOutputSize; // 4 bytes
};
3. Map the Method Table
From the dispatch table, build a map:
Selector 0 → create_surface(0 scalar, var struct, 0 scalar out, 0 struct out)
Selector 1 → release_surface(1 scalar, 0 struct, 0 scalar out, 0 struct out)
Selector 2 → ...
Each method = potential attack vector
4. Phrack Methodology
- Phrack #72 – Mapping IOKit Methods
- Systematic approach: enumerate all UserClients, map all external methods, prioritize targets
iOS 16+ Restrictions
Apple has restricted many IOKit UserClients:
Before iOS 16:
App sandbox → IOSurfaceRootUserClient (full method table)
App sandbox → IOGPUDeviceUserClient (full)
After iOS 16:
App sandbox → IOSurfaceRootUserClient_Restricted (limited methods)
App sandbox → IOGPUDeviceUserClient_Restricted (limited methods)
Restricted UserClients:
- Duplicate class inheriting from the original
- Separate method dispatch table with fewer methods
- Sensitive methods (e.g., set_value, get_value) removed
Fuzzing IOKit
Manual Fuzzing
// Brute-force selectors with random input
for (uint32_t sel = 0; sel < 256; sel++) {
uint64_t scalar[16];
char structbuf[4096];
arc4random_buf(scalar, sizeof(scalar));
arc4random_buf(structbuf, sizeof(structbuf));
IOConnectCallMethod(conn, sel,
scalar, 16,
structbuf, sizeof(structbuf),
NULL, NULL, NULL, NULL);
}
Intelligent Fuzzing
- Parse the method dispatch table to know the correct input sizes
- Use Corellium kernel hooks to monitor crashes
- Coverage-guided fuzzing (more complex but more effective)
Vulnerability Patterns in IOKit
| Pattern | Description | Prevention |
|---|---|---|
| Race condition | clientClose() concurrent with externalMethod() |
Locking, sequencing |
| UAF | Object freed but pointer still used | Reference counting |
| Integer overflow | Size validation bypassed | Safe integer arithmetic |
| Missing bounds check | Buffer overread/overwrite | Input validation |
| Type confusion | Object cast to wrong type | OSDynamicCast() checks |
| Double free | Object freed twice | Reference count tracking |