摘要

WebRTC 通过 RTCRtpSender.setParameters() 允许动态调整编码参数,其 encodings 数组用于描述 simulcast 各层的 rid 和 active 状态。然而,Chromium 实现中,对该数组与底层 RTP 流同步源的映射更新并非原子操作——攻击者可在重协商信令阶段插入一个携带伪造 rid 的高优先级编码条目,调用 setParameters() 后,内部编码器可能为这个新条目分配独立的 SSRC,而该 SSRC 并未出现在远端 SDP 中。此时音频数据会被同时发送到合法接收者和攻击者控制的 SSRC,形成无声监听。更隐蔽的是,利用 RTCRtpContributingSource 统计信息无法及时反映这一变化,使得劫持长期隐匿。

Simulcast与RTCRtpEncodingParameters的映射关系

Simulcast 的 RID 与 SSRC

WebRTC 的 Simulcast 允许一个发送器(RTCRtpSender)同时生成多个不同码率或分辨率的视频/音频流,每个流由一个独立的 SSRC 标识。

在 SDP 协商中,a=simulcast:send 属性声明了这些流的 rid(Restriction Identifier),接收方根据 rid 识别流。例如:

a=simulcast:send low;medium;high
a=rid:low send pt=96;max-br=100000
a=rid:medium send pt=96;max-br=500000
a=rid:high send pt=96;max-br=1000000

在发送方,RTCRtpSender.getParameters().encodings 返回一个数组,每个元素对应一个 simulcast 层,包含 ridactivemaxBitrate 等属性。底层媒体引擎根据这些参数创建独立的 RTP 编码流,每个流对应唯一的 SSRC,并且该 SSRC 会通过 a=ssrc: 属性在 SDP 中声明。正常情况下,rid 与 SSRC 的映射在 setParameters() 调用后保持不变,除非明确移除或禁用某层。

setParameters的非原子性

setParameters() 允许修改 encodings 数组的某些字段(在 Chrome 中,activemaxBitratemaxFramerate 等可修改,rid 仅能在初始设置时指定,不能通过 setParameters 改变)。然而,当传入一个与原数组长度不同的 encodings 列表(例如增加一个元素)时,内部媒体引擎需要分配新的 SSRC 并更新 RTP 发送器映射。这个过程涉及多个线程和异步信号:参数校验、编码器重启、SSRC 分配、SDP 更新通知等。在这些步骤之间存在窗口,其中编码层已经创建了新 SSRC,但信令层尚未通知远端,导致该 SSRC 成为“孤立”的——它活跃地发送数据,但远端无法通过 SDP 知晓其存在,也无法将其与任何 rid 关联。

更严重的是,如果这个新条目被标记为 active: true 且优先级最高,则原有的音频或视频数据可能被复制或重定向到这个孤立 SSRC,从而向网络发送一份额外的媒体流。任何能够捕获该 SSRC 的监听者(例如同一局域网内的被动嗅探器或已经加入同一 SFU 的其他客户端)都可以接收并解码这个流,实现无声监听。

孤立编码参数攻击原理

攻击者模型

假设一个多方视频会议中,攻击者已作为合法参与者加入。攻击者控制自己的浏览器或 Node.js WebRTC 客户端。在会话重协商阶段,攻击者通过信令服务器向目标受害者发送一个重新邀请(re-invite),其中包含经篡改的 SDP——在受害者的 send 方向插入一个额外的 rid 描述,但声明为“不活跃”。受害者浏览器在处理远程 SDP 时,如果不严格校验 rid 数量与本地编码器数量的一致性,可能会在本地编码数组末尾添加一个空条目,或在下一次 setParameters 时意外创建一个孤立 SSRC。

更直接的攻击:攻击者利用受害页面的 XSS 或浏览器扩展,直接调用受害者 RTCRtpSender 的 setParameters(),向其 encodings 中注入一个新的 { rid: "leak", active: true }。Chrome 可能不会立即拒绝,而是分配一个新的 SSRC 并开始发送编码数据到该 SSRC,即使远端 SDP 中从未声明该 rid。攻击者随后在自己的客户端创建一个新的接收 RTCRtpTransceiver,并监听该 SSRC 的 RTP 包(例如通过 a=ssrc-group:FID 或手动指定 a=ssrc 强制匹配),即可接收到泄露的音频流。

音频劫持的触发流程

  1. 受害者页面的 RTCRtpSender 原本发送音频到合法的 SSRC_A。
  2. 攻击者注入孤立的编码参数 { rid: "z", active: true, maxBitrate: 128000 }
  3. 媒体引擎为此分配新的 SSRC_Z,并开始将音频数据同时编码发送到 SSRC_A 和 SSRC_Z(取决于编码器实现,可能是复制或独立编码)。
  4. 攻击者在自己的客户端创建一个 RTCPeerConnection,在远端 SDP 中手工添加 a=ssrc:SSRC_Z recv,并通过信令发送给攻击者控制的另一端,或将流量导向攻击者的接收器。
  5. 攻击者成功接收到受害者的音频流,而受害者毫不知情。

