改竄とリバースエンジニアリング

リバースエンジニアリングや改竄の技法は長い間、クラッカー、MOD作成者、マルウェア解析者などの分野に属していました。「伝統的な」セキュリティテスト技術者や研究者にとって、リバースエンジニアリングはどちらかというと補完的なスキルでした。しかし状況は一変します。モバイルアプリのブラックボックステストではコンパイルされたアプリを逆アセンブルし、パッチを適用し、バイナリコードやライブプロセスを改竄することがますます必要になっています。多くのモバイルアプリが歓迎されない改竄に対する防御を実装しているという事実はセキュリティテスト技術者にとって物事を簡単にしてはくれません。

モバイルアプリのリバースエンジニアリングはコンパイルされたアプリを解析するプロセスであり、そのソースコードに関する情報を抽出します。リバースエンジニアリングの目的はコードを 理解すること です。

改竄 はモバイルアプリ (コンパイルされたアプリまたは実行中のプロセス) またはその動作に影響を与える環境を変更するプロセスです。例えば、アプリはルート化されたテストデバイス上で実行することを拒む可能性があり、一部のテストを実行できなくなる可能性があります。そのような場合には、アプリの動作を変更したいでしょう。

モバイルセキュリティテスト技術者を上手く務めるには基本的なリバースエンジニアリングの概念を理解することが必要です。モバイルデバイスやオペレーティングシステムも十分に知る必要があります。プロセッサアーキテクチャ、実行形式、プログラミング言語の錯綜などがあります。

リバースエンジニアリングは芸術であり、そのすべてのファセットを記述することはライブラリ全体を占めるでしょう。技術と専門化の幅広い領域は驚異的です。マルウェア解析の自動化や新しい逆難読化手法の開発など、非常に特殊で独立した部分問題の取り組みに何年も費やすことがあります。セキュリティテスト技術者はジェネラリストです。有能なリバースエンジニアであるためには、膨大な量の関連情報をフィルタする必要があります。

常に機能する一般的なリバースエンジニアリングプロセスはありません。それでは、よく使われる手法やツールについてこのガイドで後ほど説明し、最も一般的な防御に取り組む例を挙げましょう。

あなたがそれを必要とする理由

モバイルセキュリティテストにはいくつかの理由から少なくとも基本的なリバースエンジニアリングのスキルが必要です。

1. モバイルアプリのブラックボックステストを可能にするため。 現在のアプリには動的解析を妨げるコントロールを含むことがよくあります。SSL ピンニングとエンドツーエンド (E2E) 暗号化によりプロキシを使用したトラフィックの傍受や操作が妨げられることがあります。ルート検出はアプリがルート化されたデバイス上で実行できなくなり、高度なテストツールを使用できなくなる可能性があります。これらの防御を無効にする必要があります。

2. ブラックボックスセキュリティテストの静的解析を強化するため。 ブラックボックステストでは、アプリバイトコードやバイナリコードの静的解析はアプリの内部ロジックを理解するのに役立ちます。また、ハードコードされた資格情報などの欠陥を識別することもできます。

3. リバースエンジニアリングに対する耐性を評価するため。 モバイルアプリケーション検証標準のアンチリバースコントロール (MASVS-R) にリストされているソフトウェア保護対策を実装するアプリはある程度のリバースエンジニアリングに対して耐性を持つ必要があります。それぞれのコントロールの有効性を検証するには、テスト担当者は一般的なセキュリティテストの一部として 耐性評価 を実行します。耐性評価では、テスト担当者はリバースエンジニアの役割を引き受け、防御のバイパスを試みます。

モバイルアプリのリバーシングの世界に飛び込む前に、良いニュースと悪いニュースを共有します。良いニュースから始めましょう。

最終的に、リバースエンジニアは常に勝利します。

これはモバイルの世界では特に真実です。リバースエンジニアは本質的な利点を持っています。モバイルアプリをデプロイおよびサンドボックス化する方法は従来のデスクトップアプリのデプロイメントやサンドボックス化よりも設計上の制約があります。そのため Windows ソフトウェアでよく見られるルートキットのような防御メカニズムを含めること (DRM システムなど) は簡単には実行できません。Android のオープン性によりリバースエンジニアはオペレーティングシステムに有利な変更を加え、リバースエンジニアリングプロセスを支援することを可能にします。iOS ではリバースエンジニアはほとんどコントロールできませんが、防御の選択肢もまた制限されています。

