指纹浏览器检测之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.address、toJSON() 和 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 语义。

相关推荐
木头左5 小时前
【Python】不点浏览器发博客:逆向 CSDN 创作中心接口做 md-publish 技能
python·自动化·api
lisw055 小时前
智能化机器人系统的模块化设计自动化!
机器学习·机器人·自动化
天远Date Lab19 小时前
零信任架构实战:基于天远租凭分期报告构建自动化汽车以租代购免押风控网关
网络·人工智能·架构·自动化
迪康Defender21 小时前
终端供应链安全:应用白名单与软件正版化检测落地|终端安全治理实践
运维·服务器·网络·安全·web安全·自动化·运维开发
天远API1 天前
零信任架构实战:基于天远租凭分期报告构建自动化智慧租房免押分期履约评估网关
运维·人工智能·架构·自动化
我要见SA姐11 天前
Chrome 扩展安全攻防实录:从权限滥用到供应链投毒
运维·人工智能·python·机器学习·自动化
sun153690275721 天前
基坑边坡监测:振弦式土压力计选型、安装埋设与数据采集方案
大数据·物联网·自动化
Land03291 天前
财务RPA落地指南:银企对账、发票三单匹配、ERP台账自动化,RPA离线方案解析
服务器·自动化·rpa
Frag0ut1 天前
Notepad++ 宏录制自动化实战:让重复操作一键完成(2026效率版)
自动化·notepad++·macros·批量处理·宏录制·一键操作·重复操作自动化
DeepVisionary1 天前
为拿并网许可,OpenAI在佐治亚新园区同意高峰期断电三成
python·自动化