原文地址:colemancda.github.io/2018/03/25/…
原文作者:github.io/colemancda
发布时间:2018年3月25日
我最近做了一个小研究项目,看看我是否可以通过HCI(主机控制器接口)命令来控制Mac上的蓝牙控制器,以便在macOS上复制PureSwift/BluetoothLinux的功能。由于我的PureSwift/Bluetooth已经实现了一些HCI命令,所以我认为这项工作只是打开一个蓝牙适配器的插座并发送字节的问题,这在Linux上是如何完成的。结果发现苹果的Darwin kernet更安全,不允许用户地代码打开socket直接和硬件对话,而是CoreBlueooth作为使用IOBluetooth框架的bluetoothd的代理,进而打开一个Mach端口到Kernel Extension(设备驱动),而这个Kernel Extension是和Kernel一起加载的。理论上,这应该比Linux更安全,因为即使有sudo这样的高权限,也无法直接与蓝牙硬件对话。以下是CoreBluetooth API的工作原理。
应用程序代码(加载动态库)
-> (通过ObjC) CoreBluetooth (CBCentralManager是CBXpcConnection的代理)
-> (通过XPC) com.apple.bluetoothd deamon with root permissions (Objective-C IOBluetooth.IOBluetoothHostController)
->(通过Mach端口)内核扩展/.kext(C++ IOKit.IOBluetoothHostController)。
以下是bluetoothd的回溯,说明了这一点"
Thread 0x2f5 DispatchQueue 1 1001 samples (1-1001)
priority 31-46 (base 31) cpu time 0.022
1001 start + 1 (libdyld.dylib + 21045) [0x7fff968f8235]
1001 ??? (blued + 187775) [0x105f0ad7f]
1001 -[NSRunLoop(NSRunLoop) run] + 76 (Foundation + 140218) [0x7fff82b5f3ba]
1001 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 277 (Foundation + 140514) [0x7fff82b5f4e2]
1001 CFRunLoopRunSpecific + 420 (CoreFoundation + 553236) [0x7fff8114c114]
993 __CFRunLoopRun + 1361 (CoreFoundation + 555201) [0x7fff8114c8c1]
993 __CFRunLoopServiceMachPort + 212 (CoreFoundation + 558132) [0x7fff8114d434]
993 mach_msg_trap + 10 (libsystem_kernel.dylib + 74570) [0x7fff96a1f34a]
*993 ipc_mqueue_receive_continue + 0 (kernel + 854224) [0xffffff80002d08d0]
8 __CFRunLoopRun + 2205 (CoreFoundation + 556045) [0x7fff8114cc0d]
8 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9 (CoreFoundation + 814025) [0x7fff8118bbc9]
8 _dispatch_main_queue_callback_4CF + 505 (libdispatch.dylib + 59656) [0x7fff968cf908]
8 _dispatch_mach_invoke + 868 (libdispatch.dylib + 25751) [0x7fff968c7497]
8 _dispatch_queue_serial_drain + 443 (libdispatch.dylib + 96219) [0x7fff968d87db]
8 _dispatch_mach_msg_invoke + 414 (libdispatch.dylib + 31129) [0x7fff968c8999]
8 _dispatch_client_callout4 + 9 (libdispatch.dylib + 30502) [0x7fff968c8726]
8 _xpc_connection_mach_event + 1707 (libxpc.dylib + 39263) [0x7fff96b4a95f]
8 _xpc_connection_call_event_handler + 35 (libxpc.dylib + 44950) [0x7fff96b4bf96]
4 ??? (blued + 551462) [0x105f63a26]
4 ??? (blued + 239559) [0x105f177c7]
4 _NSSetCharValueAndNotify + 260 (Foundation + 448025) [0x7fff82baa619]
4 -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 60 (Foundation + 27629) [0x7fff82b43bed]
4 -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:usingBlock:] + 944 (Foundation + 1579207) [0x7fff82cbe8c7]
4 NSKeyValueDidChange + 486 (Foundation + 274052) [0x7fff82b7fe84]
4 NSKeyValueNotifyObserver + 350 (Foundation + 275949) [0x7fff82b805ed]
4 ??? (blued + 112657) [0x105ef8811]
1 ??? (blued + 117061) [0x105ef9945]
1 -[BroadcomHostController BroadcomHCILEAddAdvancedMatchingRuleWithAddress:address:blob:mask:RSSIThreshold:packetType:matchingCapacity:matchingRemaining:] + 200 (IOBluetooth + 420408) [0x7fff83066a38]
1 sendRawHCIRequest + 246 (IOBluetooth + 344294) [0x7fff830540e6]
1 IOConnectCallStructMethod + 56 (IOKit + 29625) [0x7fff830ab3b9]
1 IOConnectCallMethod + 336 (IOKit + 29170) [0x7fff830ab1f2]
1 io_connect_method + 375 (IOKit + 531601) [0x7fff83125c91]
1 mach_msg_trap + 10 (libsystem_kernel.dylib + 74570) [0x7fff96a1f34a]
*1 hndl_mach_scall64 + 22 (kernel + 638390) [0xffffff800029bdb6]
*1 mach_call_munger64 + 456 (kernel + 2011608) [0xffffff80003eb1d8]
*1 mach_msg_overwrite_trap + 327 (kernel + 919415) [0xffffff80002e0777]
*1 ipc_kmsg_send + 225 (kernel + 835505) [0xffffff80002cbfb1]
*1 ipc_kobject_server + 412 (kernel + 980924) [0xffffff80002ef7bc]
*1 ??? (kernel + 1827576) [0xffffff80003be2f8]
*1 is_io_connect_method + 497 (kernel + 7259025) [0xffffff80008ec391]
*1 IOBluetoothHCIUserClient::externalMethod(unsigned int, IOExternalMethodArguments*, IOExternalMethodDispatch*, OSObject*, void*) + 257 (IOBluetoothFamily + 82363) [0xffffff7f81eb81bb]
*1 IOCommandGate::runAction(int (*)(OSObject*, void*, void*, void*, void*), void*, void*, void*, void*) + 314 (kernel + 7068058) [0xffffff80008bd99a]
*1 IOBluetoothHCIUserClient::SimpleDispatchWL(IOBluetoothHCIDispatchParams*) + 918 (IOBluetoothFamily + 83308) [0xffffff7f81eb856c]
*1 IOBluetoothHostController::SendRawHCICommand(unsigned int, char*, unsigned int, unsigned char*, unsigned int) + 2423 (IOBluetoothFamily + 327391) [0xffffff7f81ef3edf]
*1 IOBluetoothHCIRequest::Start() + 515 (IOBluetoothFamily + 114737) [0xffffff7f81ec0031]
*1 IOEventSource::sleepGate(void*, unsigned long long, unsigned int) + 83 (kernel + 7062579) [0xffffff80008bc433]
*1 IOWorkLoop::sleepGate(void*, unsigned long long, unsigned int) + 126 (kernel + 7057470) [0xffffff80008bb03e]
*1 lck_mtx_sleep_deadline + 147 (kernel + 1019715) [0xffffff80002f8f43]
*1 thread_block_reason + 222 (kernel + 1061566) [0xffffff80003032be]
*1 ??? (kernel + 1066139) [0xffffff800030449b]
*1 machine_switch_context + 206
值得庆幸的是,Objetive-C IOBluetoothHostController API是一个从macOS 10.2开始就存在的公共API,尽管它是运行在bluetoothd服务上的代码。这个Objective-C类可以根据官方API对你的蓝牙控制器进行一些基本操作。我使用Hopper反编译bluetoothd,发现它使用的是IOBluetoothHostController,这让我相信它使用的是我可以利用的私有API。通过类dump,我发现苹果通过IOBluetoothHostController上的私有方法实现了蓝牙4.2规范中所有的HCI命令。通过这些私有API改变Mac的蓝牙适配器的本地名称非常简单,不需要root访问或特殊权限。
IOBluetoothHostController *hciController = [IOBluetoothHostController defaultController]; // public API
unsigned char name[256];
name[0] = 'C';
name[1] = 'D';
name[2] = 'A';
[hciController BluetoothHCIWriteLocalName:&name]; // private API
如果你在蓝牙首选项中检查,或者用LightBlue Explorer这样的应用程序发现你的蓝牙适配器,你不太可能看到你的蓝牙本地名称改变,因为这些值是缓存的。苹果公司提供了PacketLogger和BluetoothExplorer,允许你调试你的硬件(需要提升权限才能运行)。
我不满足于让苹果来构建和发送人机交互命令,决定用我最喜欢的反编译器Hopper来看看有没有办法直接控制硬件。
open /System/Library/Frameworks/IOBluetooth.framework/Versions/A/IOBluetooth -a /Applications/Hopper\ Disassembler\ v4.app
int -[IOBluetoothHostController BluetoothHCIWriteLocalName:](void * self, void * _cmd, unsigned char[248] arg2) {
var_18 = arg2;
var_A0 = 0x4;
var_A4 = 0x10e1;
var_90 = &var_A4;
var_AC = _BluetoothHCIDispatchUserClientRoutine(&var_90, &var_94, &var_A0);
if (sign_extend_64((0x0 != var_AC ? 0x1 : 0x0) & 0x1 & 0xff) == 0x0) {
var_90 = &var_94;
var_AC = _BluetoothHCIDispatchUserClientRoutine(&var_90, 0x0, 0x0);
var_90 = &var_94;
_BluetoothHCIDispatchUserClientRoutine(&var_90, 0x0, 0x0);
}
rax = var_AC;
return rax;
}
大部分人机交互命令的私有方法都遵循一个模式,即需要通过Mach端口向IOKit服务发送数据,通过BluetoothHCIDispatchUserClientRoutine()转发到人机交互命令。
int _BluetoothHCIDispatchUserClientRoutine(int arg0, int arg1, int arg2) {
var_8 = arg0;
var_10 = arg1;
var_18 = arg2;
var_1C = _BluetoothHCISetupUserClient();
if (var_1C == 0x0) {
if ((var_18 != 0x0) && (*var_18 > 0x100000)) {
var_1C = 0xe00002c2;
}
else {
var_1C = IOConnectCallStructMethod(*(int32_t *)0x12da64, 0x0, var_8, 0x74, var_10, var_18);
if (var_1C != 0x0) {
if (var_1C > 0x64) {
_BluetoothHCIDecodeIOReturnError(var_1C);
}
else {
_BluetoothHCIDecodeError(var_1C);
}
}
}
}
rax = var_1C;
return rax;
}
经过一段时间的阅读汇编和suedocode,我发现IOBluetoothHostController有特定厂商的子类。
- AtherosHostController
- BroadcomHostController
- CSRBlueICEHostController
- CSRHostController
我以为苹果工程师这样做是为了提供不属于蓝牙标准的特定厂商人机交互命令。由于这些类是在Objective-C库中实现的,我假设他们不想修改C++ IOKit Kernel Extension库,因为它需要非常稳定,任何变化都有可能导致内核崩溃,更不用说C++存在类的脆弱性,如果他们在C++ Kernel Extensions层做了ABI破坏性的改变,一个为旧版macOS编译的Kext将无法与新版IOKit链接。我能够在苹果似乎在出货前用于测试Mac硬件的厂商HCI命令之一中找到大海捞针。
int _IOBluetoothCSRLibHCISendBCCMDMessage(int arg0) {
var_8 = **___stack_chk_guard;
var_18 = arg0;
var_21 = _IOBluetoothCSRLibGetBCCMDMessageLen(var_18);
var_30 = &stack[-136] - ((var_21 & 0xff) + 0x13 & 0x1f0);
var_32 = 0xfc00;
__memmove_chk(var_30, &var_32, 0x2, 0xffffffffffffffff);
*(int8_t *)(var_30 + 0x2) = (var_21 & 0xff) + 0x1;
*(int8_t *)(var_30 + 0x3) = 0xc2;
if ((var_21 & 0xff) > 0x0) {
__memmove_chk(var_30 + 0x4, var_18, var_21 & 0xff, 0xffffffffffffffff);
}
var_1C = _BluetoothHCIRequestCreate(&var_20, 0x1770, 0x0, 0x0);
if (var_1C == 0x0) {
var_1C = _BluetoothHCISendRawCommand(var_20, var_30, sign_extend_64((var_21 & 0xff) + 0x4));
sleep(0x1);
_BluetoothHCIRequestDelete(var_20);
}
var_74 = var_1C;
if (**___stack_chk_guard == var_8) {
rax = var_74;
}
else {
rax = __stack_chk_fail();
}
return rax;
}
int -[BroadcomHostController BroadcomHCILEAddAdvancedMatchingRuleWithAddress:address:blob:mask:RSSIThreshold:packetType:matchingCapacity:matchingRemaining:](void * self, void * _cmd, char arg2, struct BluetoothDeviceAddress * arg3, struct ? arg4, struct ? arg5, char arg6, unsigned char arg7, char * arg8, char * arg9) {
var_30 = **___stack_chk_guard;
var_A0 = arg5;
var_A8 = arg_38;
memset(&var_70, 0x0, 0x39);
var_70 = 0xfce9 & 0xffff & 0xff;
memset(&var_B1, 0x0, 0x3);
var_C0 = 0x3;
var_AC = _sendRawHCIRequest(&var_70, 0x39, &var_B1, &var_C0);
if (var_AC == 0x0) {
if (var_A0 != 0x0) {
*(int8_t *)var_A0 = var_B0;
}
if (var_A8 != 0x0) {
*(int8_t *)var_A8 = var_AF;
}
}
var_104 = var_AC;
if (**___stack_chk_guard == var_30) {
rax = var_104;
}
else {
rax = __stack_chk_fail();
}
return rax;
}
要发送一个原始的HCI命令,一个userland可执行文件必须执行下面的过程。
-
在IOKit服务驱动上分配一个HCI请求缓冲区,并获得它的标识符。
-
发送一个结构体,它的值将被用来作为参数在IOKit驱动上调用一个C++函数。在这种情况下,我们包括请求标识符,原始HCI命令数据,以及将被发送到控制器的命令数据的大小。在内核扩展中,IOBluetoothHCIController服务(一个C++对象)将从发送的结构中提取这些值,并在内核加载的实际驱动代码上调用一次其对应的C++方法(例如IOBluetoothHCIUserClient::DispatchHCIReadLocalName(),IOBluetoothHostController::SendRawHCICommand)。
-
最后,我们通过请求驱动程序删除其HCI命令请求缓冲区来清理我们在Kext中的操作。
虽然这个过程通常是由com.apple.bluetoothd deamon以提升权限来完成的,但似乎任何可执行文件都可以使用这个过程,而无需特殊的权限或权利。我更新了我的方法,用Objective-C API写蓝牙控制器的本地名称,直接使用BluetoothHCISendRawCommand()。
struct HCIRequest {
uint32 identifier;
};
- (void)writeName:(id)sender {
IOBluetoothHostController *hciController = [IOBluetoothHostController defaultController];
unsigned char name[256];
name[0] = 'C';
name[1] = 'D';
name[2] = 'A';
int setNameError = [hciController BluetoothHCIWriteLocalName:&name];
if (setNameError) {
NSLog(@"Error %@", @(setNameError));
return;
}
NSLog(@"BluetoothHCIWriteLocalName");
// manually via C function
struct HCIRequest request;
int error = BluetoothHCIRequestCreate(&request, 1000, nil, 0);
NSLog(@"Created request: %lu", request.identifier);
if (error) {
BluetoothHCIRequestDelete(request);
printf("Couldnt create error: %08x\n", error);
}
size_t commandSize = 248 + 3;
unsigned char command[commandSize];
memset(&command, '\0', commandSize);
command[0] = 0x13;
command[1] = 0x0C;
command[2] = 248;
command[3] = 'A';
command[4] = 'B';
command[5] = 'C';
error = BluetoothHCISendRawCommand(request, &command, commandSize);
if (error) {
BluetoothHCIRequestDelete(request);
printf("Send HCI command Error: %08x\n", error);
}
sleep(0x1);
BluetoothHCIRequestDelete(request);
}
我尝试用ObjC私有API和C函数读取接受连接超时参数,但BluetoothHCISendRawCommand()不允许HCI命令返回参数。
- (void)readConnectionTimeout:(id)sender {
IOBluetoothHostController *hciController = [IOBluetoothHostController defaultController];
uint16 connectionAcceptTimeout = 0;
int readTimeout = [hciController BluetoothHCIReadConnectionAcceptTimeout:&connectionAcceptTimeout];
if (readTimeout) {
NSLog(@"Error %@", @(readTimeout));
return;
}
// BluetoothHCIReadConnectionAcceptTimeout 2440
NSLog(@"BluetoothHCIReadConnectionAcceptTimeout %@", @(connectionAcceptTimeout));
// manually
struct HCIRequest request;
// No funcion argument of `BluetoothHCISendRawCommand()`
// allows for writing output values.
uint16 output;
size_t outputSize = sizeof(output);
int error = BluetoothHCIRequestCreate(&request, 1000, nil, 0);
NSLog(@"Created request: %u", request.identifier);
if (error) {
BluetoothHCIRequestDelete(request);
printf("Couldnt create error: %08x\n", error);
}
size_t commandSize = 3;
uint8 * command = malloc(commandSize);
command[0] = 0x15;
command[1] = 0x0C;
command[2] = 0;
error = BluetoothHCISendRawCommand(request, command, 3);
if (error) {
BluetoothHCIRequestDelete(request);
printf("Send HCI command Error: %08x\n", error);
}
sleep(0x1);
BluetoothHCIRequestDelete(request);
// BluetoothHCIReadConnectionAcceptTimeout 0
NSLog(@"BluetoothHCIReadConnectionAcceptTimeout %@", @(output));
}
在用PacketLogger验证命令被发送到控制器后,我需要找到一种方法来检索HCI命令的返回参数值。我的解决方案是创建另一个模仿BluetoothHCISendRawCommand()的C函数,并接受一个数据指针作为输出。
int _BluetoothHCISendRawCommand(int arg0, int arg1, int arg2) {
var_8 = arg0;
var_10 = arg1;
var_18 = arg2;
memset(&var_90, 0x0, 0x74);
if ((var_10 != 0x0) && (var_18 > 0x0)) {
var_90 = &var_8;
var_4 = _BluetoothHCIDispatchUserClientRoutine(&var_90, 0x0, 0x0);
}
else {
var_4 = 0xe00002c2;
}
rax = var_4;
return rax;
}
尽管对程序进行了逆向工程,但反编译后的psuedo代码只显示了传递给BluetoothHCIDispatchUserClientRoutine()的堆栈上的一个值,而没有显示它的大小或哪种结构。我能够推断出被发送的结构/数据的大小是0x74或116字节,因为在C语言中,通过memset()将结构或数据指针的内容设置为0来正确初始化结构或数据指针是标准做法。我也可以验证提供给BluetoothHCIDispatchUserClientRoutine()的数据总是116字节,因为这个值(0x74)是作为发送到IOKit服务的数据长度提供的。
int _BluetoothHCIDispatchUserClientRoutine(int arg0, int arg1, int arg2) {
var_8 = arg0;
var_10 = arg1;
var_18 = arg2;
var_1C = _BluetoothHCISetupUserClient();
if (var_1C == 0x0) {
if ((var_18 != 0x0) && (*var_18 > 0x100000)) {
var_1C = 0xe00002c2;
}
else {
var_1C = IOConnectCallStructMethod(*(int32_t *)0x12da64, 0x0, var_8, 0x74, var_10, var_18);
if (var_1C != 0x0) {
if (var_1C > 0x64) {
_BluetoothHCIDecodeIOReturnError(var_1C);
}
else {
_BluetoothHCIDecodeError(var_1C);
}
}
}
}
rax = var_1C;
return rax;
}
IOConnectCallStructMethod()用于调用蓝牙内核扩展的C++方法(也叫IOBluetoothHostController,但用C++实现,不是Objective-C),要求C++方法的参数和返回值以数据指针的形式提供。在网上搜索了一下IOBluetooth的漏洞,发现了一个调用IOBluetoothHostController的C++成员IOConnectCallStructMethod的例子。
struct BluetoothCall { // 120 bytes
uint64_t args[7];
uint64_t sizes[7];
uint64_t index;
};
struct BluetoothCall a;
int i;
int main(void) {
/* Finding vuln service */
io_service_t service =
IOServiceGetMatchingService(kIOMasterPortDefault,
IOServiceMatching("IOBluetoothHCIController"));
if (!service) {
return -1;
}
/* Connect to vuln service */
io_connect_t port = (io_connect_t) 0;
kern_return_t kr = IOServiceOpen(service, mach_task_self(), 0, &port);
IOObjectRelease(service);
if (kr != kIOReturnSuccess) {
return kr;
}
kr = IOConnectCallMethod((mach_port_t) port, /* Connection */
(uint32_t) 0, /* Selector */
NULL, 0, /* input, inputCnt */
(const void*) &a, /* inputStruct */
120, /* inputStructCnt */
NULL, NULL, NULL, NULL); /* Output stuff */
printf("kr: %08x\n", kr);
return IOServiceClose(port);
}
我假设BluetoothHCIDispatchUserClientRoutine()中发送的116个字节是一个BluetoothCall结构,最后一个成员是uint32而不是uint64。有了这些信息,我试图重新实现BluetoothHCISendRawCommand(),以便添加参数,允许HCI命令返回参数。虽然Hopper没有指定结构的哪些值被发送,但我能够通过LDB中BluetoothHCIDispatchUserClientRoutine()的符号断点来转储这个值来推断这个信息。
(lldb) memory read --size 8 --format 'A' --count 15 $arg1
0x7ffeefbfe9b0: 0x00007ffeefbfea38 -> 0x000000010000002c HCITool`_mh_execute_header + 44 // HCI command request ID pointer
0x7ffeefbfe9b8: 0x000060c000000640 // HCI command data pointer
0x7ffeefbfe9c0: 0x00007ffeefbfea28 // HCI command data size pointer
0x7ffeefbfe9c8: 0x0000000000000000
0x7ffeefbfe9d0: 0x0000000000000000
0x7ffeefbfe9d8: 0x0000000000000000
0x7ffeefbfe9e0: 0x0000000000000000
0x7ffeefbfe9e8: 0x0000000000000004 // size of uint32 (HCI request identifier)
0x7ffeefbfe9f0: 0x0000000000000003 // size of HCI command data
0x7ffeefbfe9f8: 0x0000000000000008 // size of HCI command data size value (`size_t`)
0x7ffeefbfea00: 0x0000000000000000
0x7ffeefbfea08: 0x0000000000000000
0x7ffeefbfea10: 0x0000000000000000
0x7ffeefbfea18: 0x0000000000000000
0x7ffeefbfea20: 0x000060c000000062 // IOKit Service Method Index
有了这些信息,我就可以重新实现蓝牙HCISendRawCommand()
int _BluetoothHCISendRawCommand(struct HCIRequest request,
void *commandData,
size_t commmandSize,
void *returnParameter,
size_t returnParameterSize) {
int errorCode = 0;
struct BluetoothCall call;
size_t size = 0x74;
memset(&call, 0x0, size);
if ((commandData != 0x0) && (commmandSize > 0x0)) {
// IOBluetoothHostController::
// SendRawHCICommand(unsigned int, char*, unsigned int, unsigned char*, unsigned int)
call.args[0] = (uintptr_t)&request.identifier;
call.args[1] = (uintptr_t)commandData;
call.args[2] = (uintptr_t)&commmandSize;
call.sizes[0] = sizeof(uint32);
call.sizes[1] = commmandSize;
call.sizes[2] = sizeof(uintptr_t);
call.index = 0x000060c000000062;
errorCode = BluetoothHCIDispatchUserClientRoutine(&call, returnParameter, &returnParameterSize);
}
else {
errorCode = 0xe00002c2;
}
return errorCode;
}
你可以在GitHub上找到源代码,地址是ColemanCDA/HCITool。
通过www.DeepL.com/Translator(免费版)翻译