security

Chrome76でシークレットモード非検知を実現出来なかった件の検証

投稿日:2019年8月14日

2019/07/30にChrome76がリリースされました。

76では以前からGoogleが問題視していた「閲覧者がシークレットモードでみているかどうか運営側が分かってしまう」が解決されるはずでした。

しかし、76になってもシークレットモードかどうか判定出来る方法がまだ二つ有る、というニュースが話題になっています。

実際Chrome76でこの二つの方法を用いてシークレットモードを検出できるのかどうか検証しました。

Chrome76以前のシークレットモード判定方法

今までのシークレットモードはブラウザのFileSystemAPIが正常動作しなかった為「ブラウザからハードディスクにアクセスしてエラーになった場合はシークレットモード」と判断出来ていました。

window.webkitRequestFileSystem(
    window.TEMPORARY,
    100,
    console.log.bind(console, "通常モード"), // 正常ハンドラ
    console.log.bind(console, "シークレットモード") // 異常ハンドラ
);

Chrome76で入った対策

これに対し適当なChrome76では、シークレットモード時はディスクファイルシステムの替わりにメモリファイルシステムが提供されるようになりました。

これによってシークレットモードでもこのAPIは正常ハンドラに入るようになり、シークレットモードかどうか分からなくなります。

「ディスクファイルシステムの替わりにメモリファイルシステムを用意してFileSystemAPIが正常に動くようにする」がGoogle側の対策でした。

この対策に二つの抜け道が発見されたのが今回のニュースの内容です。

検証1:ファイルシステムのディスク容量で判断する

vikas mishraさんが発見したQuota Management APIを使い、ブラウザが使えるディスク容量を調べる方法。

Chromeが使えるHDD容量の仕様は概ねこんな感じです。

  • HDD総容量の10分の1(ただしMAXは2GByte)
  • テンポラリディスクの容量はその50%になる
  • 仮想的なメモリファイルシステムだと総容量が小さく2.4Gbyte以下
  • 2.4Gbyte以下の総容量だとテンポラリディスク容量は120Mbyte以下になる

今時2.4GByteのハードディスク容量しかない端末は少ないので、クォータが120Mbyte以下だったらメモリファイルシステム = シークレットモードである、と判断出来るとのこと。

試しにindex.htmlを作り、適当なWebサーバに載せて検証してみます。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Document</title>

        <script>
            // Chrome76以前のシークレットモードの見分け方
            var fs = window.RequestFileSystem || window.webkitRequestFileSystem;
            if (!fs) {
                console.log("check failed?");
            } else {
                fs(
                    window.TEMPORARY,
                    100,
                    console.log.bind(console, "旧調査方法の判定:通常モード"),
                    console.log.bind(console, "旧調査方法の判定:シークレットモード"
                    )
                );
            }

            // Chrome76でも見分けられるmishraさんの方法
            async function start() {
                if ("storage" in navigator && "estimate" in navigator.storage) {
                    const { usage, quota } = await navigator.storage.estimate();
                    console.log(`Using ${usage} out of ${quota} bytes.`);

                    if (quota < 120000000) {
                        console.log("新調査方法の判定:シークレットモード");
                    } else {
                        console.log("新調査方法の判定:通常モード");
                    }
                } else {
                    console.log("Can not detect");
                }
            }
            start();
        </script>
    </head>
    <body></body>
</html>

ChromeからCtrl + Shift + Nでシークレットモードタブを作り、上記index.htmlにアクセスした実行結果。

Using 0 out of 110763386 bytes.
新調査方法の判定:シークレットモード
旧調査方法の判定:通常モード

確かに120Mbyte以下になっていて、旧調査方法では見抜けなくなったシークレットモードが見抜けました。

2.4GbyteしかHDDを積んでない端末のChromeの場合は判断を誤るでしょうが、確度の高い見分け方だと言えそうです。

検証2:ディスク書き込み速度で判断する

続いてJesse Liさんが見つけた「メモリファイルシステムはディスクファイルシステムよりアクセスが速い」事実を利用した検出方法。

ファイル書き込みを100回繰り返し、処理が返ってきた時間を計るベンチマーキング方式になっています。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Document</title>

        <script>
           const largeStrings = [
                // These strings are 5000 characters long. I generated them by running
                // base64 /dev/urandom -w 0 | head -c 5000
                "odE141SCRsNhfNBb95VhqRubp+fXTF1Dricc0G9wWrQcXRvu3uhGRh4t2TiUZF1BdSKLOrnG...",
                "pdfhLvvnkBGjbuR1/0WcCcM2li/cYOQ/wZGPAofjBXxo6PvhoEAWYtEMtTlbcLm+dPxwQFm8...",
                "Xfo5aKCHnIQc9zMtUWmGYiwzBJuDQLEVyg0t9ID2ZsCVMnVD7h8juo9Bmd+e2VdmofvGkFoa...",
                "jsYalJDnye4x5Vvl9w+F7aRrVx+WcJT5E7rzB9UNxb7iyY+mFAvsllN95ZDom50+GhhBuT+l...",
                "QcaZ/f91np7UkMvy4jrJks5Iogpgik0JZA0kCeXEPc2vdFYHKKIVT+nKmrva0qUee14LXh9Y..."
            ];
            const SIZE = 6 * 1024 * 1024; // 6 MB
            // Completely arbitrary numbers. Probably make them as high as you can tolerate:
            const NUM_BENCHMARK_ITERATIONS = 200;
            const NUM_MEASUREMENTS = 100;

            const writeToFile = (fs, data) => {
                return new Promise(resolve => {
                    fs.root.getFile("data", { create: true }, fileEntry => {
                        fileEntry.createWriter(fileWriter => {
                            fileWriter.onwriteend = resolve;

                            var blob = new Blob([data], { type: "text/plain" });
                            fileWriter.write(blob);
                        });
                    });
                });
            };

            const runBenchmark = async fs => {
                const time = new Date();
                for (let i = 0; i < NUM_BENCHMARK_ITERATIONS; i++) {
                    for (let j = 0; j < largeStrings.length; j++) {
                        await writeToFile(fs, largeStrings[j]);
                    }
                }
                return new Date() - time;
            };

            const onInitFs = async fs => {
                const timings = [];
                for (let i = 0; i < NUM_MEASUREMENTS; i++) {
                    timings.push(await runBenchmark(fs));
                }

                console.log(timings);
            };

            window.webkitRequestFileSystem(window.TEMPORARY, SIZE, onInitFs);
        </script>
    </head>
    <body></body>
