Notes on Black Hat USA 2021 Talk - Can You Hear Me Now? Remote Eavesdropping Vulnerabilities in Mobile Messaging Applications

Introduction

最近空闲下来了,看了几篇感兴趣的Black Hat USA 2021 的议题演讲,收获了不少思路,故开篇博客记录一下。

这是第一篇,有关于即时通信软件里的一种漏洞类型。

一开始点进去看talk视频的时候,也是很惊讶演讲者是个女的。。。一看原来是Google Project Zero 的,那没事了。

Motivation

19年1月份的时候,苹果爆出了一个有关于FaceTime(iphone上自带的视频聊天软件)的漏洞。大家在使用FaceTime进行群组会话的时候,居然可以远程直接拉其他任何人加入到语音中???

此漏洞的危害在于,攻击者可以在受害者甚至都不知情的情况下,获取到受害者周边的声音,严重危及到了苹果用户的隐私安全。

这是一种从未有过(unprecedented)漏洞类型,演讲者从中洞察到了一种全新的可能攻击面,因此就深入去研究了一下WebRTC(Web Real-Time Communication)协议里的状态机,果真发现了不少问题。

WebRTC

绝大多数的实时通信应用都是基于WebRTC协议来实现的。

在通信双方建立WebRTC连接之前,首先会使用SDP(Session Description Protocol,会话描述协议,位于OSI第6层)来交换一些“呼叫设定信息”,这个过程被称之为signalling。Signalling并不在WebRTC实现中,这允许了通信双方通过自定的方式来交换这些初始信息。

在一个典型的SDP协议中,通常都是caller(呼叫者)先发送一个SDP offer,然后callee(被呼者)回应一个SDP answer,offer/answer中包含了此次通话所需的基本信息。在offer/answer交换之后,双方可以向对端发送SDP candidates,candidates中包含了两者建立连接所需的网络路径(IP地址、TURN服务器等)。随后,双方就可以建立起一个Peer-to-Peer的连接,可以在这个P2P连接中传输多媒体数据,进行音视频通话。

image-20220405235744702

SDP协议中的一些具体字段其实并不是很重要,就不深入研究了。


WebRTC中内置了一个状态位,用来表示是否有收到并处理完offer或answer包。这一步通常都是应用程序自定义状态机来实现的,有些应用程序可能会在callee实际交互(例如:按下“接听”键)之后才互相交换SDP信息,但大多数应用程序则会在callee还未实际交互之前就开始互相交换SDP信息了。后者可以视作为设计层面上的trade-off,通过提前“握手”交换一些信息,来减少用户按下”接听“键后的延迟,从而提高用户体验。

除此之外,在WebRTC中还有一个叫做tracks的东西,用来抽象表示一个输入的音频/视频设备(麦克风、摄像头等)。在双方进行正式通信之前,首先要通过addTrack函数来将一个设备(track)绑定到当前的连接(connection)上,随后才能开始音频/视频的传输。后续可以在会话途中,将track禁止,即表示禁音/关闭摄像头。

理论上,要在callee知情并按下”接听“键同意之后,才能将tracks加入会话连接中。但是演讲者发现,很多实际应用却并非如此,存在着各种各样的实现方式,其中有一些就存在严重的漏洞:攻击者可以在callee尚未同意的情况下,成功建立起一段通话连接。

Attack Surface

既然是从状态机的角度切入攻击面,那么就要用adversarial的思维模式,想方设法扰乱状态机内部的状态。

也许能够扰乱WebRTC状态机的操作:

  • 发送额外的包
  • 扔掉部分包
  • 错乱发包的顺序
  • 错乱发包的方向(演讲者从这一点中发现了一些问题)

Findings

作者去深入研究了多款即时通信应用,并在以下软件中发现了若干漏洞:

  • Signal(一款开源的“微信”)
  • JioChat(印度的“微信”)
  • Mocha(越南的“微信”)
  • Google Duo(Google的“微信”)
  • Facebook Messenger(Facebook的“微信”)

Signal

Signal的会话建立阶段基本上和WebRPC协议差不多。

img

当P2P连接建立之后,只要callee按下“接听”键,callee的track就会绑定到这个P2P连接上面,状态机的内部状态转移至connected状态,callee随后会向caller发送一条Connect类型的包,让caller也转移到connected状态,并且把caller的track添加到P2P连接上,自动开始后续音频流的传输。


