FlowingDev

JavaScriptの難読化:コードを白昼堂々と隠す技術

JavaScriptの難読化が、人間が読めるコードをいかにして不可解なパズルに変え、知的財産を保護しリバースエンジニアリングを抑止するのかを学びましょう。

ツールを試す: JavaScript 難読化ツール

一言で言うと

JavaScriptの難読化とは、ブラウザでの実際の動作を変えることなく、ソースコードを意図的にごちゃごちゃにして、人間が理解するのをめちゃくちゃ困難にするプロセスのことです。

これが解決する問題

ソフトウェアの広大な世界には、主に2つの陣営があります。コンパイル言語とインタプリタ言語です。C++やJavaを書くとき、コードをコンパイラに通します。この魔法の箱は、人間が読めるソースコードをむしゃむしゃ食べて、バイナリファイルを吐き出します。これはコンピュータのプロセッサだけが愛せる機械語の塊です。あなたはこのバイナリを出荷し、あなたのオリジナルのソースコード、つまり「秘伝のタレ」は、あなたのハードドライブに安全に保管されます。

そしてJavaScriptがあります。Webの共通語として、これはインタプリタ言語です。別のバイナリを出荷するコンパイラはありません。ソースコードそのものが出荷物なのです。それはユーザーのブラウザに直接送られ、ブラウザがその場で読み込んで実行します。これはオープン性やデバッグにとっては素晴らしいことです。好奇心旺盛な開発者なら誰でも、右クリックして「ページのソースを表示」を選べば、ウェブサイトがどのように機能しているかを正確に見ることができます。

でも、もしあなたが、その仕組みを人々に見られたくないとしたらどうでしょう?

もしあなたのJavaScriptに、金融モデリング用の独自のアルゴリズムが含まれていたら? チーターに悪用されたくないブラウザベースのゲームのコアロジックが含まれていたら? 競合他社に自社製品にコピペされたくないキーやビジネスロジックが含まれていたら?

それが難読化が取り組む問題です。オープンな世界のための防御メカニズムなのです。それはあなたのクリーンで、コメントがあり、論理的に構造化されたJavaScriptを、まるでエイリアンがひどいバッドトリップ中に書いたかのような、もつれた混乱状態に変えます。目的はコードを小さくすること(それはミニフィケーションです)や、本当に安全にすること(それは暗号化です)ではありません。リバースエンジニアリングしようとする者を心底うんざりさせて、諦めてもっとやりがいのあること、例えばボックスシーツをきれいにたたむ、とかをやりたくなるほどに、読むのを面倒にさせることが目的なのです。

内部の仕組み

難読化は単一のテクニックではなく、それらをカクテルのように混ぜ合わせ、幾重にも重ねて手強いパズルを作り出すものです。優れた難読化ツールは、まるで疑り深いシェフのようです。材料をみじん切りにするだけでなく、すべての瓶のラベルを貼り替え、キッチンの配置を変え、レシピを盗もうとする者を混乱させるために偽の調理器具をいくつか追加するようなものです。

識別子の名前変更

これが最も基本的な層です。難読化ツールは、あなたが愛情を込めて作った calculateTotalPrice や userProfile のような変数名、関数名、パラメータ名をすべて見つけ出し、無意味で短い名前に置き換えます。

難読化前:

function calculateTotalPrice(items, taxRate) {
  let subtotal = 0;
  for (const item of items) {
    subtotal += item.price;
  }
  return subtotal * (1 + taxRate);
}

難読化後:

function _0x2a1b(_0x5c4d, _0x3e8f) {
  let _0x1f9a = 0;
  for (const _0x4b2c of _0x5c4d) {
    _0x1f9a += _0x4b2c.price;
  }
  return _0x1f9a * (1 + _0x3e8f);
}

ロジックは同じですが、自己文書化のヒントはすべて消え去りました。まるで街からすべての道路標識を取り除いたようなものです。まだ移動はできますが、地図と多大な忍耐が必要になります。

文字列のエンコーディング

文字列は、コードを覗き見する者にとって、しばしば一番おいしいターゲットです。エラーメッセージ、UIのテキスト、URL、APIキーなどが含まれていますからね。文字列のエンコーディングは、これらのリテラル文字列をすべてコードから引き剥がして隠します。

一般的な方法は、Base64や16進数値でエンコードされた文字列の大きな共有配列を作成することです。そして、元の文字列リテラルは、実行時に配列から正しい文字列を取得してデコードする関数呼び出しに置き換えられます。

難読化前:

