什么是多点连接?



多点连接简单说就是将设备两两进行连接,从而组成一个网络,见下图:




多点连接可以基于如下两种通道建立:


 



即:蓝牙与WiFi, 且“只具有蓝牙的设备”可以与“只具有WiF的设备”通信, 这一切都是透明的,开发者根本不需要关心:




个人感觉它的能力还是比较强大的。 既然能力这么强大,它可以用来做什么呢? MC只是提供了一种数据通道,具体用途还是要看业务、看大家的想象力, 下面列几个比较常见的用途: 传文件聊天室一台设备作为数据采集外设(比如:摄像头),将实时数据导到另一台设备上网络数据转发...


多点连接 API 的使用

SDK及版本信息

 


MultipeerConnectivity.frameworkiOS 7.0OS X 10.10

 


可以看到基于MC可以做到电脑与手机的通信。 了解了其能力与SDK相关信息后,下面我们看看工作流程: 使设备可被发现--->浏览设备,建立连接--->传输数据 。 关于使用大家可以看看参考资源与 MCDemo, 这里只是做一个代码导读。


1、初始化 MCPeerID 及 MCSession, MCPeerID 用来唯一的标识设备, MCSession 是通信的基础:


-(         void         )setupPeerAndSessionWithDisplayName:(NSString *)displayName{        


                  _peerID = [[MCPeerID alloc] initWithDisplayName:displayName];        


                  


                  _session = [[MCSession alloc] initWithPeer:_peerID];        


                  _session.delegate = self;        


         }



2、广播设备,使设备可以被发现:


-(         void         )advertiseSelf:(BOOL)shouldAdvertise{        


                  if          (shouldAdvertise) {        


                  _advertiser = [[MCAdvertiserAssistant alloc] initWithServiceType:         @chat         -files        


                  discoveryInfo:nil        


                  session:_session];        


                  [_advertiser start];        


                  }        


                  else         {        


                  [_advertiser stop];        


                  _advertiser = nil;        


                  }        


         }



3、浏览“局域网”中的设备,并建立连接:


-(         void         )setupMCBrowser{        


                  _browser = [[MCBrowserViewController alloc] initWithServiceType:         @chat         -files session:_session];        


         }



MCBrowserViewController实例化后,直接弹出,这个类内部会负责查找设备并建立连接。 对于有界面定制化需求的,也可以通过相关接口实现类似的功能。


4、发送消息:

-(         void         )sendMyMessage{        


                  NSData *dataToSend = [_txtMessage.text dataUsingEncoding:NSUTF8StringEncoding];        


                  NSArray *allPeers = _appDelegate.mcManager.session.connectedPeers;        


                  NSError *error;        


                  


                  [_appDelegate.mcManager.session sendData:dataToSend        


                  toPeers:allPeers        


                  withMode:MCSessionSendDataReliable        


                  error:&error];        


                  


                  if          (error) {        


                  NSLog(@%@, [error localizedDescription]);        


                  }        


                  


                  [_tvChat setText:[_tvChat.text stringByAppendingString:[NSString stringWithFormat:         @I          wrote:        


         %@        


                  


         , _txtMessage.text]]];        


                  [_txtMessage setText:@];        


                  [_txtMessage resignFirstResponder];        


         }



发送消息时有个选项:MCSessionSendDataReliable,MCSessionSendDataUnreliable 但是不管是可靠还是不可靠,数据都是基于 UDP 进行传输的。


5、接收消息:


-(         void         )session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{        


                  NSDictionary *dict = @{         @data         : data,        


                  @peerID         : peerID        


                  };        


                  


                  [[NSNotificationCenter defaultCenter] postNotificationName:         @MCDidReceiveDataNotification        


                  object:nil        


                  userInfo:dict];        


         }



消息的接收是通过 MCSession 的回调方法进行的。 MCSession的回调方法非常重要, 设备状态的改变、消息的接收、资源的接收、流的接收都是通过这个回调进行通知的。



6、发送资源,资源可以是本地的URL,也可以是 Http 链接:


