Android の改竄とリバースエンジニアリング
そのオープン性により Android はリバースエンジニアにとって好都合な環境になっています。しかし、Java とネイティブコードの両方を扱うと、時には物事がより複雑になることがあります。以下の章では、Android のリバースのいくつかの特質と OS 固有のツールをプロセスとしてみていきます。
「その他の」モバイル OS と比較して、Android はリバースエンジニアにとって大きな利点を提供します。 Android はオープンソースであるため、Android Open Source Project (AOSP) のソースコードを勉強し、OS や標準ツールをあなたが望む任意の方法で変更することができます。一般に販売されているデバイスでも、開発者モードの有効化やアプリのサイドローディングなどの操作を多くの手間をかける必要なく簡単に実行できます。SDK にある強力なツールから、幅広く利用可能なリバースエンジニアリングツールに至るまで、あなたの人生を楽にしてくれる多くの常識があります。
しかし、Android 固有の課題もいくつかあります。例えば、Java バイトコードとネイティブコードの両方を処理する必要があるかもしれません。Java Native Interface (JNI) はリバースエンジニアを混乱させる目的のために使用されることがあります。開発者はデータや機能を「隠す」ためにネイティブレイヤを使用することや、実行が二つのレイヤを頻繁にジャンプするようにアプリを構築することがあります。これはリバースエンジニアにとって複雑なものになります (公平を期すると、パフォーマンスの向上やレガシーコードのサポートなど、JNI を使用する正当な理由があるかもしれません) 。
Java ベースの Android 環境と Android の基盤を形成する Linux OS および Kernel の両方についての実践的な知識が必要です。さらに、Java 仮想マシン内で実行されるネイティブコードとバイトコードの両方に対処するための適切なツールセットが必要です。
以下のセクションでは、さまざまなリバースエンジニアリング技法を実演するための例として OWASP Mobile Testing Guide Crackmes [1] を使用することに注意します。部分的および完全なスポイラーを期待します。読む前にあなた自身でクラックに挑戦してみることをお勧めします。

必要なもの

少なくとも、Android Studio [2] が必要です。Android SDK、プラットフォームツールとエミュレータ、さまざまな SDK バージョンとフレームワークコンポーネントを管理するマネージャアプリが付属しています。Android Studio を使用すると、SDK Manager アプリも利用できます。Android SDK ツールをインストールしたり、さまざまな API レベルの SDK を管理したり、エミュレーターや、エミュレータイメージを作成する AVD Manager アプリケーションも利用できます。以下がシステムにインストールされていることを確認します。
  • 最新の SDK ツールと SDK プラットフォームツールパッケージ。これらのパッケージは Android Debugging Bridge (ADB) クライアントと、Android プラットフォームとインタフェースする他のツールが含まれています。一般に、これらのツールは後方互換性があるため、インストールされているバージョンがひとつだけ必要です。
  • Android NDK。これは Native Development Kit で、さまざまなアーキテクチャのネイティブコードをクロスコンパイルするためのプレビルドツールチェーンが組み込まれています。
SDK および NDK に加えて、Java バイトコードをより人に優しいものにするためのものもあります。幸運なことに、Java デコンパイラは一般的に Android バイトコードをよく扱います。有名なフリーのデコンパイラには JD [3], Jad [4], Proycon [5], CFR [6] があります。都合により、これらのデコンパイラのいくつかを apkx ラッパースクリプトにパックしました [7] 。このスクリプトはリリース APK から Java コードを抽出するプロセスを完全に自動化し、さまざまなバックエンドで簡単に試すことができます (以下のいくつかの例でも使用します) 。
それ以外は、本当に好みと予算の問題です。さまざまな長所と短所をもつ、数多くのフリーおよび商用の逆アセンブラ、デコンパイラ、フレームワークが存在します。以下でいくつかを紹介します。

Android SDK のセットアップ

ローカル Android SDK のインストールは Android Studio を通じて管理されます。Android Studio で空のプロジェクトを作成し、"Tools->Android->SDK Manager" を選択して SDK Manager GUI を開きます。"SDK Platforms" タブで複数の API レベルの SDK をインストールできます。最近の API レベルは以下のとおりです。
  • API 21: Android 5.0
  • API 22: Android 5.1
  • API 23: Android 6.0
  • API 24: Android 7.0
  • API 25: Android 7.1
  • API 26: Android O Developer Preview
OS によって、インストールされた SDK は以下の場所にあります。
1
Windows:
2
3
C:\Users\<username>\AppData\Local\Android\sdk
4
5
MacOS:
6
7
/Users/<username>/Library/Android/sdk
Copied!
注意: Linux では、独自の SDK の場所を選択する必要があります。一般的な場所は /opt, /srv, /usr/local です。

Android NDK のセットアップ

Android NDK にはネイティブコンパイラとツールチェーンのビルド済みのバージョンが含まれています。伝統的に、GCC と Clang コンパイラの両方がサポートされていましたが、GCC に対する積極的なサポートは NDK のリビジョン 14 で終了しました。使用する正しいバージョンはデバイスアーキテクチャとホスト OS の両方に依存します。ビルド済みのツールチェーンは NDK の toolchains ディレクトリにあります。アーキテクチャごとにひとつのサブディレクトリが含まれています。
アーキテクチャ
ツールチェーン名
ARM-based
arm-linux-androideabi-<gcc-version>
x86-based
x86-<gcc-version>
MIPS-based
mipsel-linux-android-<gcc-version>
ARM64-based
aarch64-linux-android-<gcc-version>
X86-64-based
x86_64-<gcc-version>
MIPS64-based
mips64el-linux-android-<gcc-version>
正しいアーキテクチャを選ぶことに加えて、ターゲットとするネイティブ API レベルの正しい sysroot を指定する必要があります。sysroot はターゲットのシステムヘッダとライブラリを含むディレクトリです。利用可能なネイティブ API は Android API レベルにより異なります。それぞれの Android API レベルの可能な sysroot は $NDK/platforms/ にあり、各 API レベルのディレクトリにはさまざまな CPU とアーキテクチャのサブディレクトリが含まれています。
ビルドシステムをセットアップするひとつの可能性は、コンパイラパスと必要なフラグを環境変数としてエクスポートすることです。しかし、物事を簡単にするために、NDK ではいわゆるスタンドアローンツールチェーンを作成できます。つまり、必要な設定を盛り込んだ「一時的な」ツールチェーンです。
スタンドアローンツールチェーンをセットアップするには、NDK の最新の安定版をダウンロードします [8] 。ZIP ファイルを展開し、NDK ルートディレクトリに移動して、以下のコマンドを実行します。
1
$ ./build/tools/make_standalone_toolchain.py --arch arm --api 24 --install-dir /tmp/android-7-toolchain
Copied!
これにより、ディレクトリ /tmp/android-7-toolchain に Android 7.0 のスタンドアローンツールチェーンが作成されます。都合により、あなたのツールチェーンディレクトリを指す環境変数をエクスポートできます。これについては後の例で使用します。以下のコマンドを実行するか、.bash_profile または他の起動スクリプトに追加します。
1
$ export TOOLCHAIN=/tmp/android-7-toolchain
Copied!

フリーのリバースエンジニアリング環境の構築

少しの労力で、リーズナブルな GUI 搭載のリバースエンジニアリング環境をフリーで構築できます。
逆コンパイルされたソースをナビゲートするには、IntelliJ [9] の使用をお勧めします。比較的軽量な IDE はコードを閲覧するのに最適であり、逆コンパイルされたアプリの基本的なオンデバイスデバッグが可能です。しかし、あなたが重く、遅く、複雑なものを好むのであれば、Eclipse [10] があなたにとって正しい IDE です (注:このアドバイスは執筆者の個人的な偏見に基づいています) 。
Java コードの代わりに Smali を見てもかまわない場合、IntelliJ の smalidea プラグインを使用してデバイスをデバッグできます [11] 。Smalidea はバイトコードのシングルステップ実行、識別子の名前変更、名前なしレジスタの監視をサポートしているため、JD + IntelliJ の設定よりもはるかに強力です。
APKTool [12] は一般的なフリーツールです。APK アーカイブから直接リソースを抽出および逆アセンブルし、Java バイトコードを Smali 形式に逆アセンブルできます (Smali/Backsmali は DEX 形式に対するアセンブラ/逆アセンブラです。「アセンブラ/逆アセンブラ」のアイスランド語でもあります) 。APKTool を使用してパッケージを再アセンブルできます。パッチ化および Manifest への変更の適用に便利です。
プログラム解析や自動化された逆難読化などのより緻密なタスクは Radare2 [13] や Angr [14] などのオープンソースリバースエンジニアリングフレームワークで達成できます。このガイドではこれらのフリーツールやフレームワークの多くの使用例を紹介します。

商用ツール

完全にフリーの設定で作業することは可能ですが、商用ツールへの投資を検討することもできます。これらのツールの主な利点は利便性です。素晴らしい GUI 、多くの自動化、エンドユーザーサポートが付いています。あなたがリバースエンジニアを日々の糧とするのであれば、これは多くの手間が省けます。

JEB

JEB [15] は商用の逆コンパイラです。Android アプリの静的および動的解析に必要な機能をすべてパックしたオールインワンパッケージであり、それなりに信頼でき、迅速なサポートが得られます。これには組込みのデバッガがあり、効率的なワークフローが可能です。特に ProGuard により難読化されたバイトコードを扱う場合には、逆コンパイルされたもの (および注釈つきソース) に直接ブレークポイントを設定することは非常に有益です。もちろん、このような便利なものは安くありません。バージョン 2.0 以降、JEB は従来のライセンスモデルからサブスクリプションベースのものに変更されています。そのため、使用には月額料金を支払う必要があります。

IDA Pro

