當前位置: 華文星空 > 知識

沒登入網頁也能個人化推薦?一文詳解瀏覽器指紋

2021-07-01知識

日常生活中,生物辨識技術已經是多數智能電話的標配,大多數手機具備人臉辨識、指紋辨識等功能,目前的指紋辨識技術已經非常成熟。但我們今天要聊的並不是生物辨識技術中的指紋辨識,而是瀏覽器指紋。很多人對這項技術是又愛又恨,這究竟是為什麽呢?那我們今天就來深入了解下瀏覽器指紋。

什麽是瀏覽器指紋

瀏覽器指紋可以透過瀏覽器對網站可見的配置、設定資訊,來跟蹤 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。

    這些資訊就類似人類的體重、身高、膚色一樣,有很大的重復概率,只能作為輔助辨識,所以我們需要更精確的指紋來判斷唯一性。

    高級指紋

    普通指紋是不夠區分獨特的個人,這時就需要高級指紋,將範圍進一步縮小,甚至生成一個獨一無二的跨瀏覽器身份。

    用於生產指紋的各個資訊,有權重大小之分,資訊熵大的將擁有較大的權重。

    Variable Entropy (bits)
    user agent 10
    plugins 15.4
    fonts 13.9
    video 4.83
    supercookies 6.09
    timezone 3.04
    cookies enabled 0.353

    在論文【Cross-Browser Fingerprinting via OS and Hardware Level Features [http:// yinzhicao.org/TrackingF ree/crossbrowsertracking_NDSS17.pdf ]】中更是詳細研究了各個指標的資訊熵和穩定性。

    從該論文中可以看出, 時區、螢幕分辨率和色深、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 來辨識器材指紋,一般可以用兩種方式來做到指紋生產:
    1.WebGL 報告——完整的 WebGL 瀏覽器報告表是可獲取、可被檢測的。在一些情況下,它會被轉換成為哈希值以便更快地進行分析。
    2.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 的各個函數來阻止網站獲取各種資訊,或返回一個假的數據,以此來保護我們的私密資訊。

    推薦閱讀: