利用瀏覽器favicon的緩存機制(F-Cache)生成客戶端瀏覽器唯一指紋

利用瀏覽器favicon的緩存機制(F-Cache)生成客戶端瀏覽器唯一指紋

image

首先介紹下:

這個技術出自

UIC論文://www.cs.uic.edu/~polakis/papers/solomos-ndss21.pdf

源碼://github.com/jonasstrehle/supercookie

原理圖解:

下面這個圖是解釋了讓瀏覽器的favicon的請求緩存機制緩存我們想要緩存的路由
image

下面這個圖是解釋了針對客戶端瀏覽器的請求緩存機制反推到唯一指紋
image

本篇文章主要分析源碼層面是如何實現的

初始化參數和favicon的路由

{
  "index": 1,
  "cacheID": "eb60b0a3"
}

favicon的路由設置的是32個,那麼理論上最多可以支持創建40億個唯一指紋

[
  "eb60b0a3:yQqmEg2rcV4hX6FFrr5khA",
  "eb60b0a3:rxK3EqtBI2GdYI58UTQKsg",
  "eb60b0a3:2GaUtZwg3fFUFMg4Eirrcg",
  "eb60b0a3:49C0Fec66xaJ3yQhz0WCcw",
  "eb60b0a3:hmMDHBUG9DB4CW02clFxRw",
  "eb60b0a3:klFWlGuzbS3k49qoMu4YmQ",
  "eb60b0a3:rkb7uew0g6ZfQ1qzIr0n3A",
  "eb60b0a3:szhFZEttZK7HCAD9V8encQ",
  "eb60b0a3:Gi37CDcH90FPlb2P257xew",
  "eb60b0a3:WwLR0GW7s9VfbZc75SglxQ",
  "eb60b0a3:gR5KV6MPacNombv0ssbcGg",
  "eb60b0a3:6HLr0YwczyJkgd5a0imA0A",
  "eb60b0a3:lt3NSeEE9OjY4PnUMKQ3Kg",
  "eb60b0a3:uYx443BpkfANUbVbEiS6iQ",
  "eb60b0a3:tv0SDGvMbZWHK4siR3J9rg",
  "eb60b0a3:iM7UdU8h6I0tN35ykaGmgA",
  "eb60b0a3:6HVrMyGR0130jJq00hqv3A",
  "eb60b0a3:dasC4zubTWlxExsb6dUmig",
  "eb60b0a3:ubdfIPtJAcF3u4z7HLU1WQ",
  "eb60b0a3:rtSm3AMgDCN9ibvYRa5dAQ",
  "eb60b0a3:g1EMlXuNH0WpKlDQ8ECpXQ",
  "eb60b0a3:rL0WLoKRICrAycO8bQ0TZA",
  "eb60b0a3:UlGw3nwB0PfZgPqhnYHjRQ",
  "eb60b0a3:YIkljO2Ta2fxePjWVbUhaA",
  "eb60b0a3:buNyF0aeM5q6HBBgEMhemA",
  "eb60b0a3:vsyOlIR3mFlk5eE4DVTd4A",
  "eb60b0a3:LpM4qTHHpEXdngwBxuIrvA",
  "eb60b0a3:Xh0eIiJxG9KyW6F9JLkdYg",
  "eb60b0a3:1krk2hqGZsWZP99mVVNJSA",
  "eb60b0a3:R1Ks5T4HliO6JZZhioeIMA",
  "eb60b0a3:Tbl0S6dn7QuIcO3w0HScZw",
  "eb60b0a3:dK9174FYAVotz9hz0gLGcQ"
]

打開 //localhost:10081/ 進入服務端邏輯:

webserver_2.get('/', (_req, res) => {
    Webserver.setCookie(res, "rid", true);
    res.clearCookie("mid");
    res.redirect(`/eb60b0a3`);
});

  • eb60b0a3 這裡的這個是在上面隨機配置的

image

設置一個cookie 叫 rid:true
清除cookie mid (下面會說到)
重定向到路由/eb60b03


webserver_2.get(`/eb60b0a3`, (req, res) => {
    const rid = !!req.cookies.rid;
    res.clearCookie("rid");
    if (!rid)
        //不支持
        Webserver.sendFile(res, path.join(path.resolve(), "www/redirect.html"), {
            url_demo: WEBSERVER_DOMAIN_2
        });
    else
        Webserver.sendFile(res, path.join(path.resolve(), "www/launch.html"), {
            favicon: CACHE_IDENTIFIER
        });
});

