WebGL 指纹的两类信息
WebGL 给网页暴露了 GPU 的访问能力,风控从两个层面收集指纹:
第一类:参数信息(静态指纹)
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl');
// 基础参数
gl.getParameter(gl.VENDOR); // "WebKit"
gl.getParameter(gl.RENDERER); // "WebKit WebGL"
gl.getParameter(gl.VERSION); // "WebGL 1.0 ..."
// 调试扩展里的真实 GPU 信息(最有价值)
var ext = gl.getExtension('WEBGL_debug_renderer_info');
gl.getParameter(ext.UNMASKED_VENDOR_WEBGL); // "Google Inc. (NVIDIA)"
gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); // "ANGLE (NVIDIA GeForce RTX 3060...)"
// 支持的扩展列表(几十上百个,组合高度唯一)
gl.getSupportedExtensions();
// 各种能力上限值
gl.getParameter(gl.MAX_TEXTURE_SIZE); // 16384
gl.getParameter(gl.MAX_VERTEX_ATTRIBS); // 16UNMASKED_RENDERER_WEBGL 直接给出 GPU 型号——这一个字段的区分度就极高。再加上几十个 MAX_* 参数和扩展列表,组合指纹几乎是设备唯一的。
第二类:渲染指纹(动态指纹)
和 canvas 指纹同理:用 WebGL 渲染一个 3D 场景(三角形+光照+纹理),截图算 hash。不同 GPU 的浮点运算精度、抗锯齿实现产生像素级差异。
逆向定位:hook 采集点
// hook getParameter,看风控读了哪些参数
const origGetParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(pname) {
var result = origGetParameter.apply(this, arguments);
console.log('[webgl] getParameter(' + pname + ') =', result);
console.trace();
return result;
};
// hook getExtension,看用了哪些扩展
const origGetExt = WebGLRenderingContext.prototype.getExtension;
WebGLRenderingContext.prototype.getExtension = function(name) {
console.log('[webgl] getExtension:', name);
return origGetExt.apply(this, arguments);
};跑一遍目标站点,它采集的 WebGL 字段清单就出来了。
对抗:伪造的三个层次
层次一:改参数返回值(简单场景)
const origGetParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(pname) {
// UNMASKED_VENDOR_WEBGL = 37445, UNMASKED_RENDERER_WEBGL = 37446
if (pname === 37445) return 'Google Inc. (Intel)';
if (pname === 37446) return 'ANGLE (Intel(R) UHD Graphics 630 Direct3D11...)';
return origGetParameter.apply(this, arguments);
};层次二:成套伪造(过一致性校验)
只改 vendor/renderer 而 MAX_* 参数和扩展列表还是原机器的,一致性校验立刻露馅——Intel 核显不可能支持 32768 的 MAX_TEXTURE_SIZE。正确做法:从真实设备采集整套 WebGL 参数存档,伪造时整套替换:
var REAL_PROFILE = {
37445: 'Google Inc. (Intel)',
37446: 'ANGLE (Intel(R) UHD Graphics 630 ...)',
3379: 16384, // MAX_TEXTURE_SIZE
34921: 16, // MAX_VERTEX_ATTRIBS
// ... 几十个参数的整套真实值
extensions: ['ANGLE_instanced_arrays', 'EXT_blend_minmax', ...]
};层次三:渲染指纹干扰(终极对抗)
渲染 hash 没法用固定值伪造(风控会换渲染内容),只能加噪:hook readPixels 在返回前做最低位翻转,同 canvas 噪声方案。
精髓:WebGL 指纹在风控中的真实权重
- 它是"环境真实性"校验的核心,不是"设备唯一性"的核心。风控主要用它识别"无头浏览器/Node 环境"——真实浏览器有完整 WebGL 栈,Node 补环境的 WebGL 是空的或残缺的
- headless Chrome 的 WebGL 特征:旧版 headless 的 renderer 是
SwiftShader(软件渲染),一眼假;新版 headless 用真实 GPU 但webdriver属性和 WebGL 耗时分布仍有破绽 - Node 补环境的取舍:实现完整 WebGL 存根工作量巨大,工程上的主流方案是固定真实采集值 + 祈祷风控不做动态渲染校验。遇到做动态校验的(如部分电商大促期间),只能上真实浏览器方案
总结
- WebGL 指纹 = 参数信息(GPU 型号/能力上限/扩展列表)+ 渲染 hash
- UNMASKED_RENDERER_WEBGL 直接暴露 GPU 型号,区分度最高
- 伪造必须成套(整套真实设备档案),单改 vendor 必露馅
- 渲染 hash 只能加噪不能固定,动态校验是补环境的天花板
交流微信:run1255