序列化和反序列化是指將內存數據結構轉換為字節流,通過網絡傳輸或者保存到磁盤,然后再將字節流恢復為內存對象的過程。在 Web 安全領域,出現過很多反序列化漏洞,比如 PHP 反序列化、Java 反序列化等。由于在反序列化的過程中觸發了非預期的程序邏輯,從而被攻擊者用精心構造的字節流觸發并利用漏洞從而最終實現任意代碼執行等目的。
Android 中除了傳統的 Java 序列化機制,還有一個特殊的序列化方法,即?Parcel[1]。根據官方文檔的介紹,Parcelable 和 Bundle 對象主要的作用是用于跨進程邊界的數據傳輸(IPC/Binder),但 Parcel 并不是一個通用的序列化方法,因此不建議開發者將 Parcel 數據保存到磁盤或者通過網絡傳輸。
作為 IPC 傳輸的數據結構,Parcel 的設計初衷是輕量和高效,因此缺乏完善的安全校驗。這就引發了歷史上出現過多次的 Android 反序列化漏洞,本文就按照時間線對其進行簡單的分析和梳理。
注: 本文中所展現的 AOSP 示例代碼,如無特殊說明則都來自文章發表時的 master 分支。
Parcel 101
在介紹漏洞之前,我們還是按照慣例先來了解下基礎知識。對于有過 Android 開發或者逆向分析經驗的同學應該對 Parcel 都不陌生,但通常也很少直接使用該類去序列化/反序列化數據然后進行 IPC 通信,而是通過 AIDL 等方法去自動生成模版,然后集成實現對應接口。
AIDL
關于 AIDL 開發的示例可以參考?Android 進程間通信與逆向分析[2]?一文,簡單來說,假設有以下 AIDL 文件:
package?com.evilpan; interface?IFooService?{ ????parcelable?Person?{ ????????String?name; ????????int?age; ????????boolean?gender; ????} ????String?foo(int?a,?String?b,?in?Person?c); }
那么生成的(Java)模版大致結構如下:
public?interface?IFooService?extends?android.os.IInterface?{ ????public?java.lang.String?foo(int?a,?java.lang.String?b,?com.evilpan.IFooService.Person?c)?throws?android.os.RemoteException; ????public?static?class?Person?implements?android.os.Parcelable?{?/*?...?*/?} ????public?static?abstract?class?Stub?extends?android.os.Binder?implements?com.evilpan.IFooService?{ ????????@Override?public?boolean?onTransact(int?code,?android.os.Parcel?data,?android.os.Parcel?reply,?int?flags)?throws?android.os.RemoteException?{ ????????????data.enforceInterface(descriptor); ????????????//?... ????????????switch(code)?{ ????????????????case?TRANSACTION_foo?{ ????????????????????int?_arg0; ????????????????????_arg0?=?data.readInt(); ????????????????????java.lang.String?_arg1; ????????????????????_arg1?=?data.readString(); ????????????????????com.evilpan.IFooService.Person?_arg2; ????????????????????_arg2?=?_Parcel.readTypedObject(data,?com.evilpan.IFooService.Person.CREATOR); ????????????????????java.lang.String?_result?=?this.foo(_arg0,?_arg1,?_arg2); ????????????????????reply.writeNoException(); ????????????????????reply.writeString(_result); ????????????????????break; ????????????????} ????????????} ????????} ????} ????private?static?class?Proxy?implements?com.evilpan.IFooService?{ ????????@Override?public?java.lang.String?foo(int?a,?java.lang.String?b,?com.evilpan.IFooService.Person?c)?throws?android.os.RemoteException ????????{ ????????????android.os.Parcel?_data?=?android.os.Parcel.obtain(); ????????????android.os.Parcel?_reply?=?android.os.Parcel.obtain(); ????????????java.lang.String?_result; ????????????try?{ ????????????_data.writeInterfaceToken(DESCRIPTOR); ????????????_data.writeInt(a); ????????????_data.writeString(b); ????????????_Parcel.writeTypedObject(_data,?c,?0); ????????????boolean?_status?=?mRemote.transact(Stub.TRANSACTION_foo,?_data,?_reply,?0); ????????????_reply.readException(); ????????????_result?=?_reply.readString(); ????????????} ????????????finally?{ ????????????_reply.recycle(); ????????????_data.recycle(); ????????????} ????????????return?_result; ????????} ????} ????//?... }
其中?IFooService.Stub?類是本地的 IPC 實現,即服務端代碼通過繼承至該類并實現其?foo?方法;而?Proxy?則是客戶端的的輔助類,客戶端可以通過調用?Proxy.foo?方法間接地調用服務端的對應代碼。數據傳輸的過程通過?transact?方法實現,其底層是 Android 的 Binder IPC;而數據的封裝過程則通過 Parcel 實現。
可以看到上面模版代碼中客戶端分別調用了?writeInterfaceToken、writeInt、writeString?和?writeTypedObject?來填充傳輸的?_data,而 Stub 類的 onTransact 中以同樣的順序分別調用了?enforceInterface、readInt、readString、readTypedObject?來獲取?_data?中的數據。
Parcelable
在上面的 AIDL 中,我們還定義了一個數據結構 Person,該結構同樣會由 AIDL 生成對應的模版類:
public?static?class?Person?implements?android.os.Parcelable { ??public?java.lang.String?name; ??public?int?age?=?0; ??public?boolean?gender?=?false; ??public?static?final?android.os.Parcelable.Creator?CREATOR?=?new?android.os.Parcelable.Creator ()?{ ????@Override ????public?Person?createFromParcel(android.os.Parcel?_aidl_source)?{ ??????Person?_aidl_out?=?new?Person(); ??????_aidl_out.readFromParcel(_aidl_source); ??????return?_aidl_out; ????} ????@Override ????public?Person[]?newArray(int?_aidl_size)?{ ??????return?new?Person[_aidl_size]; ????} ??}; ??@Override?public?final?void?writeToParcel(android.os.Parcel?_aidl_parcel,?int?_aidl_flag) ??{ ????int?_aidl_start_pos?=?_aidl_parcel.dataPosition(); ????_aidl_parcel.writeInt(0); ????_aidl_parcel.writeString(name); ????_aidl_parcel.writeInt(age); ????_aidl_parcel.writeInt(((gender)?(1):(0))); ????int?_aidl_end_pos?=?_aidl_parcel.dataPosition(); ????_aidl_parcel.setDataPosition(_aidl_start_pos); ????_aidl_parcel.writeInt(_aidl_end_pos?-?_aidl_start_pos); ????_aidl_parcel.setDataPosition(_aidl_end_pos); ??} ??public?final?void?readFromParcel(android.os.Parcel?_aidl_parcel) ??{ ????int?_aidl_start_pos?=?_aidl_parcel.dataPosition(); ????int?_aidl_parcelable_size?=?_aidl_parcel.readInt(); ????try?{ ??????if?(_aidl_parcelable_size?4)?throw?new?android.os.BadParcelableException("Parcelable?too?small");; ??????if?(_aidl_parcel.dataPosition()?-?_aidl_start_pos?>=?_aidl_parcelable_size)?return; ??????name?=?_aidl_parcel.readString(); ??????if?(_aidl_parcel.dataPosition()?-?_aidl_start_pos?>=?_aidl_parcelable_size)?return; ??????age?=?_aidl_parcel.readInt(); ??????if?(_aidl_parcel.dataPosition()?-?_aidl_start_pos?>=?_aidl_parcelable_size)?return; ??????gender?=?(0!=_aidl_parcel.readInt()); ????}?finally?{ ??????if?(_aidl_start_pos?>?(Integer.MAX_VALUE?-?_aidl_parcelable_size))?{ ????????throw?new?android.os.BadParcelableException("Overflow?in?the?size?of?parcelable"); ??????} ??????_aidl_parcel.setDataPosition(_aidl_start_pos?+?_aidl_parcelable_size); ????} ??} ??@Override ??public?int?describeContents()?{ ????int?_mask?=?0; ????return?_mask; ??} }
其中關鍵的是 writeToParcel 和?CREATOR.createFromParcel?方法,分別填充了該自定義結構序列化和反序列化的實現,當然我們也可以自己繼承?Parcelable?去實現自己的可序列化數據結構。
內存布局
從接口上看,Parcel 可以支持按照一定順序寫入和讀取 int、long 等原子數據,也支持 String、IBinder、和 FileDescriptor 這些復雜的數據結構。為了理解后文介紹的漏洞,還需要了解在二進制層面這些數據的存儲方式。
Parcel 的代碼接口實現在?android/os/Parcel.java?中,但大部分方法最終都會調用到其中的 native 方法,其 JNI 定義在?frameworks/base/core/jni/android_os_Parcel.cpp?文件里,最終的實現則是在?frameworks/native/libs/binder/Parcel.cpp?中。
以?Parcel.writeInt?為例,其 Java 實現很簡單,直接轉到 native 方法:
private?static?native?int?nativeWriteInt(long?nativePtr,?int?val); public?final?void?writeInt(int?val)?{ ????int?err?=?nativeWriteInt(mNativePtr,?val); ????if?(err?!=?OK)?{ ????????nativeSignalExceptionForError(err); ????} }
C++ 中的 JNI 實現則是先將 nativePtr 轉換為 Parcel 指針,而后直接調用?writeInt32?方法:
static?int?android_os_Parcel_writeInt(jlong?nativePtr,?jint?val)?{ ????Parcel*?parcel?=?reinterpret_cast(nativePtr); ????return?(parcel?!=?NULL)???parcel->writeInt32(val)?:?OK; }
接下來就是最終實際的實現了:
status_t?Parcel::writeInt32(int32_t?val) { ????return?writeAligned(val); } templatestatus_t?Parcel::writeAligned(T?val)?{ ????static_assert(PAD_SIZE_UNSAFE(sizeof(T))?==?sizeof(T)); ????static_assert(std::is_trivially_copyable_v ); ????if?((mDataPos+sizeof(val))?<=?mDataCapacity)?{ restart_write: ????????memcpy(mData?+?mDataPos,?&val,?sizeof(val)); ????????return?finishWrite(sizeof(val)); ????} ????status_t?err?=?growData(sizeof(val)); ????if?(err?==?NO_ERROR)?goto?restart_write; ????return?err; }
writeAligned?是個模版函數,用于寫入基礎的 C++ 數據類型,即 int、float、double 等,也可以寫入指針數據。實現也相對簡單,這里面就涉及到了 Parcel 內部的幾個重要數據結構:
??mData: 序列化數據內存緩沖區的內存起始地址(指針);
??mDataPos: 序列化數據當前解析到的相對位置;
??mDataCapacity: 緩沖區的總大?。?/p>
還有一個字段 mDataSize 表示當前序列化數據的大小,其實這個字段基本上和 mDataPos 的值是一致的,二者都在 finishWrite 函數中進行更新:
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 ",?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 ",?mDataPos,?mDataSize); ????return?NO_ERROR; }
而如果當前緩沖區的內存不足,則會使用 growData 方法進行更新:
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; ????} ????if?(len?>?SIZE_MAX?-?mDataSize)?return?NO_MEMORY;?//?overflow ????if?(mDataSize?+?len?>?SIZE_MAX?/?3)?return?NO_MEMORY;?//?overflow ????size_t?newSize?=?((mDataSize+len)*3)/2; ????return?(newSize?<=?mDataSize) ??????????????(status_t)?NO_MEMORY ????????????:?continueWrite(std::max(newSize,?(size_t)?128)); }
continueWrite?方法的實現比較復雜,因為其中還支持傳入小于 mDataSize 的參數去縮小 Parcel 內存,不過在我們這里的上下文中僅用于增長內存,因此實際上最終只是通過?realloc?或者?malloc?去分配更多的內存:
if?(desired?>?mDataCapacity)?{ ????uint8_t*?data?=?reallocZeroFree(mData,?mDataCapacity,?desired,?mDeallocZero); ????if?(data)?{ ????????LOG_ALLOC("Parcel?%p:?continue?from?%zu?to?%zu?capacity",?this,?mDataCapacity, ????????????????desired); ????????gParcelGlobalAllocSize?+=?desired; ????????gParcelGlobalAllocSize?-=?mDataCapacity; ????????mData?=?data; ????????mDataCapacity?=?desired; ????}?else?{ ????????mError?=?NO_MEMORY; ????????return?NO_MEMORY; ????} }
這部分目前只需要了解即可,我們關注的還是前面寫入數據的邏輯,回顧一下是在 writeAligned 方法中,直接通過?memcpy?去寫入的數據,因此對于基礎數字類型是沒有額外開銷的,且序列化的字節序就是當前機器的字節序。這也是為什么 Parcel 只適合在同一設備中實現 IPC,如果對于不同設備中可能會出現字節序的問題。
String
說完了 Int 我們接著看常用的 String 類型,JNI 的定義和實現如下:
static?void?android_os_Parcel_writeString16(JNIEnv?*env,?jclass?clazz,?jlong?nativePtr, ????????jstring?val)?{ ????Parcel*?parcel?=?reinterpret_cast(nativePtr); ????if?(parcel?!=?nullptr)?{ ????????status_t?err?=?NO_ERROR; ????????if?(val)?{ ????????????//?NOTE:?Keep?this?logic?in?sync?with?Parcel.cpp ????????????const?size_t?len?=?env->GetStringLength(val); ????????????const?size_t?allocLen?=?len?*?sizeof(char16_t); ????????????err?=?parcel->writeInt32(len); ????????????char?*data?=?reinterpret_cast (parcel->writeInplace(allocLen?+?sizeof(char16_t))); ????????????if?(data?!=?nullptr)?{ ????????????????env->GetStringRegion(val,?0,?len,?reinterpret_cast (data)); ????????????????*reinterpret_cast (data?+?allocLen)?=?0; ????????????}?else?{ ????????????????err?=?NO_MEMORY; ????????????} ????????}?else?{ ????????????err?=?parcel->writeString16(nullptr,?0); ????????} ????????if?(err?!=?NO_ERROR)?{ ????????????signalExceptionForError(env,?clazz,?err); ????????} ????} }
和之前差別不大,值得注意的是?Parcel::writeInplace?返回的是待寫入的內存地址,直接用了?JNIEnv::GetStringRegion?去進行寫入。如果 Java 傳入的字符串是?null,則使用?writeString16(nullptr)?去寫入,其內部實現是寫入特殊的整數?-1:
GetStringRegion[3]?返回的是 Unicode 字符,因此每個字符占 2 字節;
status_t?Parcel::writeString16(const?char16_t*?str,?size_t?len) { ????if?(str?==?nullptr)?return?writeInt32(-1); ????//?... }
因此對于字符串結構,Parcel 的序列化也是無開銷順序寫入的。
Array
數組也是個常用的數據類型,但不同的數組傳輸格式有所不同。對于 char/int/long 等原始類型而言,傳輸數組實際上就是逐個寫入每個元素,并且在前面寫入數組的大小:
public?final?void?writeIntArray(@Nullable?int[]?val)?{ ????if?(val?!=?null)?{ ????????int?N?=?val.length; ????????writeInt(N); ????????for?(int?i=0;?i但是 wirteByteArray 有所優化:
public?final?void?writeByteArray(@Nullable?byte[]?b,?int?offset,?int?len)?{ ????if?(b?==?null)?{ ????????writeInt(-1); ????????return; ????} ????ArrayUtils.throwsIfOutOfBounds(b.length,?offset,?len); ????nativeWriteByteArray(mNativePtr,?b,?offset,?len); }JNI 中直接使用 memcpy 去寫入:
static?void?android_os_Parcel_writeByteArray(JNIEnv*?env,?jclass?clazz,?jlong?nativePtr, ?????????????????????????????????????????????jobject?data,?jint?offset,?jint?length) { ????Parcel*?parcel?=?reinterpret_cast(nativePtr); ????parcel->writeInt32(length); ????void*?dest?=?parcel->writeInplace(length); ????jbyte*?ar?=?(jbyte*)env->GetPrimitiveArrayCritical((jarray)data,?0); ????if?(ar)?{ ????????memcpy(dest,?ar?+?offset,?length); ????????env->ReleasePrimitiveArrayCritical((jarray)data,?ar,?0); ????} } Parcelable
對于?Parcelable?類型的數據,使用 writeParcelable 方法進行寫入:
public?final?void?writeParcelable(@Nullable?Parcelable?p,?int?parcelableFlags)?{ ????if?(p?==?null)?{ ????????writeString(null); ????????return; ????} ????writeParcelableCreator(p); ????p.writeToParcel(this,?parcelableFlags); } public?final?void?writeParcelableCreator(@NonNull?Parcelable?p)?{ ????String?name?=?p.getClass().getName(); ????writeString(name); }其中需要注意的是在調用?Parcelable.writeToParcel?之前,會先獲取 Parcelable 實際的類名,并以字符串的方式寫入。
FileDescriptor
Andorid IPC 的一個特點是可以支持傳輸文件句柄,其 JNI 實現如下:
static?void?android_os_Parcel_writeFileDescriptor(JNIEnv*?env,?jclass?clazz,?jlong?nativePtr,?jobject?object) { ????Parcel*?parcel?=?reinterpret_cast(nativePtr); ????if?(parcel?!=?NULL)?{ ????????const?status_t?err?= ????????????????parcel->writeDupFileDescriptor(jniGetFDFromFileDescriptor(env,?object)); ????????if?(err?!=?NO_ERROR)?{ ????????????signalExceptionForError(env,?clazz,?err); ????????} ????} } object 為?FileDescriptor?對象,這里的實現就是獲取?FileDescriptor.fd,即?open(2)?返回的,int 格式的文件描述符,進行?fcntl(oldFd, F_DUPFD_CLOEXEC, 0)?復制后進行寫入。
Binder 的系統調用本身就支持傳輸文件類型的數據,因此這里 Parcel 的實現只需要做好上層的封裝:
status_t?Parcel::writeFileDescriptor(int?fd,?bool?takeOwnership)?{ ????//?... ????switch?(rpcFields->mSession->getFileDescriptorTransportMode())?{ ????????case?RpcSession: ????????case?RpcSession:?{ ????????????if?(status_t?err?=?writeInt32(RpcFields::TYPE_NATIVE_FILE_DESCRIPTOR);?err?!=?OK)?{ ????????????????return?err; ????????????} ????????????if?(status_t?err?=?writeInt32(rpcFields->mFds->size());?err?!=?OK)?{ ????????????????return?err; ????????????} ????????} ????} ????flat_binder_object?obj; ????obj.hdr.type?=?BINDER_TYPE_FD; ????obj.flags?=?0; ????obj.binder?=?0;?/*?Don't?pass?uninitialized?stack?data?to?a?remote?process?*/ ????obj.handle?=?fd; ????obj.cookie?=?takeOwnership???1?:?0; ????return?writeObject(obj,?true); }值得一提的是,writeObject 中寫入數據除了更新上面提到的 mData 等字段,還需要更新?kernelFields,如?mObjects?字段,作為內核參數的記錄。
Binder
在 Android IPC 中一個重要的參數類型就是回調,即客戶端發送一個 IBinder 類型的對象給服務端,然后服務端可以調用其 onTransact 方法實現反向異步的數據傳輸。對于這種類型的數據,主要通過 writeStrongBinder 方法:
public?final?void?writeStrongBinder(IBinder?val)?{ ????nativeWriteStrongBinder(mNativePtr,?val); }JNI 實現和傳輸文件類似:
static?void?android_os_Parcel_writeStrongBinder(JNIEnv*?env,?jclass?clazz,?jlong?nativePtr,?jobject?object) { ????Parcel*?parcel?=?reinterpret_cast(nativePtr); ????if?(parcel?!=?NULL)?{ ????????const?status_t?err?=?parcel->writeStrongBinder(ibinderForJavaObject(env,?object)); ????????if?(err?!=?NO_ERROR)?{ ????????????signalExceptionForError(env,?clazz,?err); ????????} ????} } 主要分為兩步,第一步取出 IBinder Java 對象的 Native 指針,即?IBinder.mObject,然后將其轉為 C++ 的 IBinder 指針傳入:
status_t?Parcel::writeStrongBinder(const?sp&?val) { ????return?flattenBinder(val); } 傳輸 IBinder 類型的數據同樣需要更新 mObjects 緩存。
其他
除了上面介紹的這些,Parcel 實現中還有許多值得關注的細節,比如 writeBlob 同樣也是寫入?byte[],但對于過大的數據會選擇用共享內存的方式去進行傳輸。但根據上面的簡單分析,我們也能大致看出 Parcel 的一些問題,即序列化和反序列化純屬手工操作,并且在某些操作中沒有嚴格的類型檢查等,下面我們就來逐一探討。
Bundle
在 Andorid 中,Bundle 類是一個類似 HashMap 的數據結構,但其是為了在 Parcel 序列化/反序列化的使用中而高度優化的。因此理解 Bundle 本身的序列化過程對我們理解后面的內容也至關重要。
序列化
Bundle 的序列化過程調用鏈路如下:
??Bundle.writeToParcel
??BaseBundle.writeToParcelInner
??parcel.writeArrayMapInternal
writeToParcelInner 中主要負責寫入 Bundle 相關的頭部字段:
void?writeToParcelInner(Parcel?parcel,?int?flags)?{ ????//?Keep?implementation?in?sync?with?writeToParcel()?in ????//?frameworks/native/libs/binder/PersistableBundle.cpp. ????final?ArrayMap?map; ????synchronized?(this)?{ ????????//?... ????} ????//?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); } writeArrayMapInternal 則主要實現 Bundle 內部字典數據的寫入:
void?writeArrayMapInternal(@Nullable?ArrayMap?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); ????int?startPos; ????for?(int?i=0;?i 可以看到序列化 Bundle 的過程是和 序列化 ArrayMap 一致的,即先寫入整型的 size,然后依次寫入每個 key 和 value。這里值得注意的是 wirteValue 的實現:
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?{ ????????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); ????????} ????} }注意,為了便于按照時間線理解歷史漏洞,這里代碼使用的是?Android 8.0 中的版本[4]。
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; private?static?final?int?VAL_DOUBLEARRAY?=?28;可以看到其中支持大部分基礎類型以及 Parcelable、IBinder 等 Parcel 本身所支持的類型。
綜上所述,我們可以大致得出 Bundle 的內存布局:
size type value 4 Int length 4 Int BUNDLE_MAGIC (0x4C444E42) 4 Int map.size() xxx String key[0] xxx Dynamic val[0] ... ... ... xxx String key[map.size() - 1] xxx Dynamic val[map.size() - 1] 其中 key 因為是 String 類型,因此內部是長度 + String16 的布局,而 value 則根據類型不同使用不同的結構。
key:
??length Int32
??data String16
value:
??VAL_XXX Int32
??data writeInt/writeString/...
下面是在一個 Bundle 序列化的示例,原始內容為:
Bundle?A?=?Bundle() A.putInt("key1",?0x1337) A.putString("key2",?"hello") A.putLong("key33",?0x0123456789)使用 Parcel 序列化后的二進制數據如下:
bin.png
其中有幾個需要注意的細節:
??length 字段是不包括 length+magic 部分的,因此實際上序列化數據的總大小會比頭部的結果大 8 字節即 100;
??String 數據是 4 字節對齊的(如 key33);如果本身已經對齊,也會在后面進行特殊拓展,如(key1/key2);特殊拓展的值為?Parcel::writeInplace?中的 padMask,對于已經 4 字節對齊的 mask 正好是 0;
存儲
Bundle 雖然內部是 ArrayMap 結構,但實際存儲的時候還有一點優化。在平時開發中細心的朋友可能會發現,有時候在獲取遠程的 Bundle 比如 CotentResolver.call 的結果時候,直接打印輸出會是?Bundle[mParcelledData.dataSize=xxx]?的結果。但是對于自己構建的 Bundle,卻可以打印出完整的 Map 元素。
這其實可以從 Bundle 的源碼中看出來:
@Override public?synchronized?String?toString()?{ ????if?(mParcelledData?!=?null)?{ ????????if?(isEmptyParcel())?{ ????????????return?"Bundle[EMPTY_PARCEL]"; ????????}?else?{ ????????????return?"Bundle[mParcelledData.dataSize="?+ ????????????????????mParcelledData.dataSize()?+?"]"; ????????} ????} ????return?"Bundle["?+?mMap.toString()?+?"]"; }關鍵就是這個?mParcelledData?字段,該字段定義在父類即 BaseBundle 中,為 Parcel 類型。由于 Bundle 經常在進程間進行傳輸,因此設計上認為可以不要到對端的時候馬上就反序列化出所有的 ArrayMap,而是等需要用到時再進行解析,這也可以認為是一種懶加載的技術。對于一些收到 Bundle 后馬上又傳輸給其他服務的場景,這種設計可以直接傳輸未解析的 Parcel 數據而不需要來回的序列化。
因此對于 Bundle 而言,對于一些需要獲取內部元素的調用時才會進行反序列,比如 size 和 isEmpty 的實現:
public?int?size()?{ ????unparcel(); ????return?mMap.size(); } public?boolean?isEmpty()?{ ????unparcel(); ????return?mMap.isEmpty(); }值得注意的是,在 Bundle 內部,ArrayMap(mMap) 和 Parcel(mParcelledData) 同一時間只能存在一個,反序列化后會填充 mMap 把 mParcelledData 回收并置為 null。
反序列化與 Bundle 風水
最早提交 Android 反序列化漏洞的是 @BednarTildeOne 在 2014 年提交的 ParceledListSlice 漏洞,但第一次引起公眾關注的應該是 CVE-2017-0806。也是同樣的作者,不過給出了詳細的分析以及漏洞 POC 代碼,這才得以引起關注。
該漏洞的原理已經有很多師傅分析過了,就不再贅述,這里直接給出一個簡化的版本。假設有這么一個 Parcelable 數據結構,請問是否存在漏洞?漏洞如何利用?
public?class?Vulnerable?implements?Parcelable?{ ????private?long?mData; ????protected?Vulnerable(Parcel?in)?{ ????????mData?=?m.readInt(); ????} ????@Override ????public?void?writeToParcel(Parcel?parcel,?int?flags)?{ ????????parcel.writeLong(mData) ????} ????//?... }問題很明顯,可以看到其中序列化使用了 writeLong,但是反序列化過程卻用了 readInt,二者不匹配,這可能會導致一些數據錯誤,但這能算得上一個漏洞嗎?
回想 LunchAnywhere 漏洞以及對應的?patch[5],實際上是用戶可控的 Intent 數據中帶有?KEY_INTENT?extra 導致的啟動任意 Activity 的問題。修復過程是判斷用戶提供的 Intent 是否帶有該 extra,如果有則校驗調用者的簽名。當時的修復可以說沒什么問題,但如果配合上述的漏洞,就有可能出現繞過。
這里還是以測試代碼舉例,假設我們的校驗函數如下:
@Override public?void?onResult(Bundle?result)?{ ????Intent?intent?=?result.getParcelable(AccountManager.KEY_INTENT) ????if?(intent?!=?null)?{ ????????checkCallingSignature() ????} ????//?... ????send(result) } public?void?send(Bundle?result);?//?AIDL校驗時如果 intent 不為空則進行簽名校驗,隨后會調用 AIDL 的客戶端方法?send,該方法將 bundle 再次序列化然后發送給服務端,而服務端的實現如下:
class?Server?extends?IServer.Stub?{ ????@Override ????public?void?send(Bundle?result)?{ ????????Intent?intent?=?bundle.getParcelable(KEY_INTENT); ????????if?(intent?!=?null)?{ ????????????mContext.startActivity(intent) ????????} ????} }因為客戶端進行了校驗,所以服務就不需要再次校驗而是直接信任該 Bundle 并使用了。這里其實有個經典的漏洞模式即 TOCTOU 問題,一般情況下這個邏輯是沒問題的,即 bundle 檢查完后不會被再次修改。但在我們的漏洞場景中,就有可能會出現不同的 bundle,即?Self-Changing Bundle。
自修改 Bundle
Bundle 自修改,主要還是針對上面的這種 IPC 場景,一端對 Bundle 進行了校驗,發現沒問題后使用 IPC 發送給另一端,且對方不加校驗就直接使用。即 Bundle 在序列化+反序列化的過程中進行了改變。
假設服務端 B 接收我們的 Bundle 數據進行反序列化后再次序列化發送給服務端 C,而我們的 bundle 中帶有一個類型為 Vunerable 的 key,那在 B 接收到數據后再發出去的數據會如下圖右邊所示,其中本該是 Int 的字段被寫成了 Long,導致 C 再次反序列化時候對后續數據的解析出現異常:
Bundle
通過精心構造發送給 B 的數據,我們可以令 B 和 C 都能正常序列化出 Bundle 對象,甚至讓這兩個 Bundle 含有不同的 key!
漏洞利用
該漏洞如何利用呢?前文中我們已知 Bundle 序列化數據的頭部后面每個元素都是 key+value 的組合,那利用思路應該就是將多出來的 4 字節進行類型混淆。這種場景類似于二進制的緩沖區溢出,我們需要做的就是布局好數據使得同時滿足下述條件:
1.?類型混淆前后,Bundle 的 length 和 items 字段保持一致;
2.?兩次反序列化得到的對象都需要是合法對象;
3.?第二次反序列化要能夠出現一個前面沒有的 Bundle key;
溢出的 4 個字節會作為后面一個 key 的一部分,而 key 的類型是 String,根據前面的介紹,其內存是長度(Int32) + String16。其中 String16 是 unicode,使用 2 字節保存一個字符。因此,如果 map 元素足夠的話,這 4 個字節會形成下一個 key 的長度。
在大部分情況下,溢出的部分是 Long 數據的高有效位,因此會是 0,如果此時第二次反序列化處理到這里,會認為這有個 key 長度為 0 的元素,查看讀取字符串相關的代碼,如下所示:
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?考慮到 padding 的存在,即便 size 為 0,還是會讀出?(0+1)x2?即 2 字節的?x00。readInplace?中還會對齊,因此最終讀出的是 4 字節數據。因此,后續的反序列化都會在原來的基礎上后移 4 字節。
這樣一來利用思路就比較清晰了:
1.?構造一個惡意 Bundle,其中包含兩個元素 A0 和 A1,其中 A0 為 Vunlerable,在 A1 中布置 payload;
2.?反序列化+序列化后,數據多出 4 字節,且下個元素的解析從 A1 的 key 內容開始;
3.?構造 A1 的 key 和 value,使得第 2 步中解析出新的 key;
還是以前面的 Vulnerable 類為例,POC 代碼如下:
val?p?=?Parcel.obtain() val?lengthPos?=?p.dataPosition() //?header p.writeInt(-1)?//?length,?back-patch?later p.writeInt(0x4C444E42)?//?magic val?startPos?=?p.dataPosition(); p.writeInt(3)?//?numItem //?A0 p.writeString("A") p.writeInt(Type.VAL_PARCELABLE.value) p.writeString("com.evilpan.poc.Vulnerable") p.writeInt(666)?//?mData //?A1 p.writeString("u000du0000u0008")?//?0d00?0000?0800 p.writeInt(Type.VAL_BYTEARRAY.value) p.writeInt(28) p.writeString("intent")??????????????//?4?+?padSize((6?+?1)?*?2)?=?4+16?=?20 p.writeInt(Type.VAL_INTEGER.value)???//?=?4 p.writeInt(0x1337)???????????????????//?=?4 //?A2 p.writeString("BBB") p.writeInt(Type.VAL_NULL.value) //?Back-patch?length?&&?reset?position val?A?=?unparcel(p) Log.i("A?=?"?+?inspect(A)) Log.i("A.containsKey:?"?+?A.containsKey("intent")) val?B?=?unparcel(parcel(A)) Log.i("B?=?"?+?inspect(B)) Log.i("B.containsKey:?"?+?B.containsKey("intent"))在上述 POC 中,我們明面上構造了一個含有 3 個元素的 Bundle,分別是:
??A0: Parcelable 類型,元素為我們帶有漏洞的 Parcelable;
??A1: ByteArray 類型,長度為 28 字節,ByteArray 的內容為隱藏的 Intent 元素,即 20+4+4;
??A2: NULL 類型;
其中關鍵的是 A1 的 key,在觸發漏洞后,會被解析為第二個元素的元素類型和長度,即解析出來的內容為:
??0d000000: 解析為元素類型,等于 VAL_BYTEARRAY(13);
??08000000: 解析為 ByteArray 長度,即 8 字節;注意這里額外的 0 是字符串寫入時候 pad 出來的。
之所以是 8 字節,是為了把后面的長度字段吞掉,使得解析下一個元素可以直接到我們隱藏的 intent 中。
第三個元素 A2 只是用于占位,在第一次序列化時候有用,第二次時直接被我們隱藏的 intent 替代了。上述代碼的運行結果如下:
[INFO]:?A?=?Bundle(item?=?3)?length:?144 [INFO]:???=>?41000000?=?VAL_PARCELABLE:com.evilpan.poc.Vulnerable{mData=666} [INFO]:???=>?0d00000008000000?=?VAL_BYTEARRAY:0600000069006e00740065006e007400000000000100000037130000 [INFO]:???=>?4200420042000000?=?VAL_NULL:null [INFO]:?A.containsKey:?false [INFO]:?===?writeToParcel?=== [INFO]:?B?=?Bundle(item?=?3)?length:?148 [INFO]:???=>?41000000?=?VAL_PARCELABLE:com.evilpan.poc.Vulnerable{mData=666} [INFO]:???=>?03000000?=?VAL_BYTEARRAY:0d0000001c000000 [INFO]:???=>?69006e00740065006e00740000000000?=?VAL_INTEGER:1337 [INFO]:?B.containsKey:?true可以看到第一次反序列化出來的 Bundle A 包含三個 key,其中并沒有 intent 字段,但經過序列化再反序列化后的 Bundle B 已經修改了,第二個元素的變成了長度為 8 字節的 ByteArray,實際上其 key 長度為;第三個元素則是我們偽造的 intent 元素,值為 0x1337,因此我們成功地繞過了第一次 containsKey 的校驗,而在第二次中進行觸發!
上述例子在 Android 12 中進行測試,理論上之前的版本也類似。
實際上的漏洞與這個例子大同小異,由于 Android 系統對 IPC 的大量使用,許多操作都會在?system_server、Settings 應用、用戶應用中來回穿梭,也就造成了許多上述 TOCTOU 的問題。對其中細節感興趣的可以參考下面的分析文章:
??ReparcelBug - PoC for CVE-2017-0806[6]
??CVE-2017-0806 原理分析[7]
??Bundle風水——Android序列化與反序列化不匹配漏洞詳解[8]
漏洞修復
上述漏洞的修復似乎很直觀,只需要把 Vulnerable 類中不匹配的讀寫修復就行了。但實際上這類漏洞并不是個例,歷史上由于代碼編寫人員的粗心大意,曾經出現過許多因為讀寫不匹配導致的提權漏洞,包括但不限于:
??CVE-2017-0806 GateKeeperResponse
??CVE-2017-0664 AccessibilityNodelnfo
??CVE-2017-13288 PeriodicAdvertisingReport
??CVE-2017-13289 ParcelableRttResults
??CVE-2017-13286 OutputConfiguration
??CVE-2017-13287 VerifyCredentialResponse
??CVE-2017-13310 ViewPager's SavedState
??CVE-2017-13315 DcParamObject
??CVE-2017-13312 ParcelableCasData
??CVE-2017-13311 ProcessStats
??CVE-2018-9431 OSUInfo
??CVE-2018-9471 NanoAppFilter
??CVE-2018-9474 MediaPlayerTrackInfo
??CVE-2018-9522 StatsLogEventWrapper
??CVE-2018-9523 Parcel.wnteMapInternal0
??CVE-2021-0748 ParsingPackagelmpl
??CVE-2021-0928 OutputConfiguration
??CVE-2021-0685 ParsedIntentInfol
??CVE-2021-0921 ParsingPackagelmpl
??CVE-2021-0970 GpsNavigationMessage
??CVE-2021-39676 AndroidFuture
??CVE-2022-20135 GateKeeperResponse
??...
另一個修復思路是修復 TOCTOU 漏洞本身,即確保檢查和使用的反序列化對象是相同的,但這種修復方案也是治標不治本,同樣可能會被攻擊者找到其他的攻擊路徑并繞過。
因此,為了徹底解決這類層出不窮的問題,Google 提出了一種簡單粗暴的緩釋方案,即直接從 Bundle 類中下手。雖然 Bundle 本身是 ArrayMap 結構,但在反序列化時候即便只需要獲取其中一個 key,也需要把整個 Bundle 反序列化一遍。這其中的主要原因在于序列化數據中每個元素的大小是不固定的,且由元素的類型決定,如果不解析完前面的所有數據,就不知道目標元素在什么地方。
為此在 21 年左右,AOSP 中針對 Bundle 提交了一個稱為?LazyBundle(9ca6a5)[9]?的 patch。其主要思想為針對一些長度不固定的自定義類型,比如 Parcelable、Serializable、List 等結構或容器,會在序列化時將對應數據的大小添加到頭部。這樣在反序列化時遇到這些類型的數據,可以僅通過檢查頭部去選擇性跳過這些元素的解析,而此時 sMap 中對應元素的值會設置為 LazyValue,在實際用到這些值的時候再去對特定數據進行反序列化。
這個 patch 可以在一定程度上緩釋針對 Bundle 風水的攻擊,而且在提升系統健壯性也有所助益,因為即便對于損壞的 Parcel 數據,如果接收方沒有使用到對應的字段,就可以避免異常的發生。對于之前的 Bundle 解析策略,哪怕只調用了?size?方法,也會觸發所有元素的解析從而導致異常。 在這個 patch 中?unparcel?還增加了一個 boolean 參數?itemwise,如果為 true 則按照傳統方式解析每個元素,否則就會跳過 LazyValue 的解析。
CVE-2021-0928
在前面反序列化的示例中,漏洞主要出在一個自定義的 Vulnerable 類中,即手工編寫的 readFromParcel/writeToParcel 不匹配問題。在現實中,這種出現問題的類通常只在進程間使用而幾乎不用于跨進程,否則在正常 IPC 調用時候就會出現明顯的數據錯誤。但也還有一種情況,比如在 readFromParcel 方法調用的過程中出現異常,但是上級進行了“優雅”的?try-catch,從而程序并不會報錯,但異常發生后 Parcel 中還有剩余數據并未消費,因此會被后續的解析錯誤使用,比如在 AIDL 調用時候就會被后續的參數用到。
CVE-2021-0928?就是這么一個經典的漏洞。漏洞本身只存在于 Andorid 12 beta3 的短暫時間窗口中,因此我們并不需要太過于關注細節,此外漏洞的作者也已經在 Github 上公開了該漏洞的詳細介紹以及 POC 代碼。作為歷史漏洞研究,筆者這里只做簡單的介紹,并重點關注一些值得學習的漏洞利用思路和漏洞模式。完整的漏洞細節分析和 POC 可以參考原作者的分享:
??CVE-2021-0928, writeToParcel/createFromParcel serialization mismatch in android.hardware.camera2.params.OutputConfiguration[10]
漏洞介紹
主要出現漏洞的類為?android.hardware.camera2.params.OutputConfiguration,其代碼片段如下所示:
package?android.hardware.camera2.params; public?final?class?OutputConfiguration?implements?Parcelable?{ ????private?OutputConfiguration(@NonNull?Parcel?source)?{ ????????int?rotation?=?source.readInt(); ????????int?surfaceSetId?=?source.readInt(); ????????//?... ????????boolean?isMultiResolution?=?source.readInt()?==?1;?//?New?in?Android?12 ????????ArrayList?sensorPixelModesUsed?=?new?ArrayList ();?//?New?in?Android?12 ????????source.readList(sensorPixelModesUsed,?Integer.class.getClassLoader());?//?New?in?Android?12 ????????//?... ????} ????public?static?final?@android.annotation.NonNull?Parcelable.Creator ?CREATOR?= ????????????new?Parcelable.Creator ()?{ ????????@Override ????????public?OutputConfiguration?createFromParcel(Parcel?source)?{ ????????????try?{ ????????????????OutputConfiguration?outputConfiguration?=?new?OutputConfiguration(source); ????????????????return?outputConfiguration; ????????????}?catch?(Exception?e)?{ ????????????????Log.e(TAG,?"Exception?creating?OutputConfiguration?from?parcel",?e); ????????????????return?null; ????????????} ????????} ????}; } 其中在?createFromParcel?中對構造函數進行了異常捕捉,但是對于異常僅僅是打印并返回,這導致 Parcel 數據殘留從而影響后續的數據解析。
該漏洞本身并不復雜,是個很常見的 Parcel 序列化/反序列化不匹配問題,關鍵是這個漏洞如何利用?由于漏洞的本質是序列化數據殘留,因此我們可以主要關注一些 AIDL 調用場景的參數,這樣解析下一個參數時其內容也就會被我們所污染的數據控制。其中一個漏洞觸發鏈路就是 sendBroadcast。
Android 廣播
關于 Android 廣播的發送和接收流程應該已經有很多其他博客介紹過了,這里將其簡化為下面的階段,假設廣播由應用 A 發送給應用 B;
1.?A 調用 sendBroadcast,這實際上是個 AIDL 接口,接收方即實際實現為 system_server;
2.?system_server 接收到 Intent 后進行一系列處理,判斷合法的接收方,然后調用?IApplicationThread.scheduleReceiver?發送廣播數據;這同樣是一個 AIDL 接口,由各個應用在啟動之初通過?IActivityManager.attachApplication()?傳遞給 system_server;因此這個調用實際上會進入到應用 B 中;
3.?B 觸發?ActivityThread.handleMessage,進而調用?handleReceiver;
4.?handleReceiver 中通過傳來的數據去實例化應用上下文,獲取對應的 receiver,最終調用開發者注冊的 onReceive 回調;
第 2 步中 scheduleReceiver 的定義如下:
public?final?void?scheduleReceiver(Intent?intent,?ActivityInfo?info,
????CompatibilityInfo?compatInfo,?int?resultCode,?String?data,?Bundle?extras, ????boolean?sync,?int?sendingUser,?int?processState);其中第一個 intent 是 A 應用指定的 sendBroadcast 的參數,第二個參數則是 system_server 傳遞給應用 B 的。漏洞利用思路就是通過 intent 參數的反序列化數據殘留,間接地修改 info 參數,因為應用 B 會使用 ActivityInfo 中的數據去實例化代碼,具體來說就是:
private?void?handleReceiver(ReceiverData?data)?{ ????String?component?=?data.intent.getComponent().getClassName(); ????LoadedApk?packageInfo?=?getPackageInfoNoCheck( ????????????data.info.applicationInfo,?data.compatInfo); ????app?=?packageInfo.makeApplicationInner(false,?mInstrumentation); ????//?... }應用會通過傳入的 ActivityInfo 構造 LoadedApk,其中會使用內部的?zipPaths?創建 ClassLoader,攻擊者如果可以控制這個字段,就能實現針對應用 B 的任意代碼執行。
要實現這個目的,還有億點細節需要解決,首先我們先來看一個 Java 語言的特性。
Java 類型擦除
Java 類型擦除也稱為?Type Erasure[11],主要產生在 Java 的泛型代碼中。Java 虛擬機中并沒有泛型類型的概念,因此在編譯時實際類型會被替換為其限制對象或者原始對象?Object?類型,這個過程就叫做類型擦除。
例如,下述代碼:
public?static?
?void?printArray(E[]?array)?{ ????for?(E?element?:?array)?{ ????????System.out.printf("%s?",?element); ????} }會在編譯時變成:
public?static?void?printArray(Object[]?array)?{ ????for?(Object?element?:?array)?{ ????????System.out.printf("%s?",?element); ????} }即模版類型 E 變成了原始類型 Object。對于有限制類型的模版,比如:
public?static?>?void?printArray(E[]?array)?{ ????for?(E?element?:?array)?{ ????????System.out.printf("%s?",?element); ????} } 會在編譯時替換成其限制類型:
public?static?void?printArray(Comparable[]?array)?{ ????for?(Comparable?element?:?array)?{ ????????System.out.printf("%s?",?element); ????} }那么,這個特性對于漏洞的利用有什么幫助呢?回顧?OutputConfiguration?類,其中有個序列化的字段?sensorPixelModesUsed?是?ArrayList
?類型,在反序列化該字段時,使用的是?Parcel.readList?方法,其調用鏈路是: ??Parcel.readList
??Parcel.readListInternal
??Parcel.readValue
最終是使用 readValue 讀取 List 中每個元素的值。因此實際上可以讀出任何 readValue 支持的類型,比如 Parcelable、IBinder 等,并不局限于 Integer。
僅僅進行反序列化并不會出現任何問題,只不過在使用具體的元素時,如果我們實際讀取的類型無法轉換為整數,就會出現?ClassCastException?異常。在這個漏洞場景中,我們并不會使用這個數組的元素,因此我們可以指定任意的序列化類。又由于我們需要使目標類在序列化/反序列化過程產生不匹配,那么就需要找到一個類,使得該類可以在 system_server 中成功反序列化,但是在應用 B 中出現異常,比如?ClassNotFoundException。
原作者的利用是使用了?PackageManagerException,這是一個?Serializable?類而不是?Parcelable,因為?readSerializable/ObjectInputStream 不需要指定 ClassLoader[12]。當然肯定還存在其他可用的類,感興趣的可以自行發掘。
實際利用中由于讀取使用了帶 classLoader 的 readList,因此到 ObjectInputStream 時?latestUserDefinedLoader?會被影響,解決方案是將 Serializable 再套一層 Parcelable,使用不帶 ClassLoader 的 readList 去進行恢復。這里選用的是?WindowContainerTransaction?類。
其他
上面只介紹了漏洞利用的大致流程,完整的利用還有一些細節需要注意,比如:
1.?如何將任意 Parcelable 放到 Intent 中;
2.?精細的內存布局;
對于問題 1,使用 putExtras(Bundle) 并不可行,因為在 Intent 中 Bundle 會作為一個整體進行拷貝,因此 Bundle 中的反序列化錯誤并不會影響 Intent 本身。作者是用了一個在 Android 12 中加入的 Intent.ClipData 字段去實現的,不過目前已經修復了就不再展開了。
對于問題 2,有幾個關鍵點需要注意。一是在觸發異常后,并沒有直接跳到第二個參數,而是會繼續上級(解析了一半的)容器處理,因此后續 payload 需要維持這部分數據的棧平衡;另外一個問題是在 Andorid 中新增了許多隱藏 API 的限制,使得我們無法通過調用這些方法去構造 ActivityInfo 等數據,這個解決方法是參考源碼直接通過 Parcel 去寫入數據,只是過程繁瑣了一點,當然也可以用其他方式去繞過 Hidden API 的限制[13]。
漏洞修復
這個漏洞本身對終端用戶的影響不是很大,畢竟只在 Android 12 Preview 版本中就修復了。但通過這個漏洞,Google 引入了許多修復和緩釋方案,直接影響了后續的漏洞挖掘和利用思路。
首先針對漏洞本身,修復方案為:
1.?對上述類去除隱式的異常處理,修復讀寫不一致的問題;
2.?使用 readIntArray 而不是 readList/readValue 去讀取數據,消滅類型擦除的副作用;
3.?防止 ClipData.mActivityInfo 寫入 Parcel,除非顯式指定。這消除了向 Intent 寫入任意 Parcelable 的一個攻擊鏈路;
另外,在 Andorid 13 中,引入了更強的反序列化緩釋方案:
1.?新增了一個 readListInternal 方法的重載,增加額外的?Class?參數,顯式指定讀取列表的元素的類型,并且將原來的方法標記為?@Deprecated;
2.?新增了?Parcel.enforceNoDataAvail[14]?方法,用于確保反序列化結束后,Parcel 中不再存在多余的數據;回想上一節中 Bundle 風水的利用,實際上第三個元素在第二次反序列化中是多出來的,因此這個修改會導致上述 Bundle 風水的失?。划斎灰灿幸恍├@過的手法,比如通過更復雜的風水使得第二次反序列化能夠到 Parcel 的末尾即可;
3.?即上文說過的 LazyBundle patch,在 LazyBundle 實現中,Parcelable、List 等類型會在序列化數據的元素開頭單獨存儲長度信息。不過,這個 patch 并不會影響非 Bundle 造成的反序列化漏洞,比如這個漏洞。
LeakValue
時間來到 2022 年,Google 推出了 Android 13,在其中正式啟用了 LazyBundle 的 patch。我們在前面 Bundle 風水中已經簡要介紹過大致原理,這里再深入去分析其實現。
深入 LazyValue
由于 LazyValue 是在使用時才進行反序列化的,因此在讀取值時,需要預先知道它在 Parcel 中所占的數據區間,讀取后還需要修改 Parcel 結構中對應的偏移。這也是為什么 LazyValue 需要在序列化數據中寫入其數據長度的原因,因為對于這類數據(如 Parcelable),無法僅通過類型得知其數據長度。
讀取 LazyValue 的代碼實現如下所示:
public?Object?readLazyValue(@Nullable?ClassLoader?loader)?{ ????int?start?=?dataPosition(); ????int?type?=?readInt(); ????if?(isLengthPrefixed(type))?{ ????????int?objectLength?=?readInt(); ????????if?(objectLength?0)?{ ????????????return?null; ????????} ????????int?end?=?MathUtils.addOrThrow(dataPosition(),?objectLength); ????????int?valueLength?=?end?-?start; ????????setDataPosition(end); ????????return?new?LazyValue(this,?start,?valueLength,?type,?loader); ????}?else?{ ????????return?readValue(type,?loader,?/*?clazz?*/?null); ????} }這里面有幾個關鍵點。首先,LazyValue 中存儲的是 Parcel 的引用,以及其在 Parcel 中所占的數據區間,使用其內部屬性 mPosition、mLength 表示,mPostion 對應上述代碼中的?start,mLength 對應?valueLength。其中 valueLength 是添加到序列化數據頭部的長度字段,包括長度和類型所占的空間。
其次,在 Parcel 中構造 LazyValue 之后,會將 dataPostion 設置到對象對應序列化數據的尾部。在需要實際數據時,會調用?LazyValue.apply?方法進行真正的反序列化,如下所示:
@Override public?Object?apply(@Nullable?Class>?clazz,?@Nullable?Class>[]?itemTypes)?{ ????Parcel?source?=?mSource; ????if?(source?!=?null)?{ ????????synchronized?(source)?{ ????????????//?Check?mSource?!=?null?guarantees?callers?won't?ever?see?different?objects. ????????????if?(mSource?!=?null)?{ ????????????????int?restore?=?source.dataPosition(); ????????????????try?{ ????????????????????source.setDataPosition(mPosition); ????????????????????mObject?=?source.readValue(mLoader,?clazz,?itemTypes); ????????????????}?finally?{ ????????????????????source.setDataPosition(restore); ????????????????} ????????????????mSource?=?null; ????????????} ????????} ????} ????return?mObject; }可以看到在反序列化時候會將原始 Parcel 的 dataPostion 保存,并設置指向到 LazyValue 中的位置,然后調用 readValue 去進行反序列化,完成后再次恢復 Parcel 調用前的 dataPostion。
這可能會涉及到一些安全問題,比如多線程之間的條件競爭,在修改 dataPostion 之后被其他線程使用,當然這通過?synchronized?同步塊防御住了。另一個問題是,在 LazyValue 未使用之前,其所對應的 Parcel 是不能被釋放的,否則就會出現類似 UAF 的內存問題??紤]到對于 Parcel 的內存分配策略而言,大多數是使用手工管理的 obtain/recycle 方式,這個問題是有可能存在的。
Parcel 內存管理
由于 Parcel 本身是為了頻繁的 IPC 傳輸而設計的,因此對于其分配和釋放通常使用手工管理的方式,以避免 Java 堆分配或者 GC 帶來的性能損耗。在閱讀系統源碼或者 AIDL 生成的模版代碼時都能發現,Parcel 使用 obtain 進行分配,使用 recycle 進行釋放。
分配的代碼實現如下:
public?static?Parcel?obtain()?{ ????Parcel?res?=?null; ????synchronized?(sPoolSync)?{ ????????if?(sOwnedPool?!=?null)?{ ????????????res?=?sOwnedPool; ????????????sOwnedPool?=?res.mPoolNext; ????????????res.mPoolNext?=?null; ????????????sOwnedPoolSize--; ????????} ????} ????if?(res?==?null)?{ ????????res?=?new?Parcel(0); ????}?else?{ ????????res.mRecycled?=?false; ????????res.mReadWriteHelper?=?ReadWriteHelper.DEFAULT; ????} ????return?res; }其中?sOwnedPool?是靜態屬性,mPoolNext?是成員屬性,二者都是 Parcel 類型。因此內存池中的 Parcel 可以看做是一個表頭為 sOwnedPool 的單鏈表結構。obtain 本質上是從鏈表中取出表頭的數據。
對于 IPC 接收到的 Parcel 數據分配方式略有不同,因為這些 Parcel 在 C++ 層由系統創建,因此使用不同的鏈表,表頭為?sHolderPool?靜態屬性。這些 Parcel 通過構造函數?Parcel(long nativePtr)?去構建,生命周期由系統管理(mOwnsNativeParcelObject?為?false),因此需要區分開來。這類 Parcel 的分配通過重載的?obtain(long)?方法去創建,與上述實現大同小異。
在釋放過程會將這兩種情況區分開來:
public?final?void?recycle()?{ ????if?(mRecycled)?{ ????????Log.wtf(TAG,?"Recycle?called?on?unowned?Parcel.?(recycle?twice?)?Here:?" ????????????????+?Log.getStackTraceString(new?Throwable()) ????????????????+?"?Original?recycle?call?(if?DEBUG_RECYCLE):?",?mStack); ????????return; ????} ????mRecycled?=?true; ????//?We?try?to?reset?the?entire?object?here,?but?in?order?to?be ????//?able?to?print?a?stack?when?a?Parcel?is?recycled?twice,?that ????//?is?cleared?in?obtain?instead. ????mClassCookies?=?null; ????freeBuffer(); ????if?(mOwnsNativeParcelObject)?{ ????????synchronized?(sPoolSync)?{ ????????????if?(sOwnedPoolSize?釋放操作相當于單鏈表的插入,即將釋放的 Parcel 放入 sOwnPool/sHolderPool 鏈表的頭部。從上述代碼可以看出,Parcel 的分配和釋放過程是后進先出(LIFO)的,即 Parcel obtain 會分配出最近一次釋放的對象。
Parcel UAF
通過 LazyValue 的實現以及 Parcel 內存管理的策略,我們似乎可以找到一個攻擊場景: 在 Parcel 讀取 LazyValue 之后,將 Parcel 進行釋放,而后再讀取對應的 LazyValue,此時如果 Parcel 被分配并填充了敏感數據,那么我們的 LazyValue 就可以讀取出這些敏感內容造成數據泄露。如果泄露的數據來自其他進程,且數據中包含特權的 IBinder 等結構,那么還可能造成提權或者 RCE 的危害!
為此,我們首先需要找到一個 recycle 后再次使用 LazyValue 的場景。接下來,就是漏洞上場的時間了,出現上述問題的漏洞編號是?CVE-2022-20452[15],其 patch 代碼為:
diff?--git?a/core/java/android/os/BaseBundle.java?b/core/java/android/os/BaseBundle.java index?0418a4b..b599028?100644 ---?a/core/java/android/os/BaseBundle.java +++?b/core/java/android/os/BaseBundle.java @@?-438,8?+438,11?@@ ?????????????map.ensureCapacity(count); ?????????} ?????????try?{ +????????????//?recycleParcel?being?false?implies?that?we?do?not?own?the?parcel.?In?this?case,?do +????????????//?not?use?lazy?values?to?be?safe,?as?the?parcel?could?be?recycled?outside?of?our +????????????//?control. ?????????????recycleParcel?&=?parcelledData.readArrayMap(map,?count,?!parcelledByNative, -????????????????????/*?lazy?*/?true,?mClassLoader); +????????????????????/*?lazy?*/?recycleParcel,?mClassLoader); ?????????}?catch?(BadParcelableException?e)?{ ?????????????if?(sShouldDefuse)?{ ?????????????????Log.w(TAG,?"Failed?to?parse?Bundle,?but?defusing?quietly",?e); @@?-1845,7?+1848,6?@@ ?????????????//?bundle?immediately;?neither?of?which?is?obvious. ?????????????synchronized?(this)?{ ?????????????????initializeFromParcelLocked(parcel,?/*recycleParcel=*/?false,?isNativeBundle); -????????????????unparcel(/*?itemwise?*/?true); ?????????????} ?????????????return; ?????????}問題還是出現在 Bundle 反序列化的過程中,之前說過 Bundle 通過 readFromParcelInner 去進行反序列化,其實現如下:
private?void?readFromParcelInner(Parcel?parcel,?int?length)?{ ????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)); ????} ????if?(parcel.hasReadWriteHelper())?{ ????????//?If?the?parcel?has?a?read-write?helper,?it's?better?to?deserialize?immediately ????????//?otherwise?the?helper?would?have?to?either?maintain?valid?state?long?after?the?bundle ????????//?had?been?constructed?with?parcel?or?to?make?sure?they?trigger?deserialization?of?the ????????//?bundle?immediately;?neither?of?which?is?obvious. ????????synchronized?(this)?{ ????????????initializeFromParcelLocked(parcel,?/*recycleParcel=*/?false,?isNativeBundle); ????????????unparcel(/*?itemwise?*/?true); ????????} ????????return; ????} ????//?直接使用?Parcel.appendFrom?拷貝原?Parcel?的數據 ????Parcel?p?=?Parcel.obtain(); ????p.appendFrom(parcel,?offset,?length); ????mParcelledData?=?p;注意 if 分支外部,這是大部分 Bundle 反序列化的流程,即通過新分配一個 parcel 去拷貝原始數據并保存到 mParcelledData 中。因為開發者知道 parcel 參數的生命周期不由自身控制。
而在 if 內部,調用了 initializeFromParcelLocked,其中會使用 readArrayMap 對 parcel 數據進行反序列化,這其中是帶有 LazyValue 的。由于開發者知道 parcel 生命周期不可控,因此在后續調用了?unparcel(true)?去強制對每個 LazyValue 進行反序列化以去除對 parcel 數據的依賴。
如果 unparcel 內部某些 LazyValue 能夠被攻擊者繞過解析,那么這里就存在一個 UAF 漏洞,后續讀取 LazyValue 時就能泄露出復用的 Parcel 中的數據。當然,要觸發這個漏洞需要有幾個前提:
1.?hasReadWriteHelper 返回 true;
2.?unparcel 能被繞過;
對于問題 1,我們可以在 AOSP 代碼中搜索符合條件的 Parcel 類,比如?RemoteViews;
對于問題 2,我們可以嘗試使其中某個 LazyValue 解析失敗,比如 Parcelable 的類不存在時,代碼會拋出 ClassNotFoundException,被捕捉后再次拋出 BadParcelableException。有趣的是這個異常會在 getValueAt 中被捕捉,如下所示:
@Nullable final??T?getValueAt(int?i,?@Nullable?Class ?clazz,?@Nullable?Class>...?itemTypes)?{ ????Object?object?=?mMap.valueAt(i); ????if?(object?instanceof?BiFunction,??,??>)?{ ????????try?{ ????????????object?=?((BiFunction ,?Class>[],??>)?object).apply(clazz,?itemTypes); ????????}?catch?(BadParcelableException?e)?{ ????????????if?(sShouldDefuse)?{ ????????????????Log.w(TAG,?"Failed?to?parse?item?"?+?mMap.keyAt(i)?+?",?returning?null.",?e); ????????????????return?null; ????????????}?else?{ ????????????????throw?e; ????????????} ????????} ????????mMap.setValueAt(i,?object); ????} ????return?(clazz?!=?null)???clazz.cast(object)?:?(T)?object; } 如果?sShouldDefuse?為 true,那么異常不會被再次拋出,而只是打印異常并返回 null。對于?system_server?而言該條件是滿足的,因為其目的就是為了防止系統重要服務頻繁崩潰。因此,我們可以將 Bundle 的某個值設置為另外一個容器,比如 List,然后在容器中存儲一個不存在的 Parcelable,那么 List 中后續的 LazyValue 將不會被遞歸解析到,我們也就獲得了一個 UAF 對象。
漏洞利用
該漏洞的利用過程比較曲折,由于漏洞已經修復,因此筆者對于復現的興趣缺缺。此外原作者也公開了詳細利用思路和漏洞利用代碼,感興趣的可以自行參考:
??Exploit for CVE-2022-20452, privilege escalation on Android from installed app to system app (or another app) via LazyValue using Parcel after recycle()[16]
這里提煉一下其中值得學習的點:
1.?UAF 漏洞的核心利用目標是獲取目標應用的?IApplicationThread?句柄,這通常是應用啟動初期使用?attachApplication()?傳遞給?system_server?的,主要用于讓后者給應用發送四大組件的生命周期回調。通過濫用這個 Binder 句柄可以實現 RCE,參考前面的 CVE-2021-0928 (scheduleReceiver);
2.?為了能從?system_server?中泄露任意應用的 IApplicationThread 句柄,需要兩個條件。首先是有一個接口可以發送 Parcelable 數據并將其取回,AppWidgetHost、Notification.contentView、Mediasession 都可以滿足,作者選用的是 MediaSession.get/setQueue 接口;
3.?其次,要使得 UAF 對象能被包含 IApplicationThread 的 Parcel 復用,需要準確的時機。但是 attachApplication 只在應用啟動時一個很小的時間窗口中被調用,因此作者尋找其中可能存在的鎖去拓展這個窗口;
4.?ParceledListSlice 是一個在 IPC 間傳輸的特殊數據結構,在其數據量小時可以同步傳輸,而對于大量的數據,會將其轉換為 binder 發送給對方然后進行異步傳輸;
5.?ActivityManager.moveTaskToFront()?調用時可以提供 ActivityOptions 參數,以 Bundle 形式進行封裝,并在 system_server 端進行反序列化。因此攻擊者可以在 Bundle 中加入 ParceledListSlice 類型的數據,從而在反序列化時回調到自身進行阻塞。該函數調用時候持有?mGlobalLock?鎖,因此可以阻塞 attachApplication 的執行,從而更好地構造 UAF 風水;
6.?由于調用了 moveTaskToFront,種種原因導致無法使用 startActivity 來啟動目標應用,因此作者使用 ContentProvider 來啟動目標應用。
7.?在我們的 UAF LazyValue Parcel 被成功占位后,對應的 IApplicationThread 實際上在比較靠前的位置,但我們的 LazyValue 所在位置比較靠后因此無法讀取到對應 binder。解決方案是可以找其他占位對象,不過作者這里利用了一個新的漏洞?CVE-2022-20474[17],即 readLazyValue 方法中 objectLenth 只檢查了證書溢出,卻沒檢查負數的情況,因此設置負數的 prefixLength 實際上可以讀取 Parcel 前方的數據,從而繞過該限制。
其他還有億點細節,就需要動手復現才能真正理解了。理論上該漏洞可以影響安全補丁在 2022-11 之前的 Android 13 設備。 攻擊的目標應用可以是任意應用,因此可以選擇?Settings.apk,其 UID=system,攻擊成功后就相當于獲取到了 system 權限。這在 Goole VRP 中應該是 10 萬刀樂的標準。
總結
本文算是筆者學習 Android 反序列化漏洞的一個筆記,按照時間線記錄了幾個公開的經典反序列化漏洞,并且介紹相關的修復策略以及繞過方法。從中我們可以看到,Parcel 作為輕量級的序列化方案,許多操作都需要手動管理,這導致了許多讀寫不匹配的問題,雖然后續引進了 LazyBundle 優化,但又引發了新的內存管理問題,使得傳統二進制的 UAF 甚至 Double Free 類漏洞可以在 Java 世界重現。
漏洞本身只是一個引子,指導我們未來的研究方向。從這些漏洞中,我們也可以發現 Security By Default 的重要性,比如序列化 Java 容器導致的類型擦除問題和 Bundle 的序列化問題。在后期雖然嘗試進行了緩釋,但由于歷史負擔,很多不安全的設計只能縫縫補補延續下去,這也是后續挖掘和審計其他產品漏洞的一個重要著手點。
??ReparcelBug[18]
??ReparcelBug2[19]
??LeakValue[20]
??Blackhat EU22 Android parcels: the bad, the good and the better - Introducing Android’s Safer Parcel[21]
引用鏈接
[1]?Parcel:?https://developer.android.com/guide/components/activities/parcelables-and-bundles
[2]?Android 進程間通信與逆向分析:?https://evilpan.com/2020/07/11/android-ipc-tips/
[3]?GetStringRegion:?https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html
[4]?Android 8.0 中的版本:?http://androidxref.com/8.0.0_r4/xref/frameworks/base/core/java/android/os/Parcel.java#writeValue
[5]?patch:?https://android.googlesource.com/platform/frameworks/base/+/5bab9da%5E%21/
[6]?ReparcelBug - PoC for CVE-2017-0806:?https://github.com/michalbednarski/ReparcelBug
[7]?CVE-2017-0806 原理分析:?https://github.com/michalbednarski/IntentsLab/issues/2#issuecomment-344365482
[8]?Bundle風水——Android序列化與反序列化不匹配漏洞詳解:?https://xz.aliyun.com/t/2364
[9]?LazyBundle(9ca6a5):?https://cs.android.com/android/_/android/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee
[10]?CVE-2021-0928, writeToParcel/createFromParcel serialization mismatch in android.hardware.camera2.params.OutputConfiguration:?https://github.com/michalbednarski/ReparcelBug2
[11]?Type Erasure:?https://www.baeldung.com/java-type-erasure
[12]?readSerializable/ObjectInputStream 不需要指定 ClassLoader:?https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/java/io/ObjectInputStream.java;drc=9b25969d7bf3e31c5b7fec5b34c37a304b6a7fa7;l=678?q=libcore%2Fojluni%2Fsrc%2Fmain%2Fjava%2Fjava%2Fio%2FObjectInputStream.java&ss=android%2Fplatform%2Fsuperproject
[13]?繞過 Hidden API 的限制:?https://www.xda-developers.com/bypass-hidden-apis/
[14]?Parcel.enforceNoDataAvail:?https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=961;drc=9b25969d7bf3e31c5b7fec5b34c37a304b6a7fa7?q=enforceNoDataAvail&sq=&ss=android%2Fplatform%2Fsuperproject
[15]?CVE-2022-20452:?https://android.googlesource.com/platform/frameworks/base/+/1aae720772a86e2db682d2e9ed77937334e475f3%5E%21/
[16]?Exploit for CVE-2022-20452, privilege escalation on Android from installed app to system app (or another app) via LazyValue using Parcel after recycle():?https://github.com/michalbednarski/LeakValue
[17]?CVE-2022-20474:?https://android.googlesource.com/platform/frameworks/base/+/569c3023f839bca077cd3cccef0a3bef9c31af63%5E%21/
[18]?ReparcelBug:?https://github.com/michalbednarski/ReparcelBug
[19]?ReparcelBug2:?https://github.com/michalbednarski/ReparcelBug2
[20]?LeakValue:?https://github.com/michalbednarski/LeakValue
[21]?Blackhat EU22 Android parcels: the bad, the good and the better - Introducing Android’s Safer Parcel:?https://www.blackhat.com/eu-22/briefings/schedule/index.html#android-parcels-the-bad-the-good-and-the-better---introducing-androids-safer-parcel-28404編輯:黃飛
?
評論
查看更多