0x0
2019 年 1 月 29 日,一个严重的漏洞在 FaceTime 群组通话被发现,它允许攻击者呼叫目标并强制目标用户没有交互的情况下接通呼叫,从而允许攻击者在目标不知情或不同意的情况下监听他们的周围环境。该错误的影响和机制都非常出色,不获得代码执行的能力而强制目标设备向攻击者设备传输音频可能是影响力空前的漏洞,该漏洞归咎于 FaceTime 呼叫状态机的一个逻辑错误,仅需通过界面交互即可触发。虽然这个错误很快被修复,但由于状态及逻辑错误造成如此严重又轻易的漏洞是我前所未见的,这一事实让我怀疑其他状态机是否也有类似的漏洞。这篇文章描述了我对多个消息传递平台的呼叫状态机的调查,包括 Signal、JioChat、Mocha、Google Duo 和 Facebook Messenger。
0x1 WebRTC 和状态机
大多数视频会议应用使用WebRTC实现,我在过去的博文中有过讨论。WebRTC 连接是通过在对等方(Peer)之间交换会话描述协议 (SDP) 中的呼叫建立信息来创建的,这个过程称为信令。信令不是由 WebRTC 实现的,它允许对等方在任何可用的安全通信消息中交换 SDP,通常通过 Web 应用程序的 WebSockets 以及消息应用的安全消息传递。
WebRTC 节点可以交换几种类型的 SDP。在典型的连接中,呼叫者通过发送 SDP 提议开始,然后被呼叫者以 SDP 应答进行应答。这些消息包含传输和接收媒体所需的大部分信息,包括编解码器支持、加密密钥等。在提议、应答交换之后,peer 可以将 SDP 候选发送给其他 peer。候选是两个对等方可以用来相互连接的潜在网络路径,SDP 候选包含 IP 地址和 TURN 服务器等信息。Peer 通常会向一个 peer 发送多个候选,并且在连接期间的任何时间都可以发送候选。
WebRTC 连接维护与提议、应答是否已被接收和处理相关的内部状态,然而,使用 WebRTC 的应用程序通常必须维护自己的状态机来管理应用程序的用户状态。用户状态如何映射到 WebRTC 状态是 WebRTC 集成方出于安全和性能而做出的设计选择。例如一些应用程序直到被叫用户与应用程序交互以接听电话才交换 SDP,也有一些应用程序建立P2P,并在被叫接到通知之前开始由主叫向被叫发送音频和视频。
无论做出如何的设计选择,从输入设备传输音频或视频都必须由使用 WebRTC 的应用程序代码直接启用。这通常使用称为轨道(Tracks)的功能来完成。每个输入设备都被视为一个“轨道”,并且必须在传输音频或视频之前通过调用 addTrack (或等效语义)将每个特定的轨道添加到特定的P2P连接。也可以禁用轨道,这对于实现静音和关闭相机功能非常有用。每个轨道还有一个 RTPSender 属性用于微调传输属性或禁用音频、视频传输。
从理论上讲,在音频或视频传输之前确保被叫同意应该是一个相当简单的事情,在向P2P连接添加任何轨道之前等待用户接受呼叫。但是当我查看实际应用程序时它们以许多不同的方式启用传输,其中大部分导致了允许主叫在没有被叫交互的情况下连接的漏洞。
0x2 Signal Messenger
我在 2019 年 9 月查看了Signal,当时该应用程序的呼叫建立流程与 WebRTC 文档中推荐的非常相近。
建立P2P连接,然后当被叫通过与用户界面交互接受呼叫时,将被叫的音轨添加到连接中。然后通过P2P连接向主叫发送一条消息,告诉它也移动到连接状态并添加轨道。
不幸的是,应用程序没有检查接收连接消息的设备是否是主叫设备,因此可以从主叫设备向被叫设备发送连接消息。这导致音频通话允许主叫听到被叫的周围环境。我通过更改 Signal 的开源代码来发送消息并重新编译攻击客户端来测试此错误。
该漏洞于 2019 年 9 月在客户端修复,此后 Signal 的信令代码已被使用更保守的状态机的 ringrtc 项目取代。
这个错误纯粹是在 Signal 的代码中,并不是由于对 WebRTC 功能的误解。状态机设计在很大程度上有效,需要用户同意才能传输音频,但未实施特定检查。
0x3 JioChat 和 Mocha
2020 年 7 月,我在测试 WebRTC 漏洞是否适用于JioChat 和Mocha Messenger 时偶然发现了两个非常相似的漏洞。他们都有类似的信令设计,服务器参与其中。
提议和应答通过服务器交换,然后主叫和被叫都将他们的候选发送到服务器。随后服务器存储这些候选,直到被叫与他们的设备交互并接受呼叫。然后创建P2P连接,当WebRTC进入其内部连接状态时,添加轨道从而开始音频和视频传输。
这种设计有一个基本问题,因为候选可以有选择地包含在 SDP 提议或应答中。在这种情况下,P2P连接将立即开始,因为在此设计中唯一可以阻止连接的是缺少候选,这反过来会导致来自输入设备的传输。我通过使用 Frida 将候选添加到这些应用程序创建的提议中来测试这一点。我能够在未经用户同意的情况下让 JioChat 发送音频,并让 Mocha 发送音频和视频。这两个漏洞在提交后很快就通过在服务器上过滤 SDP 得以修复。
这些问题是由于对 WebRTC 的工作方式的误解以及试图通过不寻常的信令设计来提高 WebRTC 性能造成的。通常,WebRTC 集成方必须决定是否要等到被叫应答呼叫才能建立P2P连接。尽早建立连接可以提高性能并防止用户在接听电话时不得不等待,但也大大增加了 WebRTC 的远程攻击面。这些应用程序试图通过这种设计在不牺牲安全成本的情况下提高性能,但没有考虑 WebRTC 可以启动P2P连接的所有方式。
对于集成方来说,在没有添加或启用轨道的任何 WebRTC 功能上对音频或视频传输进行门控通常不是一个好主意。首先,WebRTC 的许多功能都很复杂,因此很容易犯错误导致允许传输音频或视频。此外,如果被门控的功能不常用或不是安全功能,则将来可能难以对其进行测试或更改。
0x4 Google Duo
我在 2020 年 9 月查看了Google Duo。Duo 的信令方法与很多通信应用有些不同,因为它支持被叫在接听之前预览呼叫方的视频。因此需要在接听电话之前设置单向视频流。
上图显示了单向视频流的设置。虚线表示使用 Java 执行程序进行的异步调用。没有从被主叫到被叫的传输是通过两种方法强制执行的。首先,SDP 提议包含用于视频的属性a=sendonly,这会导致视频仅在一个方向上传输。此外,当被叫收到来自主叫的提议时,它会将视频轨道添加到P2P连接,然后使用轨道的RTPSender属性禁用它(在用户接受呼叫之前不会添加或启用音频轨道)。
这些方法都没有有效地防止视频从被叫传输到主叫。SDP 属性很容易绕过,因为主叫将 SDP 提供给被叫,因此可以轻松更改它。处理提议后立即禁用视频轨道应该可以工作,但异步设计除外。通常,setLocalDescription 方法(处理 SDP 提议)调用回调onSetSuccess ,然后在回调完成后建立P2P连接。但是,如果回调再次进行异步调用,则 在连接建立之前onSetSuccess完成的保证不再成立,因为setLocalDescription 方法只等待onSetSuccess 线程完成。这会在禁用视频和建立连接之间产生竞争,因此在某些情况下,被叫可以在禁用传输之前向呼叫方传输几个视频帧。
我通过使用 Frida 来改变被叫发送的 SDP 来测试这个,然后我尝试了很多方法来赢得比赛。事实证明,获胜相当困难,我花了大约两周的时间试图弄清楚如何减慢视频禁用呼叫的速度,以便让连接有时间进行设置。我最终发送了多个提议并将候选添加到提议中,这减少了连接时间,因为网络连接已经建立。然后我通过P2P连接的数据通道发送了许多需要很长时间处理的消息,以减慢视频轨道的禁用速度。数据消息在与禁用 Duo 中的视频轨道相同的线程队列中处理,因此发送数据消息填满了禁用视频所需的队列和许多其他条目,从而延迟了轨道的禁用。
此漏洞已于 2020 年 12 月通过从onSetSuccess 中删除异步调用得到修复。虽然 Duo 通常以有效防止从被叫到主叫的视频传输的方式设计信令,但异步实现该设计会引入问题。异步信令实现在移动应用程序中变得越来越普遍,因为在许多不可预测的情况下 WebRTC 需要在网络或对等点上等待,并且将函数调用分离到不同的线程意味着一次调用的延迟不会影响无关的功能。然而,异步调用使得建模状态机在所有情况下的行为变得更加困难,因此在向 WebRTC 信令中添加异步调用时务必谨慎。在这种情况下,禁用视频轨道的异步调用对性能没有任何影响,onSetSuccess 已经在它自己的线程中运行,并且可以让步给更高优先级的线程。平衡异步调用的风险和收益并且不要不加选择地将它们包含在应用程序中很重要。
0x5 Facebook Messenger
我在 2020 年 10 月查看了 Facebook Messenger。这是一个相当具有挑战性的目标,因为需要大量的逆向工程。退一步说 ,WebRTC 具有多种编程语言的绑定,允许将其集成到使用该语言的应用程序中。大多数集成 WebRTC 的 Android 应用程序都使用 Java 绑定。这使得调查信令状态机变得相当简单,因为重要的 Java 函数,例如 setLocalDescription(处理提议和应答)、addRemoteIceCandidate(处理候选)和 addTrack(向连接添加跟踪)可以在 Frida 中挂钩并记录以进行分析。使用这些调用更改攻击者设备的行为也相当简单。
Facebook Messenger 不使用 Java 绑定来集成 WebRTC,而是使用 C++ 绑定。此外,静态链接的WebRTC到一个更大的库(librtcR20.so,这很可能是这个中提到的RSYS库文章),所以调用绑定符号已被出去,使它们难以 Hook。此外,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)。总之,这段代码可以用下面的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 进行 Hook,并将其字节数组参数输入到生成的解串器中,解码传入的消息。
我找到了一个类似的用于发送thrift消息的方法,sendThriftToPeer (这个方法的类名在Facebook Messenger的每个版本中都被混淆和变化,但可以通过grepping应用程序的smali找到它)。我还能够挂钩此方法,并更改其字节数组参数,以更改 Facebook Messenger 发送的P2PMessageRequest消息。
现在,我能够理解 Facebook Messenger 的信令状态机。根据用户登录 Facebook Messenger 的位置,可以通过两种不同的方式发出信令。如果用户在多个设备或浏览器上登录,则在被叫与其设备交互之前几乎不会发生任何事情。提议、应答和候选被交换,但它们由被叫设备存储并且在被叫用户应答呼叫之前不会被处理。这是有道理的,因为 Facebook Messenger 不知道要连接到什么设备。
如果被叫仅在单个设备上登录,则状态机更有趣。
在这种情况下,Facebook Messenger 会在收到提议后立即启用轨道,但会更改提议以使所有呼出数据流都处于非活动状态。然后,当用户与设备交互时,它会将提议替换为处于活动状态的提议。
我担心可能有办法绕过提议的更改,但当我查看了这是如何完成后,不建议使用除添加或禁用轨道以外的任何其他方法来禁用输入设备传输,它是相当健壮。在将 SDP 解码为内部 WebRTC 对象后更改提议,并且直接对该对象进行更改,从而消除了解析错误的可能性。
但是,查看传入消息的处理方式时,我注意到在接听电话之前处理了除提议、应答和候选之外的许多消息类型。一种突出的类型称为SdpUpdate 。当收到SdpUpdate消息时,通过调用setLocalDescription更新本地提议或应答。
这个消息类型在发送到上面的状态机时没有做任何事情,因为它已经在存储 SDP 并等待调用setLocalDescription 。但是在用户同时登录两台设备的情况下,会导致调用setLocalDescription 并启动音频连接。
目前尚不清楚SdpUpdate 消息类型在 Facebook Messenger 中的用途。我在我的测试设备上尝试了很多场景,包括网络切换,在正常使用中无法生成一个。无论如何,显而易见在应答呼叫之前不打算接收此消息类型。它类似于上面描述的信令错误,因为它与应用程序对 WebRTC 的使用无关,而是由于在处理可能导致状态转换的输入时缺少检查。
此漏洞已于 2020 年 11 月通过服务器更改得到修复,防止在连接呼叫之前发送此消息类型。
0x6 其他应用
我查看了其他一些应用程序,但没有发现它们的状态机存在问题。我在 2020 年 8 月查看了 Telegram,就在Telegram应用添加了视频会议之后。我没有发现任何问题,主要是因为在被叫接听电话之前,应用程序不会交换提议、应答或候选。我在 2020 年 11 月查看了 Viber,并没有发现他们的状态机有任何问题,尽管该应用程序的逆向工程挑战使这种分析不如我查看的其他应用程序那么严格。
0x7 讨论
我调查的大多数调用状态机都存在逻辑漏洞,允许在未经被叫同意的情况下将音频或视频内容从被主叫传输到主叫。在保护 WebRTC 应用程序时,这显然是一个经常被忽视的领域。
大多数错误似乎不是由于开发人员对 WebRTC 功能的误解。相反,它们是由于状态机实现方式的错误造成的。也就是说,对这些类型的问题缺乏认识可能是一个因素。很少有 WebRTC 文档或教程明确讨论从用户设备流式传输音频或视频时需要用户同意。
许多这些状态机在处理呼叫建立方面具有不必要的复杂性,这也是一个因素。不必要的线程、对模糊特征的依赖以及大量的状态和输入类型增加了信令状态机中发生此类漏洞的可能性。
还需要注意的是,我没有查看这些应用程序的任何群组通话功能,所有报告的漏洞都是在P2P通话中发现的。这是未来工作的一个领域,可能会揭示其他问题。
0x8 结论
我调查了7个视频会议应用程序的信令状态机,发现了5个漏洞,可能允许主叫设备强制被叫设备传输音频或视频数据,所有这些漏洞都已得到修复。目前尚不清楚为什么这是一个如此普遍的问题,但对这些类型的错误缺乏认识以及信令状态机中不必要的复杂性可能是一个因素。信令状态机是视频会议应用程序的一个令人担忧且研究不足的攻击面,随着进一步研究很可能会发现更多问题。