AndroidアプリケーションのSSL通信をプロキシで解析する(2)

0x00. はじめに


前回の記事ではURLConnectionクラスを使ったアクセスについて調査した。今回はSSLSocketFactoryを使い、生のSSLソケットを生成する場合について同様のアプローチを用いる。

0x01. SSLSocketを用いてSSLウェブサイトにアクセス


一般的に、AndroidアプリケーションからHTTPやHTTPSを使ったアクセスを行いたい場合、URLConnectionクラスを用いると思われる。しかしより詳細なコントロールを行いたい場合には、SSLSocketクラスのインスタンスを生成することになるだろう。例えば次のようなコードを使えば、ベリサイン(日本)のウェブサイトにアクセスすることができる。

private String socketTest()
{
try
{
SocketFactory sf = SSLSocketFactory.getDefault();
Socket s = sf.createSocket( "www.verisign.co.jp", 443 );
s.getOutputStream().write( "GET / HTTP/1.0\r\nHost: www.verisign.co.jp\r\n\r\n".getBytes() );
byte[] buf = new byte[ 1024 * 10 ];
s.getInputStream().read( buf );
s.close();
String res = new String( buf );
Log.i( TAG, res );
return res.substring( 0, 30 );
}
catch( Exception e )
{
return logException( e );
}
}

前回と同様に、エミュレータ起動時のコマンドラインオプションでHTTP(HTTPS)プロキシを使うように指定し、Doormanなどのローカルプロキシを経由させる場合、このコードは証明書が不正であるとし、例外を送出してしまう。スタックトレースは以下のようになる。

03-08 02:10:02.704: INFO/HA(6902): javax.net.ssl.SSLException: Not trusted server certificate
03-08 02:10:02.704: INFO/HA(6902):     at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:371)
03-08 02:10:02.704: INFO/HA(6902):     at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl$SSLOutputStream.(OpenSSLSocketImpl.java:564)
03-08 02:10:02.704: INFO/HA(6902):     at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.getOutputStream(OpenSSLSocketImpl.java:479)
03-08 02:10:02.704: INFO/HA(6902):     at net.jumperz.app.android.test1.HelloActivity.socketTest(HelloActivity.java:66)
03-08 02:10:02.704: INFO/HA(6902):     at net.jumperz.app.android.test1.HelloActivity.onKey(HelloActivity.java:109)
03-08 02:10:02.704: INFO/HA(6902):     at android.view.View.dispatchKeyEvent(View.java:3735)
03-08 02:10:02.704: INFO/HA(6902):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
03-08 02:10:02.704: INFO/HA(6902):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
03-08 02:10:02.704: INFO/HA(6902):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
03-08 02:10:02.704: INFO/HA(6902):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
03-08 02:10:02.704: INFO/HA(6902):     at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchKeyEvent(PhoneWindow.java:1667)
03-08 02:10:02.704: INFO/HA(6902):     at com.android.internal.policy.impl.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1102)
03-08 02:10:02.704: INFO/HA(6902):     at android.app.Activity.dispatchKeyEvent(Activity.java:2063)
03-08 02:10:02.704: INFO/HA(6902):     at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1643)
03-08 02:10:02.704: INFO/HA(6902):     at android.view.ViewRoot.deliverKeyEventToViewHierarchy(ViewRoot.java:2471)
03-08 02:10:02.704: INFO/HA(6902):     at android.view.ViewRoot.handleFinishedEvent(ViewRoot.java:2441)
03-08 02:10:02.704: INFO/HA(6902):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1735)
03-08 02:10:02.704: INFO/HA(6902):     at android.os.Handler.dispatchMessage(Handler.java:99)
03-08 02:10:02.704: INFO/HA(6902):     at android.os.Looper.loop(Looper.java:123)
03-08 02:10:02.704: INFO/HA(6902):     at android.app.ActivityThread.main(ActivityThread.java:4627)
03-08 02:10:02.704: INFO/HA(6902):     at java.lang.reflect.Method.invokeNative(Native Method)
03-08 02:10:02.704: INFO/HA(6902):     at java.lang.reflect.Method.invoke(Method.java:521)
03-08 02:10:02.704: INFO/HA(6902):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
03-08 02:10:02.704: INFO/HA(6902):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
03-08 02:10:02.704: INFO/HA(6902):     at dalvik.system.NativeStart.main(Native Method)
03-08 02:10:02.704: INFO/HA(6902): Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: TrustAnchor for CertPath not found.
03-08 02:10:02.704: INFO/HA(6902):     at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:168)
03-08 02:10:02.704: INFO/HA(6902):     at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:366)
03-08 02:10:02.704: INFO/HA(6902):     ... 24 more
03-08 02:10:02.704: INFO/HA(6902): Caused by: java.security.cert.CertPathValidatorException: TrustAnchor for CertPath not found.
03-08 02:10:02.704: INFO/HA(6902):     at org.bouncycastle.jce.provider.PKIXCertPathValidatorSpi.engineValidate(PKIXCertPathValidatorSpi.java:149)
03-08 02:10:02.704: INFO/HA(6902):     at java.security.cert.CertPathValidator.validate(CertPathValidator.java:202)
03-08 02:10:02.704: INFO/HA(6902):     at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:164)
03-08 02:10:02.704: INFO/HA(6902):     ... 25 more