问题出现在,Signal程序设定caller和callee都可以接受Connect类型的包。这样的话,攻击者(作为caller)就可以在P2P连接建立之后,不去等受害者(作为callee)是否点按“接听”键,直接给callee发送一条Connect类型的包,就会强制导致callee的track被添加到P2P连接上,从而可以在未经受害者允许的条件下,听到受害者的麦克风声音。

在实际的PoC中,为了实现上述的攻击方式,演讲者还去魔改了一下Signal的源码。我猜,大概就是在caller侧,当P2P连接建立之后的代码逻辑中,添加一段向对方发一个Connect类型的包的代码,即可实现上述攻击流程。


演讲者是19年9月发现的这个问题,并报给了Singal ,Signal随后及时对此问题进行了fix

演讲者在报告 中的漏洞复现方式是,魔改了mute按键的handler,当攻击者(caller)按下“静音”键时,就会向受害者(callee)发送一条Connect类型的数据包,从而导致受害者还尚未接听,就强制开始语音通话。

fix之前:

image-20220406225352452

同时允许STATE_REMOTE_RINGING(远端用户响铃)和STATE_LOCAL_RINGING(本地用户响铃)这两种状态都可以处理Connect类型的包。

fix之后:

image-20220406225408423

只允许STATE_REMOTE_RINGING(远端用户响铃)能够处理Connect类型的包,从而防止了callee接受并处理Connect类型的包。


这个bug的根源在于Singal自己开发写代码的时候没注意,一条if语句少check了一层的逻辑,并非是WebRTC的问题。

JioChat && Mocha

演讲者在20年6月调一个WebRTC exp能不能打的时候,也同样在JioChat和Mocha中发现了类似的问题。

和Signal不同的一点在于,JioChat和Mocha都需要借助Server端的中转来进行WebRTC协议。

img

首先双方也是交换SDP offer和SDP answer,交换的过程需要服务端的中转传递;随后,双方向服务端发送自己的SDP candidates,服务端先保存这两个candidates,直至callee按“接听”键之后(服务端接受到callee发来的Accept),才会把candidates分发给双方,双方随后切换到connected状态,并自动添加tracks,开始音频/视频会话。

问题在于,其实SDP的offer和answer里面也是可以携带着candidates的(没想到吧??)。而双方只要接受到了对方的candidates后,就会直接默认进入到connected状态,开始传输麦克风的声音/摄像头的视频。

image-20220407162740983

演讲者用Frida去搞了一下,在SDP offer包里带了candidates,可以成功在JioChat里直接强制对方发送音频、在Mocha里直接强制对方发送音频+视频。


总结:这个问题的根源在于,JioChat和Mocha的程序实现上,开启音频/视频传输是取决于是否有收到对方的candidates。虽然在预设的情景下,只有callee按下“接听”键之后,暂存于服务端的candidates才会被发过来,但是实际应用中远比这复杂,还有一些其他的方式(绕过callee端Accept这一步)也可以将candidates发过来,这就会导致一些意想不到的后果。

这说明开发人员就没搞懂WebRTC是怎么正确工作的,而且还用了一个不太寻常的signalling设计(由服务端中转),像这种没有严格遵照specification来实现、自己造轮子的方式是不可取的,自然就会有一些意想不到的安全问题。

Google Duo

Duo的signalling设计理念跟其他的大多数即时通讯软件都不太一样,Duo支持callee在应答caller之前预先看一看对面的caller长什么样,这个feature叫做Knock Knock

image-20220407165222993

为了实现这一feature,Duo会在caller呼叫之后,立马和callee先建立一个单向的视频流连接。建立连接之后,另一个方向(callee至caller)的视频流传输需要被禁止掉,主要是通过下面2种方式来强制实现的:

  1. 在caller给callee发送的SDP offer中,包含一个字段a=sendonly用来表示视频流是单向传输的

  2. 当callee接收到caller发来的通话邀请后,会将视频的track添加到这个P2P连接上,然后再用track里的RTPSender属性来把track给禁用掉,直至callee按下”接听“键,才会再把track打开。

image-20220407164902653

上图右半部分表示的即为callee对于一个来电的处理,其中虚线部分表示为异步调用。

当callee端接收到一个来电后,会调用setLocalDescription函数对SDP offer进行处理,中途会异步调用onSetSuccess回调函数,onSetSuccess里又会异步调用RTPSender.SetParameters来禁用本地视频track;异步调用之后,setLocalDescription函数紧接着会继续去建立P2P连接,并开始传输caller的视频流。当后面用户按下“接听”键后,才会再调用RTPSender.setParameters函数,来把本地的视频track打开,从而再传输双向视频流。

image-20220407204109672


