Loading

某电商 web 指纹算法 - canvas

结论

c = md5("envCollect" + canvas.toDataURL() + SALT)

完整生成链路

1. canvas 绘制 (300×150, 固定序列, 浏览器 hook 捕获)

const canvas = document.createElement('canvas');
canvas.width = 300; canvas.height = 150;
const ctx = canvas.getContext('2d');

// 阶段 1: 形状
ctx.fillStyle = 'red';
ctx.fillRect(30, 10, 200, 100);
ctx.strokeStyle = '#1a3bc1';
ctx.lineWidth = 6;
ctx.lineCap = 'round';
ctx.arc(50, 50, 20, 0, Math.PI, false);
ctx.stroke();

// 阶段 2: 英文文本 (含 ☠ 特殊字符)
ctx.fillStyle = '#42e1a2';
ctx.font = "15.4px 'Arial'";
ctx.textBaseline = 'alphabetic';
ctx.fillText('PR flacks quiz gym: TV DJ box when? ☠', 15, 60);

// 阶段 3: 中文文本 + 阴影
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 2;
ctx.shadowColor = 'white';
ctx.fillStyle = 'rgba(0, 0, 200, 0.5)';
ctx.font = "60px 'Not a real font'";
ctx.fillText('No骗', 40, 80);

2. toDataURL

canvas.toDataURL() → PNG base64(实测 17362 字符,300×150)

  • 依赖实际渲染(字体/抗锯齿/渲染器)→ c 是环境相关设备指纹
  • 同一浏览器稳定(同渲染器);不同设备/浏览器可能不同
  • SDK 缓存到 __cached_canvas(首次计算后续复用)

3. hash

input = "envCollect" + dataURL;
c = md5(input + SALT);

验证证据

步骤 结果
canvas 调用 hook 17 次调用(SET×11 + 方法×6),无 getImageData
concat hook ["envCollect", dataURL] 数组 + 字符串拼接
md5('envCollect'+dataURL+SALT) d292170ca009238e7f6c117ac97f13fa
跨会话 c 值 稳定(同渲染器)

浏览器环境采集

演示数据:

const webdriverFlag = 0;
const pluginSize = 5;
const headChildCount = 29;
const visitKey = random();
const envRandom = random();
const envJSON = {
  sua: "Windows NT 10.0; Win64; x64",
  extend: {
    wd: webdriverFlag, l: 0, ls: pluginSize, wk: 0, bu1: "v9", bu2: -1, bu3: headChildCount,
    bu4: 0, bu5: 0, bu8: 0, c: getCanvasFp()
  },
  random: envRandom,
  v: "v9",
  fp: visitKey
};
// extend 各字段生成算法
//
// 用法 (浏览器):
//   const { genExtend } = require('./algo/extend.js');
//   const extend = genExtend();            // 从全局环境检测
// 用法 (注入环境):
//   const extend = genExtend({ navigator: navStub, document: docStub, window: winStub, canvas: canvasEl });
const crypto = require('crypto');

// ---- 工具: 环境获取 (支持注入) ----
function envOf(opts, name) {
  if (opts && opts[name] !== undefined) return opts[name];
  if (name === 'navigator' && typeof navigator !== 'undefined') return navigator;
  if (name === 'document' && typeof document !== 'undefined') return document;
  if (name === 'window' && typeof window !== 'undefined') return window;
  return undefined;
}

// =====================================================================
// wd — navigator.webdriver 直读 (实证: true→1, false→0
// =====================================================================
function genWd(nav) {
  try {
    return nav && nav.webdriver === true ? 1 : 0;
  } catch (e) {
    return 0;
  }
}

// =====================================================================
// l = !navigator.languages || navigator.languages.length === 0 ? 1 : 0
// =====================================================================
function genL(nav) {
  try {
    return !nav || !nav.languages || nav.languages.length === 0 ? 1 : 0;
  } catch (e) {
    return 1;
  }
}

// =====================================================================
// ls — navigator.plugins.length
// =====================================================================
function genLs(nav) {
  try {
    return nav && nav.plugins ? nav.plugins.length : 0;
  } catch (e) {
    return 0;
  }
}

