1

没登录网页也能个性化推荐?一文详解浏览器指纹

 3 years ago
source link: https://segmentfault.com/a/1190000040263652
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

什么是浏览器指纹

浏览器指纹可以通过浏览器对网站可见的配置、设置信息,来跟踪 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。

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

高级指纹

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

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

各信息特征权重值

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

从该论文中可以看出,时区、屏幕分辨率和色深、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 图片高度,单位像素

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

Unit8ClampedArray 包含 高度宽度4 bytes数据,索引值从 0 ~ (wh4)-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 的各个函数来阻止网站获取各种信息,或返回一个假的数据,以此来保护我们的隐私信息。

go-zero:开箱即用的微服务框架

告别DNS劫持,一文读懂DoH


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK