状态机bug一览

2019年1月29日,我们在FaceTime群聊中发现了一个严重漏洞。该漏洞使黑客能呼叫目标设备,且在没有与目标进行用户交互的情况下强制连接呼叫,这样一来,即使没有获得目标同意或目标毫不知情,黑客也能够监听其有关信息。该bug不仅运行机制奇特,造成的危害也较大。它能在没有获得代码执行的情况下强制目标设备向黑客设备传输音频,这样不寻常的漏洞前所未有。此外,该漏洞是FaceTime调用状态机中的一个逻辑bug,只需使用设备的用户界面即可激活。虽然这个bug很快就被修复了,但我从未见过由于调用状态机中的逻辑bug而发生了如此严重且容易触及的漏洞,似乎没有哪个平台预设过这种攻击场景。我不禁怀疑其他状态机是否也存在类似的漏洞。本文记录了我对各大通讯平台(包括Signal、JioChat、Mocha、Google Duo和Facebook Messenger)调用状态机的调查情况。

WebRTC和状态机

大多数视频会议应用都是使用WebRTC实现的。在之前文章中我已对WebRTC有过详细讨论。WebRTC连接是通过端与端之间在交换会话描述协议(SDP)中的呼叫设置信息来创建的,这个过程被称为信令,并非由WebRTC实现。WebRTC允许端以可用的安全通信消息交换SDP,通常Web应用采用WebSockets传递,通信应用采用安全通信传递。

WebRTC端可以交换的SDP有以下几种类型。在典型连接中,主叫方先发送一个SDP提议,然后接听者用SDP应答回应。这些消息包含了传输和接收媒体所需的大部分信息,包括编解码器支持、加密密钥等。交换提议和应答后,端可以向其他端发送SDP候选者。候选者是两端用来相互连接的潜在网络路径。SDP候选者包含IP地址和TURN服务器等信息。端通常向一端发送多个候选者,发送时间可以是连接过程中的任何时候。

WebRTC连接可以维护有关提议或应答是否被接受处理的内部状态。然而使用WebRTC的应用程序通常还要维护自己的状态机来管理应用程序的用户状态。WebRTC integrator负责设计用户状态如何映射到WebRTC状态,这对安全和性能都有影响。例如有些应用在接听者与应用交互、接听电话之前,不会交换任何SDP。与此同时,另外一些应用建立了端对端连接,在接听者还没有接到呼叫通知之前就从主叫方处开始向其发送音频和视频。

除设计外,从输入设备传输音视频必须由应用程序代码使用WebRTC直接启用。该操作通常通过一种被称为track的功能来实现。每个输入设备都可看作是一个track,每个特定的track必须在传输音频或视频之前通过调用addTrack添加到特定的端对端连接中。另外,禁用track可实现静音和相机关闭。每个track都有一个RTPSender属性,可以用来微调传输的属性,也可以用来禁用音频或视频传输。

理论上,在音频或视频传输之前获取接听者同意应该就是等几分钟的事儿。直到用户接受呼叫后,再向端连接添加track。然而当我观察应用程序中的真实场景时,我发现他们以各种不同的方式启用传输。大多数方式都导致了漏洞出现,使得呼叫在没有接听者交互的情况下就连接上了。

Signal Messenger

我查看了2019年9月Signal的设置,该应用的呼叫设置与WebRTC文档中推荐的非常相似。

即建立一个端对端连接。当接听方通过与用户界面交互接受呼叫时,添加接听方的音轨至连接中。通过端对端连接向接听方发送消息,告知主叫方移动到连接状态并添加音轨。

但是应用程序没有确认接收连接消息的设备是否是接受呼叫的设备,所以呼叫设备就能向被叫设备发送连接消息,直接连接音频通话了。这样主叫方就能够听到接听方的周遭信息了。通过修改Signal的开源代码来发送消息,并重新编译攻击客户端,我测试了Signal的这个bug。

这个漏洞已于2019年9月在客户端修复,此后,Signal的信令代码被使用更保守状态机的ringrtc项目取代。

