Following up on OatmealDome's pairing program, I pulled up IOBluetooth in Ghidra. For anyone else looking to do this, searching for console strings in the binary is a very good way to find the code that printed that console log.
[IOBluetoothDevicePair peerDidRequestPairing:type:passkey:]
contains (approximately) the following code:
if (type == 6) {id adapter = [IOBluetoothDeviceCBPeerAdapter deviceFromClassicPeer:peer];os_log("peerDidRequestPairing: Pincode Pairing. Pincode received: %@", [passkey unsignedIntegerValue]);if ([self userDefinedPincode]) {os_log("%s: User-defined Passcode found.", "-[IOBluetoothDevicePair _peerDidRequestPairing:type:passkey:]");[self pinCodeRequest:adapter];} else {if (some other stuff) {some other stuff} else {os_log("peerDidRequestPairing: User has not previously provided a PINCode for device type %d & the peer \'%@\' that has no input capability. Displaying pairing options now.", [peer deviceType], peer);[self devicePairingFinished:adapter error:0xe00002bc];}}}
...pinCodeRequest:
calls devicePairingPINCodeRequest:
, so it looks like it now only sends that if userDefinedPincode
is true. Conveniently, they have getters and setters for that, so...
@interface IOBluetoothDevicePair()- (void)setUserDefinedPincode:(BOOL)enabled;@endIOBluetoothDevicePair* doPair(IOBluetoothDevice* device, id<IOBluetoothDevicePairDelegate> delegate) {IOBluetoothDevicePair* pair = [IOBluetoothDevicePair pairWithDevice:device];[pair setUserDefinedPincode:YES];[pair setDelegate:delegate];[pair start];}
and we get more fun logs, but no success:
IOBT-[IOBluetoothDevicePair _peerDidRequestPairing:type:passkey:]: User-defined Passcode found.
bluetoothdSending 'pincode request' pairing event for device 40:D2:8A:B9:DA:F1
bluetoothdReceived XPC message "CBClassicMsgIdPairingAgentRespondToPairing" from session "IOBT-555549440166398757ba342887c753e9932bab58-classic-99866-844"
bluetoothdhandlePairingAgentRespondToPairing: Pincode (received as a number) 0 for the device "<private>"
bluetoothdhandlePairingAgentRespondToPairing: Accepting Pairing Request with Pincode <private> for the device "<private>"
IOBT-[IOBluetoothCoreBluetoothCoordinator pairPeer:forType:withKey:] pair peer: <CBClassicPeer: 0x100e04080 774A203D-6851-EEA1-D597-C9FAE4044CA4, Nintendo RVL-CNT-01-UC, disconnected, Unpaired, 40:d2:8a:b9:da:f1, devType: 26, PID: 0x0330, VID: 0x057E> for type: 6 using key: 0
bluetoothdhandlePairingAgentRespondToPairing: pincode being used is <private>
bluetoothdSetting pincode <private> for device 40:D2:8A:B9:DA:F1
bluetoothd Enforcement complete with STATUS 705
bluetoothdPolicy enforcement failed STATUS 705 - cid 0x0000AC06, handle 32505856 securityFailed 1 (status=65535)
bluetoothdL2capControlConnectCfm: STATUS 705 (status=65535)
bluetoothdConnection to device 40:D2:8A:B9:DA:F1 failed - result was 705
bluetoothdReceived connection result for "HID Host" profile on device 40:D2:8A:B9:DA:F1 - result was 158
bluetoothdPairing failure reported for device 40:D2:8A:B9:DA:F1
bluetoothdUnblocking pairing for device 40:D2:8A:B9:DA:F1
bluetoothdCancelling existing pairing timeout event
bluetoothdPairing complete for device 40:D2:8A:B9:DA:F1
bluetoothdDevice 40:D2:8A:B9:DA:F1 ClassicSMPSupported:0 encryptionMode:0
bluetoothdpairingComplete result:158 device->isDerivedFromLE:0 connectionSupportsClassicSMP:0 fCTKDDisabled:0 isPendingClassicSMP:0 address:<private>
bluetoothdSending 'pairing complete' event for device 40:D2:8A:B9:DA:F1 with result 158
... lots of other messages as the failure makes its way back to the app
I checked the code that calculates the message for the "Enforcement complete with" message, and the error formatter returns "OI_STATUS_NONE"
for 0xffff
, which seems to match the header of this bluetooth library that's also used in Android: https://android.googlesource.com/platform/system/bt/+/8289925/embdrv/sbc/decoder/include/oi_status.h
If that is where this came from, 705 is OI_HCIERR_AUTHENTICATION_FAILURE
, which I assume means we sent the wrong pin.
So let's look at what happens when we call replyPINCode:PINCode:
...
- A number of checks are done on a subobject of
IOBluetoothDevicePair
, of classIOBluetoothDevicePairExpansion
. This includes checks forisWiiRemote
andisWiiUProController
, both of which are false in this case, but I assume this is the code that made it so wiimotes would just pair with older versions of macOS without any special applications. I checked the IOBluetooth dylib from Mojave, and this code is unchanged from back then. IfisWiiRemote
orisWiiUProController
is true, the correct key is calculated from the device address and used in place of the one supplied toreplyPINCode:PINCode:
. - Diverging from the Mojave code (which sent the code straight off to the kernel at this point), it calls
[[NSString alloc] initWithBytes:pincode length:8 encoding:NSUTF8Encoding]
. Sure hope that pincode was no more than 8 bytes long and valid UTF-8... - Then it calls
[CBUtil preSSPStringToParingCode:string]
which takes the string, converts it back to bytes, and memcpy's it in a 64-bit integer - Then
[NSNumber numberWithLongLong:integer]
, embedding it in an NSNumber - It calls
[[IOBluetoothCoreBluetoothCoordinator sharedInstance] pairPeer:[device classicPeer] forType:[pair currentPairingType] withKey:num]
- That wraps it up in an NSDictionary, adds some other stuff to it, and sends it over to bluetoothd using xpc
So what happens if we avoid the NSString-ification and call pairPeer:forType:withKey
directly?
@interface IOBluetoothCoreBluetoothCoordinator : NSObject+ (IOBluetoothCoreBluetoothCoordinator*)sharedInstance;- (void)pairPeer:(id)peer forType:(NSUInteger)type withKey:(NSNumber*)key;@end@interface IOBluetoothDevice()- (id)classicPeer;@end@interface IOBluetoothDevicePair()- (void)setUserDefinedPincode:(BOOL)enabled;- (NSUInteger)currentPairingType;@end@implementation MyPairingDelegate- (void)devicePairingPINCodeRequest:(id)sender {IOBluetoothDevicePair* pair = (IOBluetoothDevicePair*)sender;IOBluetoothDevice* device = [sender device];const BluetoothDeviceAddress* addr = [device getAddress];BluetoothPINCode code;memset(&code, 0, sizeof(code));for (int i = 0; i < 6; i++) {code.data[i] = addr->data[5 - i];}uint64_t num;memcpy(&num, code.data, sizeof(num));[[IOBluetoothCoreBluetoothCoordinator sharedInstance] pairPeer:[device classicPeer] forType:[pair currentPairingType] withKey:@(num)];// [pair replyPINCode:6 PINCode:&code];}@end
...and we get slightly closer
IOBT-[IOBluetoothDevicePair _peerDidRequestPairing:type:passkey:]: User-defined Passcode found.
bluetoothdSending 'pincode request' pairing event for device 40:D2:8A:B9:DA:F1
bluetoothdReceived XPC message "CBClassicMsgIdPairingAgentRespondToPairing" from session "IOBT-5555494430aa5ffb00af38bd93f2bb29e76b2953-classic-99983-847"
bluetoothdhandlePairingAgentRespondToPairing: Pincode (received as a number) 71273014745841 for the device "<private>"
<The rest is the same as above>
71273014745841 is 0x40D28AB9DAF1, which is the correct code (remember, this is little endian), it made it across this time. Sadly this still doesn't seem to be enough to pair.
I'm taking a break from this for now, if anyone else wants to look through bluetoothd code to see how it handles the code after this, I think that would be the next step. Or if anyone knows how to make bluetoothd not <private> its logs, that would also be helpful.