利用Web客户端对用户行为进行收集和追踪是重要手段之一。本文主要介绍典型的指纹追踪技术和WebRTC技术,介绍一些简单的防跟踪的方法,并给出相关参考供感兴趣的朋友深入研究。
一、 典型追踪技术
1. 浏览器指纹追踪
1) 基本指纹
基本指纹是任何浏览器都具有的特征标识,比如硬件类型(Apple)、操作系统(Mac OS)、用户代理(User agent)、系统字体、语言、屏幕分辨率、浏览器插件 (Flash, Silverlight, Java, etc)、浏览器扩展、浏览器设置 (Do-Not-Track, etc)、时区差(Browser GMT Offset)等众多信息,这些指纹信息“类似”人类的身高、年龄等,有很大的冲突概率,只能作为辅助识别。可以在该网址进行查看本地浏览器的基本特征,https://www.whatismybrowser.com/

2) 高级指纹
①、Canvas指纹
Canvas(画布)是HTML5中一种动态绘图的标签,可以使用其生成甚至处理高级图片。利用Canvas进行追踪的一般过程大致如下:

基于Canvas标签绘制特定内容的图片,使用canvas.toDataURL()方法获得图片内容的base64编码(对于PNG格式的图片,以块(chunk)划分,最后一块是32位CRC校验)作为唯一性标识,如下图。


Canvas指纹的原理大致如下:
相同的HTML5 Canvas元素绘制操作,在不同操作系统、不同浏览器上,产生的图片内容不完全相同。在图片格式上,不同浏览器使用了不同的图形处理引擎、不同的图片导出选项、不同的默认压缩级别等。在像素级别来看,操作系统各自使用了不同的设置和算法来进行抗锯齿和子像素渲染操作。即使相同的绘图操作,产生的图片数据的CRC检验也不相同。
在线测试地址:https://www.browserleaks.com/canvas,可查看浏览器的Canvas唯一性字符串。

Canvas的兼容情况:几乎已被所有主流浏览器支持,可以通过大部分的PC、平板、智能手机访问!

②、AudioContext指纹
HTML5提供给JavaScript编程用的Audio API则让开发者有能力在代码中直接操作原始的音频流数据,对其进行任意生成、加工、再造,诸如提高音色,改变音调,音频分割等多种操作,甚至可称为网页版的Adobe Audition。
AudioContext指纹原理大致如下:
方法一:生成音频信息流(三角波),对其进行FFT变换,计算SHA值作为指纹,音频输出到音频设备之前进行清除,用户毫无察觉。

方法二:生成音频信息流(正弦波),进行动态压缩处理,计算MD5值。

AudioContext指纹基本原理:
主机或浏览器硬件或软件的细微差别,导致音频信号的处理上的差异,相同器上的同款浏览器产生相同的音频输出,不同机器或不同浏览器产生的音频输出会存在差异。
从上可以看出AudioContext和Canvas指纹原理很类似,都是利用硬件或软件的差异,前者生成音频,后者生成图片,然后计算得到不同哈希值来作为标识。音频指纹测试地址:https://audiofingerprint.openwpm.com/
3) 硬件指纹
硬件指纹主要通过检测硬件模块获取信息,作为对基于软件的指纹的补充,主要的硬件模块有:GPU’s clock frequency、Camera、Speakers/Microphone、Motion sensors、GPS、Battery等。更多细节请参考:https://arxiv.org/pdf/1503.01408v3.pdf
4) 综合指纹
Web世界的指纹碰撞不可避免,将上述所有的基本指纹和多种高级指纹综合利用,进行分析、计算哈希值作为综合指纹,可以大大降低碰撞率,极大提高客户端唯一性识别的准确性。测试地址:https://panopticlick.eff.org/

2. 跨浏览器指纹
当同一用户使用同一台电脑的不同浏览器时,服务方收集到的浏览器指纹信息不同,无法将该用户进行唯一性识别,进而无法有效分析改用户的的行为。
近期有学者研究了一种跨浏览器的浏览器指纹,其依赖于浏览器与操作系统和硬件底层进行交互进而分析计算出指纹,这种指纹对于同一台电脑的不同浏览器也是相同的。更多技术细节请参考:
http://yinzhicao.org/TrackingFree/crossbrowsertracking_NDSS17.pdf
3. WebRTC
WebRTC(网页实时通信,Web Real Time Communication),是一个开源项目,旨在使得浏览器能为实时通信(RTC)提供简单的JavaScript接口,说的简单明了一点就是让浏览器提供JS的即时通信接口,让浏览器实时获取和交换视频、音频和数据。WebRTC实现了三个API,分别是:
MediaStream:通过MediaStream的API能够通过设备的摄像头及麦克风获得视频、音频的同步流。
RTCPeerConnection:RTCPeerConnection是WebRTC用于构建点对点之间稳定、高效的流传输的组件。
RTCDataChannel:RTCDataChannel使得浏览器之间(点对点)建立一个高吞吐量、低延时的信道,用于传输任意数据。
基于WebRTC的实时通讯功能,可以获取客户端的IP地址,包括本地内网地址和公网地址。其原理是利用到RTCPeerConnection 的API,大致函数如下:

