eval と new Function を含むコードの VM 難読化
VM 難読化が動的なコード構築(直接 eval と Function コンストラクタ)をどう扱うか、何がバイトコード化され何がスキップされるか、難読化ツールが発行する警告、そして実行時の ReferenceError を診断する方法を説明します。
なぜこれが重要なのか
VM 難読化は、関数本体を、ランタイム内のインタプリタを通じてディスパッチされるバイトコードにコンパイルします。周囲の
スコープ内の識別子もリネームされます。この両方の変換は、実行時に文字列から構築されるコード(eval(s)、
new Function(...s)、Function(...s))とは相性が悪くなります。実行時に構築されたコードが、難読化ツールが
リネームした識別子を参照している場合、生成された関数が最初に実行された時点で Uncaught ReferenceError: <renamed-name> is not defined が
発生します。
難読化ツールは各パターンを異なる方法で扱います。以下の対応表は短縮版です。このページの残りの部分では、 各セルを説明します。
難読化ツールの動作の概要
| ソース内のパターン | 何が起こるか |
|---|---|
eval('literal string')(本体が文字列リテラル) | 正しく動作します。この呼び出しを含む関数は VM バイトコード化を失います(直接 eval は周囲のローカル変数を読み取りますが、関数が一度バイトコードにコンパイルされると VM はそれを保持しないためです)。 |
eval(dynamicExpression) | 実行時に ReferenceError でクラッシュする可能性があります。 この呼び出しを含む関数 — およびその内部で定義されたすべての関数 — も VM バイトコード化を失います。 |
(0, eval)(s) / window.eval(s)(間接) | 正しく動作します。この呼び出しを含む関数は通常どおり VM バイトコード化されます。間接 eval は周囲のローカル変数を見られないため、リネームがそれを壊すことはありません。 |
new Function('a', 'b', 'return a + b')(すべての引数が文字列リテラル) | 正しく動作します。この呼び出しを含む関数は通常どおり VM バイトコード化されます。 |
new Function(dynamicBody) / Function(dynamicBody) | 実行時に ReferenceError でクラッシュする可能性があります。 この呼び出しを含む関数 — およびその内部で定義されたすべての関数 — も VM バイトコード化を失います。 |
これらのパターンはすべて、難読化結果に致命的でない警告としても表面化します。警告の形式と CI 用のスニペットについては、以下の 実行前に問題を検知するを参照してください。
なぜ静的と動的が異なる扱いになるのか
eval(s) は、それが呼び出された関数からローカル変数を読み書きできます。s が文字列リテラルの場合、
難読化ツールは難読化時に本体を解析し、周囲のコードと整合するように識別子をリネームできます。s が動的な式の場合、
解析は実行時にのみ行われます。その時点では識別子はすでにリネームされているため、実行時に構築されるソースは
もはや存在しない古い名前を参照してしまいます。
new Function(s) は異なる動作をします。本体は常に、あなたのファイルの先頭で定義されたかのように実行され、
グローバル変数のみにアクセスでき、呼び出し周辺のローカルには決してアクセスしません。それ自体は安全です。しかし、リネームされた
識別子を本体に連結して構築する場合(例えば、難読化ツールが内部を書き換えた関数の func.toString() を経由して)、
実行時にコンパイルされる関数はやはり同じ種類の ReferenceError に遭遇します。
静的な new Function('return 42') にこのリスクはありません。本体はリネーマが決して検査しない単純な文字列であり、
実行時にはグローバルを見るだけで済みます。難読化ツールは呼び出しをそのまま残し、周囲の関数は依然として VM バイトコード化の
対象になります。
実行時に表示されるエラー
よくある症状は、動的に構築された関数が最初に実行されたときの ReferenceError です:
ここでの TU は、難読化ツールがバンドルの IIFE スコープ内に導入したリネーム済みの識別子です。動的な eval /
Function コンストラクタの呼び出しは、それを参照する本体を評価しますが、その本体は TU が未定義のスコープで実行されます。
実行前に問題を検知する
難読化ツールは API を通じて致命的でない警告を発行するため、これらのパターンを出荷前に CI で捕らえられます。関連する 警告タイプは 2 つあります:
DynamicCodeRenameRisk— 関数に、本体が実行時に構築される動的なeval/new Function/Function呼び出しが含まれています。VMDynamicCodeSkipped— 上記パターンのいずれかにより、関数の VM バイトコード化がスキップされました。関数名(利用可能な場合)と、スキップを引き起こした構文の種類が含まれます。
ある警告タイプがビルドで想定内であり、CI でフィルタするよりもソースの段階で抑制したい場合は、
warnings オプション(v7.8.0 以降)が getWarnings() の発行内容を制御します。'none' はすべてを抑制し、
{ VMDynamicCodeSkipped: false } のようなタイプごとのマップは、他を残したまま 1 つのタイプだけをミュートします。
回避策
間接 eval(
(0, eval)(s))に切り替えるevalのケースにのみ有用です。間接 eval はグローバルスコープで実行されるため、周囲のローカル変数を見られません。 しかしその理由により、リネームされた識別子も参照できません。呼び出しを含む関数は VM バイトコード化されたままになります。本体を完全に静的にする
new Functionについては、本体を補間のない単一の文字列リテラル / テンプレートリテラルとして表現できる場合、 その呼び出しにリネームのリスクはなく、周囲の関数も依然としてバイトコード化されます。new Function('a', 'b', 'return a + b')は問題ありませんが、new Function('return ' + expr)はそうではありません。呼び出しを独自のトップレベル関数に移し、IIFE をアンラップする
各トップレベル関数は独立してチェックされます。動的コード構築の呼び出しを独自のトップレベル関数に 持ち上げれば、バンドル全体をラップするトップレベルの IIFE を通じてスキップが連鎖する代わりに、その 1 つの 関数だけが VM バイトコード化を失います。
vmTargetFunctionsMode: 'comment'に切り替えるオプトインモード:
/* javascript-obfuscator:vm */でマークされた関数だけがバイトコード化されます。動的コード 呼び出しを含む関数への注釈をスキップし、残りをバイトコード化します。 対象関数の指定 を参照してください。vmForceCompileDynamicCode: trueでスキップを上書きする(v6.14.0 以降)最終手段のエスケープハッチです。有効にすると、難読化ツールは周囲の関数をとにかくバイトコード化し、
VMDynamicCodeSkipped警告を抑制します。実行時に構築される本体が、難読化ツールがリネームする識別子を 決して参照しないと保証できる場合にのみ使用してください。そうでない場合、クリーンな難読化と引き換えに実行時のReferenceErrorを招きます。DynamicCodeRenameRiskは依然として発動するため、CI はそれをゲートに使い続けられます。ダッシュボードでは、これは VM セクションの Overrides グループにある「Force Compile Dynamic Code」スイッチです。
関連ページ
- VM 難読化 — 直接 eval の挙動 — スキップの影響範囲を限定する IIFE のアンラップパターン。
- VM 難読化 — 対象関数の指定 —
vmTargetFunctionsModeを使って関数単位でオプトイン / オプトアウトする方法。
