0x01 了解WEBRTC
WEBRTC架构:
webrtc主要应用于浏览器上的实时视频通讯。我们就从这一点应用上去了解。
关于cadidate:而在浏览器上实现视频通讯,首先就要保证视频编码解码能力的一致,这就应入了cadidate的概念,用于协商两个浏览器在数据处理能力方面的一致性。
而按照原本的通信过程,数据流的传输需要通过“服务器”在中间作为媒介才能实现相互访问(域名就需要走DNS服务器才能了解到对方是谁),私有IP地址也需要路由器经过NAT转换才能相互访问。这时候,webRTC就需要经过**STUN和TURN**
来实现对NAT的绕过。当然,在webrtc打洞穿透NAT之前,还是需要服务端来找到双方才能建立连接。
WebSocket:是一种在单个TCP连接上进行全双工通信的协议,通常是基于HTTP协议进行握手。通过WebSocket,可以实现浏览器与服务器之间的实时、双向通信。HTTP协议是一种请求-响应协议,客户端向服务器发送请求,服务器返回响应,是单向的传输连接。而WebSocket是一种双向的传输连接,客户端和服务器可以同时发送和接收数据,实现实时、双向通信。
0x02 组成部分
按照上面的描述,可以了解到,在完整建立webrtc通信的基础上,需要三部分组成:浏览器A、中继服务端(在建立连接前使用)、浏览器B。
浏览器A的作用是:建立websocket与中继服务器建立通信,建立webrtc与浏览器B建立通信,并在浏览器端处理(压缩与解析)视频流等数据流。
浏览器B的作用与浏览器A是一样的,在服务端和控制端上与浏览器A互补。
中继服务器:在建立webrtc连接前建立浏览器A、B双方的websocket,以及turn服务的搭建,即信令服务器。这里就可以理解到,webrtc中的turn是一种服务与中继的作用。
第一步:捕获本地媒体流
<script> let constraints = {audio: false, video: true}; let startBtn = document.getElementById('start') let video = document.getElementById('stream') startBtn.onclick = function() { navigator.getUserMedia(constraints, function(stream) { video.srcObject = stream; window.stream = stream; }, function(err) { console.log(err) }) } </script>
这里触发onclick就会触发WebRTC的api getUserMedia。此时第一个参数constraints只获取视频而不获取音频,第二个参数用于获取数据流,第三个参数用于返回错误,这都是显而易见的。
第二步:cadidate协调通信:信令机制
在双方传输媒介中传输的数据叫做信息,那么需要传输哪些信息?
信息的收发端点:网络信息(IP与端口)
信息加密:密钥数据
信息内容:编码解码类型和媒体元数据
信息差错:错误信息
信息起始:会话控制
这时候就需要使用JS会话建立协议(JSEP)来实现这么多信息传递的协议。引出第二个webRTC api:RTCPeerConnection
A与B之间建立一个RTCPeerConnection连接:
A使用 RTCPeerConnection.createOffer()方法产生一个offer。这个offer包括SDP协议(存储媒体元数据),也包括网络信息。
A使用offer调用setLocalDescription()生成本地会话描述(其实就是通过SDP来产生一个“输出端”),并发送给B。
B在接收到offer后调用setRemoteDescription(),生成远端会话描述(这样AB两端的视频流解析方法就一致了)
由于RTC是双工的,此时B也要调用createAnswer()生成一个answer(和offer的信息几乎一致)并发给A,并且setLocalDescription()生成(客户端)的本地会话描述。
A接收到后setRemoteDescription()将B的应答设置为远端会话描述。
上面这一系列操作都是通过信令服务器完成的,接下来再次交换“查找候选项”网络信息(ICE)。
Q:为啥不直接一次性完成cadidate的信息对等交换呢
A:因为WebRTC在建立点对点连接时需要交换的信息比较多,所以将其拆分成两步进行发送。第一步是交换SDP信息,通过SDP来协商媒体流的类型、编码格式、传输协议等信息,确定双方数据传输的方式。第二步是交换候选项信息,用于在不同网络环境中建立点对点连接。
继续看下去,A使用onicecandidate创建一个RTCPeerConnection对象,注册一个处理器函数(处理器函数的作用是将ICE发送给远端),处理器将候选数据发送到B。
B获得候选消息时,调用addIceCandidate(),添加到远端对等描述(注意:这个远端对等描述和远端会话描述不是一个东西)
补充解释一下候选数据就是 STUN 和 TURN 服务器返回的 IP 地址和端口信息,用于进行 NAT 穿越和中继转发。
上面就是描述了两个任务的实现过程:
获取和分享网络信息:可能的连接端点,也就是ICE候选获取和分享本地和远端的描述:SDP格式的本地媒体的元信息
demo分析
这里是通过websocket实现ICE传输的,和上述的onicecandidate不太一样,但是实现原理是一样的,websocket本身就完成了ICE的交换,自行理解一下。
信令服务器
// 创建 WebSocket 连接,客户端连接到信令服务器的地址 const wsServer = new WebSocket.Server({ port: 8080 }); // 用于存储连接信息的对象 let connections = {}; // 当有新的WebSocket连接时,将其存储在connections对象中 wsServer.on('connection', socket => { socket.id = Math.random().toString(36).substr(2, 9); connections[socket.id] = socket; socket.on('message', message => { const data = JSON.parse(message); if (data.type === 'offer') { // 如果是offer,就将offer存储在connections对象中 connections[data.to].send(JSON.stringify({ type: 'offer', offer: data.offer, from: socket.id })); } else if (data.type === 'answer') { // 如果是answer,就将answer存储在connections对象中 connections[data.to].send(JSON.stringify({ type: 'answer', answer: data.answer, from: socket.id })); } else if (data.type === 'candidate') { // 如果是candidate,就将candidate存储在connections对象中 connections[data.to].send(JSON.stringify({ type: 'candidate', candidate: data.candidate, from: socket.id })); } }); socket.on('close', () => { // 当WebSocket连接关闭时,将其从connections对象中删除 delete connections[socket.id]; }); })
浏览器端
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>WebRTC基础教程</title> </head> <body> <video id="remote-video" autoplay></video> <script> // 创建WebSocket连接 const socket = new WebSocket("ws://<WEBSOCKET_SERVER>:8080"); // 创建RTCPeerConnection对象 const pc = new RTCPeerConnection(); // 获取本地媒体流 navigator.mediaDevices .getUserMedia({ video: true, audio: false }) .then((stream) => { // 将本地媒体流添加到RTCPeerConnection中 pc.addStream(stream); // 当获取到远端的媒体流时,将其添加到video元素中 pc.onaddstream = (event) => { const video = document.getElementById("remote-video"); video.srcObject = event.stream; }; // 当收到远端的offer时,向其发送answer socket.onmessage = (message) => { const data = JSON.parse(message.data); if (data.type === "offer") { pc.setRemoteDescription(data.offer) .then(() => pc.createAnswer()) .then((answer) => pc.setLocalDescription(answer)) .then(() => socket.send( JSON.stringify({ type: "answer", answer: pc.localDescription, to: data.from, }) ) ); } else if (data.type === "answer") { pc.setRemoteDescription(data.answer); } else if (data.type === "candidate") { pc.addIceCandidate(new RTCIceCandidate(data.candidate)); } }; // 创建并发送offer pc.createOffer() .then((offer) => pc.setLocalDescription(offer)) .then(() => socket.send( JSON.stringify({ type: "offer", offer: pc.localDescription, to: "remote", }) ) ); }); </script> </body> </html>
RTCPeerConnection
本身只支持音视频流通道,但是你可以在音视频流的基础上,通过 RTCDataChannel
实现任意数据的传输。
这样以来,就能很容易看懂webRTC的原理和实现了
接下来,进入正题,如何进行反制?为什么能进行反制?
反制原理
我们借用下面这个3.4K star的项目进行分析
https://github.com/diafygi/webrtc-ips
补丁
原项目以及不生效了,因为(issues #51)RTCPeerConnection的createDataChannel其中一个参数出现了问题
https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/createDataChannel
简单来说,你必须在createDataChannel()中输入一个参数(通道名)。
而在diafygi的项目中,并没有实现这一点;
80: //create a bogus data channel 81: pc.createDataChannel("");
只要把任意字符(比如”blueteam“)填进去,就是一个补丁了()
代码分析
在getIPs函数中首先检测WebRtc支持(支持不同浏览器)
var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; var useWebKit = !!window.webkitRTCPeerConnection;
通过iframe标签独立浏览器进程来绕过webrtc的禁用
var servers = {iceServers: [{urls: "stun:stun.services.mozilla.com"}]};
在建立RTCPeerConnection时,第一个参数使用server定义公用的ICE server(防止空参数的时候出现奇怪的错误),第二个参数用于拓展RtpDataChannels,原本数据包是使用RTCdatachannels数据包传输。
这是因为原本的RTC通道是只支持音频数据流传输的,为了获取其他(恶意者的信息)数据,(也许你可以试试RTC直接去调用摄像头看看攻击方的正脸照)。
var mediaConstraints = { optional: [{RtpDataChannels: true}] }; var servers = {iceServers: [{urls: "stun:stun.services.mozilla.com"}]}; //construct a new RTCPeerConnection var pc = new RTCPeerConnection(servers, mediaConstraints);
在往下正常的创建offer、answer流程之前,注意到在此之前处理onicecandidate的时候(对应一下demo中websocket的处理),出现了一些变化
pc.onicecandidate = function(ice){ //skip non-candidate events if(ice.candidate) handleCandidate(ice.candidate.candidate); };
pc.onicecandidate
是一个监听器,并在当 RTCPeerConnection
发现新的可用的网络候选项(ICE)时,就会触发 onicecandidate
事件(获取IP的函数handCandidate)。
https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/localDescription
peerConnection.localDescription
返回RTCPeerConnection.pendingLocalDescription
或者RTCPeerConnection.currentLocalDescription
的值,其都是返回一个SDP。
根据SDP格式,附加属性a=*
格式a=candidate:
的第一个字段表示候选项ID
最终返回IP给回调函数进行正则判断,得到目标IP
(如果你仔细看了SDP格式,你会发现,candidate其实是走的UDP协议,也就是说,即使对方挂了socks代理,仍然可以获取到目标IP)
FIX version
https://github.com/matoujin/webrtc-ips