深入考察状态机中危险的逻辑漏洞
2019年1月29日,研究人员在Group FaceTime中发现了一个严重的安全漏洞,攻击者可以利用该漏洞呼叫目标用户并强制接通,而无需与目标用户进行互动——也就是说,攻击者可以在受害者毫无察觉的情况下监听其周围环境。对于这个漏洞来说,无论是其危害程度,还是其原理,都是令人叹为观止的。首先,无需获得代码执行权限,攻击者就能强制目标设备将音频传输到自己的设备上,就这一点来说,其安全威胁是前所未有的。此外,该漏洞是FaceTime的呼叫状态机中的一个逻辑漏洞,仅通过设备的用户界面就可以利用该漏洞。尽管该漏洞很快就得到了修复,但考虑到仅仅因为呼叫状态机中的一个逻辑错误,就导致了这样一个严重且易于利用的漏洞,我不禁怀疑其他状态机中也可能存在类似的漏洞。本文描述了我对多种消息传递平台(包括Signal、JioChat、Mocha、Google Duo和Facebook Messenger)的呼叫状态机的考察过程。
WebRTC与状态机
当前,大部分视频会议应用都是使用WebRTC实现的;关于WebRTC,我们的前几篇文章中已经讨论过了。WebRTC连接是通过在对等体之间交换会话描述协议(SDP)中的呼叫建立信息来创建的,这个过程被称为信令。不过,WebRTC并没有实现信令,而是允许对等体以任何可用的安全通信消息机制(通常情况下,对于Web应用来说就是WebSockets;对于消息应用来说就是安全消息传递)来交换SDP。
WebRTC对等体可以交换的SDP分为多种类型。在一个典型的连接中,主叫方首先发送一个SDP提议,然后被叫方用SDP应答来进行响应。这些消息包含了传输和接收媒体所需的大部分信息,包括支持的编解码器、加密密钥等。在交换提议/应答之后,对等体可以向其他对等体发送SDP候选。SDP候选是两个对等体可以用来互连的潜在网络路径,SDP候选包含IP地址和TURN服务器等信息。对等体通常向一个对等体发送多个SDP候选,SDP候选可以在连接过程中的任意时刻发送。
WebRTC连接会维护一个内部状态,该状态与是否已接收并处理提议或应答有关,然而,使用WebRTC的应用程序通常必须维护自己的状态机,以管理应用程序的用户状态。用户状态如何映射到WebRTC状态是由WebRTC integrator的设计方案决定的,这些方案对安全性和性能都有很大的影响。例如,对于一些应用程序来说,在被叫用户应答应用程序发出的呼叫之前,是不会交换任何SDP的;同时,对于另外一些应用程序来说,在建立点对点连接时,在被叫用户还没有接到呼叫通知之前,就开始从主叫方向被叫用户发送音频和视频了。
无论设计方案如何,从输入设备传输音频或视频必须由使用WebRTC的应用程序代码直接启用。这通常使用名为轨道(Tracks)的特性来完成。每个输入设备都被认为是一个轨道,每个特定的轨道必须在音频或视频传输之前通过调用addTrack(或等效语言)添加到特定的对等连接中。当然,我们也可以禁用轨道,这对于实现静音和关闭摄像功能很有用。每个轨道还有一个RTPSender属性,不仅可用于微调传输的各种属性,也可用于禁用音频或视频传输。
从理论上讲,在音频或视频传输之前确保被叫方同意应该是一件相当简单的事情,也就是说要等待用户接受呼叫之后再将轨道添加到对等连接。然而,当我观察真实的应用程序时,发现它们竟然以五花八门的方式启动传输。并且,其中大多数方式都存在安全漏洞,使得呼叫在无需被叫方交互的情况下被接通。
Signal Messenger
2019年9月,我们对Signal进行了安全审查,当时该应用的呼叫设置与WebRTC文档中推荐的非常相似。
首先,它会建立一个点对点连接,然后,当被叫方(callee)通过与用户界面交互接受呼叫时,被叫方的轨道就会被添加到连接中。然后,通过点对点连接向主叫方发送消息,让其也切换到连接状态并添加轨道。
遗憾的是,该应用程序并没有检查接收连接消息的设备是否为主叫设备,因此,攻击者可以从主叫设备向被叫设备发送连接消息。这导致了音频呼叫的连接,使主叫方能够听到被叫方的周围环境。我通过修改Signal的开源代码来发送消息,并重新编译攻击客户端对这个漏洞进行了相应的验证。
这个漏洞在2019年9月在客户端中得到了修复,从那时起,Signal的信令代码已经被使用更保守状态机的ringrtc项目所取代。
这个安全问题纯粹是Signal的代码所致,而不是因为对WebRTC功能的误解所致。在这里,状态机的设计基本上是有效的:要求用户同意传输音频,但是,它并没有实现特定的检查。
JioChat与Mocha
2020年7月,我在测试WebRTC漏洞是否会对它们起作用时,意外发现了JioChat和Mocha中两个非常相似的漏洞。它们都有一个类似的信令设计:以服务器为媒介。
首先,它们通过服务器交换提议和应答,然后主叫方和被叫方都将其的候选发送到服务器,然后,服务器将它们存储起来,直到被叫方与其设备交互并接受呼叫。然后,建立点对点的连接,当WebRTC进入内部连接状态时,就会添加轨道,启动音频和视频的传输。
这种设计存在一个根本性的安全隐患:攻击者可以选择性地将SDP候选包含在SDP提议或应答中。在这种情况下,点对点连接将立即成功,因为在这种设计中,唯一阻止连接的情况就是缺乏SDP候选,这反过来又导致从输入设备进行传输。为此,我通过使用Frida将候选添加到每个应用程序创建的提议中进行测试。我能够让JioChat在未经用户同意的情况下发送音频,以及让Mocha在未经用户同意的情况下发送音频和视频。这两个漏洞在提交后不久就通过过滤服务器上的SDP进行了修复。
这些问题是由于对WebRTC工作方式存在误解,再加上试图通过不寻常的信令设计来提高WebRTC性能所致。通常情况下,WebRTC集成商必须决定是否要等到被叫方应答呼叫后再建立点对点连接。提前建立连接虽然可以提高性能,避免用户在应答呼叫过程中的等待过程,但也大大增加了WebRTC的远程攻击面。这些应用试图通过这种设计来提高性能而不付出安全代价,但没有考虑到WebRTC可以启动点对点连接的所有方式。
对于集成商来说,在不添加或启用轨道的任何WebRTC功能上门禁(gate)音频或视频传输通常都不是一个好主意。首先,许多WebRTC功能都很复杂,所以很容易犯错,从而允许传输音频或视频。另外,如果被门禁的功能不是常用的,或者不是安全功能,那么将来可能无法进行充分的安全测试或难以进行修改。
Duo
我们于2020年9月对Google Duo进行了安全审计。Duo的信令方法有点与众不同,因为它支持这样一个功能,即在应答之前,被叫方可以预览主叫方的视频。为此,在应答呼叫之前,它需要建立一个单向视频流。
上图是单向视频流的建立过程。虚线代表使用Java执行器进行异步调用。从被叫方到主叫方的传输限制是通过两个方法来实现的。首先,SDP提议中包含了视频的属性a=sendonly,这使得视频只能朝一个方向传输。另外,当被叫方接收到主叫方的提议时,会将视频轨道添加到对等连接中,但随后使用轨道的RTPSender属性将其禁用(在用户接受呼叫之前,音频轨道不会被添加或启用)。
这两种方法都不能有效地阻止视频从被叫方传输到主叫方:SDP属性很容易被绕过,因为这里是由主叫方向被叫方提供SDP的,所以,主叫方很容易对SDP进行篡改。另外,处理提议后立即禁用视频轨道应该是可行的,但异步设计除外。正常情况下,setLocalDescription方法(处理SDP提议)会调用回调函数onSetSuccess,然后,在回调函数运行结束后才建立对等连接。但是,如果回调函数再进行一次异步调用,那么就无法保证onSetSuccess函数在建立连接之前结束,因为setLocalDescription方法只等待onSetSuccess线程完成即可。这就造成了禁用视频和建立连接之间的竟态条件,所以在某些情况下,被叫方可以在禁用传输之前向主叫方传输多个视频帧。
为了进行测试,我们使用Frida对被叫方发送的SDP进行了修改,并尝试了很多方法来赢得竞争。结果发现在竞争中取胜是相当困难的一件事情,我花了大概两周的时间,来搞清楚如何降低视频禁用呼叫的速度,以便为建立连接争取足够的时间。最后,我们采用的方法是发送多个提议,并在提议中添加相应的候选,从而减少连接时间,因为网络连接已经建立起来了。然后,我通过对等连接的数据通道发送了许多需要长时间处理的消息,以推迟视频轨道的禁用时间。在Duo中,由于处理数据消息线程与禁用视频轨道的线程位于同一个线程队列中,所以通过发送大量数据消息来填满这个队列,就能延迟轨道被禁用的时间。
这个漏油已于2020年12月得到了修复,方法是删除OnSetSuccess中的异步调用。虽然Duo设计的信令通常能有效防止视频从被叫方传输到主叫方,但该设计方案的异步实现经常会出现问题。目前,异步信令的实现在移动应用上变得越来越常见,因为有很多不可预知的情况下,WebRTC需要在网络或对等体上进行等待,将函数调用分离到不同的线程中,意味着一次调用的延迟不会影响到不相关的功能。然而,异步调用使得对状态机在所有情况下的表现进行建模变得更加困难,因此在向WebRTC信令添加异步调用时务必谨慎。在本例中,禁用视频轨道的异步调用在性能方面没有任何增益,因为禁用轨道的任何调用都没有理由阻塞,而且onSetSuccess已经在自己的线程中运行,可以让位于更高优先级的线程。重要的是要平衡异步调用的风险和收益,切莫不加区别地将它们塞到应用程序中。
Facebook Messenger
2020年10月,我们对Facebook Messenger的安全性进行了审计。这是一个相当具有挑战性的目标,因为需要进行大量的逆向分析工作。退一步讲,WebRTC提供了多种编程语言的绑定,使其可以集成到使用相应语言编写的应用程序中。不过,大多数集成WebRTC的Android应用都使用了Java绑定。这使得研究信令状态机变得相当简单,因为许多重要的Java函数,如setLocalDescription(处理提议和应答)、addRemoteIceCandidate(处理候选路径)和addTrack(将轨道添加到连接中)函数等,都可以使用Frida进行hook,并记录相关信息,以供分析之用。除此之外,使用这些调用来改变攻击者设备的行为也是相当简单的事情。
然而,Facebook Messenger并没有使用Java绑定来集成WebRTC,而是使用了C++绑定。此外,由于它是静态地将WebRTC链接到一个更大的库(librtcR20.so,很可能就是本文提到的rsys库)的,所以,调用绑定的符号会被剥离,这使得它们很难被hooked。此外,Facebook Messenger在传输SDP之前会将SDP序列化成另一种格式,因此,我们很难通过监控流量来了解信令的工作机制。
我最终意识到,要想弄清楚FacebookMessenger信令的工作原理,唯一可行的方法就是搞清楚其网络协议。值得庆幸的是,Facebook已经公开表示他们使用的协议是fbthrift,这是thrift的一个分支。因此,我们可以把librtcR20.so库加载到IDA中,看看能不能找到它调用thrift库的代码。令人遗憾的是,尽管找到了一些相关的调用,但大部分的代码都是静态链接的。我最终发现,这是因为thrift每实现一个协议都会生成序列化代码,所以大部分序列化和反序列化代码最后都会与协议处理代码一起编译。所以,我决定亲自编译fbthrift,创建一个示例序列化程序,并利用IDA对其进行考察,这样,我就可以对编译后的fbthrift序列化程序有一个感性的认识了。我注意到,在序列化过程中,对象的成员是通过调用一个叫做writeFieldBegin的方法来进行序列化的。此外,我还注意到,当调用此方法时,字段名是必需的,即使它通常不包括在序列化输出中。因此,我开始在librtcR20中查找这样一个函数:它会被非常频繁调用,并且使用不同的字符串参数,而且这些字符串看起来应该像是字段名。符合这个标准的函数并不多,所以,我们能够轻松找出writeFieldBegin函数。
此时,我们发现很多地方的对象都是序列化的,因此,我们需要确定哪一个是用来设置WebRTC调用的消息。
同时,我还注意到该库中有一个名为P2PCall::OnP2PMessageFromPeer的方法(注意,这个方法的符号是被剥离的,但在调用时,方法名会被记录下来)。它似乎是用于处理反序列化消息的。通过搜索字符串“P2PMessage”,我找到了名为P2PMessageRequest的类型的序列化代码。我认为,这就是创建呼叫建立消息(call setup messages)的地方。
Thrift序列化代码是根据thrift定义文件中的类定义生成的。基于传递给writeFieldBegin的字段名称和类型,我能够慢慢地对这种类型的完整thrift定义进行逆向工程。这是一项非常繁琐的工作,因为定义相当长,并且代码经过了混淆处理,使寄存器的使用并不一致,因此,我认为任何自动方法都很难搞定这个问题。
以下是序列化代码的示例。
请注意,这里将写入两个字段,它们来自类型为Extmap的对象。第一个字段名为id,是必填字段。下面是实现写入功能的函数的具体代码:
在这里,写入的字段标识符为1,字段类型为8,将被转换为i32类型(32位整数)。第二个字段是一个可选字段,其值来自相应的寄存器,具体代码如下所示:
其中,将字段名称设置为uri,将字段标识符设置为2,并将字段类型设置为8(也为i32)。总之,所有这些代码都可以用下面的thrift定义来表示。
struct Extmap{
1: i32id
2:optional i32 uri
}
在对P2PMessageRequest类型的每个字段进行类似的逆向分析之后,便得到了thrift的完整定义,完整代码可以在这里找到。
之后,我用这个thrift的定义做了两件事。首先,我用它弄清楚了P2PMessageRequest类型的布局的C++表示形式。这是极有价值的,因为我可以藉此将该结构体的定义加载到IDA中,并能正确命名每一个字段。这使得理解P2PCall::OnP2PMessageFromPeer中如何处理传入消息变得更加轻松。当然,这还是需要一些过程的。此外,fbthrift可以直接从thrift的定义中生成C++头文件,但这些文件非常长,因为其中包含了很多不必要的定义,并且无法被IDA处理。所以,我最后把生成的源代码进行编译后加载到了IDA中,然后,导出该结构体的定义,被将其导入到另一个已经加载了librtcR20.so的IDA实例中。在相应的汇编代码中,有几个字段的大小与Facebook的并不完全一致,但差别也不是很大,通过修修改改,还是能让它们正常使用的。
下面是在IDA中导入thrift定义后,经过反编译得到的代码示例,旨在为大家展示,如果利用这种方式来理解消息对象的处理过程的话,事情会变得多么轻松。
我还能够解码并生成通过网络发送的消息。为此,我可以使用thrift的Python定义生成相应的序列化代码,因为thrift支持多种语言的代码生成。这样,使用Frida Python来钩住Facebook Messenger中的函数时,我就可以导入这段代码了。
然后,我需要找到处理传入的P2PMessageRequest消息的代码。因为这些消息是由本地代码处理的,而大多数Facebook消息是由Java代码处理的,所以,我们可以将搜索目标定为具有合适名称的本地调用,例如com.facebook.webrtc.WebrtcEngine.onThriftMessageFromPeer。之后,我通过Frida来hook这个方法,并在生成的反序列器中为提供了相应的字节数组参数,这样,它就可以对传入的消息进行解码了。
我发现了一个类似的方法,即sendThriftToPeer,可用来发送thrift消息;需要注意的是,这个方法的类名经过了模糊处理,并且在每个版本的FacebookMessenger中都会发生变化,但可以通过grepping工具搜索该应用程序的smari找到它。此外,我们也可以hook这个方法,并改变它的字节数组参数,从而改变Facebook Messenger发送的P2PMessageRequest消息。
这样的话,我能够考察FacebookMessenger的信令状态机了。根据用户登录Facebook Messenger的位置,该软件可以通过两种不同的方式发送信令。如果用户在多个设备或浏览器上登录,那么在被叫方与他们的设备交互之前,几乎不会发生任何操作。尽管会交换提议、应答和候选路径,但它们只是由被叫方设备存储起来,直到被叫方用户应答呼叫时,才会对其进行相应的处理。这么做是非常有道理的,因为Facebook Messenger不知道要连接到什么设备。
如果被叫方只在单一设备上登录,那么状态机就更有意思了。
在这种情况下,Facebook Messenger一旦收到提议就会启用轨道,并修改提议,以使所有传出流(outgoing streams)都处于非活跃状态。然后,当用户与设备交互时,替换之前的提议,使传出流(outgoing streams)进入活跃状态。
我担心的事情是:可能存在某些方法,可以绕过提议的修改操作,并研究了这些方法的实现方式。但是,本人不建议使用除添加或禁用轨道之外的任何方法来禁用输入设备的传输操作,因为这种方法是十分健壮的。在SDP解码成一个内部的WebRTC对象后,提议就会被修改,而且是直接对这个对象进行修改,这就消除了解析错误的可能性。
但是,在研究传入消息的处理方式时,我发现除了提议、应答和候选路径之外的许多消息类型都是在呼叫被应答之前处理的。其中,最突出的一种类型名为SdpUpdate。当收到SdpUpdate消息时,该应用就会通过调用setLocalDescription函数来更新本地提议或应答。
这种消息类型发送到上面的状态机时,并没有执行任何操作,因为它已经在存储SDP并等待调用setLocalDescription。但在用户登录两台设备的情况下,则会调用setLocalDescription函数,并启动音频连接。
目前,我们还不清楚SdpUpdate消息类型在Facebook Messenger中的用途。我在测试设备上尝试了许多场景,包括网络切换,但在正常使用情况下都无法生成这种类型的消息。但是,有一点是很明显的:在呼叫被应答之前并不打算接收该消息类型。它与上面描述的信令漏洞类似,因为它与应用程序使用WebRTC无关,而是由于在处理可能导致状态转换的输入时缺少必要的安全检查所致。
该漏洞已于2020年11月得到了相应的修复:在呼叫被接通之前,服务器将禁止发送该消息类型。
其他应用程序
除此外,我们还对其他一些应用程序进行了安全审查,但是,在它们的状态机并没有发现安全问题。2020年8月,Telegram添加了视频会议功能后,我们对其进行了审查,但是没有发现任何问题,这在很大程度上是因为该应用程序在被叫方应答呼叫之前不会交换任何提议、应答或候选路径。2020年11月,我们考察了Viber,也没有发现其状态机有任何问题,当然,由于该应用的逆向分析非常棘手,使得审查过程没有像其他应用程序那么严格。
进一步探讨
我们审查过的大多数呼叫状态机都存在逻辑漏洞,这些漏洞允许音频或视频内容在未经被叫方同意的情况下从被叫方传输到主叫方。由此看来,在保护WebRTC应用程序时,这确实是一个非常薄弱或者说被忽略的环节。
同时,就我们发现的大多数漏洞来说,它们似乎并不是因开发人员对WebRTC特性的误解造成的。相反,它们是由于状态机实现方式中的错误造成的。也就是说,对这类问题缺乏认识可能是造成这种状况的原因之一:鲜有WebRTC文档或教程明确地讨论过从用户设备流传输音频或视频时需要获得用户同意的问题。
另外一个原因,许多状态机在处理呼叫创建时,把事情搞的太复杂了。例如,不必要的线程,对晦涩功能的依赖以及大量的状态和输入类型等,都增加了信令状态机中出现该类漏洞的可能性。
同样令人关切的是,我尚未审查这些应用程序的群组呼叫(group calling)特性,而目前曝出的所有漏洞都是在对等呼叫(peer-to-peercall)中发现的。这是今后发力的一个领域,可能会暴露出更多的安全问题。
小结
在本文中,我们考察了7个视频会议应用程序的信令状态机,并发现了5个漏洞;主叫设备可以利用这些漏洞强制被叫设备传输音频或视频数据。这些漏洞后来都被修复了。目前还不清楚为什么这种类型的漏洞会如此普遍,但缺乏对这类漏洞的认识以及信令状态机不必要的复杂性可能是原因之一。信令状态机是视频会议应用中一个非常值得关注的攻击面,因为目前尚未被充分研究;随着研究的深入,将来很可能会曝出更多的安全漏洞。
本文由secM整理并翻译,不代表白帽汇任何观点和立场
来源:https://googleprojectzero.blogspot.com/2021/01/windows-exploitation-tricks-trapping.html
最新评论