指纹浏览器检测之BrowserScan的webrtc指纹检测和反检测

BrowserScan WebRTC 采集与检测逻辑

目标站点:https://www.browserscan.net/zh

本 README 只记录该站点 WebRTC 模块的 JS 采集代码、候选解析、结果写出和检测逻辑。

JS 入口

WebRTC 检测代码位于:

  • 页面预加载资源:/dist/BFntJwDg.js
  • 本地落盘文件:runs/20260615_181657/resources/0017_cbcbaddb85_BFntJwDg.js
  • HTTP 落盘包:runs/20260615_181657/firefox/http_packet/000016_GET_200_www_browserscan_net_dist_BFntJwDg_js_pid8804_30000002e.http_packet.json

BFntJwDg.js 导出两个函数:

js 复制代码
export { R as a, D as w };
  • R():站点首页 WebRTC 检测主入口,采集 STUN 与 TURN 结果。
  • D(iceServer):传入任意 iceServer,返回一次 WebRTC candidate 采集结果。

浏览器 API 选择

站点优先取标准 WebRTC API,兼容旧别名:

js 复制代码
const PC =
  window.RTCPeerConnection ||
  window.mozRTCPeerConnection ||
  window.webkitRTCPeerConnection;

if (!PC) {
  return {
    stun: "disabled",
    turn: "disabled"
  };
}

检测点:

  • window.RTCPeerConnection
  • window.mozRTCPeerConnection
  • window.webkitRTCPeerConnection

没有可用构造器时,页面直接认为 WebRTC STUN/TURN 不可用。

ICE Server 配置

主入口 R() 固定创建两组配置:

js 复制代码
const configs = [
  {
    iceServers: [
      { urls: "stun:stun.l.google.com:19302" }
    ]
  },
  {
    iceServers: [
      {
        urls: "turn:aa.online-metrix.net?transport=udp",
        username: "1:null:null",
        credential: "null"
      }
    ]
  }
];

第一组用于取 srflx,作为页面显示的 WebRTC STUN IP。

第二组用于取 relay,作为页面显示的 TURN/UDP 结果。

候选采集函数

BFntJwDg.js 中的内部函数 g(PC, config) 是核心采集器。还原后的逻辑如下:

js 复制代码
function collectCandidates(PC, config) {
  return new Promise((resolve) => {
    let pc;
    let timer = 0;
    let done = false;
    const candidates = [];
    const rawEvents = [];

    pc = new PC(config);

    function close() {
      if (timer > 0) {
        clearTimeout(timer);
        timer = 0;
      }
      if (pc) {
        pc.onicecandidate = null;
        pc.close();
      }
    }

    function finish(result) {
      if (done) return;
      done = true;
      close();
      resolve(result);
    }

    if (!("iceGatheringState" in pc)) {
      finish({ sdp: "" });
      return;
    }

    timer = setTimeout(() => {
      timer = 0;
      if (candidates.length > 0) {
        const grouped = parseCandidateGroups(candidates, rawEvents);
        grouped.sdp = pc.localDescription.sdp;
        finish(grouped);
      } else {
        finish({ sdp: pc.localDescription.sdp });
      }
    }, 5000);

    pc.onicecandidate = (event) => {
      if (event.candidate) {
        candidates.push(event.candidate);
      }
      if (event) {
        rawEvents.push(event);
      }

      if (pc.iceGatheringState === "complete") {
        const grouped = parseCandidateGroups(candidates, rawEvents);
        grouped.sdp = pc.localDescription.sdp;
        finish(grouped);
      }
    };

    pc.createDataChannel("line-metric");

    try {
      pc.createOffer({
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 1
      })
        .then((offer) => pc.setLocalDescription(offer))
        .catch(() => {
          return pc.createOffer()
            .then((offer) => pc.setLocalDescription(offer));
        })
        .catch(() => {
          finish({
            sdp: pc.localDescription?.sdp || ""
          });
        });
    } catch (err) {
      if (err instanceof TypeError) {
        pc.createOffer(
          (offer) => pc.setLocalDescription(offer, function noop() {}, function noop() {}),
          function noop() {}
        );
        finish({ sdp: pc.localDescription.sdp });
      }
    }
  });
}