这种攻击的隐蔽性在于,攻击者无需解密或注入网络层,完全在 JavaScript API 层面操作,且现有的 getStats() 统计不会立即反映孤立的 SSRC(因为其未绑定到任何 rid 的统计对象)。

注入孤立编码参数实现音频窃听

以下 JavaScript 代码演示如何在 Chrome 浏览器中,通过 setParameters 向一个正在工作的音频发送器注入孤立 SSRC,并在攻击者接收端监听该 SSRC。测试环境:两个标签页或两个浏览器实例,分别作为受害者和攻击者,通过信令建立初始连接。

受害者端:音频发送器

// victim.js
const pc = new RTCPeerConnection();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
pc.addTrack(stream.getAudioTracks()[0], stream);

// 初始 SDP 协商
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// ... 发送 offer 给攻击者,接收 answer ...

攻击者端:接收正常音频

// attacker.js
const pc = new RTCPeerConnection();
pc.ontrack = (event) => {
const audio = document.createElement('audio');
    audio.srcObject = event.streams[0];
    audio.play();
console.log('正常音频轨道:', event.track);
};
// ... 信令交换,接收 answer,与受害者建立连接 ...

注入孤立编码参数

在受害者端运行以下代码(模拟 XSS 或扩展注入):

// 获取音频发送器
const sender = pc.getSenders().find(s => s.track.kind === 'audio');
const params = sender.getParameters();

// 添加一个孤立的编码层
params.encodings.push({
rid: "leak",
active: true,
maxBitrate: 128000,
priority: "high"
});

// 应用修改
sender.setParameters(params).then(() => {
console.log('孤立的编码参数已注入');
}).catch(e => {
console.error('注入失败:', e);
});

如果浏览器放行,编码器将分配一个新的 SSRC,并通过相同的 RTP 会话发送音频。使用 chrome://webrtc-internals/ 可以观察到新增的 SSRC 和发送统计。

攻击者接收孤立 SSRC

攻击者需要知道新分配的 SSRC。可以通过以下方式获取:

  • 在信令中加入自定义字段传递 SSRC 信息。
  • 利用 RTCPeerConnection.getStats() 定期查询受害者发送器的统计(需有同源访问)。

一旦获取 SSRC_Z,攻击者新建一个 RTCPeerConnection,在本地创建 offer 时在 SDP 中插入 a=ssrc:SSRC_Z recv,然后与自己的媒体服务器建立连接,即可解码音频。

// 攻击者构造接收 SSRC_Z 的 SDP
const offer = await pc.createOffer();
let sdp = offer.sdp;
sdp += `\r\na=ssrc:${SSRC_Z} recv\r\n`;
offer.sdp = sdp;
await pc.setLocalDescription(offer);
// 通过信令发送给攻击者的接收端

检测与防御

浏览器侧加固

  • 严格限制 encodings 数组长度变更:一旦 SDP 协商完成,应禁止通过 setParameters 增加或删除编码条目,仅允许修改现有条目的参数。
  • SSRC 与 rid 的强绑定:媒体引擎必须在分配新 SSRC 后立即触发 SDP 更新,确保远端始终知晓活跃的 SSRC 集合。
  • 统计信息透明getStats() 应报告所有正在发送的 SSRC,包括由于异常 setParameters 产生的孤立流。

服务端检测

  • SFU 监控:在视频会议服务器(如 Mediasoup、Janus)中,监控每个参与者实际发送的 SSRC 与其 SDP 声明的 SSRC 集合是否一致。若出现未声明的 SSRC 发送数据,立即告警并断开连接。
  • SDP 指纹比对:在每次重协商时,检查 rid 数量与 a=ssrc 条目数量的对应关系,防止注入未协商的 rid

应用程序防护

  • 避免使用 setParameters 进行非标准修改,尤其是传入不受信任的 encodings 数组。
  • 对 getParameters().encodings 的任何修改进行输入验证,拒绝添加新条目的操作。

结语

WebRTC 的 setParameters 为实时通信提供了无与伦比的灵活性,但这种灵活性建立在内部状态机的高度一致性之上。当孤立编码参数被悄然注入,音频流便如幽灵般复制出一份额外的 SSRC,飘向攻击者的接收器。这是浏览器 API 层面的逻辑漏洞,而非网络协议缺陷,传统的防火墙和加密无法察觉。防御的关键在于,媒体引擎必须严守“协商即契约”的原则,绝不允许未声明的 SSRC 发送哪怕一个比特。随着 WebRTC 在云会议和物联网中的深入应用,这种 API 滥用将成为音频安全的新战场。