打开网易新闻 查看更多图片

0x00 概述

iMessage是整个Apple生态系统中广泛使用的安全消息传递应用程序和协议。由于对在其他平台上运行的iMessage原理感到好奇,我们使用了一种逆向工程的方式来探究iMessage的运行方式,并研究将其扩展到其他平台的可能性。

本文的目标是说明Apple如何利用其硬件产品来保护其软件。为了对此进行探讨,我们将尝试通过Apple Push Notification(APN)直接在网络级别进行连接,并解决遇到的问题。在这个过程中,我们将使用流行的开源工具对macOS上apsd守护程序的一部分以及APN协议自身进行逆向工程。

0x01 当前解决方案

在当前,如果要在Apple生态系统之外运行iMessage,就必须使用Mac服务器,并依靠AppleScript脚本来自动执行Messages.app UI操作。这样一来,就无需在客户端上重新实现消息发送协议。但是,这里最大的问题在于,如果要使用iMessage,则Mac必须一直运行。

与针对独立二进制文件进行逆向有所不同,iMessage发送代码(像XNU OS中大多数内部函数一样)已经超出了Messages.app的范围,并且该进程依赖于许多系统守护程序,即微服务体系结构,并且它们依赖XPC消息作为IPC(进程间通信)机制。

Project Zero已经对iMessage中涉及的守护程序结构进行了充分的研究,因此在这篇文章中我们将省略不必要的细节。简而言之,如果我们编辑好一条消息,并按下回车键,就会经历Messages.app -> imagent -> identityservicesd -> apsd的过程。为了进一步对这个过程进行分析,我编写了两个基于Frida的工具,并在此过程中遇到了两大挑战。

首先,在反汇编程序中静态查找ObjC方法非常耗时。每个人物都有大量的API调用和复杂的层次结构。我编写了一个简单的Objective-C消息拦截工具objtree,利用它记录我关注的所有消息。输出是以树状形式展现。例如,我知道某个UI事件方法会触发消息发送,因此我使用工具对该方法进行挂钩,并查看所有后续的ObjC调用,这些调用都采用了可识别栈深度的格式。下面是在触发keyDown事件时objtree随机转储的3000个选择器:

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

接下来,在找到最重要的ObjC方法后,可以将其归纳为向某个系统进程/守护程序发送XPC消息。我为此编写了另一个工具xpcspy,它可以拦截XPC消息并启用过滤。

Xpcspy拦截到守护进程apsd的消息:

打开网易新闻 查看更多图片

最后,我们发现守护程序apsd负责通过网络发送消息。借助Objective-C的消息分发系统,搜索其名称类似于connectTo和send的选择器,我们可以迅速找到TCP连接API调用发生的位置。

radare2搜索Objective-C选择器:

0x02 与Apple通信服务器

APN协议是一个常见的协议,简称为PUSH,目前已经有一些针对其安全性的研究成果。根据一些研究成果,该过程是通过TLS协议,使用5223端口,连接到域名rand(0,255)-courier.push.apple.com(前面为0-255的随机数字),且客户端证书被用于进行TLS层的身份验证。

但是,该协议现在已经不再通过RFC 5246中定义的CertificateRequest和Certificate在消息传输层上发送客户端证书。取而代之的是,APN在应用层将连接信息/命令与公共token、随机数和签名一起发送。

它们是在方法-[APSProtocolParser copyConnectMessageWithToken:state:presenceFlags:certificate:nonce:signature:redirectCount:lastConnected:disconnectReason:]中生成的。

其中的token参数非常重要,因为它起到了用户标识符的作用,并且在协议保护机制中起到了至关重要的作用,我们将在后面看到。

因为APN客户端证书对于每个设备都是唯一的,并且TLS加密是在应用层中进行,因此这提供了一种更安全的方法。传输层未加密,可能会将证书公开给中间人。

