2012年3月22日木曜日

Androidアプリケーションのリバースエンジニアリング

結論を先に書くと、Androidアプリケーションのリバースエンジニアリングは非常に簡単である。理由は大きく2つあり、一つはそれがJavaアプリケーションであること、もうひとつはAndroidがオープンソースであることだ(ただしJNI等を使ってC++やCのコードなどを呼び出している場合には、下層のモジュールの解析は通常のCアプリケーション同様に面倒ではないかと考えられる)。

Androidアプリケーションは.apkという拡張子でファイル単体で配布されるので、まずそれを用意する。筆者はAppMonsterというツールを使っている。このツールだと簡単にSDカードにapkファイルを保存してくれる。このエントリでは例としてテスト用のアプリケーションであるandroid1.apkを使用する。

apkファイルはZIP形式の圧縮されたファイルとなっているので、unzipコマンドで任意の場所に展開する。

root@kaldi:/android/tmp# unzip android1.apk
Archive: android1.apk
inflating: res/layout/main.xml
inflating: AndroidManifest.xml
extracting: resources.arsc
extracting: res/drawable-hdpi/icon.png
extracting: res/drawable-ldpi/icon.png
extracting: res/drawable-mdpi/icon.png
inflating: classes.dex
inflating: META-INF/MANIFEST.MF
inflating: META-INF/CERT.SF
inflating: META-INF/CERT.RSA
root@kaldi:/android/tmp# ls -l
total 36
-rw-r--r-- 1 root root 1504 Jan 20 16:45 AndroidManifest.xml
drwxr-xr-x 2 root root 4096 Jan 20 17:58 META-INF
-rw-r--r-- 1 root root 14515 Jan 20 16:46 android1.apk
-rw-r--r-- 1 root root 3780 Jan 20 16:45 classes.dex
drwxr-xr-x 6 root root 4096 Jan 20 17:58 res
-rw-r--r-- 1 root root 1488 Jan 20 16:45 resources.arsc
AndroidアプリケーションではActivityをはじめさまざまな情報がAndroidManifest.xmlにまとめられている。そのため解析対象のアプリケーションの内部構造を俯瞰するには、まずこのファイルから手を付けるのがよい。しかしAndroidでは極力モバイルデバイスの負荷を減らすためにXMLファイルはあらかじめパースされたバイナリ形式となって格納されており、このままではAndroidManifest.xmlをエディタで開いても中身を読むことができない。そこでバイナリ形式から通常のXML形式に戻すためにAXMLPrinter2.jarというツールを使用する。jarという拡張子からわかるようにこのツールはJavaアプリケーションであり、java -jarして引数にファイル名を与えると標準出力にXMLを吐いてくれる。

root@kaldi:/android/tmp# java -jar ../AXMLPrinter2.jar AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
package="net.jumperz.app.android.test1"
>
<application
android:label="@7F040001"
android:icon="@7F020000"
android:debuggable="true"
>
<activity
android:label="@7F040001"
android:name=".HelloActivity"
>
<intent-filter
>
<action
android:name="android.intent.action.MAIN"
>
</action>
<category
android:name="android.intent.category.LAUNCHER"
>
</category>
</intent-filter>
</activity>
</application>
<uses-sdk
android:minSdkVersion="4"
>
</uses-sdk>
</manifest>
このようにAndroidManifest.xmlを解析することで、このアプリケーションはActivityをひとつしか持たないことがすぐにわかる。

続いて実際のアプリケーションの細かな動作をコントロールしている、中心となる部分を解析する。Androidアプリケーションでは、アプリケーションの細かな挙動は開発時にはJavaのソースコード(拡張子.javaのテキストファイル)として存在する。それがOracleのJDKなどでコンパイルされ、クラスファイル(拡張子.classのバイナリファイル)となる。Androidアプリケーションとしてデプロイされる際には、アプリケーション内部で使用されるクラスファイルはすべてAndroid独自のdex形式として1つのファイルにまとめられる。このファイルはclasses.dexという名前でapkファイル内に存在している。

classes.dexファイルはバイナリファイルなので、そのままでは中身を読むことができない。そこでbaksmaliというツールを使う。baksmaliはsmaliと対となるツールであり、それぞれアイスランド語でディスアセンブラ/アセンブラという意味を持つらしい。AndroidのJVMであるDalvikという単語がアイスランド由来であることから、このような名前を付けたようだ。

baksmali/smaliともJavaで作成されたアプリケーションであり、jarファイルとして存在している。次の例のように引数にclasses.dexファイルを渡すと、out/というディレクトリを生成してその中にsmali形式のファイルを吐いてくれる。

