しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

自分でAndroidのrootを取ってみた話(前編)

8月上旬からるくす先生のkernel exploit入門記事を読みつつroot奪取の練習をしていたらお盆休みに入ったのですが、 rkx1209.hatenablog.com

お休みで暇だし、TLはセキュリティキャンプで賑わっているし、夏休みの自由研究ということで自分でexploitを書いて自分のスマホのroot奪取にチャレンジしてみました。

「<機種名> root」でググるとCVE-2014-7911というキーワードが出てきたため、まずはここから調べてみることにしました。

なお、実験台となった端末(SONY製)はこんな感じの環境です。

  • ARM 32bit
  • Android 4.4.4 (Kitkat
  • DalvikVM(ARTへの切り替えは不可)
  • Linux Kernel 3.4.0

CVE-2014-7911

概要

この脆弱性は、Android 5.0.0未満におけるjava.io.ObjectInputStreamの実装にチェック漏れがあり、Serializableが実装されていないクラスもデシリアライズしてしまうというものです。

具体的には、シリアライズ可能なクラスHogeと、シリアライズ可能でないクラスPiyoがあった場合、

  1. Hogeインスタンスを作り、普通にシリアライズする
  2. シリアライズしてできたバイト列中の"Hoge"を"Piyo"に置き換える
  3. ObjectInputStreamで読み込ませる

とすると、シリアライズ可能でないはずの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を読めば他のアプリのメモリレイアウトもほとんど分かってしまいます。

試してみた

試しにObjectInputStreamSerializableなしのクラスをデシリアライズするかどうか確認するアプリを作り、実機で動かしてみたのですが……

成果が得られないままお盆休み終了です本当にありがとうございました。

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が呼ばれないという現象が解決できず、この脆弱性を使うのは諦めました。

ARTだとちゃんとfinalizeが呼ばれたため、気が向いたらARTな環境で再チャレンジしたいと思います。(恐らく永遠にやらない)

前編まとめ

時間をかけた割には成果が上がりませんでしたが、ユーザランドから見たAndroidの仕組みについていろいろ知ることができ、とても楽しかったです。(個人的にはASLRの件が衝撃的でした)

後編ではAndroid上でLinuxカーネルの既知の脆弱性を攻撃し、rootを取っていきます。

後編はこちら

References