Signal App语音通话逻辑漏洞

 

MissCoconut @ Vulpecker Team

 

0x01 漏洞来源

  1. https://bugs.chromium.org/p/project-zero/issues/detail?id=1943
  2. https://bugs.chromium.org/p/project-zero/issues/detail?id=1936

0x02 漏洞概述

Signal App(< 4.45.x)在进行语音通话时存在一处逻辑错误,导致攻击者可在被叫方接通电话前强制进行通话。

0x03 背景知识

  1. WebRTC框架
  2. 连接建立流程

WebRTC框架

由于浏览器本身不支持相互之间直接建立信道进行通信,需要通过服务器进行中转。两个端点间的通信需要分别和服务器建立信道,然而两个浏览器之间的通信需要通过两段信道,并且受到了信道带宽的影响,这样的信道并不适合数据流的传输如何建立浏览器之间的点对点传输,所以WebRTC应运而生。

WebRTC是一个google的开源项目,实现了浏览器之间的实时通信(RTC),并且提供了相应的JS接口。

连接建立过程

Step 1. 信令阶段

Step 2. 处理复杂网络环境

Step 3. 媒体传输

Step 1. 信令(Signalling)阶段

为了建立一个会话,两个Peer需要通过信令服务器交换以下信息:

  1. 会话控制信息,用来开始和结束通话,即开始视频、结束视频这些操作指令
  2. 处理错误的消息
  3. 元数据,如各自的音视频解码方式、带宽
  4. 网络数据,对方的公网IP、端口、内网IP及端口

一旦信令阶段完成,两个客户端之间建立了连接,理论上他们就可以进行点对点通讯了。

  • 信令阶段示意图

Step 2. 处理复杂网络环境

  1. 实际应用中,通讯的两端可能处在相对复杂的网络环境中,比如存在NAT、防火墙之类,通讯的两端可能面临无法获取到自己公网IP、防火墙阻止某些端口和协议等等情况。
  2. WebRTC应用采用ICE框架来处理网络连通性问题。借助两个服务器:STUN服务器和TURN服务器。

ICE机制解决网络连通性

  • STUN服务器处理内网地址

  • TURN服务器中继流量

  • 时序图

Step 3. 媒体传输阶段

经过上边两个阶段之后,通信双方已经建立了P2P连接或通过TURN服务器中继流量建立了连接,接下来可以进行实时数据传输了。

WebRTC API

  • MediaStream 从设备上摄像头/麦克风获取的音视频流
  • PeerConnection 传输音视频流媒体数据
  • DataChannel 传输任意二进制数据

0x04 漏洞详情

Signal语音通话流程(主叫端)

WebRtcCallService.java

  1. callState变量保存的值用于标识当前通话阶段,非WebRTC定义,是Signal应用中自定义的。
  2. 发起语音通话,调用handleOutgoingCall(Intent intent)函数,设置 callState=STATE_DIALING;
  3. 创建PeerConncection对象 WebRtcCallService.this.peerConnection = new PeerConnectionWrapper(…);
  4. 设置dataChannel,WebRtcCallService.this.dataChannel = WebRtcCallService.this.peerConnection.createDataChannel(DATA_CHANNEL_NAME);
  5. 监听dataChannel变化,注册Observer WebRtcCallService.this.dataChannel.registerObserver(WebRtcCallService.this);
  6. 创建开始通话的Offer,SessionDescription sdp = WebRtcCallService.this.peerConnection.createOffer(new MediaConstraints());
  7. 保存本地的SDP(Session Description) WebRtcCallService.this.peerConnection.setLocalDescription(sdp);
  8. SendMessage(offer)发送请求通话的Offer给对方;
  9. 收到对方的Answer后调用handleResponseMessage(Intent intent);
  10. 保存对方的SDP,this.peerConnection.setRemoteDescription(new SessionDescription(SessionDescription.Type.ANSWER, intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION)));
  11. 在接收到对方的ICEcandidates之后,调用handleRemoteIceCandidate(Intent intent),其中对peerConnection对象进行addIceCandidate操作。触发onIceCandidate()方法,调用handleLocalIceCandidate(Intent intent)将本地产生的candidate发送给对方;
  12. 在对peerConnection对象进行addIceCandidate(…)操作时,WebRTC会调用nativeAddIceCandidate(…)自动进行连接,连接成功时,调用onIceConnectionChange(…)方法,传入connected状态参数;
  13. 当被叫方接受通话时,执行handleAnswerCall()方法,并向dataChannel中写入构造好的特定数据。
  14. 主叫方dataChannel的Observer观察到dataChannel数据的变化,调用onMessage()回调函数;
  15. onMessage()方法中,解析dataChannel中数据,当dataChannel中保存通话状态的比特位为0x1时,继续发送一个Action为ACTION_CALL_CONNECTED的Intent;
  16. 调用handleIceConnected(Intent intent)方法,此时通信双方已经P2P建立连接。该方法中根据CallState的不同来进行不同的操作,当callState=STATE_DIALING时,设置UI并修改 callState=STATE_REMOTE_RINGING。
  17. onStartCommand()方法中,Action为ACTION_CALL_CONNECTED时执行handleCallConnected()方法。
  18. handleCallConnected()方法中调用相关函数采集设备麦克风、摄像头数据,开始进行流媒体传输,并进一步对根据当前callState对UI进行设置

