iMessage逆向工程分析:利用硬件来保护软件

 

写在前面的话

iMessage是苹果生态系统中广泛使用的安全消息应用程序和协议。出于对在其他平台上运行iMessage的好奇,我们采用了逆向工程的方法来理解iMessage是如何运行的,并研究了将其扩展到其他平台的可能性。

本文的目的是探讨苹果如何利用其生产的硬件来保护其软件的事实。为了研究这一点,我们将尝试通过苹果推送通知(APN)直接在网络层面进行连接。在此过程中,我们将使用流行的开源工具对macOS上的apsd守护进程和APN协议本身的一小部分进行逆向工程分析。

 

关于iMessage

iMessage是苹果公司推出的即时通信软件,可以发送短信、视频等,其拥有非常高的安全性。不同于运营商短信/彩信业务,用户仅需要通过WiFi或者蜂窝数据网络进行数据支持,就可以完成通信。

利用 iMessage,你可以与任何使用 iPad、iPhone、iPod touch 的用户或运行 Mountain Lion 的 Mac 用户相互收发信息,还可以发送照片、视频、位置信息和联系人信息。如果你有不止一台 Apple 设备,iMessage 可以在所有设备上保持不间断的对话。而且,你可以通过蜂窝网络向其他手机发送文本信息、照片和视频。甚至还能让Siri帮你发文本信息。

 

当前的解决方案

当前在苹果生态系统之外运行iMessage的实际解决方案需要一台Mac服务器,并依赖AppleScript脚本来实现自动化Messages.app UI活动,这样就不需要在客户机上重新实现消息发送协议了。但无法避免的是,只要你想使用iMessage,就必须要有一台Mac设备随之运行。

跟只对一个自包含的二进制文件进行逆向分析不同的是,iMessage发送的代码(跟XNU Oses中大多数内部函数一样)已经超出了Messages.app的范围,其中还涉及到了很多系统守护进程。它有些类似于一种微服务体系结构,并且依赖于XPC消息来作为以这种IPC(进程间通信)机制。

Project Zero已经对iMessage中涉及的守护进程的结构进行了非常详尽的研究,因此相关内容我们在此不再进行赘述。简而言之,一旦我们在iMessage中键入一条消息并按下发送键,这条信息将会“流经”多个进程,即Messages.app -> imagent -> identityservicesd -> apsd。我编写了一个基于Frida的工具来剖析这个过程,但现在遇到了两个大问题。

首先,简单地在反汇编程序中静态查找ObjC方法太费时了,每个任务都有大量API调用和一层接一层的调用。因此,我编写了一个简单的Objective-C消息拦截器-【objtree】,它可以记录我感兴趣消息相关的所有信息。工具的输出以树状图形式提供。比如说,因为我知道某个UI事件方法触发了消息发送,所以我会使用我的工具来给该方法设置钩子,这样就能查看到后续所有的ObjC调用了。下面的例子中,objtree导出了超过3000个触发keyDown事件的选择器:

sudo objtree Messages -m "-[NSResponder keyDown:]"

其次,在找到最重要的ObjC方法之后,可以归结为向某个系统进程/守护进程发送XPC消息。我为这个任务编写了另一个工具-【xpcspy】,它可以拦截XPC消息并启用过滤。

最后,我们发现守护进程apsd负责通过网络发送消息。多亏了Objective-C的消息调度系统,我们们可以通过搜索具有connectTo和send等名称的选择器快速了解TCP连接API调用发生的位置。

 

跟苹果服务器通信

APN协议并不是新的协议,一些研究集中在它的安全性上,它也被称为PUSH。然而,该协议不再通过rfc5246中定义的CertificateRequest和certificate消息在传输层上发送客户端证书。相反,APN在应用层上以connect消息/命令的形式将其与公共令牌、nonce和签名一起发送。上述这些内容是在-[APSProtocolParser copyConnectMessageWithToken:state:presenceFlags:certificate:nonce:signature:redirectCount:lastConnected:disconnectReason:]方法中生成的。

这里的令牌参数非常重要,因为它起着用户标识符的作用,并且在协议保护机制中起着至关重要的作用,我们将在后面看到。由于APN客户端证书对每个设备都是唯一的,并且TLS加密发生在应用层,因此这样可以提供一种更安全的方法。传输层没有加密,因此可能会将证书公开给中间人攻击者。

与苹果服务器的第一个接触点发生在-[APSTCPStream _connectToServerWithPeerName:]方法中。在该方法中,存在TLS会话配置API调用,包括私有调用,比如说-[NSURLSessionConfiguration set_socketStreamProperties:]和-[NSURLSessionConfiguration set_tlsTrustPinningPolicyName:]。