下面是 www/launch.html的內容

<!DOCTYPE html>
<html>
    <head>
        <link rel="shortcut icon" href="/l/{{favicon}}" type="image/x-icon"/>
    </head>
    
    <body>
        <h1>...</h1>
        <script type="module">
            window.onload = async () => {
                await new Promise((resolve) => setTimeout(resolve, 500));
                const mid = (document.cookie.match(new RegExp(`(^| )mid=([^;]+)`)) || [])[2];
                const route = !!mid ? `/write/${mid}` : "/read";
                document.cookie.split(";").forEach((c) => document.cookie = c.replace(/^ +/, "").replace(/=.*/, `=;expires=${new Date().toUTCString()};path=/`));
                window.location.href = route;
            }
        </script>
    </body>
</html>

如上面的頁面,注意有一個

<link rel="shortcut icon" href="/l/{{favicon}}" type="image/x-icon"/>

頁面加載過程 會觸發加載上面的icon,對應會進入服務端代碼:

webserver_2.get("/l/:ref", (_req, res) => {
    console.info(`supercookie | Unknown visitor detected.`);
    Webserver.setCookie(res, "mid", generateWriteToken());
    const data = Buffer.from(FILE, "base64");
    res.writeHead(200, {
        "Cache-Control": "public, max-age=31536000",
        "Expires": new Date(Date.now() + 31536000000).toUTCString(),
        "Content-Type": "image/png",
        "Content-Length": data.length
    });
    res.end(data);
});

第一次進入站點則會先訪問 /l/:ref 路由

1.如果是第一次進入網站那麼瀏覽器的F-Cache會沒有對favicon:/l/eb60b0a3,則會觸發進入上面的代碼,然後創建一個mid到cookie 返回favicon

2.window.onload事件里的timout走完後,肯定能拿到cookie裏面的mid,客戶端改變路由到/write/xxx

webserver_2.get("/write/:mid", (req, res) => {
    const mid = req.params.mid;
    if (!hasWriteToken(mid))
        return res.redirect('/');
    res.clearCookie("mid");
    deleteWriteToken(mid);
    const uid = generateUUID();
    console.info(`supercookie | Visitor uid='${uid}' is unknown • Write`, STORAGE.index);
    const profile = Profile.from(uid, STORAGE.index);
    if (profile === null)
        return res.redirect('/');
    STORAGE.index++;//這裡++的目的是留給下一個
    Webserver.setCookie(res, "uid", uid);
    res.redirect(`/t/${Webserver.getRouteByIndex(0)}`);
});

const profile = Profile.from(uid, STORAGE.index);
注意這個代碼:

比如當前的index=29,29的二進制11101,我們上面設置的32個路由,前面補0湊足32個:

0000000000000000000000000011101

然後進行reverse變成

1011100000000000000000000000000

以上就是唯一數:29

1對應哪些favicon的路由需要進入客戶端瀏覽器緩存,0的話會捨棄請求不讓客戶端瀏覽器緩存

  • 創建uid
  • index++ (唯一的編號,最上面配置的index,服務端會更新配置)
  • 轉到 route /t/第一個路由 (下面會重點介紹)

非第一次進入站點 會先走到 /read 路由

如果已經瀏覽器的F-Cache已對favicon:/l/eb60b0a3 做過緩存的話,客戶端的代碼會跳轉到 /read

webserver_2.get("/read", (_req, res) => {
    const uid = generateUUID();
    console.info(`supercookie | Visitor uid='${uid}' is known • Read`);
    const profile = Profile.from(uid);
    if (profile === null)
        return res.redirect("/read");

    //設置要遍歷的總次數
    profile._setStorageSize(Math.floor(Math.log2(STORAGE.index ?? 1)) + 1);
    Webserver.setCookie(res, "uid", uid);
    res.redirect(`/t/${Webserver.getRouteByIndex(0)}?f=${generateUUID()}`);
});

  • 創建一個隨機 uid
  • 從Profile讀取一個uid,如果不存在創建一個,如果存在的話 為null
  • 如果 為null 重新路由到/read (這目的是防止generateUUID()重複)
  • 轉到 route /t/第一個路由

