2019年1月29日,在FaceTime组中发现了一个严重漏洞,攻击者可以通过该漏洞调用目标并强制呼叫进行连接,而无需用户与目标之间的交互,从而使攻击者无需他们的知情或同意即可收听目标的周围环境。该错误在影响和机制上都非常出色。强制目标设备在不获得代码执行的情况下将音频传输到攻击者设备的能力是此漏洞的异常影响,并且可能是前所未有的影响。此外,该漏洞是FaceTime调用状态机中的逻辑错误,可以仅使用设备的用户界面来执行。尽管此错误很快得到修复,但由于调用状态机中的逻辑错误(这种情况我从未在任何平台上考虑过),因此发生了这样一个严重且易于实现的漏洞,这一事实使我感到奇怪。状态机也有类似的漏洞。这篇文章描述了我对许多消息传递平台(包括Signal,JioChat,Mocha,Google Duo和Facebook Messenger)的呼叫状态机的调查。
大多数视频会议应用程序都是使用WebRTC实现的,我在过去的几篇博客文章中都对此进行了讨论。通过在对等方之间的会话描述协议(SDP)中交换呼叫建立信息来创建WebRTC连接,此过程称为信令。信令不是由WebRTC实现的,WebRTC允许对等方以对他们可用的任何安全通信消息交换SDP,通常是Web应用程序的WebSockets,以及消息传递应用程序的安全消息传递。
WebRTC对等方可以交换几种SDP。在典型的连接中,呼叫者通过发送SDP报价开始,然后被叫方以SDP答复进行响应。这些消息包含传输和接收媒体所需的大多数信息,包括编解码器支持,加密密钥等等。交换报价/答案后,对等方可以将SDP候选者发送给其他对等方。候选对象是两个对等方可以用来相互连接的潜在网络路径,而SDP候选对象包含诸如IP地址和TURN服务器之类的信息。对等方通常会向一个对等方发送多个候选人,并且可以在连接期间的任何时间发送候选人。
WebRTC连接维护一个内部状态,该内部状态与是否已接收或处理要约或答复有关,但是,使用WebRTC的应用程序通常必须维护自己的状态机,以管理应用程序的用户状态。用户状态如何映射到WebRTC状态是WebRTC集成商做出的设计选择,这对安全性和性能都有影响。例如,某些应用程序不交换任何SDP,直到被叫方用户与该应用程序进行交互以接听电话,与此同时,其他一些应用程序则建立对等连接,并在被叫方被呼叫之前开始从主叫方向被叫方发送音频和视频。甚至接到电话通知。
无论设计如何,都必须使用WebRTC由应用程序代码直接启用从输入设备传输音频或视频的功能。通常使用称为轨道的功能来完成此操作。每个输入设备都被视为一个“轨道”,并且在传输音频或视频之前,必须通过调用addTrack(或等效语言)将每个特定轨道添加到特定对等连接。也可以禁用音轨,这对于实现静音和关闭摄像头功能很有用。每个音轨还具有RTPSender属性,可用于微调传输属性,也可用于禁用音频或视频传输。
从理论上讲,在音频或视频传输之前确保被叫方同意应该是一个相当简单的问题,即等到用户接受呼叫后再向对等连接添加任何曲目。但是,当我查看实际应用程序时,它们以许多不同的方式启用了传输。其中大多数导致了漏洞,这些漏洞使呼叫得以连接而无需被叫者进行交互。
我在2019年9月查看了Signal,当时该应用程序的调用设置与WebRTC文档中所建议的非常相似。
建立对等连接,然后当被呼叫者通过与用户界面交互接受呼叫时,将被呼叫者的音轨添加到该连接。然后,一条消息通过对等连接发送给呼叫者,告诉它也移至连接状态并添加曲目。
不幸的是,应用程序没有检查接收连接消息的设备是否是呼叫者设备,因此可以将连接消息从呼叫者设备发送到被呼叫者。这导致了音频呼叫的连接,从而使呼叫者能够听到被呼叫者的周围环境。我通过更改Signal的开源代码以发送消息并重新编译攻击的客户端来测试了此错误。
此漏洞已于2019年9月在客户端中修复,此后,Signal的信令代码已被ringrtc项目所取代,该项目使用了更为保守的状态机。
该错误纯属Signal的代码,并非由于对WebRTC功能的误解。状态机设计在很大程度上有效,需要用户同意才能传输音频,但未执行特定检查。
我在2020年7月偶然发现了WebRTC漏洞是否适用于JioChat和Mocha Messenger中的两个非常相似的漏洞。他们都有类似的信令设计,这是服务器介导的。
通过服务器交换要约和答案,然后呼叫者和被呼叫者都将其候选者发送到服务器。然后,服务器将它们存储起来,直到被呼叫者与他们的设备进行交互并接受呼叫为止。然后,创建对等连接,当WebRTC进入其内部连接状态时,将添加轨道,从而导致音频和视频被传输。
这种设计有一个根本性的问题,因为可以选择将候选人包含在SDP报价或答案中。在这种情况下,对等连接将立即开始,因为在此设计中唯一阻止该连接的原因是缺少候选对象,这反过来又导致从输入设备进行传输。我通过使用Frida将候选人添加到这些应用程序创建的报价中进行了测试。我能够导致JioChat未经用户同意发送音频,而使Mocha发送音频和视频。这两个漏洞在通过过滤服务器上的SDP提交后很快就得到修复。
这些问题是由于对WebRTC的工作方式的误解以及试图通过不寻常的信号设计来提高WebRTC性能的原因所致。通常,WebRTC集成商必须决定是否等待被叫方应答呼叫以建立对等连接。尽早建立连接可以提高性能,并防止用户在接听电话时不得不等待,但同时也大大增加了WebRTC的远程攻击面。这些应用程序通过这种设计试图在不增加安全性成本的情况下提高性能,但并未考虑WebRTC可以启动对等连接的所有方式。
对于集成商来说,在不添加或启用轨道的任何WebRTC功能上控制音频或视频传输通常不是一个好主意。首先,许多WebRTC功能都很复杂,因此很容易犯错误,使音频或视频得以传输。同样,如果门控的功能不是常用功能也不是安全功能,则将来可能会对其进行不良测试或更改。
我在2020年9月查看了GoogleDuo。Duo的信令方法与许多Messenger有所不同,因为它支持一项功能,该功能允许被叫方在应答前预览呼叫者的视频。因此,需要在接听电话之前设置单向视频流。
上图显示了单向视频流的设置。虚线表示使用Java执行程序进行的异步调用。从被叫方到主叫方的传输不足是由两种方法引起的。首先,SDP报价包含视频的属性a = sendonly,这导致视频仅在一个方向上传输。同样,当被叫方收到来自呼叫方的报价时,它将视频轨道添加到对等连接,然后使用该轨道的RTPSender属性将其禁用(在用户接受呼叫之前不会添加或启用音频轨道)。
这些方法均不能有效地防止视频从被呼叫者传输到呼叫者。 SDP属性很容易解决,因为调用方将SDP提供给被调用方,因此可以轻松更改它。处理报价后,立即禁用视频轨道应该可以工作,但异步设计除外。通常,setLocalDescription方法(处理SDP报价)将调用回调onSetSuccess,然后在回调完成后建立对等连接。但是,如果回调进行了另一个异步调用,则将不再保持onSetSuccess在建立连接之前完成的保证,因为setLocalDescription方法仅等待onSetSuccess线程完成。这在禁用视频和建立连接之间造成了竞争,因此在某些情况下,被叫方可以在禁用传输之前向呼叫者发送一些视频帧。
我通过使用Frida更改被叫方发送的SDP进行了测试,然后尝试了多种方法来赢得比赛。事实证明,这很难取胜,我花了大约两个星期的时间来弄清楚如何放慢视频禁用呼叫的速度,以腾出时间来建立连接。我最终发送了多个要约,并向要约中添加了候选者,这减少了连接时间,因为已经建立了网络连接。然后,我通过对等连接的数据通道发送了许多消息,这些消息需要很长时间才能处理,从而减慢了视频轨道的禁用速度。数据消息的处理与在Duo中禁用视频轨道的线程队列相同,因此发送数据消息会填满队列,而该队列需要禁用具有许多其他条目的视频,从而延迟了轨道的禁用。
该错误已于2020年12月通过从onSetSuccess中删除异步调用而得到修复。虽然Duo通常以有效防止从被呼叫者到呼叫者的视频传输的方式设计信令,但是实现该设计异步引入了问题。在许多不可预测的情况下,WebRTC需要在网络或对等点上等待,并且将函数调用分为不同的线程意味着一个调用的延迟不会影响不相关的功能,因此异步信令实现在移动应用程序上变得越来越普遍。但是,异步调用使建模状态机在所有情况下的行为变得更加困难,因此在向WebRTC信令添加异步调用时务必谨慎。在这种情况下,禁用视频轨道的异步调用在性能方面没有增加任何内容,因为没有理由禁用轨道的任何调用都可以阻塞,并且onSetSuccess已经在其自己的线程中运行并且可以产生更高的优先级线程。重要的是要平衡异步调用的风险和收益,并且不要不加选择地将它们包含在应用程序中。
我在2020年10月查看了Facebook Messenger。由于需要大量的逆向工程,因此这是一个颇具挑战性的目标。退一步,WebRTC具有几种编程语言的绑定,使它可以使用该语言集成到应用程序中。集成WebRTC的大多数Android应用程序都使用Java绑定。这使调查信号状态机变得相当简单,因为重要的Java函数(例如setLocalDescription(处理提供和答复),addRemoteIceCandidate(处理候选对象)和addTrack(将连接添加轨迹)可以挂接到Frida中并记录下来进行分析。使用这些调用更改攻击者设备的行为也相当简单。
Facebook Messenger不使用Java绑定来集成WebRTC,而是使用C ++绑定。而且,它静态地将WebRTC链接到一个更大的库(librtcR20.so,这很可能是本文中提到的rsys库),因此,用于绑定的调用的符号被剥离,从而使其难以挂接。此外,Facebook Messenger在传输SDP之前将其串行化为另一种格式,因此很难通过监视流量来确定信令的工作方式。
我最终意识到,弄清Facebook Messenger信号工作方式的唯一合理方法是弄清其网络协议。值得庆幸的是,Facebook公开表示他们使用了Thrbift的分支fbthrift。我将librtcR20.so库加载到IDA中,以查看是否可以在thrift库中找到它调用的位置,但是尽管有几次调用,但看起来代码大部分是静态链接的。我最终发现这是因为thrift为实现的每个协议生成序列化代码,因此大多数序列化和反序列化代码最终都使用协议处理代码进行了编译。因此,我决定编译fbthrift,制作一个示例序列化程序,并在IDA中对其进行查看,以便对编译后的fbthrift序列化程序的外观有一个印象。我注意到在序列化过程中,对象的成员通过调用称为writeFieldBegin的方法进行序列化。我还注意到,调用此方法时,即使通常不将其包含在序列化输出中,该字段名称也是必需的。因此,我在librtcR20中寻找了一个函数,该函数经常用不同的字符串参数调用,这对于字段名来说似乎是合理的。满足该条件的功能不是很多,因此我能够确定writeFieldBegin。
在这一点上,我可以找到许多对象被序列化的地方,并且需要确定用于设置WebRTC调用的消息是哪一个。
早些时候,我注意到库中有一个称为P2PCall :: OnP2PMessageFromPeer的方法(请注意,该方法的符号已被删除,但调用该方法时会记录该方法的名称)。这似乎是处理反序列化消息的地方。搜索字符串“ P2PMessage”,我找到了名为P2PMessageRequest的类型的序列化代码。我以为这是创建呼叫建立消息的地方。
节俭序列化代码是根据节俭定义文件中的类定义生成的。基于传递给writeFieldBegin的字段名称和类型,我能够对这种类型的完整节俭定义进行缓慢的反向工程。这是繁琐的工作,因为定义时间很长,并且代码的混淆方式使寄存器的使用不一致,因此我不相信任何自动方法都是准确的。
请注意,它从类型为Extmap的对象写入两个字段。第一个名为id,是必填字段。编写代码的功能如下。
写入的字段标识符为1,字段类型为8,它转换为i32(32位整数)。第二个字段是一个可选字段,用于编写它的寄存器在以下代码中设置。
这会将字段名称设置为uri,将字段标识符设置为2,并将字段类型设置为8(也为i32)。总之,此代码可由以下节俭定义表示。
在对P2PMessageRequest类型的每个字段进行类似的反向工程之后,我有了一个完整的节俭定义,可以在这里找到。
我用这个节俭的定义做了两件事。首先,我用它来确定C ++中P2PMessageRequest类型的布局。这非常有价值,因为它允许我使用正确命名的每个字段将结构定义加载到IDA中。这使得了解P2PCall :: OnP2PMessageFromPeer如何处理传入消息变得更加容易。最终这只是一个过程。 fbthrift可以直接从Thrift定义生成C ++头文件,但是它们很长,并且包含许多不必要的定义,因此IDA无法对其进行处理。因此,我最终编译了生成的源并将其加载到IDA中,然后导出结构定义并将其导入到已经加载librtcR20.so的另一个IDA实例中。我编辑的几个字段的大小与Facebook的大小不同,但是由于距离足够近,我可以对其进行一些修改。
下面是在IDA中使用节俭定义导入的反编译代码示例,以使它更容易理解消息对象的处理。
我还能够解码并生成通过网络发送的消息。为此,我从Python中的Thrift定义生成了序列化代码,因为Thrift支持多种语言的代码生成。然后,当使用Frida Python挂钩Facebook Messenger中的函数时,我能够导入此代码。
然后我需要找到处理传入的P2PMessageRequest消息的代码。由于这些消息是由本机代码处理的,而大多数Facebook消息是由Java代码处理的,因此我寻找了一个具有适当名称的本机调用。我发现com.facebook.webrtc.WebrtcEngine.onThriftMessageFromPeer。我将这种方法与Frida挂钩,并将其字节数组参数输入生成的解串器中,然后对传入的消息进行解码。
我发现了一种用于发送节俭消息的类似方法sendThriftToPeer(该方法的类名被混淆,并且在每个版本的Facebook Messenger中都进行了更改,但是可以通过grep应用程序的smali来找到)。我还可以挂钩此方法并更改其字节数组参数,以更改Facebook Messenger发送的P2PMessageRequest消息。
现在,我能够理解Facebook Messenger的信号状态机。发生信号的方式有两种,具体取决于用户在何处登录Facebook Messenger。如果用户在多个设备或浏览器上登录,那么在被呼叫者与其设备进行交互之前,几乎不会发生任何事情。报价,答案和候选者被交换,但是它们由被叫设备存储并且在被叫用户应答呼叫之前不被处理。这是有道理的,因为Facebook Messenger不知道要连接哪个设备。
如果被叫方仅在单个设备上登录,则状态机会更有趣。
在这种情况下,Facebook Messenger会在收到要约后立即启用跟踪,但会更改要约,以使所有传出流都处于非活动状态。然后,当用户与设备进行交互时,它会用一个激活的提议来代替。
我担心可能有一种绕过要约变更的方法,但我研究了这样做的方法,尽管我一般不建议使用添加或禁用轨道来禁用输入设备传输的其他方法,但是相当强大。在将SDP解码为内部WebRTC对象之后,更改要约,并直接对此对象进行更改,从而消除了解析错误的可能性。
但是,在查看传入消息的处理方式时,我注意到在应答呼叫之前,除了要约,答案和候选者之外还处理了许多消息类型。一种突出的类型称为SdpUpdate。收到SdpUpdate消息后,将通过调用setLocalDescription更新本地要约或答案。
发送到上述状态机时,此消息类型没有任何作用,因为它已经存储了SDP,正在等待调用
......