与Apple服务器的第一次通信发生在-[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密钥,我们看到正在使用次协议协商(Next Protocol Negotiation,NPN)。

NPN现在被称作应用层协议(ALPN),是ClientHello消息中嵌入的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 : pop rdi

0x10b447001 : push 0x0

0x10b447003 : mov rbp, rsp

0x10b447006 : and rsp, -0x10

0x10b44700a : sub rsp, 0x10

0x10b44700e : mov esi, dword ptr [rbp + 0x8]

0x10b447011 : lea rdx, [rbp + 0x10]

0x10b447015 : 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

在获得理想的流量转储后,我们看看握手:

在握手中,包含一些值得关注的TLS扩展。我们可以使用openssl s_client工具将大多数扩展与握手一并发送,但是在我们的尝试中,除了openssl(实际是LibreSSL 2.8.3)默认发送的消息之外,仅需要两个,也就是server_name和application_layer_protocol_negotiation扩展。对于ALPN,客户端会发送apns-security-v3和apns-pack-v1。在实际尝试中,服务器始终选择了apns-pack-v1。我们尝试使用这些参数连接到服务器:

% 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

至此,我们已经成功连接到Apple的服务器。在这里,如果省略掉了-alpn或者-servername 任何一个选项,都会导致握手失败。可以忽略其中的verify error:num=19提示,这个提示是openssl针对自签名的CA证书发出的提示。

0x03 拦截APN消息

现在,我们需要拦截未加密的TLS消息。此前,证书绑定(Certificate Pinning)相对容易在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 : push rbp

0x109a55d84 : mov rbp, rsp

0x109a55d87 : push r15

0x109a55d89 : push r14

0x109a55d8b : push rbx

0x109a55d8c : sub rsp, 0x18

0x109a55d90 : mov rbx, rdi

0x109a55d93 : mov rax, qword ptr [rip + 0x924a6] ; (void *)0x00007fff88a98af0: __stack_chk_guard

Target 0: (apsd) stopped.

(lldb) po $rdx

rdx拥有对NSData对象的引用,该对象的字节将被写入输出流。在输入流上接收数据也使用了与之相同的机制。

0x04 与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.{

此时,连接响应消息中包含响应代码(0x08)、服务器时间以及其他参数。

0x05 断开连接

APN是一种二进制协议。这些命令已经在APSProtocolParser类中进行了序列化,其内部并不是我们想要的。根据我们对apsd内部进行的分析,以下是能够发送iMessage所需的最小化命令序列:

0x07:使用uid 0连接用户(每个用户都有自己的公共push token);

0x0c:保持连接(Keep Alive);

0x14:活动状态;

0x07:使用uid 501连接用户;

0x09:过滤主题;

0x0a:发送消息。

通过复制apsd发送时的二进制消息数据,并将其作为我们的openssl FIFO设置的输入,我们可以完全从openssl复现iMessage的发送。在这些命令中,最值得关注的是filter(0x09)。filter消息方法-[APSProtocolParser copyFilterMessageWithEnabledHashes:ignoredHashes:opportunisticHashes:nonWakingHashes:pausedHashes:token:]中被序列化。参数中的哈希值用于主题或使用APN的服务。出于某种原因,iMessage的主题名称为com.apple.madrid。如果没有过滤器消息,客户端就无法通过sendcommand(0x0a)发送或接收APN消息。因此,我们必须在发送消息之前调用filter命令。

0x06 总结

如我们所见,复现APN流量非常容易,但需要注意的一点是,filter命令将导致服务器删除同一公共token的所有先前连接。

假设我们已经经历了逆向协议的痛苦,从头开始生成APN有效消息,然后构建了一个名为fakeapsd的Linux APN客户端,并从Mac设备上原样复制了连接消息参数(公共token和密钥对)。如上所述,使用filter命令进行连接断开的含义是,每次fakeapsd尝试与服务器进行任何有意义的通信时,都会导致真正的apsd连接断开,进而尝试重新连接,并且fakeapsd永远会和apsd抢夺连接。

既然服务器会针对同一个公共token断开连接,而这是连接消息的关键参数,那么,是否可以生成一个新的来绕过这一限制呢?无需花费宝贵的时间来进行研究,我们可以向Hackintosh(黑苹果)寻求答案。

我们从技术角度上来分析Hackintosh,这其实是一个完美的实验,因为在其他所有条件(包括协议处理等等)相同的情况下,它能够控制一个重要的参数,使系统误认为它运行在一台真正的Apple设备上。长期以来,在非苹果设备上使用iMessage和FaceTime一直存在着很多问题,因为必须首先暴力破解出序列号,直到寻找到一个真实、尚未购买的序列号为止。

我们知道,在攻击者可以完整访问软件的白盒场景中,控制硬件可能是用于“保护”协议的一个最重要的元素。类似于Frida这样的动态分析工具可以加速逆向工程的进度。

要了解有关移动应用程序安全性和更多研究成果,欢迎与我们的专家进行沟通。

参考及来源:https://www.nowsecure.com/blog/2021/01/27/reverse-engineering-imessage-leveraging-the-hardware-to-protect-the-software/