</html>

通常モード(ディスクファイルシステム)の実行結果。

[2435, 2136, 2154, 2198, 2147, 2122, 2124, 2091, 2274, 2067, 2202, 2240, 2334, 2218, 2129, 2154, 2122, 2233, 2232, 2140, 2131, 2192, 2243, 2214, 2140, 2200, 2253, 2353, 2294, 
2216, 2137, 2066, 2112, 2101, 2118, 2035, 2173, 2285, 2117, 2167, 2091, 2137, 2077, 2200, 2219, 2108, 2128, 2110, 2104, 2304, 2186, 2781, 3418, 2757, 2546, 2356, 2391, 2443, 2366, 
2391, 2301, 2405, 2380, 2375, 2511, 2578, 2410, 2450, 2245, 2387, 2357, 2349, 2293, 2391, 2326, 2289, 2347, 2416, 2322, 2470, 2249, 2281, 2335, 2222, 2320, 2360, 2252, 2350, 2264, 2315, 
2402, 2452, 2403, 2390, 2336, 2305, 2409, 2340, 2396, 2326]

プライベートモード(メモリファイルシステム)の実行結果。

[1094, 997, 998, 994, 1010, 1039, 1063, 1171, 1044, 1016, 984, 981, 984, 1004, 972, 983, 991, 1017, 982, 987, 982, 996, 1284, 996, 978, 992, 1015, 1157, 969, 981, 1008, 976, 
985, 989, 997, 1008, 989, 1196, 974, 1040, 973, 981, 1102, 996, 987, 995, 972, 1019, 994, 987, 982, 1013, 1214, 980, 987, 982, 1033, 980, 1144, 984, 1036, 983, 985, 995, 977, 1023, 
977, 1001, 1210, 1004, 984, 985, 982, 1026, 1147, 977, 991, 982, 1017, 993, 1139, 981, 1046, 980, 983, 982, 1019, 988, 998, 977, 1197, 983, 985, 982, 990, 1010, 985, 1196, 1040, 1048]

明らかにプライベートモードの方が速いですね。

めちゃくちゃ速いHDDだったらどうするのかとか、ベンチマークが終わるまで結構時間が掛かるとか懸念は残りますが判断材料には出来そうです。

今後も改善は続くとのこと

どちらもかなり信憑性のある見分け方でした。

冒頭のニュースサイトがGoogle側に問い合わせたところ、今後改善していく返答があったようです。

ただ改善するにしてもChromeが使えるメモリを増やすのはナンセンスですし、RAMがHDDより速いことは事実です。

どういった対応がなされるのか、ウォッチする側としてはGoogleの出方が楽しみになって来ました。

-security
-,

執筆者:

関連記事

ブラウザでRSA暗号化したデータをサーバで復号する(Angular + JSEncrypt、Spring MVC)【後編】

前回の続きです。One IT ThingブラウザでRSA暗号化したデータをサーバで復号する(Angular + JSEncrypt、Spring …https://one-it-thing.com …

pgcryptoで公開鍵暗号の動作確認

共通鍵暗号で暗号化されたデータはパスワードが漏洩すると復号される危険が高まるのに対し、公開鍵暗号で暗号化されたデータは秘密鍵とパスワードの二つが漏洩しないと復号できません。 APサーバとDBサーバ通信 …

H2 Databaseで生成したSHA256値をJavaで生成したSHA256値と比較してみる

ファイル、サーバ、メモリ、様々な動作形態がとれてプロトタイピングや、配布アプリの組み込みDBとして便利に使えるPure Javaデータベースの「H2」。 h2database.com  2 …

HTTPS開発環境用、自己署名証明書の作成

(2020/09/18追記) mkcertを使ってより簡単に完璧なSSL証明書を作ることができました。 ささっと開発用のHTTPSサーバを作りたい時はmkcert使った方が良いですね。 One IT …

インターネット上に流れる通信をTSUBAMEで俯瞰する

目次1 TSUBAMEとは1.1 四半期ごとに統計を公開してくれる1.2 週間レポートもしてくれる TSUBAMEとは JPCERTコーディネーションセンター TSUBAME(インターネット定点観測シ …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯技術者として楽しく生きることを目指しています。デスマに負けず健康第一。