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したいプロキシの立場からすると接続したいホスト名を教えて欲しいところだろう。