-(         void         )actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{        


                  if          (buttonIndex != [[_appDelegate.mcManager.session connectedPeers] count]) {        


                  NSString *filePath = [_documentsDirectory stringByAppendingPathComponent:_selectedFile];        


                  NSString *modifiedName = [NSString stringWithFormat:@%         @_         %@, _appDelegate.mcManager.peerID.displayName, _selectedFile];        


                  NSURL *resourceURL = [NSURL fileURLWithPath:filePath];        


                  


                  dispatch_async(dispatch_get_main_queue(), ^{        


                  NSProgress *progress = [_appDelegate.mcManager.session sendResourceAtURL:resourceURL        


                  withName:modifiedName        


                  toPeer:[[_appDelegate.mcManager.session connectedPeers] objectAtIndex:buttonIndex]        


                  withCompletionHandler:^(NSError *error) {        


                  if          (error) {        


                  NSLog(         @Error         : %@, [error localizedDescription]);        


                  }        


                  


                  else         {        


                  UIAlertView *alert = [[UIAlertView alloc] initWithTitle:         @MCDemo        


                  message:         @File          was successfully sent.        


                  delegate:self        


                  cancelButtonTitle:nil        


                  otherButtonTitles:         @Great         !, nil];        


                  


                  [alert performSelectorOnMainThread:         @selector         (show) withObject:nil waitUntilDone:NO];        


                  


                  [_arrFiles replaceObjectAtIndex:_selectedRow withObject:_selectedFile];        


                  [_tblFiles performSelectorOnMainThread:         @selector         (reloadData)        


                  withObject:nil        


                  waitUntilDone:NO];        


                  }        


                  }];        


                  


                  //NSLog(@*** %f, progress.fractionCompleted);        


                  


                  [progress addObserver:self        


                  forKeyPath:         @fractionCompleted        


                  options:NSKeyValueObservingOptionNew        


                  context:nil];        


                  });        


                  }        


         }



可以通过 NSProgress查询相关状态。


7、接收资源:



-(         void         )session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress{        


                  


                  NSDictionary *dict = @{         @resourceName           :   resourceName,        


                  @peerID                 :   peerID,        


                  @progress               :   progress        


                  };        


                  


                  [[NSNotificationCenter defaultCenter] postNotificationName:         @MCDidStartReceivingResourceNotification        


                  object:nil        


                  userInfo:dict];        


                  


                  


                  dispatch_async(dispatch_get_main_queue(), ^{        


                  [progress addObserver:self        


                  forKeyPath:         @fractionCompleted        


                  options:NSKeyValueObservingOptionNew        


                  context:nil];        


                  });        


         }




协议逆向



协议分析时,我们是基于WiFi进行分析,因为这样便于抓包。 抓到的数据包如下图:




可以看到主要是基于如下几个协议:




Bonjour在法语中是 Hello 的意思,即:主要用来做服务发现。 STUN主要用来做端口映射,便于两台设备直接建立连接。 剩下的两个协议未知:一个基于TCP,一个基于UDP。 基于 TCP 的,我们看下TCP Stream:




注意下图中红框部分:




这是某种握手机制,首先是交换设备ID,然后会基于Binary Plist 交换信息。 首先提取plist,提取plist时要参考 tcp stream 中的起始字节与结束字节, 将 plist 提出来后, 会看到一共交换了三个plist:


plist-1:




MCNearbyServiceInviteIDKey:MCEncryptionOption—>1, MCEncryptionNone—>0;


MCNearbyServiceMessageIDKey:序号


MCNearbyServiceRecipientPeerIDKey:接收者的PeerID


MCNearbyServiceSenderPeerIDKey:发送者的PeerID



plist-2:





MCNearbyServiceAcceptInviteKey:是否接收连接


MCNearbyServiceConnectionDataKey



plist-3:




MCNearbyServiceConnectionDataKey



