[ News ] [ Issues ] [ Authors ] [ Archives ] [ Contact ]


..[ Phrack Magazine ]..
.:: Mapping IOKit Methods Exposed to User Space on macOS ::.

Issues: [ 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 ] [ 65 ] [ 66 ] [ 67 ] [ 68 ] [ 69 ] [ 70 ] [ 71 ] [ 72 ]
Current issue : #72 | Release date : date: 2025-08-19 | Editor : author: Phrack Staff
IntroductionPhrack Staff
Phrack Prophile on GeraPhrack Staff
LinenoisePhrack Staff
LoopbackPhrack Staff
The Art of PHP - My CTF Journey and Untold Stories!Orange Tsai
Guarding the PHP Templemr_me
APT Down - The North Korea FilesSaber, cyb0rg
A learning approach on exploiting CVE-2020-9273dukpt
Mapping IOKit Methods Exposed to User Space on macOSKarol Mazurek
Popping an alert from a sandboxed WebAssembly moduleth0mas.nl
Desync the Planet - Rsync RCESimon, Pedro, Jasiel
Quantom ROPYoav Shifman, Yahav Rahom
Revisiting Similarities of Android AppsJakob Bleier, Martina Lindorfer
Money for Nothing, Chips for FreePeter Honeyman
E0 - Selective Symbolic InstrumentationJex Amro
Roadside to EveryoneJon Gaines
A CPU Backdooruty
The Feed Is Ourstgr
The Hacker's Renaissance - A Manifesto RebornTMZ
Title : Mapping IOKit Methods Exposed to User Space on macOS
Author : Karol Mazurek
View as text

|=------------------------------------------------------------------------=|
|=-------=[ Mapping IOKit Methods Exposed to User Space on macOS ]=-------=|
|=------------------------------------------------------------------------=|
|=-----------------=[ Karol Mazurek (@karmaz95) of AFINE ]=---------------=|
|=------------------------------------------------------------------------=|
|=----------------------=[ mapping-iokit-macos.pdf ]=---------------------=|


