什么是浏览器指纹
“浏览器指纹”是一种通过浏览器对网站可见的配置和设置信息来跟踪Web浏览器的方法,浏览器指纹就像我们人手上的指纹一样,具有个体辨识度,只不过现阶段浏览器指纹辨别的是浏览器。
那么浏览器指纹也是同理,获取浏览器具有辨识度的信息,进行一些计算得出一个值,那么这个值就是浏览器指纹。辨识度的信息可以是UA、时区、地理位置或者是你使用的语言等等,你所选取的信息决定了浏览器指纹的准确性。
对于网站而言,拿到浏览器指纹并没有实际价值,真正有价值的是这个浏览器指纹对应的用户信息。作为网站站长,收集用户浏览器指纹并记录用户的操作,是一个有价值的行为,特别是针对没有用户身份的场景。
例如在一个内容分发网站上,用户A喜欢浏览二次元的内容,通过浏览器指纹记录这个兴趣,那么下次用户不需要登录即可向A用户推送二次元的信息。在个人PC如此普及的当下,这也是一种内容分发的方式。
浏览器指纹背景
浏览器指纹追踪技术到目前已经进入2.5代。
- 第一代是状态化的,主要集中在用户的cookie和evercookie上,需要用户登录才可以得到有效的信息。
- 第二代才有了浏览器指纹的概念,通过不断增加浏览器的特征值从而让用户更具有区分度,例如(UA、浏览器插件信息)
- 第三代是已经将目光放在人身上了,通过收集用户的行为、习惯来为用户建立特征值甚至模型,可以实现真正的追踪技术,这部分目前实现比较复杂,依然在探索中。
目前处于2.5代是因为现在需要解决的问题是如何解决跨浏览器识别指纹的问题上,稍后会介绍下这方面所取得的成果。
指纹采集
信息熵(entropy)是接收的每条消息中包含的信息的平均量,熵越高,则能传输越多的信息,熵越低,则意味着传输的信息越少。
浏览器指纹是由许多浏览器的特征信息综合起来的,其中特征值的信息熵也是不尽相同。指纹也分为 基本指纹 和 高级指纹 ,基本指纹就是容易被发现和修改的部分,如http的header
{"headers": {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3","Accept-Encoding": "gzip, deflate, br","Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8","Host": "httpbin.org","Sec-Fetch-Mode": "navigate","Sec-Fetch-Site": "none","Sec-Fetch-User": "?1","Upgrade-Insecure-Requests": "1","User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36"}}
除了http中拿到的指纹,还可以通过其他方式来获得浏览器的特征信息,例如:
- 每个浏览器的用户代理字符串
- 浏览器发送的HTTP ACCEPT标头
- 屏幕分辨率和色彩深度
- 系统设置为时区
- 浏览器中安装的浏览器扩展/插件,例如Quicktime,Flash,Java或Acrobat,以及这些插件的版本
- 计算机上安装的字体,由Flash或Java报告。
- 浏览器是否执行JavaScript脚本
- 浏览器是否能种下各种cookie和“超级cookie(super cookies)”
- 通过Canvas指纹生成的图像的哈希
- WebGL指纹生成的图像的哈希
- 是否浏览器设置为“Do Not Track”
- 系统平台(例如Win32,Linux x86)
- 系统语言(例如,cn,en-US)
- 浏览器是否支持触摸屏
拿到这些值后可以进行一些运算,得到浏览器指纹具体的信息熵以及浏览器的uuid。计算方式。
用于生产指纹的各,也有权重之分,信息熵大的特征值将拥有较大的权重。
| Variable | Entropy (bits) |
|---|---|
| user agent | 10.0 |
| plugins | 15.4 |
| fonts | 13.9 |
| video | 4.83 |
| supercookies | 6.09 |
| timezone | 3.04 |
| cookies enabled | 0.353 |
普通指纹是不够区分独特的个人的,这就需要高级指纹,将范围进一步缩小,甚至生成一个独一无二的跨浏览器身份。
这篇 论文 研究了各个指标的信息熵和稳定性。可以看出,时区、屏幕分辨率和色深、canvas、webGL的信息熵在跨浏览器指纹上是比较大的。

canvas 指纹
Canvas是HTML5中的动态绘图标签,也可以用它生成图片或者处理图片。即便使用Canvas绘制相同的元素,但是由于系统的差别,字体渲染引擎不同,对抗锯齿、次像素渲染等算法也不同,canvas将同样的文字转成图片,得到的结果也是不同的。
实现代码大致为:在画布上渲染一些文字,再用toDataURL转换出来,即便开启了隐私模式一样可以拿到相同的值。
function getCanvasFingerprint () {var canvas = document.createElement('canvas');var context = canvas.getContext("2d");context.font = "18pt Arial";context.textBaseline = "top";context.fillText("Hello, user.", 2, 2);return canvas.toDataURL("image/jpeg");}getCanvasFingerprint()
流程很简单,渲染文字,toDataURL,得到值。如果我们想混淆canvas指纹,似乎只需要在toDataURL得到的结果上做手脚就可以。
**toDataURL()** 是将整个canvas的内容导出,如果我们需要将canvas中的部分内容修改,这个时候可以通过 getImageData() 复制画布上指定矩形的像素数据,然后通过 putImageData() 将图像数据放回,然后再使用toDataURL()导出的图片就有了差异。**CanvasRenderingContext2D**``**.getImageData()** 返回一个[ImageData](https://developer.mozilla.org/zh-CN/docs/Web/API/ImageData)对象,用来描述canvas区域隐含的像素数据。这个区域通过矩形表示,起始点为(sx, sy)、宽为sw、高为sh。
ImageData 对象
ImageData 接口描述了
- width 图片宽度,单位像素
- height 图片高度,单位像素
- data
[Uint8ClampedArray](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray) 类型的一位数组,包含着RGBA的整型数据,范围在0~255。它可以视作初始像素数据,每个像素用4个1 bytes值(按照red,green,blue,alpha的顺序), 每个颜色值用0~255中数字代表。每个部分被分配到一个数组内的连续索引,左上角第一个像素的红色部分,位于数组索引的第0位。像素从左到右从上到下被处理,遍历整个数组。``[Unit8ClampedArray](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Uint8ClampedArray) 包含 高度宽度4 bytes数据,索引值从0 ~ (wh4)-1 。
例如,读取图片中位于第50行,200列的像素的蓝色部分,则
const blueComponent = imageData[50*(imageData.width * 4) + 200*4 + 2]
混淆canvas 指纹的方法
function antiCanvasFP () {const script = document.createElement('script')script.dataset.active = true // overwrites enabled or notscript.dataset.once = true // only manipulate onceif (window.top === window) {window.script = script} else {// try to get preferences from the top frame when possibletry {Object.assign(script.dataset, window.top.script.dataset)}catch (e) {}}script.addEventListener('called', e => {e.preventDefault()e.stopPropagation()// chrome.runtime.sendMessage({// method: 'possible-fingerprint'// })console.log('This website may get your canvas fingerprint')}, false)// 对getImageData生成的imageData(像素源数据)中的每一个像素的r、g、b部分的值进行进行随机改变从而生成唯一的图像。script.textContent = `{const script = document.currentScript;script.dataset.injected = true;const toBlob = HTMLCanvasElement.prototype.toBlob;const toDataURL = HTMLCanvasElement.prototype.toDataURL;HTMLCanvasElement.prototype.manipulate = function() {const {width, height} = this;const context = this.getContext('2d'); // 拿到在进行toDataURL或者toBlob前的canvas所生成的CanvasRenderingContext2Dconst shift = {'r': Math.floor(Math.random() * 10) - 5,'g': Math.floor(Math.random() * 10) - 5,'b': Math.floor(Math.random() * 10) - 5};const matt = context.getImageData(0, 0, width, height);for (let i = 0; i < height; i += Math.max(1, parseInt(height / 10))) {for (let j = 0; j < width; j += Math.max(1, parseInt(width / 10))) {const n = ((i * (width * 4)) + (j * 4));matt.data[n + 0] = matt.data[n + 0] + shift.r; // 加上随机扰动matt.data[n + 1] = matt.data[n + 1] + shift.g;matt.data[n + 2] = matt.data[n + 2] + shift.b;}}context.putImageData(matt, 0, 0); // 重新放回去if (script.dataset.once === 'true') {this.manipulate = () => {script.dispatchEvent(new Event('called'));};}script.dispatchEvent(new Event('called'));};Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {value: function() {if (script.dataset.active === 'true') {try {this.manipulate(); // 在每次toBlob前,先混淆下ImageData}catch(e) {console.warn('manipulation failed', e);}}return toBlob.apply(this, arguments);}});Object.defineProperty(HTMLCanvasElement.prototype, 'toDataURL', {value: function() {if (script.dataset.active === 'true') {try {this.manipulate(); // 在每次toDataURL前,先混淆下ImageData}catch(e) {console.warn('manipulation failed', e);}}return toDataURL.apply(this, arguments);}});}`document.documentElement.appendChild(script)// 如果script没有加载,确保加载if (script.dataset.injected !== 'true') {window.frameElement.classList.add('workaround')const polyscript = document.createElement('script')Object.assign(polyscript.dataset, script.dataset)polyscript.textContent = `for (const iframe of [...document.querySelectorAll('iframe.workaround')]) {try {Object.assign(iframe.contentWindow.HTMLCanvasElement.prototype, {toBlob: HTMLCanvasElement.prototype.toBlob,toDataURL: HTMLCanvasElement.prototype.toDataURL,manipulate: HTMLCanvasElement.prototype.manipulate});}catch (e) {}iframe.classList.remove('workaround');}`window.top.document.documentElement.appendChild(polyscript)polyscript.remove()}script.remove()}
WebGL 指纹
WebGL(Web图形库)是一个JavaScript API,可在任何兼容的Web浏览器中渲染高性能的交互式3D和2D图形,而无需使用插件。WebGL通过引入一个与OpenGL ES 2.0非常一致的API来做到这一点,该API可以在HTML5 元素中使用。 这种一致性使API可以利用用户设备提供的硬件图形加速。网站可以利用WebGL来识别设备指纹,一般可以用两种方式来做到指纹生产,
- WebGL报告——完整的WebGL浏览器报告表是可获取、可被检测的。在一些情况下,它会被转换成为哈希值以便更快地进行分析。
WebGL图像 ——渲染和转换为哈希值的隐藏3D图像。由于最终结果取决于进行计算的硬件设备,因此此方法会为设备及其驱动程序的不同组合生成唯一值。这种方式为不同的设备组合和驱动程序生成了唯一值。
可以通过Browserleaks test检测网站来查看网站可以通过该API获取哪些信息。
fingerprint2js的webgl指纹生产方式 [源码](https://github.com/AJLoveChina/fingerprintDemo/blob/master/src/core/webglFP.js#L35)
// 部分源码gl = getWebglCanvas()if (!gl) { return null }// WebGL fingerprinting is a combination of techniques, found in MaxMind antifraud script & Augur fingerprinting.// First it draws a gradient object with shaders and convers the image to the Base64 string.// Then it enumerates all WebGL extensions & capabilities and appends them to the Base64 string, resulting in a huge WebGL string, potentially very unique on each device// Since iOS supports webgl starting from version 8.1 and 8.1 runs on several graphics chips, the results may be different across ios devices, but we need to verify it.var result = []var vShaderTemplate = 'attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}'var fShaderTemplate = 'precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}'var vertexPosBuffer = gl.createBuffer()gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer)var vertices = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.732134444, 0])gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)vertexPosBuffer.itemSize = 3vertexPosBuffer.numItems = 3var program = gl.createProgram()var vshader = gl.createShader(gl.VERTEX_SHADER)gl.shaderSource(vshader, vShaderTemplate)gl.compileShader(vshader)var fshader = gl.createShader(gl.FRAGMENT_SHADER)gl.shaderSource(fshader, fShaderTemplate)gl.compileShader(fshader)gl.attachShader(program, vshader)gl.attachShader(program, fshader)gl.linkProgram(program)gl.useProgram(program)program.vertexPosAttrib = gl.getAttribLocation(program, 'attrVertex')program.offsetUniform = gl.getUniformLocation(program, 'uniformOffset')gl.enableVertexAttribArray(program.vertexPosArray)gl.vertexAttribPointer(program.vertexPosAttrib, vertexPosBuffer.itemSize, gl.FLOAT, !1, 0, 0)gl.uniform2f(program.offsetUniform, 1, 1)gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertexPosBuffer.numItems)try {result.push(gl.canvas.toDataURL())} catch (e) {/* .toDataURL may be absent or broken (blocked by extension) */}
参考资料:
浏览器指纹追踪技术简述
Cross-Browser Fingerprinting via OS and Hardware Level Features
imageData MDN
putImageData MDN
WebGLRenderingContext MDN
WebGL常量