如上只是说了plist的内容, 但是在 tcp stream 中我们还看到了设备ID, 设备ID是如何生成的呢? 通过代码逆向可以得到一个大概的结论: 设备ID在 -[MCPeerIDInternal initWithIDString:pid64:displayName:] 中实现, 基本策略是: IDString: 随机,base36pid64:随机displayName:外部传入,如:”Proteas-iPhone5s” 设备间交换ID时需要进行序列化, 序列化的方法为:-[MCPeerID serializedRepresentation] 总结起来就是:PeerID = 基于pid64生成前 9 byte + displayName 附反编译结果:


void          * -[MCPeerID initWithDisplayName:](         void          * self,          void          * _cmd,          void          * arg2) {        


                  STK33 = r5;        


                  STK35 = r7;        


                  sp = sp -          0x28         ;        


                  r5 = arg2;        


                  arg_20 = self;        


                  arg_24 = *         0x568f0         ;        


                  r6 = [[&arg_20          super         ] init];        


                  if          (r6 !=          0x0         ) {        


                  if          ((r5 ==          0x0         ) || ([r5 length] ==          0x0         )) {        


                  r0 = [r6          class         ];        


                  r0 = NSStringFromClass(r0);        


                  var_0 = r0;        


                  [NSException raise:*_NSInvalidArgumentException format:         @Invalid          displayName passed to %@];        


                  }        


                  else          {        


                  if          ([r5 lengthOfBytesUsingEncoding:         0x4         ] >=          0x40         ) {        


                  r0 = [r6          class         ];        


                  r0 = NSStringFromClass(r0);        


                  var_0 = r0;        


                  [NSException raise:*_NSInvalidArgumentException format:         @Invalid          displayName passed to %@];        


                  }        


                  }        


                  arg_8 = r6;        


                  arg_C = r5;        


                  r8 = CFUUIDCreate(*_kCFAllocatorDefault);        


                  CFUUIDGetUUIDBytes(&arg_10);        


                  r11 = (arg_1C ^ arg_14) <<          0x18          | (arg_1C ^ arg_14) &          0xff00          |          0xff00          & (arg_1C ^ arg_14) | arg_1C ^ arg_14;        


                  r10 =          0xff00          & (arg_10 ^ arg_18) | ((arg_10 ^ arg_18) &          0xff00         ) <<          0x8          | arg_10 ^ arg_18 | arg_10 ^ arg_18;        


                  r5 = _makebase36string(r11, r10);        


                  if          (*_gVRTraceErrorLogLevel <          0x6         ) {        


                  asm{ strd       r4, r5, [sp] };        


                  VRTracePrint_();        


                  }        


                  else          {        


                  if          (*(int8_t *)_gVRTraceModuleFilterEnabled !=          0x0         ) {        


                  asm{ strd       r4, r5, [sp] };        


                  VRTracePrint_();        


                  }        


                  }        


                  r4 = [NSString stringWithUTF8String:r5];        


                  free(r5);        


                  CFRelease(r8);        


                  r0 = [MCPeerIDInternal alloc];        


                  var_0 = r10;        


                  arg_4 = arg_C;        


                  r0 = [r0 initWithIDString:r4 pid64:r11 displayName:STK-         1         ];        


                  r6 = arg_8;        


                  r6->_internal = r0;        


                  }        


                  r0 = r6;        


                  Pop();        


                  Pop();        


                  Pop();        


                  return          r0;        


         }        


                  


         [[MCPeerIDInternal alloc] initWithIDString:_makebase36string(...) pid64:r11 displayName:STK-         1         ]



前面的 plist 中有 Data Key,我们没有做过多说明, 接下来我们大概看看 Data Key 的生成:




在初始化一个多点连接的 Session 时,我们可以指定

加密

方式, 这个加密方式是个枚举类型: MCEncryptionOptional = 0


MCEncryptionRequired = 1


MCEncryptionNone = 2


从上图可以看出加密方式会影响Data Key, 但是完全通过抓包来分析 Data Key 是比较耗时的, 而且很可能会有遗漏。 通过代码逆向,我们找到负责 Data Key 生成的类:




这里可以作为分析 Data Key 的起点, 有需要的兄弟可以进行深入分析。


上面我们都是在说基于 TCP 的未知协议, 接下来我们看看基于 UDP 的未知协议。 UDP数据流:




