One IT Thing

IT業界を楽しむ為の学習系雑記

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
-,

執筆者:

関連記事

pgcryptoの有効化と共通鍵暗号の動作確認

PostgreSQLで「pgcrypto」拡張機能を有効にすると、PostgreSQLに保存するデータを暗号化出来るようになります。 INSERTする際、共通鍵暗号はpgp_sym_encrypt関数 …

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

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

開発用のRSA鍵ペア(バイナリ、テキスト)をopensslで作っておく

目次1 環境2 秘密鍵3 公開鍵4 まとめ 環境 CentOS7.6に付属のopenssl 1.0.2k。 $ cat /etc/redhat-release CentOS Linux release …

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

セキュリティ的にクリティカルなデータをクライアントブラウザで暗号化保存するようにしてみます。 通信経路はHTTPSで暗号化されていてもスマホに重要なデータが平文で残っていたら珠に傷です。 目次1 環境 …

主要ブラウザに保存させたパスワードの確認方法を比較してみる(Firefoxをメインブラウザにしない理由)

アクセスしたサイトのパスワードをブラウザに覚えてもらったけどなんて入れたっけ? なんてこと結構あるんじゃないかと思います。 各種ブラウザとも覚えさせたパスワードは後から確認が出来るのでそれぞれの方法を …


shingo nakanishi。東京で消耗中の職歴20年越え中年ITエンジニアです。「生涯現役プログラマを楽しむ」ことができる働き方探しをライフワークにしています。

19歳(1996年)から書き始めた個人日記が5,000日を超え、残りの人生は発信をして行きたいと思い、令和元日からこのサイトを開始しました。勉強と試行錯誤をしながら、自分が経験したIT関連情報を投稿しています。