Sitemap

Drivers on macOS

Introduction to IOKit and BSD drivers on macOS

--

Press enter or click to view image in full size

INTRO

This article dives into the internals of XNU, specifically focusing on drivers. Since the main article on XNU has grown quite extensive, I decided to split this topic into its own pieces to keep things manageable — for you and me.

Here, you will learn what drivers do, how they are identified, and, most importantly, how we can interact with them from user mode.

Press enter or click to view image in full size

Enjoy!

Drivers

They are software that allows OS communication with hardware. Think of them as translators between the hardware that speaks its own language and the OS that needs to understand it, e.g., when we plug in a USB device:

  1. The hardware sends electrical signals
  2. The driver converts these signals into data the OS can understand
  3. The OS can then interact with the device through the driver

We encounter two main types of drivers on macOS: IOKit and BSD.

BSD Drivers

This older approach was inherited from Unix. They are accessed through files in /dev and follow the traditional Unix file I/O model (written in C):

// Opening a BSD device
int fd = open("/dev/mydrive", O_RDWR);
if (fd < 0) {
perror("Failed to open device");
return 1;
}

// Writing to device
char buffer[] = "Hello Device";
write(fd, buffer, strlen(buffer));

// Reading from device
char read_buffer[1024];
ssize_t bytes_read = read(fd, read_buffer, sizeof(read_buffer));

// Device control
struct my_ioctl_data data = {.value = 42};
int result = ioctl(fd, MY_IOCTL_CMD, &data);

// Closing device
close(fd);

Each device file in /dev represents a communication channel with a specific driver. We have there two types of BSD device files:

  • Character (c): Handle data as a stream of bytes
  • Block (b): Handle data in fixed-size blocks (typically storage devices)
Press enter or click to view image in full size
ls -l /dev

The table below shows all the devices we can find in /dev on macOS:

Press enter or click to view image in full size

An example open source implementation of a BSD character device is /dev/nsmb — we can find its code in SMBClient/kernel/netsmb/smb_dev.c

Network drivers do not use device nodes (/dev) for access. Access to a network device is typically initiated using the socket system call. See: bsd/net/.

BSD Driver Registration

BSD drivers register themselves with the kernel using a structure called cdevsw (character device switch) or bdevsw (block device switch).

The BSD subsystem structures still exist in XNU for backward compatibility and internal use but are not exposed to third-party driver development.

These structures contain function pointers the kernel calls when specific operations are requested. Here’s what BSD driver registration looks like:

#include <sys/conf.h>
#include <sys/kernel.h>

// Function prototypes for our driver
static int mydriver_open(dev_t dev, int flags, int devtype, struct proc *p);
static int mydriver_close(dev_t dev, int flags, int devtype, struct proc *p);
static int mydriver_ioctl(dev_t dev, u_long cmd, caddr_t data, int flags, struct proc *p);

// Define the driver's entry points
static struct cdevsw mydriver_cdevsw = {
.d_open = mydriver_open,
.d_close = mydriver_close,
.d_ioctl = mydriver_ioctl,
// Other entry points set to default handlers
.d_read = eno_read,
.d_write = eno_write,
};

// Major number for our device
static int mydriver_major;

// Initialize the driver
static int mydriver_init(void) {
// Allocate a major number dynamically
mydriver_major = cdevsw_add(-1, &mydriver_cdevsw);
if (mydriver_major == -1) {
printf("Failed to allocate major number\n");
return KERN_FAILURE;
}

return KERN_SUCCESS;
}

If you need to write a driver for macOS that interfaces with hardware or provides device functionality, you must use either IOKit or DriverKit.

BSD Device Interface Model

BSD drivers expose their functionality through several entry points:

// Basic entry points every BSD driver should implement
static int mydriver_open(dev_t dev, int flags, int devtype, struct proc *p) {
// Called when a process opens the device file
// dev: contains major and minor numbers
// flags: O_RDONLY, O_WRONLY, O_RDWR, etc.
return 0;
}

static int mydriver_close(dev_t dev, int flags, int devtype, struct proc *p) {
// Called when the last reference to the device is closed
return 0;
}

static int mydriver_ioctl(dev_t dev, u_long cmd, caddr_t data, int flags, struct proc *p) {
// Handle device-specific commands
switch (cmd) {
case MY_CUSTOM_COMMAND:
// Handle the command
break;
default:
return ENOTTY; // Inappropriate ioctl for device
}
return 0;
}