0x02. 書き換え


今回はわかりやすい箇所としてTrustManagerImplクラスのcheckServerTrustedメソッドを選択する。このメソッドは戻り値を持たないメソッドであり、証明書の検証を行う。もし証明書が正しいと判断できない場合には例外を送出するという動作をする。そこで、このメソッドを何もしない実装に書き換えてしまう。

143     public void checkServerTrusted(X509Certificate[] chain, String authType)
144             throws CertificateException {
145             }

145行目以降をばっさり削除し、このように中身のない関数にする。対象となるファイルは、Froyoでは ./dalvik/libcore/x-net/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.javaとなる。

makeを行えば、SSLSocketクラスからのアクセスについて証明書を一切検証しないDalvikを載せたFroyoが出来上がる。コマンドラインオプションでプロキシを使うように指定すれば、先述したコードからプロキシを通してベリサインのウェブサイトにアクセスすることが可能となる。

Advertisements

AndroidアプリケーションのSSL通信をプロキシで解析する(1)


0x00. はじめに


Androidアプリケーションの解析の際に、それがどのようなSSL通信を行っているかが重要となる場面がある。そのようなとき、Doormanのようなローカルプロキシでその通信をフックすることができれば目的が達成できる。

しかし通常の(PC上の)ウェブブラウザのSSL通信と同じように、Androidにも元々「信頼できるもの」として扱われるルート証明書群がインストールされており、これらの証明書を元にSSL通信が実施されてしまう。ただ単にローカルプロキシでSSL通信をフックしようとしても、当然「偽物の証明書である」としてエラーとなってしまうため、プロキシでのフックを実施するためには少し工夫が必要となる。いくつかの方法が考えられるが、このエントリではまず筆者が一番はじめに試した方法を紹介する。

0x01. 対象


まず、今回はURLConnectionクラスを使ったSSLのアクセスを対象とする。なぜならば、ふつうAndroidアプリケーションの開発者がインターネット上のウェブサイトと通信を行いたい場合には、このクラスを使うと想定されるからだ(実は筆者自身は殆どこのクラスを使ったことがないのでよく知らないのだが…)。

URLConnectionクラスはAndroid上では以下のようなクラス階層となっており、URLがHTTPSの場合には自動的にSSL通信を行ってくれるようになっている。