综合上诉,给出一个demo,大家可以研究一下
import x64Hash from './x64Hash'; //需要引入x64Hash加密算法function setDomainCookie(key, value, exp) {if (exp && exp instanceof Date) {document.cookie = key + "=" + escape(value) + ";path=/;domain=bilibili.com;expires=" + exp;} else {document.cookie = key + "=" + escape(value) + ";path=/;domain=bilibili.com";}}function getCanvasFp() {var result = []var canvas = document.createElement('canvas')canvas.width = 2000canvas.height = 200canvas.style.display = 'inline'var ctx = canvas.getContext('2d')ctx.rect(0, 0, 10, 10)ctx.rect(2, 2, 6, 6)result.push('canvas winding:' + ((ctx.isPointInPath(5, 5, 'evenodd') === false) ? 'yes' : 'no'))ctx.textBaseline = 'alphabetic'ctx.fillStyle = '#f60'ctx.fillRect(125, 1, 62, 20)ctx.fillStyle = '#069'ctx.font = '11pt no-real-font-123'ctx.fillText('Cwm fjordbank glyphs vext quiz, \ud83d\ude03', 2, 15)ctx.fillStyle = 'rgba(102, 204, 0, 0.2)'ctx.font = '18pt Arial'ctx.fillText('Cwm fjordbank glyphs vext quiz, \ud83d\ude03', 4, 45)ctx.globalCompositeOperation = 'multiply'ctx.fillStyle = 'rgb(255,0,255)'ctx.beginPath()ctx.arc(50, 50, 50, 0, Math.PI * 2, true)ctx.closePath()ctx.fill()ctx.fillStyle = 'rgb(0,255,255)'ctx.beginPath()ctx.arc(100, 50, 50, 0, Math.PI * 2, true)ctx.closePath()ctx.fill()ctx.fillStyle = 'rgb(255,255,0)'ctx.beginPath()ctx.arc(75, 100, 50, 0, Math.PI * 2, true)ctx.closePath()ctx.fill()ctx.fillStyle = 'rgb(255,0,255)'ctx.arc(75, 75, 75, 0, Math.PI * 2, true)ctx.arc(75, 75, 25, 0, Math.PI * 2, true)ctx.fill('evenodd')if (canvas.toDataURL) {result.push('canvas fp:' + canvas.toDataURL())}return result.join('~');}function getWebglCanvas() {var canvas = document.createElement('canvas')var gl = nulltry {gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl')} catch (e) { /* squelch */ }if (!gl) {gl = null}return gl}function getWebglFp() {var glvar fa2s = function (fa) {gl.clearColor(0.0, 0.0, 0.0, 1.0)gl.enable(gl.DEPTH_TEST)gl.depthFunc(gl.LEQUAL)gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)return '[' + fa[0] + ', ' + fa[1] + ']'}var maxAnisotropy = function (gl) {var ext = gl.getExtension('EXT_texture_filter_anisotropic') || gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic') || gl.getExtension('MOZ_EXT_texture_filter_anisotropic')if (ext) {var anisotropy = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT)if (anisotropy === 0) {anisotropy = 2}return anisotropy} else {return null}}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) */}result.push('extensions:' + (gl.getSupportedExtensions() || []).join(';'))result.push('webgl aliased line width range:' + fa2s(gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE)))result.push('webgl aliased point size range:' + fa2s(gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE)))result.push('webgl alpha bits:' + gl.getParameter(gl.ALPHA_BITS))result.push('webgl antialiasing:' + (gl.getContextAttributes().antialias ? 'yes' : 'no'))result.push('webgl blue bits:' + gl.getParameter(gl.BLUE_BITS))result.push('webgl depth bits:' + gl.getParameter(gl.DEPTH_BITS))result.push('webgl green bits:' + gl.getParameter(gl.GREEN_BITS))result.push('webgl max anisotropy:' + maxAnisotropy(gl))result.push('webgl max combined texture image units:' + gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS))result.push('webgl max cube map texture size:' + gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE))result.push('webgl max fragment uniform vectors:' + gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS))result.push('webgl max render buffer size:' + gl.getParameter(gl.MAX_RENDERBUFFER_SIZE))result.push('webgl max texture image units:' + gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS))result.push('webgl max texture size:' + gl.getParameter(gl.MAX_TEXTURE_SIZE))result.push('webgl max varying vectors:' + gl.getParameter(gl.MAX_VARYING_VECTORS))result.push('webgl max vertex attribs:' + gl.getParameter(gl.MAX_VERTEX_ATTRIBS))result.push('webgl max vertex texture image units:' + gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS))result.push('webgl max vertex uniform vectors:' + gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS))result.push('webgl max viewport dims:' + fa2s(gl.getParameter(gl.MAX_VIEWPORT_DIMS)))result.push('webgl red bits:' + gl.getParameter(gl.RED_BITS))result.push('webgl renderer:' + gl.getParameter(gl.RENDERER))result.push('webgl shading language version:' + gl.getParameter(gl.SHADING_LANGUAGE_VERSION))result.push('webgl stencil bits:' + gl.getParameter(gl.STENCIL_BITS))result.push('webgl vendor:' + gl.getParameter(gl.VENDOR))result.push('webgl version:' + gl.getParameter(gl.VERSION))try {// Add the unmasked vendor and unmasked renderer if the debug_renderer_info extension is availablevar extensionDebugRendererInfo = gl.getExtension('WEBGL_debug_renderer_info')if (extensionDebugRendererInfo) {result.push('webgl unmasked vendor:' + gl.getParameter(extensionDebugRendererInfo.UNMASKED_VENDOR_WEBGL))result.push('webgl unmasked renderer:' + gl.getParameter(extensionDebugRendererInfo.UNMASKED_RENDERER_WEBGL))}} catch (e) { /* squelch */ }if (!gl.getShaderPrecisionFormat) {return result.join('~')}result.push('webgl vertex shader high float precision:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT).precision)result.push('webgl vertex shader high float precision rangeMin:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT).rangeMin)result.push('webgl vertex shader high float precision rangeMax:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT).rangeMax)result.push('webgl vertex shader medium float precision:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT).precision)result.push('webgl vertex shader medium float precision rangeMin:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT).rangeMin)result.push('webgl vertex shader medium float precision rangeMax:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT).rangeMax)result.push('webgl vertex shader low float precision:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_FLOAT).precision)result.push('webgl vertex shader low float precision rangeMin:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_FLOAT).rangeMin)result.push('webgl vertex shader low float precision rangeMax:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_FLOAT).rangeMax)result.push('webgl fragment shader high float precision:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).precision)result.push('webgl fragment shader high float precision rangeMin:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).rangeMin)result.push('webgl fragment shader high float precision rangeMax:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT).rangeMax)result.push('webgl fragment shader medium float precision:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT).precision)result.push('webgl fragment shader medium float precision rangeMin:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT).rangeMin)result.push('webgl fragment shader medium float precision rangeMax:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT).rangeMax)result.push('webgl fragment shader low float precision:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_FLOAT).precision)result.push('webgl fragment shader low float precision rangeMin:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_FLOAT).rangeMin)result.push('webgl fragment shader low float precision rangeMax:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_FLOAT).rangeMax)result.push('webgl vertex shader high int precision:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_INT).precision)result.push('webgl vertex shader high int precision rangeMin:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_INT).rangeMin)result.push('webgl vertex shader high int precision rangeMax:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_INT).rangeMax)result.push('webgl vertex shader medium int precision:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_INT).precision)result.push('webgl vertex shader medium int precision rangeMin:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_INT).rangeMin)result.push('webgl vertex shader medium int precision rangeMax:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_INT).rangeMax)result.push('webgl vertex shader low int precision:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_INT).precision)result.push('webgl vertex shader low int precision rangeMin:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_INT).rangeMin)result.push('webgl vertex shader low int precision rangeMax:' + gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_INT).rangeMax)result.push('webgl fragment shader high int precision:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT).precision)result.push('webgl fragment shader high int precision rangeMin:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT).rangeMin)result.push('webgl fragment shader high int precision rangeMax:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT).rangeMax)result.push('webgl fragment shader medium int precision:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_INT).precision)result.push('webgl fragment shader medium int precision rangeMin:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_INT).rangeMin)result.push('webgl fragment shader medium int precision rangeMax:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_INT).rangeMax)result.push('webgl fragment shader low int precision:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_INT).precision)result.push('webgl fragment shader low int precision rangeMin:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_INT).rangeMin)result.push('webgl fragment shader low int precision rangeMax:' + gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_INT).rangeMax)return result.join('~')}let canvasFp = x64Hash.x64hash128(getCanvasFp(), 31),webglFp = x64Hash.x64hash128(getWebglFp(), 31),ua = navigator.userAgent,screenInfo = screen.width + "*" + screen.height + "*" + screen.colorDepth,feSign = x64Hash.x64hash128(canvasFp + "~" + webglFp + "~" + screenInfo + "~" + ua, 31);let cookies = [{key: 'canvasFp',value: canvasFp}, {key: 'webglFp',value: webglFp}, {key: 'screenInfo',value: screenInfo}, {key: 'feSign',value: feSign}]cookies.forEach(item => {setDomainCookie(item.key, item.value);})export default {canvasFp,webglFp,screenInfo,feSign}
2. 插件
推荐几个较好的浏览器插件来阻止第三方广告追踪和广告:
Ghostery,Privacy Badger,uMatrix(仅Chrome和FireFox),NoScript(仅FireFox),Chameleon(仅Chrome)
本文地址:https://paper.seebug.org/229/
作者:wellee