核心行为:

  • 构造 new RTCPeerConnection(config)
  • 创建 createDataChannel("line-metric"),触发 data channel 场景下的 ICE gathering。
  • 调用 createOffer({ offerToReceiveAudio: 1, offerToReceiveVideo: 1 })
  • 调用 setLocalDescription(offer)
  • 监听 pc.onicecandidate
  • iceGatheringState === "complete" 结束。
  • 最长等待 5 秒,超时后用已收集到的 candidate 或 SDP 兜底。
  • 结束前关闭 peer connection。

Candidate 解析逻辑

站点解析函数会深拷贝 candidate 数组,然后逐条读取 candidate.candidate

js 复制代码
function parseCandidateGroups(candidates, rawEvents) {
  candidates = JSON.parse(JSON.stringify(candidates));
  rawEvents = JSON.parse(JSON.stringify(rawEvents));

  if (candidates.length === 0 && rawEvents.length > 0) {
    return {};
  }

  const groups = {};

  for (let i = 0; i < candidates.length; i++) {
    try {
      const tokens = candidates[i].candidate.split(" ");

      if (tokens.length >= 8 && (isIPv4(tokens[4]) || isIPv6(tokens[4]))) {
        const address = tokens[4];
        const type = tokens[7];

        if (type in groups) {
          groups[type].push(address);
        } else {
          groups[type] = [address];
        }
      }
    } catch (_) {}
  }

  return groups;
}

关键字段:

  • tokens[4]:candidate 地址。
  • tokens[7]:candidate 类型。
  • typ host:本机或 mDNS host candidate。
  • typ srflx:STUN server-reflexive candidate。
  • typ relay:TURN relay candidate。

站点只把 tokens[4] 是 IPv4 或 IPv6 的 candidate 放入结果分组。.local mDNS host candidate 不会进入 stun/turn 最终值。

STUN/TURN 结果选择

主入口同时跑 STUN 和 TURN 两个采集器:

js 复制代码
const [stunResult, turnResult] = await Promise.all([
  collectCandidates(PC, configs[0]),
  collectCandidates(PC, configs[1])
]);

let turn = "disabled";
if (turnResult.relay && turnResult.relay.length) {
  turn = turnResult.relay[0];
}

let stun = "disabled";
if (stunResult.srflx && stunResult.srflx.length) {
  stun = stunResult.srflx[0];
}

return { stun, turn };

页面最终只取每类数组的第一个值:

  • stun = stunResult.srflx[0] || "disabled"
  • turn = turnResult.relay[0] || "disabled"

在本次不带 fpfile 的采集中,页面拿到:

json 复制代码
{
  "stun": "183.157.85.95",
  "turn": "disabled"
}

页面 UI 将 stun 和内部 udp 状态写成同一个 WebRTC IP:183.157.85.95

页面写出逻辑

DOM trace 复核到页面状态写入:

text 复制代码
Reflect.set(..., "stun", "183.157.85.95", ...)
Reflect.set(..., "udp", "183.157.85.95", ...)

对应证据:

  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:71961
  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:71963

写入后,页面展示:

  • WebRTC
  • WebRTC STUN

两项都显示 183.157.85.95

WebRTC IP 地理位置查询

页面拿到 WebRTC IP 后,会调用 BrowserScan 的 IP 查询接口:

text 复制代码
https://ip-scan.browserscan.net/sys/config/ip/get-visitor-ip

带 WebRTC IP 的请求形态:

text 复制代码
https://ip-scan.browserscan.net/sys/config/ip/get-visitor-ip
  ?_t=<timestamp>
  &_from=browserscan
  &_sign=<sign>
  &ip=183.157.85.95
  &type=ip-api

对应 trace:

  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:72979
  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:77559

该接口用于给 WebRTC IP 补国家、地区、城市、ISP 等展示信息,并与页面出口 IP 进行对比。

检测表面