演讲者发现,上述两种方法都无法防止视频流从callee传向caller:

  1. SDP offer是由caller发送的,因此caller可以随便改offer包里的字段,轻松绕过第一种方法
  2. 如果说offer的整个处理流程是同步的倒没什么问题,但是Duo的设计是异步的,这就使得onSetSuccess函数执行之后,还真不一定能保证RTPSender.setParameters能在P2P视频流传输之前完成。如果说回调函数onSetSuccess在调用RTPSender.setParameters之前耗时比较久,且setLocalDescription函数在开启P2P视频流之前的耗时比较短,此时就会在“禁用视频track”和“开启P2P视频流”之间存在一个竞争,有那么一种可能的情况就会导致“禁用视频track”略晚于“开启P2P视频流”,从而能让callee(受害者)泄漏出几帧视频画面给caller(攻击者)。

image-20220407204120658

想赢得这个竞争还是十分困难的,演讲者用Frida调试了两周的时间,用尽了各种方法让P2P视频流快一点传输+RTPSender.setParameters晚一点执行(发很多个offer包、很多个candidates、很多个data messages),最终才成功达到攻击目的。其中data messages的处理线程和禁用视频track的线程属于同一个线程队列(thread queue),所以塞很多歌data messages可以有效地延缓禁用视频track的执行时间。


总结:Duo虽然在设计上有2种方法可以有效地避免从callee泄漏视频数据给caller,但这个“异步调用”的设计却引入了新的问题。“异步调用“在移动端应用中已经非常流行了,虽然这种设计能带来一定的便利,但也会让状态机的转移变得模糊不清,存在一些意想不到的情景,看来也是一个可以深挖的攻击面。

实际上,在这个案例中,“异步调用”根本就没有带来任何增益。在实际开发中,也要去平衡异步调用可能会带来的风险和收益,并不能不假思索盲目地去用“异步调用”。

Facebook Messenger

演讲者在2020年11的时候,开始对Facebook Messenger下手了。

Facebook Messenger是这些软件里面最具有挑战性的,因为没源码,要疯狂逆向。

中途略过一万年的逆向过程,然后演讲者总算是基本弄清楚了Facebook Messaenger的signalling状态机。

image-20220407212154687

只要有来电(收到一个SDP offer),Facebook Messeneger就会开启track,并把offer重写为inactive状态,防止任何外流的音视频流,并且还会把offer保存起来。当用户按下“接听”键后,才会把offer重写为active状态,再去调用setLocalDescription函数处理offer,放行音视频流外流。

演讲者其实是非常不建议用这种方式来禁用音视频设备的传输,最好还是用开启/禁用track来实现。

演讲者就在想,有没有什么法子可以绕过offer的inactive状态的重写。但是一轮跟进后发现,offer会被解码为一个内部对象存储在内存里,对offer的修改是直接反映在内部对象上的,并不能通过解析错误等方式去操作。

随后,演讲者在看接受的包是如何处理的时候,注意到了除了offer、answer、candidate之外,还有一些其他类型的包可以在呼叫应答之前被处理,其中有一个SdpUpdate类型的包就额外引人注目。

这种类型的数据包并不会对状态机做任何改变,但是神奇的事情就是,当用户登录了2个设备(例如:手机、电脑同时在线)的时候,SdpUpdate包会导致setLocalDescription被调用,并去把本地存储的offer重写为active状态,从而建立P2P连接,传输音频流数据。


总结:这个漏洞跟之前Signal的很像,都不是因为WebRTC的问题,而是因为开发人员写代码的时候,少了一层check而导致的。

后来这个漏洞在2020年11月的时候被修了,修复方案是服务端禁止SdpUpdate类型的包在通话之前进行传输。

Other Applications

演讲者还去看了一些其他的即时通讯软件,包括Telegram、Viber等,都没有发现状态机上类似的问题。(其中Viber可能逆向量太大了,导致演讲者并没有非常严谨地去看)

Summary

总结一些方法论:

  • 从一件看似不起眼的新闻事件中,洞察出新攻击面,并投入时间和精力去深入研究
  • 发现一个软件有问题后,可以横向找同类型软件继续Hunting。但演讲者实际上也并非是顺着这个攻击面去hunting,只是脑海里多了这么一个攻击面,在后续对即时通信软件继续进行研究(此时脑海里可能有n个攻击面)的时候,又重新发现了这个攻击面可以搞事情。
  • 一些新的攻击思路:角色互换会怎么样?有没有其他的触发条件? 异步调用会不会存在竞争?
  • 基础功还是要扎实才行(逆向分析、代码审计、网络协议、操作系统)