While we cannot create new BSD drivers, we can still interact with existing BSD drivers that are part of macOS through these standard interfaces.

ioctl(fd, CUSTOM_COMMAND, &data);  // Send commands
read(fd, buffer, size); // Read data
write(fd, buffer, size); // Write data
close(fd); // Close when done

Each driver implements its own command handler. We can talk to it using ioctl.

Example ioctl on macOS

Below, we use the BSD driver interface through an ioctl system call to communicate with a disk driver and send it DKIOCEJECT (eject command):

// clang DKIOCEJECT_ioctl.c -o DKIOCEJECT_ioctl
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <paths.h>
#include <sys/disk.h>

int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s /dev/diskX\n", argv[0]);
return 1;
}

// Open the disk device
int fd = open(argv[1], O_RDWR);
if (fd < 0) {
perror("Failed to open disk device");
return 1;
}

// Attempt to eject the disk using ioctl
if (ioctl(fd, DKIOCEJECT, NULL) < 0) {
perror("Failed to eject disk");
close(fd);
return 1;
}

close(fd);
printf("Disk ejected successfully\n");
return 0;
}

Before testing it, we must create and mount the disk and then unmount it but not detach it (we could also just plug in a flash disk and umount it).

Press enter or click to view image in full size
# Create a 10MB disk image
hdiutil create -size 10m -type UDIF -fs HFS+ -volname "TestDisk" testdisk.dmg
# Mount it
hdiutil attach testdisk.dmg
# Umount it
hdiutil unmount /dev/disk4s1

We can check if the disk is unmounted but still attached and finally use our DKIOCEJECT ioctl to eject it so disk completely disappears from the list:

Press enter or click to view image in full size
# Check if disk is unmounted
mount | grep TestDisk
# Check if disk is attached
diskutil list | grep disk4
# Eject it using DKIOCEJECT ioctl
./DKIOCEJECT_ioctl "/dev/disk4"

We need to unmount first because the system cannot safely release the device (eject) while the filesystem is still mounted.

Network Drivers

Although the primary network stack is implemented in bsd/net/, we cannot talk directly to drivers through ioctl. The actual drivers are implemented by IOKit and, on macOS, can be found in:

Press enter or click to view image in full size
/System/Library/Extensions/

We are not using the Character/Block devices from /dev. Instead, we are using a socket with an interface name to get the fd and then use ioctl:

// Create a UDP socket for interface communication
sockfd = socket(AF_INET, SOCK_DGRAM, 0);

// Copy the interface name (e.g. "en0") into the interface request structure
strncpy(ifr.ifr_name, "en0", IFNAMSIZ-1);

// Get the interface flags through ioctl
ioctl(sockfd, SIOCGIFFLAGS, &ifr);

Here is an example ifstatus program that uses the above code:

Press enter or click to view image in full size

The BSD network stack (in bsd/net/) uses IOKit’s kernel extensions (.kext files) as a bridge to communicate with hardware drivers. These drivers register with the networking subsystem through IOKit, allowing packets to travel from user mode through the BSD stack and finally to the hardware via the IOKit driver.

Filesystem Drivers

Similarly to Networks, Filesystem drivers operate at two levels:

Press enter or click to view image in full size
Press enter or click to view image in full size
# HFS | APFS | LIFS | NFS
ioreg | grep filesystems

The filesystem driver architecture demonstrates macOS’s hybrid design — combining Unix VFS with IOKit’s object-oriented driver framework.

IOKit

Unlike BSD drivers, IOKit provides an object-oriented framework (written in C++) for driver development on macOS. It is built on top of Mach, focusing on dynamic loading and matching drivers to hardware.

Think of IOKit as Apple’s newer, smarter way of managing how your Mac talks to all its connected devices. The key concept here is the device tree.

Device Family Tree

IOKit maintains a registry (IORegistry) of all devices in the system that is organized as a tree structure. Each node (IOService) represents either:

  • physical device
  • virtual device
  • nubs (attachment points for other devices)
  • driver that manages a device

For example, if you plug a USB hub into your Mac and then plug a keyboard and mouse into that hub, the family tree would look like this:

Press enter or click to view image in full size