BrowserScan 这个 WebRTC 模块实际读取或依赖的表面包括:

  • RTCPeerConnection 构造器。
  • RTCPeerConnection.prototype.createDataChannel
  • RTCPeerConnection.prototype.createOffer
  • RTCPeerConnection.prototype.setLocalDescription
  • RTCPeerConnection.prototype.close
  • pc.onicecandidate
  • pc.iceGatheringState
  • pc.localDescription.sdp
  • RTCPeerConnectionIceEvent.candidate
  • RTCIceCandidate.candidate

本次页面侧 hook 额外记录了 RTCIceCandidate.addresstoJSON()getStats(),这些不是 BFntJwDg.js 最终选择 stun/turn 的必要字段,但属于 WebRTC 指纹一致性检查中容易被站点扩展读取的表面。

Trace 证明点

http_packet 证明源码:

  • runs/20260615_181657/firefox/http_packet/000016_GET_200_www_browserscan_net_dist_BFntJwDg_js_pid8804_30000002e.http_packet.json:2
  • runs/20260615_181657/firefox/http_packet/000016_GET_200_www_browserscan_net_dist_BFntJwDg_js_pid8804_30000002e.http_packet.json:137

jscall 证明 BFntJwDg.js 构造 WebRTC:

  • runs/20260615_181657/firefox/jscall/trace_jscall_process_11132.jsonl:70599
  • runs/20260615_181657/firefox/jscall/trace_jscall_process_11132.jsonl:70630
  • runs/20260615_181657/firefox/jscall/trace_jscall_process_11132.jsonl:70653
  • runs/20260615_181657/firefox/jscall/trace_jscall_process_11132.jsonl:70672

domtrace 证明 WebRTC API 调用链:

  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:57344:构造 STUN RTCPeerConnection
  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:57394:构造 TURN RTCPeerConnection
  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:57387:调用 createOffer
  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:57430:调用第二路 createOffer
  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:60498:调用 setLocalDescription
  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:60507:调用第二路 setLocalDescription
  • runs/20260615_181657/firefox/domtrace/trace_process_11132.jsonl:62593:62608:读取 candidate 并进入 BFntJwDg.js handler。

fpfile 改写后的站点可见结果

带 WebRTC IP 改写时,站点可见 candidate 地址统一为:

text 复制代码
104.251.229.181

DOM trace 证明:

  • runs/20260615_181513/firefox/domtrace/trace_process_8972.jsonl:663
  • runs/20260615_181513/firefox/domtrace/trace_process_8972.jsonl:664

对应值:

text 复制代码
RTCIceCandidate.candidate = candidate:0 1 UDP 2122252543 104.251.229.181 52948 typ host
RTCIceCandidate.address   = 104.251.229.181

这说明对该站点来说,WebRTC 指纹伪装必须至少保证 candidate 字符串与 candidate 地址字段一致;如果后续站点扩展读取 toJSON()、SDP 或 getStats(),这些表面也需要保持同一 IP 语义。

相关推荐
工业机器人销售服务1 小时前
法奥精密治具底座自动找平打磨,统一平面精度,保障工装定位稳定性
机器人·自动化
糖果店的幽灵1 小时前
软件测试接口测试从入门到精通:Python接口自动化 - requests库
开发语言·软件测试·python·功能测试·自动化·接口测试
chinesegf1 小时前
构建高效工具调用Prompt的极简范例
人工智能·自动化
换个昵称都难2 小时前
webrtc TURN 主要源码介绍
webrtc
极客先躯2 小时前
高级java每日一道面试题-2026年02月12日-实战篇[Docker]-什么是容器的 Seccomp 配置?如何自定义?
java·运维·分布式·docker·容器·自动化·文件
FII工业富联科技服务3 小时前
“可持续灯塔工厂”技术解密:AI+IoT如何落地端到端碳管理闭环
大数据·人工智能·物联网·ai·数据分析·自动化·制造
Black蜡笔小新3 小时前
零代码私有化自动化AI算法训练服务器DLTM如何破解企业AI落地难题
人工智能·算法·自动化
LT10157974443 小时前
2026年开源自动化测试工具选型指南:功能与适用场景解析
测试工具·开源·自动化
中科岩创3 小时前
某景区地下隧道结构健康监测工程项目
大数据·物联网·自动化