1 ポイント 投稿者 GN⁺ 4 시간 전 | 1件のコメント | WhatsAppで共有
  • 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
  • Appleとglibcは全入力のおよそ4分の1で概ね1 ULPの差を示し、Windows UCRTは両ライブラリと数パーセントの入力で異なる
    • ULP(unit in the last place)は、特定の大きさで表現可能な連続する浮動小数点数同士の間隔であり、1 ULPはdoubleが表せる最小の差である
  • 実際の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 754doubleの格納方式を定義するが、sincostanhexpのような超越関数が必ず正しく丸められることは要求していない
  • 各OSの**数学ライブラリ(libm)**は、性能とULP誤差をトレードオフし、互いに異なるミニマックス近似多項式係数、ルックアップテーブル、範囲縮小定数を使う
    • Linuxはglibc
    • macOSはApple libsystem_m
    • WindowsはUCRTのucrtbase.dll
  • 検出器は数学演算そのものを解析する必要はなく、実際のChromeの入力別結果表と値を比較できる

再現を難しくする4つの落とし穴

  • V8の一部関数だけが漏えいする

    • V8は数学実装の大半を静的リンクしているため、OSに関係なく同じ結果を出す
    • Math.expMath.powMath.atanなどは内蔵のllvm-libc実装を使う
    • Math.sinMath.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を漏えいする
    • ビット単位の再現には、最終的な数学関数だけでなく、度単位の範囲縮小とラジアン・度変換の過程も含める必要がある
  • macOS内にも2つの異なるライブラリがある

    • Apple Siliconにはスカラーlibsystem_mAccelerateのベクトルルーチンであるvvsinvvtanhが併存しており、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.ccvector_math_mac.hbiquad.ccBUILDFLAG(IS_MAC)がある
    • 呼び出し箇所に合わないAppleライブラリを選ぶと、多くの入力で1 ULPずれる可能性がある
  • CPUアーキテクチャも結果に介入する

    • ARMとx86は**融合積和演算(FMA)**およびNaN符号伝播で差を示す
    • 数学的手順が正しくても、コンパイラが片方のアーキテクチャでだけ乗算と加算を融合すると、結果ビットが変わる

エンジンと機能別の漏えい経路

  • JavaScriptのV8 Math.*はほぼすべて内蔵実装を使い、ホストlibmにつながる箇所はMath.tanhだけである
    • sincostanasinacosatanatan2exploglog2log10powはV8内蔵実装を使う
    • sqrtabs、四則演算はハードウェア演算である
  • CSS calc()の数学関数はBlinkがプラットフォームライブラリを直接呼び出す
    • sincostanasinacosatanatan2exploglog2log10powが**ホストlibm**を使う
    • CSSには対応するtanh経路がない
  • Web Audioは呼び出し箇所に応じて複数の実装を混在させて使う
    • MacのオシレーターFFT、ベクトル加算・乗算・スケール、FFTはAccelerateのvDSPを使う
    • DynamicsCompressorのsinexplog10fpowfなどのサンプル別超越関数はスカラーlibsystem_mを使う
    • 1つのオーディオグラフが、V8内蔵数学、スカラーライブラリ、Accelerateという3つのライブラリにまたがることがある
  • WebAssemblyには超越関数命令がない
    • sinなどの結果はモジュールに含まれるlibmによって決まる
    • f64.sqrtf64.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.tanhMath.sinのループ実行時間比を比較すると、実ブラウザにはない性能パターンが露出する可能性がある
  • ハードウェアFMAを有効にすると、各融合演算が単一命令になって約6倍高速化し、glibcより速い一方で結果ビットは同一だった

871,000個の入力で検証

  • 検証ハーネスはリリースごとに871,000個の入力をすべての分岐と定義域にわたって実行する
    • 密な入力格子
    • 区間境界
    • 非正規数
    • 符号付きゼロ
    • 無限大
    • NaN
  • 2種類の実環境を基準値として使う
    • 実機Macがすべての入力でスカラーとAccelerateの結果をそれぞれ計算し、2つの実装が分岐する箇所を確認する
    • 実機MacのChromeをデバッグプロトコルで駆動し、Math.tanhとすべてのCSS三角関数の全精度結果を収集する
  • Math.tanhとCSSのsincostanasinacosatanatan2が、実機MacのChromeとビット単位で一致する
  • 再現実装が配布バイナリ内の実際の機械語と同じように動作するかも検証する
  • 定義域境界でのブラウザ後処理まで合わせる必要がある
    • 実機MacでCSS asin(2)は定義域外のためNaNになり、CSSがNaNを0に制限するので最終値は0である
    • 単純な再現実装では、これを誤って90度として返す可能性がある