最后,配置对象信息如下所示:

{
"_kCFStreamPropertyEnableConnectionStatistics" = 1;
"_kCFStreamPropertyNPNProtocolsAvailable" = (
"apns-security-v3",
"apns-pack-v1"
);
"_kCFStreamPropertyNoCompanion" = 1;
"_kCFStreamPropertyPrefersNoProxy" = 1;
"_kCFStreamSocketSetNoDelay" = 1;
kCFStreamPropertySSLSettings = {
kCFStreamSSLPeerName = "courier.push.apple.com";
kCFStreamSSLValidatesCertificateChain = 1;
};
}

我们现在的目标是打开一个与xx-courier.push.apple.com的连接。我们尝试使用openssl来打开一个TLS连接:

% openssl s_client -connect 12-courier.push.apple.com:5223 -quiet
depth=2 O = Entrust.net, OU = www.entrust.net/CPS_2048 incorp. by ref. (limits liab.), OU = (c) 1999 Entrust.net Limited, CN = Entrust.net Certification Authority (2048)
verify return:1
depth=1 C = US, O = "Entrust, Inc.", OU = See www.entrust.net/legal-terms, OU = "(c) 2012 Entrust, Inc. - for authorized use only", CN = Entrust Certification Authority - L1K
verify return:1
depth=0 C = US, ST = California, L = Cupertino, O = Apple Inc., CN = courier.push.apple.com
verify return:1
4548513452:error:14020410:SSL routines:CONNECT_CR_SESSION_TICKET:sslv3 alert handshake failure:/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-56.40.4/libressl-2.8/ssl/ssl_pkt.c:1200:SSL alert number 40
4548513452:error:140200E5:SSL routines:CONNECT_CR_SESSION_TICKET:ssl handshake failure:/AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/libressl/libressl-56.40.4/libressl-2.8/ssl/ssl_pkt.c:585:

握手失败了,查看上述的_kCFStreamPropertyNPNProtocolsAvailable键后,我们发现这里使用了NPN次协议协商协议。

NPN协议现在改名为了ALPN应用层协议协商,它是一种TLS的扩展,允许在安全连接的基础上进行应用层协议的协商。它可以告诉TLS服务器客户端希望使用哪个应用层协议。考虑到使用额外的TLS扩展,明智的做法是使用tcpdump记录并检查通信量。但首先,我们需要重新启动apsd,因为连接是在启动时发生的。launchctl使我们能够终止进程然后在调试器中生成守护程序:

% sudo launchctl attach -k system/com.apple.apsd

现在,我们让apsd停在了_dyld_start处:

