Apple 的推送通知服务实现是否容易受到中间人攻击

信息安全 tls 证书 中间人 苹果系统 IOS
2021-08-21 05:54:16

最近(更多信息)我在最近全新安装的OS X Mavericks 10.9.2的日志中发现了一条奇怪的消息

Apr 27 15:26:47 Ivans-MacBook-Pro.local apsd[194]: Unrecognized leaf certificate

它每 15 分钟左右出现一次。我已经用谷歌搜索了它,还有许多其他用户将他们的日志(很多时候甚至与该问题无关)粘贴到了有该消息的网络上。它似乎至少出现了半年。

apsd是一个持续运行的 Apple Push Notification 服务守护进程

它随机连接到以下服务器之一:

1-courier.push.apple.com
2-courier.push.apple.com
3-courier.push.apple.com
4-courier.push.apple.com

.
.
200-courier.push.apple.com

在端口5223上(自定义,但SSL

每隔一段时间(我猜是为了检查更新)

尝试:https://1-courier.push.apple.com:5223在浏览器中但是显示存在与服务器证书相关的问题。“此证书无效(主机名不匹配)”我想这是因为 Apple 没有创建包含通用名称通配符的证书。但是使用Wireshark我看到apsd和 Apple 服务器之间的通信仍在继续。我虽然,那不可能是对的。apsd 是否忽略了有效性检查?!

然后我尝试使用自签名证书执行中间人攻击。这就是我在日志中得到的:

Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: CFNetwork SSLHandshake failed (-9807)
Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: Failed to evaluate trust: No error. (0), result=5; retrying with revocation checking optional
Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: failed to evaluate trust: No error. (0), result=5; retrying with system roots
Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: Failed to evaluate trust: No error. (0), result=5
Apr 27 15:42:07 Ivans-MacBook-Pro.local apsd[194]: Untrusted peer, closing connection immediately

还值得注意的是,我发现(在使用 Wireshark 观察实际流量时)服务器需要客户端发送他的证书。并且apsd发送:

主题名称
通用名称XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX (UUID)

发行人名称

组织苹果公司
组织单位的苹果iPhone
通用名称苹果iPhone设备CA

.
.

奇怪的是,Mac OS X 发送的似乎是一个公钥,将它自己呈现为 iPhone?我确实安装了 Xcode,如果它与安装 iPhone 模拟器有关。但是无论 Xcode 是否正在运行,我都会一直收到提到的日志消息。从互联网上的日志来看,显然其他人也是如此。

那么发生了什么?apsd
如何能够找出我的假证书并通过他们的?这里有什么问题吗?注意:我的 MitM 实现非常简单,通过 DNS 欺骗,我给apsd的响应是 X-courier.push.apple.com 是 127.0.0.1。然后,我使用模仿 Apple 服务器证书(国家、组织、通用名称...)的自签名证书进行监听。但我没有实现要求apsd的客户端证书。所以我还不知道这是否与我失败的 MitM 有关。可以吗?

你们在 Mac OS X 上是否在控制台中收到相同的日志消息(它在您启动它时出现在“所有消息”中,但实际上在 system.log 中),在搜索框中键入“叶子”...

更新:
我已经实现了向客户端请求其证书。是一样的(SSLHandshake 失败/无法评估信任)。所以 apsd 正在检查一些东西,我只是不太确定它是否以正确的方式实现。希望有更多经验的人会研究这个......

2个回答

我现在没有太多时间,也没有彻底验证这一点,所以这只是我几个月前研究这个问题时的记忆。

在 OS X 10.9 和 iOS 7 中,Apple 为推送服务引入了某种证书固定,即不允许系统知道的任何 CA 签署他们的推送服务器证书,而只允许特定的 CA。这个证书固定有点奇怪,因为它的到期日期是 2014 年 1 月 1 日,但我并没有过多研究这到底意味着什么,我不知道最新版本的 OS X 和 iOS 是否仍然包括这。

无法识别的叶子证书

因此,根据我目前的理解,此消息并不意味着 apsd 根本无法验证证书,而是可能意味着证书固定无法验证它(这意味着证书固定当前是可选的,而这又可能必须这样做与到期日期)。

您可能不必直接担心这一点,因为正常的 SSL 证书验证仍然有效(正如您的测试所证实的那样)。另一方面,我很想知道为什么证书固定显然不起作用以及到期日期意味着什么。

更新:这里有一些关于为什么显示日志消息的更多信息。

您可以使用 openssl 显示服务器证书(似乎在端口 443 和 5223 上提供相同的证书):

openssl s_client -prexit -connect 1-courier.push.apple.com:5223  2>/dev/null | openssl x509 -noout -text |grep -E '(Subject.*CN|Serial)'

截至 2014 年 5 月 10 日,这显示了以下信息:

Serial Number: 1277288244 (0x4c21df34)
Subject: C=US, ST=California, L=Cupertino, O=Apple Inc., CN=courier.push.apple.com

apsd二进制文件包含多个用于证书固定的 X.509 证书。我编写了一个脚本来查找二进制文件中的证书这是运行时的摘录apsd

+ 469280 Found cert with CN "courier.sandbox.push.apple.com" and serial "1277027356"
+ 470416 Found cert with CN "courier.push.apple.com" and serial "1276925395"
+ 471584 Found cert with CN "Entrust.net Certification Authority (2048)" and serial "946059622"
[skipping some code signing certificates]

如您所见,固定证书的courier.push.apple.com序列号与 Apple 推送服务器提供的序列号不同Unrecognized leaf certificate所以两者都是不同的证书,显然这就是您看到日志消息的原因。由于apsd仍然连接到服务器,这意味着apsd不需要固定的叶证书来匹配。

您可以看到的另一件事是第三个证书,即CA 证书去年,在尝试apsd连接pushproxy时,我不得不更换 CA 证书以建立apsd信任 pushproxy。这意味着,虽然不检查叶证书,但apsd可能只允许来自某个根 CA 的证书。我最近没有验证这一点。

为了彻底检查这一点,除了检查 SSL 连接的 CA 证书固定是否仍然有效之外,还必须检查另一件事。在连接到推送服务器之前,apsd下载一个“配置包”此包是通过 HTTP 下载的,但已签名。该包包含主机名apsd,然后尝试连接。这是我用于 pushproxy 重定向apsd到另一台主机的一种方式。您可以在pushproxy README生成此类包的脚本中找到有关包的更多信息

由于还没有人回应,我想也许我应该把我的测试方法写出来,然后有人可能会指出这是否可以。

我在python中编写了一个基本服务器,监听5223端口,如下所示:

#!/usr/bin/python
import SocketServer
import ssl

class requestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        print self.client_address[0] + ' connected.'
        receivedData = self.request.recv(4096)
        print receivedData

        #self.request.sendall('test')
        return

server = SocketServer.TCPServer(('0.0.0.0', 5223), requestHandler)
server.socket = ssl.wrap_socket(server.socket, server_side=True, keyfile='privateKey.pem', certfile='selfSignedCert.pem')
#server.socket = ssl.wrap_socket(server.socket, server_side=True, keyfile='privateKey.pem', certfile='selfSignedCert.pem', cert_reqs=ssl.CERT_REQUIRED, ca_certs='expectedClient.pem')
print 'Listening on port 5223'
server.serve_forever()

最后注释的 server.socket 行是向客户端(apsd)请求证书的版本。我通过在控制台上调用openssl创建了证书:

openssl req -new -x509 -days 365 -nodes -out selfSignedCert.pem -keyout privateKey.pem

我使用WiresharkDER格式的实际流量中提取expectedClient.pem 。因此,我使用以下代码将其转换为PEM :

#!/usr/bin/python
import ssl

inFileHandle = open('clientCert.der', 'rb')
outFileHandle = open('clientCert.pem', 'wb')
outFileContent = ssl.DER_cert_to_PEM_cert(inFileHandle.read())
outFileHandle.write(outFileContent)
inFileHandle.close()
outFileHandle.close()

现在我需要欺骗apsd xxx-courier.push.apple.com实际上127.0.0.1我编写了一个基本的 DNS 保镖(代理),它查找具有此类地址的查询并做出 IP 为 127.0.0.1 的响应。
这是代码:

#!/usr/bin/python
import SocketServer
import socket

DNS_server = '8.8.8.8' # Your ISP's DNS server(8.8.8.8 is Google Public DNS)
DNS_formatted_address_match = '-courier' + chr(4) + 'push' + chr(5) + 'apple' + chr(3) + 'com' + chr(0)

class requestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        queryData = self.request[0]
        incomingSocket = self.request[1]

        transactionID = queryData[:2] # First two bytes
        DNS_formatted_query_address = queryData[12:].split(chr(0))[0] + chr(0) # First 12 bytes are DNS header

        if DNS_formatted_address_match in queryData:
            # DNS protocol explained: http://technet.microsoft.com/en-us/library/dd197470(v=ws.10).aspx
            forgedResponse = (transactionID + 
                            chr(129) + chr(128) + 
                            chr(0) + chr(1) + 
                            chr(0) + chr(1) + 
                            chr(0) + chr(0) + 
                            chr(0) + chr(0) + 
                            DNS_formatted_query_address + 
                            chr(0) + chr(1) + 
                            chr(0) + chr(1) + 
                            chr(192) + chr(12) + chr(0) + chr(1) + 
                            chr(0) + chr(1) + 
                            chr(0) + chr(0) + chr(0) + chr(60) + chr(0) + chr(4) + 
                            chr(127) + chr(0) + chr(0) + chr(1))

            incomingSocket.sendto(forgedResponse, self.client_address)
        else:
            outgoingSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            outgoingSocket.sendto(queryData, (DNS_server, 53))
            responseData = outgoingSocket.recv(4096) # DNS response should not be more than 512 bytes so this should be more than enough
            incomingSocket.sendto(responseData, self.client_address)

        return

server = SocketServer.UDPServer(('0.0.0.0', 53), requestHandler)
print 'Listening on port 53'
server.serve_forever()

您需要使用sudo启动它,否则它将不想绑定端口 53。最后一步是将我的网络首选项中的 DNS 服务器更改为 127.0.0.1 以便 DNS 查找实际上通过保镖然后到 ISP。

而已!

现在,如果您愿意,您可以自己测试...