长亭百川云 - 文章详情

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

Proteas的专栏

33

2024-07-19

参考资源

  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 的攻击面。
相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2