日常生活中,生物识别技术已经是多数智能手机的标配,大多数手机具备人脸识别、指纹识别等功能,目前的指纹识别技术已经非常成熟。但我们今天要聊的并不是生物识别技术中的指纹识别,而是浏览器指纹。很多人对这项技术是又爱又恨,这究竟是为什么呢?那我们今天就来深入了解下浏览器指纹。


什么是浏览器指纹

浏览器指纹可以通过浏览器对网站可见的配置、设置信息,来跟踪 Web 浏览器,它就像我们人手上的指纹一样,具有个体辨识度,只不过现阶段浏览器指纹辨别的是浏览器。

浏览器指纹辨识的信息可以是 UA、时区、地理位置或者是使用的语言等等,浏览器所开发的信息决定了浏览器指纹的准确性。

对于网站而言,拿到浏览器指纹并没有实际价值,真正有价值的是浏览器指纹对应的用户信息。作为网站站长,收集用户浏览器指纹并记录用户的操作,是一个有价值的行为,特别是针对没有用户身份的场景。

例如一个视频网站,未注册该网站的用户 A 喜欢浏览二次元的视频,通过浏览器指纹记录这个,那么下次可以直接向该浏览器推送二次元的视频。因为现在的上网设备大都是私人的,这样的推送方式很容易获得大部分用户的好感,从而使之成为网站的用户。

浏览器指纹的发展