ブラウザ偽装で数学が重要な理由

  • 数学結果は決定的かつ低コストで検査できるが、正確に偽装するにはベンダーlibmの内部とエンジン別の呼び出し経路を知る必要がある
  • 実ブラウザと一致させるには、V8・Blink・Web Audioが呼び出し箇所ごとにどの数学ライブラリを選ぶかを把握し、最下位ビット、アーキテクチャ別動作、実行時間まで合わせなければならない
  • ScrapflyのScrapiumは、macOSとして表示するよう要求された場合、コサインの丸めビットまで実際のmacOSトラフィックと合うように構成されている

1件のコメント

 
GN⁺ 4 시간 전
Hacker Newsの反応
  • 適切な入力に対して tanh を1回呼び出した結果がOSごとのシグネチャになる、という説明は、ブラウザのバージョン範囲を特定できる可能性を見落としている
    ほとんどの人はUser-AgentのOSを偽装しないし、フィンガープリンティングが関心を持つのはOSそのものよりも準固有な特性の組み合わせだ。この発見自体は興味深いが、記事があまりにもLLMっぽく書かれていて信頼感を損ねている

    • この記事を書いた会社は、実際には Linux VM上のボット をWindowsやmacOSの物理マシンのように偽装しようとしている
      そうすることでボット検知をより簡単にすり抜け、他サイトから収集したデータを顧客に販売できる
    • 今のところこの方法だけでは Chromium 148以降 としか判定できないが、各バージョンで追加されたV8やBlinkの機能をJavaScriptやCSSで調べれば、だいたいバージョン120以降ならメジャーバージョンを確実に特定できる
      LLMで書いたことは記事とブログ内で公開しており、隠したり人間のふりをしたりしたわけではない。時間がなかったので、そうしていなければ記事自体を公開できなかっただろうし、この選択には責任を持つつもりだ
    • バージョン範囲を特定することはできるが、そうした手段はすでに無数にある
      ブラウザは継続的に機能を追加しバグを修正しており、その大半は JavaScriptで検出 できる
    • 内容が事実なら誰が書いたかは関係なく、LLMの中核的な主張 も妥当だ
  • あらゆるフィンガープリンティング手法を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/ に追加されて、自分の数学関数の結果がより大きな母集団の中でどれだけ固有か確認できるようになるといい

    • この会社は、Chromiumの 550個以上のC++ファイル で4,000以上のシグナルをパッチしたと主張している
      本当かどうかは分からないが、coveryourtracks.eff.org が使うシグナルは25個くらいではないかと思う
  • 記事には Claudeが書いた感じ が残っている

    • 記事上部のAI要約リンクは、選択したAIプロバイダに記事要約だけでなく製品広告まで頼んでいる点がひどい
      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/>;) というプロンプトが渡される
    • タイトルの発見は興味深いが、残りは実質的に Claudeが書いた内容
    • ここ数か月HNをあまり見ていなかったが、コミュニティがコンテンツを低品質と決めつけて LLM利用を検知 することに偏執的なほど執着しているように見える
  • Tor BrowserとMullvad Browserも結局OS隠蔽を諦めたが、ひょっとすると諦めるべきではなかったのかもしれない
    それだけ フィンガープリンティングの経路 が多すぎるように見える

    • OSを隠すこと自体が可能かもはっきりしないので、正しい判断だったと思う
      ブラウザ内外でOSごとの差異が多すぎて全部に対処するのは難しい。Canvasの読み出しをブロックしたりノイズを加えたりしてもレンダリング差が露出しうるし、Tor Browserの開発者も、完全に別のOSどころか X11とWayland の違いすら隠せないと確認している。https://forum.torproject.org/t/linux-is-it-alright-to-run-th...
    • Tor Browserは 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命令自体もフィンガープリンティングされうる

    • x87 FPUは超越関数を マイクロコード で実装していたが、ほとんどの命令セットはそれを提供していない
      マイクロコードは分岐予測のような利点を得られないため、実際にはソフトウェア実装より遅い
    • 記憶が正しければ、ChromeはJITされていないモードでNaN値の 未使用ビット を保持する唯一のブラウザで、コードがJITされるとそのビットは0になる
  • この戦いに勝てるのか疑問だ
    十分に多くの関数を実行すれば、実行時間の比率と丸め結果を組み合わせてOSや正確な機種だけでなく、同じマシン上で動いている他の作業まで推定できそうだ。完全に防ぐより、少し難しくする程度しかできないように思える
    結局は社会と法が追いつくしかない。ドアの鍵が侵入を完全には防げなくても社会的非難や刑事罰が補うのと同じように、こうした 個人追跡を違法化 し、その利益を利用する企業やそこで働く人を社会的に排斥すべきだ

    • サイバー空間では、違法な、あるいは違法であるべき行為をする者が、執行不能な法域にいることが多い
      ロシア・ミャンマー・北朝鮮のような場所では 法の支配が機能しておらず、外国人をだます犯罪者を現地当局が積極的に保護することさえあるため、ドアの鍵の比喩は成り立たない