http://www.2cto.com/kf/201411/352716.html
什么是多点连接?
多点连接简单说就是将设备两两进行连接,从而组成一个网络,见下图:
多点连接可以基于如下两种通道建立:
即:蓝牙与WiFi, 且“只具有蓝牙的设备”可以与“只具有WiF的设备”通信, 这一切都是透明的,开发者根本不需要关心:
个人感觉它的能力还是比较强大的。 既然能力这么强大,它可以用来做什么呢? MC只是提供了一种数据通道,具体用途还是要看业务、看大家的想象力, 下面列几个比较常见的用途: 传文件聊天室一台设备作为数据采集外设(比如:摄像头),将实时数据导到另一台设备上网络数据转发...
多点连接 API 的使用
SDK及版本信息
MultipeerConnectivity.frameworkiOS 7.0OS X 10.10
可以看到基于MC可以做到电脑与手机的通信。 了解了其能力与SDK相关信息后,下面我们看看工作流程: 使设备可被发现--->浏览设备,建立连接--->传输数据 。 关于使用大家可以看看参考资源与 MCDemo, 这里只是做一个代码导读。
1、初始化 MCPeerID 及 MCSession, MCPeerID 用来唯一的标识设备, MCSession 是通信的基础:
1
2
3
4
5
6
|
-(
void
)setupPeerAndSessionWithDisplayName:(NSString *)displayName{
_peerID = [[MCPeerID alloc] initWithDisplayName:displayName];
_session = [[MCSession alloc] initWithPeer:_peerID];
_session.delegate = self;
}
|
2、广播设备,使设备可以被发现:
1
2
3
4
5
6
7
8
9
10
11
12
|
-(
void
)advertiseSelf:(BOOL)shouldAdvertise{
if
(shouldAdvertise) {
_advertiser = [[MCAdvertiserAssistant alloc] initWithServiceType:
@chat
-files
discoveryInfo:nil
session:_session];
[_advertiser start];
}
else
{
[_advertiser stop];
_advertiser = nil;
}
}
|
3、浏览“局域网”中的设备,并建立连接:
1
2
3
|
-(
void
)setupMCBrowser{
_browser = [[MCBrowserViewController alloc] initWithServiceType:
@chat
-files session:_session];
}
|
MCBrowserViewController实例化后,直接弹出,这个类内部会负责查找设备并建立连接。 对于有界面定制化需求的,也可以通过相关接口实现类似的功能。
4、发送消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
-(
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、接收消息:
1
2
3
4
5
6
7
8
9
|
-(
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 链接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
-(
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、接收资源:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
-(
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 附反编译结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
-- 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 都截获后, 修改,再发送。