Canvas 指纹:工作原理与应对方法
了解 Canvas 渲染差异如何变成稳定的设备标识,为什么 VPN 和隐私浏览不会改变它,以及由配置文件驱动的确定性输出如何工作。
BotBrowser Team
BotBrowser 可以在获授权且声明清楚的 profile 与 seed 测试中让 Canvas 读回结果可复现。但它不能保证匿名、被网站接受,也不能保证不同宿主机和图形栈得到完全相同的渲染,因此本文把 Canvas 视为一个有边界的隐私与兼容性信号。
摘要
Canvas 指纹来自已渲染的像素,清除存储或改变 IP 不会改变它。比较时声明 profile、--bot-noise-seed、显示器、字体和图形后端;输出一致只说明有边界的 Canvas 结果,不证明身份,也不证明网站会接受访问。
目录
- Canvas 指纹测量的是什么
- 为什么同一段绘制在不同设备上会不同
- 为什么 VPN、隐私浏览和扩展不够
- 处理 Canvas 输出的四种方式
- BotBrowser 如何处理 Canvas
- 检查稳定性并排查差异
观看受控 Canvas 演示
这段英文视频比较了一个合成 Canvas 在人为修改一个字节前后的结果,展示受控本地环境中的像素字节对比。它不证明设备身份,也不证明不同设备之间的自然差异。音轨为英文。
HTML5 Canvas 元素的本职是绘图。图表、地图、图片编辑器、签名板、游戏和仪表盘都在使用它,所以它是使用最广泛的 Web API 之一,普通网站每天都依赖它。
Canvas 也有一个副作用。浏览器把文字或图形绘制到画布上时,最终的像素取决于设备的 GPU、图形驱动、操作系统的文字渲染、色彩管理和抗锯齿方式。两台电脑执行同样的绘制指令,返回的像素数据可能略有不同,而同一台电脑每次返回的数据都相同。正是这种可重复性,让输出可以被用作识别浏览器的信号。
追踪研究已经在真实网络中记录过这种做法。普林斯顿 Web Census 对排名前一百万的网站做了大规模测量,在其中超过 14,000 个网站上发现了 Canvas 指纹脚本。这些数字属于该研究,并非 BotBrowser 的测量结果。
Canvas 指纹不需要权限提示,不在设备上保存任何内容,也不会向访问者显示任何迹象。浏览器没有内置的开关可以关闭这个信号,而且无论访问者是否看到页面元素,像素都会被生成,因此除非有手段改变 Canvas 输出的生成方式,这个信号就会一直存在。下面的章节依次说明这个信号如何产生、常见隐私工具为什么不会改变它、处理 Canvas 输出的几种方式,以及如何在授权测试中检查输出是否稳定。
Canvas 指纹测量的是什么
指纹脚本并不关心图像本身。典型的收集流程只有五个简短步骤:
- 脚本在页面或嵌入的框架中运行,通常与广告或统计组件打包在一起。
- 它创建一块画布,绘制固定的测试图案:多种字体的文字、渐变、相互重叠的形状和半透明颜色。
- 它用
toDataURL或getImageData读回像素。 - 它把像素数据压缩成一个简短的摘要值。
- 它把这个摘要值连同其他浏览器信号一起发送到服务器,并与之前的访问进行匹配。
这个摘要值对追踪者有用,原因有三:清除 Cookie 后它不变,更换 IP 地址后它不变,而且在任何页面上收集它的成本都很低。网站再次看到同一个摘要值,就可以把两次访问关联起来;而部署在许多网站上的追踪者,还能把互不相关的域名之间的访问关联起来。
Canvas 很少单独使用。脚本会把它与 WebGL 渲染器信息、已安装字体、屏幕属性、时区和语言组合起来。合在一起后,即使每个信号都被许多浏览器共享,也足以锁定某一台设备。Canvas 贡献的是来自图形和文字渲染路径的那一部分,而这一部分很难靠修改其他设置来改变。
读回像素本身也是正常操作。图表、图片编辑器、签名采集和部分验证码都出于正常原因读取像素,所以仅凭这个调用并不可疑。值得关注的是那种唯一目的就是生成标识的隐藏测试图案。MDN 上的 Canvas API 文档和 HTML 标准的 canvas 章节描述了接口本身,两者都列在下方的公开来源中。
审查网站是否存在指纹收集的人,通常寻找的是一种可辨认的形态,而不是单个 API 调用。创建后从未挂载到页面上的画布、很小的固定尺寸、包含大量不同字母的句子、相互重叠的彩色形状,以及读回像素后立即通过网络发送,都是典型迹象。这些迹象单独都不能证明意图,正常的图表也可能具备其中一部分,所以审查者会结合脚本如何处理结果来综合判断。
为什么同一段绘制在不同设备上会不同
要做可复现的比较,先记录具体绘制配方:Canvas 的 width 和 height、传给 fillText() 的字符串与字体,再比较 getImageData(0, 0, width, height) 返回的字节。任何一个输入改变,都不能证明图形栈已经改变。
规范描述的是一次绘制调用的含义,而不是每个输出像素的精确数值,因此各个平台有差异的空间。实际上,渲染路径中有好几层都会产生影响:
- 字体光栅化。 Windows、macOS 和 Linux 各自使用自己的文字渲染栈,字形形状、微调和边缘平滑都不同。
- GPU 与驱动。 渐变、混合和合成经过图形硬件和驱动处理,每个颜色通道的最后几位可能不同。
- 色彩管理。 操作系统采用不同的颜色配置文件和伽马处理,这会改变渐变和半透明形状内部的数值。
- 抗锯齿。 边缘平滑因平台、驱动版本和系统设置而异。这些差异肉眼看不见,但存在于像素数据中。
- 浮点运算。 曲线和几何运算在不同硬件上的舍入方式可能不同,会改变边缘上的少数像素。
字体还增加了一层。脚本请求的字体未安装时,平台会回退到默认字体,而回退的选择取决于操作系统及其已安装字体。因此,用同一个字体名称绘制同样的文字,在两个系统上可能得到不同的字形,这也是字体本身被视为独立指纹面的原因。
每个差异都很小,但指纹只需要一个像素不同即可。这种组合在同一台机器上稳定,在不同机器之间则有所变化。重启浏览器、清除缓存或打开隐私窗口之后它依然存在,因为这些操作都不会改变底层的图形栈。
反过来也一样。操作系统版本、GPU、驱动和字体都相同的两台机器,可能返回完全一致的输出,所以单靠 Canvas 只能缩小范围,而不能指认某一台设备。这就是追踪者把它与其他信号配合使用的原因,也是只关注 Canvas 的防御会让其余组合保持原样的原因。
这也是 Canvas 与设备配置文件的其余部分紧密相连的原因。一个浏览器报告苹果 GPU 和 Mac 用户代理,却产生典型的 Linux 文字渲染所得的 Canvas 输出,它在内部就是不一致的,而信号之间是否一致,恰恰是分析指纹的人关注的重点。
为什么 VPN、隐私浏览和扩展不够
边界体现在 API 上:VPN 改变的是 RTCPeerConnection 或 HTTP 可见的地址,Canvas 识别通过 getImageData() 或 toDataURL() 读取像素。清除 localStorage 或 Cookie 改变的是保存状态,不是这些读回调用返回的位图。
每种常见的隐私工具作用的层次,都不是 Canvas 读取数据的那一层。
VPN 与代理。 VPN 或代理改变的是网站看到的网络地址。Canvas 输出由浏览器基于本地渲染生成,从不经过网络路径,所以处在同一个 VPN 之后的两台设备,依然各自返回自己的 Canvas 输出。
隐私浏览。 隐私窗口会在会话结束时丢弃 Cookie、历史记录和站点存储。它们不会改变渲染路径,所以隐私窗口中的 Canvas 输出与普通窗口一致。隐私浏览指南说明了这些模式会改变什么、不会改变什么。
阻止 Canvas 的扩展。 阻止读回会让调用失败或返回空数据。很少有真实的浏览器会这样表现,因此阻止行为本身就可能显得突出,而正当使用 Canvas 的页面也可能无法工作。
添加随机噪声的扩展。 随机噪声会在每次加载时产生新的值。这能避免出现一个稳定的标识,但每次访问都变化的值同样不寻常,而且在脚本层添加的噪声,未必与真实设备之间的自然差异相似。
返回虚假数据的扩展。 被替换的像素可能破坏图表、地图和验证码,而且它们常常与同一浏览器在其他地方报告的 GPU、字体和操作系统不一致。
浏览器内置的随机化。 一些浏览器,包括 Brave,会按站点为 Canvas 输出添加少量随机变化。这比阻止要好,但结果仍可能与同一浏览器报告的 GPU、字体和操作系统信号对不上,而且每种实现自行决定覆盖哪些 Canvas 操作。
各工具效果不一。 没有哪个工具能改变每一层。VPN 改变网络地址而不改变像素,隐私浏览改变存储而不改变像素,扩展改变脚本看到的内容而不改变页面的渲染方式。每个工具只解决一个方面的问题,而让 Canvas 信号保持原样,因此要得到持久的解决办法,就必须从像素本身入手。
权限提示。 Tor Browser 会在网站读取 Canvas 图像数据之前询问。这让读回行为变得可见,但决定权交给了访问者,而被拒绝的请求可能让发起请求的页面出错。
处理 Canvas 输出的四种方式
用两个决定性配置记录选择:--bot-noise-canvas=true 启用渲染层策略,--bot-noise-seed=42 固定一个可复现的噪声模式。不要把未声明种子的运行与固定种子的运行相互比较。
这几种方式各有取舍。用两个问题就能轻松比较:同样的设置下输出是否稳定,以及它是否与浏览器报告的其他一切保持一致?
- 阻止。 输出因为始终为空而稳定,但与正常浏览器不一致,还很可能让需要 Canvas 的页面出错。
- 随机噪声。 输出在每次加载或每个来源上都不同。这限制了噪声范围内的关联,但无法为测试提供可重复的结果,而且可能与其他信号不匹配。
- 虚假数据。 固定的替代值可以重复,但与页面实际绘制的内容无关,所以依赖绘制结果的功能和其他信号都可能与它冲突。
- 配置文件驱动的确定性输出。 Canvas 输出遵循已加载的配置文件和固定的噪声种子,因此相同的配置文件和种子每次得到相同的结果,而不同的配置文件或种子得到不同的结果。
最后一种方式对授权测试和隐私研究很重要,因为它让 Canvas 行为可以复现。如果测试显示输出发生了变化,原因就是被改动的配置文件或种子,而不是上一次运行遗留的噪声。
选哪种方式取决于目标。阻止适合可以接受页面出错的严格受限的只读环境。随机噪声适合日常浏览,目的只是避免在站点之间被关联。确定性输出适合需要重复并解释结果的工作,比如对你自己运营的网站做回归检查,或者比较一种配置与另一种配置的研究。
确定性并不意味着信号消失。Canvas 只是众多信号之一,一个稳定的值如果与 WebGL、字体、时区、区域设置或网络位置不一致,看起来仍然不协调。因此最后一节中的检查不会只限于 Canvas。
BotBrowser 如何处理 Canvas
BotBrowser 的 Canvas 文档描述了针对 Canvas 读回的、由配置文件驱动的保护。加载配置文件后,BotBrowser 在渲染层对 Canvas 2D 输出应用确定性噪声,而不是通过 JavaScript 注入,文档说明这涵盖像素读回和文字度量。对上述场景有意义的有四项已记录的行为:
- Canvas 噪声默认启用,也可以用
--bot-noise-canvas=true或--bot-noise-canvas=false显式设置。 --bot-noise-seed选择一种特定且可复现的噪声模式。每个种子产生独特但稳定的 Canvas 输出,同一个种子也适用于 WebGL、WebGPU 和文字度量等其他表面。- 加载配置文件后,BotBrowser 使用内置字体库和渲染能力,让团队在文档所述的配置文件、种子、显示和图形条件下比较不同宿主上的 Canvas 输出;它不保证所有宿主或图形栈都产生完全相同的输出。
- BotBrowser 150 对标准和宽色域色彩空间(包括
display-p3)的 Canvas 2D 保护保持有效。
固定配置文件和种子的最简启动方式如下:
chromium-browser --bot-profile="path/to/profile.enc" --bot-noise-seed=42
由于每个种子都有各自稳定的结果,运行多个会话的团队可以给每个会话各自的种子。这样会话彼此保持区分,而在重复使用同一个种子时又能完全重现,这正是回归测试所需要的特性。
保持设置简单。再叠加一个同样会修改 Canvas 输出的工具,比如扩展,会让结果更难解读,因为两层都在塑造同一批像素。
为了便于审查,CanvasLab 文档介绍了如何用 --bot-canvas-record-file 记录页面发出的 Canvas 2D、WebGL、WebGL2 和 WebGPU 调用,让团队在决定测试什么之前先看清页面实际绘制和读取了什么。
检查稳定性并排查差异
一次有用的检查分三步,不需要自定义的收集代码。只在你获得授权测试的页面和系统上运行。
- 使用配置文件和固定的
--bot-noise-seed启动,打开一个公开的指纹测试页面,记下报告的 Canvas 输出。 - 重新加载,然后用同一个配置文件和种子关闭并重启会话。输出应与第一次运行一致。
- 更换种子或配置文件。输出应当改变。
让测试范围保持狭窄并有文档记录。使用你拥有的页面或已获得书面许可的页面,并且只把结果当作关于 Canvas 输出的证据,而不是对其他任何指纹表面或某个具体网站反应的结论。
在每个结果旁边记下设置。BotBrowser 版本、配置文件、种子值、宿主操作系统,以及这次运行是无头还是有头模式,这些信息足以让之后的运行得到公平的比较。没有这份记录,两次运行之间的差异就无法追溯到原因。
当结果与预期不符时,BotBrowser 文档指出了几个常见原因:
- 使用同一个配置文件,输出在会话之间发生变化。没有固定种子时,噪声按设计在每个会话中不同,所以请确认使用的是同一个
--bot-noise-seed值。 - 无头和有头运行的输出不同。请确认两者都设置了
--bot-noise-canvas=true并使用同一个配置文件。 - 输出看起来像原始的系统输出。请确认配置文件已正确加载,并查看启动消息。
- 在另一种宿主操作系统上输出不同。先确认 profile、seed、显示、图形后端和字体条件;配置文件让比较可复现,但不保证所有宿主输出完全相同。
- 宽色域 Canvas 的表现与标准 Canvas 不同。请使用 BotBrowser 150.0.7871.46 或更新版本,并搭配匹配的配置文件包。
请谨慎解读第一次的结果。单次运行只能说明输出存在。跨重新加载、重启、种子和配置文件的比较,才能说明输出是否按预期工作,而第三步没有出现变化,与第二步出现不一致一样能说明问题。
然后检查 Canvas 没有覆盖的部分。把 WebGL 渲染器的值、已安装字体、时区、区域设置和网络位置与同一个配置文件对照,因为稳定的 Canvas 结果旁边如果是与配置文件不一致的时区或语言,整体画面仍然不一致。关于 WebGL 指纹、字体指纹和噪声种子的可复现性的文章涵盖了这些相邻的信号。
BotBrowser 能够在渲染层应用由配置文件驱动的确定性 Canvas 2D 噪声,在声明的条件下可按 --bot-noise-seed 复现,供团队在本文所述的授权测试和隐私研究场景中比较 Canvas 读回。BotBrowser 不能保证匿名或网站不会识别、追踪或拦截访问者;它也不能控制网站的 Canvas 收集脚本或服务器端设备聚类。跨宿主的可复现比较要求保持配置文件、种子、显示、图形后端和字体条件一致,但不代表所有图形栈都会产生完全相同的输出。这也不能替代让 WebGL、字体、时区、区域设置和代理信号与同一个配置文件保持一致的工作。
相关延伸阅读:文本测量指纹识别:亚像素追踪技术。
实践结论
在授权比较中固定绘制配方,并把 profile、--bot-noise-seed、显示器、字体和图形后端与像素读回一并记录。意外差异首先是配置问题;稳定或变化的 digest 都不能证明匿名,也不能证明远端网站的决定。
公开来源
本文属于指南: 什么是浏览器指纹?原理、风险、信号与隐私边界