Parents are the things other devices plug into. Children are the devices that get plugged in. Each branch shows how devices are connected to each other.

Driver Matching

IOKit automatically matches drivers to devices using:

Example matching dictionary in a driver’s Info.plist:

<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 include multiple personalities, allowing it to support different device types or hardware variants without needing separate drivers for each one.

Driver Structure

It is implemented as a class that inherits from an appropriate superclass. The MyUSBDriver inherits from IOService, so the system can interact with it as though it were an IOService object, it makes it dynamic.

// Example of a basic USB driver class
class MyUSBDriver : public IOService {
public:
// Called during driver initialization
// dictionary contains properties from the driver's Info.plist
virtual bool init(OSDictionary *dictionary = NULL) override;

// Called when the driver is starting up
// provider is the parent (usually an IOUSBDevice) in the device tree
virtual bool start(IOService *provider) override;

// Called when the driver is being terminated
virtual void stop(IOService *provider) override;

// Driver-specific methods for device communication (optional)
IOReturn sendCommand(uint8_t cmd, uint32_t value);
private:
// Pointer to the USB device we're driving
IOUSBDevice *fDevice;
};

Driver is loaded using OSKext::start. I described it on the AMFI example here.

IOKit Device Interface Model

Unlike BSD’s ioctl, IOKit uses IOUserClient to let user-space apps talk to drivers safely. A driver defines its interface by subclassing IOUserClient and implementing methods for different communication channels:

  • External methods: Most common way to send commands and data
  • Properties: For reading/writing driver configuration values
  • Notifications: For receiving asynchronous events from the driver
  • Shared memory: For efficient large data transfers
  • External traps: Legacy method (use external methods instead)
  • Shared Data Queue: For bidirectional queued data transfer
  • IOStream: For continuous data streaming
// Example implementation
class MyDriverUserClient : public IOUserClient {
public:
// Required: Handle initial connection from user-space
virtual bool initWithTask(task_t owningTask,
void* securityID,
UInt32 type) override;

// Required: Cleanup when user-space disconnects
virtual IOReturn clientClose() override;

// Optional: Handle external method calls (preferred way since Darwin 9)
virtual IOReturn externalMethod(uint32_t selector,
IOExternalMethodArguments* arguments,
IOExternalMethodDispatch* dispatch,
OSObject* target,
void* reference) override;

// Optional: Handle memory mapping requests
virtual IOReturn clientMemoryForType(UInt32 type,
IOOptionBits* options,
IOMemoryDescriptor** memory) override;

// Optional: Handle notifications setup
virtual IOReturn registerNotificationPort(mach_port_t port,
UInt32 type,
UInt32 refCon) override;

// Optional: For shared data queue support
IOSharedDataQueue* dataQueue;

// Optional: For continuous data streaming
IOStream* stream;

private:
// Initialize optional features in initWithTask() if needed
bool setupSharedQueue() {
dataQueue = IOSharedDataQueue::withCapacity(1024);
return dataQueue != nullptr;
}

bool setupStream() {
stream = IOStream::withBuffers(count, bufferSize);
return stream != nullptr;
}
};

External Methods must specify number of arguments, direction (input/output) and type of each argument (scalar/struct). See User-Space Interactions.

Talking to Driver

We have ioctl syscall to access driver methods in BSD. In IOKit, we can use IOKit.framework APIs to communicate with IOUserClient interfaces:

Press enter or click to view image in full size
// Example how talk to driver through different interfaces
int main() {
// 1. Find our driver
io_service_t service = IOServiceMatching("MyDriver");

// 2. Connect to it
io_connect_t connect;
IOReturn ret = IOServiceOpen(service, mach_task_self(),
kMyUserClientType, &connect);

// 3. Available communication methods:

// Method 1: External Methods - for command and control
// a) Send/receive simple numbers
uint64_t input = 42;
uint64_t output;
size_t outputCnt = 1;
ret = IOConnectCallScalarMethod(connect, 0, &input, 1, &output, &outputCnt);

// b) Send/receive structures
MyData inputStruct = {/* data */};
MyData outputStruct;
size_t outputSize = sizeof(outputStruct);
ret = IOConnectCallStructMethod(connect, 1, &inputStruct, sizeof(inputStruct),
&outputStruct, &outputSize);

// Method 2: Properties - for configuration and status
// Read/write key-value pairs through registry
CFStringRef key = CFSTR("MyProperty");
CFTypeRef value = CFSTR("MyValue");
ret = IORegistryEntrySetCFProperty(service, key, value);

// Method 3: Notifications - for async events from driver
mach_port_t notifyPort;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &notifyPort);
ret = IOConnectSetNotificationPort(connect, 0, notifyPort, 0);

