指纹浏览器检测之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 语义。

相关推荐
2603_965148114 小时前
如何解析JSON数据?API返回的商品信息处理教程
开发语言·数据库·python·自动化·json·api
湘美书院--湘美谈教育7 小时前
湘美书院主理人谈AI文学:提示词与Skill的与时俱进
大数据·人工智能·安全·自动化·生活
DeepVisionary8 小时前
从GPT-5.6 Sol的Ultrafast模式看推理速度竞赛:750 token/秒背后,Cerebras的晶圆级生意
python·自动化
微硬创新9 小时前
老旧产线改造:耐达讯自动化16路4‑20mA转PROFINET的工程实践
人工智能·网络协议·自动化·信息与通信
HiDev_10 小时前
【非标自动化】2、认识元器件(固态继电器)
运维·自动化
明达智控技术10 小时前
国产 PLC 落地立库自动化,MC5000边缘智能控制器破局
运维·自动化
cdprinter11 小时前
信刻——留置谈话音视频数据集中自动刻录归档解决方案
自动化·音视频
桐桐桐11 小时前
PDF 翻译工具横评与自建批量方案:从在线工具到 Python 自动化
python·pdf·自动化
安科瑞-小李13 小时前
铁路智能运维进化路线:自动化→状态可视→预测维护
运维·自动化·数据采集·能耗可视化
酷可达拉斯14 小时前
自动化运维-Ansible任务控制与流程控制
运维·自动化·ansible