1. 从“显卡身份证”到“指纹追踪”:WebGPU指纹到底是什么?
如果你用过指纹浏览器,肯定知道浏览器指纹这回事。简单说,就是网站通过你浏览器暴露的各种信息,比如屏幕分辨率、安装的字体、时区、语言等等,像拼图一样拼出一个几乎能唯一标识你的“指纹”。这玩意儿在风控和广告追踪里用得可太广了。
那WebGPU指纹又是啥新花样?你可以把它理解成你电脑里那块显卡(GPU)的“身份证”。以前,网页里的JavaScript想用GPU干点重活(比如3D游戏、AI计算),只能通过一个叫WebGL的老接口,能获取的信息比较有限。但现在,一个更强大、更现代的新标准来了,它就是WebGPU。
WebGPU的初衷是好的,它让网页应用能更高效、更直接地调用你设备的GPU能力,带来媲美原生应用的图形和计算性能。但就像任何新技术一样,它一出现,就被“有心人”盯上了。因为为了和你的GPU正确通信,浏览器必须向网页JavaScript暴露一系列关于GPU的详细信息:比如GPU的型号、驱动版本、支持哪些高级特性、各种性能参数的上限是多少……这些信息组合起来,经过哈希(一种不可逆的加密计算)处理,就生成了所谓的WebGPU指纹。
我实测过,用同一台电脑、同一个Chrome浏览器,这个指纹值在短时间内是稳定不变的。这就意味着,网站可以通过这个值,在你清除Cookie、甚至使用无痕模式后,依然认出“哦,又是你”。不过,目前WebGPU指纹的“杀伤力”还相对有限。主要原因有两个:一是还有很多用户的浏览器不支持WebGPU(特别是某些旧版本或移动端),采集不到;二是不同用户如果用的是同型号显卡、同版本驱动,生成的指纹可能相同,唯一性没有传统浏览器指纹那么高。所以目前在一些风控系统里,它的权重可能还不算顶级。但作为搞技术对抗的我们,必须得有前瞻性,等它普及开来、算法优化后再动手,可能就晚了。
2. 攻防第一课:网站如何采集你的WebGPU指纹?
有攻才有防,咱们先得摸清楚“敌人”是怎么出招的。网站采集WebGPU指纹的代码其实非常直接,全是标准的JavaScript API调用,没有任何黑魔法。下面我带你一步步拆解,你甚至可以自己打开浏览器的开发者工具(F12),在Console(控制台)里亲手运行一下,看看自己的“显卡身份证”长啥样。
整个流程就像查户口:先问浏览器“你支持WebGPU吗?”,如果支持,就找到GPU适配器(Adapter),再通过适配器请求一个逻辑设备(Device)。最关键的信息,就藏在这个设备对象里。
async function getWebGPUFingerprint() {
// 第一步:检查浏览器是否支持WebGPU
if (!navigator.gpu) {
console.log("很遗憾,您的浏览器不支持WebGPU。");
return null;
}
// 第二步:请求GPU适配器(可以理解为显卡的抽象接口)
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
console.log("未能获取到GPU适配器。");
return null;
}
// 第三步:通过适配器请求一个逻辑设备(用于执行具体任务)
const device = await adapter.requestDevice();
// 第四步:收集关键指纹信息
// 1. 收集支持的扩展特性列表(比如是否支持某种特殊的纹理压缩格式)
const supportedFeatures = Array.from(adapter.features.values());
console.log("GPU支持的特性扩展:", supportedFeatures);
// 2. 收集设备的限制参数(这是指纹的核心!)
const limits = {};
for (const key in device.limits) {
limits[key] = device.limits[key];
}
console.log("GPU硬件限制参数:", limits);
// 第五步:生成指纹
// 将上面收集的信息序列化成字符串
const dataString = JSON.stringify({
features: supportedFeatures,
limits: limits
});
console.log('待哈希的原始数据:', dataString);
// 使用SHA-256算法对字符串进行哈希,得到最终指纹(一串固定长度的十六进制数)
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(dataString));
const hashArray = Array.from(new Uint8Array(hashBuffer));
const fingerprint = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
console.log("您的WebGPU指纹是:", fingerprint);
return fingerprint;
}
// 执行函数
await getWebGPUFingerprint();
运行这段代码,你会看到一串类似 ffa2f3d7b37fd10b2c2eb8c7bb973462443ec1d10040191a399c68fab4d812ee 的64位十六进制字符串。这就是你的WebGPU指纹。网站后台拿到这个字符串,就可以存入数据库。下次你再来访问,即使换了IP、清了Cookie,只要再生成一次指纹一对比,大概率就能匹配上,从而实现跨会话的追踪。
这里面的 limits 对象是重中之重,它包含了GPU的各种能力上限,比如最大纹理尺寸、最大绑定组数量、最大计算工作组大小等等。这些值是由你的显卡硬件型号、驱动版本、以及浏览器对WebGPU的实现共同决定的,所以具有相当高的辨识度。
3. 防御的核心思路:让指纹“动”起来
知道了采集原理,防御思路就清晰了:我们不能让网站每次采集到的指纹值都一样。理想的状态是,每次浏览器实例启动,甚至每次刷新页面,这个指纹值都能发生变化,让网站无法建立有效的追踪。
怎么实现呢?最根本、最彻底的方法,就是修改浏览器源码。我们不是要伪造一个假的GPU信息(那可能导致WebGPU功能异常),而是要对那些影响指纹生成、但又不太影响普通网页应用正常使用的关键参数进行“随机化处理”。打个比方,你的身份证号码(GPU型号)不能改,但身份证上的签发日期(某些限制参数)可以稍微变一变,只要在合理的范围内变动,不影响你坐高铁住酒店(使用WebGPU功能),但足以让只看签发日期来认人的系统搞糊涂。
在WebGPU的 limits 里,有很多参数值非常大(比如 maxTextureDimension2D 可能是16384),是给极端专业应用准备的。绝大多数普通网页应用,比如一个3D产品展示页或者一个简单的AI推理demo,根本用不到这么高的极限。这就给了我们操作空间:我们可以把这些参数的值,在每次浏览器启动时,在一个合理的范围内随机化。
比如,一个参数标准值是65535,我们可以在编译浏览器时,让它每次返回一个介于60000到65535之间的随机数。这样,对于绝大多数网站功能毫无影响,但采集到的指纹却彻底变了。这就是“随机化编译”的核心思想——在浏览器构建(编译)阶段,就注入随机性,生成一个独一无二、且动态变化的二进制执行文件。
4. 实战:修改Chromium源码,实现参数随机化
光说不练假把式,接下来我手把手带你进入硬核实战环节。这里假设你已经按照我之前文章里教的方法,成功搭建了Chromium的编译环境。如果还没搞定,那得先去把基础打好,因为编译一次Chromium确实挺耗时间的。
我们的目标很明确:找到Chromium源码中定义WebGPU限制参数的地方,并修改它,让其中一个关键参数——maxComputeWorkgroupsPerDimension(它表示GPU在每个维度上能处理的最大计算工作组数量)——每次查询时返回一个随机值。
第一步:定位关键源码文件
用你的代码编辑器打开Chromium源码目录,找到这个文件:
/third_party/blink/renderer/modules/webgpu/gpu_supported_limits.cc
这个文件就是定义所有WebGPU限制参数具体值的地方。
第二步:添加必要的头文件
我们需要用到Chromium的命令行工具来传递随机种子。在文件顶部,找个已有 #include 的地方,加上下面这行:
#include "base/command_line.h"
同时,为了生成随机数,我们可能还需要用到时间戳,可以加上:
#include <chrono>
#include <ctime>
#include <sstream>
第三步:注释掉原有的参数定义
在这个文件中,你会看到一个宏定义 SUPPORTED_LIMITS,它像列表一样枚举了所有参数。我们要找到 maxComputeWorkgroupsPerDimension 这一行,并把它注释掉。这意味着我们不再使用系统查询到的固定值。
#define SUPPORTED_LIMITS(X) \
X(maxTextureDimension1D) \
X(maxTextureDimension2D) \
... // 其他很多参数
X(maxComputeWorkgroupSizeZ) \
//X(maxComputeWorkgroupsPerDimension) // 我们注释掉这一行!
第四步:实现自定义的随机化函数
在同一个 .cc 文件的某个函数定义区域后面(比如文件末尾),添加我们自己的函数实现。这里我提供两个更健壮的方案:
方案A:基于启动命令的随机种子(推荐,更可控)
unsigned GPUSupportedLimits::maxComputeWorkgroupsPerDimension() const {
// 尝试从命令行参数获取种子
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
static int seed = 0; // 使用静态变量,确保一次浏览器生命周期内值固定
static bool seed_initialized = false;
if (!seed_initialized) {
if (command_line->HasSwitch("webgpu-seed")) {
// 如果启动Chrome时带了 --webgpu-seed=12345 参数,则使用这个值
std::string seed_str = command_line->GetSwitchValueASCII("webgpu-seed");
seed = std::abs(std::hash<std::string>{}(seed_str)) % 128;
} else {
// 否则,使用当前时间戳的毫秒部分作为随机源
auto now = std::chrono::system_clock::now();
auto duration = now.time_since_epoch();
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
seed = static_cast<int>(millis % 128);
}
seed_initialized = true;
}
// 返回一个介于64和seed+64之间的值,确保不会太小导致兼容性问题
return 64 + (seed % 64);
}
方案B:纯随机生成(每次查询都变,但可能影响性能)
unsigned GPUSupportedLimits::maxComputeWorkgroupsPerDimension() const {
// 使用高质量的随机数引擎,以进程ID和时间戳为种子
static std::mt19937 generator(static_cast<unsigned>(
std::chrono::system_clock::now().time_since_epoch().count() ^ getpid()));
static std::uniform_int_distribution<unsigned> distribution(64, 127); // 合理范围
return distribution(generator);
}
我推荐使用方案A。因为它允许你通过启动参数来控制随机值,这在批量管理指纹浏览器 profiles 时非常有用。你可以为每个profile设置不同的种子,从而保证它们拥有不同且稳定的WebGPU指纹。
第五步:重新编译Chromium
保存文件后,回到你的编译输出目录,执行编译命令:
ninja -C out/Default chrome
这个过程会花费一些时间,取决于你的电脑性能。编译成功后,在 out/Default 目录下就会生成新的 chrome.exe(Windows)或 chrome(Linux/Mac)可执行文件。
5. 效果验证与高级对抗策略
编译好了,赶紧打开我们新编译的浏览器测试一下。再次访问之前那个指纹检测网站,或者运行我们第二章里的采集代码。多刷新几次页面,你会发现 maxComputeWorkgroupsPerDimension 的值变了,从而导致整个WebGPU指纹的哈希值彻底改变。恭喜你,第一步对抗成功了!
但是,真正的攻防哪有这么简单。一个成熟的指纹采集系统,不会只依赖一个参数。它们可能会检查多个参数的一致性和合理性。比如,一个消费级显卡却返回了类似专业计算卡才有的极限值,这就会引起怀疑。因此,我们的随机化策略需要更聪明:
- 多参数联动随机化:不要只改一个参数。可以挑选5-10个对普通应用影响小、但不同硬件间差异明显的参数(如
maxStorageBufferBindingSize,maxComputeInvocationsPerWorkgroup等)进行批量修改。它们的随机值可以关联起来,比如基于同一个种子生成,这样能保证参数间看起来是“自洽”的。 - 范围合理化:随机不是乱来。每个参数都有其合理的范围。你需要查询WebGPU标准文档或参考主流显卡的实际值,为每个参数设置一个最小值和最大值(例如,
maxComputeWorkgroupsPerDimension常见值是65535,我们可以让它在32768到65535之间随机)。我建议建立一个参数-合理范围映射表。 - 指纹稳定性与可变性的权衡:对于指纹浏览器,有时你需要指纹在单次使用会话中保持稳定(避免同一个标签页内刷新就变),但每次全新启动浏览器时发生变化。这可以通过将随机种子与浏览器Profile的某个持久化标识(如一个本地文件的内容)绑定来实现,而不是单纯用时间戳。
- 特征模拟:更高级的做法是模拟某一类常见显卡的参数组合。你可以收集一批常见显卡(如 NVIDIA GTX 1060, AMD RX 580, Intel Iris Xe)的WebGPU
limits真实数据,建立数据库。然后你的修改代码不是随机生成,而是从这个数据库中随机选取一套数据来返回。这样生成的指纹,不仅动态,而且“真实”。
验证网站方面,除了代码测试,你可以使用一些专业的在线指纹检测服务进行全方位验证,例如 browserleaks.com/webgpu 和 abrahamjuliot.github.io/creepjs/。它们会从多个维度检测你的浏览器指纹,观察WebGPU指纹项是否已成功“隐身”或“变化”。
最后必须提个醒,修改浏览器源码是底层且有效的方案,但需要一定的技术门槛和持续的维护(Chromium源码更新后需要合并修改)。对于大多数用户,使用成熟的、已集成此类对抗技术的指纹浏览器(如某些商业产品)是更便捷的选择。但了解其背后的原理,能让你更好地理解风险,并能在需要时进行深度定制。对抗指纹追踪是一场持续的技术博弈,核心思路就是:增加噪声,打破唯一性,让追踪的成本高于收益。

2549

被折叠的 条评论
为什么被折叠?