function showMessage(type) {
  if (type === 'success') {
    console.log("Operation successful!");
  } else {
    console.log("Error: Something went wrong.");
  }
}

難読化後:

// 難読化ツールによって追加された、簡略化されたデコーダと文字列配列
const _0xdead = ['0x4572726f723a20536f6d657468696e672077656e742077726f6e672e', '0x4f7065726174696f6e207375636365737366756c21'];
const _0xbeef = function(i) {
  // 実際には、この関数はもっとずっと複雑です
  return decodeURIComponent(
    _0xdead[i].replace(/0x/g, '%')
  );
};

function showMessage(type) {
  if (type === 'success') {
    console.log(_0xbeef(1)); // "Operation successful!"
  } else {
    console.log(_0xbeef(0)); // "Error: Something went wrong."
  }
}

これで、「Error」や「API_KEY」でテキスト検索しても、何もヒットしなくなります。攻撃者は、隠されたテキストを見るためだけに、まず _0xbeef デコード関数がどのように機能するかを解明しなければなりません。

制御フローの平坦化

ここからが本当に頭がクラクラするところです。制御フローの平坦化は、if、else、for、while といったコードの自然で直線的な流れを破壊し、はるかに複雑なものに置き換えます。

元のコードのさまざまなブロックをバラバラの断片に分解します。そして、それらの断片をすべて、巨大な switch 文を持つ単一の巨大な while ループの中に放り込みます。次にどのコード片を実行するかを決定するために、「状態変数」が使用されます。かつては簡単に追えた論理的な流れは、今やバラバラになり、一見ランダムに見える数値の割り当てによって決定されるようになります。

難読化前:

function greet(name) {
  let greeting = "Hello, ";
  if (name) {
    console.log(greeting + name);
  } else {
    console.log("Hello, world!");
  }
}

難読化後(概念的な簡略化):

function greet(name) {
  let state = '1';
  let greeting;
  while (true) {
    switch (state) {
      case '1':
        greeting = "Hello, ";
        state = name ? '4' : '2';
        continue;
      case '2':
        console.log("Hello, world!");
        state = '3';
        continue;
      case '3':
        return; // ループの終わり
      case '4':
        console.log(greeting + name);
        state = '3';
        continue;
    }
    break;
  }
}

2番目の例の実行パスをたどろうとすると頭が痛くなります。上から下へ読むだけではダメなのです。熱い鉄板の上のカエルのように switch 文の中を飛び回り、各ステップで state 変数を追跡しなければなりません。このテクニックだけで、手動での解析は悪夢と化します。

実録、こんな話

スタートアップの「秘伝のタレ」

データサイエンティストの小さなチームが、医療画像を分析するための素晴らしいブラウザ内ツールを構築しました。JavaScriptで書かれた彼らのユニークなアルゴリズムは、他のツールが見逃すパターンを検出できました。彼らはまだ収益化前で、特許も取得していませんでした。ローンチ当日、彼らは、資金力のある大きな競合他社が開発者ツールを開き、コアの .js ファイルをコピーし、1週間以内にそのロジックを自社製品に統合できてしまうことを知っていました。時間を稼ぐため、彼らは本番コードを、識別子の名前変更、文字列のエンコーディング、そして積極的な制御フローの平坦化を駆使する強力な難読化ツールにかけました。これにより、国家レベルの執拗な攻撃者を止めることはできませんでしたが、コードが非常に読みにくくなったため、気軽な企業スパイ活動は選択肢から外れました。

教訓: 難読化は「市場先行の盾」として機能し、あなたが足場を固めるのに十分な期間、知的財産を保護することができます。

オンラインゲームのチーターたち

あるインディー開発者が人気のHTML5マルチプレイヤーゲームをローンチしました。数日も経たないうちに、リーダーボードはありえないスコアのプレイヤーで埋め尽くされました。開発者が調査したところ、ユーザーがチートスクリプトを共有しているフォーラムを発見しました。彼らはゲームのJavaScriptを読み解き、player.health = 100 のような変数や addScore(10) のような関数を見つけていたのです。チーターたちは単純にブラウザのコンソールを開き、player.health = 999999 とタイプしていました。開発者の次のアップデートには、難読化されたコードが含まれていました。変数 player.health は _0x5abf['h'] となり、ロジックはステートマシンに平坦化されました。チーターたちが次にコードを見たとき、彼らを待っていたのは意味不明な文字列の壁で、ゲームの状態を見つけて悪用するのが指数関数的に難しくなりました。

