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

執筆者:

関連記事

CentOSで暗号鍵用のパスワードを生成

そこそこ長くて文字種の入り混じった強度の高いものを自分で考えるのは面倒です。 暗号化処理を使う際に必要なパスワード文字列を、mkpasswdコマンドでいい感じに生成出来るようにしておきます。 目次1 …

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

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

開発用のPKCS#12ファイルをOpenSSLで出来るだけ速く作る

「電子署名法」に則るにはセキュリティトークンが必要で、デジタル署名をちょっと試したいケースならPKCS#12が手っ取り早いです。 One IT Thing電子署名法で求められる「本人性」と「非改ざん性 …

主要ブラウザに保存させたパスワードの確認方法を比較してみる

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

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

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

 

shingo.nakanishi
 

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