// =====================================================================
// wk — 反自动化 bitfield (实证模型)
//   位1 (1): window 上 ChromeDriver/DevTools 标记 (参考版 4428: cdc_adoQpo 等)
//   位2 (2): window 上第二类自动化标记 (参考版同段)
//   位4 (4): navigator.webdriver 属性存在 (实证: 原生 true→4, 篡改后仍含 4)
//   位8+32 (40): navigator.webdriver 被 defineProperty 篡改 (实证: 改后 wk=44)
//   实证: 原生 webdriver=true→4; defineProperty 覆盖→44; 旧会话(webdriver 缺失)→0
// =====================================================================
const DRIVER_MARKS = [
  'cdc_adoQpo',      // ChromeDriver 注入标记
  'webdriver',       // 自动化标记 (window 级)
  '__nightmare',     // Nightmare
  'domAutomation',   // 老版 WebDriver
  '_phantom',        // PhantomJS
];
function genWk(nav, win) {
  let wk = 0;
  try {
    // 位1/2: window 级自动化标记 (参考版检测 cdc_adoQpo... 字符串)
    let markCount = 0;
    for (const m of DRIVER_MARKS) {
      if (win && m in win) {
        markCount++;
        wk |= markCount === 1 ? 1 : 2;
      }
    }
    // 位4: navigator.webdriver 属性存在 (非 undefined)
    if (nav && typeof nav.webdriver !== 'undefined') wk |= 4;
    // 位8+32: navigator.webdriver 被篡改 (data property 而非原生 getter)
    try {
      const desc = Object.getOwnPropertyDescriptor(nav, 'webdriver');
      if (desc && desc.value !== undefined && !desc.get && !desc.set) {
        wk |= 8;
        wk |= 32;
      }
    } catch (e) {}
  } catch (e) {}
  return wk;
}

// =====================================================================
// bu1 — 版本标记 "v9" (与 envJSON.v 同值, 所有样本恒定)
// =====================================================================
function genBu1() {
  return 'v9';
}

// =====================================================================
// bu2 — 非浏览器运行时检测 (node 实证: 输出 Error.stack 的特征行; 浏览器恒 -1)
//   字符串表: "bu2","bu3","head","childElementCount","process","release",
//             "name","versions","node","Deno","version","deno","Bun"
//   node 模拟实证: bu2 = "    at wrapModuleLoad (node:internal/modules/cjs/...)"
// =====================================================================
function genBu2() {
  try {
    if (typeof process !== 'undefined' && process.release && process.release.name === 'node') {
      // node 环境: 返回 stack 中 node 特征行
      const stack = new Error().stack || '';
      const line = stack.split('\n').find((l) => /node:internal|node_modules/.test(l));
      return line ? line.trim() : -1;
    }
    if (typeof Deno !== 'undefined' || typeof Bun !== 'undefined') {
      return 1; // 其他非浏览器运行时 (推断)
    }
    return -1; // 浏览器正常
  } catch (e) {
    return -1;
  }
}

// =====================================================================
// bu3 — document.head.childElementCount
// =====================================================================
function genBu3(doc) {
  try {
    return doc && doc.head ? doc.head.childElementCount : 0;
  } catch (e) {
    return 0;
  }
}

// =====================================================================
// bu4 — querySelector 劫持检测 (Bf 缓存实证)
//   SDK 在 querySelector 调用链构造 Error('test err'), stack 暴露调用来源;
//   若被代理/劫持 (微前端 __MICRO_APP_ENVIRONMENT__ 等) → 检测到 → 1
//   缓存键: "main.sign#__detecting" (window.__JDWEBSIGNHELPER_$DATA__)
// =====================================================================
const DETECT_KEY = 'main.sign#__detecting';
function genBu4(win) {
  try {
    const dataStore = (win && win.__JDWEBSIGNHELPER_$DATA__) || {};
    const cached = dataStore[DETECT_KEY];
    let stack = cached && cached.querySelector;
    if (!stack) {
      // 复现 SDK 检测: 记录 querySelector 调用点的 stack
      const e = new Error('test err');
      stack = e.stack || '';
      if (win) {
        win.__JDWEBSIGNHELPER_$DATA__ = win.__JDWEBSIGNHELPER_$DATA__ || {};
        win.__JDWEBSIGNHELPER_$DATA__[DETECT_KEY] = { querySelector: stack };
      }
    }
    // 异常帧特征: 微前端代理 / 劫持标记
    return /__MICRO_APP_ENVIRONMENT__|\[as querySelector\]|proxy/i.test(stack) ? 1 : 0;
  } catch (e) {
    return 0;
  }
}

// =====================================================================
// bu5 — document.all / HTMLAllCollection 检测
//   字符串表: "bu5","all","HTMLAllCollection","prototype"
//   浏览器 document.all 是特殊 falsy 对象 (typeof === 'undefined') → 0
//   无 all 或异常 → 检测值变化 (推断)
// =====================================================================
function genBu5(doc, win) {
  try {
    const hasAll = doc && 'all' in doc;
    const hasCtor = win && typeof win.HTMLAllCollection !== 'undefined';
    // 正常浏览器: all 存在 (特殊对象) + HTMLAllCollection 构造器存在 → 0
    if (hasAll && hasCtor) return 0;
    if (!hasAll) return 1; // 无 document.all (异常)
    return 0;
  } catch (e) {
    return 0;
  }
}

// =====================================================================
// bu8 — canvas 环境检测 (c 的前置检查)
//   字符串表: "bu8","c","__cached_canvas","createElement","getContext"
//   正常: getContext('2d') 可用 → 0 (推断)
// =====================================================================
function genBu8(doc) {
  try {
    if (!doc || typeof doc.createElement !== 'function') return 1;
    const canvas = doc.createElement('canvas');
    if (!canvas || typeof canvas.getContext !== 'function') return 1;
    const ctx = canvas.getContext('2d');
    return ctx ? 0 : 1;
  } catch (e) {
    return 1;
  }
}

// =====================================================================
// c — canvas 指纹
// =====================================================================
function genC(doc) {
  try {
    const canvas = doc.createElement('canvas');
    canvas.width = 300;
    canvas.height = 150;
    const ctx = canvas.getContext('2d');
    if (!ctx) return '';
    const { drawFingerprint } = require('./canvas_fp.js');
    drawFingerprint(ctx);
    return customHash('envCollect' + canvas.toDataURL());
  } catch (e) {
    return '';
  }
}

// =====================================================================
// genExtend — 生成完整 extend 对象
// =====================================================================
function genExtend(opts = {}) {
  const nav = envOf(opts, 'navigator');
  const doc = envOf(opts, 'document');
  const win = envOf(opts, 'window');
  return {
    wd: genWd(nav),
    l: genL(nav),
    ls: genLs(nav),
    wk: genWk(nav, win),
    bu1: genBu1(),
    bu2: genBu2(),
    bu3: genBu3(doc),
    bu4: genBu4(win),
    bu5: genBu5(doc, win),
    bu8: genBu8(doc),
    c: opts.canvas ? customHash('envCollect' + opts.canvas.toDataURL()) : genC(doc),
  };
}

module.exports = { genExtend, genWd, genL, genLs, genWk, genBu1, genBu2, genBu3, genBu4, genBu5, genBu8, genC };

// =====================================================================
// 自测: mock 浏览器环境 → 输出与实证样本对比
// =====================================================================
if (require.main === module) {
  const mockCtx = {
    fillStyle: '', fillRect() {}, strokeStyle: '', lineWidth: 0, lineCap: '', arc() {}, stroke() {},
    font: '', textBaseline: '', fillText() {}, shadowOffsetX: 0, shadowOffsetY: 0, shadowColor: '',
    toDataURL: () => 'data:image/png;base64,stub',
  };
  const nav = {};
  // 模拟浏览器原生: webdriver 是原型 getter (data property 会被 wk 判为篡改)
  Object.defineProperty(nav, 'webdriver', { get: () => true, configurable: true });
  Object.defineProperty(nav, 'languages', { get: () => ['zh-CN', 'zh'], configurable: true });
  // 模拟 PluginArray (ls = plugins.length, convry 页面 plugins=5)
  Object.defineProperty(nav, 'plugins', { get: () => ({ length: 5 }), configurable: true });
  const doc = {
    head: { childElementCount: 29 },
    createElement: () => ({ getContext: () => mockCtx, toDataURL: mockCtx.toDataURL }),
  };
  // 模拟 document.all (特殊 falsy 对象)
  doc.all = [];
  const win = { HTMLAllCollection: function () {}, __JDWEBSIGNHELPER_$DATA__: {} };
  const env = genExtend({ navigator: nav, document: doc, window: win });
  console.log('mock 输出:', JSON.stringify(env, null, 1));
  // 实证样本 (convry.com 实机, headCC=29): {wd:1,l:0,ls:5,wk:4,bu1:"v9",bu2:-1,bu3:29,bu4:0,bu5:0,bu8:0,c:"d292170ca009238e7f6c027ac97f13fa"}
  // 注意: bu2 在 node 环境必然输出 stack 行 (实证行为), 浏览器才恒 -1
  const expect = { wd: 1, l: 0, ls: 5, wk: 4, bu1: 'v9', bu3: 29, bu4: 0, bu5: 0, bu8: 0 };
  const diff = [];
  for (const [k, v] of Object.entries(expect)) {
    if (env[k] !== v) diff.push(`${k}: ${JSON.stringify(env[k])} ≠ ${JSON.stringify(v)}`);
  }
  if (/node:internal/.test(String(env.bu2))) console.log('bu2: node 环境 stack 行 ✓ (实证行为, 浏览器为 -1)');
  else if (env.bu2 !== -1) diff.push(`bu2: ${JSON.stringify(env.bu2)} ≠ -1/stack行`);
  console.log(diff.length ? '差异: ' + diff.join(', ') : '✅ 与实证样本一致 (c 除外, mock canvas)');
}
posted @ 2026-08-06 18:07  xtyuns  阅读(5)  评论(0)    收藏  举报