教訓: 難読化は、ウェブベースのゲームにおけるアンチチート開発というイタチごっこにおいて、極めて重要なツールです。

ウェブスクレイパーの天敵

商品データを集約するeコマースサイトが、自社のサーバーがボットに叩かれまくっていることに気づきました。これらは単にHTMLページを叩くような単純なボットではありませんでした。サイトのフロントエンドJavaScriptをリバースエンジニアリングした、洗練されたスクレイパーだったのです。彼らは内部APIのエンドポイント /api/v2/getProductDetails を見つけ出し、フロントエンドのトラッキングやレート制限をすべて迂回して直接呼び出していました。セキュリティチームは、API呼び出しを担当するJavaScriptコードを難読化することで対抗しました。文字列 /api/v2/getProductDetails はエンコードされ、APIリクエストを構築するロジックは平坦化されました。その特定のエンドポイントを探すようにハードコードされていたスクレイパーたちは、突如として失敗し始めました。

教訓: 難読化は、クライアントサイドのロジックだけでなく、フロントエンドがバックエンドと通信するために使用するパターンやエンドポイントを隠すためにも使用できます。

よくある間違いと落とし穴

  • セキュリティだと思い込むこと。 難読化は暗号化ではありません。「不明瞭さによるセキュリティ」です。十分に意欲的で熟練した人物であれば、あなたのコードを非難読化することができます。これは抑止力であり、レンガの壁ではなく、スピードバンプ(減速帯)です。プライベートなAWSキーやデータベースのパスワードのような秘密情報を、どれだけ強力に難読化しようとも、クライアントサイドのJSに決して、絶対に、置いてはいけません。
  • ミニフィケーションと混同すること。 ミニフィケーションの目的は、ダウンロードを速くするためにファイルを小さくすることです(例:calculateTotalPrice が a になる)。難読化の目的は、コードを理解しにくくすることです。識別子の名前変更のように一部のテクニックは重なりますが、制御フローの平坦化のような機能を持つ強力な難読化は、ほとんどの場合、コードをより大きく、そして実行をより遅くします。
  • 元のソースコードを失うこと。 難読化されたコードベースをまともにデバッグしたり保守したりすることはできません。それは一方通行です。難読化されたコードは、コンパイルされたバイナリと同じように、常にビルド成果物として扱ってください。あなたの元の、クリーンでコメント付きのソースコードは金(きん)です。Gitのようなバージョン管理システムで安全に保管しましょう。
  • ソースマップを忘れること。 難読化された本番コードでエラーが発生すると、スタックトレースは _0x2a1b at line 1, column 5421 のようなものを指し示します。これではデバッグの役には立ちません。ソースマップは、難読化されたコードを元のソースにマッピングする特別なファイルです。エラー監視サービスにアップロードしたり、ブラウザの開発者ツールで使ったりすることで、エラーが発生した場所の、本物の読みやすいコードを、一般に公開することなく確認できます。

なぜ頭の片隅に置いておくべきか

あなたが価値ある資産だと考えるクライアントサイドのコードを書いているときはいつでも、JavaScriptの難読化について考えるべきです。すべてのプロジェクトに必要なわけではありません。あなたの個人ブログや、単純なカタログサイトには不要です。

しかし、もしあなたが商用製品、ゲーム、プロプライエタリなライブラリ、ライセンスロジックを含むツール、あるいは「秘伝のタレ」がユーザーのブラウザ内に存在する何かを構築しているなら、難読化は本番ビルドプロセスの標準的な一部であるべきです。誰かがあなたの成果物を盗んだり、エクスプロイトを見つけたりするために必要なコストと労力を増やすための、現実的な一歩なのです。

もっと深く

  • Wikipedia: Obfuscation (software) - JavaScriptだけでなく、コンピュータサイエンス全般におけるこの概念についての、素晴らしい学術的な概観。
  • OWASP: Reverse Engineering and Obfuscation Guide - Open Web Application Security Projectによる、セキュリティ文脈における難読化の役割についての見解。
  • JavaScript Obfuscator Tool - 人気のあるオープンソースの難読化ツールのホームページ。そのドキュメントは、さまざまなテクニックとそのトレードオフについて、素晴らしく実践的な視点を提供しています。
  • Understanding JavaScript Source Maps - 難読化されたコードのデバッグに不可欠なツールであるソースマップに関する、Googleの公式ドキュメント。

理論はOK。さあ手を動かそう — 100%ブラウザ内で。

ツールを試す: JavaScript 難読化ツール