// Method 4: Shared Memory - for high-performance data transfer
mach_vm_address_t address;
mach_vm_size_t size;
ret = IOConnectMapMemory(connect, 0, mach_task_self(), &address, &size,
kIOMapAnywhere);

// Method 5: External Traps - legacy method, limited to 6 arguments
ret = IOConnectTrap0(connect, 0); // No arguments
ret = IOConnectTrap1(connect, 0, someArg); // One argument
// ... up to IOConnectTrap6

// Method 6: Shared Data Queue - for bidirectional queued data transfer
IODataQueueMemory* queueMemory;
ret = IOConnectMapMemory(connect, kQueueType, mach_task_self(),
(mach_vm_address_t*)&queueMemory, &queueSize,
kIOMapAnywhere);

// Dequeue data from shared queue
IODataQueueEntry* entry;
size_t entrySize;
while (IODataQueueDequeue(queueMemory, entry, &entrySize) == kIOReturnSuccess) {
// Process entry data
}

// Method 7: IOStream - for continuous data streaming
IOStreamRef stream;
ret = IOStreamCreate(connect, &stream);

IOStreamBuffer* buffer;
while (IOStreamRead(stream, &buffer) == kIOReturnSuccess) {
// Process buffer->data
IOStreamBufferRelease(buffer);
}

// Cleanup
if (stream) {
IOStreamDestroy(stream);
}
if (queueMemory) {
IOConnectUnmapMemory(connect, kQueueType, mach_task_self(),
(mach_vm_address_t)queueMemory);
}
if (address) {
IOConnectUnmapMemory(connect, 0, mach_task_self(), address);
}
mach_port_deallocate(mach_task_self(), notifyPort);
IOServiceClose(connect);
}

External methods APIs are IOConnectCallMethod wrappers and eventually call it. Under the hood, everything is done over Mach IPC to IO Master Port.

IOConnectCallMethod

This is like ioctl for BSD. We will use it most of the time when talking to drivers. Below is the IOConnectCallMethod with additional comments:

kern_return_t IOConnectCallMethod(
io_connect_t connection, // connection handle
uint32_t selector, // method to call
const uint64_t *input, // scalar input values
uint32_t inputCnt, // number of scalar inputs
const void *inputStruct, // struct input
size_t inputStructCnt, // size of struct input
uint64_t *output, // scalar output values
uint32_t *outputCnt, // number of scalar outputs
void *outputStruct, // struct output
size_t *outputStructCnt // size of struct output
);

We will use it later in this article on the example driver exposed method.

Talking to a driver with Python

We can use the F-Secure LABS coralsun to call IOConnectCallMethod from Python (below code is a PoC for CVE-2020–3919):

import iokitlib

# IOHIDLibUserClient
iokit = iokitlib.iokit()
conn = iokit.open_service(b"IOHIDDevice",4737348)
print(conn)

# Open the device with selector 1.
selector = 1

input_scalar = [10]
input_struct = None
output_scalar = None
output_struct = None

kr = iokit.connect_call_method(conn,selector,input_scalar,input_struct,output_scalar,output_struct)
print(kr)

# Now pass an invalid port in of type 1.
iokit.ioconnect_setnotificationport(conn,1,0)
Press enter or click to view image in full size
Source

More examples in iokit_test.py

iOKit Drivers Recon on macOS

We can use ioreg command or IORegistryExplorer.app see the IORegistry. We can use planes(views) for various relationships between objects:

  1. IOService: General plane showing driver relationships
  2. CoreCapture: System event capturing and monitoring
  3. IO80211Plane: Wi-Fi functionality and dependencies
  4. IOAccessory: Accessory devices and their integration
  5. IODeviceTree: Physical device tree hierarchy
  6. IOPort: Communication ports and interfaces
  7. IOPower: Power management dependencies
  8. IOUSB: USB devices and their relationships
Press enter or click to view image in full size
ioreg -l         # List all
ioreg -w 0 # unlimited line width
ioreg -p <plane> # Check other plane (default is IOService)