悪いニュースとしては、マルチスレッドでのアンチデバッグコントロール、暗号化ホワイトボックス、隠れた耐タンパ性機能、非常に複雑なコントロールフロー変換を扱うことは容易ではないということです。最も効果的なソフトウェア保護スキームは独自のものであり、標準の微調整やトリックで太刀打ちできないでしょう。それらを打ち破るには、面倒な手動解析、コーディング、フラストレーション、そしてあなたの性格によっては、眠れない夜と緊張状態の関係が要求されます。

初心者にとってリバーシングの膨大な範囲に圧倒されてしまうことはよくあります。始める際の最善の方法はいくつかの基本的なツール (Android および iOS のリバーシングの章の関連するセクションを参照) をセットアップし、簡単なリバーシングタスクや crackme を開始することです。アセンブラやバイトコード言語、オペレーティングシステム、難読化などに遭遇し学ぶ必要があるでしょう。簡単なタスクから始めて、より難しいものへ徐々にレベルアップしていきます。

以下のセクションでは、モバイルアプリのセキュリティテストで最もよく使用される技法の概要を説明します。以降の章では、Android と iOS の両方について OS 固有の詳細を掘り下げていきます。

基本的な改竄技法

バイナリパッチ適用

パッチ適用 とはコンパイルされたアプリを変更するプロセスです。バイナリ実行形式のコード変更、Java バイトコードの改変、リソースの改竄などがあります。このプロセスはモバイルゲームのハッキングシーンで MOD適用 として知られています。パッチは多くの方法で適用できます。16進エディタでのバイナリファイルの編集やアプリの逆コンパイル、編集、逆アセンブルなどがあります。有用なパッチの詳細な例について以降の章で説明します。

心に留めておくものとして、現在のモバイルオペレーティングシステムはコード署名を厳しく強制することがあります。そのため、改変されたアプリを実行することはデスクトップ環境で使用するほど簡単ではありません。セキュリティ専門家は90年代にははるかに簡単な人生を送っていました。幸運なことに、あなた自身のデバイスで作業する場合、パッチ適用はそれほど難しいことではありません。つまり、改変したコードを実行するには、アプリを再署名するか、デフォルトのコード署名検証機能を無効にする必要があるというだけです。

コードインジェクション

コードインジェクションは非常に強力な技法であり、実行時にプロセスを探索および改変できます。インジェクションはさまざまな方法で実装されますが、自由に利用でき十分に文書化されたプロセスを自動化するツールのおかげで、すべての詳細を知らなくても使用できます。これらのツールは、アプリによりインスタンス化されたライブオブジェクトなどの、プロセスメモリや重要な構造体に直接アクセスできます。また、ロードされたライブラリの解決、メソッドやネイティブ関数のフックなどに役立つ多くのユーティリティ関数があります。プロセスメモリの改竄はファイルにパッチを適用するよりも検出が難しく、大半の場合に推奨される方法です。

Substrate, Frida, Xposed はモバイル業界で最も広く使用されているフックとコードインジェクションのフレームワークです。三つのフレームワークは設計の哲学と実装の詳細が異なります。Substrate と Xposed はコードインジェクションやフックに焦点を当てています。一方、Frida は本格的な「動的計装フレームワーク」とすることを目指しており、コードインジェクション、言語バインディング、インジェクト可能な JavaScript VM およびコンソールを組み込んでいます。

それだけでなく、Cycript をインジェクトするために Substrate を使用してアプリを計装することもできます。Cycript は Cydia で有名な Saurik が作成したプログラミング環境 (通称 "Cycript-to-JavaScript" コンパイラ) です。さらに物事は複雑になりますが、Frida の作者も "frida-cycript" と呼ばれる Cycript のフォークを作成しました。これは Cycript のランタイムを Mjølner と呼ばれる Frida ベースのランタイムに置き換えます。これにより frida-core で保守されているすべてのプラットフォームとアーキテクチャで Cycript を実行できます (この時点で混乱しても、心配ありません) 。frida-cycript のリリースには Frida の開発者 Ole によるブログ記事 "Cycript on Steroids" が付いていました。このタイトルは Saurik はあまり好きではありませんでした