浏览器指纹技术的发展跟大多数技术一样,并非一蹴而就的,现有的几代浏览器指纹技术是这样的:

  • 第一代是状态化的,主要集中在用户的 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 中拿到的指纹,还可以通过其他方式来获得浏览器的特征信息,例如:

    • 每个浏览器的UA

    • 浏览器发送的 HTTP ACCEPT 标头

    • 浏览器中安装的浏览器扩展/插件,例如 Quicktime,Flash,Java 或 Acrobat,以及这些插件的版本

    • 计算机上安装的字体。

    • 浏览器是否执行 JavaScript 脚本

    • 浏览器是否能种下各种 cookie 和 “super cookies”

    • 是否浏览器设置为“Do Not Track”

    • 系统平台(例如 Win32、Linux x86)

    • 系统语言(例如 cn、en-US)

    • 浏览器是否支持触摸屏

    拿到这些值后可以进行一些运算,得到浏览器指纹具体的信息熵以及浏览器的 uuid。

    这些信息就类似人类的体重、身高、肤色一样,有很大的重复概率,只能作为辅助识别,所以我们需要更精确的指纹来判断唯一性。

    高级指纹

    普通指纹是不够区分独特的个人,这时就需要高级指纹,将范围进一步缩小,甚至生成一个独一无二的跨浏览器身份。

    用于生产指纹的各个信息,有权重大小之分,信息熵大的将拥有较大的权重。

    1.png△ 各信息特征权重值

    在论文《Cross-Browser Fingerprinting via OS and Hardware Level Features [http://yinzhicao.org/TrackingFree/crossbrowsertracking_NDSS17.pdf]》中更是详细研究了各个指标的信息熵和稳定性。

    2.png

    从该论文中可以看出,时区、屏幕分辨率和色深、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 的内容导出,得到值。

      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 获取哪些信息。

      产生WebGL指纹原理是首先需要用着色器(shaders)绘制一个梯度对象,并将这个图片转换为Base64字符串。然后枚举WebGL所有的拓展和功能,并将他们添加到Base64字符串上,从而产生一个巨大的字符串,这个字符串在每台设备上可能是非常独特的。

      例如fingerprint2js库的 WebGL 指纹生产方式:

        // 部分代码 
        gl = getWebglCanvas()    
        if (!gl) { return null }    
        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])
        // 创建并初始化了Buffer对象的数据存储区。
        gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW) 
        vertexPosBuffer.itemSize = 3
        vertexPosBuffer.numItems = 3
        // 创建和初始化一个WebGLProgram对象。
        var program = gl.createProgram()
        // 创建着色器对象
        var vshader = gl.createShader(gl.VERTEX_SHADER)
        // 下两行配置着色器 
        gl.shaderSource(vshader, vShaderTemplate)  // 设置着色器代码  
        gl.compileShader(vshader) // 编译一个着色器,以便被WebGLProgram对象所使用
            
        var fshader = gl.createShader(gl.FRAGMENT_SHADER)   
        gl.shaderSource(fshader, fShaderTemplate)    
        gl.compileShader(fshader)    
        // 添加预先定义好的顶点着色器和片段着色器  
        gl.attachShader(program, vshader)
        gl.attachShader(program, fshader) 
        // 链接WebGLProgram对象   
        gl.linkProgram(program)
        // 定义好的WebGLProgram对象添加到当前的渲染状态  
        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) */
        }


        如何防止被生成“用户指纹”

        文章开头也提到了,很多人对浏览器这项技术是又爱又恨。因为一大堆网站使用各种技术来“生成”用户指纹,以便给网站用户带来更精准的推荐和符合用户的浏览习惯。而用户在享受技术带来便利的同时,也不免会有“隐私泄露”的焦躁和不安感。那么我们如何防止被生成“用户指纹”呢?

        混淆 Canvas 指纹

        我们已经了解了是如何获取 canvas 指纹的,那么应该如何防范被恶意获取呢?想混淆 Canvas 指纹,只需要在 toDataURL 得到的结果上做手脚就可以。

        toDataURL()  将整个canvas的内容导出,我们需要将 Canvas 中的部分内容修改,这个时候可以通过 getImageData() 复制画布上指定矩形的像素数据,然后通过 putImageData() 将图像数据放回,然后再使用 toDataURL() 导出的图片就有了差异。

        CanvasRenderingContext2D.getImageData() 返回一个ImageData对象,用来描述 Canvas 区域隐含的像素数据。这个区域通过矩形表示,起始点为(sx, sy)、宽为sw、高为sh。

        ImageData 接口描述了<Canvas>元素的一个隐含像素数据的区域,可以由 ImageData()  方法构造,或者由canvas 在一起的 CanvasRenderingContext2D 对象的创建方法:createImageData() 和 getImageData()

        ImageData 对象存储着canvas对象真实的像素数据,它包含几个只读属性:

        • width 图片宽度,单位像素

        • height 图片高度,单位像素

        • data

        Uint8ClampedArray 类型的一位数组,包含着 RGBA 的整型数据,范围在 0~255。它可以视作初始像素数据,每个像素用 4 个 1 bytes 值(按照 red、green、blue、alpha 的顺序),每个颜色值用0~255 中的数字代表。每个部分被分配到一个数组内的连续索引,左上角第一个像素的红色部分,位于数组索引的第 0 位。像素从左到右从上到下被处理,遍历整个数组。

        Unit8ClampedArray 包含 高度*宽度*4 bytes数据,索引值从 0 ~ (w*h*4)-1 。

        例如,读取图片中位于第 50 行,200 列的像素的蓝色部分,则:

          const blueComponent = imageData[50*(imageData.width * 4) + 200*4 + 2]

          下面是实现混淆 Canvas 指纹的方法:

            
            
            const toBlob = HTMLCanvasElement.prototype.toBlob;
            const toDataURL = HTMLCanvasElement.prototype.toDataURL;
            HTMLCanvasElement.prototype.manipulate = function() {
              const {width, height} = this;
              // 拿到在进行toDataURL或者toBlob前的canvas所生成的CanvasRenderingContext2D
              const context = this.getContext('2d'); 
              const 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);
              // 对getImageData生成的imageData(像素源数据)中的每一个像素的r、g、b部分的值进行进行随机改变从而生成唯一的图像。
              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); // 重新放回去
            // 修改prototype.toBlob
            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);
              }
            });
            // 修改prototype. toDataURL
            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);
              }
            });

            混淆其他指纹

            与前面混淆canvas指纹混淆的思路是一致的,都是更改被获取对象的原型的方法。

            比如混淆时区,就是更改 Date.prototype.getTimezoneOffset 的返回值。

            混淆分辨率则是更改documentElement.clientHeight  documentElement.clientWidth

            混淆 WebGL 则要更改 WebGLbufferData getParameter方法等等。

            当然,我们也有一些简单的方法来防止被生成用户指纹。例如我们可以通过浏览器的扩展插件(Canvas Blocker、WebGL Fingerprint Defender、Fingerprint Spoofing等),在网页加载前执行一段 JS 代码,更改、重写 JS 的各个函数来阻止网站获取各种信息,或返回一个假的数据,以此来保护我们的隐私信息。

            参考资料:

            浏览器指纹追踪技术简述 

            [https://juejin.cn/post/6844903970180169742]

            Cross-Browser Fingerprinting via OS and Hardware Level Features:

            [http://yinzhicao.org/TrackingFree/crossbrowsertracking_NDSS17.pdf]

            imageData MDN:

            [https://developer.mozilla.org/zh-CN/docs/Web/API/ImageData]

            putImageData MDN 

            WebGLRenderingContext MDN:

            [https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext]

            fingerprintjs:

            [https://github.com/fingerprintjs/fingerprintjs]

            WebGL常量:

            [https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Constants]