这个bug纯粹是因为Signal的代码,并不是对WebRTC功能的误解。状态机的设计在请求用户同意传输音频上基本是有效的,但确实没有进行更具体的检查。

JioChat和Mocha

2020年7月,我在测试一项WebRTC实现是否对JioChat和Mocha Messenger起效时,意外发现了JioChat和Mocha中两个非常相似的漏洞。它们都有一个类似的以服务器为媒介信令设计。

通过服务器交换提议和应答,主叫方和接听方将他们的候选人发送到服务器。接听方和他们的设备交互并接受呼叫服务器前,都由服务器存储候选人。之后建立端对端连接。当WebRTC进入内部连接状态时添加track,音视频开始传输。

这种设计有一个根本性问题——候选者可以选择性地包含在SDP提议或应答中。在这种情况下,端对端连接会立马开始,因为在该设计中,只有候选人未到位才能阻止连接,而这反过来又会导致输入设备的传输。通过使用Frida将候选者添加到每个应用程序创建的提议中,我对这两个应用漏洞进行了测试。我可以让JioChat在未经用户同意的情况下发送音频,让Mocha发送音视频。这两个漏洞在提交后不久,就通过过滤服务器上的SDP进行了修复。

会产生这些漏洞,是由于我们误解了WebRTC的工作方式,以及我们试图通过独特的信令设计来提高WebRTC性能。通常情况下,WebRTC integrator必须决定是否要等到接听方接听电话后再建立端对端连接。提前设置连接可以提高性能,避免用户在接听电话时长时间等待。但这也会使对WebRTC的远程攻击面增多。这些应用试图通过这种设计在保证安全的同时提高性能,但他们没有考虑到WebRTC启动端对端连接的所有方式。

对于integrator来说,在任何没有添加或启用track的WebRTC功能上,把控音视频传输都不是个好主意。因为许多WebRTC功能都很复杂,所以很容易导致允许传输音视频的错误。另外,如果被把控的功能不常用或不是安全功能,将来可能会出现测试不周或必须更改的情况。

Duo

2020年9月,我查看了Google Duo的相关设置。Duo的信令方法与大部分messenger都不同——它支持电话接通前接听者可预览主叫方视频这一功能。因此在接听电话之前需要设置一个单向视频流。

上图为单向视频流的设置流程。虚线处的步骤是在使用Java执行器进行异步调用。两方面原因促成了接收方到主叫方的传输缺失 。首先,SDP要约中包含了视频属性a=sendonly,这使得视频只能单向传输。其次,如果接收方收到主叫方的要约,接收方会将Video Track添加至端对端连接中,但随后接收方又使用track的RTPSender属性将其禁用了(即在用户接受呼叫之前,不会添加或启用Audio Track)。

然而,这两种方法都不能真正阻止接收方的视频被传输给主叫方。因为向接收方提供了SDP,主叫方很容易掌控和更改SDP属性。如果没有异步设计,一处理完邀约就停用video track应该是可以的。正常情况下,setLocalDescription方法(用于处理SDP要约)会回调onSetSuccess,回调结束后再设置端对端连接。但如果回调又进行了异步调用,那么在建立连接之前就不一定能完成onSetSuccess,因为setLocalDescription方法只等待onSetSuccess线程完成,也就是要看禁用视频和设置连接的速度哪个更快。所以在某些情况下,接收方在传输被禁用之前可能还是会有几个视频帧发送给主叫方了。

我用Frida改变接收方发送的SDP来测试上述漏洞,也试了好多种方法来赢得这场速度的竞赛。但我发现要赢得比赛相当困难,我花了大约两周的时间试图找出如何减缓视频禁用呼叫的速度,以便给连接争取时间。最后我发现,发送多个offer,在offer中添加候选人能够减少连接时间。因为此时已经建立了网络连接。之后我通过端对端连接的数据通道发送了许多需要长时间处理的消息,以减缓Video Track的禁用时间。处理数据消息与禁用Duo中的Video Track是在同一个线程队列中进行的,所以发送的数据消息同许多其他条目把禁用视频所需的队列占满了,延迟了track被禁用的时间。

