序列化和反序列化是指將內(nèi)存數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為字節(jié)流,通過網(wǎng)絡(luò)傳輸或者保存到磁盤,然后再將字節(jié)流恢復(fù)為內(nèi)存對(duì)象的過程。在 Web 安全領(lǐng)域,出現(xiàn)過很多反序列化漏洞,比如 PHP 反序列化、Java 反序列化等。由于在反序列化的過程中觸發(fā)了非預(yù)期的程序邏輯,從而被攻擊者用精心構(gòu)造的字節(jié)流觸發(fā)并利用漏洞從而最終實(shí)現(xiàn)任意代碼執(zhí)行等目的。
Android 中除了傳統(tǒng)的 Java 序列化機(jī)制,還有一個(gè)特殊的序列化方法,即?Parcel[1]。根據(jù)官方文檔的介紹,Parcelable 和 Bundle 對(duì)象主要的作用是用于跨進(jìn)程邊界的數(shù)據(jù)傳輸(IPC/Binder),但 Parcel 并不是一個(gè)通用的序列化方法,因此不建議開發(fā)者將 Parcel 數(shù)據(jù)保存到磁盤或者通過網(wǎng)絡(luò)傳輸。
作為 IPC 傳輸?shù)臄?shù)據(jù)結(jié)構(gòu),Parcel 的設(shè)計(jì)初衷是輕量和高效,因此缺乏完善的安全校驗(yàn)。這就引發(fā)了歷史上出現(xiàn)過多次的 Android 反序列化漏洞,本文就按照時(shí)間線對(duì)其進(jìn)行簡單的分析和梳理。
注: 本文中所展現(xiàn)的 AOSP 示例代碼,如無特殊說明則都來自文章發(fā)表時(shí)的 master 分支。
Parcel 101
在介紹漏洞之前,我們還是按照慣例先來了解下基礎(chǔ)知識(shí)。對(duì)于有過 Android 開發(fā)或者逆向分析經(jīng)驗(yàn)的同學(xué)應(yīng)該對(duì) Parcel 都不陌生,但通常也很少直接使用該類去序列化/反序列化數(shù)據(jù)然后進(jìn)行 IPC 通信,而是通過 AIDL 等方法去自動(dòng)生成模版,然后集成實(shí)現(xiàn)對(duì)應(yīng)接口。
AIDL
關(guān)于 AIDL 開發(fā)的示例可以參考?Android 進(jìn)程間通信與逆向分析[2]?一文,簡單來說,假設(shè)有以下 AIDL 文件:
package?com.evilpan; interface?IFooService?{ ????parcelable?Person?{ ????????String?name; ????????int?age; ????????boolean?gender; ????} ????String?foo(int?a,?String?b,?in?Person?c); }
那么生成的(Java)模版大致結(jié)構(gòu)如下:
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 實(shí)現(xiàn),即服務(wù)端代碼通過繼承至該類并實(shí)現(xiàn)其?foo?方法;而?Proxy?則是客戶端的的輔助類,客戶端可以通過調(diào)用?Proxy.foo?方法間接地調(diào)用服務(wù)端的對(duì)應(yīng)代碼。數(shù)據(jù)傳輸?shù)倪^程通過?transact?方法實(shí)現(xiàn),其底層是 Android 的 Binder IPC;而數(shù)據(jù)的封裝過程則通過 Parcel 實(shí)現(xiàn)。
可以看到上面模版代碼中客戶端分別調(diào)用了?writeInterfaceToken、writeInt、writeString?和?writeTypedObject?來填充傳輸?shù)?_data,而 Stub 類的 onTransact 中以同樣的順序分別調(diào)用了?enforceInterface、readInt、readString、readTypedObject?來獲取?_data?中的數(shù)據(jù)。
Parcelable
在上面的 AIDL 中,我們還定義了一個(gè)數(shù)據(jù)結(jié)構(gòu) Person,該結(jié)構(gòu)同樣會(huì)由 AIDL 生成對(duì)應(yīng)的模版類:
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; ??} }
其中關(guān)鍵的是 writeToParcel 和?CREATOR.createFromParcel?方法,分別填充了該自定義結(jié)構(gòu)序列化和反序列化的實(shí)現(xiàn),當(dāng)然我們也可以自己繼承?Parcelable?去實(shí)現(xiàn)自己的可序列化數(shù)據(jù)結(jié)構(gòu)。
內(nèi)存布局
從接口上看,Parcel 可以支持按照一定順序?qū)懭牒妥x取 int、long 等原子數(shù)據(jù),也支持 String、IBinder、和 FileDescriptor 這些復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。為了理解后文介紹的漏洞,還需要了解在二進(jìn)制層面這些數(shù)據(jù)的存儲(chǔ)方式。
Parcel 的代碼接口實(shí)現(xiàn)在?android/os/Parcel.java?中,但大部分方法最終都會(huì)調(diào)用到其中的 native 方法,其 JNI 定義在?frameworks/base/core/jni/android_os_Parcel.cpp?文件里,最終的實(shí)現(xiàn)則是在?frameworks/native/libs/binder/Parcel.cpp?中。
以?Parcel.writeInt?為例,其 Java 實(shí)現(xiàn)很簡單,直接轉(zhuǎn)到 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 實(shí)現(xiàn)則是先將 nativePtr 轉(zhuǎn)換為 Parcel 指針,而后直接調(diào)用?writeInt32?方法:
static?int?android_os_Parcel_writeInt(jlong?nativePtr,?jint?val)?{ ????Parcel*?parcel?=?reinterpret_cast(nativePtr); ????return?(parcel?!=?NULL)???parcel->writeInt32(val)?:?OK; }
接下來就是最終實(shí)際的實(shí)現(xiàn)了:
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?是個(gè)模版函數(shù),用于寫入基礎(chǔ)的 C++ 數(shù)據(jù)類型,即 int、float、double 等,也可以寫入指針數(shù)據(jù)。實(shí)現(xiàn)也相對(duì)簡單,這里面就涉及到了 Parcel 內(nèi)部的幾個(gè)重要數(shù)據(jù)結(jié)構(gòu):
??mData: 序列化數(shù)據(jù)內(nèi)存緩沖區(qū)的內(nèi)存起始地址(指針);
??mDataPos: 序列化數(shù)據(jù)當(dāng)前解析到的相對(duì)位置;
??mDataCapacity: 緩沖區(qū)的總大小;
還有一個(gè)字段 mDataSize 表示當(dāng)前序列化數(shù)據(jù)的大小,其實(shí)這個(gè)字段基本上和 mDataPos 的值是一致的,二者都在 finishWrite 函數(shù)中進(jìn)行更新:
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; }
而如果當(dāng)前緩沖區(qū)的內(nèi)存不足,則會(huì)使用 growData 方法進(jìn)行更新:
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?方法的實(shí)現(xiàn)比較復(fù)雜,因?yàn)槠渲羞€支持傳入小于 mDataSize 的參數(shù)去縮小 Parcel 內(nèi)存,不過在我們這里的上下文中僅用于增長內(nèi)存,因此實(shí)際上最終只是通過?realloc?或者?malloc?去分配更多的內(nèi)存:
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; ????} }
這部分目前只需要了解即可,我們關(guān)注的還是前面寫入數(shù)據(jù)的邏輯,回顧一下是在 writeAligned 方法中,直接通過?memcpy?去寫入的數(shù)據(jù),因此對(duì)于基礎(chǔ)數(shù)字類型是沒有額外開銷的,且序列化的字節(jié)序就是當(dāng)前機(jī)器的字節(jié)序。這也是為什么 Parcel 只適合在同一設(shè)備中實(shí)現(xiàn) IPC,如果對(duì)于不同設(shè)備中可能會(huì)出現(xiàn)字節(jié)序的問題。
String
說完了 Int 我們接著看常用的 String 類型,JNI 的定義和實(shí)現(xiàn)如下:
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?返回的是待寫入的內(nèi)存地址,直接用了?JNIEnv::GetStringRegion?去進(jìn)行寫入。如果 Java 傳入的字符串是?null,則使用?writeString16(nullptr)?去寫入,其內(nèi)部實(shí)現(xiàn)是寫入特殊的整數(shù)?-1:
GetStringRegion[3]?返回的是 Unicode 字符,因此每個(gè)字符占 2 字節(jié);
status_t?Parcel::writeString16(const?char16_t*?str,?size_t?len) { ????if?(str?==?nullptr)?return?writeInt32(-1); ????//?... }
因此對(duì)于字符串結(jié)構(gòu),Parcel 的序列化也是無開銷順序?qū)懭氲摹?/p>
Array
數(shù)組也是個(gè)常用的數(shù)據(jù)類型,但不同的數(shù)組傳輸格式有所不同。對(duì)于 char/int/long 等原始類型而言,傳輸數(shù)組實(shí)際上就是逐個(gè)寫入每個(gè)元素,并且在前面寫入數(shù)組的大小:
public?final?void?writeIntArray(@Nullable?int[]?val)?{ ????if?(val?!=?null)?{ ????????int?N?=?val.length; ????????writeInt(N); ????????for?(int?i=0;?i但是 wirteByteArray 有所優(yōu)化:
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
對(duì)于?Parcelable?類型的數(shù)據(jù),使用 writeParcelable 方法進(jìn)行寫入:
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); }其中需要注意的是在調(diào)用?Parcelable.writeToParcel?之前,會(huì)先獲取 Parcelable 實(shí)際的類名,并以字符串的方式寫入。
FileDescriptor
Andorid IPC 的一個(gè)特點(diǎn)是可以支持傳輸文件句柄,其 JNI 實(shí)現(xiàn)如下:
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?對(duì)象,這里的實(shí)現(xiàn)就是獲取?FileDescriptor.fd,即?open(2)?返回的,int 格式的文件描述符,進(jìn)行?fcntl(oldFd, F_DUPFD_CLOEXEC, 0)?復(fù)制后進(jìn)行寫入。
Binder 的系統(tǒng)調(diào)用本身就支持傳輸文件類型的數(shù)據(jù),因此這里 Parcel 的實(shí)現(xiàn)只需要做好上層的封裝:
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 中寫入數(shù)據(jù)除了更新上面提到的 mData 等字段,還需要更新?kernelFields,如?mObjects?字段,作為內(nèi)核參數(shù)的記錄。
Binder
在 Android IPC 中一個(gè)重要的參數(shù)類型就是回調(diào),即客戶端發(fā)送一個(gè) IBinder 類型的對(duì)象給服務(wù)端,然后服務(wù)端可以調(diào)用其 onTransact 方法實(shí)現(xiàn)反向異步的數(shù)據(jù)傳輸。對(duì)于這種類型的數(shù)據(jù),主要通過 writeStrongBinder 方法:
public?final?void?writeStrongBinder(IBinder?val)?{ ????nativeWriteStrongBinder(mNativePtr,?val); }JNI 實(shí)現(xiàn)和傳輸文件類似:
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 對(duì)象的 Native 指針,即?IBinder.mObject,然后將其轉(zhuǎn)為 C++ 的 IBinder 指針傳入:
status_t?Parcel::writeStrongBinder(const?sp&?val) { ????return?flattenBinder(val); } 傳輸 IBinder 類型的數(shù)據(jù)同樣需要更新 mObjects 緩存。
其他
除了上面介紹的這些,Parcel 實(shí)現(xiàn)中還有許多值得關(guān)注的細(xì)節(jié),比如 writeBlob 同樣也是寫入?byte[],但對(duì)于過大的數(shù)據(jù)會(huì)選擇用共享內(nèi)存的方式去進(jìn)行傳輸。但根據(jù)上面的簡單分析,我們也能大致看出 Parcel 的一些問題,即序列化和反序列化純屬手工操作,并且在某些操作中沒有嚴(yán)格的類型檢查等,下面我們就來逐一探討。
Bundle
在 Andorid 中,Bundle 類是一個(gè)類似 HashMap 的數(shù)據(jù)結(jié)構(gòu),但其是為了在 Parcel 序列化/反序列化的使用中而高度優(yōu)化的。因此理解 Bundle 本身的序列化過程對(duì)我們理解后面的內(nèi)容也至關(guān)重要。
序列化
Bundle 的序列化過程調(diào)用鏈路如下:
??Bundle.writeToParcel
??BaseBundle.writeToParcelInner
??parcel.writeArrayMapInternal
writeToParcelInner 中主要負(fù)責(zé)寫入 Bundle 相關(guān)的頭部字段:
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 則主要實(shí)現(xiàn) Bundle 內(nèi)部字典數(shù)據(jù)的寫入:
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,然后依次寫入每個(gè) key 和 value。這里值得注意的是 wirteValue 的實(shí)現(xiàn):
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); ????????} ????} }注意,為了便于按照時(shí)間線理解歷史漏洞,這里代碼使用的是?Android 8.0 中的版本[4]。
writeValue?會(huì)根據(jù)對(duì)象類型分別寫入一個(gè)代表類型的整數(shù)以及具體的數(shù)據(jù)。所支持的類型如下所示:
//?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;可以看到其中支持大部分基礎(chǔ)類型以及 Parcelable、IBinder 等 Parcel 本身所支持的類型。
綜上所述,我們可以大致得出 Bundle 的內(nèi)存布局:
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 因?yàn)槭?String 類型,因此內(nèi)部是長度 + String16 的布局,而 value 則根據(jù)類型不同使用不同的結(jié)構(gòu)。
key:
??length Int32
??data String16
value:
??VAL_XXX Int32
??data writeInt/writeString/...
下面是在一個(gè) Bundle 序列化的示例,原始內(nèi)容為:
Bundle?A?=?Bundle() A.putInt("key1",?0x1337) A.putString("key2",?"hello") A.putLong("key33",?0x0123456789)使用 Parcel 序列化后的二進(jìn)制數(shù)據(jù)如下:
bin.png
其中有幾個(gè)需要注意的細(xì)節(jié):
??length 字段是不包括 length+magic 部分的,因此實(shí)際上序列化數(shù)據(jù)的總大小會(huì)比頭部的結(jié)果大 8 字節(jié)即 100;
??String 數(shù)據(jù)是 4 字節(jié)對(duì)齊的(如 key33);如果本身已經(jīng)對(duì)齊,也會(huì)在后面進(jìn)行特殊拓展,如(key1/key2);特殊拓展的值為?Parcel::writeInplace?中的 padMask,對(duì)于已經(jīng) 4 字節(jié)對(duì)齊的 mask 正好是 0;
存儲(chǔ)
Bundle 雖然內(nèi)部是 ArrayMap 結(jié)構(gòu),但實(shí)際存儲(chǔ)的時(shí)候還有一點(diǎn)優(yōu)化。在平時(shí)開發(fā)中細(xì)心的朋友可能會(huì)發(fā)現(xiàn),有時(shí)候在獲取遠(yuǎn)程的 Bundle 比如 CotentResolver.call 的結(jié)果時(shí)候,直接打印輸出會(huì)是?Bundle[mParcelledData.dataSize=xxx]?的結(jié)果。但是對(duì)于自己構(gòu)建的 Bundle,卻可以打印出完整的 Map 元素。
這其實(shí)可以從 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()?+?"]"; }關(guān)鍵就是這個(gè)?mParcelledData?字段,該字段定義在父類即 BaseBundle 中,為 Parcel 類型。由于 Bundle 經(jīng)常在進(jìn)程間進(jìn)行傳輸,因此設(shè)計(jì)上認(rèn)為可以不要到對(duì)端的時(shí)候馬上就反序列化出所有的 ArrayMap,而是等需要用到時(shí)再進(jìn)行解析,這也可以認(rèn)為是一種懶加載的技術(shù)。對(duì)于一些收到 Bundle 后馬上又傳輸給其他服務(wù)的場(chǎng)景,這種設(shè)計(jì)可以直接傳輸未解析的 Parcel 數(shù)據(jù)而不需要來回的序列化。
因此對(duì)于 Bundle 而言,對(duì)于一些需要獲取內(nèi)部元素的調(diào)用時(shí)才會(huì)進(jìn)行反序列,比如 size 和 isEmpty 的實(shí)現(xiàn):
public?int?size()?{ ????unparcel(); ????return?mMap.size(); } public?boolean?isEmpty()?{ ????unparcel(); ????return?mMap.isEmpty(); }值得注意的是,在 Bundle 內(nèi)部,ArrayMap(mMap) 和 Parcel(mParcelledData) 同一時(shí)間只能存在一個(gè),反序列化后會(huì)填充 mMap 把 mParcelledData 回收并置為 null。
反序列化與 Bundle 風(fēng)水
最早提交 Android 反序列化漏洞的是 @BednarTildeOne 在 2014 年提交的 ParceledListSlice 漏洞,但第一次引起公眾關(guān)注的應(yīng)該是 CVE-2017-0806。也是同樣的作者,不過給出了詳細(xì)的分析以及漏洞 POC 代碼,這才得以引起關(guān)注。
該漏洞的原理已經(jīng)有很多師傅分析過了,就不再贅述,這里直接給出一個(gè)簡化的版本。假設(shè)有這么一個(gè) Parcelable 數(shù)據(jù)結(jié)構(gòu),請(qǐng)問是否存在漏洞?漏洞如何利用?
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,二者不匹配,這可能會(huì)導(dǎo)致一些數(shù)據(jù)錯(cuò)誤,但這能算得上一個(gè)漏洞嗎?
回想 LunchAnywhere 漏洞以及對(duì)應(yīng)的?patch[5],實(shí)際上是用戶可控的 Intent 數(shù)據(jù)中帶有?KEY_INTENT?extra 導(dǎo)致的啟動(dòng)任意 Activity 的問題。修復(fù)過程是判斷用戶提供的 Intent 是否帶有該 extra,如果有則校驗(yàn)調(diào)用者的簽名。當(dāng)時(shí)的修復(fù)可以說沒什么問題,但如果配合上述的漏洞,就有可能出現(xiàn)繞過。
這里還是以測(cè)試代碼舉例,假設(shè)我們的校驗(yàn)函數(shù)如下:
@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校驗(yàn)時(shí)如果 intent 不為空則進(jìn)行簽名校驗(yàn),隨后會(huì)調(diào)用 AIDL 的客戶端方法?send,該方法將 bundle 再次序列化然后發(fā)送給服務(wù)端,而服務(wù)端的實(shí)現(xiàn)如下:
class?Server?extends?IServer.Stub?{ ????@Override ????public?void?send(Bundle?result)?{ ????????Intent?intent?=?bundle.getParcelable(KEY_INTENT); ????????if?(intent?!=?null)?{ ????????????mContext.startActivity(intent) ????????} ????} }因?yàn)榭蛻舳诉M(jìn)行了校驗(yàn),所以服務(wù)就不需要再次校驗(yàn)而是直接信任該 Bundle 并使用了。這里其實(shí)有個(gè)經(jīng)典的漏洞模式即 TOCTOU 問題,一般情況下這個(gè)邏輯是沒問題的,即 bundle 檢查完后不會(huì)被再次修改。但在我們的漏洞場(chǎng)景中,就有可能會(huì)出現(xiàn)不同的 bundle,即?Self-Changing Bundle。
自修改 Bundle
Bundle 自修改,主要還是針對(duì)上面的這種 IPC 場(chǎng)景,一端對(duì) Bundle 進(jìn)行了校驗(yàn),發(fā)現(xiàn)沒問題后使用 IPC 發(fā)送給另一端,且對(duì)方不加校驗(yàn)就直接使用。即 Bundle 在序列化+反序列化的過程中進(jìn)行了改變。
假設(shè)服務(wù)端 B 接收我們的 Bundle 數(shù)據(jù)進(jìn)行反序列化后再次序列化發(fā)送給服務(wù)端 C,而我們的 bundle 中帶有一個(gè)類型為 Vunerable 的 key,那在 B 接收到數(shù)據(jù)后再發(fā)出去的數(shù)據(jù)會(huì)如下圖右邊所示,其中本該是 Int 的字段被寫成了 Long,導(dǎo)致 C 再次反序列化時(shí)候?qū)罄m(xù)數(shù)據(jù)的解析出現(xiàn)異常:
Bundle
通過精心構(gòu)造發(fā)送給 B 的數(shù)據(jù),我們可以令 B 和 C 都能正常序列化出 Bundle 對(duì)象,甚至讓這兩個(gè) Bundle 含有不同的 key!
漏洞利用
該漏洞如何利用呢?前文中我們已知 Bundle 序列化數(shù)據(jù)的頭部后面每個(gè)元素都是 key+value 的組合,那利用思路應(yīng)該就是將多出來的 4 字節(jié)進(jìn)行類型混淆。這種場(chǎng)景類似于二進(jìn)制的緩沖區(qū)溢出,我們需要做的就是布局好數(shù)據(jù)使得同時(shí)滿足下述條件:
1.?類型混淆前后,Bundle 的 length 和 items 字段保持一致;
2.?兩次反序列化得到的對(duì)象都需要是合法對(duì)象;
3.?第二次反序列化要能夠出現(xiàn)一個(gè)前面沒有的 Bundle key;
溢出的 4 個(gè)字節(jié)會(huì)作為后面一個(gè) key 的一部分,而 key 的類型是 String,根據(jù)前面的介紹,其內(nèi)存是長度(Int32) + String16。其中 String16 是 unicode,使用 2 字節(jié)保存一個(gè)字符。因此,如果 map 元素足夠的話,這 4 個(gè)字節(jié)會(huì)形成下一個(gè) key 的長度。
在大部分情況下,溢出的部分是 Long 數(shù)據(jù)的高有效位,因此會(huì)是 0,如果此時(shí)第二次反序列化處理到這里,會(huì)認(rèn)為這有個(gè) key 長度為 0 的元素,查看讀取字符串相關(guān)的代碼,如下所示:
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,還是會(huì)讀出?(0+1)x2?即 2 字節(jié)的?x00。readInplace?中還會(huì)對(duì)齊,因此最終讀出的是 4 字節(jié)數(shù)據(jù)。因此,后續(xù)的反序列化都會(huì)在原來的基礎(chǔ)上后移 4 字節(jié)。
這樣一來利用思路就比較清晰了:
1.?構(gòu)造一個(gè)惡意 Bundle,其中包含兩個(gè)元素 A0 和 A1,其中 A0 為 Vunlerable,在 A1 中布置 payload;
2.?反序列化+序列化后,數(shù)據(jù)多出 4 字節(jié),且下個(gè)元素的解析從 A1 的 key 內(nèi)容開始;
3.?構(gòu)造 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 中,我們明面上構(gòu)造了一個(gè)含有 3 個(gè)元素的 Bundle,分別是:
??A0: Parcelable 類型,元素為我們帶有漏洞的 Parcelable;
??A1: ByteArray 類型,長度為 28 字節(jié),ByteArray 的內(nèi)容為隱藏的 Intent 元素,即 20+4+4;
??A2: NULL 類型;
其中關(guān)鍵的是 A1 的 key,在觸發(fā)漏洞后,會(huì)被解析為第二個(gè)元素的元素類型和長度,即解析出來的內(nèi)容為:
??0d000000: 解析為元素類型,等于 VAL_BYTEARRAY(13);
??08000000: 解析為 ByteArray 長度,即 8 字節(jié);注意這里額外的 0 是字符串寫入時(shí)候 pad 出來的。
之所以是 8 字節(jié),是為了把后面的長度字段吞掉,使得解析下一個(gè)元素可以直接到我們隱藏的 intent 中。
第三個(gè)元素 A2 只是用于占位,在第一次序列化時(shí)候有用,第二次時(shí)直接被我們隱藏的 intent 替代了。上述代碼的運(yùn)行結(jié)果如下:
[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 包含三個(gè) key,其中并沒有 intent 字段,但經(jīng)過序列化再反序列化后的 Bundle B 已經(jīng)修改了,第二個(gè)元素的變成了長度為 8 字節(jié)的 ByteArray,實(shí)際上其 key 長度為;第三個(gè)元素則是我們偽造的 intent 元素,值為 0x1337,因此我們成功地繞過了第一次 containsKey 的校驗(yàn),而在第二次中進(jìn)行觸發(fā)!
上述例子在 Android 12 中進(jìn)行測(cè)試,理論上之前的版本也類似。
實(shí)際上的漏洞與這個(gè)例子大同小異,由于 Android 系統(tǒng)對(duì) IPC 的大量使用,許多操作都會(huì)在?system_server、Settings 應(yīng)用、用戶應(yīng)用中來回穿梭,也就造成了許多上述 TOCTOU 的問題。對(duì)其中細(xì)節(jié)感興趣的可以參考下面的分析文章:
??ReparcelBug - PoC for CVE-2017-0806[6]
??CVE-2017-0806 原理分析[7]
??Bundle風(fēng)水——Android序列化與反序列化不匹配漏洞詳解[8]
漏洞修復(fù)
上述漏洞的修復(fù)似乎很直觀,只需要把 Vulnerable 類中不匹配的讀寫修復(fù)就行了。但實(shí)際上這類漏洞并不是個(gè)例,歷史上由于代碼編寫人員的粗心大意,曾經(jīng)出現(xiàn)過許多因?yàn)樽x寫不匹配導(dǎo)致的提權(quán)漏洞,包括但不限于:
??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
??...
另一個(gè)修復(fù)思路是修復(fù) TOCTOU 漏洞本身,即確保檢查和使用的反序列化對(duì)象是相同的,但這種修復(fù)方案也是治標(biāo)不治本,同樣可能會(huì)被攻擊者找到其他的攻擊路徑并繞過。
因此,為了徹底解決這類層出不窮的問題,Google 提出了一種簡單粗暴的緩釋方案,即直接從 Bundle 類中下手。雖然 Bundle 本身是 ArrayMap 結(jié)構(gòu),但在反序列化時(shí)候即便只需要獲取其中一個(gè) key,也需要把整個(gè) Bundle 反序列化一遍。這其中的主要原因在于序列化數(shù)據(jù)中每個(gè)元素的大小是不固定的,且由元素的類型決定,如果不解析完前面的所有數(shù)據(jù),就不知道目標(biāo)元素在什么地方。
為此在 21 年左右,AOSP 中針對(duì) Bundle 提交了一個(gè)稱為?LazyBundle(9ca6a5)[9]?的 patch。其主要思想為針對(duì)一些長度不固定的自定義類型,比如 Parcelable、Serializable、List 等結(jié)構(gòu)或容器,會(huì)在序列化時(shí)將對(duì)應(yīng)數(shù)據(jù)的大小添加到頭部。這樣在反序列化時(shí)遇到這些類型的數(shù)據(jù),可以僅通過檢查頭部去選擇性跳過這些元素的解析,而此時(shí) sMap 中對(duì)應(yīng)元素的值會(huì)設(shè)置為 LazyValue,在實(shí)際用到這些值的時(shí)候再去對(duì)特定數(shù)據(jù)進(jìn)行反序列化。
這個(gè) patch 可以在一定程度上緩釋針對(duì) Bundle 風(fēng)水的攻擊,而且在提升系統(tǒng)健壯性也有所助益,因?yàn)榧幢銓?duì)于損壞的 Parcel 數(shù)據(jù),如果接收方?jīng)]有使用到對(duì)應(yīng)的字段,就可以避免異常的發(fā)生。對(duì)于之前的 Bundle 解析策略,哪怕只調(diào)用了?size?方法,也會(huì)觸發(fā)所有元素的解析從而導(dǎo)致異常。 在這個(gè) patch 中?unparcel?還增加了一個(gè) boolean 參數(shù)?itemwise,如果為 true 則按照傳統(tǒng)方式解析每個(gè)元素,否則就會(huì)跳過 LazyValue 的解析。
CVE-2021-0928
在前面反序列化的示例中,漏洞主要出在一個(gè)自定義的 Vulnerable 類中,即手工編寫的 readFromParcel/writeToParcel 不匹配問題。在現(xiàn)實(shí)中,這種出現(xiàn)問題的類通常只在進(jìn)程間使用而幾乎不用于跨進(jìn)程,否則在正常 IPC 調(diào)用時(shí)候就會(huì)出現(xiàn)明顯的數(shù)據(jù)錯(cuò)誤。但也還有一種情況,比如在 readFromParcel 方法調(diào)用的過程中出現(xiàn)異常,但是上級(jí)進(jìn)行了“優(yōu)雅”的?try-catch,從而程序并不會(huì)報(bào)錯(cuò),但異常發(fā)生后 Parcel 中還有剩余數(shù)據(jù)并未消費(fèi),因此會(huì)被后續(xù)的解析錯(cuò)誤使用,比如在 AIDL 調(diào)用時(shí)候就會(huì)被后續(xù)的參數(shù)用到。
CVE-2021-0928?就是這么一個(gè)經(jīng)典的漏洞。漏洞本身只存在于 Andorid 12 beta3 的短暫時(shí)間窗口中,因此我們并不需要太過于關(guān)注細(xì)節(jié),此外漏洞的作者也已經(jīng)在 Github 上公開了該漏洞的詳細(xì)介紹以及 POC 代碼。作為歷史漏洞研究,筆者這里只做簡單的介紹,并重點(diǎn)關(guān)注一些值得學(xué)習(xí)的漏洞利用思路和漏洞模式。完整的漏洞細(xì)節(jié)分析和 POC 可以參考原作者的分享:
??CVE-2021-0928, writeToParcel/createFromParcel serialization mismatch in android.hardware.camera2.params.OutputConfiguration[10]
漏洞介紹
主要出現(xiàn)漏洞的類為?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?中對(duì)構(gòu)造函數(shù)進(jìn)行了異常捕捉,但是對(duì)于異常僅僅是打印并返回,這導(dǎo)致 Parcel 數(shù)據(jù)殘留從而影響后續(xù)的數(shù)據(jù)解析。
該漏洞本身并不復(fù)雜,是個(gè)很常見的 Parcel 序列化/反序列化不匹配問題,關(guān)鍵是這個(gè)漏洞如何利用?由于漏洞的本質(zhì)是序列化數(shù)據(jù)殘留,因此我們可以主要關(guān)注一些 AIDL 調(diào)用場(chǎng)景的參數(shù),這樣解析下一個(gè)參數(shù)時(shí)其內(nèi)容也就會(huì)被我們所污染的數(shù)據(jù)控制。其中一個(gè)漏洞觸發(fā)鏈路就是 sendBroadcast。
Android 廣播
關(guān)于 Android 廣播的發(fā)送和接收流程應(yīng)該已經(jīng)有很多其他博客介紹過了,這里將其簡化為下面的階段,假設(shè)廣播由應(yīng)用 A 發(fā)送給應(yīng)用 B;
1.?A 調(diào)用 sendBroadcast,這實(shí)際上是個(gè) AIDL 接口,接收方即實(shí)際實(shí)現(xiàn)為 system_server;
2.?system_server 接收到 Intent 后進(jìn)行一系列處理,判斷合法的接收方,然后調(diào)用?IApplicationThread.scheduleReceiver?發(fā)送廣播數(shù)據(jù);這同樣是一個(gè) AIDL 接口,由各個(gè)應(yīng)用在啟動(dòng)之初通過?IActivityManager.attachApplication()?傳遞給 system_server;因此這個(gè)調(diào)用實(shí)際上會(huì)進(jìn)入到應(yīng)用 B 中;
3.?B 觸發(fā)?ActivityThread.handleMessage,進(jìn)而調(diào)用?handleReceiver;
4.?handleReceiver 中通過傳來的數(shù)據(jù)去實(shí)例化應(yīng)用上下文,獲取對(duì)應(yīng)的 receiver,最終調(diào)用開發(fā)者注冊(cè)的 onReceive 回調(diào);
第 2 步中 scheduleReceiver 的定義如下:
public?final?void?scheduleReceiver(Intent?intent,?ActivityInfo?info,
????CompatibilityInfo?compatInfo,?int?resultCode,?String?data,?Bundle?extras, ????boolean?sync,?int?sendingUser,?int?processState);其中第一個(gè) intent 是 A 應(yīng)用指定的 sendBroadcast 的參數(shù),第二個(gè)參數(shù)則是 system_server 傳遞給應(yīng)用 B 的。漏洞利用思路就是通過 intent 參數(shù)的反序列化數(shù)據(jù)殘留,間接地修改 info 參數(shù),因?yàn)閼?yīng)用 B 會(huì)使用 ActivityInfo 中的數(shù)據(jù)去實(shí)例化代碼,具體來說就是:
private?void?handleReceiver(ReceiverData?data)?{ ????String?component?=?data.intent.getComponent().getClassName(); ????LoadedApk?packageInfo?=?getPackageInfoNoCheck( ????????????data.info.applicationInfo,?data.compatInfo); ????app?=?packageInfo.makeApplicationInner(false,?mInstrumentation); ????//?... }應(yīng)用會(huì)通過傳入的 ActivityInfo 構(gòu)造 LoadedApk,其中會(huì)使用內(nèi)部的?zipPaths?創(chuàng)建 ClassLoader,攻擊者如果可以控制這個(gè)字段,就能實(shí)現(xiàn)針對(duì)應(yīng)用 B 的任意代碼執(zhí)行。
要實(shí)現(xiàn)這個(gè)目的,還有億點(diǎn)細(xì)節(jié)需要解決,首先我們先來看一個(gè) Java 語言的特性。
Java 類型擦除
Java 類型擦除也稱為?Type Erasure[11],主要產(chǎn)生在 Java 的泛型代碼中。Java 虛擬機(jī)中并沒有泛型類型的概念,因此在編譯時(shí)實(shí)際類型會(huì)被替換為其限制對(duì)象或者原始對(duì)象?Object?類型,這個(gè)過程就叫做類型擦除。
例如,下述代碼:
public?static?
?void?printArray(E[]?array)?{ ????for?(E?element?:?array)?{ ????????System.out.printf("%s?",?element); ????} }會(huì)在編譯時(shí)變成:
public?static?void?printArray(Object[]?array)?{ ????for?(Object?element?:?array)?{ ????????System.out.printf("%s?",?element); ????} }即模版類型 E 變成了原始類型 Object。對(duì)于有限制類型的模版,比如:
public?static?>?void?printArray(E[]?array)?{ ????for?(E?element?:?array)?{ ????????System.out.printf("%s?",?element); ????} } 會(huì)在編譯時(shí)替換成其限制類型:
public?static?void?printArray(Comparable[]?array)?{ ????for?(Comparable?element?:?array)?{ ????????System.out.printf("%s?",?element); ????} }那么,這個(gè)特性對(duì)于漏洞的利用有什么幫助呢?回顧?OutputConfiguration?類,其中有個(gè)序列化的字段?sensorPixelModesUsed?是?ArrayList
?類型,在反序列化該字段時(shí),使用的是?Parcel.readList?方法,其調(diào)用鏈路是: ??Parcel.readList
??Parcel.readListInternal
??Parcel.readValue
最終是使用 readValue 讀取 List 中每個(gè)元素的值。因此實(shí)際上可以讀出任何 readValue 支持的類型,比如 Parcelable、IBinder 等,并不局限于 Integer。
僅僅進(jìn)行反序列化并不會(huì)出現(xiàn)任何問題,只不過在使用具體的元素時(shí),如果我們實(shí)際讀取的類型無法轉(zhuǎn)換為整數(shù),就會(huì)出現(xiàn)?ClassCastException?異常。在這個(gè)漏洞場(chǎng)景中,我們并不會(huì)使用這個(gè)數(shù)組的元素,因此我們可以指定任意的序列化類。又由于我們需要使目標(biāo)類在序列化/反序列化過程產(chǎn)生不匹配,那么就需要找到一個(gè)類,使得該類可以在 system_server 中成功反序列化,但是在應(yīng)用 B 中出現(xiàn)異常,比如?ClassNotFoundException。
原作者的利用是使用了?PackageManagerException,這是一個(gè)?Serializable?類而不是?Parcelable,因?yàn)?readSerializable/ObjectInputStream 不需要指定 ClassLoader[12]。當(dāng)然肯定還存在其他可用的類,感興趣的可以自行發(fā)掘。
實(shí)際利用中由于讀取使用了帶 classLoader 的 readList,因此到 ObjectInputStream 時(shí)?latestUserDefinedLoader?會(huì)被影響,解決方案是將 Serializable 再套一層 Parcelable,使用不帶 ClassLoader 的 readList 去進(jìn)行恢復(fù)。這里選用的是?WindowContainerTransaction?類。
其他
上面只介紹了漏洞利用的大致流程,完整的利用還有一些細(xì)節(jié)需要注意,比如:
1.?如何將任意 Parcelable 放到 Intent 中;
2.?精細(xì)的內(nèi)存布局;
對(duì)于問題 1,使用 putExtras(Bundle) 并不可行,因?yàn)樵?Intent 中 Bundle 會(huì)作為一個(gè)整體進(jìn)行拷貝,因此 Bundle 中的反序列化錯(cuò)誤并不會(huì)影響 Intent 本身。作者是用了一個(gè)在 Android 12 中加入的 Intent.ClipData 字段去實(shí)現(xiàn)的,不過目前已經(jīng)修復(fù)了就不再展開了。
對(duì)于問題 2,有幾個(gè)關(guān)鍵點(diǎn)需要注意。一是在觸發(fā)異常后,并沒有直接跳到第二個(gè)參數(shù),而是會(huì)繼續(xù)上級(jí)(解析了一半的)容器處理,因此后續(xù) payload 需要維持這部分?jǐn)?shù)據(jù)的棧平衡;另外一個(gè)問題是在 Andorid 中新增了許多隱藏 API 的限制,使得我們無法通過調(diào)用這些方法去構(gòu)造 ActivityInfo 等數(shù)據(jù),這個(gè)解決方法是參考源碼直接通過 Parcel 去寫入數(shù)據(jù),只是過程繁瑣了一點(diǎn),當(dāng)然也可以用其他方式去繞過 Hidden API 的限制[13]。
漏洞修復(fù)
這個(gè)漏洞本身對(duì)終端用戶的影響不是很大,畢竟只在 Android 12 Preview 版本中就修復(fù)了。但通過這個(gè)漏洞,Google 引入了許多修復(fù)和緩釋方案,直接影響了后續(xù)的漏洞挖掘和利用思路。
首先針對(duì)漏洞本身,修復(fù)方案為:
1.?對(duì)上述類去除隱式的異常處理,修復(fù)讀寫不一致的問題;
2.?使用 readIntArray 而不是 readList/readValue 去讀取數(shù)據(jù),消滅類型擦除的副作用;
3.?防止 ClipData.mActivityInfo 寫入 Parcel,除非顯式指定。這消除了向 Intent 寫入任意 Parcelable 的一個(gè)攻擊鏈路;
另外,在 Andorid 13 中,引入了更強(qiáng)的反序列化緩釋方案:
1.?新增了一個(gè) readListInternal 方法的重載,增加額外的?Class?參數(shù),顯式指定讀取列表的元素的類型,并且將原來的方法標(biāo)記為?@Deprecated;
2.?新增了?Parcel.enforceNoDataAvail[14]?方法,用于確保反序列化結(jié)束后,Parcel 中不再存在多余的數(shù)據(jù);回想上一節(jié)中 Bundle 風(fēng)水的利用,實(shí)際上第三個(gè)元素在第二次反序列化中是多出來的,因此這個(gè)修改會(huì)導(dǎo)致上述 Bundle 風(fēng)水的失?。划?dāng)然也有一些繞過的手法,比如通過更復(fù)雜的風(fēng)水使得第二次反序列化能夠到 Parcel 的末尾即可;
3.?即上文說過的 LazyBundle patch,在 LazyBundle 實(shí)現(xiàn)中,Parcelable、List 等類型會(huì)在序列化數(shù)據(jù)的元素開頭單獨(dú)存儲(chǔ)長度信息。不過,這個(gè) patch 并不會(huì)影響非 Bundle 造成的反序列化漏洞,比如這個(gè)漏洞。
LeakValue
時(shí)間來到 2022 年,Google 推出了 Android 13,在其中正式啟用了 LazyBundle 的 patch。我們?cè)谇懊?Bundle 風(fēng)水中已經(jīng)簡要介紹過大致原理,這里再深入去分析其實(shí)現(xiàn)。
深入 LazyValue
由于 LazyValue 是在使用時(shí)才進(jìn)行反序列化的,因此在讀取值時(shí),需要預(yù)先知道它在 Parcel 中所占的數(shù)據(jù)區(qū)間,讀取后還需要修改 Parcel 結(jié)構(gòu)中對(duì)應(yīng)的偏移。這也是為什么 LazyValue 需要在序列化數(shù)據(jù)中寫入其數(shù)據(jù)長度的原因,因?yàn)閷?duì)于這類數(shù)據(jù)(如 Parcelable),無法僅通過類型得知其數(shù)據(jù)長度。
讀取 LazyValue 的代碼實(shí)現(xiàn)如下所示:
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); ????} }這里面有幾個(gè)關(guān)鍵點(diǎn)。首先,LazyValue 中存儲(chǔ)的是 Parcel 的引用,以及其在 Parcel 中所占的數(shù)據(jù)區(qū)間,使用其內(nèi)部屬性 mPosition、mLength 表示,mPostion 對(duì)應(yīng)上述代碼中的?start,mLength 對(duì)應(yīng)?valueLength。其中 valueLength 是添加到序列化數(shù)據(jù)頭部的長度字段,包括長度和類型所占的空間。
其次,在 Parcel 中構(gòu)造 LazyValue 之后,會(huì)將 dataPostion 設(shè)置到對(duì)象對(duì)應(yīng)序列化數(shù)據(jù)的尾部。在需要實(shí)際數(shù)據(jù)時(shí),會(huì)調(diào)用?LazyValue.apply?方法進(jìn)行真正的反序列化,如下所示:
@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; }可以看到在反序列化時(shí)候會(huì)將原始 Parcel 的 dataPostion 保存,并設(shè)置指向到 LazyValue 中的位置,然后調(diào)用 readValue 去進(jìn)行反序列化,完成后再次恢復(fù) Parcel 調(diào)用前的 dataPostion。
這可能會(huì)涉及到一些安全問題,比如多線程之間的條件競爭,在修改 dataPostion 之后被其他線程使用,當(dāng)然這通過?synchronized?同步塊防御住了。另一個(gè)問題是,在 LazyValue 未使用之前,其所對(duì)應(yīng)的 Parcel 是不能被釋放的,否則就會(huì)出現(xiàn)類似 UAF 的內(nèi)存問題??紤]到對(duì)于 Parcel 的內(nèi)存分配策略而言,大多數(shù)是使用手工管理的 obtain/recycle 方式,這個(gè)問題是有可能存在的。
Parcel 內(nèi)存管理
由于 Parcel 本身是為了頻繁的 IPC 傳輸而設(shè)計(jì)的,因此對(duì)于其分配和釋放通常使用手工管理的方式,以避免 Java 堆分配或者 GC 帶來的性能損耗。在閱讀系統(tǒng)源碼或者 AIDL 生成的模版代碼時(shí)都能發(fā)現(xiàn),Parcel 使用 obtain 進(jìn)行分配,使用 recycle 進(jìn)行釋放。
分配的代碼實(shí)現(xiàn)如下:
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?是靜態(tài)屬性,mPoolNext?是成員屬性,二者都是 Parcel 類型。因此內(nèi)存池中的 Parcel 可以看做是一個(gè)表頭為 sOwnedPool 的單鏈表結(jié)構(gòu)。obtain 本質(zhì)上是從鏈表中取出表頭的數(shù)據(jù)。
對(duì)于 IPC 接收到的 Parcel 數(shù)據(jù)分配方式略有不同,因?yàn)檫@些 Parcel 在 C++ 層由系統(tǒng)創(chuàng)建,因此使用不同的鏈表,表頭為?sHolderPool?靜態(tài)屬性。這些 Parcel 通過構(gòu)造函數(shù)?Parcel(long nativePtr)?去構(gòu)建,生命周期由系統(tǒng)管理(mOwnsNativeParcelObject?為?false),因此需要區(qū)分開來。這類 Parcel 的分配通過重載的?obtain(long)?方法去創(chuàng)建,與上述實(shí)現(xiàn)大同小異。
在釋放過程會(huì)將這兩種情況區(qū)分開來:
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?釋放操作相當(dāng)于單鏈表的插入,即將釋放的 Parcel 放入 sOwnPool/sHolderPool 鏈表的頭部。從上述代碼可以看出,Parcel 的分配和釋放過程是后進(jìn)先出(LIFO)的,即 Parcel obtain 會(huì)分配出最近一次釋放的對(duì)象。
Parcel UAF
通過 LazyValue 的實(shí)現(xiàn)以及 Parcel 內(nèi)存管理的策略,我們似乎可以找到一個(gè)攻擊場(chǎng)景: 在 Parcel 讀取 LazyValue 之后,將 Parcel 進(jìn)行釋放,而后再讀取對(duì)應(yīng)的 LazyValue,此時(shí)如果 Parcel 被分配并填充了敏感數(shù)據(jù),那么我們的 LazyValue 就可以讀取出這些敏感內(nèi)容造成數(shù)據(jù)泄露。如果泄露的數(shù)據(jù)來自其他進(jìn)程,且數(shù)據(jù)中包含特權(quán)的 IBinder 等結(jié)構(gòu),那么還可能造成提權(quán)或者 RCE 的危害!
為此,我們首先需要找到一個(gè) recycle 后再次使用 LazyValue 的場(chǎng)景。接下來,就是漏洞上場(chǎng)的時(shí)間了,出現(xiàn)上述問題的漏洞編號(hào)是?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; ?????????}問題還是出現(xiàn)在 Bundle 反序列化的過程中,之前說過 Bundle 通過 readFromParcelInner 去進(jìn)行反序列化,其實(shí)現(xiàn)如下:
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?的數(shù)據(jù) ????Parcel?p?=?Parcel.obtain(); ????p.appendFrom(parcel,?offset,?length); ????mParcelledData?=?p;注意 if 分支外部,這是大部分 Bundle 反序列化的流程,即通過新分配一個(gè) parcel 去拷貝原始數(shù)據(jù)并保存到 mParcelledData 中。因?yàn)殚_發(fā)者知道 parcel 參數(shù)的生命周期不由自身控制。
而在 if 內(nèi)部,調(diào)用了 initializeFromParcelLocked,其中會(huì)使用 readArrayMap 對(duì) parcel 數(shù)據(jù)進(jìn)行反序列化,這其中是帶有 LazyValue 的。由于開發(fā)者知道 parcel 生命周期不可控,因此在后續(xù)調(diào)用了?unparcel(true)?去強(qiáng)制對(duì)每個(gè) LazyValue 進(jìn)行反序列化以去除對(duì) parcel 數(shù)據(jù)的依賴。
如果 unparcel 內(nèi)部某些 LazyValue 能夠被攻擊者繞過解析,那么這里就存在一個(gè) UAF 漏洞,后續(xù)讀取 LazyValue 時(shí)就能泄露出復(fù)用的 Parcel 中的數(shù)據(jù)。當(dāng)然,要觸發(fā)這個(gè)漏洞需要有幾個(gè)前提:
1.?hasReadWriteHelper 返回 true;
2.?unparcel 能被繞過;
對(duì)于問題 1,我們可以在 AOSP 代碼中搜索符合條件的 Parcel 類,比如?RemoteViews;
對(duì)于問題 2,我們可以嘗試使其中某個(gè) LazyValue 解析失敗,比如 Parcelable 的類不存在時(shí),代碼會(huì)拋出 ClassNotFoundException,被捕捉后再次拋出 BadParcelableException。有趣的是這個(gè)異常會(huì)在 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,那么異常不會(huì)被再次拋出,而只是打印異常并返回 null。對(duì)于?system_server?而言該條件是滿足的,因?yàn)槠淠康木褪菫榱朔乐瓜到y(tǒng)重要服務(wù)頻繁崩潰。因此,我們可以將 Bundle 的某個(gè)值設(shè)置為另外一個(gè)容器,比如 List,然后在容器中存儲(chǔ)一個(gè)不存在的 Parcelable,那么 List 中后續(xù)的 LazyValue 將不會(huì)被遞歸解析到,我們也就獲得了一個(gè) UAF 對(duì)象。
漏洞利用
該漏洞的利用過程比較曲折,由于漏洞已經(jīng)修復(fù),因此筆者對(duì)于復(fù)現(xiàn)的興趣缺缺。此外原作者也公開了詳細(xì)利用思路和漏洞利用代碼,感興趣的可以自行參考:
??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]
這里提煉一下其中值得學(xué)習(xí)的點(diǎn):
1.?UAF 漏洞的核心利用目標(biāo)是獲取目標(biāo)應(yīng)用的?IApplicationThread?句柄,這通常是應(yīng)用啟動(dòng)初期使用?attachApplication()?傳遞給?system_server?的,主要用于讓后者給應(yīng)用發(fā)送四大組件的生命周期回調(diào)。通過濫用這個(gè) Binder 句柄可以實(shí)現(xiàn) RCE,參考前面的 CVE-2021-0928 (scheduleReceiver);
2.?為了能從?system_server?中泄露任意應(yīng)用的 IApplicationThread 句柄,需要兩個(gè)條件。首先是有一個(gè)接口可以發(fā)送 Parcelable 數(shù)據(jù)并將其取回,AppWidgetHost、Notification.contentView、Mediasession 都可以滿足,作者選用的是 MediaSession.get/setQueue 接口;
3.?其次,要使得 UAF 對(duì)象能被包含 IApplicationThread 的 Parcel 復(fù)用,需要準(zhǔn)確的時(shí)機(jī)。但是 attachApplication 只在應(yīng)用啟動(dòng)時(shí)一個(gè)很小的時(shí)間窗口中被調(diào)用,因此作者尋找其中可能存在的鎖去拓展這個(gè)窗口;
4.?ParceledListSlice 是一個(gè)在 IPC 間傳輸?shù)奶厥鈹?shù)據(jù)結(jié)構(gòu),在其數(shù)據(jù)量小時(shí)可以同步傳輸,而對(duì)于大量的數(shù)據(jù),會(huì)將其轉(zhuǎn)換為 binder 發(fā)送給對(duì)方然后進(jìn)行異步傳輸;
5.?ActivityManager.moveTaskToFront()?調(diào)用時(shí)可以提供 ActivityOptions 參數(shù),以 Bundle 形式進(jìn)行封裝,并在 system_server 端進(jìn)行反序列化。因此攻擊者可以在 Bundle 中加入 ParceledListSlice 類型的數(shù)據(jù),從而在反序列化時(shí)回調(diào)到自身進(jìn)行阻塞。該函數(shù)調(diào)用時(shí)候持有?mGlobalLock?鎖,因此可以阻塞 attachApplication 的執(zhí)行,從而更好地構(gòu)造 UAF 風(fēng)水;
6.?由于調(diào)用了 moveTaskToFront,種種原因?qū)е聼o法使用 startActivity 來啟動(dòng)目標(biāo)應(yīng)用,因此作者使用 ContentProvider 來啟動(dòng)目標(biāo)應(yīng)用。
7.?在我們的 UAF LazyValue Parcel 被成功占位后,對(duì)應(yīng)的 IApplicationThread 實(shí)際上在比較靠前的位置,但我們的 LazyValue 所在位置比較靠后因此無法讀取到對(duì)應(yīng) binder。解決方案是可以找其他占位對(duì)象,不過作者這里利用了一個(gè)新的漏洞?CVE-2022-20474[17],即 readLazyValue 方法中 objectLenth 只檢查了證書溢出,卻沒檢查負(fù)數(shù)的情況,因此設(shè)置負(fù)數(shù)的 prefixLength 實(shí)際上可以讀取 Parcel 前方的數(shù)據(jù),從而繞過該限制。
其他還有億點(diǎn)細(xì)節(jié),就需要?jiǎng)邮謴?fù)現(xiàn)才能真正理解了。理論上該漏洞可以影響安全補(bǔ)丁在 2022-11 之前的 Android 13 設(shè)備。 攻擊的目標(biāo)應(yīng)用可以是任意應(yīng)用,因此可以選擇?Settings.apk,其 UID=system,攻擊成功后就相當(dāng)于獲取到了 system 權(quán)限。這在 Goole VRP 中應(yīng)該是 10 萬刀樂的標(biāo)準(zhǔn)。
總結(jié)
本文算是筆者學(xué)習(xí) Android 反序列化漏洞的一個(gè)筆記,按照時(shí)間線記錄了幾個(gè)公開的經(jīng)典反序列化漏洞,并且介紹相關(guān)的修復(fù)策略以及繞過方法。從中我們可以看到,Parcel 作為輕量級(jí)的序列化方案,許多操作都需要手動(dòng)管理,這導(dǎo)致了許多讀寫不匹配的問題,雖然后續(xù)引進(jìn)了 LazyBundle 優(yōu)化,但又引發(fā)了新的內(nèi)存管理問題,使得傳統(tǒng)二進(jìn)制的 UAF 甚至 Double Free 類漏洞可以在 Java 世界重現(xiàn)。
漏洞本身只是一個(gè)引子,指導(dǎo)我們未來的研究方向。從這些漏洞中,我們也可以發(fā)現(xiàn) Security By Default 的重要性,比如序列化 Java 容器導(dǎo)致的類型擦除問題和 Bundle 的序列化問題。在后期雖然嘗試進(jìn)行了緩釋,但由于歷史負(fù)擔(dān),很多不安全的設(shè)計(jì)只能縫縫補(bǔ)補(bǔ)延續(xù)下去,這也是后續(xù)挖掘和審計(jì)其他產(chǎn)品漏洞的一個(gè)重要著手點(diǎn)。
??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 進(jìn)程間通信與逆向分析:?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風(fēng)水——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編輯:黃飛
?
評(píng)論
查看更多