自分でAndroidのrootを取ってみた話(前編)
8月上旬からるくす先生のkernel exploit入門記事を読みつつroot奪取の練習をしていたらお盆休みに入ったのですが、 rkx1209.hatenablog.com
お休みで暇だし、TLはセキュリティキャンプで賑わっているし、夏休みの自由研究ということで自分でexploitを書いて自分のスマホのroot奪取にチャレンジしてみました。
「<機種名> root」でググるとCVE-2014-7911というキーワードが出てきたため、まずはここから調べてみることにしました。
なお、実験台となった端末(SONY製)はこんな感じの環境です。
CVE-2014-7911
概要
この脆弱性は、Android 5.0.0未満におけるjava.io.ObjectInputStream
の実装にチェック漏れがあり、Serializable
が実装されていないクラスもデシリアライズしてしまうというものです。
具体的には、シリアライズ可能なクラスHoge
と、シリアライズ可能でないクラスPiyo
があった場合、
とすると、シリアライズ可能でないはずのPiyo
のインスタンスが作られてしまうという現象です。
影響
Androidでは、アプリ間やシステムサービスとの間でシリアライズしたオブジェクトをやりとりするということがよく行われています。
これは、攻撃側からすると、他の権限で動いているアプリやサービス上で任意のメンバ変数を持つ任意のクラスのインスタンスを作れるということでもあります。
大抵の場合、任意のオブジェクトを送りつけたところで、受け取り側が期待するオブジェクトではないためClassCastException等が発生しGCに片付けられて終了なのですが、送りつけるオブジェクトのクラスが
- 受け取り側がロード可能なクラス
finalize
内でネイティブコードの呼び出しがあり、そのネイティブコード内でオブジェクトのメンバ変数がポインタとして使われる
という条件を満たしていると、送りつけたオブジェクトをGCが片付ける際にメモリ破壊や任意コード実行に繋がる可能性が出てきます。(当然ながら、Serializable
が実装されていないクラスはシリアライズされることを想定した作りになっていない)
攻撃例
Androidでは一般アプリからアクセスできる領域は多くないため、高い権限で動いているシステムサービスを攻略するとカーネルが狙いやすくなります。
そのため、報告者のJann Horn氏が公開したPoCでは、高い権限で動いているsystem_serverというプロセスに対して細工したandroid.os.BinderProxy
(本来シリアライズ可能ではない)を送りつける手法がとられています。
android.os.BinderProxy
のメンバ変数をコントロールできるとき、finalize
時に何ができるのか、実装を見てみます。
// taken from https://android.googlesource.com/platform/frameworks/base/+/kitkat-release/core/java/android/os/Binder.java package android.os; final class BinderProxy implements IBinder { // attacker can control both mObject and mOrgue private int mObject; private int mOrgue; private native final void destroy(); @Override protected void finalize() throws Throwable { try { destroy(); } finally { super.finalize(); } } // ... }
// taken from https://android.googlesource.com/platform/frameworks/base/+/kitkat-release/core/jni/android_util_Binder.cpp static void android_os_BinderProxy_destroy(JNIEnv* env, jobject obj) { // attacker can control both b and drl IBinder* b = (IBinder*) env->GetIntField(obj, gBinderProxyOffsets.mObject); DeathRecipientList* drl = (DeathRecipientList*) env->GetIntField(obj, gBinderProxyOffsets.mOrgue); LOGDEATH("Destroying BinderProxy %p: binder=%p drl=%p\n", obj, b, drl); env->SetIntField(obj, gBinderProxyOffsets.mObject, 0); env->SetIntField(obj, gBinderProxyOffsets.mOrgue, 0); drl->decStrong((void*)javaObjectForIBinder); b->decStrong((void*)javaObjectForIBinder); IPCThreadState::self()->flushCommands(); }
// taken from https://android.googlesource.com/platform/system/core/+/kitkat-release/libutils/RefBase.cpp void RefBase::decStrong(const void* id) const { weakref_impl* const refs = mRefs; // note: mRefs == this refs->removeStrongRef(id); const int32_t c = android_atomic_dec(&refs->mStrong); // c = refs->mStrong--; #if PRINT_REFS ALOGD("decStrong of %p from %p: cnt=%d\n", this, id, c); #endif ALOG_ASSERT(c >= 1, "decStrong() called on %p too many times", refs); if (c == 1) { refs->mBase->onLastStrongRef(id); // [!] arbitrary code execution via vtable call if ((refs->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) { delete this; } } refs->decWeak(id); }
事前にオブジェクトを送りまくってヒープスプレーしておけば、stack pivot→ROPで任意コード実行に持ち込めそうな感じです。
なお、AndroidにもASLRがあるにはあるのですが、DalvikVMやARTで動くアプリはzygoteというプロセスからforkする仕組み(こうすることでアプリ起動時のVM初期化の手間を省ける)になっているため、/proc/self/maps
を読めば他のアプリのメモリレイアウトもほとんど分かってしまいます。
Androidで動いてるアプリ、みんなzygoteというプロセスからforkする関係でどのアプリでもlibc baseが一緒なのASLRの意味なくない? pic.twitter.com/3bYwiDgpd5
— しゃろ (@Charo_IT) August 13, 2017
試してみた
試しにObjectInputStream
がSerializable
なしのクラスをデシリアライズするかどうか確認するアプリを作り、実機で動かしてみたのですが……
夏休みの自由研究と称して自分のスマホ用にCVE-2014-7911のexploitを書いて遊ぼうとしてたんですが、なぜか修正済だったので終了ですお疲れさまでした
— しゃろ (@Charo_IT) August 15, 2017
成果が得られないままお盆休み終了です本当にありがとうございました。
CVE-2015-3837
概要
CVE-2014-7911に対して、Googleはデシリアライズ時のチェックを実装するという修正を行いました。
しかし、
Serializable
が実装されている- 受け取り側がロード可能なクラス
finalize
(もしくはその他のメソッド)内でネイティブコードの呼び出しがあり、そのネイティブコード内でオブジェクトのメンバ変数がポインタとして使われる- ポインタとして使われるメンバ変数に
transient
修飾子が付いていない(transient
修飾子がついたメンバ変数はシリアライズ処理の対象外となる)
という条件を満たすクラスが存在する場合、CVE-2014-7911と同じ手法で他のアプリやサービスを攻撃することができます。
Or Peles氏とRoee Hay氏はこれらの条件を満たすクラスを探し、org.conscrypt.OpenSSLX509Certificate
が使えることを見つけました。
攻撃例
org.conscrypt.OpenSSLX509Certificate
の実装を見てみます。
// taken from https://android.googlesource.com/platform/external/conscrypt/+/android-5.0.0_r1/src/main/java/org/conscrypt/OpenSSLX509Certificate.java package org.conscrypt; import java.security.cert.X509Certificate; // X509Certificate is a subclass of java.security.cert.Certificate which implements Serializable public class OpenSSLX509Certificate extends X509Certificate { // attacker can control mContext private final long mContext; // <- this must have "transient" @Override protected void finalize() throws Throwable { try { if (mContext != 0) { NativeCrypto.X509_free(mContext); } } finally { super.finalize(); } } // ... }
// taken from https://android.googlesource.com/platform/external/conscrypt/+/android-5.0.0_r1/src/main/native/org_conscrypt_NativeCrypto.cpp static void NativeCrypto_X509_free(JNIEnv* env, jclass, jlong x509Ref) { X509* x509 = reinterpret_cast<X509*>(static_cast<uintptr_t>(x509Ref)); JNI_TRACE("X509_free(%p)", x509); if (x509 == NULL) { jniThrowNullPointerException(env, "x509 == null"); JNI_TRACE("X509_free(%p) => x509 == null", x509); return; } X509_free(x509); }
OpenSSLのソースはマクロ地獄なため、X509_free
の定義を探すのが大変なのですが、発表者のスライドによると
void simplified_X509_free(void *mContext) { int *ref = (int *)(mContext + 0x10); if(*ref > 0) { *ref--; } else { free(...); } }
とのことなので、これを使って関数ポインタを書き換えればipを奪えそうです。
試してみた
端末内のバイナリを調べたところ、脆弱性が存在することは分かったのですが、
デシリアライズで作ったオブジェクトをGCが処理する際になぜかfinalize
が呼ばれないという現象が解決できず、この脆弱性を使うのは諦めました。
Android 4.4.4でObjectInputStreamで作ったオブジェクトのfinalizeが呼ばれない問題が解決できない……
— しゃろ (@Charo_IT) August 26, 2017
ARTだとちゃんとfinalize
が呼ばれたため、気が向いたらARTな環境で再チャレンジしたいと思います。(恐らく永遠にやらない)
前編まとめ
時間をかけた割には成果が上がりませんでしたが、ユーザランドから見たAndroidの仕組みについていろいろ知ることができ、とても楽しかったです。(個人的にはASLRの件が衝撃的でした)
後編ではAndroid上でLinuxカーネルの既知の脆弱性を攻撃し、rootを取っていきます。
後編はこちら
References
- CVE-2014-7911
- CVE-2015-3837