Process 1925 stopped
* thread #1, stop reason = signal SIGSTOP
frame #0: 0x000000010b447000 dyld` _dyld_start
dyld`_dyld_start:
-> 0x10b447000 <+0>: pop rdi
0x10b447001 <+1>: push 0x0
0x10b447003 <+3>: mov rbp, rsp
0x10b447006 <+6>: and rsp, -0x10
0x10b44700a <+10>: sub rsp, 0x10
0x10b44700e <+14>: mov esi, dword ptr [rbp + 0x8]
0x10b447011 <+17>: lea rdx, [rbp + 0x10]
0x10b447015 <+21>: lea rcx, [rip - 0x101c]
Target 0: (apsd) stopped.
Executable module set to "/System/Library/PrivateFrameworks/ApplePushService.framework/apsd".
Architecture set to: x86_64h-apple-macosx-.
We'll start the packet recordin, then continue apsd's execution to record the connection:
% sudo tcpdump -i en0 -w /tmp/apsd.pcap
And:
(lldb) c

既然我们已经导出了通信流量,那我们来检查一下握手包:

接下来,我们可以尝试使用下列参数来跟服务器建立连接:

% openssl s_client -connect 11-courier.push.apple.com:5223 -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername courier.push.apple.com -quiet

depth=2 C = US, O = Apple Inc., OU = Apple Certification Authority, CN = Apple Root CA
verify error:num=19:self signed certificate in certificate chain
verify return:0

很好,太好了,现在我们已经连接到苹果的服务器了!如果省略-alpn或-servername参数,那么就会出现握手失败的情况。

 

拦截APN消息

现在我们需要截获未加密的TLS消息。在此之前,我们可以通过证书绑定等技术来轻松绕过APN。但现在我们选择在明文协议Payload被发送之气爱你拦截到它,这里可以通过在数据发送和接收方法上设置断点来拦截它。对应的函数分别为-[APSTCPStream writeDataInBackground:]和-[APSCourier tcpStream:dataReceived:]:

Process 1958 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000109a55d83 apsd` ___lldb_unnamed_symbol2607$$apsd
apsd`___lldb_unnamed_symbol2607$$apsd:
-> 0x109a55d83 <+0>: push rbp
0x109a55d84 <+1>: mov rbp, rsp
0x109a55d87 <+4>: push r15
0x109a55d89 <+6>: push r14
0x109a55d8b <+8>: push rbx
0x109a55d8c <+9>: sub rsp, 0x18
0x109a55d90 <+13>: mov rbx, rdi
0x109a55d93 <+16>: mov rax, qword ptr [rip + 0x924a6] ; (void *)0x00007fff88a98af0: __stack_chk_guard
Target 0: (apsd) stopped.
(lldb) po $rdx
<07ffef04 41203dfe ...lots of redacted data>

rdx 保存了针对NSData对象的引用,其中的字节数据将被写入到输出流中,同样的机制也应用到了在输入流中接收数据的场景下。

 

跟APN通信

既然我们已经拿到了通信连接和通信数据,那我们能通过APN进行通信吗?我们一起来尝试一下!

这里,我使用了一个FIFO来将输出数据提供给openssl:

% mkfifo /tmp/in
% openssl s_client -connect 12-courier.push.apple.com:5223 -tls1_2 -alpn apns-security-v3,apns-pack-v1 -servername courier.push.apple.com -quiet < /tmp/in > /tmp/out
And for reading response messages enter:
% xxd /tmp/out
00000000: 0822 0180 04a1 1400 0588 0683 08a9 3800 ."............8.
00000010: 0aa5 0176 1474 ee7b 0ca5 0176 1474 ee7b ...v.t.{...v.t.{

大家可以看到,connect响应信息中包含了响应码(0x08)和服务器时间以及其他的参数。

 

断开连接

APN是一个二进制协议,这些命令是在APSProtocolParser类中序列化的,不过我们对它的内部结构并不感兴趣。根据apsd中发生的情况,这是发送iMessage的最小命令序列:

·0x07: 使用uid 0与用户建立连接(每个用户都有其自己的公共push令牌)
·0x0c: 保持连接活动
·0x14: 激活通信状态
·0x07: 使用uid 501与用户建立连接
·0x09: 过滤主题
·0x0a: 发送消息

现在,我能够直接通过从apsd中拷贝二进制消息数据来从openssl发送一条纯iMessage消息了。在上述命令中,最有趣的就是(0x09: 过滤主题),过滤器消息是在-[APSProtocolParser copyFilterMessageWithEnabledHashes:ignoredHashes:opportunisticHashes:nonWakingHashes:pausedHashes:token:]方法中序列化的。参数中的哈希代表的是消息主题或使用了APN的服务。如果没有过滤器消息,客户端就无法通过(0x0a: 发送消息)来发送或接受APN消息了。因此,我们必须在发送消息之前调用过滤器命令。

 

总结

正如我们所看到的那样,在白盒尝试场景中,控制硬件对于保护协议来说是最基本的一个方面了。在此场景下,攻击者完全可以获取到软件的访问权限!

除此之外我们还可以看到,复制APN通信信息其实非常容易,但需要注意的是,过滤器命令将会导致服务器删除使用了同一公共令牌的任何以前的连接。现在我们提到服务器将删除同一公共令牌的连接,这是connect消息的关键参数。是否可以生成一个新的以绕过此限制呢?这一点留给同学们自己去思考吧!

 

参考资料

1、https://developer.apple.com/documentation/xpc?language=objc
2、https://googleprojectzero.blogspot.com/2020/01/remote-iphone-exploitation-part-1.html
3、https://github.com/hot3eed/objtree
4、https://en.wikipedia.org/wiki/Tree_(command)
5、https://github.com/hot3eed/xpcspy
6、https://blog.quarkslab.com/resources/2013-10-17_imessage-privacy/slides/iMessage_privacy.pdf
7、https://github.com/mfrister/pushproxy/blob/master/doc/apple-push-protocol-ios5-lion.md
8、https://tools.ietf.org/html/rfc5246#section-7.4.6
9、https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation
10、https://tools.ietf.org/html/rfc7301
11、https://github.com/mfrister/pushproxy#ios–7-os-x–109
12、https://github.com/mfrister/pushproxy/blob/master/doc/apple-push-protocol-ios5-lion.md#08-connect-response
13、https://en.wikipedia.org/wiki/Binary_protocol
14、https://www.nowsecure.com/contact/

(完)