下面重點的是 /t/ 路由

webserver_2.get("/t/:ref", (req, res) => {
    const referrer = req.params.ref;
    const uid = req.cookies.uid;
    const profile = Profile.get(uid);
    if (!Webserver.hasRoute(referrer) || profile === null)
        return res.redirect('/');
    const route = Webserver.getNextRoute(referrer);
    if (profile._isReading() && profile.visited.has(referrer))
        return res.redirect('/');
    let nextReferrer = null;
    const redirectCount = profile._isReading() ?
        profile.storageSize :
        Math.floor(Math.log2(profile.identifier)) + 1;
    if (route)
        nextReferrer = `t/${route}?f=${generateUUID()}`;
    if (!profile._isReading()) {
        if (Webserver.getIndexByRoute(referrer) >= redirectCount - 1)
            nextReferrer = "read";
    }
    else if (Webserver.getIndexByRoute(referrer) >= redirectCount - 1 || nextReferrer === null)
        nextReferrer = "identity";
    console.log(nextReferrer)
    const bit = !profile._isReading() ? profile.vector.includes(referrer) : "{}";
    Webserver.sendFile(res, path.join(path.resolve(), "www/referrer.html"), {
        delay: profile._isReading() ? 500 : 800,
        referrer: nextReferrer,
        favicon: referrer,
        bit: bit,
        index: `${Webserver.getIndexByRoute(referrer) + 1} / ${redirectCount}`
    });
});

  • 最上面的路由數組挨個的遍歷,遍歷的次數為 Math.floor(Math.log2(index)) + 1,(2的對數去掉小數點+1),總次數是和唯一數相對的一個算法,比如說當前已經生成了到100萬個唯一數需要20次,16億個唯一數,那麼要遍歷的總次數為31次!40億就是32次 到頭了!
  • 會返回客戶端www/referrer.html

這個www/referrer.html 的html內容裏面有一個

<link rel="shortcut icon" href="/f/{{favicon}}" type="image/x-icon"/>

對應服務端:

webserver_2.get("/f/:ref", (req, res) => {
    const referrer = req.params.ref;
    const uid = req.cookies.uid;
    console.log(referrer);
    if (!Profile.has(uid) || !Webserver.hasRoute(referrer))
        return res.status(404), res.end();
    const profile = Profile.get(uid);
    if (profile._isReading()) {
        profile._visitRoute(referrer);
        console.info(`supercookie | Favicon requested by uid='${uid}' • Read `, Webserver.getIndexByRoute(referrer), "•", Array.from(profile.visited).map(route => Webserver.getIndexByRoute(route)));
        return;
    }
    if (!profile.vector.includes(referrer)) {
        //第一次進入站點會進入寫的邏輯
        console.info(`supercookie | Favicon requested by uid='${uid}' • Write`, Webserver.getIndexByRoute(referrer), "•", Array.from(profile.vector).map(route => Webserver.getIndexByRoute(route)));
        return;
    }
    const data = Buffer.from(FILE, "base64");
    res.writeHead(200, {
        "Cache-Control": "public, max-age=31536000",
        "Expires": new Date(Date.now() + 31536000000).toUTCString(),
        "Content-Type": "image/png",
        "Content-Length": data.length
    });
    res.end(data);
});

  • 如果瀏覽器有F-cache存在的話不會走到上面的代碼
  • 走進去了代表沒有該icon,那麼服務端會把這個路由記錄下來

路由 /identity

/t/ 路由 走完後,會走到 /identity

webserver_2.get("/identity", (req, res) => {
    const uid = req.cookies.uid;
    const profile = Profile.get(uid);
    if (profile === null)
        return res.redirect('/');
    res.clearCookie("uid");
    res.clearCookie("vid");
    const identifier = profile._calcIdentifier();
    if (identifier === maxN || profile.visited.size === 0 || identifier === 0)
        return res.redirect(`/write/${generateWriteToken()}`);
    if (identifier !== 0) {
        const identifierHash = hashNumber(identifier);
        console.info(`supercookie | Visitor successfully identified as '${identifierHash}' • (#${identifier}).`);
        Webserver.sendFile(res, path.join(path.resolve(), "www/identity.html"), {
            hash: identifierHash,
            identifier: `#${identifier}`,
            url_workwise: `${WEBSERVER_DOMAIN_1}/workwise`,
            url_main: WEBSERVER_DOMAIN_1
        });
    }
    else
        Webserver.sendFile(res, path.join(path.resolve(), "www/identity.html"), {
            hash: "AN ON YM US",
            identifier: "browser not vulnerable",
            url_workwise: `${WEBSERVER_DOMAIN_1}/workwise`,
            url_main: WEBSERVER_DOMAIN_1
        });
});

  • 走到這裡 基本上能確定了客戶端走了哪些favicon請求
  • 根據記錄了哪些favicon路由請求 就可以確定是哪個index(一個index代表一個唯一的用戶)

