某电商 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)');
}

浙公网安备 33010602011771号