root@kaldi:/android/tmp# mkdir smali
root@kaldi:/android/tmp# cd smali
root@kaldi:/android/tmp/smali# java -Xmx1G -jar ../../baksmali-1.2.6.jar ../classes.dex
root@kaldi:/android/tmp/smali# find . -type f
./out/net/jumperz/app/android/test1/R$drawable.smali
./out/net/jumperz/app/android/test1/R.smali
./out/net/jumperz/app/android/test1/R$id.smali
./out/net/jumperz/app/android/test1/R$layout.smali
./out/net/jumperz/app/android/test1/R$attr.smali
./out/net/jumperz/app/android/test1/R$string.smali
./out/net/jumperz/app/android/test1/HelloActivity.smali
拡張子.smaliのsmali形式のファイルはテキストファイルであり、エディタで編集することができる。以下にHelloActivity.smaliの一部を掲載する。

# virtual methods
.method public onCreate(Landroid/os/Bundle;)V
.registers 5
.parameter "savedInstanceState"

.prologue
.line 28
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

.line 29
const/high16 v0, 0x7f03

invoke-virtual {p0, v0}, Lnet/jumperz/app/android/test1/HelloActivity;->setContentView(I)V

.line 30
const v0, 0x7f050001

invoke-virtual {p0, v0}, Lnet/jumperz/app/android/test1/HelloActivity;->findViewById(I)Landroid/view/View;
smaliファイルの中身はDalvik VMのバイトコードをディスアセンブルしたものである。最初に見たときは「わけがわからない」と投げ出したくなるかもしれないが、Javaの開発者であれば大部分はすぐに読めるようになる(難しいアルゴリズムの実装部分などはそのまま読むのは厳しいかもしれないが)。smaliファイルに慣れるためには、まず自分で簡単なAndroidアプリケーションをJavaで書き、それを上記の方法でsmali形式に変換する。そしてJavaソースコードとsmaliファイルを行ごとに付き合わせて読んでいけばいい。おそらく30分もあれば基本的な構造についてはかなり読めるようになっているだろう。Dalvikのバイトコードやその他の説明は以下のURLが参考になる。

http://code.google.com/p/smali/w/list
http://www.netmite.com/android/mydroid/dalvik/docs/dalvik-bytecode.html

今回、例として使用しているアプリケーションは筆者が作成したものなので、もちろん元となるJavaのソースコードが手に入る。簡単に以下の行について説明を行う。

.line 28
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
.line 28とあることから、元のJavaソースコードの28行目であることがわかる。該当するJavaソースコードは以下となっている。