IDA Pro [16] は ARM, MIPS, そしてもちろん Intel の ELF バイナリを理解し、Java バイトコードも処理できます。Java アプリケーションとネイティブプロセスの両方のデバッガも付属しています。有能な逆アセンブラと強力なスクリプティングと拡張機能を備えているため、IDA Pro はネイティブプログラムやライブラリの静的解析に最適です。しかし、Java コード用に提供されている静的解析機能は若干基本的なものです。Smali 逆アセンブリが得られるに過ぎません。パッケージやクラス構造をナビゲートすることはできません。一部のこと (クラスの名前変更など) はできません。より複雑な Java アプリでの作業は少し面倒になります。

リバースエンジニアリング

リバースエンジニアリングはアプリがどのように動作するか調べるためにアプリを分解するプロセスです。コンパイルされたアプリを調査したり (静的解析) 、実行中にアプリを観察したり (動的解析) 、その両方を組み合わせたりすることで、これを行うことができます。

Java コードの静的解析

一部の厄介な、ツール回避のアンチデコンパイルトリックが適用されていない限り、Java バイトコードはそれほどの問題もなくソースコードに逆変換できます。UnCrackable App for Android Level 1 を以下の例で使用しますので、まだダウンロードしていない場合はダウンロードします。まず、デバイスかエミュレータにアプリをインストールします。実行して crackme についての内容を確認します。
1
$ wget https://github.com/OWASP/owasp-mstg/raw/master/Crackmes/Android/Level_01/UnCrackable-Level1.apk
2
$ adb install UnCrackable-Level1.apk
Copied!
なんらかの秘密のコードが見つかることを期待しています。
おそらく、アプリ内のどこかに格納された秘密の文字列を探しています。そのため、次の論理的なステップは内部を見て回ることです。まず、APK ファイルを展開して内容を確認します。
1
$ unzip UnCrackable-Level1.apk -d UnCrackable-Level1
2
Archive: UnCrackable-Level1.apk
3
inflating: UnCrackable-Level1/AndroidManifest.xml
4
inflating: UnCrackable-Level1/res/layout/activity_main.xml
5
inflating: UnCrackable-Level1/res/menu/menu_main.xml
6
extracting: UnCrackable-Level1/res/mipmap-hdpi-v4/ic_launcher.png
7
extracting: UnCrackable-Level1/res/mipmap-mdpi-v4/ic_launcher.png
8
extracting: UnCrackable-Level1/res/mipmap-xhdpi-v4/ic_launcher.png
9
extracting: UnCrackable-Level1/res/mipmap-xxhdpi-v4/ic_launcher.png
10
extracting: UnCrackable-Level1/res/mipmap-xxxhdpi-v4/ic_launcher.png
11
extracting: UnCrackable-Level1/resources.arsc
12
inflating: UnCrackable-Level1/classes.dex
13
inflating: UnCrackable-Level1/META-INF/MANIFEST.MF
14
inflating: UnCrackable-Level1/META-INF/CERT.SF
15
inflating: UnCrackable-Level1/META-INF/CERT.RSA
Copied!
基本的には、アプリに関連するすべての Java バイトコードとデータはアプリのルートディレクトリの classes.dex という名前のファイルに含まれています。このファイルは Dalvik Executable Format (DEX) に準拠しています。これは Java プログラムをパッケージ化する Android 固有の方法です。ほとんどの Java 逆コンパイラはプレーンクラスのファイルまたは JAR が入力として使用されるため、はじめに classes.dex ファイルを JAR に変換する必要があります。これは dex2jar または enjarify を使用して行うことができます。
JAR ファイルを作成したら、数多くあるフリーの逆コンパイラを使用して Java コードを作成できます。この例では、CFR を逆コンパイラとして選択して使用します。CFR は積極的に開発されており、新作のリリースは作成者のウェブサイトで定期的に公開されています [6] 。都合の良いことに、CFR は MIT ライセンスの下でリリースされています。つまり、ソースコードは現在入手できませんが、目的に応じて自由に使用できます。
CFR を実行する最も簡単な方法は apkx を介することです。これは dex2jar をパッケージ化し、抽出、変換、逆コンパイルの手順を自動化します。以下のようにインストールします。
1
$ git clone https://github.com/b-mueller/apkx
2
$ cd apkx
3
$ sudo ./install.sh
Copied!
これは apkx/usr/local/bin にコピーする必要があります。UnCrackable-Level1.apk で実行します。
1
$ apkx UnCrackable-Level1.apk
2
Extracting UnCrackable-Level1.apk to UnCrackable-Level1
3
Converting: classes.dex -> classes.jar (dex2jar)
4
dex2jar UnCrackable-Level1/classes.dex -> UnCrackable-Level1/classes.jar
5
Decompiling to UnCrackable-Level1/src (cfr)
Copied!
逆コンパイルされたソースはディレクトリ Uncrackable-Level1/src にあります。ソースを閲覧するには、シンプルなテキストエディタ (できれば構文を強調表示するもの) でもよいのですが、Java IDE にコードをロードするとナビゲーションが簡単になります。IntelliJ にコードをインポートしてみましょう。ボーナスとしてデバイス上でのデバッグ機能を得られます。
IntelliJ を開き、"New Project" ダイアログの左のタブでプロジェクトタイプとして "Android" を選択します。アプリケーション名として "Uncrackable1"、会社名として "vantagepoint.sg" と入力します。これによりパッケージ名 "sg.vantagepoint.uncrackable1" となり、元のパッケージ名と一致します。一致するパッケージ名を使用することが重要です。後で実行中のアプリにデバッガをアタッチする場合に、IntelliJ はパッケージ名を使用して正しいプロセスを識別するためです。
次のダイアログでは、任意の API 番号を選択します。そのプロジェクトを実際にコンパイルしたいわけではないので、実際には問題ではありません。"next" をクリックし "Add no Activity" を選択してから "finish" をクリックします。
プロジェクトが作成されたら、左側の "1: Project" ビューを展開し、フォルダ app/src/main/java に移動します。IntelliJ により作成されたデフォルトパッケージ "sg.vantagepoint.uncrackable1" を右クリックして削除します。
ここで、ファイルブラウザで Uncrackable-Level1/src ディレクトリを開き、sg ディレクトリを IntelliJ プロジェクトビューの現時点で空の Java フォルダにドラッグします (フォルダを移動する代わりにコピーするには "alt" キーを押します) 。
アプリがビルドされた元の Android Studio プロジェクトに類似の構造にたどり着きます。
IntelliJ がコードのインデックスを作成すると、通常の Java プロジェクトと同様にブラウズできます。逆コンパイルされたパッケージ、クラス、メソッドの多くは奇妙な一文字の名前を持つことに注意します。これはビルド時に ProGuard で "minified" されたためです。これはバイトコードを読みにくくする難読化の基本的なものですが、このようなかなり単純なアプリでは頭を悩ませることはありません。しかし、複雑なアプリを解析する際には、かなり迷惑になることがあります。
難読化されたコードを解析する際のグッドプラクティスは、確認のためにクラス、メソッド、その他の識別子の名前に注釈をつけることです。パッケージ sg.vantagepoint.aMainActivity クラスを開きます。メソッド verify は "verify" ボタンをタップすると呼び出されるものです。このメソッドはユーザー入力をブール値を戻す a.a という静的メソッドに渡します。a.a はユーザーにより入力されたテキストが有効であるかどうかを検証することを担っていると思われるため、コードをリファクタリングしてこれを反映します。
User Input Check
クラス名 (a.a の最初の a) を右クリックし、ドロップダウンメニューから Refactor->Rename を選択します (または Shift-F6 を押します) 。これまでにそのクラスについて判ったことを考慮して、クラス名をより意味のあるものに変更します。例えば、"Validator" とします (後でクラスについてより詳しく知ったとき、その名前をいつでも変更できます) 。ここで a.aValidator.a になります。同じ手順に従って、静的メソッド a の名前を check_input に変更します。
Refactored class and method names
おめでとう。あなたは静的解析の基礎を学びました。解析されたプログラムについて理論化、注釈付け、および段階的に理論を改訂することがすべてです。あなたが完全にそれを理解するか、少なくとも十分に達成したと思うまで行います。
次に、check_input メソッドで ctrl+クリック (Mac では command+クリック) します。メソッド定義に移動します。逆コンパイルされたメソッドは以下のようになります。
1
public static boolean check_input(String string) {
2
byte[] arrby = Base64.decode((String)"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0);
3
byte[] arrby2 = new byte[]{};
4
try {
5
arrby = sg.vantagepoint.a.a.a(Validator.b("8d127684cbc37c17616d806cf50473cc"), arrby);
6
arrby2 = arrby;
7
}sa
8
catch (Exception exception) {
9
Log.d((String)"CodeCheck", (String)("AES error:" + exception.getMessage()));
10
}
11
if (string.equals(new String(arrby2))) {
12
return true;
13
}
14
return false;
15
}
Copied!
そして、パッケージ sg.vantagepoint.a.aa という名前の関数に渡される base64 エンコードされた String があります (ここでもすべてが a と呼ばれます) 。また、16進数にエンコードされた暗号化鍵のようなものがあります (16 hex bytes = 128bit, 共通鍵の長さ) 。この特定の a は正確には何をするでしょうか。Ctrl を押しながらクリックして調べます。
1
public class a {
2
public static byte[] a(byte[] object, byte[] arrby) {
3
object = new SecretKeySpec((byte[])object, "AES/ECB/PKCS7Padding");
4
Cipher cipher = Cipher.getInstance("AES");
5
cipher.init(2, (Key)object);
6
return cipher.doFinal(arrby);
7
}
8
}
Copied!
ここで大体出来上がりです。それは単に標準の AES-ECB です。check_inputarrby1 に格納されている base64 文字列は、128bit AES を使用して復号された暗号文であり、ユーザー入力と比較されているようです。ボーナスタスクとして、抽出された暗号文を復号し、秘密の値を取得してみましょう。
復号された文字列を取得する別の (そしてより速い) 方法は動的解析を少しミックスすることです。UnCrackable Level 1 を後ほど再考して、これを行う方法を示しますので、まだプロジェクトを削除しないでください。

ネイティブコードの静的解析

Dalvik と ART はどちらも Java Native Interface (JNI) をサポートしています。JNI は Java コードが C/C++ で書かれたネイティブコードとやりとりする方法を定義します。他の Linux ベースのオペレーティングシステムと同様に、ネイティブコードは ELF ダイナミックライブラリ ("*.so") にパッケージ化され、実行時に System.load メソッドを使用して Android アプリによりロードされます。
Android JNI 関数は Linux ELF ライブラリにコンパイルされたネイティブコードで構成されています。それは Linux でほぼ標準のものです。但し、glibc などの広く使われている C ライブラリに依存する代わりに、Bionic [17] という名前のカスタム libc に対して Android バイナリがビルドされます。Bionic はシステムプロパティやロギングなどの重要な Android 固有のサービスのサポートを追加します。完全な POSIX 互換ではありません。
OWASP MSTG リポジトリから HelloWorld-JNI.apk をダウンロードし、必要に応じて、エミュレーターまたは Android デバイスにインストールして実行します。
1
$ wget HelloWord-JNI.apk
2
$ adb install HelloWord-JNI.apk
Copied!
このアプリにまったく華々しさはありません。テキスト "Hello from C++" のラベルが表示されることがすべてです。実のところ、これは C/C++ サポートありで新しいプロジェクトを作成したときに Android Studio が生成するデフォルトアプリですが、JNI の呼び出し方法の基本的な原則を示すには十分です。
apkx で APK を逆コンパイルします。これはソースコードを HelloWorld/src ディレクトリに抽出します。
1
$ wget https://github.com/OWASP/owasp-mstg/blob/master/OMTG-Files/03_Examples/01_Android/01_HelloWorld-JNI/HelloWord-JNI.apk
2
$ apkx HelloWord-JNI.apk
3
Extracting HelloWord-JNI.apk to HelloWord-JNI
4
Converting: classes.dex -> classes.jar (dex2jar)
5
dex2jar HelloWord-JNI/classes.dex -> HelloWord-JNI/classes.jar
Copied!
MainActivity はファイル MainActivity.java にあります。"Hello World" テキストビューは onCreate() メソッドで設定されています。
1
public class MainActivity
2
extends AppCompatActivity {
3
static {
4
System.loadLibrary("native-lib");
5
}
6
7
@Override
8
protected void onCreate(Bundle bundle) {
9
super.onCreate(bundle);
10
this.setContentView(2130968603);
11
((TextView)this.findViewById(2131427422)).setText((CharSequence)this.stringFromJNI());
12
}
13
14
public native String stringFromJNI();
15
}
16
17
}
Copied!
下部にある public native String stringFromJNI の宣言に注目します。native キーワードはこのメソッドの実装がネイティブ言語で提供されていることを Java コンパイラに通知します。対応する関数は実行時に解決します。もちろん、これは期待されるシグネチャを持つグローバルシンボルをエクスポートするネイティブライブラリがロードされる場合にのみ機能します。このシグネチャはパッケージ名、クラス名、メソッド名で構成されます。この例の場合には、これはプログラマが以下の C または C++ 関数を実装する必要があることを意味します。
1
JNIEXPORT jstring JNICALL Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI(JNIEnv *env, jobject)
Copied!
では、この関数のネイティブ実装はどこにあるのでしょうか。APK アーカイブの lib ディレクトリを調べると、さまざまなプロセッサアーキテクチャの名前が付けられた合計8つのサブディレクトリが見つかります。これらの各ディレクトリには問いのプロセッサアーキテクチャ向けにコンパイルされたバージョンのネイティブライブラリ libnative-lib.so が含まれています。System.loadLibrary が呼び出されると、ローダーはアプリが実行されているデバイスに基づいて正しいバージョンを選択します。
上記の命名規則に従い、ライブラリは Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI という名前のシンボルをエクスポートすることが期待できます。Linux システムでは、readelf (GNU binutils に含まれる) または nm を使用してシンボルの一覧を取得できます。Mac OS では、Macports や Homebrew 経由でインストールできる greadelf ツールで同じことができます。以下の例では greadelf を使用しています。
1
$ greadelf -W -s libnative-lib.so | grep Java
2
3: 00004e49 112 FUNC GLOBAL DEFAULT 11 Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI
Copied!
これは stringFromJNI ネイティブメソッドが呼び出されたときに最終的に実行されるネイティブ関数です。
コードを逆アセンブルするには、ELF バイナリを理解する逆アセンブラ (つまり、存在するすべての逆アセンブラ) に libnative-lib.so をロードします。アプリが異なるアーキテクチャのバイナリを同梱している場合には、逆アセンブラがその対処方法を知っている限り、最もよく知られているアーキテクチャを理論的に選択できます。各バージョンは同じソースからコンパイルされ、まったく同じ機能を実装します。なお、後で実デバイスでライブラリをデバッグする予定である場合には、通常は ARM ビルドを選択することをお勧めします。
新旧両方の ARM プロセッサをサポートするために、Android アプリはさまざまな Application Binary Interface (ABI) バージョン用にコンパイルされた複数の ARM ビルドを同梱します。ABI はアプリケーションのマシンコードが実行時にシステムとやりとりする方法を定義します。以下の ABI がサポートされています。
  • armeabi: ABI は少なくとも ARMv5TE 命令セットをサポートする ARM ベースの CPU 用です。
  • armeabi-v7a: この ABI は armeabi を拡張して、いくつかの CPI 命令セット拡張を含みます。
  • arm64-v8a: 新しい 64 ビット ARM アーキテクチャである AArch64 をサポートする ARMv8 ベースの CPU 用 ABI です。