三つすべてのフレームワークについて例を紹介します。私たちは Frida で始めることをお勧めします。これは三つの中で最も汎用性が高いからです (このため、Frida の詳細と事例が多く紹介されています) 。特に、Frida は Android と iOS の両方のプロセスに JavaScript VM をインジェクトできます。一方で Substrate での Cycript インジェクションは iOS でのみ動作します。しかし最終的には、いずれのフレームワークでも多くの同じ目標に到達できます。

静的および動的バイナリ解析

リバースエンジニアリングはコンパイルされたプログラムのソースコードの意味を再構築するプロセスです。言い換えると、何をしているのか、どのようにしているのかを理解するために、プログラムを分割し、実行し、その一部をシミュレートし、他では言い表せないものにします。

逆アセンブラと逆コンパイラの使用

逆アセンブラと逆コンパイラはアプリのバイナリコードやバイトコードを多かれ少なかれ理解できる形式に逆変換できます。ネイティブバイナリにこれらのツールを使用することで、アプリがコンパイルされたアーキテクチャに一致するアセンブラコードを取得できます。逆アセンブラはマシンコードをアセンブリコードに変換し、逆コンパイラはこのアセンブリコードを同等の高級言語コードを生成するために順に使用します。Android Java アプリは smali に逆アセンブルできます。smali は Android の Java VM である Dalvik で使用される DEX 形式のアセンブラ言語です。Smali アセンブリは逆コンパイルして同等の Java コードに戻すことも簡単です。

理論的には、アセンブリコードとマシンコード間のマッピングは一対一となるべきであるため、逆アセンブルは単純なタスクであるという印象を与える可能性があります。しかし実際には、以下のような複数の落とし穴があります。

  • コードとデータ間の確実な判別。

  • 可変命令サイズ。

  • 間接分岐命令。

  • 実行可能なコードセグメント内に明示的な CALL 命令がない関数。

  • 位置独立コード (PIC) シーケンス。

  • 手作りのアセンブリコード。

同様に、逆コンパイルは非常に複雑なプロセスであり、多くの決定論的および発見的アプローチに基づいています。結果として、逆コンパイルは一般的に実際には正確ではありませんが、それでも解析対象の関数をすばやく理解するのに非常に役立ちます。逆コンパイルの精度は逆コンパイルされるコードで利用可能な情報の量と逆コンパイラの洗練度に依存します。さらに、多くのコンパイルおよびポストコンパイルツールは理解や逆コンパイル自体の難しさを増すためにコンパイルされたコードをさらに複雑にします。そのようなコードを 難読化コード と呼びます。

長年にわたり多くのツールが逆アセンブリと逆コンパイルのプロセスを完成させ、高い忠実度で出力を作成しています。なにかしらの利用可能なツールの高度な使用手順は多くの場合それ自体の本を簡単に埋めてしまいます。開始する最善の方法はニーズと予算にあったツールを選択して十分にレビューされたユーザーガイドを取得することです。このセクションでは、これらのツールの一部を紹介し、以降の Android および iOS の「リバースエンジニアリングと改竄」の章では、特に身近にあるプラットフォームに固有のテクニック自体にフォーカスします。

難読化 (Obfuscation)

難読化とはコードやデータを変換して、より理解しにくくする (ときには逆アセンブルさえも難しくする) ためのプロセスです。これは通常、ソフトウェア保護スキームに不可欠なものです。難読化は単純にオンまたはオフにできるものではなく、プログラムの全体または一部を、多くの方法でさまざまな度合いで理解できないようにすることができます。

注: 以下に示すすべての技法は十分な時間と予算がある人があなたのアプリをリバースエンジニアリングすることを止められるものではありません。しかし、これらの技法を組み合わせることでその作業は著しく困難になります。したがって、その目的はリバースエンジニアがさらなる解析を実行することを思いとどまらせ、その努力に見合わないようにすることです。

アプリケーションの難読化には以下のような技法があります。

  • 名前の難読化 (Name obfuscation)

  • 命令の置換 (Instruction substitution)

  • 制御フローの平坦化 (Control flow flattening)

  • デッドコードインジェクション (Dead code injection)

  • 文字列の暗号化 (String encryption)

  • パッキング (Packing)

名前の難読化 (Name Obfuscation)