See: IOKit Fundamentals — The I/O Registry. There is also ioclasscount to count instances of IO Classes and ioalloccount to summarize IOKit memory usage

Getting Driver Binaries

We can find a driver path by its bundleID with the below commands:

kextfind -bundle-id com.apple.iokit.IOBiometricFamily
kextfind -bundle-id -substring Bluetooth
Press enter or click to view image in full size

Driver binaries are packed in KernelCache. To learn how to get them, see:

With extracted binaries, we can also filter them for IOKit:

Press enter or click to view image in full size
ls | grep -i iokit

In this article, you will also learn how to properly load it to IDA.

Getting Entry Points

First, we check if the driver exposes any interface in IOUserClient:

Press enter or click to view image in full size
CrimsonUroboros -p DRIVER_BIN --symbols | grep "UserClient.*.externalMethod"
CrimsonUroboros -p DRIVER_BIN --symbols | grep "UserClient.*.setProperties"
CrimsonUroboros -p DRIVER_BIN --symbols | grep "UserClient.*.registerNotificationPort"
CrimsonUroboros -p DRIVER_BIN --symbols | grep "UserClient.*.clientMemoryForType"

Then, load the kernelcache to IDA and search for their code.

Reverse Engineering Entry Point

It depends on what entry points we are interested in. In the IDA, we should look for UserClient::* classes with entry point type we want to RE, e.g.:

Press enter or click to view image in full size
0xFFFFFE000995B940 __ZN25AppleJPEGDriverUserClient14externalMethodEjP31IOExternalMethodArgumentsOpaque

Here, we will reverse externalMethod that AppleJPEGDriver expose to the user trough UserClient. We are interested here in these three things:

Press enter or click to view image in full size

To begin, we must learn the structure returned by the externalMethod. As we can see, (1) it is IOUserClient2022::dispatchExternalMethod:

Press enter or click to view image in full size
iokit/IOKit/IOUserClient.h#L574

Looking at the above function, we can learn that our (2) stores an array of methods exposed for user mode, and (3) is the number of methods (0xA).

This is important because of how ugly this array looks in IDA. Still, it is usable, and we can see resolved pointers to functions, e.g., 0xFFFFFE0007E1B140:

Press enter or click to view image in full size
com.apple.driver.AppleJPEGDriver:__const:FFFFFE0007E1B118    EXPORT __ZN25AppleJPEGDriverUserClient22sIOExternalMethodArrayE

It is an array of IOExternalMethodDispatch2022 structures:

struct IOExternalMethodDispatch2022 {
IOExternalMethodAction function;
uint32_t checkScalarInputCount;
uint32_t checkStructureInputSize;
uint32_t checkScalarOutputCount;
uint32_t checkStructureOutputSize;
uint8_t allowAsync;
const char* checkEntitlement;
};

I made this IDA script that prints valid pointers for the exposed methods:

Press enter or click to view image in full size
# format_externalmethods.py <array_start_address> <count>
format_external_method_array(0xFFFFFE0007E1B118, 10)

We can use UI and double-click now on functions to get the implementation.

External Methods Example Analysis

No entitlement checks (checkEntitlement set to null) means any user process can call these methods, potentially exposing the attack surface.

  • Methods 0,2,8,9 are initialization methods with no data transfer
  • Methods 1,3 handle 88-byte structures
  • Methods 4,5 handle 4KB structures
  • Methods 6,7 handles 3.4KB structures

This interface exposes significant functionality with no access controls, including direct hardware access and large data transfers.

Example usage of IOConnectCallMethod

The functionality similar to ioctl provides IOConnectCallMethod. To call an exported function from user space, we do not need to call the function's name but the selector number. The below code shows a simple example:

#include <IOKit/IOKitLib.h>
#include <stdio.h>

int main() {
// Get service
io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleJPEGDriver"));

// Connect to service
io_connect_t connect;
kern_return_t kr = IOServiceOpen(service, mach_task_self(), 1, &connect);
IOObjectRelease(service);

// Call external method (selector 0 == method 0)
kr = IOConnectCallMethod(connect, 0, NULL, 0, NULL, 0, NULL, NULL, NULL, NULL);
printf("Method call result: 0x%x\n", kr);

// Cleanup
IOServiceClose(connect);
return 0;
}
Press enter or click to view image in full size

