Chromium 148以降、Math.tanhで基盤OSを識別可能に
(scrapfly.dev)- Chrome 148以降、V8の
Math.tanhが内蔵fdlibmではなくホストのstd::tanhを呼び出すようになり、同じ入力でもLinuxのglibc、macOSのlibsystem_m、WindowsのUCRTで互いに異なる最下位ビットを返す Math.tanh(0.8)はLinuxで0.6640367702678491、macOSで0.664036770267849、Windowsで0.6640367702678489となり、1回の呼び出しで3つのOSを区別できる。User-Agentが主張するOSと結果が異なれば偽装が露見する- エンジンごとに漏えい経路は異なり、V8の
Math.*ではtanhだけがホストの数学ライブラリを使う一方、BlinkのCSS三角関数全体とWeb Audioの一部演算もOS別ライブラリを経由する - 値を任意に揺らすと実際のどのOSとも一致せず、決定性まで壊れるため、対象ライブラリの係数・テーブル・範囲縮小・FMA動作をビット単位で再現するか、元のUCRTコードを直接マッピングする必要がある
- Scrapflyはリリースごとに871,000個の入力を実機MacとChromeで照合し、
Math.tanhとCSS三角関数7種のビット一致を検証している。精度だけでなく、アーキテクチャ差と実行時間も実ブラウザ水準に合わせている
Math.tanhが明かすOS
Math.tanh(0.8)の結果はホストの数学ライブラリによって変わる- Linux Chromeのglibc:
0.6640367702678491 - macOS Chromeの
libsystem_m:0.664036770267849 - Windows ChromeのUCRT:
0.6640367702678489
- Linux Chromeのglibc:
- Appleとglibcは全入力のおよそ4分の1で概ね1 ULPの差を示し、Windows UCRTは両ライブラリと数パーセントの入力で異なる
- ULP(unit in the last place)は、特定の大きさで表現可能な連続する浮動小数点数同士の間隔であり、1 ULPは
doubleが表せる最小の差である
- ULP(unit in the last place)は、特定の大きさで表現可能な連続する浮動小数点数同士の間隔であり、1 ULPは
- 実際のChrome 150をLinux、Apple SiliconベースのmacOS 26、Windows 11でDevTools Protocolにより測定すると、入力によって分類力が変わる
tanh(0.5)は3つのOSすべてで0.46211715726000974と同じで、検出には使えないtanh(0.7)はLinuxだけが1 ULP異なるtanh(0.8)は3つのOSがすべて異なり、全体の幅は2 ULPであるtanh(0.9)はWindowsだけが1 ULP異なる
- およそ4分の3の入力では3つのOSが同じ結果を出すが、適切な入力が1つあればOS別のシグネチャを得られる
- macOSを名乗りながらLinuxの数学ビットを返すと、
Math.tanhの結果がUser-Agentと矛盾する
Chrome 148で起きた変更
- Chrome 147までは、V8が移植可能な数学実装であるfdlibmのポートを内蔵して
Math.tanhを計算していたため、すべてのOSで同じビットを返していた - V8コミット
c1486295ae5は、内蔵実装をプラットフォームのstd::tanhに置き換えた- この変更はV8 14.8.57とChrome 148に初めて含まれた
- Chrome 148・149・150はホスト
libmの差を露出するが、Chrome 147以前はこの経路でOSを漏らさない
- IEEE 754は
doubleの格納方式を定義するが、sin、cos、tanh、expのような超越関数が必ず正しく丸められることは要求していない - 各OSの**数学ライブラリ(libm)**は、性能とULP誤差をトレードオフし、互いに異なるミニマックス近似多項式係数、ルックアップテーブル、範囲縮小定数を使う
- Linuxはglibc
- macOSはApple
libsystem_m - WindowsはUCRTの
ucrtbase.dll
- 検出器は数学演算そのものを解析する必要はなく、実際のChromeの入力別結果表と値を比較できる
再現を難しくする4つの落とし穴
-
V8の一部関数だけが漏えいする
- V8は数学実装の大半を静的リンクしているため、OSに関係なく同じ結果を出す
Math.exp、Math.pow、Math.atanなどは内蔵のllvm-libc実装を使うMath.sinとMath.cosはglibc由来の内蔵dbl-64ルーチンを使う- Chrome 148以降、プラットフォームの
std::tanhを使うMath.tanhだけが、Math.*の中でOSを漏えいする - 漏えいしない関数まで対象OSらしく偽装すると、V8の実際の呼び出し構造と食い違い、
tanhだけが異なるという非対称性自体も検査できる
-
JavaScriptとCSSは異なる経路を使う
- CSSの
sin()、cos()、atan2()はJavaScriptのMath.sinとコードを共有しない - Blinkレイアウトエンジンは角度を度単位で縮小した後、縮小後の値に対してプラットフォームの
std::sinなどを呼び出す - 直接ラジアン入力を計算した結果とは異なり、CSS三角関数7種すべてがホスト
libmを通じてOSを漏えいする - ビット単位の再現には、最終的な数学関数だけでなく、度単位の範囲縮小とラジアン・度変換の過程も含める必要がある
- CSSの
-
macOS内にも2つの異なるライブラリがある
- Apple Siliconにはスカラー
libsystem_mとAccelerateのベクトルルーチンであるvvsin、vvtanhが併存しており、2つの実装は同じではない - 100万個の入力で、関数によって**10〜89%**の結果が異なった
cos(0)はスカラー実装では正確に1.0である- Accelerateでは
0.9999999999999999を返す - 実機MacのChromeをデバッグプロトコルで測定し、呼び出し箇所ごとのライブラリを区別する
Math.tanh、CSS三角関数、オーディオコンプレッサーのサンプル別超越関数はスカラーlibsystem_mを使う- MacのWeb Audio DSP、FFT、ベクトル数学、バイクアッドフィルターはAccelerateを使う
- 関連するChromiumのパスには
fft_frame_mac.cc、vector_math_mac.h、biquad.ccとBUILDFLAG(IS_MAC)がある - 呼び出し箇所に合わないAppleライブラリを選ぶと、多くの入力で1 ULPずれる可能性がある
- Apple Siliconにはスカラー
-
CPUアーキテクチャも結果に介入する
- ARMとx86は**融合積和演算(FMA)**およびNaN符号伝播で差を示す
- 数学的手順が正しくても、コンパイラが片方のアーキテクチャでだけ乗算と加算を融合すると、結果ビットが変わる
エンジンと機能別の漏えい経路
- JavaScriptのV8
Math.*はほぼすべて内蔵実装を使い、ホストlibmにつながる箇所はMath.tanhだけであるsin、cos、tan、asin、acos、atan、atan2、exp、log、log2、log10、powはV8内蔵実装を使うsqrt、abs、四則演算はハードウェア演算である
- CSS
calc()の数学関数はBlinkがプラットフォームライブラリを直接呼び出すsin、cos、tan、asin、acos、atan、atan2、exp、log、log2、log10、powが**ホストlibm**を使う- CSSには対応する
tanh経路がない
- Web Audioは呼び出し箇所に応じて複数の実装を混在させて使う
- MacのオシレーターFFT、ベクトル加算・乗算・スケール、FFTはAccelerateの
vDSPを使う - DynamicsCompressorの
sin、exp、log10f、powfなどのサンプル別超越関数はスカラーlibsystem_mを使う - 1つのオーディオグラフが、V8内蔵数学、スカラーライブラリ、Accelerateという3つのライブラリにまたがることがある
- MacのオシレーターFFT、ベクトル加算・乗算・スケール、FFTはAccelerateの
- WebAssemblyには超越関数命令がない
sinなどの結果はモジュールに含まれるlibmによって決まるf64.sqrt、f64.mulのような算術はハードウェアで実行されるため、OS間で同じである- 残るフィンガープリント軸は、ARMとx86間のNaN正規化および一部SIMD丸め差である
- 検出信号は
Math.tanh、すべてのCSS三角関数、Web Audioに集中する- Web AudioのAccelerate FFTはCPUアーキテクチャを明かす
- コンプレッサーのスカラー
libsystem_mはOSを明かす
値を揺らすのではなく正確に再現する
-
ノイズが失敗する理由
- 結果にノイズを加えると、基準表にあるどの実OSの値とも一致しない可能性がある
- 呼び出しごとにランダム値が変わると決定性が壊れ、この現象自体が別の検出信号になる
- 目標は似た値ではなく、主張したOSが返す値とビット単位で同じ結果である
-
対象アルゴリズムの全要素を復元する
- 対象
libmからミニマックス近似係数、指数テーブル、範囲縮小定数を復元し、移植可能なCコードへ移す - 対象ライブラリが誤った方向に丸める入力まで、そのまま一致させなければならない
- Apple
sinの再現は、libsystem_mから抽出した係数の正確なビットパターンと明示的なfma()呼び出しを使う - 係数を10進数に移すと転記過程で再び丸められる可能性があるため、16進浮動小数点値として保存する
- Appleが融合する各乗算・加算を、コード上でも明示的に融合する
- 対象
-
FMAを決定的に固定する
-ffp-contract=offでコンパイルし、コンパイラが任意にFMAを追加・削除できないようにする- コードに明示した
fma()だけがAppleと同じ位置で実行されるため、ARMを模倣しながらx86サーバー上で実行しても同じビットが得られる - ハードウェアFMAと正しく丸められるソフトウェアFMAは同じビットを返す
Windows UCRTの元コードを使う
- Windows UCRTはLinuxサーバーと同じx86-64 ISAを使い、位置独立であるため、実際の
ucrtbase.dllをランタイムメモリにマッピングし、数学関数のエクスポートを直接呼び出せる - 元コードを実行するため、別途数学アルゴリズムをリバースエンジニアリングしなくても実際のUCRTビットを得られる
- LinuxのSystem V ABIとWindows x64 ABIの差を処理する必要がある
- Windows x64では、呼び出し先が戻りアドレスの上にある32バイトのshadow spaceを使う
- 呼び出し先保存レジスタ集合もSystem Vとは異なる
- 関数ポインタを
ms_abiとして宣言しないと、shadow-spaceへの書き込みがclangのスタックフレームを破壊し、間接呼び出しが誤ったアドレスへ移動する可能性がある
- マッピングしたDLLコードはCFIに登録された間接呼び出し先ではない
- 本番環境の
-fsanitize=cfi-icallが呼び出しごとに#UDトラップとSIGILLを発生させる可能性がある - 関数ポインタを呼び出すラッパーには
clang::no_sanitize("cfi-icall")が必要である
- 本番環境の
- UCRT数学関数は冒頭の
mov eax, [rip+disp32]でCPUディスパッチフラグを読み、スカラーまたはFMA/AVX2経路を選択する- 新たにマッピングしたDLLではフラグが0のため、遅いスカラー経路を選ぶ
- この経路の結果は最新Windowsシステムの結果ビットと異なる
tanhプロローグでフラグアドレスを見つけ、初回呼び出し前にFMA経路を強制しなければ、実際のWindowsとビット単位で一致しない
パッチ位置と性能制約
- エンジンが
libmを呼び出す単一のボトルネック箇所をフックし、ブラウザが主張するOSに応じて経路を選ぶ- Linuxを主張するならglibcを維持する
- macOSを主張するならApple再現実装を使う
- 結果が正確でも、実行時間が実ブラウザと異なれば検出できる
- 最初のビルドは基本x86ベースラインがハードウェアFMAより古かったため、すべての
fma()がソフトウェア呼び出しに下げられ、ネイティブより2.5〜6倍遅かった Math.tanhとMath.sinのループ実行時間比を比較すると、実ブラウザにはない性能パターンが露出する可能性がある- ハードウェアFMAを有効にすると、各融合演算が単一命令になって約6倍高速化し、glibcより速い一方で結果ビットは同一だった
871,000個の入力で検証
- 検証ハーネスはリリースごとに871,000個の入力をすべての分岐と定義域にわたって実行する
- 密な入力格子
- 区間境界
- 非正規数
- 符号付きゼロ
- 無限大
- NaN
- 2種類の実環境を基準値として使う
- 実機Macがすべての入力でスカラーとAccelerateの結果をそれぞれ計算し、2つの実装が分岐する箇所を確認する
- 実機MacのChromeをデバッグプロトコルで駆動し、
Math.tanhとすべてのCSS三角関数の全精度結果を収集する
Math.tanhとCSSのsin、cos、tan、asin、acos、atan、atan2が、実機MacのChromeとビット単位で一致する- 再現実装が配布バイナリ内の実際の機械語と同じように動作するかも検証する
- 定義域境界でのブラウザ後処理まで合わせる必要がある
- 実機MacでCSS
asin(2)は定義域外のためNaNになり、CSSがNaNを0に制限するので最終値は0である - 単純な再現実装では、これを誤って90度として返す可能性がある
- 実機MacでCSS
ブラウザ偽装で数学が重要な理由
- 数学結果は決定的かつ低コストで検査できるが、正確に偽装するにはベンダー
libmの内部とエンジン別の呼び出し経路を知る必要がある - 実ブラウザと一致させるには、V8・Blink・Web Audioが呼び出し箇所ごとにどの数学ライブラリを選ぶかを把握し、最下位ビット、アーキテクチャ別動作、実行時間まで合わせなければならない
- ScrapflyのScrapiumは、macOSとして表示するよう要求された場合、コサインの丸めビットまで実際のmacOSトラフィックと合うように構成されている
1件のコメント
Hacker Newsの反応
適切な入力に対して
tanhを1回呼び出した結果がOSごとのシグネチャになる、という説明は、ブラウザのバージョン範囲を特定できる可能性を見落としているほとんどの人はUser-AgentのOSを偽装しないし、フィンガープリンティングが関心を持つのはOSそのものよりも準固有な特性の組み合わせだ。この発見自体は興味深いが、記事があまりにもLLMっぽく書かれていて信頼感を損ねている
そうすることでボット検知をより簡単にすり抜け、他サイトから収集したデータを顧客に販売できる
LLMで書いたことは記事とブログ内で公開しており、隠したり人間のふりをしたりしたわけではない。時間がなかったので、そうしていなければ記事自体を公開できなかっただろうし、この選択には責任を持つつもりだ
ブラウザは継続的に機能を追加しバグを修正しており、その大半は JavaScriptで検出 できる
あらゆるフィンガープリンティング手法をAIで分析して公開し、議論の末にブラウザ側に対策させれば、自社のスクレイピング事業がより儲かるのだから賢い戦略だ
こういう会社がなければ ブラウザフィンガープリンティング は今ほど蔓延しておらず、インターネットはもっと良いものだったはずだ。むしろ fingerprint.js のように利害関係が明確な反対側の文章のほうが好ましい
正しく丸められた超越関数 を推進すべき理由がまた一つ増えた
最近、この問題は事実上解決されていると知った。https://arith2026.org/program.html の2番目の基調講演を参照
libm関数は素晴らしいが、過去のglibcのpowのように 最悪時の性能 がひどくてはならない丸め境界付近で使う高精度の代替経路を直接SLPベクトル化して最悪性能を改善することはできるかもしれないが、すでにほとんどの用途には十分だ。JavaScriptエンジンがECMAScript仕様で推奨されている
fdlibmを使い続けていないのは意外で、Math.tanhがJavaScriptのボトルネック経路になるならかなり珍しいコードだ工学では、もっと単純なハードウェアで動かせて誤差も数学的にモデル化しやすいため、固定小数点がよく使われてきた。IEEE 754浮動小数点は理論的にもやや疑わしく、精度損失の観点では仮数部より小さい整数、つまり24ビット未満の整数のほうが32ビット浮動小数点より良い場合すらある
この手法が https://coveryourtracks.eff.org/ に追加されて、自分の数学関数の結果がより大きな母集団の中でどれだけ固有か確認できるようになるといい
本当かどうかは分からないが、coveryourtracks.eff.org が使うシグナルは25個くらいではないかと思う
記事には Claudeが書いた感じ が残っている
Claudeリンクを押すと、
summarize+this+article+and+explain+how+scrapfly+helps+me+scrape+any+website+at+scale+and+bypass+anti-bot+systems+for+my+use+case:+[https://scrapfly.dev/posts/browser-math-os-fingerprint/](<https://scrapfly.dev/posts/browser-math-os-fingerprint/>)というプロンプトが渡されるTor BrowserとMullvad Browserも結局OS隠蔽を諦めたが、ひょっとすると諦めるべきではなかったのかもしれない
それだけ フィンガープリンティングの経路 が多すぎるように見える
ブラウザ内外でOSごとの差異が多すぎて全部に対処するのは難しい。Canvasの読み出しをブロックしたりノイズを加えたりしてもレンダリング差が露出しうるし、Tor Browserの開発者も、完全に別のOSどころか X11とWayland の違いすら隠せないと確認している。https://forum.torproject.org/t/linux-is-it-alright-to-run-th...
navigator.platformすら書き換えないので、Windowsではない環境 を見抜くのはとても簡単だ好みのJavaScript注入プラグインで次のコードを入れればいい:
let oldTanh = Math.tanh; Math.tanh = x => oldTanh(x) + Math.random()/10000000;Math.tanh = Math.random;のほうが好きだNo noiseを検索すればよい最新のglibcはCORE-MATHの 正しく丸められた
tanhを使うため、記事で引用されている値とは異なる結果を返す他の超越関数でも妥当な性能で正しい丸めを実現できるかはまだ不明で、各関数がそれぞれ固有のフィンガープリントを残す
Chromeは実行コードだけで数百MBあるので、ユーザー空間ライブラリの半分くらいは静的リンクしているものと思っていた
また
tanhは関数呼び出しではなくJavaScript JITがCPU命令として出力する 組み込み演算 だと思っていたので、数学演算のためにdlsym()関数へ分岐するというのは奇妙だ。CPU命令自体もフィンガープリンティングされうるマイクロコードは分岐予測のような利点を得られないため、実際にはソフトウェア実装より遅い
この戦いに勝てるのか疑問だ
十分に多くの関数を実行すれば、実行時間の比率と丸め結果を組み合わせてOSや正確な機種だけでなく、同じマシン上で動いている他の作業まで推定できそうだ。完全に防ぐより、少し難しくする程度しかできないように思える
結局は社会と法が追いつくしかない。ドアの鍵が侵入を完全には防げなくても社会的非難や刑事罰が補うのと同じように、こうした 個人追跡を違法化 し、その利益を利用する企業やそこで働く人を社会的に排斥すべきだ
ロシア・ミャンマー・北朝鮮のような場所では 法の支配が機能しておらず、外国人をだます犯罪者を現地当局が積極的に保護することさえあるため、ドアの鍵の比喩は成り立たない