Signal语音通话流程(被叫端)

  1. 被叫端接收到语音通话请求时,从handleIncomingCall()函数开始执行;
  2. handleIncomingCall()函数中,设置被叫端的 callState=STATE_ANWSERING ;
  3. 中间的流程和主叫端2~13相同;
  4. 在执行到handleIceConnected(Intent intent)时,进入 callState=STATE_ANWSERING 分支,该方法会设置接下来的 callState=STATE_LOCAL_RINGING ,并启动语音通话的Activity WebRtcCallActivity.java;
  5. WebRtcCallActivity.handleAnswerCall()方法中,当被叫端选择同意接通电话时,发送Action为ACTION_ANSWER_CALL,目标组件为WebRtcCallService;
  6. 执行到WebRtcCallService.handleAnswerCall()方法中,向dataChannel对象写入特定的数据;
  7. 触发dataChannel的Observer回调函数onMesage(),之后流程同主叫端流程15~19。

漏洞成因

该漏洞的根本原因在于被叫端还未接收语音通话,Signal就已经开始处理dataChannel中的数据并进行相应操作了。

理论上比较恰当的修复方案是在被叫端同意后才开始处理dataChannel的数据,但考虑到性能等原因,这种修复方案被放弃了。

漏洞分析

dataChannel的建立是在对方用户确认接通前就已经建立的。当通话的对方向dataChannel写入数据时,Obsever监听到dataChannel的改变,并调用回调函数onMessage()进行处理,随后onMessage()方法会调用handleCallConnected(Intent intent)方法,从设备的麦克风采集音频流并进行语音通话。

主叫端和被叫端最终都会执行到onMessage()和handleCallConnected(Intent intent)这两个方法。

然鹅在这两个方法中,都没有区分当前是主叫方状态(callState=STATE_REMOTE_RINGING)还是被叫方状态 ( callState=STATE_LOCAL_RINGING )。

 

0x05 漏洞利用

正常的流程下,被叫方接通电话后,会调用handleAnswerCall(Intent intent)方法,

handleAnswerCall(Intent intent){
this.dataChannel.send(new DataChannel.Buffer(
·ByteBuffer.wrap(Data.newBuilder()
.setConnected(Connected.newBuilder().setId(this.callId))
.build().toByteArray()), false));
}

send()方法对dataChannel对象写入特定的数据后,通话的双方开始进行语音通话。攻击者可以参考handleAnswerCall(Intent intent)方法来向dataChannel写入特定数据开始语音通话。

例如修改WebRtcService.handleSetMuteAudio()方法,代码如下:

private void handleSetMuteAudio(Intent intent) {
   this.dataChannel.send(new DataChannel.Buffer(
   ByteBuffer.wrap(
   Data.newBuilder()
      .setConnected(Connected.newBuilder().setId(this.callId))
      .build().toByteArray()), false));
   intent.putExtra(EXTRA_CALL_ID, this.callId);
   intent.putExtra(EXTRA_REMOTE_ADDRESS, 
   recipient.getAddress());
   handleCallConnected(intent);
}

编译后在攻击者设备上安装运行。此时当攻击者(主叫方)发起语音通话时,被叫方响铃,此时双方之间连接已经建立,攻击者通过点击静音按钮执行handleSetMuteAudio()方法,向dataChannel中写入特定的数据,触发被叫端onMessage()回调,进一步执行到handleCallConnected()方法。

此时受害者处于被叫方状态,callState=STATE_LOCAL_RINGING ,可以通过handleCallConnected()中对CallState的检查,所以应用继续执行handleCallConnected()方法后续流程,开启被叫方的麦克风进行语音通话。

 

0x06 漏洞修复

Signal对于这个漏洞修复方案是将主叫方与被叫方的函数执行流程剥离开。

当前处于主叫方状态时,执行handleCallConnected()->activateCallMedia()流程。

当处于被叫方状态时,执行activateCallMedia()。

patch :

https://github.com/signalapp/Signal-Android/commit/8f8f41f18432bf97a207883b24cd04fb2f8e04c6?diff=split

(完)