Here, we just called the external method exposed by the driver to user space. It was a simple example as it does not take any arguments:

Press enter or click to view image in full size

We can achieve the same thing using Python with coralsun:

import iokitlib

# Get service and connect
iokit = iokitlib.iokit()
conn = iokit.open_service(b"AppleJPEGDriver", 1)
print(f"Connection handle: {conn}")

# Call external method
kr = iokit.connect_call_method(conn, 0, None, None, None, None)
print(f"Method call result: {kr}")

IOKit exposed functions could perform additional security checks. Moreover, apps can be blocked by sandbox with IOKit-related operations like iokit-external-trap.

IOConnectCallMethod with Data

Here, we will talk to the exposed externalMethod with selector 1, which expects 88 bytes of data from the user mode:

Press enter or click to view image in full size

Actual decoding happens in returned (11) AppleJPEGDriver::startDecoder:

Press enter or click to view image in full size

From analyzing the decompiled code and reading this, it seems the driver expects an AppleJPEGDriverIOStruct structure that is used for both input and output. The structure may look something like this:

struct AppleJPEGDriverIOStruct {
uint32_t src_surface; // 0x00
uint32_t input_size; // 0x04
uint32_t dst_surface; // 0x08
uint32_t output_size; // 0x0C
uint32_t reserved1[2]; // 0x10
uint32_t pixelX; // 0x18
uint32_t pixelY; // 0x1C
uint32_t reserved2; // 0x20
uint32_t xOffset; // 0x24
uint32_t yOffset; // 0x28
uint32_t subsampling; // 0x2C
uint32_t callback; // 0x30
uint32_t reserved3[3]; // 0x34
uint32_t value100; // 0x40
uint32_t reserved4[2]; // 0x44
uint32_t decodeWidth; // 0x4C
uint32_t decodeHeight; // 0x50
uint32_t reserved5; // 0x54
} __attribute__((packed)); // Total: 0x58 (88) bytes

While building the correct structure, there will always be trials and errors. We can help ourselves by checking system logs, debugging other apps that talk to the driver, tracing kernel calls, and using descriptive errors:

sudo log stream | grep -i "AppleJPEGDriver"
ioreg -l -r -n "AppleJPEGDriver"
mach_error_string(result)

For example, while working on the correct structure for this interface (see commit), I encountered 0xe00002c2 code (invalid argument):

Press enter or click to view image in full size
// It was because I used 65535 instead of 88 bytes for the output size!
char output[65536];
size_t output_size = sizeof(output);

kern_return_t kr = IOConnectCallStructMethod(conn, 1, &input, sizeof(input), output, &output_size);

After patching this, we could observe new code 0xe00002d1 ((iokit/common) Media Error) and additional details in the logs:

Press enter or click to view image in full size

I uploaded the final version here, but it is still not fully working due to some image magic voodoo I did not want to dig into.

// Call IOKit method #1 on the connection, passing input/output buffers
// kr: stores kernel return code (KERN_SUCCESS = 0 on success)
// conn: connection to IOKit driver/service
// 1: selector/method number to call
// &input: pointer to input structure
// sizeof(input): size of input buffer in bytes
// output: buffer to receive output data
// &output_size: pointer to size of output buffer
kern_return_t kr = IOConnectCallStructMethod(conn, 1, &input, sizeof(input), output, &output_size);

However, reversing the proper structure here was not the main point. Just to show how to talk to the exposedMethod interface from user mode with data.

FINAL WORDS

While IOKit provides the foundation, modern macOS driver development has shifted toward DriverKit — Apple’s user-space driver framework that enhances security through privilege separation and memory isolation.

DriverKit drivers run with limited permissions outside the kernel, reducing the impact of potential vulnerabilities. More on that in the future!

References

In my opinion, the two best presentations about IOKit drivers:

Source
Source

While working on this article, I read some very good shorter and longer pieces about drivers from many cool security researchers. Here they are:

I also used some of Apple’s official documentation and various other manuals that should be referenced here:

The best book about IOKit: *OS Internals Volume II by Jonathan Levin:

Press enter or click to view image in full size

Self-references

Here are some of my articles connected to the driver subject. In the one about AMFI, you can read about the driver loading process during boot:

Sssssssssstay tuned.

--

--