2020年12月,这个bug被修复了。取消了onSetSuccess的异步调用。虽然总体上Duo设计的信令能有效防止接收方的视频传输给主叫越来越多,因为有很多不可预知的情况下,WebRTC需要在网络中或端上等待。将函数调用分离到不同的线程中意味着一次调用的延迟不会影响到不相关的功能。然而异步调用增加了对状态机在所有情况下的表现进行建模的困难。因此我们要对WebRTC信令中加入异步调用更加谨慎。在本例中,禁用Video Track的异步调用没有给性能方面增加负担,因为禁用track的任何调用都没理由被屏蔽,而且onSetSuccess已经在自己的线程中运行,可以让位于优先级更高的线程了。平衡异步调用的风险和益处是很重要的,我们不能不加分辨地将其安装至应用程序中。

Facebook Messenger

2020年10月我研究了Facebook Messenger。鉴于需要做大量逆向工程,该研究着实是个不小的挑战。退一步讲,WebRTC绑定了几种编程语言,允许它集成到使用该语言的应用程序中。大多数集成了WebRTC的Android应用也都绑定了Java。这样,研究信令状态机就不用绕弯子了。因为重要的Java函数,如setLocalDescription(处理提议和应答)、addRemoteIceCandidate(处理候选者)和addTrack(将track添加到连接中)可以连接Frida,并记录下来进行分析。同时,使用这些调用来改变攻击者设备的行为也可信手拈来。

Facebook Messenger没有绑定Java来集成WebRTC,而是绑定了C++。此外,它将WebRTC静态链接到一个更大的库(librtcR20.so,同这篇文章中提到的rsys库差不多),因此调用绑定的符号会被剥离,使得它们难以连接。此外,Facebook Messenger在传输前会将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)。将这些操作整合后就可用thirft定义来表示这段代码了。

struct Extmap{

        1: i32 id