For Mac, delete: ${user.home}/Library/Application Support/Google/Chrome/Default/Favicons

For Windows: go to %LocalAppData%\Google\Chrome\User Data\Default and delete favicons and favicons-journal files

以下是index=29的路由日誌:
可以看出來第一次進入,將29對應的favicon路由寫進客戶端瀏覽器的F-Cache緩存,
然後在觸發read 讀取哪些緩存哪些沒緩存會可以還原得到29

supercookie | Unknown visitor detected.
supercookie | Visitor uid='8ec7-m226-b3d9-k5yc-i5yh' is known • Read
t/eb60b0a3:rxK3EqtBI2GdYI58UTQKsg?f=3inj-ac2u-9wwg-jh96-dww7
supercookie | Favicon requested by uid='8ec7-m226-b3d9-k5yc-i5yh' • Read  0 • [ 0 ]
t/eb60b0a3:2GaUtZwg3fFUFMg4Eirrcg?f=jwoo-x1u9-uu4g-8u1y-32i2
supercookie | Favicon requested by uid='8ec7-m226-b3d9-k5yc-i5yh' • Read  1 • [ 0, 1 ]
t/eb60b0a3:49C0Fec66xaJ3yQhz0WCcw?f=q9i4-hopc-lcq2-j0pv-h1hx
supercookie | Favicon requested by uid='8ec7-m226-b3d9-k5yc-i5yh' • Read  2 • [ 0, 1, 2 ]
t/eb60b0a3:hmMDHBUG9DB4CW02clFxRw?f=81lb-gevm-3tdj-ycg4-ap8q
supercookie | Favicon requested by uid='8ec7-m226-b3d9-k5yc-i5yh' • Read  3 • [ 0, 1, 2, 3 ]
identity
supercookie | Favicon requested by uid='8ec7-m226-b3d9-k5yc-i5yh' • Read  4 • [ 0, 1, 2, 3, 4 ]
supercookie | Visitor uid='gr8o-kmh5-j2fo-rh6m-uj8z' is unknown • Write 29
t/eb60b0a3:rxK3EqtBI2GdYI58UTQKsg?f=y451-fhdp-5asc-qqww-tvan
t/eb60b0a3:2GaUtZwg3fFUFMg4Eirrcg?f=hpf9-w33z-ank8-oesm-hmmn
supercookie | Favicon requested by uid='gr8o-kmh5-j2fo-rh6m-uj8z' • Write 1 • [ 0, 2, 3, 4 ]
t/eb60b0a3:49C0Fec66xaJ3yQhz0WCcw?f=evwy-vcni-sxlg-ncc8-mr51
t/eb60b0a3:hmMDHBUG9DB4CW02clFxRw?f=ropd-egna-2q0c-3f8k-svew
read
supercookie | Visitor uid='ai0j-6xbm-9ikb-mcs3-23ty' is known • Read
t/eb60b0a3:rxK3EqtBI2GdYI58UTQKsg?f=f5mh-8rgh-zrtp-ao7a-fgie
t/eb60b0a3:2GaUtZwg3fFUFMg4Eirrcg?f=xusf-lse6-fssp-1pgb-4w1c
supercookie | Favicon requested by uid='ai0j-6xbm-9ikb-mcs3-23ty' • Read  1 • [ 1 ]
t/eb60b0a3:49C0Fec66xaJ3yQhz0WCcw?f=qqsk-36vu-a333-5fnq-rmdp
t/eb60b0a3:hmMDHBUG9DB4CW02clFxRw?f=geij-0sqf-6hw1-dkax-6kj3
identity
supercookie | Visitor successfully identified as '44 40 C3 17 E2 1B' • (#29).