-
-
[翻译]状态机的状态
-
发表于: 2021-3-5 10:43 11024
-
原文链接
作者:Natalie Silvanovich, Project Zero
译者:阳春
翻译时间:2021/3/5
译者注:转载清注明作者、译者和出处
2019年1月29日,Group Facetime(译者注,Apple的视频聊天)被发现一系列漏洞,攻击者可以呼叫目标并不经用户允许强制接通呼叫,这样攻击者就可以在用户不知情不同意的情况下监听其周围的声音。该漏洞的影响和机制相当出色。能够强制目标设备发送音频到攻击者的设备而不用执行远程代码是很不同寻常并且可能前所未见的。更有趣的是,该漏洞是Facetime通信状态机的逻辑漏洞,仅仅通过设备的用户界面就可以利用。虽然这个漏洞很快被修复了,但是它揭示了通信状态机中的逻辑漏洞利用起来如此简单(一个我从未设想的攻击场景),让我想知道其它的通信状态机是否有同样的问题。这篇文章是我对众多通信平台的研究报告,包括了Signal,JioChat,Mocha,Google Duo和Facebook Messenger。
WebRTC和状态机
大部分视频会议应用通过WebRTC实现,之前的几篇博客我已有讨论过。建立WebRTC端点连接需要通过会话描述协议(Session Description Protocol,SDP),这个过程叫做呼叫(signalling)。WebRTC未实现呼叫过程,允许通信端点在任意的安全消息通道中交换SDP,通常web应用使用WenSockets,消息应用使用安全消息。
WebRTC端点可以交换几种类型的SDP。典型的连接从呼叫者发送SDP询问消息(offer)开始,然后被呼叫者通过SDP答复消息(answer)回应。这些消息中含有收发内容所需的大部分信息,包括编码支持,加密密钥等等。询问/答复交换完成后,收发端点可以向其它端点发送SDP候选消息(candicates)。候选端点是两端可以连接起来的潜在网络路径,SDP候选消息含有IP地址和隧道服务器等信息。收发端点通常发送多个候选消息,而且通讯期间任何时候都可以发送。
WebRTC连接会维护与询问/答复消息是否被接收和处理相关的内部状态,然而,使用了WebRTC的应用通常需要维护特有的状态机以管理应用的用户状态。用户状态如何映射到WebRTC状态就由WebRTC集成者来设计,需要考虑安全和性能因素。举例来说,有些应用在被呼叫者交互答复之前不会交换任何SDP,而有些应用在通知被呼叫者之前就建立P2P连接,从呼叫者向被呼叫者发送视频和音频。
无关设计的是,从输入设备获取音频或者视频都必须调用WebRTC代码实现。通常这个功能叫音轨/视轨(track)。输入设备被看作一个“轨道”,在传输视频和音频之前,每个特定轨道都必须通过调用addTrack(或类似的)加入到特定的端点。轨道也可以被禁用以实现静音和关闭摄像头功能。每个轨道有个RTPSender属性用于传输微调,也可以用于禁用传输。
理论上,确保在用户同意前就传输音频或者视频是很简单的,在用户同意之前不要添加任何轨道就可以了。然而,当我研究真实应用时发现它们使用各种方式来开启传输。大部分都会导致未交互即可连接的漏洞。
Signal
2019年九月我研究了Signal,那个时候,这个应用的呼叫设置于WebRTC文档的建议很接近。
先建立端到端的连接,然后被呼叫者的录音轨道在界面交互同意之后被加入到连接。然后一个消息通过P2P连接发送给呼叫者,告知其也进入connected状态并加入轨道。
不幸的是,应用并未检查此消息的接收者是不是呼叫者设备,所以存在一种呼叫者向被呼叫者发送connect消息的可能性。这导致录音设备被连接,呼叫者可以听到被呼叫者周围的声音。我通过修改Signal的开源代码,重编译客户端,发送攻击消息进行了测试。
该漏洞在2019年9月的一个版本被修复,从那之后,Signal的呼叫代码来自ringrtc项目,使用了一个更保守的状态机。
这个漏洞纯粹由Signal代码引起,并非由于对WebRTC功能的误解。状态机设计很大程度上是有效保障了用户同意的,出问题仅是因为没有做一个特定的检查。
JioChat和Mocha
2020年7月我测试WebRTC漏洞的时候,偶然发现了JioChat和Mocha的两个漏洞。它们的呼叫设计很接近,是经过服务器介入的。
询问和答复通过服务器交换,然后呼叫者和被呼叫者都把候选消息发给服务器。服务器存储它们直到被呼叫者通过设备交互并且同意接通。随后端对端的连接建立,WebRTC进入其内部的connected状态,轨道被添加,音频和视频开始传输。
这个设计有个基本问题,因为在SDP询问/答复中是可以添加候选消息的。在这种情况下,p2p连接会立即建立,因为在设计中阻止连接建立的唯一因素就是没有候选消息;当然随后输入设备的传输就会开始了。我通过Frida向应用的询问消息中添加候选消息进行了测试。导致在未得用户允许的情况下,JioChat发送了语音,Mocha发送了语音和视频。随后他们通过在服务器上过滤SDP很快修复了这两个漏洞。
产生这几个问题的原因是对WebRTC的工作方式有所误解并且试图使用不常见的呼叫设计来提升性能。通常,WebRTC集成者必须决定是否要等到答复再建立P2P连接。提前建立连接可以改善性能并且减少等待,但是会大大增加WebRTC的攻击面。这几个应用的设计试图改善性能而不增加安全消耗,但是没有考虑周全WebRTC可以建立连接的所有路径。
通常来讲,集成者不通过WebRTC轨道而是其它功能来传输音频和视频不是个好主意。首先,很多WebRTC功能很复杂,所以允许传输的时候容易犯错。其次,如果用来控制的功能不常被使用或者不是个安全的功能,它有可能没被充分测试或者将来会变更。
Duo
2020年9月我研究了Google Duo。Duo的呼叫方式跟其他的很不一样,因为它支持被呼叫者在答复之前预览呼叫者的视频。所以在答复前一个单向的传输流就建立了。
上图展示了单向视频流如何建立。虚线表示Java异步调用方法。阻止被呼叫者传输到呼叫者的方法有两个。第一个,SDP询问消息包含了视频属性 a=sendonly,导致了视频的单向传输。第二个,当被呼叫者拿到呼叫者的询问时,它将视频轨道添加到了端点连接中,但是随后通过RTPSender属性禁用了它(音频轨道在用户同意之前未被添加)。
两者都无法有效防止视频从被呼叫者向呼叫者传输。SDP属性很容易被修改绕过,因为SDP询问是被呼叫者给呼叫者的。禁用视频轨道应该有效,但是它设计成了异步的。通常来说,setLocalDescription方法(处理SDP询问的方法)会调用回调方法onSetSuccess,然后在回调结束之后设置好p2p连接。然而,如果回调又进行了另一个异步调用,就无法保证onSetSiccess在设置连接之前结束,因为setLocalDescription仅仅等待onSetSuccess结束。这导致了禁用视频和设置连接之间的竞速,所以在某些状况下,在禁用视频前被呼叫者会向呼叫者传输一小段视频。
我使用Firda修改了SDP询问消息属性,然后尝试了各种方法试图赢得竞速。事实证明,这是很难的,我花了大概两周时间才搞清楚如何降低禁用视频的速度,给连接设置腾出足够的时间。最终我发送多个询问消息,并附带候选消息,以此减少了连接时间,因为网络连接已经建立了。然后我通过p2p连接的数据通道发送了很多需要大量时间处理的信息,降低了禁用视频轨道的速度。数据信息是在与禁用视频轨道的同一个线程队列上处理的,所以发送大量其它的数据信息来填充用来禁用视频轨道的队列可以延缓其执行。
这个漏洞在2020年12月被修复,移除了onSetSuccess里的异步调用。虽然Duo设计了一个有效防止视频从被呼叫者向呼叫者传输的方案,但是异步调用导致其失败。呼叫的异步实现在移动应用上越来越常见,因为有很多不可预料的WebRTC需要等待网络,端点的情景,而分拆函数调用到不同的线程意味着一个调用的延迟不会影响无关的功能。然而,异步调用使建模状态机在所有情况下的行为变更困难了,所以向WebRTC呼叫添加异步调用要谨慎。在这个案例中,禁用视频轨道的异步调用对性能没有任何影响,所以没有任何理由阻塞禁用视频轨道的调用,而且onSetSuccess已经在独立的线程运行,可以给高优先级的线程让道。平衡异步调用的风险和收益是很重要的,不要不加考虑就直接使用。
Facebook Messenger
2020年10月我研究了Facebook Messenger。这是个极具挑战性的目标,因为需要大量的逆向工程。退一步来说,WebRTC有一些不同语言的绑定来让应用进行集成,大部分安卓应用使用WebRTC的Java绑定进行集成。这让研究呼叫状态变得很简单,因为重要的Java函数,比如setLocalDescription(处理询问/答复),addRemoteIceCandidate(处理候选消息)和addTrack(向连接添加轨道)可以被Frida hook并且输出日志以便分析。使用这些函数来改变攻击设备的行为也相当简单。
Facebook Messenger没有使用Java绑定集成WebRTC,而是C++绑定。而且,它静态链接WebRTC到一个更大的库(librtcR20.so,看起来像是这篇文章提到的rsys库),所以绑定函数的符号被抹去了,使得分析变得困难。此外,Facebook Messenger序列化SDP成另一种格式,所以通过抓包很难观察其如何呼叫。
最后我意识到唯一可行的研究Facebook Messenger呼叫的方式是理解其网络协议。好在Facebook公开表示他们使用fbthrift,是thrift的一个分支。我在IDA中加载了librtcR20.so库,看能否找到它调用thrift的位置,虽然有几次调用的样子,但是看起来大部分代码都被静态链接了。最后我发现这是因为thrift为每个实现的协议生成序列化代码,所以大部分序列化和反序列化代码最终都使用了协议处理代码进行编译。所以我决定编译fbthrift,搞一个示例序列化程序到IDA看看,这样我才能对编译后的fbthift序列化程序有个印象。我注意到在序列化过程中,对象的成员由writeFieldBegin序列化。我同时注意到当此方法被调用,会使用字段名称,而这通常不会出现在序列化后的输出。所以我在librtcR20里寻找一个被频繁调用并带有看起来像字段名称的字符串参数的函数。没有很多满足该条件的函数,因此我能够找出writeFieldBegin。
这时候我能够找到很多对象被序列化的地方,要看看哪个是用来设置WebRTC调用的信息。
早些时候,我注意到库里面有个函数P2PCall::OnP2PMessageFromPeer(注意符号其实被抹掉了,但是函数名在调用时被日志记录下来)。这个函数看起来是反序列化消息被处理的地方。搜索字符串“P2PMessage”,我找到一个叫P2PMessageRequest的某类型的序列化函数。我假设这是呼叫设置消息创建的地方。
Thrift序列化代码的生成基于thrift定义文件里的类定义。基于传给writeFieldBegin的字段名称和类型,我慢慢的逆向thift对此类型的完整定义。这是项繁琐的任务,因为定义超级长,代码是混淆过的使得寄存器使用不一致,所以我不相信自动化分析会准确。
以下是序列化代码的一个例子。
注意它从一个Extmap类型的对象带来两个字段。第一个,叫id,是必填项。这个函数写出如下代码。
字段标识是1,字段类型是8,译成了i32(32位整数)。第二个字段是可选项,以下代码设置其寄存器。
字段名是uri,字段标识是2,字段类型是8(一样的i32)。加起来,这些代码可以被以下thrift定义表示:
1 2 3 4 | struct Extmap{ 1 : i32 id 2 : optional i32 uri } |
类似的对P2PMessageRequest类型的每个字段进行逆向之后,我有了完整的thrift定义,在这里。
我利用这个定义做了两个事情。首先,我用它确定了P2PMessageRequest类型在C++中的布局。这非常有价值,因为它允许我将struct定义导入IDA来确保每个字段被正确命名。这使我更容易理解P2PCall::OnP2PmessageFromPeer怎么处理进来的消息。这是巨大的进展。fbthrift可以从thrift定义直接生成C++头文件,但是非常长并且包含一堆不必要的定义,也不能被IDA所处理。所以我最终编译了生成的代码导进IDA,然后导出结构定义导入加载了librtcR20.so的IDA实例中。我编译的一些字段的长度不同于Facebook的,但是已经足够接近,我稍微修改一下就可以用了.
下面是个导入定义周IDA反编译代码的例子,看得出来它让消息对象的处理简单了很多。
我也能够解码和生成通过网络发送的消息了。为此,我用Python从thrift定义生成了序列化代码,因为thrift支持各种语言的代码生成。然后,我可以在Frida Python 中hook Facebook Messenger的函数时导入代码。
然后我需要找到处理P2PMessageRequest 消息的代码。因为这些消息由native代码处理,与此同时大部分Facebook消息由Java代码处理,我要找一个有着合适名称的native调用。我找到了com.facebook.webrtc.WebrtcEngine.onThriftMessageFromPeer。我用Frida hook这个函数,在生成的反序列化程序中输入它的byte数组参数,解码出传入的消息。
我还找到一个类似的发送thrift消息的方法,sendThriftToPeer (该方法的类名在Facebook Messenger的每个版本都被混淆更改,但是可以通过在应用的smali找到)。我也可以hook这个方法,修改其byte数组参数,用来更改Facebook Messenger发送的P2PMessageRequest 消息。
到这里,我就可以理解Facebook Messenger的呼叫状态机了。有两种呼叫的方式,取决于用户在哪里登录。如果用户在多个设备或者浏览器上登录,在用户交互之前几乎不会发生任何事情。询问/答复/候选消息会被交换,但是它们会被被呼叫者存储,在用户答复之前不会处理。这很合理,因为Facebook Messenger不知道要连接到哪个设备。
如果被呼叫者仅在一台设备上登录,状态机变得有趣起来。
在这种情况下,Facebook Messenger在收到询问之后立即打开轨道,但是会更改询问消息使得所有的向外的流都是inactive。然后在用户同意之后再把询问消息替换成active的。
我担心变更询问消息有可能被绕过,但我研究了要怎么做之后——尽管我一般不推荐除了添加/禁用轨道的方式来禁用设备传输——觉得它还是挺健壮的。询问消息在内部WebRTC对象进行SDP解码之后立即更改,是直接改的,从而消除了传递错误的可能性。
然而,详细研究进来的消息是怎么处理的,我注意到很多其它类型的消息(除了询问/答复/候选)会在用户答复之前处理。一个典型的类型是SdpUpdate。当收到一个SdpUpdate消息时,本地的询问/答复会被setLocalDescription更新。
这种消息类型发送给上面的状态机时没有影响,不过它已经携带了SDP,用来调用setLocalDescription。但是在用户登录两台设备的时候,它会导致setLocalDescription 被调用并且开始连接音频。
不清楚SdpUpdate 消息类型用来做什么。我在测试机上尝试了各种场景,包括网络切换,但是没办法在正常使用过程中稳定生成。无论如何,很明显在用户答复之前不应收到这种类型的消息。它和上面的呼叫漏洞相似,跟WebRTC的使用没啥关系,仅仅是因为少了个能导致状态变换的输入检查。
该漏洞在2020年11月修复,服务器在连接建立前会拒收此种消息。
其它应用
我还研究了一些其它应用,没有发现它们的状态机问题。2020年8月我研究了Telegram,那时候刚加入了视频会议功能。我没发现任何问题,因为应用在被呼叫者答复之前不会交换任何SDP询问/答复/候选消息。2020年11月我研究了Viber,没有找到任何状态机问题,不过应用的逆向工程挑战使得研究没有其它应用那么细致。
讨论
我研究的大部分呼叫状态机都有逻辑漏洞,使得音频或视频在被呼叫者同意之前就开始传输了。这显然是一个保障WebRTC应用安全时常被忽略的领域。
大部分漏洞出现不是因为开发者误解WebRTC功能。相反它们仅仅跟状态机如何实现有关。也就是说,缺乏这方面的意识是个重要原因。罕有WebRTC文档或教程明确讨论从用户设备接入音频/视频需要经过用户同意。
很多这类状态机在处理通讯设置时增加了不必要的复杂性,也是个重要原因。不必要的线程,依赖晦涩的功能、大量的状态和输入类型也增加了此类漏洞出现的可能性。
另外,注意我还没有研究任何群聊天功能,这些漏洞都是点对点通信漏洞。这将是可能会发现更多问题的领域。
结论
我研究了7个视频会议应用的状态机,发现了5个可以实现未经用户同意就可以发送被呼叫者音频/视频的漏洞。所有的漏洞都已修复了。不清楚为何这是个普遍的问题,但是缺乏这类问题的意识和引入不必要的呼叫状态机复杂性是重要原因。呼叫状态机是一个值得关注并且还未充分挖掘的视频会议应用攻击面,看起来在将来的研究中还会发现更多问题。
赞赏
- [翻译]渗透测试备忘单 17993
- [翻译]为编程和逆向搭建RISC-V开发环境 13865
- [翻译]状态机的状态 11025
- [原创]看雪CTF.TSRC 2018 团队赛 第一题 初世纪 writeup 2924
- [原创]京东AI CTF大挑战Writeup 7194