27: public void onCreate(Bundle savedInstanceState) {
28: super.onCreate(savedInstanceState);
invoke-superは親クラスのメソッドを呼び出すことを意味している。次のp0とp1は呼び出しの際に使うレジスタを表している。pではじまる名前のレジスタは実行中の関数の引数(パラメータ)であり、p0が0番目(this)、p1がひとつめの引数だ。Landroid/app/Activity;はandroid.app.Activityクラスのことだ(頭のLは続く文字列がクラス名であることを意味する)。続くonCreateは呼び出す関数の名前であり、引数が(Landroid/os/Bundle;なので)android.os.Bundleクラスのインスタンスであるという意味になる。つまりthisオブジェクト(p0)の親のonCreateを、実行中の関数のひとつめの引数(p1)であるsavedInstanceStateを引数として実行する、ということになる。最後のVは戻り値がvoidであることを意味する。

実際のアプリケーション解析では、元となるJavaのソースコードは手に入らないだろう。しかしbaksmaliを使用することで、このようにある程度読みやすい形のディスアセンブルリストは手にすることができる。smali形式を読み解くことができれば、アプリケーションはあなたの前で丸裸になったようなものだ。

しかし、もっと楽をしたいと考える人もいるだろう。実はdex2jarと呼ばれるツールが存在しており、このツールを使うとclasses.dexをjar形式に変換することができる。jarファイルはJavaのクラスファイル群をZIP形式で固めたものなので、unzipすることでJavaのクラスファイルを手に入れることができる。知っている人も多いかと思うが、Javaのクラスファイルはディスアセンブルが非常に容易であり、JDやjadなどのツールを使うことでほぼ完全なJavaソースコードを手にすることができる。しかし残念なことに、Androidアプリケーションではこの前段の変換を行うdex2jarの変換精度はあまり良くないようだ。これはDalvik VMと従来のJavaVMの仕様があまりにも異なっているため仕方がないことなのだと考えられる。

それでもdex2jarとJDなどを組み合わせてJavaソースコードを手に入れることには意味がある。読みやすさではsmali形式よりもJavaソースコードの方が圧倒的に優れているので、解析する際に、目的の処理がアプリケーション全体においてだいたいどの辺り(どのクラス)に存在しているのかを探すのには非常に便利なのだ。

dex2jarは以下のようにして使用する。

root@kaldi:/android/tmp# mkdir dex2jar
root@kaldi:/android/tmp# cd dex2jar
root@kaldi:/android/tmp/dex2jar# ../../dex2jar/dex2jar.sh ../classes.dex
version:0.0.7.8-SNAPSHOT
2 [main] INFO pxb.android.dex2jar.v3.Main - dex2jar ../classes.dex -> ../classes.dex.dex2jar.jar
Done.
root@kaldi:/android/tmp/dex2jar# jar -x < ../classes.dex.dex2jar.jar
root@kaldi:/android/tmp/dex2jar# find . -type f
./net/jumperz/app/android/test1/R.class
./net/jumperz/app/android/test1/R$drawable.class
./net/jumperz/app/android/test1/R$string.class
./net/jumperz/app/android/test1/HelloActivity.class
./net/jumperz/app/android/test1/R$layout.class
./net/jumperz/app/android/test1/R$attr.class
./net/jumperz/app/android/test1/R$id.class
クラスファイルが生成されているので、後はJDやjadなどのツールを使ってJavaソースコードに変換すればいい。

Androidアプリケーションのリバースエンジニアリングをする場合には、目的として「解析だけしたい」場合と、「解析した上で、さらに動作を自分好みに変更する」、つまりアプリケーションの改造までを行いたい場合があるだろう。前者の場合、JDによるJavaソースコード形式への(ときに不完全な)変換で十分な場合もあるだろう。この場合、読みにくいsmali形式のファイルと格闘する必要がないかもしれない。しかし後者、つまり改造までを行いたい場合、アプリケーション内の目的の箇所を自分の意図を達成するように書き換え、ふたたびAndroidアプリケーションとして動作するよう、正しくアセンブルしなおす必要がある。smaliはこれを可能にしてくれる。非常に精度が高いディスアセンブル・アセンブルが可能なので、classes.dex -> smali -> classes.dexという変換が可能なのだ。

ここでは筆者がAndroidの参考書からコピペで作成したサンプルアプリケーションを例に、改造する例を見ていく。このアプリケーションはテキスト入力欄をひとつ持ち、そこに文字列を入力してEnterキーを押すと、その下にあるリストに文字列が追加されていく、というだけのものだ。ソースコードは以下のようになる。

package net.jumperz.app.android.test1;

import android.app.Activity;
import android.os.Bundle;
import android.widget.*;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.text.*;
import android.content.Context;

import java.util.*;

public class HelloActivity
extends Activity
implements OnKeyListener
{
private List todoItems = new ArrayList();
private ListView lv;
private EditText et;
private ArrayAdapter aa;
//----------------------------------

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView( R.layout.main );
lv = ( ListView )findViewById( R.id.ListView01 );
et = ( EditText )findViewById( R.id.EditText01 );

aa = new ArrayAdapter( this, android.R.layout.simple_list_item_1, todoItems );
lv.setAdapter( aa );

et.setOnKeyListener( this );
}
//----------------------------------
public boolean onKey( View view, int keyCode, KeyEvent event )
{
if( event.getAction() == KeyEvent.ACTION_DOWN )
{
if( keyCode == KeyEvent.KEYCODE_ENTER )
{
String str = et.getText().toString();
todoItems.add( 0, str );
aa.notifyDataSetChanged();
et.setText( "" );
return true;
}
}
return false;
}
//----------------------------------
}
このアプリケーションを改造し、Enterキーが押された際に、そのときの文字列がクリップボードにコピーされるよう動作を変更(追加)してみる。改造する際にはsmali形式のファイルを直接編集することになる。ここでは「文字列をクリップボードにコピーする」という処理をsmali形式で記述しなければならない。

先述したようにこのような場合、ゼロからsmali形式で記述する必要はなく、自分で文字列をクリップボードにコピーするアプリケーションを作成して、該当部分がどのようにsmali形式に変換されるのかを確認すればよい。処理を追加する際には、その部分をわかりやすい引数を持つ関数として独立させるのがおすすめだ。ここでは以下のような関数を使用する。