--[ Table of contents 
============================================================================
0 - Introduction
1 - IOKit Interface Fundamentals
    1.0 - Introduction to IOKit
    1.1 - Registry
        1.1.1 - Planes
        1.1.2 - Services
        1.1.3 - Driver
        1.1.4 - Matching
        1.1.5 - IOKit Personalities
        1.1.6 - Service Instances
    1.2 - User Clients
        1.2.1 - Multiple User Clients per Service
        1.2.2 - External Methods
        1.2.3 - Dispatch Mechanism
        1.2.4 - getTargetAndMethodForIndex
        1.2.5 - Access Control
    1.3 - User Space App
        1.3.1 - Service Discovery
        1.3.2 - Spawning User Client
        1.3.3 - Calling External Method
    1.4 - User Space Application Flow
2 - IOKit Reconnaissance
    2.0 - What to Map
    2.1 - KEXT Analysis
        2.1.1 - Bundle Names
        2.1.2 - Driver Names
        2.1.3 - NewUserClients
        2.1.4 - UC Types
        2.1.5 - External Methods
        2.1.6 - Arguments
    2.2 - Runtime Enumeration
        2.2.1 - Service Discovery Automation
        2.2.2 - Driver Hooking in Kernel Space
        2.2.3 - Driver Hooking in User Space
        2.2.4 - Corpus
        2.2.5 - Tracer
    2.3 - Map Verification
3 - Conclusion
4 - Acknowledgements 
5 - References
============================================================================



        ============
--[ 0 - Introduction
============================================================================
    IOKit is the core framework macOS uses for communication between user 
space and the kernel, exposing numerous driver interfaces. Despite ongoing 
efforts to harden the platform, IOKit continues to be a frequent source of 
vulnerabilities. Identifying which methods are accessible from user space 
is a first step for vulnerability research in this area. It enables the 
accurate enumeration of all available endpoints, ensuring complete coverage 
during fuzz testing.

This article outlines a structured methodology for mapping the IOKit 
external methods exposed to user space. By employing a combination of 
static analysis and runtime enumeration, we can identify accessible 
interfaces, pinpoint potential attack vectors, and establish a solid 
foundation for effective fuzzing. Our goal is to enhance the precision and 
effectiveness of IOKit vulnerability research.

This guide focuses solely on "external methods," but it is important to 
note that IOKit drivers also provide other communication channels, such as:
 - Properties:        For reading and writing driver configuration values
 - Notifications:     For receiving asynchronous events from the driver
 - Shared memory:     For efficient large data transfers
 - External traps:    A legacy method (use external methods instead)
 - Shared Data Queue: For bidirectional queued data transfer
 - IOStream:          For continuous data streaming

While this guide does not cover these additional channels, familiarizing 
yourself with the material presented here will help improve your 
understanding of them.

        ============================
--[ 1 - IOKit Interface Fundamentals
============================================================================
    The first part of the guide teaches about the main components of the 
IOKit. It introduces IOKit kernel space components and how to interact with 
them from User Space.

          =====================
--[ 1.0 - Introduction to IOKit
============================================================================
    IOKit is a framework in macOS that allows user-space applications to 
interact with hardware devices, forming part of the XNU kernel. It provides 
C++ classes and APIs for device drivers, abstracting hardware for easier 
management. The framework is documented extensively in Apple's IOKit 
Fundamentals[0].

          ========
--[ 1.1 - Registry
============================================================================
    Apple's IOKit maintains an IORegistry[1] of all devices in the system, 
organized as a tree structure. Each node (I/O Service instance) in this 
tree represents a device, driver, or attachment point, and the 
relationships between nodes reflect how devices are physically or logically 
connected. Here is an example of device family tree where a Mac machine has 
a USB port, a USB hub is connected to that port, and both a keyboard and 
mouse are plugged into the hub:

[Mac]
  |
  +-- [USB Port]
          |
          +-- [USB Hub]
                  |
                  +-- Keyboard
                  +-- Mouse

Each item ("Mac", "USB Port", etc.) is an IORegistryEntry[2] object (or an 
object derived from IORegistryEntry), forming the hierarchical structure of 
the IORegistry.

            ======
--[ 1.1.1 - Planes
============================================================================
    IOKit's IORegistry organizes all services in a tree structure that can 
be viewed through different "planes". Each plane[3] represents a distinct 
type of relationship or hierarchy among the same set of objects. The 
previous example of the USB family tree illustrates the Service plane. It 
should be the primary plane for mapping the attack surface in IOKit during 
vulnerability research, as it serves as the root plane. Focusing on other 
planes may result in missing services.

            ========
--[ 1.1.2 - Services
============================================================================
    The IOService[4] is the base class for all drivers in IOKit, 
representing 
hardware devices, virtual devices, or kernel-only system services. These 
services operate with full kernel privileges and direct hardware access. 
User space applications cannot directly instantiate, access, or call 
methods on I/O service objects.

            ======
--[ 1.1.3 - Driver
============================================================================
    Drivers are classes that inherit from superclasses, all ultimately 
deriving from the IOService class. For example, MyUSBDriver inherits from 
IOService, allowing dynamic interaction with it as an IOService object:

class MyUSBDriver : public IOService {
public:
    virtual bool init(OSDictionary *dictionary = NULL) override;
    virtual bool start(IOService *provider) override;
    virtual void stop(IOService *provider) override;
    IOReturn sendCommand(uint8_t cmd, uint32_t value);
private:
    IOUSBDevice *fDevice;
};

The key methods (init, start, stop) are lifecycle hooks invoked by IOKit as 
the driver is loaded, initialized, and terminated. Drivers on macOS are 
Kernel Extensions (KEXTS[5]). The driver is activated using 
OSKext::start()[6]. In real life, USB drivers are often a subclass of 
IOUSBDevice or IOUSBInterface, not directly an IOService. The provider 
parameter in start() is a pointer to the parent object in the IORegistry 
tree (often a device nub such as IOUSBDevice). The fDevice is a reference 
to the hardware device or logical service the driver manages.

            ========
--[ 1.1.4 - Matching
============================================================================
    Matching in IOKit refers to the process of finding and loading the 
appropriate driver for a detected device or service. When a new device, 
such as a USB device, is detected, IOKit creates a nub (for example, 
an IOUSBDevice object) in the IORegistry. IOKit then searches for drivers 
whose matching dictionaries (IOKitPersonalities[7]) specify compatibility 
with the nub, using a three-phase process: 

Class matching   : eliminates drivers whose IOProviderClass[8] does not 
match the nub's class

Passive matching : checks the remaining drivers' personalities for 
properties in KEXT Info.plist (e.g., vendor, product ID) to further narrow 
the candidates.

Active matching  : for each remaining candidate, it calls the driver's 
probe()[9] method to verify compatibility and assign a score actively. 

[IORegistryEntry] (base class)
   |
   +-- [IOService] (abstract service/driver class)
         |
         +-- [IOUSBDevice] (nub created for the USB device)
               |
               +-- [MyUSBDriver] (driver matched and attached to the device)

The driver with the highest score is started and attached to the nub, 
forming the provider-client relationship in the IORegistry tree.

            ===================
--[ 1.1.5 - IOKit Personalities
============================================================================
    The most important for us is that we can use the matching APIs to find 
the services we want to enumerate or fuzz — more on that in "1.3.1". Yet, 
to do that, we need to know the name under which the service is registered 
in the IORegistry. These can be found in IOKitPersonalities. Each key is 
a potential service name, for instance, under IOClass or IOProviderClass:

<key>IOKitPersonalities</key>
<dict>
    <key>MyUSBDriver</key>
    <dict>
        <key>CFBundleIdentifier</key>
        <string>com.example.driver.MyUSBDriver</string>
        <key>IOClass</key>
        <string>MyUSBDriver</string>
        <key>IOProviderClass</key>
        <string>IOUSBDevice</string>
        <key>idVendor</key>
        <integer>0x05AC</integer>
        <key>idProduct</key>
        <integer>0x1234</integer>
    </dict>
</dict>

A single driver can have multiple personalities, enabling support for 
different device types or hardware variants without needing separate 
drivers.

            =================
--[ 1.1.6 - Service Instances
============================================================================
    Not all IOKit personality entries result in instantiated services. 
A personality defined in a driver's Info.plist will only be matched and 
loaded if the hardware or software conditions are met. For example, on 
a Mac mini without an external monitor, any display-related personalities 
will not be matched, and the corresponding services will not appear in the 
IORegistry. It's also common for a single driver to have multiple 
instantiated services when the associated hardware appears more than once, 
such as with multiple monitors or input devices. Tools like ioscan[10] can
be used to list all instantiated services and often reveal multiple entries 
with the same service name, such as IOThunderboltPort or IONetworkStack:

IOThunderboltPort
IOThunderboltSwitchType5
IOThunderboltPort
IONetworkStack

To interact with or fuzz a driver, at least one instance of the 
corresponding service must be active in the IORegistry, allowing access to 
the driver's code. It is discussed further in section "2.1.2".

          ============
--[ 1.2 - User Clients
============================================================================
    The IOUserClient[11] is a subclass of IOService that serves as a secure 
bridge between user-space applications and kernel I/O service objects. 
It does not interact with hardware directly but provides a controlled 
interface for safe communication with kernel services. IOUserClient objects 
serve as security gatekeepers, running in kernel space and handling 
requests from unprivileged user-space applications. They validate input, 
enforce access controls, and sanitize data before passing it to services. 
The core logic is implemented in newUserClient[12] functions.

            =================================
--[ 1.2.1 - Multiple User Clients per Service
============================================================================
    A single Service can have multiple User Clients registered. They can 
expose different interfaces to the same underlying Service. Each User 
Client type has a unique numeric identifier (uint32_t). Applications 
specify which User Client type they want when connecting with 
IOServiceOpen()[13]. Type 0 is typically the default/primary interface.

            ================
--[ 1.2.2 - External Methods
============================================================================
    The externalMethod[14] within the User Client handles incoming 
IOConnectCallMethod[15] requests from the user-space app. It validates that 
the selector is within bounds and argument sizes. Based on the selector 
value, it routes them to appropriate handler functions. These are the final 
endpoints where core logic and most of the vulnerabilities lie.

            ==================
--[ 1.2.3 - Dispatch Mechanism
============================================================================
    At WWDC22, the validation portion of the external method was moved to a 
new "2022" dispatchExternalMethod, which serves as a wrapper around the 
method array (sIOExternalMethodArray):

IOReturn AppleJPEGDriverUserClient::externalMethod(
    IOUserClient* userClient,             // this user client instance
    uint32_t selector,                    // method id from user space
    IOExternalMethodArguments* arguments  // I/O parameters from user space
)
{
    return IOUserClient2022::dispatchExternalMethod(
        userClient,               // the user client object
        selector,                 // which method to call (0-9)
        arguments,                // parameters from user space
        &sIOExternalMethodArray,  // dispatch table with 10 methods
        10,                       // number of methods in table
        userClient,               // target object for method calls
        0                         // additional flags/options
    );
}

The dispatchExternalMethod()[16] validates the selector is within bounds, 
calculates the dispatch table entry at methodArray[selector], checks 
arguments sizes (I/O scalars and structs), optionally validates 
entitlements for privileged operations, and finally, call the target 
handler function if all checks pass. 

            ==========================
--[ 1.2.4 - getTargetAndMethodForIndex
============================================================================
    Although many UserClient::externalMethods were rewritten to include 
IOUserClient2022::dispatchExternalMethods, there is still a significant 
amount of code that follows the old method[17]. There are also 
getTargetAndMethodForIndex[18]. Drivers using the old way are more prone to 
misuse or missing validation.

            ==============
--[ 1.2.5 - Access Control
============================================================================
    The same selector can mean different things in different User Clients:

IOService "MyDevice"
|- IOUserClient Type 0 (standard interface)
|  |- Selector 0: GetStatus
|  `- Selector 1: SetConfig
`- IOUserClient Type 1 (admin interface)
   |- Selector 0: FactoryReset
   `- Selector 1: UpdateFirmware

This design provides both functional separation and security boundaries - 
unprivileged apps receive limited user clients, while privileged ones 
receive full-featured ones. The entitlements[19] embedded in the 
application's code signature[20] define access to these interfaces.

          ==============
--[ 1.3 - User Space App
============================================================================
    User-space applications can be sandboxed, restricting their access to 
IOKit even if they possess the necessary entitlements. Sandboxed apps are 
generally restricted from performing sensitive operations, such as opening 
hardware service connections or modifying properties. On macOS Sequoia, the 
Sandbox Operations affecting IOKit access include[21]:
- "iokit*"
- "iokit-get-properties"
- "iokit-issue-extension"
- "iokit-open*"
- "iokit-open-user-client"
- "iokit-open-service"
- "iokit-set-properties"

The key permission required for a sandboxed app to use IOConnectCallMethod 
is "iokit-open-user-client." Note that unsandboxed malware does not have 
such restrictions, and the remainder of the article discusses the context 
of an unsandboxed app.

            =================
--[ 1.3.1 - Service Discovery
============================================================================
    User space applications can use matching dictionaries to find services 
based on properties like IOProviderClass, IONameMatch[22], or custom 
attributes. IOServiceGetMatchingServices()[23] searches the registry and 
returns matching IOService objects:

CFDictionaryRef matching = IOServiceMatching("IOService");
result = IOServiceGetMatchingServices(masterPort, matching, &iterator);

However, this method iterates only over the root services that directly 
inherit from the IOService class. To explore deeper into the hierarchy and 
access all services, use a recursive iterator:

io_iterator_t iter;
io_service_t service_ref;

// Create an iterator for the IOService plane, recursively
const kern_return_t kr = IORegistryCreateIterator(
    kIOMainPortDefault,
    kIOServicePlane,
    kIORegistryIterateRecursively,
    &iter
);

// Iterate over every service in the plane
while ((service_ref = IOIteratorNext(iter)) != MACH_PORT_NULL) {

Let's say we want to find driver named "NS_01", we can use 
IORegistryEntryGetName[24]:

    char name_buf[128];
    if (IORegistryEntryGetName(service_ref, name_buf) == KERN_SUCCESS) { 
        if (strcmp(name_buf, "NS_01") == 0) {
            // Found target service - use service_ref for IOServiceOpen()
            found_service_ref = service_ref;
            break;
        }
    }
    IOObjectRelease(service_ref);
}

This provides a service handle ready for communication.

            ====================
--[ 1.3.2 - Spawning User Client
============================================================================
    Once a target service is located, we can use IOServiceOpen()[25] to 
create an IOUserClient instance for communication:

io_connect_t connection;
kern_return_t result = IOServiceOpen(
    found_service_ref,    // service from discovery
    mach_task_self(),     // current task
    0,                    // user client type
    &connection           // returned connection handle
);

The service validates the request and instantiates the appropriate 
IOUserClient subclass, returning a connection handle for method calls. On 
the kernel side, this is handled by SERVICE_NAME::newUserClient functions.

            =======================
--[ 1.3.3 - Calling External Method
============================================================================
    Finally, we can use IOConnectCallMethod() to invoke the functionality 
we want through the established connection. Although there is no direct 
kernel memory access, this exposure can still introduce vulnerabilities 
that may lead to kernel code execution[26].

uint64_t input_scalar = 0x1234;
uint64_t output_scalar = 0;
uint32_t output_count = 1;

result = IOConnectCallMethod(
    connection,       // connection handle
    5,                // selector (method index)
    &input_scalar,    // scalar inputs
    1,                // scalar input count
    NULL,             // struct input buffer
    0,                // struct input size
    &output_scalar,   // scalar outputs
    &output_count,    // scalar output count
    NULL,             // struct output buffer
    NULL              // struct output size
);

The output from the external method, if any, is received through the 
structure of the output buffer and scalar outputs, while the status code is 
stored in the result. It's important to note that IOConnectCallMethod is 
the most commonly used function; however, there are other similar methods, 
all of which begin with IOConnectCall*[27].

          ===========================
--[ 1.4 - User Space Application Flow
============================================================================

Service Discovery
    |
    v
+-------------------+     IOServiceMatching()
| Matching Dict     |---> IOServiceGetMatchingServices()
| "DriverName"      |     or IORegistryCreateIterator()
+-------------------+
    |
    v
+-------------------+     IOIteratorNext()
| Service Iterator  |---> IORegistryEntryGetName()
| Loop through all  |     strcmp(name, "target")
+-------------------+
    |
    v
Connection Establishment
    |
    v
+-------------------+     IOServiceOpen()
| io_service_t      |---> Creates IOUserClient
| (Service Handle)  |     Returns io_connect_t
+-------------------+
    |
    v
Method Invocation
    |
    v
+-------------------+     IOConnectCallMethod()
| io_connect_t      |---> Selector + Arguments
| Connection Handle |     Routed to externalMethod()
+-------------------+


User App  -->  I/O Registry  -->  IOService  -->  IOUserClient
   |              ^                   ^              |
   |              |                   |              |
   +-- Discovery --+                  +-- Bridge ----+
   |                                                 |
   +------------- IOConnectCallMethod ---------------+

        ===================
--[ 2 - IOKit Reconeiscance
============================================================================
    The second part of the guide shows how to map the attack surface 
properly to have a complete picture of External Methods exposed from Kernel 
Space to User Space.

          ===========
--[ 2.0 - What to Map
============================================================================
    We need data from IOKit drivers, which enable targeted fuzzing and 
facilitate faster crash analysis. The result of this entire process should 
include a structured YAML (or your preferred format) file and a corpus 
directory with binary files for IOConnectCallMethod.

Fields of interests:
- Bundle Names     : Kext identifiers containing the driver code. 
                     Example: com.apple.iokit.IOAVBFamily
- Driver Names     : IORegistry service names, so we can match them before 
                     using IOServiceOpen. Example: IOAVBNub
- NewUserClients   : Methods that handle new user client creation.
                     Example: IOAVBNub::newUserClient
- Types            : Valid `type` values for IOServiceOpen.
- External Methods : externalMethod selectors exposed by each user client.
- Arguments        : Valid scalar and struct sizes for input/output.
- Endpoints        : Selector IDs per UC, each mapped to argument sizes.
- Corpus           : Binary samples for inputStruct used by the system or 
                     known to be valid.

Each driver maps to its connection types, which map to selector IDs with 
their argument layout:

  DriverName:
    TypeValue:
      SELECTOR_ID: [INPUT_SCALAR_CNT, INPUT_STRUCT_SIZE, OUTPUT_SCALAR_CNT, 
OUTPUT_STRUCT_SIZE]

Example YAML Output Format:

AFKEndpointInterface     # AFKEPInterfaceKextV2::newUserClient
    1768910955           # AFKEndpointInterfaceUserClient::externalMethod
        0: [2, 10, 16, 10] # extOpenMethod
        1: [1, 10, 1, 10]  # extCloseMethod
        2: [7, 0, 0, 0]    # extEnqueueCommandMethod
...

Corpus Directory Layout:
Each selector gets a directory containing valid binary payloads. These can 
be passed directly into IOConnectCallMethod as inputStruct payloads.

    AFKEndpointInterface/1768910955
    |- 0/
    |  `- corpus_0.bin
    |- 1/
    |  `- corpus_0.bin
    `- ...

This is a suggested structure for the IOKit MAP. Feel free to organize the 
data as you like; it’s just how I categorize information for my fuzzer.

          =============
--[ 2.1 - KEXT Analysis
============================================================================
    Kernel extensions (KEXTs) are loaded into a kernel cache. Their source 
directories are in: /System/Library/Extensions. To get the actual binaries, 
decompress the kernel cache:

ipsw kernel dec $(ls 
/System/Volumes/Preboot/*/boot/*/System/Library/Caches/com.apple.kernelcache
s/kernelcache) -o kernelcache

Then extract all KEXTs from the decompressed kernel cache:
ipsw kernel extract $(kernelcache.decompressed) --all

Alternatively, download the latest IPSW manually:
ipsw dl appledb --os macOS --latest --kernel

Then extract KEXTs as before:
ipsw kernel extract kernelcache.release.* --all

Each KEXT includes an Info.plist file, which helps with mapping. Unlike 
binaries, these files are found at:
/System/Library/Extensions/KEXT_BUNDLE_NAME/Contents/Info.plist

For more, see: "Kernel Extensions on macOS"[28] (not required to follow 
the rest of this paper).

            ============
--[ 2.1.1 - Bundle Names
============================================================================
    We need these names, because they represent binaries. Having all 
binaries ensures we won't miss any exposed external methods. There are many 
KEXTs, but we are only interested in drivers. These typically contain 
"driver" or "iokit" in their bundle names.

To list them from the extracted kernel cache (Sequoia returns 304 names):

/bin/ls -1 
kernelcache/System/Volumes/Preboot/*/boot/*/System/Library/Caches/com.apple.
kernelcaches/ | grep ".iokit.\|.driver."
...
com.apple.driver.AppleALSColorSensor
com.apple.driver.AppleANELoadBalancer
com.apple.driver.AppleAOPAudio
...
com.apple.iokit.IOStreamFamily
com.apple.iokit.IOSurface
com.apple.iokit.IOThunderboltFamily
...

Another method is to use kextstat, but it only displays currently loaded 
KEXTs. On Sequoia 15.4.1, this returns 220 entries:

kextstat | grep ".iokit.\|driver" | awk '{print $6}'
...
com.apple.iokit.IOGraphicsFamily
com.apple.driver.DiskImages
com.apple.iokit.IOKitRegistryCompatibility
...

To get the complete list, use the first method.

            ============
--[ 2.1.2 - Driver Names
============================================================================
    To interact with a driver using IOServiceOpen, we must know the name of 
an instantiated IOService object it creates. These names typically come 
from the IOKitPersonalities section of the driver's Info.plist, where each 
key or value under fields like IOClass, IOProviderClass, IOName, or 
IONameMatch can indicate a potential service name. To enumerate all 
potential service names defined in the Info.plist, the following command 
extracts relevant fields:

plutil -convert json -o - 
/System/Library/Extensions/KEXT_NAME/Contents/Info.plist \
| jq -r '
  .IOKitPersonalities
  | to_entries[]
  | [
      .key,
      .value.IOClass,
      .value.IOProviderClass,
      .value.IONameMatch,
      .value.IOName
    ]
  | flatten
  | .[]
  | select(. != null)
' | sort -u

However, not all services declared in the Info.plist are instantiated at 
runtime. Matching only occurs if the system satisfies the personality's 
conditions — such as the presence of specific hardware. Conversely, not all 
instantiated services are declared in the Info.plist. Some are created 
programmatically within the KEXT via calls like registerService[29], or by 
overriding probe or start methods to instantiate custom service classes 
without corresponding plist entries.

            ==============
--[ 2.1.3 - NewUserClients
============================================================================
    Another way to identify driver service names is by analyzing 
IOService::newUserClient overrides in the KEXT binary. This method creates 
IOUserClient instances, which handle communication with user space via 
IOServiceOpen. To find them, use ipsw[30]:
ipsw macho info --symbols KEXT_NAME | grep -i "NewUserClient"

Alternatively, nm may work, but it can fail on some KEXTs:
nm -m KEXT_NAME | grep -i "NewUserClient"

The results are mangled symbols, for example:
__ZN14ANEClientHints13newUserClientEP4taskPvjPP12IOUserClient  
__ZN21ANEPrivilegedVMAccess13newUserClientEP4taskPvjPP12IOUserClient  
__ZN8H11ANEIn13newUserClientEP4taskPvjPP12IOUserClient

Use c++filt[31] to demangle:
c++filt "__ZN14ANEClientHints13newUserClientEP4taskPvjPP12IOUserClient"
ANEClientHints::newUserClient(task*, void*, unsigned int, IOUserClient**)

This reveals possible driver service names: ANEClientHints, 
ANEPrivilegedVMAccess, and H11ANEIn. Each name maps to a different 
IOService class, which may implement unique newUserClient logic. The name 
used during IOServiceOpen determines which logic and IOUserClient subclass 
gets instantiated, so instance behavior depends directly on the selected 
service.

            ========
--[ 2.1.4 - UC Types
============================================================================
    In addition to the service name, the type argument passed to 
IOServiceOpen further influences which IOUserClient subclass is 
instantiated. It allows a single-driver service to expose multiple client 
interfaces with different logic.

Consider the decompiled H11ANEIn::newUserClient implementation, which shows 
that when opening the H11ANEIn service, passing type == 1 results in the 
creation of H11ANEInDirectPathClient, while any other value defaults to 
H11ANEInUserClient. These are distinct IOUserClient subclasses, each with 
its own set of external methods and privilege checks.

__int64 __fastcall H11ANEIn::newUserClient(
    H11ANEIn *this, task *clientTask, 
    void *securityToken, 
    __int64 type, 
    IOUserClient **userClientOut)
{
  *userClientOut = nullptr;

  if (type == 1) {
    // Logs: Creating direct evaluate client
    auto client = new H11ANEInDirectPathClient;
    if (client && client->initWithTask(clientTask, 0) &&
        client->attach(this) &&
        client->start(this)) {
      *userClientOut = client;
      return 0;
    }
    if (client) {
      client->detach(this);
      client->release();
    }
  } else {
    // Logs: Creating default full-entitlement client
    auto client = new H11ANEInUserClient;
    if (client && client->initWithTask(clientTask, 0) &&
        client->attach(this) &&
        client->start(this)) {
      *userClientOut = client;
      return 0;
    }
    if (client) {
      client->detach(this);
      client->release();
    }
  }

  return 0xE00002C9; // kIOReturnUnsupported
}

Thus, not only does the selected service name determine which newUserClient 
logic is triggered, but the type parameter allows additional branching 
within that logic. It is commonly used to expose limited or privileged 
functionality through different user-client implementations. 

Automating type enumeration through dynamic analysis is often impossible, 
as the logic is frequently structured to return the same kernel code for 
both valid and invalid types. To ensure we map all possible types, we must 
reverse these NewUserClients and verify them by hand.

            ================
--[ 2.1.5 - External Methods
============================================================================
    For each previously identified IOUserClient subclass, we can extract 
user-accessible methods by locating either externalMethod or 
getTargetAndMethodForIndex. These functions define how the driver 
dispatches incoming method calls from user space. To locate them, I use IDA 
and search for symbol names like H11ANEInDirectPathClient::externalMethod. 

It can also be done in the terminal:
ipsw macho info --symbols KEXT_NAME | grep 
"externalMethod\|getTargetAndMethodForIndex"

Based on the previous example, this would yield:
0xfffffe0009a796c0: H11ANEInUserClient::externalMethod(unsigned int, 
IOExternalMethodArgumentsOpaque*)
0xfffffe0009a788a0: H11ANEInDirectPathClient::externalMethod(unsigned int, 
IOExternalMethodArgumentsOpaque*)

As discussed in Part 1, there are several common dispatch patterns, but 
ultimately, most of them rely on a method array and a fixed number of 
entries. In our case, the decompiled dispatcher looks like this:
IOReturn H11ANEInUserClient::externalMethod(
    uint32_t selector, 
    IOExternalMethodArguments *args)
{
    return IOUserClient2022::dispatchExternalMethod(
        selector,
        args,
        &H11ANEInUserClient::sMethods,
        0x32,  // Number of supported methods
        this,
        nullptr
    );
}

We're specifically interested in H11ANEInUserClient::sMethods, which is the 
method dispatch table. The symbol name for this array may vary, so manual 
inspection is required. If you know the symbol name, you can locate it with:

ipsw macho info --symbols KEXT_NAME | grep "sMethods"
0xfffffe0007f57b78: __ZN18H11ANEInUserClient8sMethodsE
0xfffffe0007f57a10: __ZN24H11ANEInDirectPathClient8sMethodsE

This tells us the dispatch table for H11ANEInUserClient is at 
0xfffffe0007f57b78, and it contains 0x32 method entries. To parse the table 
correctly, we must know the size of each entry, which depends on the 
dispatcher used. After reversing all macOS drivers I found these possible 
dispatchers and sizes:

IOExternalMethodDispatch2022 == 0x28
IOExternalMethodDispatch     == 0x18
getTargetAndMethodForIndex   == 0x30
Custom                       == ???

Note that some custom implementations require manual handling. With this 
information, we can iterate over the array and identify each method. In 
IDA, double-clicking works. In other tools, manually add the entry size to 
the base address:

Method 0  == 0xfffffe0007f57b78 + 0x28
Method 1  == 0xfffffe0007f57b78 + (0x28 * 2)
...
Method 49 == 0xfffffe0007f57b78 + (0x28 * 0x31)

By doing this, we can find the addresses of all functions exposed to user 
space for IOConnectCallMethod(), as well as the selectors to use for 
calling them. The final question is, what arguments do they expect?

            =========
--[ 2.1.6 - Arguments
============================================================================
    Even with the correct driver name, service matching, user client type, 
and external method selector, calls can still fail due to argument size 
validation. Most functions validate sizes to prevent buffer overflows 
(though some historically lacked this protection). IOConnectCallMethod() 
expects these arguments:

kern_return_t IOConnectCallMethod(
    mach_port_t     connection,        // Connection handler from 
IOServiceOpen
    uint32_t        selector,          // Method index on user client
    const uint64_t  *input,            // Scalar input parameters array
    uint32_t        inputCnt,          // Number of scalar inputs
    const void      *inputStruct,      // Structured input data buffer  
    size_t          inputStructCnt,    // Size of inputStruct buffer
    uint64_t        *output,           // Scalar output values array
    uint32_t        *outputCnt,        // Max/actual elements in output
    void            *outputStruct,     // Structured output data buffer
    size_t          *outputStructCnt   // Max/actual size of outputStruct
);

Invalid sizes for inputCnt, inputStructCnt, outputCnt, or outputStructCnt 
will cause verification failure and dropped calls. Only with valid sizes 
are input and inputStruct values passed to the target function. Without 
knowing these values, testing is impossible. Automation is impractical due 
to (2^32-1)^4 = ~3.4*10^38 possibilities, and identical error codes make 
brute force ineffective. Manual analysis is required. Valid sizes are found 
in dispatch tables, which appear complex in disassemblers:

__const:FFFFFE0007F57BC8  DCQ  
__ZN18H11ANEInUserClient23_ANE_ProgramSendRequestE...
                                ; 
H11ANEInUserClient::_ANE_ProgramSendRequest(...)
__const:FFFFFE0007F57BD0  DCB  1     ; inputCnt
__const:FFFFFE0007F57BD1  DCB  0     
__const:FFFFFE0007F57BD2  DCB  0     
__const:FFFFFE0007F57BD3  DCB  0     
__const:FFFFFE0007F57BD4  DCB  0x48  ; inputStructCnt (72 bytes)
__const:FFFFFE0007F57BD5  DCB  9     
__const:FFFFFE0007F57BD6  DCB  0     
__const:FFFFFE0007F57BD7  DCB  0     
__const:FFFFFE0007F57BD8  DCB  0     ; outputCnt
__const:FFFFFE0007F57BD9  DCB  0     
__const:FFFFFE0007F57BDA  DCB  0     
__const:FFFFFE0007F57BDB  DCB  0     
__const:FFFFFE0007F57BDC  DCB  0x28  ; outputStructCnt (40 bytes)
__const:FFFFFE0007F57BDD  DCB  0     
...

To make it easier, I created this IDA script[32] that prints valid pointers 
for the exposed methods, their selectors, and valid sizes:

print_methods(0xfffffe0007f57b78, 50)

Method summary (ID: [SCALAR_IN, IN_SIZE, SCALAR_OUT, OUT_SIZE]):
0: [0, 96, 0, 96]
1: [0, 0, 0, 0]
2: [1, 2376, 0, 40]
3: [0, 3480, 0, 0]
4: [0, 56, 0, 56]
...
49: [2, 0, 0, 0]

This way, we mapped everything we needed to reach the logic of the exposed 
functions. At this point, our map is complete, and we can start dumb 
fuzzing with random data in inputStruct. However, to be more efficient, we 
should find valid data sent to these exposed methods and store it as our 
corpus. For that, we need to do some runtime analysis.

          ===================
--[ 2.2 - Runtime Enumeration
============================================================================
    In the previous section, we covered how to map drivers using static 
analysis. Now, let's look at a few runtime techniques to enumerate all 
instantiated IOKit services and gather useful data for calling methods via 
IOConnectCallMethod. This section may seem short, but don't underestimate 
its value. These runtime tricks have saved me countless hours during 
research. They also make a huge difference in the accuracy and coverage of 
my fuzzer. I recommend completing Section "2.1 KEXT Analysis" before using 
runtime tools. While these tools can help verify service names, we still 
need to reverse-engineer the User Client logic regardless of the method 
used. Performing static analysis first ensures that we do not rely solely 
on partial runtime results, which may overlook unregistered services!

            ============================
--[ 2.2.1 - Service Discovery Automation
============================================================================
    The main tool used here is Siguza's ioscan[33]. It enumerates all 
currently instantiated IOKit services, showing their class names, instance 
names, associated UserClient class (if specified in properties), and 
whether the UserClient can be successfully opened from user space. 

Below is an example output:

Class                     Name            UC             Spawn
------------------------ --------------- -------------- --------------------
IORegistryEntry          Root            -              invalid argument
IOPlatformExpertDevice   J314cAP         -              invalid argument
IODTNVRAM                options         -              unsupported function
IODTNVRAMDiags           IODTNVRAMDiags  -              unsupported function
IODTNVRAMVariables       options-system  -              unsupported function
IODTNVRAMVariables       options-common  -              unsupported function
AppleARMPE               AppleARMPE      -              unsupported function
IOPMrootDomain           IOPMrootDomain  RootDomainUserClient    successful
IORootParent             IORootParent    -              unsupported function
IOUSBHostInterface       HID I2C         AppleUSBHostFWIntfClt   successful

Most importantly, the "Name" column displays instantiated services. After 
reading this paper, you know it is not so trivial to get them. By using 
this tool, we can list them all. However, another problem arises here: we 
have the names, but how do we assign them to a valid User Client logic? 

To illustrate this, there is the AppleUSBHostFrameworkInterfaceClient, 
which is assigned to the names IOUSBHostInterface, HID I2C, and 
IOUSBHostInterface. All of them have the same Class IOUSBHostInterface:

IOUSBHostInterface  IOUSBHostInterface  AppleUSBHostFrameworkInterfaceClient
IOUSBHostInterface  HID I2C             AppleUSBHostFrameworkInterfaceClient
IOUSBHostInterface  IOUSBHostInterface  AppleUSBHostFrameworkInterfaceClient

However, we only see AppleUSBHostFrameworkInterfaceClient if we can 
successfully establish a connection. The Spawn column shows the result of 
calling selector 0 using IOServiceOpen. If the call fails due to an invalid 
argument, the service remains accessible but requires different input 
sizes. If it returns an unsupported function, selector 0 is unimplemented, 
though higher selectors may still be valid. In both cases, we do not get 
the User Client name. How to get it? In the first column, we have a Class 
name. We can use it to find NewUserClient logic and, inside, the User 
Client name. For our example, this is "IOUSBHostInterface::newUserClient":

IOReturn IOUSBHostInterface::newUserClient()
...
if (type == 0) {
    // Used when type == 0
    AppleUSBHostFrameworkInterfaceClient *client =
        new AppleUSBHostFrameworkInterfaceClient(...);
}
else if (type == 1 || type == 2) {
    // Used when type == 1 or 2
    AppleUSBHostInterfaceUserClient *client =
        new AppleUSBHostInterfaceUserClient(...);
}

In most cases, we will have only the Class name and no UC, so we have to 
perform this short RE step manually. Additionally, we can benefit from this 
by obtaining the Types quickly. As shown in the above example, there are 
two UC classes that this driver supports: 0 and 1||2.

Another way of listing is IORegistry reader command:

ioreg -l | grep "IOUserClientClass"
ioreg -l | grep -i "IOClass"
ioreg -l | grep "com.apple.driver.AppleH11ANEInterface" -A 5

We can use it to list all registered UC classes, services, and essentially 
anything we have learned here or found in the IOKit documentation. 

            ==============================
--[ 2.2.2 - Driver Hooking in Kernel Space
============================================================================
    The complete way to analyze how drivers handle user-space interactions 
— such as spawning user clients (newUserClient) or handling external method 
calls — is to debug these Kernel functions directly. However, on Apple 
Silicon, kernel debugging is effectively impossible, and writing a custom 
KEXT to trace or hook into these paths would be a research project on its 
own. However, we can use DTrace to list all instrumented IOKit functions in 
the Kernel. It shows us some places where kernel methods are being used:

sudo dtrace -l | grep IOService
sudo dtrace -l | grep IOUserClient
sudo dtrace -l | grep NewUserClient
sudo dtrace -l | grep externalMethod
sudo dtrace -l | grep getTargetAndMethodForIndex
sudo dtrace -l | grep dispatchExternalMethod

It is handy as an additional source of information about services, but this 
does not cover all places, as it only shows instrumented functions. 
However, as we learned in the first part of this paper, ultimately, 
everything is an IOService, so we can still use DTrace to trace system-wide 
calls. For that, we need to disable SIP (System Integrity Protection), 
which is required to use DTrace on macOS:

csrutil disable
# csrutil enable --without dtrace

Using the information below, we can trace the processes that spawn 
UserClients, which, in fact, call IOServiceOpen in user space:

sudo dtrace -n '
fbt::*NewUserClient*:entry,
fbt::*newUserClient*:entry {
    printf("%s: PID=%d CMD=%s\n", probefunc, pid, execname);
}'

Example output:

_ZN9IOService13newUserClientEP4taskPvjP12OSDictionaryPP12IOUserClient:entry 
_ZN9IOService13newUserClientEP4taskPvjP12OSDictionaryPP12IOUserClient: 
PID=367 CMD=opendirectoryd
_ZN9IOService13newUserClientEP4taskPvjP12OSDictionaryPP12IOUserClient:entry 
_ZN9IOService13newUserClientEP4taskPvjP12OSDictionaryPP12IOUserClient: 
PID=609 CMD=Finder
_ZN9IOService13newUserClientEP4taskPvjP12OSDictionaryPP12IOUserClient:entry 
_ZN9IOService13newUserClientEP4taskPvjP12OSDictionaryPP12IOUserClient: 
PID=339 CMD=configd
_ZN5IOGPU13newUserClientEP4taskPvjPP12IOUserClient:entry 
_ZN5IOGPU13newUserClientEP4taskPvjPP12IOUserClient: PID=13910 
CMD=com.apple.WebKit.GPU

Ultimately, we can create a script that dumps names and demangles 
functions, allowing us to see which process calls which UserClients. 
I prepared dtrace_NewUserClient.py[34], which example output is below:

PID: 609
Path: /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder
Function: IOService::newUserClient(task*, void*, unsigned int, OSDictionary*
, IOUserClient**)
--- Call Stack---

libsystem_kernel.dylib`mach_msg2_trap+0x8
IOKit`io_service_open_extended+0xb0
IOKit`IOServiceOpen+0x34
APFS`apfs_container_iouc+0x110
APFS`APFSExtendedSpaceInfo+0x94
DesktopServicesPriv`TAPFSContainer::Refresh(bool)+0xc8
DesktopServicesPriv`APFSUsedSpace(std::__1::vector<TString, 
std::__1::allocator<TString>>, std::__1::shared_ptr<TAPFSContainer>, 
TString const&)+0x54
DesktopServicesPriv`TFSVolumeInfo::RecalculateFreeSpaceAndCapacity(bool) 
const+0x2b4
DesktopServicesPriv`TNode::RecalculateFreeSpaceAndCapacity(bool)+0x80
DesktopServicesPriv`TNode::SynchronizeChildren(NodeRequestOptions, 
TNodeEventPtrs&)+0x1bc0
DesktopServicesPriv`TNode::HandleSync(NodeRequestOptions, 
TNodeEventPtrs&)+0xd6c
DesktopServicesPriv`TNode::HandleSync(NodeRequestOptions)+0x2c
DesktopServicesPriv`TNode::HandleSync(std::__1::shared_ptr<TNodeTask> 
const&, TNodePtr const&)+0xc4
DesktopServicesPriv`TNode::HandleNodeRequest(std::__1::shared_ptr<TNodeTask>
 const&)+0x2d4
DesktopServicesPriv`ExceptionSafeBlock(void () block_pointer)+0x30
DesktopServicesPriv`invocation function for block in 
TNodeTask::PostNodeTaskRequest(std::__1::shared_ptr<TNodeTask> const&, 
std::__1::shared_ptr<TVolumeSyncThread> const&)+0xa8
libdispatch.dylib`_dispatch_call_block_and_release+0x20
libdispatch.dylib`_dispatch_client_callout+0x10
libdispatch.dylib`_dispatch_lane_serial_drain+0x2e4
libdispatch.dylib`_dispatch_lane_invoke+0x184
----------------------------------------
PID: 75884
Path: crims0n (process terminated before path lookup)
Function: IOService::newUserClient(task*, void*, unsigned int, 
OSDictionary*, IOUserClient**)
--- Call Stack---

0x192b00c34
0x1969cbb80
0x1969cbab8
0x1025bdf4c
0x1025c0ac4
0x1025bd130
0x1927a2b98
----------------------------------------

The above DTrace is helpful, but the one below is a real game-changer for 
Vulnerability Research. You will learn why on the first run ;)
Using the below, we can trace processes that use external methods - so, in 
fact, that calls in user-space the IOConnectCallMethod.

sudo dtrace -n '
fbt::*getTargetAndMethodForIndex*:entry,
fbt::*externalMethod*:entry,
fbt::*ExternalMethod*:entry {
    printf("%s: PID=%d CMD=%s\n", probefunc, pid, execname);
}'

Example output:

1 362094 
_ZN14AppleSMCClient26getTargetAndMethodForIndexEPP9IOServicej:entry 
_ZN14AppleSMCClient26getTargetAndMethodForIndexEPP9IOServicej: PID=393 
CMD=corebrightnessd
2 408402 
_ZN16IOUserClient202222dispatchExternalMethodEjP31IOExternalMethodArgumentsO
paquePK28IOExternalMethodDispatch2022mP8OSObjectPv:entry 
_ZN16IOUserClient202222dispatchExternalMethodEjP31IOExternalMethodArgumentsO
paquePK28IOExternalMethodDispatch2022mP8OSObjectPv: PID=14020 CMD=crims0n
2 268098 
_ZN24H11ANEInDirectPathClient15_ANE_DeviceOpenEPS_PvP25IOExternalMethodArgum
ents:entry 
_ZN24H11ANEInDirectPathClient15_ANE_DeviceOpenEPS_PvP25IOExternalMethodArgum
ents: PID=14020 CMD=crims0n
0 362094 
_ZN14AppleSMCClient26getTargetAndMethodForIndexEPP9IOServicej:entry 
_ZN14AppleSMCClient26getTargetAndMethodForIndexEPP9IOServicej: PID=804 
CMD=System Monitor

As before, we can wrap it out in some parser and print better output. Here 
is example from dtrace_IOConnectCallMethod.py[35]:

PID: 398
Path: 
/System/Library/PrivateFrameworks/SkyLight.framework/Resources/WindowServer 
-daemon
Function: IOUserClient::externalMethod(unsigned int, 
IOExternalMethodArguments*, IOExternalMethodDispatch*, OSObject*, void*)
--- Call Stack---

libsystem_kernel.dylib`mach_msg2_trap+0x8
IOKit`io_connect_method+0x208
IOKit`IOConnectCallScalarMethod+0x50
IOMobileFramebuffer`kern_SwapWait+0x4c
...
QuartzCore`thread_fun(void*)+0x20
libsystem_pthread.dylib`_pthread_start+0x88
libsystem_pthread.dylib`thread_start+0x8
----------------------------------------

PID: 97690
Path: 
/System/Library/Frameworks/VideoToolbox.framework/Versions/A/XPCServices/VTE
ncoderXPCService.xpc/Contents/MacOS/VTEncoderXPCService
Function: AppleAVE2UserClient::IO_Process(AppleAVE2UserClient*, void*, 
IOExternalMethodArguments*)
--- Call Stack---

libsystem_kernel.dylib`mach_msg2_trap+0x8
IOKit`io_connect_method+0x208
IOKit`IOConnectCallMethod+0xec
IOKit`IOConnectCallStructMethod+0x38
AppleVideoEncoder`0x00000001033ee040+0x80
AppleVideoEncoder`0x0000000103330a70+0x128
...
libdispatch.dylib`_dispatch_worker_thread+0x10c
libsystem_pthread.dylib`_pthread_start+0x88

It displays a massive output with exact function names and the executables 
that call them, which is real gold. Yet, DTrace shows us only the Kernel 
Space, and we do not see what data is being sent in the 
IOConnectCallMethod. So, there is one last step we need to do in User Space.

            ============================
--[ 2.2.3 - Driver Hooking in User Space
============================================================================
    Using DTrace, we can obtain system-wide information on executables and 
services they call. Now, we'll hook the user-space interfaces that 
ultimately trigger kernel interactions. These are IOServiceOpen and 
IOConnectCallMethod. It allows us to trace how processes open services and 
invoke methods without modifying the kernel or tracing the kernel handlers.

We can use LLDB to monitor calls to IOServiceOpen and IOConnectCallMethod 
to see which processes connect to which services using what types, selector 
IDs, argument buffers, and their sizes.

The IOServiceOpen(), as the first argument, uses service: io_service_t. The 
io_service_t is an alias of io_object_t, which is an alias of the 
mach_port_t. So we deal here with a number. This number is, in fact, 
io_registry_entry_t, which also comes down to mach_port_t. Like most  
things, it is a mach port! The point is we can use IORegistryEntryGetName 
on that mach port to get returns a C-string name assigned to a registry 
entry. This string is the Driver Name (or, in other words, the name of the 
instantiated service). It is a reverse lookup. 

To achieve this manually on a breakpoint, we could set a breakpoint in LLDB:

breakpoint set --name IOServiceOpen

Then, on a breakpoint, hit extracts the service name like this:

expr -l c -o -- extern int IORegistryEntryGetName(void*, char*); char name[128] = {0}; IORegistryEntryGetName((void *)$x0, name); name

To get the type, just read the x2 register:

reg read x2

I prepared the trace_ioserviceopen.py[36] script for that:

lldb -o "command script import trace_ioserviceopen.py" -o "setup_ioserviceopen --pid 81203"

Example output on breakpoint hit (when a process calls IOServiceOpen)

PID: 81203
EXE PATH: /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder
SERVICE: AppleAPFSContainer
TYPE: 0

This way, we know what executable calls what service with a proper name and 
the type of the user client spawned.

            ======
--[ 2.2.4 - Corpus
============================================================================
    The last step is to monitor external method calls by inspecting 
IOConnectCallMethod. By doing so, we can get exact information on what 
selector is used (what exposed function we call) and the exact data sent 
(which can be used for testing as the corpus for fuzzer). In LLDB, when we 
set the breakpoint on the IOConnectCallMethod, we can see the arguments in 
the x0-x9, so it is easy to extract each argument value. 

I made an iokit_dump.py[37] for that:

command script import ~/.lldb/iokit_dump.py
breakpoint set -n IOConnectCallMethod
iokit_dump

kern_return_t IOConnectCallMethod
-------------------------------------
mach_port_t               connection:        0x1d07
uint32_t                  selector:          0x0
const uint64_t *          input:             0x0
uint32_t                  inputCnt:          0x0
const void *              inputStruct:       0x600001310240
size_t                    inputStructCnt:    0x60
uint64_t *                output:            0x0
uint32_t *                outputCnt:         0x16fdfe994
void *                    outputStruct:      0x16fdfe998
size_t *                  outputStructCnt:   0x100068a20

Input Struct Hexdump (first 32 bytes):
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

However, the most handy function I made is iokit_dump. By using this, we 
can gather corpus for fuzzer:

(lldb) iokit_dump corpus.bin
Successfully dumped 96 bytes of inputStruct to 'corpus.bin'

            ======
--[ 2.2.5 - Tracer
============================================================================
One more thing. We can also upgrade the previously introduced 
trace_ioserviceopen.py to monitor the IOConnectCallMethod, so we have the 
whole picture of what process opens a connection to which service using 
what type of user client with what selector number, and finally, the proper 
sizes of the arguments. So here it is, the iokit_tracer.py[38]:

command script import iokit_tracer.py
trace_iokit --pid 81203
----------------------------------------------------------------
PID: 81203
EXE PATH: /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder
SERVICE: AppleAPFSContainer (Connection: 0x21267)
TYPE: 0
SELECTOR: 8 (0x8)
inputCnt: 0
inputStruct: 0x0
inputStructCnt: 0
output: 0x0
outputCnt: 0x0
outputStruct: 0x0
outputStructCnt: 0x0
----------------------------------------------------------------

trace_iokit --executable_path --executable_path crims0n -- -arg1 val1
----------------------------------------------------------------
PID: 8236 | EXE: /Users/karmaz/r/scripts/FUZZER/CRIMS0N/bin/crims0n
SERVICE: H11ANE (Connection: 0x1a07) | TYPE: 1
SELECTOR: 0 (0x0)
--- INPUT ---
input Scalars (2 values at 0x600000330020): [0x123, 0x123]
inputStruct (96 bytes at 0x600002534240):
  0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
  0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
  0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
  0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
  0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
  0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
--- OUTPUT ---
outputCnt: 0 (capacity pointer: 0x16b08e974)
outputStructCnt: 0 (capacity pointer: 0x104dd8a10)

This effectively finishes the runtime mapping part. How do we verify that 
the data we gather through all of these guides is valid, though?

          ================
--[ 2.3 - Map Verification
============================================================================
    Finally, a tool of the trade when it comes down to verification of our 
map. IOVerify[39]. I created it a long time ago as part of my fuzzer. I 
decided to extract the logic of it to make it a separate tool that can be 
used for testing external methods user clients types, matching services, 
etc. Bascially all of the things we learned here.

I recommend checking out the code. Here is the helper:

❯ ./IOVerify -h
Usage: ./IOVerify -n <name> (-m <method> | -y <spec>) [options]
Options:
  -n <name>      Target driver class name (required).
  -t <type>      Connection type (default: 0).
  -m <id>        Method selector ID.
  -y <spec>      Specify method and buffer sizes in one string.
                 Format: "ID: [IN_SCA, IN_STR, OUT_SCA, OUT_STR]"
                 Example: -y "0: [0, 96, 0, 96]"
  -p <string>    Payload as a string.
  -f <file>      File path for payload.
  -b <hex_str>   Space-separated hex string payload.
  -i <size>      Input buffer size (ignored if -y is used).
  -o <size>      Output buffer size (ignored if -y is used).
  -s <value>     Scalar input (uint64_t). Can be specified multiple times.
  -S <count>     Scalar output count (ignored if -y is used).
  -h             Show this help message.

And here is the most common usage so we can check if we enumerated properly 
a driver. For instance below, we check if we properly enumerated the 
service H11ANEIn that, we found handle type 1 User Client, and for selector 
0 it expects 96 for the inputStructCnt and outputStructCnt. As seen below, 
we can only "talk" to it when all of these values match. First invalid ones:

 ./IOVerify -n "H11ANEIn" -t 1 -y "0: [0,1,0,1]"
Starting verification for driver: H11ANEIn

--- [VERIFY] Event Log ---
Driver:          H11ANEIn
Connection Type: 1
Method Selector: 0
Result:          0xe00002c2 ((iokit/common) invalid argument)

--- Scalar I/O ---
Scalar In Cnt:   0
Scalar Out Cnt:  0

--- Structure I/O ---
Input Size:  1 bytes
Input Data:
00

Output Size: 1 bytes
Output Data:
00
--- End of Log ---

Now with valid sizes ovserve `0x0 ((os/kern) successful)`:

 ./IOVerify -n "H11ANEIn" -t 1 -y "0: [0,96,0,96]"
Starting verification for driver: H11ANEIn

--- [VERIFY] Event Log ---
Driver:          H11ANEIn
Connection Type: 1
Method Selector: 0
Result:          0x0 ((os/kern) successful)

--- Scalar I/O ---
Scalar In Cnt:   0
Scalar Out Cnt:  0

--- Structure I/O ---
Input Size:  96 bytes
Input Data:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Output Size: 96 bytes
Output Data:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00
--- End of Log ---
The tool can be used for testing corpus with the -f corpus.bin flag, and with a slight adjustment, it can serve as a fuzzer interface. ========== --[ 3 - Conclusion ============================================================================ In conclusion, the methodology presented in this paper for mapping IOKit external methods, which combines static analysis with runtime enumeration, offers an exhaustive approach to identifying the attack surface in macOS drivers. This process enables the precise enumeration of all available endpoints, establishing a solid foundation for effective fuzzing and vulnerability research. The application of the described tools and techniques—such as KEXT file analysis, user-space and kernel-space call tracing, and runtime hooking—increases not only the accuracy of the analysis but also significantly streamlines the process of discovering potential vulnerabilities. Future work could focus on fully automating this process with AI and adapting the methodology to new inter-kernel communication mechanisms introduced by Apple. ================ --[ 4 - Acknowledgements ============================================================================ Thank you to AFINE for supporting this research and enabling me to dedicate time to it. Your support significantly accelerated my progress! I also appreciate the macOS security research community for the inspiring exchange of ideas. You all are amazing! ========== --[ 5 - References ============================================================================ Here is the list of all resources used above in [REF_NUMBERS]: [0] Apple Developer Documentation: IOKit Fundamentals https://developer.apple.com/library/archive/documentation/ DeviceDrivers/Conceptual/IOKitFundamentals/ [1] Apple Developer Documentation: IORegistry https://developer.apple.com/library/archive/documentation/ DeviceDrivers/Conceptual/IOKitFundamentals/TheRegistry/TheRegistry.html [2] Apple Developer Documentation: IORegistryEntry Class Reference https://developer.apple.com/documentation/kernel/ioregistryentry [3] Apple Developer Documentation: IORegistry Planes https://developer.apple.com/library/archive/documentation/ DeviceDrivers/Conceptual/IOKitFundamentals/TheRegistry/TheRegistry.html #//apple_ref/doc/uid/TP0000014-CGGFIFEC [4] Apple Developer Documentation: IOService https://developer.apple.com/documentation/kernel/ioservice [5] Kernel Extensions on macOS https://karol-mazurek.medium.com/kernel-extensions-on-macos- 1b0f38b632ea?sk=v2%2Fb6920735-90f9-459c-9c10-30980247bae7 [6] Apple Open Source: OSKext::start() https://github.com/apple-oss-distributions/xnu/blob/ 1031c584a5e37aff177559b9f69dbd3c8c3fd30a/ libkern/c%2B%2B/OSKext.cpp#L8221 [7] Apple Developer Documentation: IOKitPersonalities https://developer.apple.com/documentation/bundleresources/ information-property-list/iokitpersonalities [8] Apple Developer Documentation: IOProviderClass https://developer.apple.com/documentation/bundleresources/ information-property-list/ioproviderclass [9] Apple Developer Documentation: IOService::probe https://developer.apple.com/documentation/kernel/ioservice/ 1810605-probe [10] Siguza's ioscan tool https://github.com/Siguza/iokit-utils?tab=readme-ov-file#ioscan [11] Apple Developer Documentation: IOUserClient https://developer.apple.com/documentation/kernel/iouserclient [12] Apple Developer Documentation: IOService::newUserClient https://developer.apple.com/documentation/kernel/ioservice/ t1810417-newuserclient [13] Apple Developer Documentation: IOServiceOpen https://developer.apple.com/documentation/iokit/1514515-ioserviceopen [14] Apple Developer Documentation: IOUserClient::externalMethod https://developer.apple.com/documentation/driverkit/iouserclient/ externalmethod [15] Apple Developer Documentation: IOConnectCallMethod https://developer.apple.com/documentation/iokit/ 1514240-ioconnectcallmethod [16] Apple Open Source: IOUserClient2022::dispatchExternalMethod https://fergofrog.com/code/codebrowser/xnu/iokit/Kernel/ IOUserClient.cpp.html#6452 [17] Analyzing macOS IONVMeFamily Driver Denial of Service Issue https://afine.com/case-study-analyzing-macos-ionvmefamily-driver -denial-of-service-issue/#reverse-engineering [18] Apple Developer Documentation: getTargetAndMethodForIndex https://developer.apple.com/documentation/kernel/ iouserclient/3553399-gettargetandmethodforindex [19] Apple Developer Documentation: Entitlements https://developer.apple.com/documentation/bundleresources/ entitlements [20] Snake & Apple II — Code Signing https://karol-mazurek.medium.com/snake-apple-ii-code-signing -f0a9967b7f02?sk=v2%2Fbbc87007-89ca-4135-91d6-668b5d2fe9ae [21] Snake & Apple VIII — App Sandbox https://karol-mazurek.medium.com/snake-apple-viii-app-sandbox -5aff081f07d5?sk=v2%2F5b65151b-d1f3-4f18-93da-4ad9aeacadb7 [22] Apple Developer Documentation: IONameMatch https://developer.apple.com/documentation/bundleresources/ information-property-list/ionamematch [23] Apple Developer Documentation: IOServiceGetMatchingServices https://developer.apple.com/documentation/iokit/ 1514494-ioservicegetmatchingservices?language=objc [24] Apple Developer Documentation: IORegistryEntryGetName https://developer.apple.com/documentation/iokit/ 1514323-ioregistryentrygetname [25] Apple Developer Documentation: IOServiceOpen https://developer.apple.com/documentation/iokit/ 1514515-ioserviceopen?language=objc [26] History of NULL Pointer Dereferences on macOS https://afine.com/history-of-null-pointer-dereferences-on-macos/ [27] Apple Developer Documentation: IOConnectCall* Methods https://developer.apple.com/search/ ?q=IOConnectCall&type=documentation [28] Kernel Extensions on macOS https://karol-mazurek.medium.com/kernel-extensions-on-macos -1b0f38b632ea?sk=v2%2Fb6920735-90f9-459c-9c10-30980247bae7 [29] Apple Developer Documentation: IOService::registerService https://developer.apple.com/documentation/kernel/ ioservice/1810726-registerservice [30] Blacktop ipsw tool https://blacktop.github.io/ipsw/docs/guides/macho/ [31] GNU c++filt tool https://www.ibm.com/docs/en/xl-c-and-cpp-aix/16.1.0 ?topic=names-demangling-compiled-c-cfilt [32] IDA Extension - print_externalmethods.py https://github.com/Karmaz95/Snake_Apple/commit/ 95752eefc7cfdd82c9e8e2adfdd2fa44bd53f215 [33] Siguza's ioscan https://github.com/Siguza/iokit-utils ?tab=readme-ov-file#ioscan [34] Example DTrace script: dtrace_NewUserClient.py https://github.com/Karmaz95/Snake_Apple/commit/ 18dfa39f42b7b2f9ba4e9009c4ccdf924d4c97c5 [35] Example DTrace script: dtrace_IOConnectCallMethod.py https://github.com/Karmaz95/Snake_Apple/commit/ 014ce2b5d503cbf3324ebacd0fb8a7235b6779a6 [36] Example LLDB script: trace_ioserviceopen.py https://github.com/Karmaz95/Snake_Apple/commit /b0439e72208038f546ad2230df5178edc3cd47f6 [37] Example LLDB script: iokit_dump.py https://github.com/Karmaz95/Snake_Apple/commit/ 8eb7589493aec280c605ab35814aac68ee72e348 [38] Example LLDB script: iokit_tracer.py https://github.com/Karmaz95/Snake_Apple/commit/ 2e208d662c70ae9068a4e43af2fb1b8c32735458 [38] IOVerify tool https://github.com/Karmaz95/Snake_Apple/commit/ 1a00625b0fa70d994b9ceab344274337279a4bae
[ News ] [ Issues ] [ Authors ] [ Archives ] [ Contact ]
© Copyleft 1985-2025, Phrack Magazine.