什么是浏览器指纹
“浏览器指纹”是一种通过浏览器对网站可见的配置和设置信息来跟踪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 中拿到的指纹,还可以通过其他方式来获得浏览器的特征信息,例如:
每个浏览器的UA
浏览器发送的 HTTP ACCEPT 标头
浏览器中安装的浏览器扩展/插件,例如 Quicktime,Flash,Java 或 Acrobat,以及这些插件的版本
计算机上安装的字体。
浏览器是否执行 JavaScript 脚本
浏览器是否能种下各种 cookie 和 “super cookies”
是否浏览器设置为“Do Not Track”
系统平台(例如 Win32、Linux x86)
系统语言(例如 cn、en-US)
浏览器是否支持触摸屏
拿到这些值后可以进行一些运算,得到浏览器指纹具体的信息熵以及浏览器的 uuid。
这些信息就类似人类的体重、身高、肤色一样,有很大的重复概率,只能作为辅助识别,所以我们需要更精确的指纹来判断唯一性。
高级指纹
普通指纹是不够区分独特的个人,这时就需要高级指纹,将范围进一步缩小,甚至生成一个独一无二的跨浏览器身份。
用于生产指纹的各个信息,有权重大小之分,信息熵大的将拥有较大的权重。
如何完整修改浏览器指纹?
接下来教大家如何修改浏览器指纹,例如修改navigator全部参数:
(function() { 'use strict';
function fakeActiveVRDisplays() { return "Not Spoofed"; } function fakeAppCodeName() { return "Mozilla"; } function fakeAppName() { return "Netscape"; }
function fakeAppVersion() { return "5.0 (Windows)"; } function fakeBattery() { return "Not Spoofed"; } function fakeConnection() { return "Not Spoofed"; } function fakeGeoLocation() { return "Not Spoofed"; } function fakeHardwareConcurrency() { return 1; } function fakeJavaEnabled() { return false; } function fakeLanguage() { // NOTE: TOR Browser uses American English return "en-US"; } function fakeLanguages() { // NOTE: TOR Browser uses American English return "en-US,en"; } function fakeMimeTypes() { return "Not Spoofed"; } function fakeOnLine() { return true; } function fakeOscpu() { return "Windows NT 6.1"; } function fakePermissions() { return "Not Spoofed"; } function fakePlatform() { return "Win32"; } function fakePlugins() { return window.navigator.plugins; } function fakeProduct() { return "Gecko"; } function fakeServiceWorker() { return "Not Spoofed"; } function fakeStorage() { return "Not Spoofed"; } function fakeUserAgent() { // NOTE: Current TOR User Agent as of 19 July 2017 // NOTE: This will need constant updating. // NOTE: As TOR changes firefox versions each update, // NOTE: Shape Shifter will need to keep up. return "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0"; } function fakeBuildID() { return "20100101"; }
const fakeActiveVRDisplaysValue = fakeActiveVRDisplays(); const fakeAppCodeNameValue = fakeAppCodeName(); const fakeAppNameValue = fakeAppName(); const fakeAppVersionValue = fakeAppVersion(); const fakeBatteryValue = fakeBattery(); const fakeConnectionValue = fakeConnection(); const fakeGeoLocationValue = fakeGeoLocation(); const fakeHardwareConcurrencyValue = fakeHardwareConcurrency(); const fakeJavaEnabledValue = fakeJavaEnabled(); const fakeLanguageValue = fakeLanguage(); const fakeLanguagesValue = fakeLanguages(); const fakeMimeTypesValue = fakeMimeTypes(); const fakeOnLineValue = fakeOnLine(); const fakeOscpuValue = fakeOscpu(); const fakePermissionsValue = fakePermissions(); const fakePlatformValue = fakePlatform(); const fakePluginsValue = fakePlugins(); const fakeProductValue = fakeProduct(); const fakeServiceWorkerValue = fakeServiceWorker(); const fakeStorageValue = fakeStorage(); const fakeUserAgentValue = fakeUserAgent(); const fakeBuildIDValue = fakeBuildID();
Object.defineProperties(window.navigator, { /* activeVRDisplays: { configurable: true, enumerable: true, get: function getActiveVRDisplays() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.activeVRDisplays"); return fakeActiveVRDisplaysValue; } }, */
appCodeName: { configurable: true, enumerable: true, get: function getAppCodeName() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.appCodeName");
return fakeAppCodeNameValue; } }, appName: { configurable: true, enumerable: true, get: function getAppName() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.appName");
return fakeAppNameValue; } }, appVersion: { configurable: true, enumerable: true, get: function getAppVersion() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.appVersion");
return fakeAppVersionValue; } },
// TODO: This is getBattery() now /* battery: { configurable: true, enumerable: true, get: function getBattery() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.battery"); return fakeBatteryValue; } }, connection: { configurable: true, enumerable: true, get: function getConnection() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.connection"); return fakeConnectionValue; } }, geolocation: { configurable: true, enumerable: true, get: function getGeoLocation() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.geolocation"); return fakeGeoLocationValue; } }, */
hardwareConcurrency: { configurable: true, enumerable: true, get: function getHardwareConcurrency() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.hardwareConcurrency");
return fakeHardwareConcurrencyValue; } },
/* javaEnabled: { configurable: true, enumerable: true, value: function getJavaEnabled() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.javaEnabled"); return fakeJavaEnabledValue; } }, */
language: { configurable: true, enumerable: true, get: function getLanguage() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.language");
return fakeLanguageValue; } }, languages: { configurable: true, enumerable: true, get: function getLanguages() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.languages");
return fakeLanguagesValue; } },
/* mimeTypes: { configurable: true, enumerable: true, get: function getMimeTypes() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.mimeTypes"); return fakeMimeTypesValue; } }, */
onLine: { configurable: true, enumerable: true, get: function getOnLine() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.onLine");
return fakeOnLineValue; } }, oscpu: { configurable: true, enumerable: true, get: function getOscpu() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.oscpu");
return fakeOscpuValue; } },
/* permissions: { configurable: true, enumerable: true, get: function getPermissions() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.permissions"); return fakePermissionsValue; } }, */
platform: { configurable: true, enumerable: true, get: function getPlatform() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.platform");
return fakePlatformValue; } },
/* plugins: { configurable: true, enumerable: true, get: function getPlugins() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.plugins"); return fakePluginsValue; } }, */
product: { configurable: true, enumerable: true, get: function getProduct() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.product");
return fakeProductValue; } },
/* serviceWorker: { configurable: true, enumerable: true, get: function getServiceWorker() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.serviceWorker"); return fakeServiceWorkerValue; } }, storage: { configurable: true, enumerable: true, get: function getStorage() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.storage"); return fakeStorageValue; } }, */
userAgent: { configurable: true, enumerable: true, get: function getUserAgent() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.userAgent");
return fakeUserAgentValue; } }, buildID: { configurable: true, enumerable: true, get: function getBuildID() { console.log("[ALERT] " + window.location.hostname + " accessed property Navigator.buildID");
return fakeBuildIDValue; } } });})();
另外关于浏览器硬件指纹,包括canvas,webgl,fonts,audio等。
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 = 3vertexPosBuffer.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) */}
防御浏览器指纹追踪
这是一个比较暴力的方法,直接禁止网站使用JavaScript可以非常有效地防御浏览器指纹追踪,但是这样会导致页面较大部分地功能不可用。
而且非常不幸的是,即便禁止了js但是还可以通过css来采取浏览器的信息,例如:
@media(device-width: 1080px) { body { background: url("https://example.org/1080.png"); }}
总结
对于浏览器指纹,攻与防在不断的转换,目前浏览器指纹也不能绝对的标识一台主机,如果用户切换显卡或者双系统,虚拟机这些因素,那么目前的浏览器指纹就无法唯一标识了。
未来随着新的HTML5技术不断更新,新的浏览器技术会提供更多的API,以及通过侧信道技术,在浏览器指纹会有新的突破。