首页 > 代码库 > iOS多点连接的使用、协议逆向、安全性

iOS多点连接的使用、协议逆向、安全性

参考资源

  1. WWDC-2013-Session-708

  2. BlackHat-US-2014-“It Just (Net)works”

  3. Understanding Multipeer Connectivity Framework in iOS 7 - Part 1 & 2

  4. MCDemo.zip: https://dl.dropboxusercontent.com/u/2857188/MCDemo.zip


什么是多点连接?


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



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

    

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



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

多点连接 API 的使用


SDK及版本信息

  1. MultipeerConnectivity.framework
  2. iOS 7.0
  3. OS 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:\n%@\n\n", _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:] 中实现,
基本策略是:
  1. IDString: 随机,base36
  2. pid64:随机
  3. 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 时,我们可以指定加密方式,
这个加密方式是个枚举类型:
  1. MCEncryptionOptional = 0
  2. MCEncryptionRequired = 1
  3. 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 都截获后,
修改,再发送。


其他

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

iOS多点连接的使用、协议逆向、安全性