百趣云 百趣云的博客

WebGL 指纹:GPU 信息是怎么被收集的

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);       // 16

UNMASKED_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 指纹在风控中的真实权重

  1. 它是"环境真实性"校验的核心,不是"设备唯一性"的核心。风控主要用它识别"无头浏览器/Node 环境"——真实浏览器有完整 WebGL 栈,Node 补环境的 WebGL 是空的或残缺的
  2. headless Chrome 的 WebGL 特征:旧版 headless 的 renderer 是 SwiftShader(软件渲染),一眼假;新版 headless 用真实 GPU 但 webdriver 属性和 WebGL 耗时分布仍有破绽
  3. Node 补环境的取舍:实现完整 WebGL 存根工作量巨大,工程上的主流方案是固定真实采集值 + 祈祷风控不做动态渲染校验。遇到做动态校验的(如部分电商大促期间),只能上真实浏览器方案

总结

  1. WebGL 指纹 = 参数信息(GPU 型号/能力上限/扩展列表)+ 渲染 hash
  2. UNMASKED_RENDERER_WEBGL 直接暴露 GPU 型号,区分度最高
  3. 伪造必须成套(整套真实设备档案),单改 vendor 必露馅
  4. 渲染 hash 只能加噪不能固定,动态校验是补环境的天花板

交流微信:run1255

By 百趣云 阅读量:3 On