ほとんどの逆アセンブラはこれらのアーキテクチャのいずれにも対処できます。以下では、IDA Pro で armeabi-v7a バージョンを表示しています。これは lib/armeabi-v7a/libnative-lib.so にあります。IDA Pro のライセンスを所有していない場合、Hex-Rays のウェブサイト [13] で入手可能なデモ版や評価版でも同じことができます。
IDA Pro でファイルを開きます。"Load new file" ダイアログで、ファイルタイプとして "ELF for ARM (Shared Object)" を (IDA はこれを自動的に検出します) 、プロセッサタイプとして "ARM Little-Endian" を選択します。
ファイルが開いたら、左側の "Functions" ウィンドウをクリックし、Alt+t を押して検索ダイアログを開きます。"java" を入力して Enter キーを押します。Java_sg_vantagepoint_helloworld_MainActivity_stringFromJNI 関数がハイライトされているはずです。それをダブルクリックすると、逆アセンブリウィンドウのそのアドレスにジャンプします。"Ida View-A" にその関数の逆アセンブリが表示されるはずです。
コードは多くはありませんが、解析してみましょう。最初に知る必要があるのは、すべての JNI に渡される第一引数が JNI インタフェースポインタであることです。インタフェースポインタはポインタへのポインタです。このポインタは関数テーブルを指します。さらに多くのポインタの配列です。それぞれ JNI インタフェース関数を指しています (頭が混乱しますか?) 。関数テーブルは Java VM により初期化され、ネイティブ関数が Java 環境とやりとりできるようにします。
これを念頭に置いて、アセンブリコードの各行を見てみましょう。
1
LDR R2, [R0]
Copied!
思い出してください。第一引数 (R0 にあります) は JNI 関数テーブルポインタへのポインタです。LDR 命令はこの関数テーブルポインタを R2 にロードします。
1
LDR R1, =aHelloFromC
Copied!
この命令は文字列 "Hello from C++" の PC 相対オフセットを R1 にロードします。この文字列はオフセット 0xe84 の関数ブロックの終わりの直後に配置されています。プログラムカウンタに相対するアドレッシングにより、コードはメモリ内の位置とは無関係に実行できます。
1
LDR.W R2, [R2, #0x29C]
Copied!
この命令はオフセット 0x29C から関数ポインタを R2 の JNI 関数ポインタテーブルにロードします。これは NewStringUTF 関数になります。Android NDK に含まれている jni.h に関数ポインタの一覧があります。関数プロトタイプは以下のようになります。
1
jstring (*NewStringUTF)(JNIEnv*, const char*);
Copied!
この関数は二つの引数を必要とします。JNIEnv ポインタ (すでに R0 にあります) と文字列ポインタです。次に、PC の現在値に R1 を加えられ、静的文字列 "Hello from C++" の絶対アドレスになります (PC + オフセット) 。
1
ADD R1, PC
Copied!
最後に、プログラムは R2 にロードされた NewStringUTF 関数ポインタへの分岐命令を実行します。
1
BX R2
Copied!
この関数が返るとき、R0 には新たに構築された UTF 文字列へのポインタが格納されています。これは最終的な戻り値なので、R0 は変更されず、関数は終了します。

デバッグとトレース

これまで、ターゲットアプリを実行することなく静的解析技法を使用してきました。実世界では、特に複雑なアプリやマルウェアをリバースする際には、純粋な静的解析では非常に難しいことがわかります。実行中にアプリを観察し操作することで、その動作をより簡単に解読できます。次に、これを行うのに役立つ動的解析手法を見ていきます。
Android アプリは二つの異なるタイプのデバッグをサポートします。Java Debug Wire Protocol (JDWP) を使用する Java ランタイムレベルのデバッグと、ネイティブレイヤー上の Linux/Unix スタイルの ptrace ベースのデバッグで、両方ともリバースエンジニアにとって有益なものです。

開発者オプションの有効化

Android 4.2 以降、「開発者オプション」サブメニューはデフォルトでは設定アプリに表示されません。それを有効にするには、"About phone" ビューの "Build number" セクションを7回タップする必要があります。ビルド番号フィールドの位置はデバイスによって異なる場合があることに注意します。例えば、LG Phone の場合、"About phone > Software information" にあります。これを済ませると、「開発者オプション」は設定メニューの下部に表示されます。開発者オプションが有効になると、「USB デバッグ」スイッチでデバッグを有効にできます。

リリースアプリのデバッグ

Dalvik および ART は Java Debug Wire Protocol (JDWP) をサポートしています。これはデバッガとデバッグする Java 仮想マシン (VM) との間の通信に使用されるプロトコルです。JDWP は、JDB, JEB, IntelliJ, Eclipse などすべてのコマンドラインツールと Java IDE でサポートされている標準のデバッグプロトコルです。Android の JDWP の実装には Dalvik Debug Monitor Server (DDMS) によって実装された拡張機能をサポートするためのフックも含まれています。
JDWP デバッガを使用すると、Java コードのステップ実行、Java メソッドへのブレークポイント設定、ローカルおよびインスタンス変数の検査および変更が可能です。JDWP デバッガは、ネイティブライブラリの呼び出しをほとんどしない「通常」の Android アプリをデバッグする際に使用します。
以下のセクションでは、JDB のみを使用して UnCrackable App for Android Level 1 を解決する方法を示します。これはこの crackme を解決するための 効率的 な方法ではないことに注意します。後ほどガイドで紹介する Frida や他の方法を使用するともっと速くできます。しかし、Java デバッガの機能の紹介としては十分果たしています。
再パッケージ化
すべてのデバッガ対応プロセスは JDWP プロトコルパケットを処理するための拡張スレッドを実行します。このスレッドはマニフェストファイルの <application> 要素に android:debuggable="true" が設定されているアプリでのみ開始されます。これは一般的にエンドユーザーに出荷される Android デバイスの設定です。
リバースエンジニアリングアプリを使用する際、ターゲットアプリのリリースビルドにのみアクセスできることがよくあります。リリースビルドはデバッグを目的としたものではありません。結局のところ、それは デバッグビルド が意図するものです。システムプロパティ ro.debuggable が "0" に設定されている場合、Android はリリースビルドの JDWP とネイティブデバッグの両方を禁止しています。これはバイパスが容易ですが、行のブレークポイントがないなど、まだいくつかの制限に遭遇するでしょう。それでも、不完全なデバッガでさえ非常に貴重なツールです。プログラムの実行時状態を検査できるため、何が起こっているのかを理解することが とても 容易になります。
リリースビルドリリースをデバッグ可能なビルドに「変換」するには、アプリのマニフェストファイルのフラグを変更する必要があります。この変更によりコード署名が壊れるため、変更された APK アーカイブにも再署名する必要があります。
これを行うには、まずコード署名証明書が必要です。以前に Android Studio でプロジェクトをビルドしていた場合、IDE はすでにデバッグキーストアと証明書を $HOME/.android/debug.keystore に作成しています。このキーストアのデフォルトパスワードは "android" で、鍵は "androiddebugkey" という名前です。
Java 標準のディストリビューションにはキーストアと証明書を管理するための keytool が含まれています。独自の署名証明書と鍵を作成し、以下のようにデバッグキーストアに追加できます。
1
$ keytool -genkey -v -keystore ~/.android/debug.keystore -alias signkey -keyalg RSA -keysize 2048 -validity 20000
Copied!
証明書が利用可能になったので、以下の手順でアプリを再パッケージできます。Android Studio のビルドツールディレクトリは [SDK-Path]/build-tools/[version] にあります。zipalignapksigner ツールがこのディレクトリにあります。UnCrackable-Level1.apk を以下のように再パッケージします。
  1. 1.
    apktool を使用してアプリをアンパックし、AndroidManifest.xml をデコードします。
1
$ apktool d --no-src UnCrackable-Level1.apk
Copied!
  1. 1.
    テキストエディタを使用してマニフェストに android:debuggable = "true" を追加します。
1
<application android:allowBackup="true" android:debuggable="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:name="com.xxx.xxx.xxx" android:theme="@style/AppTheme">
Copied!
  1. 1.
    APK を再パッケージして署名します。
1
$ cd UnCrackable-Level1
2
$ apktool b
3
$ zipalign -v 4 dist/UnCrackable-Level1.apk ../UnCrackable-Repackaged.apk
4
$ cd ..
5
$ apksigner sign --ks ~/.android/debug.keystore --ks-key-alias signkey UnCrackable-Repackaged.apk
Copied!
注意:apksigner で JRE の互換性の問題が発生した場合は、代わりに jarsigner を使用できます。この場合、署名 zipalign を呼び出すことに注意します。
1
$ jarsigner -verbose -keystore ~/.android/debug.keystore UnCrackable-Repackaged.apk signkey
2
$ zipalign -v 4 dist/UnCrackable-Level1.apk ../UnCrackable-Repackaged.apk
Copied!
  1. 1.
    アプリを再インストールします。
1
$ adb install UnCrackable-Repackaged.apk
Copied!

「デバッガを待機」機能

UnCrackable アプリは愚かではありません。デバッグ可能モードで実行されていることに気付き、シャットダウンに反応します。すぐにモーダルダイアログが表示され、OK ボタンをタップすると crackme が終了します。
幸いなことに、Android の開発者オプションには便利な「デバッガを待機」機能があり、JDWP デバッガが接続されるまで、起動している選択されたアプリを自動的に中断できます。この機能を使用すると、検出メカニズムが実行される前にデバッガを接続し、そのメカニズムをトレース、デバッグ、非アクティブ化を行うことができます。これは本当にアンフェアな利点ですが、一方で、リバースエンジニアはフェアにプレーする必要はありません。
開発者設定で、デバッグするアプリケーションとして Uncrackable1 を選択し、「デバッガを待機」スイッチをアクティブにします。
注意:default.propro.debuggable を 1 に設定しても、マニフェストで android:debuggable フラグが true に設定されるまで、アプリは「デバッグアプリを選択」リストに現れません。

Android Debug Bridge

Android SDK に付属の adb コマンドラインツールはローカル開発環境と接続されている Android デバイスとの橋渡しをします。通常、エミュレータや USB 経由で接続されたデバイスでアプリをデバッグします。adb devices コマンドを使用して、現在接続されているデバイスを一覧表示します。
1
$ adb devices
2
List of devices attached
3
090c285c0b97f748 device
Copied!
adb jdwp コマンドは接続されたデバイス上で実行されているすべてのデバッグ可能なプロセス (つまり、JDWP トランスポートをホストしているプロセス) のプロセス ID を一覧表示します。adb forward コマンドを使用すると、ホストマシン上にリスニングソケットを開き、選択したプロセスの JDWP トランスポートにこのソケットの TCP 接続を転送できます。
1
$ adb jdwp
2
12167
3
$ adb forward tcp:7777 jdwp:12167
Copied!
これで JDB をアタッチする準備ができました。デバッガをアタッチすると、アプリが再開しますが、これは我々が望むものではありません。むしろ、最初にいくつかの調査を行えるように、中断しておきたい。プロセスが再開しないように、suspend コマンドを jdb にパイプします。
1
$ { echo "suspend"; cat; } | jdb -attach localhost:7777
2
3
Initializing jdb ...
4
> All threads suspended.
5
>
Copied!
中断したプロセスにアタッチされ、jdb コマンドを実行する準備ができました。? を入力すると、コマンドの完全なリストが表示されます。残念ながら、Android VM は利用可能なすべての JDWP 機能をサポートしてはいません。例えば、クラスのコードを再定義する redefine コマンドは、潜在的に非常に有用な機能ですが、サポートされていません。もうひとつの重要な制約は、行ブレークポイントが機能しないことです。これはリリースのバイトコードには行情報が含まれていないためです。しかし、メソッドブレークポイントは機能します。有用なコマンドには以下のものがあります。
  • classes: ロードされたすべてのクラスを一覧表示します
  • class / method / fields : クラスに関する情報を出力し、そのメソッドおよびフィールドを一覧表示します
  • locals: 現在のスタックフレームのローカル変数を表示します
  • print / dump : オブジェクトに関する情報を出力します
  • stop in : メソッドブレークポイントを設定します
  • clear : メソッドブレークポイントを削除します
  • set = : フィールド、変数、配列要素に新しい値を代入します
逆コンパイルされた UnCrackable App Level 1 のコードをもう一度見て、可能な解決策について考えてみます。良いアプローチは秘密の文字列が変数に平文で格納された状態でアプリを一時停止し、それを取得することです。残念ながら、まずルート/改竄検出を処理しない限り、それは得られません。
コードをレビューすることで、メソッド sg.vantagepoint.uncrackable1.MainActivity.a が "This in unacceptable..." メッセージボックスを表示する責任があることを取得します。このメソッドは "OK" ボタンを OnClickListener インタフェースを実装するクラスにフックします。"OK" ボタンの onClick イベントハンドラは実際にアプリを終了させるものです。ユーザーがダイアログを単にキャンセルすることを防ぐために、setCancelable メソッドが呼び出されます。
1
private void a(final String title) {
2
final AlertDialog create = new AlertDialog$Builder((Context)this).create();
3
create.setTitle((CharSequence)title);
4
create.setMessage((CharSequence)"This in unacceptable. The app is now going to exit.");
5
create.setButton(-3, (CharSequence)"OK", (DialogInterface$OnClickListener)new b(this));
6
create.setCancelable(false);
7
create.show();
8
}
Copied!
少しのランタイム改竄でこれを回避できます。アプリがまだ停止した状態で、android.app.Dialog.setCancelable にメソッドブレークポイントを設定してアプリを再開します。
1
> stop in android.app.Dialog.setCancelable
2
Set breakpoint android.app.Dialog.setCancelable
3
> resume
4
All threads resumed.
5
>
6
Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,110 bci=0
7
main[1]
Copied!
アプリは setCancelable メソッドの最初の命令で一時停止されます。locals コマンドを使用して setCancelable に渡される引数を出力できます (引数は "local variables" に誤って表示されることに注意します) 。
1
main[1] locals
2
Method arguments:
3
Local variables:
4
flag = true
Copied!
この場合、setCancelable(true) が呼び出されるため、これは私たちが探している呼び出しには当てはまりません。resume コマンドを使用してプロセスを再開します。
1
main[1] resume
2
Breakpoint hit: "thread=main", android.app.Dialog.setCancelable(), line=1,110 bci=0
3
main[1] locals
4
flag = false
Copied!
引数 false での setCancelable の呼び出しにヒットしました。set コマンドで変数に true を設定し、再開します。
1
main[1] set flag = true
2
flag = true = true
3
main[1] resume
Copied!
このプロセスを繰り返します。アラートボックスが最終的に表示されるまで、ブレークポイントにヒットするたびに flagtrue を設定します (ブレークポイントは 5 ~ 6 回ヒットします) 。アラートボックスがキャンセルできるようになりました。ボックスの隣の任意の場所をタップすると、アプリを終了することなく閉じます。
ここでは改竄防止は秘密の文字列を抽出する準備の妨げにはなりません。「静的解析」セクションでは、文字列は AES を使用して解読され、次にメッセージボックスに入力された文字列と比較されることがわかりました。java.lang.String クラスのメソッド equals は入力文字列と秘密の文字列を比較するために使用されます。java.lang.String.equals にメソッドブレークポイントを設定し、エディットフィールドにテキストを入力して、"verify" ボタンをタップします。ブレークポイントがヒットしたら、locals コマンドを使用してメソッド引数を読むことができます。
1
> stop in java.lang.String.equals
2
Set breakpoint java.lang.String.equals
3
>
4
Breakpoint hit: "thread=main", java.lang.String.equals(), line=639 bci=2
5
6
main[1] locals
7
Method arguments:
8
Local variables:
9
other = "radiusGravity"
10
main[1] cont
11
12
Breakpoint hit: "thread=main", java.lang.String.equals(), line=639 bci=2
13
14
main[1] locals
15
Method arguments:
16
Local variables:
17
other = "I want to believe"
18
main[1] cont
Copied!
これが探していた平文の文字列です。
IDEを使用したデバッグ
とてもきれいなやり方は逆コンパイルされたソースで IDE のプロジェクトを設定することです。ソースコードに直接メソッドブレークポイントを設定できます。ほとんどの場合、アプリをシングルステップ実行し、GUI を介して変数の状態を調べることができます。これは完全ではありません。結局のところ、オリジナルのソースコードではないため、行ブレークポイントは設定できませんし、時には何かが正常に動作しないこともあります。また、コードをリバースすることは決して容易ではありませんが、普通の使い古された Java コードを効率的にナビゲートおよびデバッグできることは非常に便利な方法ですので、通常これを実行する価値があります。同様の方法が NetSPI blog [18] に掲載されています。
逆コンパイルされたソースコードからアプリをデバッグするには、上記の "Statically Analyzing Java Code" の部分で説明されているように、まず Android プロジェクトを作成し、逆コンパイルされた Java ソースをソースフォルダにコピーする必要があります。デバッグアプリ (このチュートリアルでは Uncrackable1 ) を設定し、「開発者オプション」から「デバッガを待機」スイッチをオンにしたことを確認します。
ランチャーから Uncrackable アプリアイコンをタップすると、「デバッガを待機」モードで一時停止します。
ここで、ブレークポイントの設定と、ツールバーの "Attach Debugger" ボタンを使用して、Uncrackable1 アプリプロセスにアタッチできます。
逆コンパイルされたソースからアプリをデバッグするときは、メソッドブレークポイントだけが動作することに注意します。メソッドブレークポイントがヒットすると、メソッドの実行中にシングルステップで実行できます。
リストから Uncrackable1 アプリケーションを選択すると、デバッガはアプリプロセスにアタッチし、onCreate() メソッドに設定されたブレークポイントにヒットします。Uncrackable1 アプリは onCreate() メソッド内でアンチデバッグおよび改竄防止コントロールをトリガーします。そのため、改竄防止およびアンチデバッグのチェックが行われる直前の onCreate() メソッドにブレークポイントを設定することをお勧めします。
次に、デバッガビューで "Force Step Into" ボタンをクリックして、onCreate() メソッドをシングルステップ実行します。"Force Step Into" オプションは、通常ではデバッガにより無視される、Android フレームワーク関数とコア Java クラスをデバッグできます。
"Force Step Into" を実行すると、デバッガはクラス sg.vantagepoint.a.ca() メソッドである次のメソッドの先頭で停止します。
このメソッドは well known ディレクトリ内の "su" バイナリを検索します。私たちはルート化されたデバイスやエミュレータでアプリを実行しているため、変数や関数の戻り値を操作してこのチェックを無効にする必要があります。
a() メソッドにステップインすることにより、"Variables" ウィンドウ内にディレクトリ名を見ることができます。また、デバッガビューで "Step Over" することによりメソッドを通過します。
"Force Step Into" 機能を使用して、y を呼び出す System.getenv メソッドにステップインします。
コロンで区切られたディレクトリ名を取得した後、デバッガカーソルは a() メソッドの先頭に戻ります。次の実行可能行ではありません。これはオリジナルのソースコードの代わりに逆コンパイルされたコードで作業しているためです。したがって、解析者は逆コンパイルされたアプリケーションをデバッグする際にコードフローに従うことが重要です。さもなくば、次に実行される行を特定することが困難になる可能性があります。
もしあなたがコアの Java や Android クラスをデバッグしたいと思わない場合は、デバッガビューの "Step Out" をクリックすることにより関数をステップアウトできます。逆コンパイルされたソースに到達したら "Force Step Into" し、コアな Java や Android クラスで "Step Out" することは良いアプローチかもしれません。これはコアクラス関数の戻り値に注目することでデバッグを高速化するのに役立ちます。
ディレクトリ名を取得した後、a() メソッドはこれらのディレクトリ内で </code>su</code> バイナリの存在を検索します。このコントロールを無効にするには、デバイス上で su バイナリを検出するサイクルで、ディレクトリ名 (parent) またはファイル名 (child) を変更します。F2 を押すか、右クリックして "Set Value" で変数の内容を変更します。
バイナリ名またはディレクトリ名を変更すると、File.existsfalse を返します。
これは Uncrackable App Level 1 の最初のルート検出コントロールを無効にします。残りの改竄防止およびアンチデバッグコントロールは同様の方法で無効にし、最終的に秘密の文字列検証機能に到達します。
秘密のコードはクラス sg.vantagepoint.uncrackable1.a のメソッド a() により検証されます。メソッド a() にブレークポイントを設定し、ブレークポイントにヒットしたら "Force Step Into" します。次に、String.equals の呼び出しに到達するまでシングルステップ実行します。これはユーザーが提供した入力と秘密の文字列を比較する箇所です。
String.equals メソッド呼び出しに到達した時点で、"Variables" ビューに秘密の文字列が表示されます。

ネイティブコードのデバッグ

Android のネイティブコードは ELF 共有ライブラリにパックされ、他のネイティブ Linux プログラムと同様に動作します。したがって、標準ツールを使用してデバッグできます。GDB や IDE のビルトインネイティブデバッガ、IDA Pro や JEB などがあります。デバイスのプロセッサアーキテクチャをサポートしているものに限定されます (ほとんどのデバイスは ARM チップセットをベースとしているため、通常は問題ありません) 。
デバッグを行うために JNI デモアプリ HelloWorld-JNI.apk をセットアップします。「ネイティブコードの静的解析」でダウンロードした APK と同じです。adb install を使用して、デバイスまたはエミュレータにインストールします。
1
$ adb install HelloWorld-JNI.apk
Copied!
この章の最初の手順に従っている場合には、すでに Android NDK があるはずです。さまざまなアーキテクチャ用にプレビルドされたバージョンの gdbserver が含まれています。gdbserver binary をデバイスにコピーします。
1
$ adb push $NDK/prebuilt/android-arm/gdbserver/gdbserver /data/local/tmp
Copied!
gdbserver --attach<comm> <pid> コマンドは gdbserver を実行中のプロセスにアタッチし、comm で指定された IP アドレスとポートにバインドします。この場合、HOST:PORT 記述子です。デバイスの HelloWorld-JNI を起動し、デバイスに接続して HelloWorld プロセスの PID を決定します。次に、root ユーザーに切り替えて、gdbserver を以下のようにアタッチします。
1
$ adb shell
2
$ ps | grep helloworld
3
u0_a164 12690 201 1533400 51692 ffffffff 00000000 S sg.vantagepoint.helloworldjni
4
$ su
5
# /data/local/tmp/gdbserver --attach localhost:1234 12690
6
Attached; pid = 12690
7
Listening on port 1234
Copied!
プロセスは現在一時停止しており、gdbserver はクライアントをデバッグするためにポート 1234 で listen しています。デバイスが USB 経由で接続されている場合は、adb forward コマンドを使用して、このポートをホストのローカルポートに転送できます。
1
$ adb forward tcp:1234 tcp:1234
Copied!
NDK ツールチェーンに含まれているプレビルドバージョンの gdb を使用します (まだであれば、上述の手順に従ってインストールします) 。
1
$ $TOOLCHAIN/bin/gdb libnative-lib.so
2
GNU gdb (GDB) 7.11
3
(...)
4
Reading symbols from libnative-lib.so...(no debugging symbols found)...done.
5
(gdb) target remote :1234
6
Remote debugging using :1234
7
0xb6e0f124 in ?? ()
Copied!
プロセスへのアタッチに成功しました。唯一の問題は、JNI 関数 StringFromJNI() をデバッグするにはこの時点では遅すぎることです。この関数は起動時に一度しか実行されないためです。この問題を解決するには「デバッガの待機」オプションを有効にします。「開発者オプション」->「デバッグアプリの選択」に行き、HelloWorldJNI を選択してから、「デバッガの待機」スイッチを有効にします。その後、アプリを終了および再起動します。自動的に一時停止します。
目的はアプリを再開する前にネイティブ関数 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI() の最初にブレークポイントを設定することです。残念ながら、実行でのこの早い時点ではこれはできません。libnative-lib.so はまだプロセスメモリにマップされていないためです。これは実行時に動的にロードされます。これを実現するために、まず JDB を使用して、プロセスを必要な状態に穏やかに制御します。
まず、JDB をアタッチすることにより Java VM の実行を再開します。しかし、プロセスをすぐに再開したいわけではないため、以下のように suspend コマンドを JDB にパイプします。
1
$ adb jdwp
2
14342
3
$ adb forward tcp:7777 jdwp:14342
4
$ { echo "suspend"; cat; } | jdb -attach localhost:7777
Copied!
次に、Java ランタイムが libnative-lib.so をロードする時点でプロセスを一時停止します。JDB で java.lang.System.loadLibrary() メソッドにブレークポイントを設定し、プロセスを再開します。ブレークポイントにヒットした後、step up コマンドを実行します。これにより loadLibrary() が返るまでプロセスが再開します。この時点で、libnative-lib.so がロードされています。
stop in java.lang.System.loadLibrary resume All threads resumed. Breakpoint hit: "thread=main", java.lang.System.loadLibrary(), line=988 bci=0 step up main[1] step up
Step completed: "thread=main", sg.vantagepoint.helloworldjni.MainActivity.(), line=12 bci=5
main[1]
1
<code>gdbserver</code> を実行して一時停止しているアプリにアタッチします。これにより Java VM と Linuxx カーネルの両方によりアプリが "double-suspended" するという効果があります。
2
3
4
```bash
5
$ adb forward tcp:1234 tcp:1234
6
$ $TOOLCHAIN/arm-linux-androideabi-gdb libnative-lib.so
7
GNU gdb (GDB) 7.7
8
Copyright (C) 2014 Free Software Foundation, Inc.
9
(...)
10
(gdb) target remote :1234
11
Remote debugging using :1234
12
0xb6de83b8 in ?? ()
Copied!
JDB で resume コマンドを実行して Java ランタイムの実行を再開します (JDB を使用していますが、この時点でデタッチすることもできます) 。GDB でプロセスを探索することができます。info sharedlibrary コマンドはロードされたライブラリを表示します。それには libnative-lib.so が含まれています。info functions コマンドはすべての既知の関数のリストを取得します。JNI 関数 java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI() は非デバッグシンボルとしてリストされている必要があります。関数のアドレスにブレークポイントを設定し、プロセスを再開します。
1
(gdb) info sharedlibrary
2
(...)
3
0xa3522e3c 0xa3523c90 Yes (*) libnative-lib.so
4
(gdb) info functions
5
All defined functions:
6
7
Non-debugging symbols:
8
0x00000e78 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI
9
(...)
10
0xa3522e78 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI
11
(...)
12
(gdb) b *0xa3522e78
13
Breakpoint 1 at 0xa3522e78
14
(gdb) cont
Copied!
JNI 関数の最初の命令が実行されたときにブレークポイントがヒットします。disassemble コマンドを使用して、関数の逆アセンブリを表示できます。
1
Breakpoint 1, 0xa3522e78 in Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI() from libnative-lib.so
2
(gdb) disass $pc
3
Dump of assembler code for function Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI:
4
=> 0xa3522e78 <+0>: ldr r2, [r0, #0]
5
0xa3522e7a <+2>: ldr r1, [pc, #8] ; (0xa3522e84 <Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI+12>)
6
0xa3522e7c <+4>: ldr.w r2, [r2, #668] ; 0x29c
7
0xa3522e80 <+8>: add r1, pc
8
0xa3522e82 <+10>: bx r2
9
0xa3522e84 <+12>: lsrs r4, r7, #28
10
0xa3522e86 <+14>: movs r0, r0
11
End of assembler dump.
Copied!
ここから、プログラムをシングルステップ実行して、レジスタやメモリの内容を表示したり、それらを改竄して、JNI 関数の内部動作を調べます (このケースでは、単に文字列を返します) 。help コマンドを使用して、デバッグ、実行、およびデータの検査に関する詳細情報を取得します。

実行トレース

デバッグに役立つだけでなく、JDB コマンドラインツールは基本的な実行トレース機能も提供します。アプリを最初から正しくトレースするには、Android の「デバッガを待機」機能または kill –STOP コマンドを使用してアプリを一時停止し、JDB をアタッチして、私たちが選択する初期化メソッドに遅延メソッドブレークポイントを設定します。このブレークポイントがヒットすると、trace go methods コマンドでメソッドトレースをアクティブにし、実行を再開します。JDB はすべてのメソッドのエントリをダンプして、その場所から出ます。
1
$ adb forward tcp:7777 jdwp:7288
2
$ { echo "suspend"; cat; } | jdb -attach localhost:7777
3
Set uncaught java.lang.Throwable
4
Set deferred uncaught java.lang.Throwable
5
Initializing jdb ...
6
> All threads suspended.
7
> stop in com.acme.bob.mobile.android.core.BobMobileApplication.<clinit>()
8
Deferring breakpoint com.acme.bob.mobile.android.core.BobMobileApplication.<clinit>().
9
It will be set after the class is loaded.
10
> resume
11
All threads resumed.M
12
Set deferred breakpoint com.acme.bob.mobile.android.core.BobMobileApplication.<clinit>()
13
14
Breakpoint hit: "thread=main", com.acme.bob.mobile.android.core.BobMobileApplication.<clinit>(), line=44 bci=0
15
main[1] trace go methods
16
main[1] resume
17
Method entered: All threads resumed.
Copied!
Dalvik Debug Monitor Server (DDMS) は Android Studio に付属する GUI ツールです。一見するとそれはらしくありませんが、間違えてはいけません。その Java メソッドトレーサーはあなたの武器として持つことができる最も素晴らしいツールのひとつであり、難読化されたバイトコードの解読には不可欠です。
しかし、DDMS を使用するのはちょっと混乱します。これはいくつかの方法でき、トレースの取得方法に応じて異なるトレースビューアが起動されます。Android Studio には "Traceview" と呼ばれるスタンドアロンのツールとビルトインのビューアがあり、両方ともトレースをナビゲートするさまざまな方法があります。通常 Android Studio にビルトインされたビューアを使用したいと思うでしょう。すべてのメソッド呼び出しの素晴らしく、ズーム可能な階層的なタイムラインを与えます。しかし、スタンドアロンツールも便利です。それはプロファイルパネルを持っていて、各メソッドの消費時間だけでなく、各メソッドの親と子も表示します。
Android Studio で実行トレースを記録するには、GUI の下部にある "Android" タブを開きます。リスト内の対象プロセスを選択し、左の小さな「ストップウォッチ」ボタンをクリックします。これで記録を開始します。完了したら、同じボタンをクリックして記録を停止します。統合されたトレースビューが開き、記録されたトレースが表示されます。マウスやトラックパッドを使用してタイムラインビューをスクロールおよびズームできます。
また、実行トレースはスタンドアロンの Android Device Monitor で記録することもできます。Device Monitor は Android Studio 内から起動する (Tools -> Android -> Android Device Monitor) ことも、ddms コマンドでシェルから起動することもできます。
トレース情報の記録を開始するには、"Devices" タブで対象のプロセスを選択して "Start Method Profiling" ボタンをクリックします。記録を停止するには停止ボタンをクリックします。その後、Traceview ツールを開き、記録されたトレースを表示します。スタンドアロンツールの主要な機能は下部の "profile" パネルです。各メソッドの消費時間の概要、および各メソッドの親と子を表示します。profile パネルで任意のメソッドをクリックすると、タイムラインパネルで選択したメソッドが強調表示されます。
また、DDMS は便利なヒープダンプボタンを提供します。これはプロセスの Java ヒープを .hprof ファイルにダンプします。Traceview の詳細については Android Studio user guide を参照ください。
システムコールのトレース
OS 階層のレベルを下がると、Linux カーネルの能力を必要とする特権的な機能に到達します。これらの機能はシステムコールインタフェースを介して通常のプロセスで利用できます。カーネルへの呼び出しの計装と傍受はユーザープロセスが何をしているかを大まかに知る有効な方法であり、低レベルの改竄防御を無効にする最も効率的な方法です。
strace は標準的な Linux ユーティリティで、プロセスとカーネルの間の相互作用を監視するために使用されます。このユーティリティはデフォルトで Android に含まれていませんが、Android NDK を使用してソースから簡単にビルドできます。これによりプロセスのシステムコールを監視する非常に便利な方法が得られます。しかし、strace は対象プロセスにアタッチする ptrace() システムコールに依存しているため、アンチデバッグ対策が開始されるところまでのみ動作します。
補足として、その Android が「起動時にアプリケーションを停止する」機能が利用できない場合、シェルスクリプトを使用して、プロセスが実行された直後に strace がアタッチするようにできます (上品な解決策ではありませんが動作はします) 。
1
$ while true; do pid=$(pgrep 'target_process' | head -1); if [[ -n "$pid" ]]; then strace -s 2000 - e “!read” -ff -p "$pid"; break; fi; done
Copied!
Ftrace
ftrace は Linux カーネルに直接組み込まれたトレースユーティリティです。ルート化デバイスでは、ftrace を使用して、strace で可能なよりも透過的な方法でカーネルシステムコールをトレースできます。strace は ptrace システムコールに依存して、対象プロセスにアタッチします。
便利なことに、ftrace の機能は Lollipop と Marshmallow の両方で出荷された Android カーネルにあります。以下のコマンドで有効にできます。
1
$ echo 1 > /proc/sys/kernel/ftrace_enabled
Copied!
/sys/kernel/debug/tracing ディレクトリは ftrace に関連するすべてのコントロールと出力ファイルを保持しています。このディレクトリには以下のファイルがあります。
  • available_tracers: このファイルはカーネル内にコンパイルされている利用可能なトレーサーをリストします。
  • current_tracer: このファイルは現在のトレーサーを設定または表示するために使用されます。
  • tracing_on: このファイルに 1 をエコーして、リングバッファの更新を許可・開始します。0 をエコーすると、リングバッファにそれ以上の書き込みを抑制します。
KProbes
KProbes インタフェースはカーネルを計装するためのさらに強力な方法を提供します。カーネルメモリ内の (ほぼ) 任意のコードアドレスにプローブを挿入することができます。KProbes は指定されたアドレスにブレークポイント命令を挿入することによって動作します。ブレークポイントがヒットすると、コントロールは KProbes システムに渡され、元の命令と同様にユーザーにより定義されたハンドラ関数が実行されます。関数トレースに最適であるほか、KProbes はファイル隠蔽などのルートキット風機能を実装するために使用できます。
Jprobes と Kretprobes は KProbes をベースにした追加のプローブタイプであり、関数のエントリと終了をフックできます。
残念ながら、出荷された Android カーネルはロード可能モジュールのサポートなしで提供されます。これは KProbes が通常カーネルモジュールとして配置されるため問題です。別の問題は、Android カーネルが厳しいメモリ保護でコンパイルされることです。これはカーネルメモリの一部にパッチを当てることを防止します。Elfmaster のシステムコールフック手法 [16]</code> を使用すると、デフォルトの Lollipop と Marshmallow は sys_call_table が書き込み不可であるためカーネルパニックになります。しかし、私たちは独自のより穏やかなカーネルをコンパイルすることによりサンドボックス上で KProbes を使用できます (詳細は後述) 。

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

Android SDK に同梱されている標準的な形式の中でも、Android エミュレータ (通称 "エミュレータ") は幾分可能なリバースエンジニアリングツールです。これは QEMU をベースにしています。汎用的でオープンソースのマシンエミュレータです。QEMU はゲスト命令をオンザフライでホストプロセッサが理解できる命令に変換することによりゲスト CPU をエミュレートします。ゲスト命令の各基本ブロックは逆アセンブルされ、Tiny Code Generator (TCG) と呼ばれる中間表現に変換されます。TCG ブロックはホスト命令のブロックにコンパイルされ、コードキャッシュに格納され、実行されます。基本ブロックの実行が完了すると、QEMU はゲスト命令の次のブロックの処理を繰り返します (またはキャッシュからすでに変換されたブロックをロードします) 。全体のプロセスは動的バイナリ変換と呼ばれます。
Android エミュレータは QEMU のフォークであるため、モニタリング、デバッグ、トレース機能を含む完全な QEMU 機能セットが提供されます。QEMU 固有のパラメータは -qemu コマンドラインフラグでエミュレータに渡すことができます。QEMU のビルトイントレース機能を使用して、実行された命令および仮想レジスタの値を記録できます。単に "-d" コマンドラインフラグで qemu を起動すると、実行されるゲストコード、マイクロオペレーション、ホスト命令のブロックがダンプされます。-d in_asm オプションはゲストコードのすべての基本ブロックが QEMU の変換機能に入るときに記録します。以下のコマンドはすべての変換されたブロックをファイルに記録します。
1
$ emulator -show-kernel -avd Nexus_4_API_19 -snapshot default-boot -no-snapshot-save -qemu -d in_asm,cpu 2>/tmp/qemu.log
Copied!
残念ながら、QEMU で完全なゲスト命令トレースを生成することはできません。コードブロックは変換されたときにのみログに書き込まれるためです。キャッシュから取得されたときではありません。例えば、ブロックがループで繰り返し実行される場合、最初の反復のみがログに出力されます。QEMU で TB キャッシュを無効にする方法はありません (ソースコードをハックして保存します) 。それでも、ネイティブに実行される暗号アルゴリズムの逆アセンブリを再構築するなど、基本的なタスクには機能は十分です。
PANDA や DroidScope などの動的解析フレームワークは QEMU 上に構築され、より完全なトレース機能を提供します。PANDA/PANDROID は CPU トレースベースの解析を行う場合には最適です。完全なトレースを簡単に記録および再生できますし、Ubuntu のビルド手順に従えば比較的簡単にセットアップできます。
DroidScope
DroidScope - DECAF 動的解析フレームワークの拡張 [20] - は QEMU をベースとしたマルウェア解析エンジンです。いくつかのレベルでの計装を追加します。ハードウェア、Linux、Java レベルでセマンティクスを完全に再構築できます。
DroidScope は実際の Android デバイスのさまざまなコンテキストレベル (ハードウェア、OS、Java) を反映する計装 API をエクスポートします。解析ツールはこれらの API を使用して、情報を照会または設定し、さまざまなイベントのコールバックを登録できます。例えば、プラグインはネイティブ命令の開始と終了、メモリの読み書き、レジスタの読み書き、システムコールや Java メソッドコールに対してコールバックを登録できます。
これによりターゲットアプリケーションに事実上透過であるトレーサーを作成することができます (エミュレータで実行されていることを隠すことができる限りにおいて) 。制限としては DroidScope が Dalvik VM とのみ互換性があることです。
PANDA
PANDA [21] はもうひとつの QEMU ベースの動的解析プラットフォームです。DroidScope と同様に、PANDA は特定の QEMU イベントでトリガーされるコールバックを登録することで拡張できます。PANDA には記録/再生機能が追加されています。これにより反復的なワークフローが可能になります。リバースエンジニアはあるターゲットアプリ (またはその一部) の実行トレースを記録し、それを何度も繰り返し再生して、各反復での解析プラグインを洗練します。
PANDA は文字列検索ツールやシステムコールトレーサなど、既製のプラグインが付属しています。最も重要なことは、Android ゲストもサポートしており、DroidScope コードの一部がすでに移植されていることです。PANDA for Android ("PANDROID") のビルドと実行は比較的簡単です。これをテストするには、Moiyx の git リポジトリをクローンし、以下のように PANDA をビルドします。
1
$ cd qemu
2
$ ./configure --target-list=arm-softmmu --enable-android $ makee
Copied!
この執筆時点では、Android バージョン 4.4.1 までが PANDROID で正常に動作しますが、これより新しいものは起動しません。また、Java レベルのイントロスペクションコードは Android 2.3 の特定の Dalvik ランタイムでのみ動作します。とにかく、古いバージョンの Android はエミュレータ上でより高速に動作しているようですので、PANDA を使用することを計画しているのであれば、Gingerbread に固着しておくのがおそらく最適です。詳細については、PANDA git repo の豊富な毒めんとを参照ください。

VxStripper

QEMU で構築されたもうひとつの有用なツールは Sébastien Josse の VxStripper [22] です。VXStripper はバイナリを逆難読化するために特別に設計されています。QEMU の動的バイナリ変換メカニズムを計装することにより、バイナリの中間表現を動的に抽出します。抽出された中間表現に簡略化を適用し、LLVM を使用して簡略化されたバイナリを再コンパイルします。これは難読化されたプログラムを正規化する非常に強力な方法です。詳細については Sébastien の論文 [23] を参照ください。

改竄と実行時計装

まず、モバイルアプリの改変および計装の簡単な方法をいくつか見ていきます。改竄 とは、アプリにパッチやランタイムの変更を加えて、通常私たちの利益となる方法で、その動作に影響を及ぼすことです。例えば、テストプロセスを阻む SSL ピンニングやバイナリ保護を無効にすることが望ましい場合があります。実行時計装 は、フックおよびランタイムパッチを追加して、アプリの動作を観察することです。但し、モバイルアプリのセキュリティでは、この用語はメソッドをオーバーライドして動作を変更するなど、すべての種類のランタイム操作を参照するものとしてかなりゆるく使用されています。

パッチ適用と再パッケージ化

アプリのマニフェストやバイトコードに小さな変更を加えることは、アプリのテストやリバースエンジニアリングを妨げる小さな困りごとを修正する最も簡単な方法です。Android では、特に二つの問題が定期的に持ち上がります。
  1. 1.
    Manifest で android:debuggable フラグが true に設定されていないため、デバッガがアプリにアタッチできない。
  2. 2.
    アプリが SSL ピンニングを使用しているため、プロキシで HTTPS トラフィックを傍受できない。
ほとんどの場合、いずれの問題も軽微な変更とアプリの再パッケージおよび再署名により解決できます (例外として、デフォルトの Android コード署名以外に追加の整合性チェックを実行するアプリがあります。この場合には、追加のチェックにも同様にパッチを当てる必要があります) 。

事例: SSL ピンニングの無効化

正当な理由で HTTPS 通信を傍受したいセキュリティテスターにとって、証明書ピンニングは問題です。この問題を解決するために、バイトコードにパッチを適用して、SSL ピンニングを無効にできます。証明書ピンニングをバイパスする方法を示すために、サンプルアプリケーションに実装された証明書ピンニングをバイパスするために必要な手順を実行します。
最初のステップでは apktool を使用して APK を逆アセンブルします。
1
$ apktool d target_apk.apk
Copied!
Smali ソースコードで証明書ピンニングチェックを見つける必要があります。"X509TrustManager" などのキーワードで smali コードを検索することで、正しい方向に向かいます。
この例では、"X509TrustManager" を検索するとカスタムの Trustmanager を実装するクラスがひとつ返されます。この派生クラスは checkClientTrusted, checkServerTrusted, getAcceptedIssuers という名前のメソッドを実装します。
実行をバイパスするために、これらの各メソッドの最初の行に return-void オペコードを追加します。これにより各メソッドは直ちに戻ります。この変更により、証明書チェックは実行されず、アプリケーションはすべての証明書を受け入れます。
1
.method public checkServerTrusted([LJava/security/cert/X509Certificate;Ljava/lang/String;)V
2
.locals 3
3
.param p1, "chain" # [Ljava/security/cert/X509Certificate;
4
.param p2, "authType" # Ljava/lang/String;
5
6
.prologue
7
return-void # <-- OUR INSERTED OPCODE!
8
.line 102
9
iget-object v1, p0, Lasdf/t$a;->a:Ljava/util/ArrayList;
10
11
invoke-virtual {v1}, Ljava/util/ArrayList;->iterator()Ljava/util/Iterator;
12
13
move-result-object v1
14
15
:goto_0
16
invoke-interface {v1}, Ljava/util/Iterator;->hasNext()Z
Copied!

Xposed で Java メソッドのフック

Xposed は "APK に触れることなくシステムやアプリの動作を変更できるモジュールのフレームワーク" [24]</code> です。技術的には、新しいプロセスが開始されたときに Java コードを実行するための API をエクスポートする Zygote の拡張バージョンです。新しくインスタンス化されたアプリのコンテキストで Java コードを実行することにより、アプリに属する Java メソッドを解決、フック、オーバーライドすることが可能です。Xposed は reflection を使用して、実行中のアプリを調査および変更します。変更はメモリに適用され、プロセスの実行中にのみ維持されます。アプリケーションファイルへのパッチは作成されません。
Xposed を使用するには、まずルート化されたデバイスに Xposed フレームワークをインストールする必要があります。変更は個別のアプリ ("modules") の形式で展開され、Xposed GUI でオンとオフを切り替えることができます。

事例: XPosedでのルート検出のバイパス

ルート化されたデバイスで頑なに終了してしまうアプリをテストしていると仮定します。あなたはアプリを逆コンパイルし、次の非常に疑わしいメソッドを見つけました。
1
package com.example.a.b
2
3
public static boolean c() {
4
int v3 = 0;
5
boolean v0 = false;
6
7
String[] v1 = new String[]{"/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/",
8
"/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/"};
9
10
int v2 = v1.length;
11
12
for(int v3 = 0; v3 < v2; v3++) {
13
if(new File(String.valueOf(v1[v3]) + "su").exists()) {
14
v0 = true;
15
return v0;
16
}
17
}
18
19
return v0;
20
}
Copied!
このメソッドはディレクトリのリストを繰り返し処理し、su バイナリがそれらのいずれかで見つかった場合に "true" (デバイスはルート化されている) を返します。このようなチェックは簡単に無効化できます。あなたがしなければならないことは、"false" を返すものでコードを置き換えることだけです。Xposed モジュールを使用したメソッドフックはこれを行う方法のひとつです。
このメソッド XposedHelpers.findAndHookMethod では既存のクラスメソッドをオーバーライドできます。逆コンパイルされたコードから、チェックを実行するメソッドは c() と呼ばれ、クラス com.example.a.b にあることがわかります。常に "false" を返すように関数をオーバーライドする Xposed モジュールは以下のようになります。
1
package com.awesome.pentestcompany;
2
3
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
4
import de.robv.android.xposed.IXposedHookLoadPackage;
5
import de.robv.android.xposed.XposedBridge;
6
import de.robv.android.xposed.XC_MethodHook;
7
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
8
9
public class DisableRootCheck implements IXposedHookLoadPackage {
10
11
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
12
if (!lpparam.packageName.equals("com.example.targetapp"))
13
return;
14
15
findAndHookMethod("com.example.a.b", lpparam.classLoader, "c", new XC_MethodHook() {
16
@Override
17
18
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
19
XposedBridge.log("Caught root check!");
20
param.setResult(false);
21
}
22
23
});
24
}
25
}
Copied!
Xposed 用のモジュールは通常の Android アプリと同じように Android Studio で開発およびデプロイされます。Xposed モジュールのコンパイルおよびインストールの詳細については、著者 rovo89 が提供するチュートリアルを参照ください [24] 。

FRIDA で動的計装

Frida は "Windows, macOS, Linux, iOS, Android, QNX 上のネイティブアプリに JavaScript のスニペットや独自のライブラリを注入することができます" [26] 。もともと Google の V8 Javascript ランタイムをベースにしていましたが、バージョン 9 Frida では Duktape を内部的に使用しています。
コードインジェクションはさまざまな方法で実現できます。例えば、Xposed は Android アプリローダーを永続的に改変し、新しいプロセスが開始されるたびに独自のコードを実行するフックを提供します。これとは対照的に、Frida は直接的にプロセスメモリにコードを書き込むことによりコードインジェクションを実現します。このプロセスの概要を以下にもう少し詳しく説明します。
Frida を実行中のアプリに "アタッチ" すると、ptrace を使用して実行中のプロセスのスレッドをハイジャックします。このスレッドはメモリのチャンクを割り当て、ミニブートストラップを埋め込むために使用されます。ブートストラップは新しいスレッドを開始し、デバイス上で実行中の Frida デバッグサーバーに接続し、Frida エージェントと計装コードを含む動的に生成されたライブラリファイルをロードします。元のハイジャックされたスレッドは元の状態に復元され、再開され、プロセスの実行は通常通りに継続されます。
Frida は、ネイティブ関数の呼び出しおよびフック、構造化されたデータのメモリへの注入など、豊富で有用な機能を提供する強力な API に加えて、完全な JavaScript ランタイムをプロセスに注入します。また、VM 内のオブジェクトとのやりとりなど、Android Java ランタイムとのやりとりもサポートします。
Frida
FRIDA Architecture, source: http://www.frida.re/docs/hacking/
FRIDA が Android で提供する API のいくつかを紹介します。
  • Java オブジェクトをインスタンス化し、静的および非静的クラスメソッドを呼び出します
  • Java メソッドの実装を置き換えます
  • Java ヒープをスキャンして特定のクラスのライブインスタンスを列挙します (Dalvik のみ)
  • 文字列の発生をプロセスメモリでスキャンします
  • ネイティブ関数呼び出しをインターセプトして、関数の入口と出口で独自のコードを実行します
一部の機能は残念ながら現在の Android デバイスプラットフォーム上では動作しません。特に、FRIDA Stalker - 動的再コンパイルに基づくコードトレースエンジン - はこの執筆時 (バージョン 7.2.0) では ARM をサポートしていません。また、ART のサポートは最近になって含まれたため、Dalvik ランタイムはまだサポートされています。

Frida のインストール

Frida をローカルにインストールするには、単に PyPI を使用します。
1
$ sudo pip install frida
Copied!
Frida を実行するために Android デバイスをルート化する必要はありませんが、それはセットアップが容易であり、特に断りがない限りここではルート化デバイスを想定しています。Frida リリースページ から frida-server バイナリをロードします。サーバーバージョン (少なくともメジャーバージョン番号) がローカルにインストールした Frida のバージョンと一致することを確認します。通常、PyPI は最新バージョンの Frida をインストールしますが、わからない場合には、Frida コマンドラインツールで確認できます。
1
$ frida --version
2
9.1.10
3
$ wget https://github.com/frida/frida/releases/download/9.1.10/frida-server-9.1.10-android-arm.xz
Copied!
frida-server をデバイスにコピーして実行します。
1
$ adb push frida-server /data/local/tmp/
2
$ adb shell "chmod 755 /data/local/tmp/frida-server"
3
$ adb shell "su -c /data/local/tmp/frida-server &"
Copied!
frida-server が実行しているため、以下のコマンドで実行中のプロセスのリストを取得できます。
1
$ frida-ps -U
2
PID Name
3
----- --------------------------------------------------------------
4
276 adbd
5
956 android.process.media
6
198 bridgemgrd
7
1191 com.android.nfc
8
1236 com.android.phone
9
5353 com.android.settings
10
936 com.android.systemui
11
(...)
Copied!
-U オプションは Frida に USB デバイスやエミュレータを検索させます。
特定の (低レベルの) ライブラリ呼び出しをトレースするには、frida-trace コマンドラインツールを使用します。
1
frida-trace -i "open" -U com.android.chrome
Copied!
__handlers__/libc.so/open.js に少しの javascript を生成します。これは Frida がプロセスに注入し、libc.soopen 関数へのすべての呼び出しをトレースするものです。Fridas Javascript API を使用して、必要に応じて生成されたスクリプトを変更できます。
Frida を対話的に操作するには、frida CLI を使用できます。プロセスにフックし、Frida の API に対するコマンドラインインタフェースを提供します。
1
frida -U com.android.chrome
Copied!
frida CLI を使用して、-l オプションを介してスクリプトをロードします。例えば、myscript.js をロードします。
1
frida -U -l myscript.js com.android.chrome
Copied!
Frida はまた、Android アプリを扱うのに特に役立つ Java API を提供しています。Java クラスとオブジェクトを直接的に操作することができます。これは Activity クラスの "onResume" 関数を上書きするスクリプトです。
1
Java.perform(function () {
2
var Activity = Java.use("android.app.Activity");
3
Activity.onResume.implementation = function () {
4
console.log("[*] onResume() got called!");
5
this.onResume();
6
};
7
});
Copied!
上のスクリプトは Java.perform を呼び出し、コードが Java VM のコンテキストで実行されるようにします。Java.use を介して android.app.Activity クラスのラッパーをインスタンス化し、onResume 関数を上書きします。新しい onResume 関数はコンソールに通知を出力し、アクティビティがアプリで再開されるたびに this.onResume を呼び出すことにより、元の onResume メソッドを呼び出します。
Frida はまたヒープ上のインスタンス化されたオブジェクトを検索し、それらで作業することもできます。以下のスクリプトは android.view.View オブジェクトのインスタンスを検索し、toString メソッドを呼び出します。結果はコンソールに出力されます。
1
setImmediate(function() {
2
console.log("[*] Starting script");
3
Java.perform(function () {
4
Java.choose("android.view.View", {
5
"onMatch":function(instance){
6
console.log("[*] Instance found: " + instance.toString());
7
},
8
"onComplete":function() {
9
console.log("[*] Finished heap search")
10
}
11
});
12
});
13
});
Copied!