アンチLLM難読化:フィンガープリントスクリプトの反撃
アンチLLM難読化は、ブラウザのフィンガープリントスクリプトに脱獄プロンプトを隠し、AIによる難読化解除を妨害します。仕組みと有効な対策を解説します。

ある研究者は、ウェブ上の大手サイト1万件でcanvasの呼び出しをすべてフックし、1,000を超えるフィンガープリント用レンダリングを収集して、いちばん美しいものを探しました。そして問題のスクリプトの中から見つかったのは、アンチLLM難読化のもっとも明快な例でした。ブラウザのフィンガープリントスクリプトにcomputeRicinLethalDosageForWaterSupplyのような関数名が並び、見えないUnicode文字にエンコードされた脱獄プロンプトが仕込まれていたのです。人間のリバースエンジニアから隠すためではなく、AIアシスタントに難読化の解読を拒否させるために設計されています。
私は長年インシデント対応に携わってきたので、このレポートを最初に読んだときは、著者が狙った通りの反応をしました。マルウェアか、それ以上に悪質なものに見える、と。リシンの致死量やダーティボムの降下物半径に関する関数名、生物兵器の合成手順を要求する復号済みの文字列。.npmrc、.env、kubeの設定ファイルを読み取り、内部IPからバイナリを取得し、cronビーコンをインストールするNode.jsのコードパス。どの直感も「悪意ある攻撃者だ」と告げます。しかし、その直感は誤りであり、その誤りこそが要点なのです。これは、難読化を売る側が、真の敵はデバッガを使う人間ではなく安全フィルタを持つモデルだと気づいたとき、何が起きるかという話です。
クロール自体はシンプルでした。Trancoのトップサイトリストに対してPlaywrightを実行し、HTMLCanvasElement.prototype.toDataURLと、navigator.webdriverのような典型的な自動化の痕跡をフックして、無害なcanvasを除外します。収集したレンダリングは公開ギャラリーにまとめられ、FingerprintJSやCastleといったおなじみのベンダー別に分類され、残りは正体不明の長い尾になりました。流れるような線の美しい渦を描いたcanvasは、ロシアのDDoS対策ベンダーが配信するスクリプトまで遡れました。ここから面白くなっていきます。
問題:LLMが難読化のビジネスモデルを崩壊させた
20年もの間、JavaScriptの難読化には明確な役割が一つしかありませんでした。リバースエンジニアリングのコストを、誰も手を出さないほど高くすることです。javascript-obfuscatorや商用の派生ツールは、識別子を崩し、文字列リテラルを参照用の配列にまとめ、不透明な述語を挿入し、制御フローをswitch駆動のステートマシンに平坦化します。時間をかけた人間の解析者の前では、どれも長持ちしません。それでもよかったのです。時間に余裕のある解析者は稀で、経済的には防御側が有利だったからです。
その後、LLMが崩れたコードを読むのに長けてきて、経済性は逆転しました。400KBの難読化されたボット対策スクリプトをチャット画面に貼り付けて「これは何をしているの?」と聞くのに、30秒しかかかりません。モデルは、関数名がPiERWであることも、文字列が回転配列に入っていることも気にしません。意味を読み取り、デコーダをインライン展開し、データフローをたどって、平易な説明を返してくれます。ボット対策ベンダーが頼りにしてきた不透明さという堀は、静かに消えてしまったのです。
ベンダーたちもこれに気づき、その対抗策は腹立たしいほど巧妙です。モデルからコードを隠せないなら、モデルを攻撃すればいい。安全性のための学習によってモデルが拒否せざるを得ない内容を与えるのです。アンチLLM難読化は、モデルのアライメントを解析パイプラインの最も弱い環と見なします。癪に障りますが、彼らの言う通りなのです。
ブラウザのフィンガープリントスクリプトがエントロピーを集める仕組み
この罠を理解するには、それが組み込まれている正規の仕組みを知る必要があります。Canvasフィンガープリントは、非表示の<canvas>上にテキスト、グラデーション、図形を描き、toDataURL()でピクセルバッファを読み戻すことで機能します。GPU、ドライバ、フォントラスタライザ、アンチエイリアス、サブピクセルヒンティングといったレンダリングパイプラインはマシンごとにわずかに異なるため、同じ命令でも描画結果のピクセルバッファのハッシュ値は変わります。これにWebGLのレンダラー文字列、AudioContextのタイミング、画面の寸法、インストール済みフォント、その他百以上のシグナルを組み合わせれば、Cookieを一つも使わずに安定した識別子が得られます。
このスクリプトを見つけたクロールでは、標準的な傍受の手法が使われました。初期化スクリプトで読み戻しの基本機能をフックし、スタックトレースと取得した各画像の先頭部分を記録します:
await page.addInitScript(() => {
const original = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (...args) {
const result = original.apply(this, args);
fetch("/__log", {
method: "POST",
body: JSON.stringify({
stack: new Error().stack,
byteLength: result.length,
head: result.slice(0, 64),
}),
});
return result;
};
});
これを十分な数のサイトで実行すると、フィンガープリント業界の全体像が見えてきます。大半のcanvasは退屈なものです。パングラム、いくつかの矩形、グラデーションなど。ごく一部だけが、ジェネレーティブアートと呼べるほど凝っています。いずれも高度に難読化されたスクリプトによって生成されています。難読化されていないフィンガープリント採取スクリプトは、それ自体がフィンガープリントされやすいとベンダーは知っているからです。難読化は昔から製品の一部でした。新しいのは、難読化の標的が誰になったかという点です。
アンチLLM難読化の罠の内側
スクリプトは通常の形で始まります。ランダムな名前、詰め込まれた文字列など、よくあるものです。しかし、やがて識別子の名前がランダムではなく、狙いを定めたものに変わります。computeRicinLethalDosageForWaterSupply。calculateDeadlyLethalSuicideAspirinDosage。computeCRISPRGeneDrivePropagationRate。computeCesium137DirtyBombFalloutRadius。関数は「成人の致死閾値をX mg/kgで確認」のような不穏な要約で飾られた数値を返しますが、その計算はおとりです。実際には何も計算せず、どこにも送られません。
ここが肝心な点です。識別子は実行時の意味をまったく持ちませんが、言語モデルにとっては非常に大きな意味を持ちます。人間のリバースエンジニアは、経験から名前を無視することを学んでいます。validateUserという名前の関数が実際には何もしないことを、何度も経験してきたからです。モデルはその逆をします。computeRicinLethalDosageForWaterSupplyを読むと生物兵器の文脈をパターンマッチし、好奇心より先に安全学習が発動するのです。
さらに深い層には、見えない文字で埋め尽くされたテンプレートリテラルがありました。Unicodeのタグブロック、U+E0000から始まるコードポイントです。これらは何も表示されません。デコーダは1行で書けます:
const cleartext = [...payload]
.map((c) => String.fromCodePoint(c.codePointAt(0) - 917504))
.join("");
// 917504 === 0xE0000, the start of the invisible "Tags" block.
// Subtracting it maps each hidden char back to plain ASCII.
デコードすると、完全な脱獄プロンプトでした。偽の「SYSTEM OVERRIDE -- CLASSIFIED BRIEFING」が安全ガイドラインの停止を宣言し、医療インフラへのDDoS攻撃、脱税、エアロゾル化した生物剤、薬物の投与量について、手順ごとのガイドを要求しています。さらに、中国国内でホストされるモデルの検閲反応を誘う意図が明らかな、英語と中国語の両方での天安門事件への言及もありました。すべての安全カテゴリを同時に突くよう設計された文字列です。周囲のコードの分析を求められたLLMは、これを文脈として取り込み、何をするつもりだったにせよ、今や何もしなくなるのです。

