macOS 内核拓展与用户态进程的通信实现(二)

孙康

前面 已经讲解了基于 KernControl API 的通信实现,该实现相对简单,但有一些缺点。内核向应用层传输消息需调用ctl_enqueuedata接口,该接口实际将数据缓存至缓冲区,当需要瞬时大量传输消息时,缓冲区容量有限,将丢弃后来的数据。如果传输的数据有优先级,则需对ctl_enqueuedata接口进行二次封装,避免高优先级数据丢失。

IOKit Fundamentals  框架提供更加全面且方便的内核驱动 API。IOService 是大多数内核驱动的基类,提供驱动实例化相关的各项服务。IOUserClient 是与用户态应用程序间通信的基类,通过继承并实现该类,可与客户端应用程序建立通信机制。IOSharedDataQueue 是非常便于使用的内核与用户态进程进行数据交换的通用队列,用户可自行设置队列大小。基于 IOKit 中的 IOService、IOUserClient、IOSharedDataQueue 可方便的实现内核与用户进程的通信和数据传输。下面结合代码进行简单实现,更多代码细节和工程配置可参考 NuwaStone 项目。

IOService 内核编程

由于本次编写的为内核拓展,仅需要 IOService 进行驱动的加载与卸载管理,这里的代码实现很简单,仅需重写startstop方法。如有加载卸载时的自定义操作可在函数中实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DriverService.hpp

class DriverService : public IOService {
OSDeclareDefaultStructors(DriverService);

public:
// Called by the kernel when the kext is loaded
bool start(IOService *provider) override;

// Called by the kernel when the kext is unloaded
void stop(IOService *provider) override;

private:
void clearInstances();
};

IOUserClient 内核编程

编写继承于 IOUserClient 的类后需重写如下方法,相关源文件实现请参考开源项目。registerNotificationPortclientMemoryForType用于数据交换队列的配置,externalMethod配置对外函数调用接口,对外接口的函数原型如callYourMethod定义。

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
DriverClient.hpp

class DriverClient : public IOUserClient {
OSDeclareDefaultStructors(DriverClient);

public:
// Called as part of IOServiceOpen in clients.
bool initWithTask(task_t owningTask, void *securityID, UInt32 type) override;

// Called after initWithTask as part of IOServiceOpen.
bool start(IOService *provider) override;

// Called when this class is stopping.
void stop(IOService *provider) override;

// Called when a client manually disconnects (via IOServiceClose).
IOReturn clientClose(void) override;

// Called when a client dies.
IOReturn clientDied(void) override;

// Called during termination.
bool didTerminate(IOService *provider, IOOptionBits options, bool *defer) override;

// Called in clients with IOConnectSetNotificationPort. 用于数据传输
IOReturn registerNotificationPort(mach_port_t port, UInt32 type, UInt32 refCon) override;

// Called in clients with IOConnectMapMemory. 用于数据传输
IOReturn clientMemoryForType(UInt32 type, IOOptionBits *options, IOMemoryDescriptor **memory) override;

// Called in clients with IOConnectCallScalarMethod. 设置对外通信调用接口
IOReturn externalMethod(UInt32 selector, IOExternalMethodArguments *arguments, IOExternalMethodDispatch *dispatch, OSObject *target, void *reference) override;

// 自定义对外调用方法
static IOReturn callYourMethod(OSObject *target, void *reference, IOExternalMethodArguments *arguments);
};

IOKit 客户端编程

连接内核拓展前需先进行加载,加载调用KextManagerLoadKextWithIdentifierKextManagerLoadKextWithURL即可。内核拓展需在 plist 中配置IOServiceIOUserClient类名,在拓展启动后可通过类名进行查找匹配。首先需查找注册了指定类名的内核驱动,代码如下:

1
2
3
4
5
6
7
8
9
func startProvider() -> Bool {
guard let service = IOServiceMatching("your service name") else {
return false
}

Logger(.Info, "Wait for kext to be connected.")
waitForDriver(matchingDict: service)
return true
}

service 存放匹配成功的驱动字典,然后需要创建通信端口和处理队列进行处理连接请求。处理连接请求时所持有的 IOService 对象需注意释放。调用IOServiceOpen接口即可建立连接,后面的IOConnectCallScalarMethod表示调用驱动对外接口进行连接测试。函数返回前需将用于连接请求处理的端口释放。通过IOConnectCallScalarMethod或IOConnectCallStructMethod可调用驱动对外接口,其中ScalarMethod仅可传输有限数量的常量,StructMethod则可传输自定义结构体类型,相关驱动配置可参照 NuwaStone 项目。

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
func processConnectionRequest(iterator: io_iterator_t) {
repeat {
// 持有的对象需进行释放
let nextService = IOIteratorNext(iterator)
guard nextService != 0 else {
break
}

// 建立与内核驱动的连接
var result = IOServiceOpen(nextService, mach_task_self_, 0, &connection)
if result != kIOReturnSuccess {
Logger(.Error, "Failed to open kext service [\(String.init(format: "0x%x", result))].")
IOObjectRelease(nextService)
break
}

// 调用驱动方法测试连接
result = IOConnectCallScalarMethod(connection, kNuwaUserClientOpen.rawValue, nil, 0, nil, nil)
if result != kIOReturnSuccess {
Logger(.Error, "An error occurred while opening the connection [\(result)].")
IOObjectRelease(nextService)
break
}

IOObjectRelease(nextService)
IONotificationPortDestroy(notificationPort)
isConnected = true
Logger(.Info, "Connected with kext successfully.")
} while true
}

func waitForDriver(matchingDict: CFDictionary) {
var iterator: io_iterator_t = 0
let selfPointer = Unmanaged.passUnretained(self).toOpaque()
let notificationQueue = DispatchQueue(label: "your queue name")

let appearedCallback: IOServiceMatchingCallback = { refcon, iterator in
let selfPtr = Unmanaged<YourClassName>.fromOpaque(refcon!).takeUnretainedValue()
selfPtr.processConnectionRequest(iterator: iterator)
}

notificationPort = IONotificationPortCreate(kIOMasterPortDefault)
IONotificationPortSetDispatchQueue(notificationPort, notificationQueue)
IOServiceAddMatchingNotification(notificationPort, kIOMatchedNotification, matchingDict, appearedCallback, selfPointer, &iterator)
processConnectionRequest(iterator: iterator)
}

我们通过调用IOServiceOpen存放io_connect_t类型的对象建立了与内核驱动的连接,相应的,断开连接时需调用IOServiceClose接口。

1
2
3
4
5
6
7
8
9
10
11
func stopProvider() -> Bool {
let result = IOServiceClose(connection)
if result != KERN_SUCCESS {
Logger(.Error, "Failed to close IOService [\(String.init(format: "0x%x", result))].")
return false
}

connection = IO_OBJECT_NULL
isConnected = false
return true
}
  • Title: macOS 内核拓展与用户态进程的通信实现(二)
  • Author: 孙康
  • Created at : 2023-06-21 13:12:57
  • Updated at : 2023-08-31 19:56:25
  • Link: https://conradsun.github.io/2023/0699a600b0.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
macOS 内核拓展与用户态进程的通信实现(二)