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 は以下の場所にあります。
注意: 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 ルートディレクトリに移動して、以下のコマンドを実行します。
これにより、ディレクトリ /tmp/android-7-toolchain
に Android 7.0 のスタンドアローンツールチェーンが作成されます。都合により、あなたのツールチェーンディレクトリを指す環境変数をエクスポートできます。これについては後の例で使用します。以下のコマンドを実行するか、.bash_profile
または他の起動スクリプトに追加します。
フリーのリバースエンジニアリング環境の構築
少しの労力で、リーズナブルな 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 についての内容を確認します。
なんらかの秘密のコードが見つかることを期待しています。
おそらく、アプリ内のどこかに格納された秘密の文字列を探しています。そのため、次の論理的なステップは内部を見て回ることです。まず、APK ファイルを展開して内容を確認します。
基本的には、アプリに関連するすべての 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
をパッケージ化し、抽出、変換、逆コンパイルの手順を自動化します。以下のようにインストールします。
これは apkx
を /usr/local/bin
にコピーする必要があります。UnCrackable-Level1.apk
で実行します。
逆コンパイルされたソースはディレクトリ 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.a
の MainActivity
クラスを開きます。メソッド verify
は "verify" ボタンをタップすると呼び出されるものです。このメソッドはユーザー入力をブール値を戻す a.a
という静的メソッドに渡します。a.a
はユーザーにより入力されたテキストが有効であるかどうかを検証することを担っていると思われるため、コードをリファクタリングしてこれを反映します。
クラス名 (a.a
の最初の a
) を右クリックし、ドロップダウンメニューから Refactor->Rename を選択します (または Shift-F6 を押します) 。これまでにそのクラスについて判ったことを考慮して、クラス名をより意味のあるものに変更します。例えば、"Validator" とします (後でクラスについてより詳しく知ったとき、その名前をいつでも変更できます) 。ここで a.a
は Validator.a
になります。同じ手順に従って、静的メソッド a
の名前を check_input
に変更します。
おめでとう。あなたは静的解析の基礎を学びました。解析されたプログラムについて理論化、注釈付け、および段階的に理論を改訂することがすべてです。あなたが完全にそれを理解するか、少なくとも十分に達成したと思うまで行います。
次に、check_input
メソッドで ctrl+クリック (Mac では command+クリック) します。メソッド定義に移動します。逆コンパイルされたメソッドは以下のようになります。
そして、パッケージ sg.vantagepoint.a.a
の a
という名前の関数に渡される base64 エンコードされた String があります (ここでもすべてが a
と呼ばれます) 。また、16進数にエンコードされた暗号化鍵のようなものがあります (16 hex bytes = 128bit, 共通鍵の長さ) 。この特定の a
は正確には何をするでしょうか。Ctrl を押しながらクリックして調べます。
ここで大体出来上がりです。それは単に標準の AES-ECB です。check_input
の arrby1
に格納されている 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 デバイスにインストールして実行します。
このアプリにまったく華々しさはありません。テキスト "Hello from C++" のラベルが表示されることがすべてです。実のところ、これは C/C++ サポートありで新しいプロジェクトを作成したときに Android Studio が生成するデフォルトアプリですが、JNI の呼び出し方法の基本的な原則を示すには十分です。
apkx
で APK を逆コンパイルします。これはソースコードを HelloWorld/src
ディレクトリに抽出します。
MainActivity はファイル MainActivity.java
にあります。"Hello World" テキストビューは onCreate()
メソッドで設定されています。
下部にある public native String stringFromJNI
の宣言に注目します。native
キーワードはこのメソッドの実装がネイティブ言語で提供されていることを Java コンパイラに通知します。対応する関数は実行時に解決します。もちろん、これは期待されるシグネチャを持つグローバルシンボルをエクスポートするネイティブライブラリがロードされる場合にのみ機能します。このシグネチャはパッケージ名、クラス名、メソッド名で構成されます。この例の場合には、これはプログラマが以下の C または C++ 関数を実装する必要があることを意味します。
では、この関数のネイティブ実装はどこにあるのでしょうか。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
を使用しています。
これは 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 環境とやりとりできるようにします。
これを念頭に置いて、アセンブリコードの各行を見てみましょう。
思い出してください。第一引数 (R0 にあります) は JNI 関数テーブルポインタへのポインタです。LDR
命令はこの関数テーブルポインタを R2 にロードします。
この命令は文字列 "Hello from C++" の PC 相対オフセットを R1 にロードします。この文字列はオフセット 0xe84 の関数ブロックの終わりの直後に配置されています。プログラムカウンタに相対するアドレッシングにより、コードはメモリ内の位置とは無関係に実行できます。
この命令はオフセット 0x29C から関数ポインタを R2 の JNI 関数ポインタテーブルにロードします。これは NewStringUTF
関数になります。Android NDK に含まれている jni.h に関数ポインタの一覧があります。関数プロトタイプは以下のようになります。
この関数は二つの引数を必要とします。JNIEnv ポインタ (すでに R0 にあります) と文字列ポインタです。次に、PC の現在値に R1 を加えられ、静的文字列 "Hello from C++" の絶対アドレスになります (PC + オフセット) 。
最後に、プログラムは R2 にロードされた NewStringUTF 関数ポインタへの分岐命令を実行します。
この関数が返るとき、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
が含まれています。独自の署名証明書と鍵を作成し、以下のようにデバッグキーストアに追加できます。
証明書が利用可能になったので、以下の手順でアプリを再パッケージできます。Android Studio のビルドツールディレクトリは [SDK-Path]/build-tools/[version]
にあります。zipalign
と apksigner
ツールがこのディレクトリにあります。UnCrackable-Level1.apk を以下のように再パッケージします。
apktool を使用してアプリをアンパックし、AndroidManifest.xml をデコードします。
テキストエディタを使用してマニフェストに android:debuggable = "true" を追加します。
APK を再パッケージして署名します。
注意:apksigner
で JRE の互換性の問題が発生した場合は、代わりに jarsigner
を使用できます。この場合、署名 後 に zipalign
を呼び出すことに注意します。
アプリを再インストールします。
「デバッガを待機」機能
UnCrackable アプリは愚かではありません。デバッグ可能モードで実行されていることに気付き、シャットダウンに反応します。すぐにモーダルダイアログが表示され、OK ボタンをタップすると crackme が終了します。
幸いなことに、Android の開発者オプションには便利な「デバッガを待機」機能があり、JDWP デバッガが接続されるまで、起動している選択されたアプリを自動的に中断できます。この機能を使用すると、検出メカニズムが実行される前にデバッガを接続し、そのメカニズムをトレース、デバッグ、非アクティブ化を行うことができます。これは本当にアンフェアな利点ですが、一方で、リバースエンジニアはフェアにプレーする必要はありません。
開発者設定で、デバッグするアプリケーションとして Uncrackable1
を選択し、「デバッガを待機」スイッチをアクティブにします。
注意:default.prop
で ro.debuggable
を 1 に設定しても、マニフェストで android:debuggable
フラグが true
に設定されるまで、アプリは「デバッグアプリを選択」リストに現れません。
Android Debug Bridge
Android SDK に付属の adb
コマンドラインツールはローカル開発環境と接続されている Android デバイスとの橋渡しをします。通常、エミュレータや USB 経由で接続されたデバイスでアプリをデバッグします。adb devices
コマンドを使用して、現在接続されているデバイスを一覧表示します。
adb jdwp
コマンドは接続されたデバイス上で実行されているすべてのデバッグ可能なプロセス (つまり、JDWP トランスポートをホストしているプロセス) のプロセス ID を一覧表示します。adb forward
コマンドを使用すると、ホストマシン上にリスニングソケットを開き、選択したプロセスの JDWP トランスポートにこのソケットの TCP 接続を転送できます。
これで JDB をアタッチする準備ができました。デバッガをアタッチすると、アプリが再開しますが、これは我々が望むものではありません。むしろ、最初にいくつかの調査を行えるように、中断しておきたい。プロセスが再開しないように、suspend
コマンドを jdb にパイプします。
中断したプロセスにアタッチされ、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
メソッドが呼び出されます。
少しのランタイム改竄でこれを回避できます。アプリがまだ停止した状態で、android.app.Dialog.setCancelable
にメソッドブレークポイントを設定してアプリを再開します。
アプリは setCancelable
メソッドの最初の命令で一時停止されます。locals
コマンドを使用して setCancelable
に渡される引数を出力できます (引数は "local variables" に誤って表示されることに注意します) 。
この場合、setCancelable(true)
が呼び出されるため、これは私たちが探している呼び出しには当てはまりません。resume
コマンドを使用してプロセスを再開します。
引数 false
での setCancelable
の呼び出しにヒットしました。set
コマンドで変数に true
を設定し、再開します。
このプロセスを繰り返します。アラートボックスが最終的に表示されるまで、ブレークポイントにヒットするたびに flag
に true
を設定します (ブレークポイントは 5 ~ 6 回ヒットします) 。アラートボックスがキャンセルできるようになりました。ボックスの隣の任意の場所をタップすると、アプリを終了することなく閉じます。
ここでは改竄防止は秘密の文字列を抽出する準備の妨げにはなりません。「静的解析」セクションでは、文字列は AES を使用して解読され、次にメッセージボックスに入力された文字列と比較されることがわかりました。java.lang.String
クラスのメソッド equals
は入力文字列と秘密の文字列を比較するために使用されます。java.lang.String.equals
にメソッドブレークポイントを設定し、エディットフィールドにテキストを入力して、"verify" ボタンをタップします。ブレークポイントがヒットしたら、locals
コマンドを使用してメソッド引数を読むことができます。
これが探していた平文の文字列です。
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.c
の a()
メソッドである次のメソッドの先頭で停止します。
このメソッドは 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()
メソッドはこれらのディレクトリ内で su バイナリの存在を検索します。このコントロールを無効にするには、デバイス上で su
バイナリを検出するサイクルで、ディレクトリ名 (parent) またはファイル名 (child) を変更します。F2 を押すか、右クリックして "Set Value" で変数の内容を変更します。
バイナリ名またはディレクトリ名を変更すると、File.exists
は false
を返します。
これは 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
を使用して、デバイスまたはエミュレータにインストールします。
この章の最初の手順に従っている場合には、すでに Android NDK があるはずです。さまざまなアーキテクチャ用にプレビルドされたバージョンの gdbserver が含まれています。gdbserver binary
をデバイスにコピーします。
gdbserver --attach<comm> <pid>
コマンドは gdbserver を実行中のプロセスにアタッチし、comm
で指定された IP アドレスとポートにバインドします。この場合、HOST:PORT
記述子です。デバイスの HelloWorld-JNI を起動し、デバイスに接続して HelloWorld プロセスの PID を決定します。次に、root ユーザーに切り替えて、gdbserver
を以下のようにアタッチします。
プロセスは現在一時停止しており、gdbserver
はクライアントをデバッグするためにポート 1234
で listen しています。デバイスが USB 経由で接続されている場合は、adb forward
コマンドを使用して、このポートをホストのローカルポートに転送できます。
NDK ツールチェーンに含まれているプレビルドバージョンの gdb
を使用します (まだであれば、上述の手順に従ってインストールします) 。
プロセスへのアタッチに成功しました。唯一の問題は、JNI 関数 StringFromJNI()
をデバッグするにはこの時点では遅すぎることです。この関数は起動時に一度しか実行されないためです。この問題を解決するには「デバッガの待機」オプションを有効にします。「開発者オプション」->「デバッグアプリの選択」に行き、HelloWorldJNI を選択してから、「デバッガの待機」スイッチを有効にします。その後、アプリを終了および再起動します。自動的に一時停止します。
目的はアプリを再開する前にネイティブ関数 Java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI()
の最初にブレークポイントを設定することです。残念ながら、実行でのこの早い時点ではこれはできません。libnative-lib.so
はまだプロセスメモリにマップされていないためです。これは実行時に動的にロードされます。これを実現するために、まず JDB を使用して、プロセスを必要な状態に穏やかに制御します。
まず、JDB をアタッチすることにより Java VM の実行を再開します。しかし、プロセスをすぐに再開したいわけではないため、以下のように suspend
コマンドを JDB にパイプします。
次に、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]
JDB で resume
コマンドを実行して Java ランタイムの実行を再開します (JDB を使用していますが、この時点でデタッチすることもできます) 。GDB でプロセスを探索することができます。info sharedlibrary
コマンドはロードされたライブラリを表示します。それには libnative-lib.so
が含まれています。info functions
コマンドはすべての既知の関数のリストを取得します。JNI 関数 java_sg_vantagepoint_helloworldjni_MainActivity_stringFromJNI()
は非デバッグシンボルとしてリストされている必要があります。関数のアドレスにブレークポイントを設定し、プロセスを再開します。
JNI 関数の最初の命令が実行されたときにブレークポイントがヒットします。disassemble
コマンドを使用して、関数の逆アセンブリを表示できます。
ここから、プログラムをシングルステップ実行して、レジスタやメモリの内容を表示したり、それらを改竄して、JNI 関数の内部動作を調べます (このケースでは、単に文字列を返します) 。help
コマンドを使用して、デバッグ、実行、およびデータの検査に関する詳細情報を取得します。
実行トレース
デバッグに役立つだけでなく、JDB コマンドラインツールは基本的な実行トレース機能も提供します。アプリを最初から正しくトレースするには、Android の「デバッガを待機」機能または kill –STOP
コマンドを使用してアプリを一時停止し、JDB をアタッチして、私たちが選択する初期化メソッドに遅延メソッドブレークポイントを設定します。このブレークポイントがヒットすると、trace go methods
コマンドでメソッドトレースをアクティブにし、実行を再開します。JDB はすべてのメソッドのエントリをダンプして、その場所から出ます。
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 がアタッチするようにできます (上品な解決策ではありませんが動作はします) 。
Ftrace
ftrace は Linux カーネルに直接組み込まれたトレースユーティリティです。ルート化デバイスでは、ftrace を使用して、strace で可能なよりも透過的な方法でカーネルシステムコールをトレースできます。strace は ptrace システムコールに依存して、対象プロセスにアタッチします。
便利なことに、ftrace の機能は Lollipop と Marshmallow の両方で出荷された Android カーネルにあります。以下のコマンドで有効にできます。
/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] を使用すると、デフォルトの 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 の変換機能に入るときに記録します。以下のコマンドはすべての変換されたブロックをファイルに記録します。
残念ながら、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 をビルドします。
この執筆時点では、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 では、特に二つの問題が定期的に持ち上がります。
Manifest で android:debuggable フラグが true に設定されていないため、デバッガがアプリにアタッチできない。
アプリが SSL ピンニングを使用しているため、プロキシで HTTPS トラフィックを傍受できない。
ほとんどの場合、いずれの問題も軽微な変更とアプリの再パッケージおよび再署名により解決できます (例外として、デフォルトの Android コード署名以外に追加の整合性チェックを実行するアプリがあります。この場合には、追加のチェックにも同様にパッチを当てる必要があります) 。
事例: SSL ピンニングの無効化
正当な理由で HTTPS 通信を傍受したいセキュリティテスターにとって、証明書ピンニングは問題です。この問題を解決するために、バイトコードにパッチを適用して、SSL ピンニングを無効にできます。証明書ピンニングをバイパスする方法を示すために、サンプルアプリケーションに実装された証明書ピンニングをバイパスするために必要な手順を実行します。
最初のステップでは apktool
を使用して APK を逆アセンブルします。
Smali ソースコードで証明書ピンニングチェックを見つける必要があります。"X509TrustManager" などのキーワードで smali コードを検索することで、正しい方向に向かいます。
この例では、"X509TrustManager" を検索するとカスタムの Trustmanager を実装するクラスがひとつ返されます。この派生クラスは checkClientTrusted
, checkServerTrusted
, getAcceptedIssuers
という名前のメソッドを実装します。
実行をバイパスするために、これらの各メソッドの最初の行に return-void
オペコードを追加します。これにより各メソッドは直ちに戻ります。この変更により、証明書チェックは実行されず、アプリケーションはすべての証明書を受け入れます。
Xposed で Java メソッドのフック
Xposed は "APK に触れることなくシステムやアプリの動作を変更できるモジュールのフレームワーク" [24] です。技術的には、新しいプロセスが開始されたときに Java コードを実行するための API をエクスポートする Zygote の拡張バージョンです。新しくインスタンス化されたアプリのコンテキストで Java コードを実行することにより、アプリに属する Java メソッドを解決、フック、オーバーライドすることが可能です。Xposed は reflection を使用して、実行中のアプリを調査および変更します。変更はメモリに適用され、プロセスの実行中にのみ維持されます。アプリケーションファイルへのパッチは作成されません。
Xposed を使用するには、まずルート化されたデバイスに Xposed フレームワークをインストールする必要があります。変更は個別のアプリ ("modules") の形式で展開され、Xposed GUI でオンとオフを切り替えることができます。
事例: XPosedでのルート検出のバイパス
ルート化されたデバイスで頑なに終了してしまうアプリをテストしていると仮定します。あなたはアプリを逆コンパイルし、次の非常に疑わしいメソッドを見つけました。
このメソッドはディレクトリのリストを繰り返し処理し、su
バイナリがそれらのいずれかで見つかった場合に "true" (デバイスはルート化されている) を返します。このようなチェックは簡単に無効化できます。あなたがしなければならないことは、"false" を返すものでコードを置き換えることだけです。Xposed モジュールを使用したメソッドフックはこれを行う方法のひとつです。
このメソッド XposedHelpers.findAndHookMethod
では既存のクラスメソッドをオーバーライドできます。逆コンパイルされたコードから、チェックを実行するメソッドは c()
と呼ばれ、クラス com.example.a.b
にあることがわかります。常に "false" を返すように関数をオーバーライドする Xposed モジュールは以下のようになります。
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 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 を使用します。
Frida を実行するために Android デバイスをルート化する必要はありませんが、それはセットアップが容易であり、特に断りがない限りここではルート化デバイスを想定しています。Frida リリースページ から frida-server バイナリをロードします。サーバーバージョン (少なくともメジャーバージョン番号) がローカルにインストールした Frida のバージョンと一致することを確認します。通常、PyPI は最新バージョンの Frida をインストールしますが、わからない場合には、Frida コマンドラインツールで確認できます。
frida-server をデバイスにコピーして実行します。
frida-server が実行しているため、以下のコマンドで実行中のプロセスのリストを取得できます。
-U
オプションは Frida に USB デバイスやエミュレータを検索させます。
特定の (低レベルの) ライブラリ呼び出しをトレースするには、frida-trace
コマンドラインツールを使用します。
__handlers__/libc.so/open.js
に少しの javascript を生成します。これは Frida がプロセスに注入し、libc.so
の open
関数へのすべての呼び出しをトレースするものです。Fridas Javascript API を使用して、必要に応じて生成されたスクリプトを変更できます。
Frida を対話的に操作するには、frida CLI
を使用できます。プロセスにフックし、Frida の API に対するコマンドラインインタフェースを提供します。
frida CLI を使用して、-l
オプションを介してスクリプトをロードします。例えば、myscript.js
をロードします。
Frida はまた、Android アプリを扱うのに特に役立つ Java API を提供しています。Java クラスとオブジェクトを直接的に操作することができます。これは Activity クラスの "onResume" 関数を上書きするスクリプトです。
上のスクリプトは Java.perform を呼び出し、コードが Java VM のコンテキストで実行されるようにします。Java.use
を介して android.app.Activity
クラスのラッパーをインスタンス化し、onResume
関数を上書きします。新しい onResume
関数はコンソールに通知を出力し、アクティビティがアプリで再開されるたびに this.onResume
を呼び出すことにより、元の onResume
メソッドを呼び出します。
Frida はまたヒープ上のインスタンス化されたオブジェクトを検索し、それらで作業することもできます。以下のスクリプトは android.view.View
オブジェクトのインスタンスを検索し、toString
メソッドを呼び出します。結果はコンソールに出力されます。
出力は以下のようになります。
Java リフレクション機能を使用することもできます。android.view.View クラスの public メソッドを表示するには、Frida でこのクラスのラッパーを作成し、
classプロパティから
getMethods()` を呼び出します。
frida CLI
を介してスクリプトをロードするだけでなく、Frida は Python, C, NodeJS, Swift などのさまざまなバインディングも提供しています。
Frida による OWASP Uncrackable Crackme Level1 の解決
Frida は OWASP UnCrackable Crackme Level 1 を簡単に解決する可能性を与えます。上記では Frida でメソッド呼び出しをフックできることをすでに見てきました。
エミュレータまたはルート化デバイス上でそのアプリを起動すると、ルートを検出するため、アプリはダイアログボックスを表示し、"Ok" を押すとすぐに終了します。
これを防ぐ方法を見てみます。 逆コンパイルされた main メソッド (CFR 逆コンパイラを使用) は以下のようになります。
onCreate
メソッドの Root detected
メッセージと、実際のルートチェックを実行する前の if
ステートメントで呼び出されるさまざまなメソッドに注目します。クラスの最初のメソッド private void a
の This is unacceptable...
メッセージにも注意します。明らかに、これはダイアログボックスが表示される箇所です。setButton
メソッド呼び出しに設定される alertDialog.onClickListener
コールバックがあります。これはルート検出に成功した後に System.exit(0)
を介してアプリケーションを終了する責任があります。Frida を使用すると、このコールバックをフックすることによりアプリが終了しないようにできます。
ダイアログボタンの onClickListener 実装はそれほど多くはありません。
それは単にアプリを終了します。今度は Frida を使用してそれを傍受し、ルート検出後にアプリが終了しないようにします。
コードを setImmediate 関数にラップして (あなたがこれを必要かどうかはわかりませんが) タイムアウトを防いでから、Java.perform を呼び出して Java を処理するための Frida メソッドを使用します。その後、OnClickListener
インタフェースを実装し、onClick
メソッドを上書きするクラスのラッパーを取得します。オリジナルとは異なり、新しいバージョンの onClick
はいくつかのコンソール出力を書き出し、 アプリを終了しません 。Frida を介してこのメソッドの私たちのバージョンを注入すると、ダイアログの OK
ボタンをクリックしてもアプリはもはや終了することはありません。
上記のスクリプトを uncrackable1.js
として保存し、それをロードします。
onClickHandler modified
メッセージが表示されたら、アプリの OK ボタンを安全に押すことができます。アプリはもう終了しません。
私たちは今 "secret string" を入力しようとすることができます。しかし、どこで手に入れられるでしょうか。
クラス sg.vantagepoint.uncrackable1.a
を見ると、入力と比較される暗号化された文字列を見ることができます。
メソッドの最後にある string.equals の比較と、上の try
ブロックにある文字列 arrby2
の作成に注目します。arrby2
は関数 sg.vantagepoint.a.a.a
の戻り値です。string.equals
の比較は私たちの入力を arrby2
と比較します。そのため、私たちが求めているものは sg.vantagepoint.a.a.a.
の戻り値です。
復号化ルーチンをリバースして共通鍵を再構築する代わりに、単にアプリの復号化ロジックをすべて無視し、sg.vantagepoint.a.a.a
関数をフックして戻り値をキャッチします。 ルートでの終了を防ぎ、secret string の復号化を傍受する完全なスクリプトを以下に示します。
Frida でスクリプトを実行し、コンソールに [*] sg.vantagepoint.a.a.a modified
メッセージが表示された後、"secret string" にランダムな値を入力して verify を押します。以下のような出力が得られます。
フックされて関数は復号化された文字列を出力しました。アプリケーションコードとその復号化ルーチンを深く掘り下げることなく、secret string をうまく抽出できました。
ここまで Android での静的/動的解析の基礎について説明しました。もちろん、実際に それを学ぶ唯一の方法はハンズオンの体験です。Android Studio で独自のプロジェクトをビルドし、コードがバイトコードやネイティブコードにどのように変換されるかを観察することを始め、クラッキングの課題に挑戦します。
残りのセクションでは、カーネルモジュールや動的実行などの高度なテーマをいくつか紹介します。
バイナリ解析フレームワーク
バイナリ解析フレームワークは手動で完了することがほとんど不可能なタスクを自動化する強力な方法を提供します。このセクションでは Angr フレームワークを見ていきます。静的および動的シンボリック ("concolic") 解析の両方に役立つバイナリを解析するための python フレームワークです。Angr は VEX 中間言語で動作し、ELF/ARM バイナリ用のローダーが付属しているため、ネイティブ Android バイナリを扱うのに最適です。
ターゲットプログラムは単純なライセンスキー検証プログラムです。確かに、通常このようなライセンスキー検証器は出回っているものには見つかりませんが、ネイティブコードの静的/シンボリック解析の基礎を実演するのに十分役立ちます。難読化されたネイティブライブラリを同梱する Android あぷりでも同じ技法を使用できます (実際、難読化されたコードはしばしばネイティブライブラリに入れられ、まさに逆難読化をより困難にします) 。
Angr のインストール
Angr は Python 2 で書かれていて、PyPI から入手できます。pip を使用して *nix オペレーティングシステムや Mac OS にインストールするのは簡単です。
Virtualenv で専用の仮想環境を作成することをお勧めします。依存関係の一部には元のバージョンを上書きしたフォークされたバージョン Z3 と PyVEX が含まれています (これらのライブラリを他の用途に使用しない場合にはこの手順をスキップしてかまいません - そうでなければ、Virtualenv を使用することは一般的には良い考えです) 。
angr に関する非常に包括的なドキュメントが Gitbook に用意されています。インストールガイド、チュートリアル、使用例などがあります [5] 。完全な API リファレンスも利用可能です [6] 。
逆アセンブラバックエンドの使用
シンボリック実行
シンボリック実行は特定のターゲットに到達するために必要な条件を判断できます。これはプログラムのセマンティクスを論理式に変換することにより行います。これにより一部の変数は特定の制約を持つシンボルとして表現されます。制約を解決することにより、プログラムのある分岐が実行されるように必要な条件を見つけることができます。
とりわけ、これはあるコードブロックに到達するための正しい入力を見つける必要がある場合に便利です。以下の例では、Angr を使用して単純な Android crackme を自動化された方法で解決します。crackme はネイティブ ELF バイナリの形式を取り、ここからダウンロードできます。
https://github.com/angr/angr-doc/tree/master/examples/android_arm_license_validation
任意の Android デバイスで実行可能ファイルを実行すると、以下の出力が得られます。
今のところ順調ですが、有効なライセンスキーがどのようになりそうかについてはまったく何も分かりません。どこかわ始めますか。IDA Pro を起動して、まず何が起こっているかを見るのが良いでしょう。
main 関数は逆アセンブリのアドレス 0x1874 にあります (これは PIE が有効なバイナリであり、IDA Pro は image ベースアドレスとして 0x0 を選択することに注意します) 。関数名は取り除かれていますが、幸いなことにデバッグ文字列への参照がいくつかあります。入力文字列は base32 でデコードされているようです (sub_1340 への呼び出し) 。main の冒頭には loc_1898 で長さチェックもあります。入力文字列の長さが正確に 16 であることを確認します。したがって、私たちは 16 文字の base32 でエンコードされた文字列を探しています。デコードされた入力は次に関数 sub_1760 に渡され、ライセンスキーの有効性を検証します。
16 文字の base32 入力文字列は 10 バイトにデコードされるため、検証関数は 10 バイトバイナリ文字列を期待することがわかります。次に、0x1760 のコア検証関数を見ていきます。
loc_1784 に何らかの XOR マジックが行われているループがあります。おそらく入力文字列をデコードします。loc_17DC には、デコードされた値とそれ以降のサブ関数呼び出しから取得された値との一連の比較があります。これは高度に洗練されたものではありませんが、このチェックを完全にリバースし、それに渡すライセンスキーを生成するにはさらに何らかの解析を行う必要があります。しかし、ここで工夫をします。動的シンボリック実行を使用することにより、有効なキーを自動的に構築できます。シンボリック実行はライセンスチェックの最初の命令 (0x1760) と "Product activation passed" メッセージを表示するコード (0x1840) との間のパスをマップし、入力文字列の各バイトの制約を決定します。ソルバーエンジンこれらの制約を満たす入力:有効なライセンスキーを探します。
シンボリック実行エンジンにはいくつかの入力を提供する必要があります。
実行を開始するアドレス。シリアル検証関数の最初の命令で状態を初期化します。これにより、Base32 の実装をシンボリックに実行することを避けるため、タスクをかなり簡単に (そしてこの場合はほとんど瞬時に) 解決します。
実行を到達したいコードブロックのアドレス。この場合には、"Product activation passed" メッセージを出力する要因となるコードへのパスを探すことを望みます。このブロックは 0x1840 から開始します。
到達して欲しくないアドレス。この場合には、0x1854 の "Incorrect serial" メッセージを表示するコードのブロックに至るパスには興味がありません。
Angr ローダーはベースアドレスが 0x400000 の PIE 実行可能ファイルをロードするため、これを上記のアドレスに追加する必要があることに注意します。解は以下のようになります。
最終的な入力文字列が得られるプログラムの最後の部分に注意します。単純にメモリから解を読み取っていた場合に表示されます。しかしシンボリックメモリから読み取っています。文字列もポインタも実際には存在しません。実際に何が起こっているかというと、ソルバーがそのプログラムの状態で見つかる可能性のある具体的な値を計算しているということです。その時点まで実際のプログラムの実行を観察します。
このスクリプトを実行すると、以下を返します。
リバースエンジニアリング向けに Android のカスタマイズ
実デバイスでの作業は特にインタラクティブな、デバッガでサポートされる静的/動的解析において強みがあります。ひとつは、実デバイスでの作業が単純に高速であることです。また、実デバイスで実行することはターゲットアプリに疑いや誤動作となる理由をより少なくします。ライブ環境を戦略的ポイントで計装することにより、有用なトレース機能を取得し、環境を操作して、アプリが実装している可能性のある耐タンパ性防御をバイパスすることができます。
RAMDisk のカスタマイズ
initramfs はブートイメージの中に格納された小さな CPIO アーカイブです。実際のルートファイルシステムがマウントされる前に、ブート時に必要なファイルがいくつか含まれています。Android では、initramfs は無期限にマウントされたままであり、いくつかの基本的なシステムプロパティを定義する default.prop という名前の重要な構成ファイルが含まれます。このファイルをいくつか変更することにより、Android 環境をもう少しリバースエンジニアリングに適したものにできます。私たちの目的として、default.prop の最も重要な設定は ro.debuggable
と ro.secure
です。
ro.debuggable を 1 に設定すると、システム上で実行しているすべてのアプリがデバッグ可能になります (すなわち、すべてのプロセスでデバッガスレッドが実行されます) 。アプリのマニフェストの android:debuggable 属性には依存しません。ro.secure を 0 に設定すると adbd を root として実行されます。 任意の Android デバイス上で initrd を変更するには、TWRP を使用して元のブートイメージをバックアップするか、単に以下のようなコマンドでダンプします。
Krzysztof Adamski のハウツーで説明されているように、abootimg ツールを使用して、ブートイメージの内容を抽出します。
bootimg.cfg に書かれているブートパラメータを書き留めておきます。後で新しいカーネルとラムディスクをブートするときに、これらのパラメータが必要になります。
default.prop を修正し、新しいラムディスクをパッケージ化します。
Android カーネルのカスタマイズ
Android カーネルはリバースエンジニアにとって強力な味方です。通常の Android アプリは絶えず制限されサンドボックス化されていますが、リバースする人はオペレーティングシステムとカーネルの動作を自由にカスタマイズおよび変更できます。これはあなたに本当に不当な優位性を与えます。なぜならほとんどの完全性チェックと改竄防止機能は最終的にカーネルにより実行されるサービスに依存するためです。この信頼を悪用するカーネルを配備し、自らとその環境について臆面もなくうそをつくことは、マルウェア作者 (または通常の開発者) があなたに投げることができるほとんどのリバース防御を破るのに大いに役立ちます。
Android アプリはいくつかの方法で OS 環境とやり取りします。標準的な方法は Android Application Framework の API を使用するものです。しかし最も低いレベルでは、メモリの割り当てやファイルへのアクセスなど、多くの重要な機能が完全に旧来の Linux システムコールに変換されています。ARM Linux では、SVC 命令を介してシステムコールが呼び出され、ソフトウェア割込みをトリガします。この割込みは vector_swi() カーネル関数を呼び出し、システムコール番号を関数ポインタテーブル (通称 sys_call_table on Android) へのオフセットとして使用します。
システムコールを傍受する最も簡単な方法は、カーネルメモリに独自のコードを注入し、システムコールテーブルの元の関数を上書きして実行をリダイレクトすることです。残念ながら、現在出荷されている Android カーネルはメモリ制限を強制し、これが動作することを妨げます。具体的には、出荷された Lollipop と Marshmallow カーネルは CONFIG_STRICT_MEMORY_RWX オプションを有効にしてビルドされています。これにより読み取り専用としてマークされたカーネルメモリ領域に書き込むことができなくなります。つまり、カーネルコードやシステムコールテーブルにパッチを当てようとすると、セグメンテーションフォルトが発生して再起動します。これを回避する方法は独自のカーネルをビルドすることです。この保護を無効にして、その他多くの便利なカスタマイズを行い、リバースエンジニアリングを容易にします。習慣的に Android アプリをリバースしているのであれば、独自のリバースエンジニアリングサンドボックスをビルドすることは非常に簡単です。
ハッキングの目的において、AOSP 対応のデバイスを使用することをお勧めします。Google の Nexus スマートフォンとタブレットは最も合理的な候補で、AOSP からビルドされたカーネルやシステムコンポーネントが問題なく実行されます。また、ソニーの Xperia シリーズもオープンであると知られています。AOSP カーネルをビルドするには、ツールチェーン (ソースをクロスコンパイルするためのプログラムセット) と適切なバージョンのカーネルソースが必要です。Google の説明に従って、特定のデバイスおよび Android バージョンの正しい git リポジトリとブランチを確認します。
https://source.android.com/source/building-kernels.html#id-version
例えば、Nexus 5 と互換性のある Lollipop のカーネルソースを取得するには、"msm" リポジトリをクローンし、"android-msm-hammerhead" ブランチをチェックアウトします (hammerhead は Nexus 5 のコードネームです。そう、正しいブランチを見つけることは紛らわしいプロセスなのです) 。ソースをダウンロードしたら、hammerhead_defconfig (もしくはターゲットデバイスに応じた 何とか_defconfig) コマンドを使用して、デフォルトのカーネル設定を作成します。
以下の設定を使用して、最も重要なトレース機能を有効にし、ロード可能なモジュールのサポートを追加し、パッチを適用するためのカーネルメモリを開くことをお勧めします。
編集が終わったら、.config ファイルを保存し、カーネルをビルドします。
編集が終わったら .config ファイルを保存します。オプションとして、カーネルと以降のタスクをクロスコンパイルするためのスタンドアロンのツールチェーンを作成できるようになりました。Android Nougat 用のツールチェーンを作成するには、Android NDK パッケージの make-standalone-toolchain.sh を以下のように実行します。
CROSS_COMPILE 環境変数を NDK ディレクトリを指すように設定し、"make" を実行してカーネルをビルドします。
カスタム環境のブート
新しいカーネルをブートする前に、デバイスからオリジナルのブートイメージのコピーを作成します。以下のようにブートパーティションの場所を探します。
それから、その全体をひとつのファイルにダンプします。
次に、ブートイメージの構造に関する情報と ramdisk を抽出します。これを行うためのさまざまなツールがあります。私は Gilles Grandou の abootimg ツールを使用しました。ツールをインストールし、ブートイメージに以下のコマンドを実行します。
これによりローカルディレクトリに bootimg.cfg, initrd.img, zImage (オリジナルのカーネル) というファイルが作成されます。
fastboot を使用して新しいカーネルをテストできるようになりました。"fastboot boot" コマンドを使用すると、実際にフラッシュすることなくカーネルを実行できます (すべてが動くことを確認したら、fastboot flash で永続的に変更することができますが、そうする必要はありません) 。以下のコマンドでデバイスを fastboot モードに再起動します。
そして、"fastboot boot" コマンドを使用して、新しいカーネルで Android を起動します。新しくビルドされたカーネルとオリジナルの ramdisk に加えて、kernel offset, ramdisk offset, tags offset, commandline (前に解凍した bootimg.cfg にリストされている値を使用) を指定します。
システムは正常に起動するはずです。正しいカーネルが実行されていることをすばやく確認するには、設定 -> バージョン情報 に移動し、「カーネルバージョン」フィールドを確認します。
カーネルモジュールを使用したシステムコールフック
システムコールのフックにより、カーネルが提供する機能に依存するアンチリバース防御を攻撃することができます。カスタムカーネルを使用して、LKM を使用してカーネルに追加のコードをロードすることができます。/dev/kmem インタフェースにもアクセスできます。カーネルメモリをオンザフライでパッチを当てるために使用できます。これは伝統的な Linux ルートキットのテクニックであり、Dong-Hoon You [1] により Android 向けに記述されました。
必要な情報の最初の部分は sys_call_table のアドレスです。幸いなことに、Android カーネルのシンボルとしてエクスポートされています (iOS のリバースをする人は幸運ではありません) 。/proc/kallsyms ファイルでそのアドレスを調べることができます。
これはカーネルモジュールを書くために必要な唯一のメモリアドレスです。他のものはカーネルヘッダから取得したオフセットを使用して計算できます (願わくば、まだそれらが削除されていないとよいのですが) 。
事例: ファイル隠蔽
このハウツーでは、カーネルモジュールを使用してファイルを隠します。デバイス上にファイルを作成し、それを後で隠すことができます。
ついにカーネルモジュールを書くときがやってきました。ファイルを隠すには、ファイルを開く (または存在を確認する) ために使用されるシステムコールのひとつをフックする必要があります。実際にはそれらは多くあります。open, openat, access, accessat, facessat, stat, fstat など。ここでは、openat システムコールだけをフックします。これは "/bin/cat" プログラムがファイルにアクセスするときに使用されるシステムコールですので、デモンストレーションには十分役立ちます。
すべてのシステムコールの関数プロトタイプはカーネルヘッダファイル arch/arm/include/asm/unistd.h にあります。以下のコードを使用して kernel_hook.c というファイルを作成します。
カーネルモジュールをビルドするには、カーネルソースと作業用のツールチェーンが必要です。前もって完全なカーネルをビルドしているので、すべて設定されています。以下の内容で Makefile を作成します。
"make" を実行してコードをコンパイルします。これでファイル kernel_hook.ko が作成されます。kernel_hook.ko ファイルをデバイスにコピーし、insmod コマンドでそれをロードします。lsmod コマンドでそのモジュールがロードに成功したことを確認します。
今度は /dev/kmem にアクセスして、sys_call_table のオリジナルの関数ポインタを新しく注入する関数のアドレスで上書きします (これはカーネルモジュールで直接行うこともできますが、/dev/kmem を使用することでより簡単にフックのオンとオフを切り替えることができます) 。この目的のために Dong-Hoon You's Phrack の記事 [19]
を修正しました。mmap() の代わりにファイルインタフェースを使用したのは、何らかの理由でカーネルパニックを引き起こすことが判ったためです。以下のコードを使用して kmem_util.c というファイルを作成します。
あらかじめビルドされたツールチェーンを使用して kmem_util.c をビルドし、デバイスにコピーします。Android Lollipop から、すべての実行形式を PIE サポートでコンパイルする必要があることに注意します。
カーネルメモリに手を出す前に、まずシステムコールテーブルへの正しいオフセットを知る必要があります。openat システムコールはカーネルソースにある unistd.h で定義されています。
パズルの最後のピースは取り替えられた openat のアドレスです。ここでも /proc/kallsyms からこのアドレスを取得できます。
これで sys_call_table エントリを上書きするために必要となるものがすべて得られました。kmem_util の構文は以下のとおりです。
以下のコマンドは openat システムコールテーブルが新しい関数を指すようにパッチします。
すべてがうまくいったと仮定すると、/bin/cat はこのファイルを「見る」ことができなくなります。
完成です。ファイル "nowyouseeme" はすべてのユーザーモードプロセスの視点から幾らか隠されています (ファイルを適切に隠すにはさらに多くのことが必要であることに注意します。stat(), access(), その他のシステムコールをフックし、ディレクトリリストのファイルを隠します) 。
ファイルの隠蔽はもちろん氷山の一角です。多くのルート検出手段、整合性チェック、アンチデバッグトリックを回避するなど、多くのことを達成できます。Bernhard Mueller's Hacking Soft Tokens Paper [27] の "case studies" セクションにいくつかの事例があります。
参考情報
[1] UnCrackable Mobile Apps - https://github.com/OWASP/owasp-mstg/blob/master/Crackmes/
[2] Android Studio - https://developer.android.com/studio/index.html
[3] JD - http://jd.benow.ca/
[4] JAD - http://www.javadecompilers.com/jad
[5] Proycon - http://proycon.com/en/
[6] CFR - http://www.benf.org/other/cfr/
[7] apkx - APK Decompilation for the Lazy - https://github.com/b-mueller/apkx
[8] NDK Downloads - https://developer.android.com/ndk/downloads/index.html#stable-downloads
[9] IntelliJ IDEA - https://www.jetbrains.com/idea/
[10] Eclipse - https://eclipse.org/ide/
[11] Smalidea - https://github.com/JesusFreke/smali/wiki/smalidea
[12] APKTool - https://ibotpeaches.github.io/Apktool/
[13] Radare2 - https://www.radare.org
[14] Angr - http://angr.io/
[15] JEB Decompiler - https://www.pnfsoftware.com
[16] IDA Pro - https://www.hex-rays.com/products/ida/
[17] Bionic libc - https://github.com/android/platform_bionic
[18] NetSPI Blog - Attacking Android Applications with Debuggers - https://blog.netspi.com/attacking-android-applications-with-debuggers/
[19] Phrack Magazine - Android Platform based Linux kernel rootkit
[20] DECAF - https://github.com/sycurelab/DECAF
[21] PANDA - https://github.com/moyix/panda/blob/master/docs/
[22] VxStripper - http://vxstripper.pagesperso-orange.fr
[23] Dynamic Malware Recompliation - http://ieeexplore.ieee.org/document/6759227/
[24] Xposed - http://repo.xposed.info/module/de.robv.android.xposed.installer
[25] Xposed Development Tutorial - https://github.com/rovo89/XposedBridge/wiki/Development-tutorial
[26] Frida - https://www.frida.re
[27] Hacking Soft Tokens on - https://packetstormsecurity.com/files/138504/HITB_Hacking_Soft_Tokens_v1.2.pdf
Last updated