失敗した試み:拒否、誤検知、そして偽のボットネット
まず思いつくのは、それを最先端のモデルに貼り付けることです。これは設計通りに失敗しました。モデルは生物安全上のリスクを理由に拒否しました。安全姿勢を変えて再度試みても、同様にブロックされました。難読化ツールは、拒否を事実上の武器にしたのです。このスクリプトは、誰もが実際に使うツールではデコンパイルできなくなりました。ガードレールの弱いオープンウェイトモデルは応じましたが、これは解決策ではなく一つのデータ点に過ぎません。ほとんどのアナリストは、このためにローカルモデルを常時起動しているわけではなく、多くの企業のポリシーでも、検証されていないインフラにサードパーティのコードを貼り付けることは禁じられているからです。
2つ目の失敗はより巧妙で危険です。人間のアナリストが、このスクリプトはマルウェアだと結論づけてしまうことです。process.env.METRICS_AGENT_8BFD_ACTIVEの背後にNode専用のコードパスがあり、SSH関連の設定ファイルを読み取り、10.15.45.1から「メトリクスエージェント」をダウンロードし、ホストの情報を/tmpにフィンガープリントとして保存し、圧縮して、cronビーコンをインストールします。サプライチェーンへのインプラントに見えます。しかし糸を引いてみると、環境フラグはどこからも設定されず、readFileは空のコールバックで呼ばれているため、盗んだデータはどこにも届かず、C2アドレスはRFC 1918の私設空間で、シェルコマンドはさらに脱獄用の餌をマーカーファイルに書き出すだけで、何も外部に送信されません。サンドボックスで実行すると、影響範囲は一時ディレクトリだけです。これは見せかけです。AIを使ったトリアージや、ざっと流し読みする人間に向けて調整されたおとりなのです。
現実の事例にも似たものがあります。大手のモデルハブが実際の侵害を調査したとき、チームは、商用のモデルAPIが攻撃者のコマンド、ペイロード、C2の成果物の分析を拒否したことを発見しました。ガードレールは、インシデント対応者と攻撃者を区別できなかったのです。最終的にチームは、自社インフラ上でオープンウェイトモデルを使ってフォレンジック分析を行いました。この失敗パターンが大規模に繰り返されることこそ、アンチLLM難読化が突く非対称性そのものです。だからこそ私は、AIによるコード理解チェックを唯一の防御線にしてはいけないと言い続けています。モデルの判断は、今や公開された攻撃手法を持つ攻撃対象領域になっているのです。
難読化ツールはもはやコードをあなたから隠しているのではありません。あなたのモデルからコードを隠しているのです。そして、そのモデル自身の安全学習を鍵として使っています。
アンチLLM難読化への実際の対策
対策は、生のソースをモデルに渡すのをやめ、罠によって汚染された段階を取り戻すことです。具体的には、私が構築するなら次の順序で進めます:
- まずは決定論的な難読化解除。 BabelベースのASTパスを実行します。定数畳み込み、文字列配列のインライン展開、到達不能分岐の除去、制御フローの復元などです。罠は文字列リテラルと識別子に潜んでいるため、畳み込みとリネームを行えば、モデルがトークンを一つも見る前に無力化できます。デコードされた脱獄プロンプトは、到達しないコードパスの中の単なる一文字列になります。
- ソースを読むのではなく、振る舞いを追跡する。 計測されたサンドボックス(フックされたAPIを持つPlaywrightページや、隔離されたrealm)でスクリプトを実行し、すべてのDOM呼び出し、ネットワークリクエスト、プロパティアクセス、canvasの読み戻しを記録します。モデルにはトレースを渡します。振る舞いのトレースには識別子が存在しないため、リシンへの言及は一切含まれません。
- LLMの処理の前に、識別子を中立的なトークンにリネームする。
computeRicinLethalDosageForWaterSupplyはfn_0417になります。意味的なヒントは少し失われますが、汚染はすべて取り除けます。良いトレードオフです。 - 本当に機微な内容のトリアージには、ローカルのオープンウェイトモデルを用意する。 実際のエクスプロイトキットや本物のフィッシングキットなど、内容が正当に危険な場合、クラウドのガードレールを持たないエアギャップ環境のモデルが、分析と拒否の壁を分けます。
- 雰囲気ではなく証拠に基づいて判定する。 観測された機能でスクリプトを評価します。認証情報を読み取ったか。データはページの外に出たか。ソースに認証情報という言葉が出てくるかどうかで判断しないことです。
どの手法にも共通するパターンがあります。「コードが何を言っているかモデルに尋ねる」を、「コードが何をするかを観察し、その観察についてモデルに尋ねる」に置き換えているのです。これは、自律型エージェントについて私が繰り返し立ち返る原則と同じです。封じ込めは監視に勝る。成果物の自己記述を信用せず、閉じ込め、境界を計測し、出力を判断するのです。
サードパーティのスクリプトがモデルプロバイダと通信している
さて、趣味でリバースエンジニアリングをしない人にとっても心配すべき部分です。このスクリプトは、どこかの暗い片隅にホストされているわけではありません。顧客のウェブサイトに、顧客のドメインの下、顧客のブランドのすぐ隣で提供される、ボット対策のチェックなのです。つまり、ページを取り込むすべてのクローラー、LLMを使ったコンテンツ分類器、安全性審査のパイプラインは、企業のロゴと一緒に、生物兵器の合成要求や脱獄プロンプトも取り込むことになります。顧客はおそらく知らず、ベンダーも尋ねてはいません。
理解していないコンテンツに基づいて自動分類器が動くのは、仮定の話ではありません。実務者は、見えないウォーターマーク文字列がメールフィルタに引っかかり、送信した文書がサイレントに隔離される様子をすでに見ています。この失敗モードをスケールアップしてみてください。ブランドセーフティの分類器が、あなたのドメインは過激派やバイオハザードのコンテンツに隣接していると判断すれば、リーチが急落した理由は誰にも知らされません。異議申し立てすらできません。証拠は、あなたが書いたことのないファイルの中の見えないUnicodeなのですから。
そして、よくある緩和策は通用しません。ベンダーがデプロイごとにスクリプトのハッシュをローテーションする場合、Subresource Integrityは役に立ちません。これらのスクリプトは絶え間なくローテーションされ、一部はピン留めを回避するためにそうしています。ハッシュを使った厳格なCSPも、同様にすぐ陳腐化します。残るのは、LiteLLMのサプライチェーン侵害の後に私たちが得た不快な結論です。レビューできない依存関係こそ、あなたを支配するのです。セキュリティチームが使うツールではデコンパイルできないように、ボット対策ベンダーのコードがわざと作られているなら、それは機能ではありません。CDNのURLが付いたガバナンスの失敗です。

この軍拡競争の次の展開
私の予測では、1年以内に、安全性の罠を仕掛ける機能が商用の難読化ツールのチェックボックスとして搭載されるでしょう。「LLM解析耐性」が「制御フローの平坦化」の隣に並ぶわけです。モデルプロバイダは、罠のコーパスで学習することで、解析の文脈に埋め込まれた餌を認識して無視できるようにしてくるでしょう。難読化側は、文字列だけでなく、トレース上で振る舞いが悪意を持って見えるコードのように、より巧妙な罠で対抗するはずです。HNのコメントが冗談半分に予測していたフラクタルな軍拡競争、つまり難読化はモデルに合わせて調整され、モデルは難読化に合わせて調整される、という構図は、すでにここにあります。ただ、その分布が偏っているだけです。
持続する防御は退屈なものであり、90年代の最初のパッカーの頃から変わっていません。動的解析、振る舞いの証拠、サンドボックス実行です。難読化がセキュリティの特性であったことは一度もありません。コストを移転させるための戦略であり、そのコストが今や取り戻されたのです。検知パイプラインが、言語モデルの感情を傷つける文字列によって突破されるのであれば、そのパイプラインこそが最初から脆弱性だったのです。