具体一个UDP数据包:




可以看出它是在 DTLS 之上做了封装, 我们只要抛弃到 0xd0 就可以让 Wireshark 进行识别分析。 这里需要说下 BH-US 大会上没有公布具体的工具与方法, 我处理的方法是写一个 Custom Protocol Dissector:


-- Apple Mutipeer Connectivity Custom DTLS Protocl        


                  


         -- cache globals to local          for          speed.        


         local format = string.format        


         local tostring = tostring        


         local tonumber = tonumber        


         local sqrt = math.sqrt        


         local pairs = pairs        


                  


         -- wireshark API globals        


         local Pref = Pref        


         local Proto = Proto        


         local ProtoField = ProtoField        


         local DissectorTable = DissectorTable        


         local Dissector = Dissector        


         local ByteArray = ByteArray        


         local PI_MALFORMED = PI_MALFORMED        


         local PI_ERROR = PI_ERROR        


                  


         -- dissectors        


         local dtls_dissector = Dissector.get(dtls)        


                  


         apple_mcdtls_proto = Proto(apple_mcDTLS, Apple Multipeer Connectivity DTLS, Apple Multipeer Connectivity DTLS Protocol)        


         function apple_mcdtls_proto.dissector(buffer, pinfo, tree)        


                  local mctype = buffer(         0         ,          1         ):uint()        


                  if          mctype ==          208          then        


                  pinfo.cols.protocol = AppleMCDTLS         


                  pinfo.cols.info = Apple MC DTLS Payload Data         


                  local subtree = tree:add(apple_mcdtls_proto, buffer(), Apple MC DTLS Protocol)        


                  subtree:add(buffer(         0         ,          1         ),Type:  .. buffer(         0         ,          1         ):uint())        


                  local size = buffer:len()         


                  subtree:add(buffer(         1         , size -          1         ), Data:  .. tostring(buffer))        


                  dtls_dissector:call(buffer(         1         ):tvb(), pinfo, tree)        


                  end        


         end        


                  


         local function unregister_udp_port_range(start_port, end_port)        


                  if          not start_port or start_port <=          0          or not end_port or end_port <=          0          then        


                  return        


                  end        


                  udp_port_table = DissectorTable.get(udp.port)        


                  for          port = start_port,end_port          do        


                  udp_port_table:remove(port, apple_mcdtls_proto)        


                  end        


         end        


                  


         local function register_udp_port_range(start_port, end_port)        


                  if          not start_port or start_port <=          0          or not end_port or end_port <=          0          then        


                  return        


                  end        


                  udp_port_table = DissectorTable.get(udp.port)        


                  for          port = start_port,end_port          do        


                  udp_port_table:add(port, apple_mcdtls_proto)        


                  end        


         end        


                  


         register_udp_port_range(         16400         ,          16499         )



在 Wireshark 中使用自定义协议进行处理后:




这里识别出协议后,我们不做继续分析, 但是评估安全性时,比如在手机上 kill 调 ssl 后, 可以在 DTLS 的 Payload 中看到明文数据。


安全性分析



前文中也提到了,安全性的控制是在初始化 MCSession 时控制的, 默认是使用 MCEncryptionOptional, 但是当有一方是 MCEncryptionNone 时会发生降级,即:通信不加密。




但是当双方都是 MCEncryptionOptional,通信也是不安全的, 可能发生中间人攻击:




实施中间人攻击首先要识别出基于 TCP 一些数据包, 如上图中的浅色部分,数据包都是有特点的, 因此是可以识别的。 但是没有演示中间人攻击的原因是, plist文件中的数据貌似是有关联关系,简单的将0改为1, 并不会将 false 改成 true,会造成 plist 无效, 因此实施中间人攻击时可能需要将整个 plist 都截获后, 修改,再发送。



其他


目前没有逆向出整个通信协议,但是如果想将一些外设模拟成 MC 设备,需要进一步逆向出整个协议。MultipeerConnectivity 链接了 IOKit,因此可能间接得暴露出 IOKit 的攻击面。