標準のコンパイラはソースコードからクラスメイト関数名を基にバイナリシンボルを生成します。したがって、難読化を行わなければ、シンボル名は意味があるままと残り、アプリのバイナリから簡単に抽出できます。たとえば、脱獄を検出する関数は関連するキーワード ("jailbreak" など) を検索することで見つけることができます。以下のリストは Damn Vulnerable iOS App (DVIA-v2) から逆アセンブルされた関数 JailbreakDetectionViewController.jailbreakTest4Tapped を示しています。

__T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest4TappedyypF:
stp        x22, x21, [sp, #-0x30]!
mov        rbp, rsp

難読化した後では以下のリストが示すようにシンボルの名前はもはや意味をなさないことがわかります。

__T07DVIA_v232zNNtWKQptikYUBNBgfFVMjSkvRdhhnbyyFySbyypF:
stp        x22, x21, [sp, #-0x30]!
mov        rbp, rsp

とはいえ、これは関数、クラス、フィールドの名前にのみ適用されます。実際のコードは変更されないままなので、攻撃者は逆アセンブルされたバージョンの関数を読み、その目的を理解しようと試みることが可能です (セキュリティアルゴリズムのロジックを取得するためなど) 。

命令の置換 (Instruction Substitution)

この技法は加算や減算などの標準的な二項演算子をより複雑な表現に置き換えるものです。たとえば、加算 x = a + bx = -(-a) - (-b) と表現できます。しかし、同じ置換表現を使用すると簡単にリバースできてしまうので、一つのケースに対して複数の置換手法を追加し、ランダムな要素を導入することをお勧めします。この技法は逆コンパイル時にリバースできますが、置換の複雑さと深さによってはリバースに時間がかかるようになります。

制御フローの平坦化 (Control Flow Flattening)

制御フローの平坦化では元のコードをより複雑な表現に置き換えます。この変換では関数本体を基本的なブロックに分割し、それらをすべて単一の無限ループに配置し、switch ステートメントでプログラムフローを制御します。これにより通常はコードを読みやすくする自然な条件構造が削除されるため、プログラムフローをたどることが著しく困難になります。

この画像は制御フローの平坦化がどのようにコードを変更するかを示しています。詳細については "Obfuscating C++ programs via control flow flattening" を参照してください。

デッドコードインジェクション (Dead Code Injection)

この技法はデッドコードをプログラムに注入することによってプログラムの制御フローをより複雑にします。デッドコードは元のプログラムの動作には影響を与えないが、リバースエンジニアリングプロセスのオーバーヘッドを増加させるコードのスタブです。

文字列の暗号化 (String Encryption)

アプリケーションはハードコードされた鍵、ライセンス、トークン、エンドポイント URL とともにコンパイルされることがよくあります。デフォルトでは、これらはすべて、アプリケーションのバイナリのデータセクションに平文で格納されます。この技法ではこれらの値を暗号化し、プログラムで使用される前にそのデータを復号化するコードのスタブをプログラムに注入します。

パッキング (Packing)

Packing は元の実行可能ファイルを圧縮または暗号化して実行時に動的に復元する、動的書き換えの難読化技法です。実行可能ファイルをパッキングすると、署名ベースの検出を回避するためにファイル署名を変更します。

デバッグとトレース

従来の意味では、デバッグはソフトウェアライフサイクルの一部としてプログラム内の問題を特定および分離するプロセスです。デバッグに使用される同じツールは、バグを特定することが主な目的ではありませんがリバースエンジニアリングにとって価値があります。デバッガは実行時に任意の箇所でプログラムを一時停止したり、プロセスの内部状態を検査したり、レジスタやメモリの改変さえも可能です。これらの能力はプログラムの検査を容易にします。

デバッグ は一般的には対話的デバッグセッションを意味し、デバッガは実行中のプロセスにアタッチされます。対照的に、 トレース は (API コールなどの) アプリの実行に関する情報の受動的なログ出力を指します。トレースは、デバッグ API、関数フック、カーネルトレース機能などのいくつかの方法で実行できます。改めて、これらの技法の多くは OS ごとの「リバースエンジニアリングと改竄」の章で説明します。

高度な技法

強力に難読化されたバイナリを逆難読化するなど、より複雑なタスクの場合、解析の特定の部分を自動化することなく成功することはありません。例えば、逆アセンブラでの手動解析を基にして複雑なコントロールフローグラフを理解および単純化するには何年もかかるでしょう (そして、完了する前におそらく気が狂うことでしょう) 。代わりに、カスタムメイドのツールでワークフローを増強します。幸いにも、現代の逆アセンブラにはスクリプティングと拡張 API が付属しており、一般的な逆アセンブラには多くの便利な拡張が用意されています。さらに、オープンソースの逆アセンブラエンジンやバイナリ解析フレームワークも存在します。

ハッキングの常として、何でもありのルールが適用されます。単純に最も効率的なものを使用します。すべてのバイナリは異なり、すべてのリバースエンジニアは独自のスタイルを持っています。多くの場合、目標に到達する最善の方法は (エミュレータベースのトレースやシンボリック実行など) アプローチを組み合わせることです。始めるには、優れた逆アセンブラやリバースエンジニアリングフレームワークを選択し、それらの特定の機能や格拡張 API に慣れることです。最終的には、より良くなる最善の方法は実践的な経験を積むことです。

動的バイナリ計装

ネイティブバイナリに対するもう一つの便利なアプローチには動的バイナリ計装 (DBI) があります。Valgrind や PIN などの計装フレームワークは単一プロセスの細かい命令レベルのトレースをサポートします。これは動的に生成されたコードを実行時に挿入することにより実現されます。Valgrind は Android でうまくコンパイルされ、事前にビルドされたバイナリをダウンロードして利用できます。

Valgrind README には Android 向けのコンパイル手順が記述されています。

エミュレーションベースの動的解析

エミュレーションは異なるプラットフォームや別のプログラム内で実行される特定のコンピュータープラットフォームやプログラムのイミテーションです。このイミテーションを実行するソフトウェアやハードウェアは エミュレータ と呼ばれます。エミュレータは実デバイスに代わるはるかに安価な代替手段を提供し、ユーザーはデバイスにダメージを与えることを心配することなく操作できます。Android には複数のエミュレータがありますが、iOS には実際に実行可能なエミュレータはほとんどありません。iOS にはシミュレータのみがあり、Xcode 内で出荷されています。

シミュレータとエミュレータの違いはしばしば混乱を引き起こし、二つの用語を同じ意味で使用することがありますが、実際には、特に iOS のユースケースでは、それらは異なるものです。エミュレータはターゲットプラットフォームのソフトウェアとハードウェア環境の両方を模倣します。一方、シミュレータはソフトウェア環境のみを模倣します。

Android 用 QEMU ベースのエミュレータはアプリケーションの実行中に RAM, CPU, バッテリーパフォーマンスなど (ハードウェアコンポーネント) を考慮しますが、iOS シミュレータでは、このハードウェアコンポーネントの動作はまったく考慮されません。iOS シミュレータは iOS カーネルの実装すらありません。結果として、アプリケーションが syscall を使用している場合にはこのシミュレータでは実行できません。

端的に言えば、エミュレータはターゲットプラットフォームに非常に近いイミテーションですが、シミュレータはその一部のみを模倣します。

エミュレータでアプリを実行することにより、その環境を監視および操作するための強力な方法が得られます。一部のリバースエンジニアリングタスク、特に低レベルの命令トレースが必要な場合、エミュレーションは最善の (または唯一の) 選択肢です。残念ながら、このタイプの解析は Android の場合にのみ実行可能です。iOS にはフリーまたはオープンソースのエミュレータが存在しないためです (iOS シミュレータはエミュレータではなく、iOS デバイス向けにコンパイルされたアプリは実行できません) 。利用可能な唯一の iOS エミュレータは商用の SaaS ソリューションである Corellium です。「Android の改竄とリバースエンジニアリング」の章で Android 向けの一般的なエミュレーションベースの解析フレームワークの概要を説明します。

リバースエンジニアリングフレームワークを使用したカスタムツール

ほとんどのプロフェッショナルな GUI ベースの逆アセンブラはスクリプト機能と拡張性を備えていますが、特定の問題を解決するにはあまり適していないことがあります。リバースエンジニアリングフレームワークは重量のある GUI に依存することなくある種のリバースエンジニアリングタスクを実行および自動化できます。特に、ほとんどのリバーシングフレームワークはオープンソースであるか、フリーで利用可能です。モバイルアーキテクチャをサポートする一般的なフレームワークには radare2Angr があります。

例:シンボリック実行やコンコリック実行を使用したプログラム解析

2000年代後半には、シンボリック実行ベースのテストがセキュリティ脆弱性を特定する手段として普及しました。シンボリック「実行」とは実際にプログラムを通る可能性のあるパスを一次論理の式として表現するプロセスを指します。充足可能性モジュロ理論 (SMT) ソルバーを使用してこれらの式の充足可能性をチェックし、解決された式に対するパス上の特定の実行点に到達するために必要な変数の具体的な値などのソリューションを提供します。

簡単に言えば、シンボリック実行とはプログラムを実行せずに数学的に解析することです。解析の中で、それぞれの未知の入力は数学的変数 (シンボリック値) として表されるため、これらの変数に対して実行されるすべての操作は操作のツリー (別名、AST (抽象構文木)、コンパイラ理論より) として記録されます。これらの AST は SMT ソルバーにより解釈されるいわゆる 制約 に変換できます。この解析の最後に、変数は値が不明な入力となる最終数学方程式が得られます。SMT ソルバーは最終状態を与えられた入力変数に可能な値を与えるためにこれらの方程式を解く特別なプログラムです。

これを説明するために、一つの入力 (x) を取り、二つ目の入力 (y) の値で乗算する関数を想像してください。最後に、if 条件があります。計算された値が外部変数 (z) の値よりも大きいかどうかをチェックし、true の場合は "success" を返し、そうでなければ "fail" を返します。この操作の方程式は (x * y) > z になるでしょう。

関数が常に "success" (最終状態) を返すようにしたい場合、対応する方程式を満たす xy (入力変数) の値を計算するように SMT ソルバーに伝えることができます。グローバル変数の場合と同様に、それらの値はこの関数の外部から変更でき、この関数が実行されるたびに異なる出力となる可能性があります。これにより正しいソリューションを決定する際の複雑さが増します。

内部的な SMT ソルバーはさまざまな方程式解法を使用して、そのような方程式の解を生成します。いくつかの技法は非常に高度であり、それらの議論は本書の範疇を超えています。

現実世界の状況では、関数は上記の例よりもはるかに複雑です。関数の複雑さが増すと従来のシンボリック実行に大きな課題が生じる可能性があります。課題のいくつかを以下に要約します。

  • プログラム内のループと再帰は 無限実行ツリー につながる可能性があります。

  • 複数の条件分岐やネストされた条件は パス爆発 につながる可能性があります。

  • シンボリック実行により生成された複雑な方程式は SMT ソルバーの制限により解決できない可能性があります。

  • プログラムはシンボリック実行では処理できないシステムコール、ライブラリコール、またはネットワークイベントを使用しています。

これらの課題を克服するには、通常、シンボリック実行を 動的実行 (具象的実行 とも呼ばれる) などの他の技法と組み合わせて、従来のシンボリック実行に特有のパス爆発を軽減します。この具象的な (実際の) 実行とシンボリック実行の組み合わせは コンコリック実行 (concolic という名前は concrete と symbolic に由来します) と呼ばれ、 動的シンボリック実行 と呼ばれることもあります。

これを視覚化するために、上記の例では、さらにリバースエンジニアリングを実行するかプログラムを動的に実行してこの情報をシンボリック実行解析に渡すことにより、外部変数の値を取得できます。この追加情報により方程式の複雑さを軽減し、より正確な解析結果を生み出すこともあります。改善された SMT ソルバーと現在のハードウェアスピードを併せることで、コンコリック実行は中規模のソフトウェアモジュール (10 KLOC 程度) のパスを探索できます。

さらに、シンボリック実行はコントロールフローグラフの簡素化など逆難読化タスクのサポートにも役立ちます。例えば、Jonathan Salwan と Romain Thomas は 動的シンボリック実行を使用して VM ベースのソフトウェア保護をリバースエンジニアリングする方法を示しました [#salwan] (つまり、実際の実行トレース、シミュレーション、シンボリック実行を組み合わせて使用します) 。

Android のセクションでは、シンボリック実行を使用して Android アプリケーションの簡単なライセンスチェックをクラックするためのウォークスルーを説明します。

参考情報

Last updated