        2: optional i32 uri

在对P2PMessageRequest类型的每个字段进行类似的逆向工程之后,我完成了一个完整的thrift定义,详情点击此处

我利用该定义做了两件事。首先,我用其来确定P2PMessageRequest类型在C++中的位置。这很有用,因为这样我就能将结构定义加载到IDA中,并正确命名每一个字段,P2PCall::OnP2PMessageFromPeer中是如何处理传入消息也就更容易理解了。最后形成了一些进程。fbthrift可以直接从thrift定义中生成C++头文件,但这些文件非常长,包含了很多不必要的定义,IDA无法处理。所以最后我把生成的源码编译后加载到IDA中,导出结构定义,再将其导入到另一个已经加载了librtcR20.so的IDA实例中。我的编译中有几个字段的大小与Facebook的不同,但差别不大,修改一下就可运行了。

下图是一个在IDA中反编译并导入thrift定义的代码案例,通过该例我们可以感受到该代码使处理消息对象变得多么容易。

我还能够解码并生成通过网络发送的消息。为做到这一点,我用Python从thrift定义中生成了序列化代码,也是因为thrift支持生成多种语言形式的代码我才能这样做。之后,我就能在使用Frida Python hook Facebook Messenger中的函数时导入这段代码。

然后,我需要找到处理传入的P2PMessageRequest消息的代码。这些消息是由本地代码处理的,而大多数Facebook消息是由Java代码处理的,所以我要找一个命名合适的本地调用。最终我找到了com.facebook.webrtc.WebrtcEngine.onThriftMessageFromPeer。我把这个方法同Frida hook,在生成的反序列器中输入了它的字节数组参数,然后该方法就对传入的消息进行了解码。

之后,我发现了一个类似的用来发送thrift消息的方法——sendThriftToPeer(这个方法的类名模糊,各版本的Facebook Messenger类名都不同,但我们可以通过查询应用程序的smari找到它)。我也能hook这个方法,改变它的字节数组参数,这样就能改变Facebook Messenger发送的P2PMessageRequest消息。

现在我能够理解Facebook Messenger的信令状态机了。有两种不同的方式产生信令,具体哪一种取决于用户在哪里登录Facebook Messenger。如果用户在多个设备或浏览器上登录,那么在接收房与他们的设备交互之前,几乎没有信息丢失的情况出现。邀约、应答和候选人确实已经交换了,但它们被储存在了接收者的设备里,在接收者接听电话后才会进行处理。这很合理,因为Facebook Messenger不知道会连接到什么设备。

如果接收方只在单一设备上登录,状态机的操作就更显有趣了。

这种情况下,Facebook Messenger一收到邀约就会启用track,但也会更改邀约减少所有外发流的活跃度。然后,它用用户与设备交互时它们处于活跃状态的邀约来替换该邀约。

我担心可能会有人想办法绕过改变邀约的操作,所以我深入了解了该操作的机制。虽然一般我不建议使用除了添加或禁用track以外的任何东西来禁用输入设备传输,但此操作是个例外,它很坚挺。该邀约是在SDP解码成一个内部的WebRTC对象后被改变的,并且是直接对这个对象进行修改的,这样就不可能出现解析错误的问题。

然而在观察传入消息的处理方式时,我发现除了邀约、应答和候选人之外,很多消息类型都是在呼叫被应答之前处理的。其中有一种类型很突出,叫做SdpUpdate。当收到SdpUpdate消息时,本地的邀约或应答是通过调用setLocalDescription来更新的。

这种消息类型在发送到上述状态机时并无任何效力,因为它已经在存储SDP并等待调用setLocalDescription。但在用户登录两台设备的情况下,该类型会导致setLocalDescription的调用,并开启音频连接。

目前还不清楚SdpUpdate消息类型在Facebook Messenger中的用途。我在测试设备上尝试了网络切换等许多场景,但在日常使用情况下却无法生成该操作。无论如何,很明显在接听电话之前,这种消息类型不应该被接收。与上述的Signal bug类似,它与应用程序使用WebRTC无关,而是由于在处理输入时遗漏检查,导致了状态转换。

漏洞已于2020年11月通过服务器变更修复,防止在呼叫连接之前发送该消息类型。

其他应用程序

我还检查了其他几个应用,没有发现他们的状态机有问题。2020年8月,视频会议刚刚添加到应用中之后,我检查了Telegram,没有发现任何问题。大概是因为该应用在接收方接听电话之前不会交换邀约、应答或候选人。2020年11月我查看了Viber,也没有发现他们的状态机有任何问题,但由于逆向工程应用困难,我对于该应用状态机的分析并没有对其他应用那么仔细。

思考

我调查的大多数调用状态机都存在逻辑漏洞,这会导致音视频内容在未经接收方同意的情况下传输给主叫方。这显然是在确保WebRTC应用安全时经常被忽视的一个领域。

大多数问题不是由于开发人员误解WebRTC功能造成的。相反,是由于状态机实现方式错误造成的。也就是说缺乏对这些类型问题的认识可能是引发问题的一部分原因。很少有WebRTC文档或教程明确讨论过从用户的设备流式传输音视频时需要得到用户同意这一点。

许多状态机处理呼叫设置的方式根本不需要那么复杂。这也是造成问题的一个原因。不必要的线程、对复杂功能的依赖以及大量的状态和输入类型增加了该漏洞出现在信令状态机中的可能性。

另外,我并没有研究这些应用程序的任何群组呼叫功能,所有报告的漏洞都是在端对端呼叫中发现的。未来工作中我们可能会在这方面发现更多问题。

结语

我调查了7个视频会议应用程序的信令状态机,发现了5个漏洞,这些漏洞可以让主叫方设备强制接收设备传输音视频数据。这些漏洞后来都被修复了。目前我们还不清楚为什么该漏洞影响这么广泛,但缺乏对这类漏洞的认识以及信令状态机不必要的复杂性可能是其中的一个原因。信令状态机是视频会议应用中一个令人关注且未被充分调查的弱点,随着进一步的研究,我们很可能会发现更多的问题。


文章地址:https://googleprojectzero.blogspot.com/2021/01/the-state-of-state-machines.html

原文作者:Natalie Silvanovich

填写常用邮箱,接收社区更新

WebRTC 中文社区由

运营