private void copyToClipboard( Activity activity, String str )
{
ClipboardManager cm = (ClipboardManager)activity.getSystemService( Context.CLIPBOARD_SERVICE );
cm.setText( str );
}
この関数はsmali形式では次のようになる。

.method private copyToClipboard(Landroid/app/Activity;Ljava/lang/String;)V
.registers 5
.parameter "activity"
.parameter "str"

.prologue
.line 62
const-string v1, "clipboard"

invoke-virtual {p1, v1}, Landroid/app/Activity;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;

move-result-object v0

check-cast v0, Landroid/text/ClipboardManager;

.line 63
.local v0, cm:Landroid/text/ClipboardManager;
invoke-virtual {v0, p2}, Landroid/text/ClipboardManager;->setText(Ljava/lang/CharSequence;)V

.line 64
return-void
.end method
この部分をまるごとout/net/jumperz/app/android/test1/HelloActivity.smaliファイル内に追記することで、HelloActivityクラスにcopyToClipboardという関数が追加されることになる。非常にシンプルで簡単だ。

次に、この追加した関数を呼び出す必要がある。Javaの以下の部分で目的の文字列がstrという変数に格納されていることに注目する。

String str = et.getText().toString();
対応するsmaliファイル内の記述は以下の部分だ。

invoke-interface {v1}, Landroid/text/Editable;->toString()Ljava/lang/String;
move-result-object v0
ここで、対象の文字列がv0というレジスタに格納されているので、この処理の後に関数呼び出しを追記すればよい。追記する内容は以下となる。

invoke-direct {p0, p0, v0}, Lnet/jumperz/app/android/test1/HelloActivity;->copyToClipboard(Landroid/app/Activity;Ljava/lang/String;)V
HelloActivityクラス自身のメソッドを呼び出すため、invoke-directを使う。thisインスタンス(p0)の関数を、thisインスタンス(p0)と対象文字列(v0)を引数として呼び出すため、{p0, p0, v0},という記述になる。

smaliファイルをこのように書き換えたら、アセンブラであるsmaliを使って再びclasses.dexの形に変換する。

root@kaldi:/android/tmp/smali# ls
out
root@kaldi:/android/tmp/smali# java -jar ../../smali-1.2.6.jar -o classes.dex out/
root@kaldi:/android/tmp/smali# ls -l
total 8
-rw-r--r-- 1 root root 3768 Jan 21 00:21 classes.dex
drwxr-xr-x 3 root root 4096 Jan 21 00:05 out
apkファイル内のclasses.dexを、zipコマンドで入れ替える。

root@kaldi:/android/tmp/smali# zip ../android1.apk classes.dex
updating: classes.dex (deflated 50%)
apkファイルは署名する必要があるので、jarsignerコマンドを使って署名する。まず署名関連のファイルを削除する。

root@kaldi:/android/tmp# zip -d android1.apk META-INF/*
deleting: META-INF/MANIFEST.MF
deleting: META-INF/CERT.SF
deleting: META-INF/CERT.RSA
そして再び署名する。jarsignerコマンドを使うためには.keystoreファイルの用意などが必要だが、本題ではないのでここでは細かいプロセスは省略する。Javaのキーストア関連の知識がないと少し面倒かもしれない。

root@kaldi:/android/tmp# jarsigner -verbose android1.apk test
Enter Passphrase for keystore:
adding: META-INF/MANIFEST.MF
adding: META-INF/TEST.SF
adding: META-INF/TEST.RSA
signing: res/layout/main.xml
signing: AndroidManifest.xml
signing: resources.arsc
signing: res/drawable-hdpi/icon.png
signing: res/drawable-ldpi/icon.png
signing: res/drawable-mdpi/icon.png
signing: classes.dex
これでアプリケーションの改造は終了だ。android1.apkをインストールして文字列を入力し、Enterキーを押すと、その文字列はクリップボードにコピーされる。

以下参考リンク

JD Java Decompiler
http://java.decompiler.free.fr/

AXMLPrinter2.jar
http://code.google.com/p/android4me/downloads/detail?name=AXMLPrinter2.jar&can=2&q=

Smali/Baksmali
http://code.google.com/p/smali/

dex2jar
http://code.google.com/p/dex2jar/

冒頭のイメージで、実線は頼りになる変換、破線は頼りにならない変換を示す(リバースエンジニアリング時)。この画像はcacoo.comで作成した。cacoo.com最高すぎる。Ubuntuに移行してから、SmartDrawが使えなくなったのが悩みだったのだが、完全に解消された!

0 件のコメント:

コメントを投稿