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