LLM の解析から関数名を隠す
問題
vmObfuscation: true を有効にして、validateLicense のような関数を含むファイルを難読化したところ、難読化された出力に validateLicense という文字列がそのまま残っていることに気づいたとします。本体は消えてバイトコードに置き換えられていますが、名前そのものは誰にでも見える状態で残っています。
これを実際のコードベース全体に当てはめると、validateLicense、decryptPayload、processPayment、checkSubscription のような関数名の一覧ができあがります。LLM はプログラムの動作を理解するためにバイトコードを解読する必要はありません。名前だけで、モジュールの振る舞いについて自信に満ちた正確な要約を作れてしまいます。バイトコードは不透明でも、目次は丸見えなのです。
VM 難読化がこれらの名前を残す理由
vmTargetFunctionsMode: 'root'(デフォルト)では、難読化ツールはルートレベルのすべての関数の本体を VM バイトコードに変換しますが、名前は意図的にそのまま残します。ルートレベルの関数宣言は、意味的には周囲のスコープへの束縛です。スクリプトであればグローバルオブジェクト、モジュールであればモジュールスコープへの束縛になります(エクスポートされていないトップレベルの関数はグローバルではなくモジュールスコープですが、ファイル内ではやはりルートレベルです)。別のバンドル、インラインの <script>、HTML の onclick="validateLicense(...)" 属性、動的な window['validateLicense'] の参照など、他に誰がその関数を参照しているかを知る手段がないため、難読化ツールは安全にリネームできません。
つまり、デフォルトが選んでいるトレードオフは「実装を保護し、公開されている部分は維持する」というものです。これにより連携は壊れませんが、LLM はすべてのエントリーポイントの索引を無償で手に入れることにもなります。この場合、難読化の結果には VMGlobalFunctionNamesNotRenamed 警告が出力され、読める状態で残った名前が一覧表示されます。
LLM を使ったリバースエンジニアリングで問題になる理由
数百行のバイトコードのディスパッチを前にした人間の攻撃者は、たいてい諦めます。同じファイルを渡された LLM は、そもそもバイトコードを攻略しようとはしません。名前を読み、見えている少数の文字列リテラルと照らし合わせて、次のような出力をします。
「このモジュールは有料機能へのアクセスを制御しています。validateLicense は署名付きトークンを検証し、checkExpiry は期限切れのライセンスを拒否し、activateFeature はチェックに通ると UI のロックを解除します。復号処理のヘルパーは decryptPayload にあります。」
この要約があれば、攻撃者は VM に一切触れることなく、狙いを定めた回避策を計画できます。漏洩しているのは名前なのです。
解決策:コードを IIFE でラップする
この漏洩をなくす最も簡単で確実な方法は、機密性の高い関数をスコープツリーの 1 段深い位置に移すことです。別の関数の内部で宣言された関数はルートレベルではないため、難読化ツールは自由にリネームでき、その宣言を他の文と同様にバイトコードに取り込めます。
IIFE(即時実行関数式)は、それを実現する最も軽量な方法です。一度だけ実行され、名前で何も公開しないラッパー関数を 1 つ追加するだけです。
変更前:名前が露出している
VM 難読化の後も、validateLicense と checkExpiry の両方が名前のまま出力に残ります。
変更後:名前が IIFE の内側に隠れている
これで、両方の関数宣言は IIFE の本体の中にあります。ルートレベルにあるのは IIFE 自体だけで、無名の IIFE には漏洩する名前がありません。VM 難読化の後、関数名はリネームされ、本体はバイトコードに変換されます。
変更点。 関数はグローバルとしては参照できなくなりました(名前が漏洩していた原因はこれです)。関数は引き続き呼び出せますが、同じ IIFE の内側からに限られます。外部から validateLicense を本当に呼び出す必要があるものがあった場合、それはもう関数に到達できません。下記のトランポリンパターンを参照してください。そのようなものがなければ、失うものは何もありません。
エクスポートされた名前、予約済みの識別子、文字列、外部から観測できる動作は、引き続き見える場合があります。ラップした後は連携部分をテストしてください。特に、グローバル、モジュールのエクスポート、関数名を調べるコードに注意してください。
知っておくべきトレードオフが 1 つあります:IIFE の本体に直接の eval、または動的な本体を持つ new Function(...) / Function(...) の呼び出しが含まれていると、実行時に組み立てられるソースが難読化ツールによってリネームされた識別子を参照する可能性があるため、VM 難読化はその関数全体とその内部にネストされたすべてを対象外にします(VMDynamicCodeSkipped 警告)。その場合、IIFE の内側に移したばかりのコードは通常の難読化に戻り、バイトコードによる保護を失います。間接 eval((0, eval)(...))を使うか、選択肢については 直接 eval() による VM 難読化の無効化 を参照してください。
関数がどうしてもグローバルである必要がある場合
インラインのイベントハンドラー、JSONP コールバック、サードパーティ SDK のフックなど、関数が本当に公開エントリーポイントである場合もあります。その場合の選択肢は 2 つです。
薄いトランポリンを公開し、ロジックは IIFE の内側に置く。 IIFE のスコープ内にある実装を呼び出すだけの、小さなグローバルのラッパーを宣言します。トランポリンの名前は漏洩しますが、意味のある情報は含まれません(
__entry1のような名前にします)。意味のあるロジックはすべて隠れたままです。呼び出し箇所を書き換える。 インラインの
onclick="validateLicense(...)"が必要とするためだけにグローバルが存在している場合は、そのインラインハンドラーを IIFE の内側からのaddEventListenerに置き換えます。HTML が関数名を参照しなくなり、関数をグローバルにする必要もなくなり、漏洩は完全になくなります。
トップレベルの変数の初期化式:vmWrapTopLevelInitializers
ファイルのルートに置かれるのは関数宣言だけではありません。トップレベルの変数の初期化式(文字列定数、設定オブジェクト、ルックアップテーブル)も、プレーンな JavaScript のまま残っていれば、出力内で同じように読めてしまいます。const API_BASE = '/api/v2/license' のような 1 行は、function validateLicense と同じくらい多くのことを LLM に伝えます。
vmWrapTopLevelInitializers オプション(ブール値、デフォルトは false。現在の VM プリセットでは有効)は、対象となるトップレベルの初期化式を IIFE でラップします。これにより、値そのものがリテラルとしてソースに残るのではなく、実行時に VM バイトコードによって計算されます。root モードでは、プレーンな JavaScript のまま残った初期化式は VMTopLevelInitializerNotVirtualized 警告で報告されます。
オプションなしの場合
vmWrapTopLevelInitializers: true の場合
束縛名(MY_STRING)は、関数名と同じ理由でルートレベルに残ります。ファイルの外部から参照されている可能性があるからです。しかし、それが保持する値は VM によって生成されるようになり、読めるテキストとしては出力に現れなくなります。
このオプションが有効になるのは、vmTargetFunctionsMode が 'root'(デフォルト)で、かつ vmAsyncExecutor がオフの場合のみです。comment モードでは、このオプションは効果がなく、警告も報告されません。root モードで vmAsyncExecutor(async 関数のみを仮想化します)が有効な場合も効果はなく、プレーンな JavaScript のまま残った初期化式についてビルドが VMTopLevelInitializerNotVirtualized を報告します。
ファイルに VM が仮想化できる関数がない場合、結果には VMNoFunctionsToVirtualize が報告されます。保護したいコードを関数でラップするか、comment モードを使って機密性の高い関数を明示的に選択してください。
これだけでは不十分な場合
- 他のモジュールからインポートされた名前。 複数のファイルをバンドルし、あるモジュールが別のモジュールのインポート用に
validateLicenseをエクスポートしている場合、ルートレベルの関数と同様に、バンドラーはその名前をバンドル後の出力に見える形で残します。バンドル自体を IIFE でラップするか(ほとんどのバンドラーで可能です)、エクスポートを IIFE の内側に移し、意味のないトランポリン経由で再公開してください。 - 実行時の動作は依然として観測できる。 難読化は、コードを理解し改変するのに必要な労力を増やしますが、リバースエンジニアリングが不可能であることを保証するものではありません。秘密情報や、最終的な判断を下すセキュリティ上の処理はサーバー側に置いてください。
最初に renameGlobals に頼らないでください。 このオプションは存在し、ルートレベルの識別子をリネームします。しかし、それらの識別子のうちどれがファイルの外部(他のバンドル、インライン HTML、動的な参照)から参照されているかを知る手段がありません。有効にすると、多くの場合、連携が気づきにくい形で壊れます。IIFE でのラップのほうが安全です。グローバルは何もリネームせず、不要なグローバルを作らなくなるだけだからです。