パッケージ名も含めたクラス名は以下のようになっている。

  • java.lang.Object
  • java.net.URLConnection
  • java.net.HttpURLConnection
  • javax.net.ssl.HttpsURLConnection
  • org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl
  • org.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnectionImpl
  • org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl.HttpsEngine
  • 上記パッケージ名を見てわかるとおり、AndroidのJava実装はApacheのHarmonyが使われている。わざわざこんなマニアックな実装(失礼)を使っている理由はどうやらライセンスにあるらしい。うーむ…。

    OracleのJREではSSLの実装はすべてJavaで完結しているのに対し、AndroidではJNIを通じて下層のOpenSSLのネイティブコードを呼び出す形になっているようだ(つまり、OpenSSLの脆弱性の影響をもろに受けるということである)。

    0x02. エミュレータを選択する


    筆者ははじめに簡単なアプリケーションを作成し、プロキシサーバを経由せずにURLConnectionクラスを使ってSSLのサイトにアクセスできるようにした。次にプロキシサーバを経由してアクセスするようにしようとしたのだが、ここでなんと衝撃の事実に直面した。AndroidではWiFi使用時にプロキシサーバを使用できないのだ。この問題はずいぶん前から大騒ぎになっていたようなのだが(当然だ)、未だに対応されておらず、また対応されない理由も不明のようだ。普通に考えて、オフィスのWiFiからは必ずプロキシを使用しないとインターネットに出ることができないような環境はたくさんあると思うのだが、いったい何がどうなっているのだろうか。そもそもオープンソースなのになぜこの点が改善できないのか?なかなか謎が深い。該当スレッドのコメントはすでに1000を超えており、「Fuck Google」などの建設的な意見で埋め尽くされている。

    さて今回の目的はあくまでも対象となるAndroidアプリケーションの通信内容の解析であるため、実機でのプロキシ設定についてはひとまずあきらめ、エミュレータで動作させるという妥協をすることにした。エミュレータでは起動の際のコマンドライン引数に、以下のように-http-proxyを渡すことでプロキシが使用されるようになる。

    -http-proxy http://192.168.1.20:8080/
    

    先述したように普通にMITMしようとしても証明書のエラーとなる。少し長いが、このときのスタックトレースを以下に示す。

    02-23 12:13:57.144: INFO/HA(274): https://www.verisign.co.jp/
    02-23 12:13:57.315: INFO/HA(274): javax.net.ssl.SSLException: Not trusted server certificate
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:371)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.luni.internal.net.www.protocol.http.HttpConnection.getSecureSocket(HttpConnection.java:168)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:399)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:1152)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:253)
    02-23 12:13:57.315: INFO/HA(274):     at net.jumperz.app.android.test1.HelloActivity.http(HelloActivity.java:113)
    02-23 12:13:57.315: INFO/HA(274):     at net.jumperz.app.android.test1.HelloActivity.onKey(HelloActivity.java:89)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.View.dispatchKeyEvent(View.java:3735)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchKeyEvent(PhoneWindow.java:1667)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.policy.impl.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1102)
    02-23 12:13:57.315: INFO/HA(274):     at android.app.Activity.dispatchKeyEvent(Activity.java:2063)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1643)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewRoot.deliverKeyEventToViewHierarchy(ViewRoot.java:2471)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewRoot.handleFinishedEvent(ViewRoot.java:2441)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1735)
    02-23 12:13:57.315: INFO/HA(274):     at android.os.Handler.dispatchMessage(Handler.java:99)
    02-23 12:13:57.315: INFO/HA(274):     at android.os.Looper.loop(Looper.java:123)
    02-23 12:13:57.315: INFO/HA(274):     at android.app.ActivityThread.main(ActivityThread.java:4627)
    02-23 12:13:57.315: INFO/HA(274):     at java.lang.reflect.Method.invokeNative(Native Method)
    02-23 12:13:57.315: INFO/HA(274):     at java.lang.reflect.Method.invoke(Method.java:521)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
    02-23 12:13:57.315: INFO/HA(274):     at dalvik.system.NativeStart.main(Native Method)
    02-23 12:13:57.315: INFO/HA(274): Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: TrustAnchor for CertPath not found.
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:168)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:366)
    02-23 12:13:57.315: INFO/HA(274):     ... 26 more
    02-23 12:13:57.315: INFO/HA(274): Caused by: java.security.cert.CertPathValidatorException: TrustAnchor for CertPath not found.
    02-23 12:13:57.315: INFO/HA(274):     at org.bouncycastle.jce.provider.PKIXCertPathValidatorSpi.engineValidate(PKIXCertPathValidatorSpi.java:149)
    02-23 12:13:57.315: INFO/HA(274):     at java.security.cert.CertPathValidator.validate(CertPathValidator.java:202)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:164)
    02-23 12:13:57.315: INFO/HA(274):     ... 27 more
    

    余談だがスタックトレースの下の方を見ると、bouncycastleのライブラリも使用されているらしいこともわかる。

    さてこのスタックトレースの中で、どこかで例外が発生しないようにしてしまえばいい。つまり実行されるJavaのコードのどこかをあらかじめ書き換えておくというアプローチだ。androidアプリケーションのリバースエンジニアリングで示したように、アプリケーション自身の書き換えを行ってもよいのだが(この方法については別エントリで書くかもしれない)、せっかくのオープンソースのOSなので、OS側を書き換えてしまうことにする。なぜなら、OS側でSSLのエラーが出ないようにしてしまえば、その上で動作するあらゆるアプリケーションの通信をフックできるようになるからだ。そこで、今回はAndroidをソースコードからビルドする環境を整えた上で、OS側(Dalvik側)のコードを書き換えることにした。

    0x03. 関数の追加


    まず、先述のスタックトレースの5行目である

    org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:399)
    

    に注目する。ソースコードは以下のようになっている。

    sslSocket = connection.getSecureSocket(getSSLSocketFactory(), getHostnameVerifier());
    

    getSecureSocket関数を呼び出す際の2つの引数は、それぞれSSLのソケットファクトリのインスタンスと、ホスト名が証明書のCommonNameとマッチしているかどうかを判定するHostnameVerifierのインスタンスとなっている。この2つのインスタンスがきちんと仕事をしてくれるおかげで正しいSSL通信が実現される。そこで、今回はこの2つのインスタンスとして、仕事をしないだめだめなソケットファクトリとHostnameVerifierを渡すように書き換えてみる。

    HttpsURLConnectionImpl.javaに、以下の2つの関数を追加する。都合がよいことにこれらの関数はsuperクラスで定義されているものなので、このクラスに追加することで自動的にこの実装が呼び出されるようになる。

    public javax.net.ssl.SSLSocketFactory getSSLSocketFactory()
    {
    try
    {
    javax.net.ssl.SSLContext ctx = javax.net.ssl.SSLContext.getInstance( "TLS" );
    javax.net.ssl.X509TrustManager bogusTm = new javax.net.ssl.X509TrustManager(){
    public java.security.cert.X509Certificate[] getAcceptedIssuers(){return null;}
    public void checkClientTrusted( java.security.cert.X509Certificate[] arg0, String arg1 )
    throws java.security.cert.CertificateException{}
    public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String arg1 )
    throws java.security.cert.CertificateException{}
    };
    ctx.init( null, new javax.net.ssl.TrustManager[]{ bogusTm }, null );
    return ctx.getSocketFactory();
    }
    catch( Exception e )
    {
    return getDefaultSSLSocketFactory();
    }
    }

    public javax.net.ssl.HostnameVerifier getHostnameVerifier()
    {
    javax.net.ssl.HostnameVerifier hv = new javax.net.ssl.HostnameVerifier()
    {
    public boolean verify( String hostname, javax.net.ssl.SSLSession session )
    {
    return true;
    }
    };
    return hv;
    }

    内容の詳しい解説は省略するが、とにかく一切証明書やホスト名等を評価しない、やる気なしのインスタンスを返す実装となっている。実に無駄のない、美しいソースコードだと言える。

    上記の2つの関数を追加したら、Androidをビルドする。するとURLConnectionクラスを使っている部分については、SSLの例外が発生しなくなるため、プロキシを使ってフックできるようになる。

    筆者はFroyoでテストした。Froyoの場合、書き換える対象のファイルは

    dalvik/libcore/luni/src/main/java/org/apache/harmony/luni/internal/net/www/protocol/https/HttpsURLConnectionImpl.java
    

    となる。

    0x04. Kindle for Androidでテスト


    実際にこのように書き換えたAndroidエミュレータ上でKindle for Androidを動作させてみたところ、見事にAmazonのサーバとの間の通信がフックできるようになった(すべての通信がSSLなところはさすがといったところである)。

    0x05. まとめ


    今回はAndroid上で動作するアプリケーションのSSL通信の内容を、あらかじめ書き換えたDalvik上で動作させることにより、プロキシサーバを使って解析する方法を1つ紹介した。この方法は下記のような特徴がある。

  • エミュレータ上で動作するアプリケーションのみ対象となる
  • URLConnectionクラスを使った操作のみが対象となる
  • エミュレータさえ準備できれば、すべてのアプリケーションをインストールして動作させるだけで解析することができる
  • 筆者の個人的な目的はKindle for Androidの通信を見てみたいというものだったので、既に目的は達成されてしまったのだが、別の方法についても探ってみるつもりである。

    0x06. 余談


    HTTPプロキシを経由させるためにはAndroidエミュレータ(QEMU)の起動時にコマンドラインオプションを渡すわけだが、この実装には下記2点のクセがあることがわかったのでここに記しておく。

  • chunkedエンコーディングされたレスポンスをまともに扱えないバグがある(新しいバージョンでは修正済みらしいが、筆者が落としてきたFroyoのソースコードでは直っていなかった)。そのため、プロキシ側で可能であればchunkedエンコーディングを扱わないようにしておくのがよい。Doormanであれば(.chunkToNormal response)というHookを定義すればOKである。
  • CONNECTリクエストの引数で、普通はホスト名を渡してくるものだが、QEMUは自身で名前解決を行った後のIPアドレスを渡してくる。MITMしたいプロキシの立場からすると接続したいホスト名を教えて欲しいところだろう。

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

    [PR]株式会社セキュアスカイ・テクノロジーのAndroidアプリケーション診断

    ReversingAndroidApp

    結論を先に書くと、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が使えなくなったのが悩みだったのだが、完全に解消された!