2018年4月,Android安全公告公布了CVE-2017-13287漏洞。
與同期披露的其他漏洞一起,同屬于框架中Parcelable對(duì)象的寫入(序列化)與讀出(反序列化)的不一致所造成的漏洞。
在剛看到谷歌對(duì)于漏洞給出的補(bǔ)丁時(shí)一頭霧水,
在這里要感謝heeeeen@MS509Team在這個(gè)問題上的成果,啟發(fā)了我的進(jìn)一步研究。
原理
谷歌在Android中提供了Parcelable作為高效的序列化實(shí)現(xiàn),用來支持IPC調(diào)用中多樣的對(duì)象傳遞需求。
但是序列化和反序列化的過程依舊依靠程序員編寫的代碼進(jìn)行同步。
那么當(dāng)不同步的時(shí)候,漏洞就產(chǎn)生了。
Bundle
傳輸?shù)臅r(shí)候Parcelable對(duì)象按照鍵值對(duì)的形式存儲(chǔ)在Bundle內(nèi),Bundle內(nèi)部有一個(gè)ArrayMap用hash表進(jìn)行管理。
反序列化過程如下:
/* package */ void unparcel() { synchronized (this) { final Parcel parcelledData = mParcelledData; int N = parcelledData.readInt(); if (N < 0) { return; } ArrayMap<String, Object> map = mMap; try { parcelledData.readArrayMapInternal(map, N, mClassLoader); } catch (BadParcelableException e) { } finally { mMap = map; parcelledData.recycle(); mParcelledData = null; } } }
首先讀取一個(gè)int指示里面有多少對(duì)鍵值對(duì)。
/* package */ void readArrayMapInternal(ArrayMap outVal, int N, ClassLoader loader) { if (DEBUG_ARRAY_MAP) { RuntimeException here = new RuntimeException("here"); here.fillInStackTrace(); Log.d(TAG, "Reading " + N + " ArrayMap entries", here); } int startPos; while (N > 0) { if (DEBUG_ARRAY_MAP) startPos = dataPosition(); String key = readString(); Object value = readValue(loader); outVal.Append(key, value); N--; } outVal.validate(); }
之后的每一對(duì)先是Key的字符串,然后是對(duì)應(yīng)的Value。
public final Object readValue(ClassLoader loader) { int type = readInt(); switch (type) { case VAL_NULL: return null; case VAL_STRING: return readString(); case VAL_INTEGER: return readInt(); case VAL_MAP: return readHashMap(loader); case VAL_PARCELABLE: return readParcelable(loader); case VAL_SHORT: return (short) readInt(); case VAL_LONG: return readLong();
值內(nèi)部先是一個(gè)int指示值的類型,再存儲(chǔ)實(shí)際值。
當(dāng)Bundle被寫入Parcel時(shí):
void writeToParcelInner(Parcel parcel, int flags) { final ArrayMap<String, Object> map; synchronized (this) { if (mParcelledData != null) { if (mParcelledData == NoImagePreloadHolder.EMPTY_PARCEL) { parcel.writeInt(0); } else { int length = mParcelledData.dataSize(); parcel.writeInt(length); parcel.writeInt(BUNDLE_MAGIC); parcel.appendFrom(mParcelledData, 0, length); } return; } map = mMap; } }
先寫入Bundle總共的字節(jié)數(shù),再寫入魔數(shù),之后是指示鍵值對(duì)數(shù)的N,還有相應(yīng)的鍵值對(duì)。
LaunchAnyWhere
弄明白Bundle的內(nèi)部結(jié)構(gòu)后,先來看看漏洞觸發(fā)的地方:

這個(gè)流程是AppA在請(qǐng)求添加一個(gè)帳號(hào):
- AppA請(qǐng)求添加一個(gè)帳號(hào)
- System_server接受到請(qǐng)求,找到可以提供帳號(hào)服務(wù)的AppB,并發(fā)起請(qǐng)求
- AppB返回了一個(gè)Bundle給系統(tǒng),系統(tǒng)把Bundle轉(zhuǎn)發(fā)給AppA
- AccountManagerResponse在AppA的進(jìn)程空間中調(diào)用startActivity(intent)調(diào)起一個(gè)Activity。
在第4步中,如果AppA的權(quán)限較高,比如Settings,那么AppA可以調(diào)用正常App無法調(diào)用的未導(dǎo)出Activity。
并且在第3步中,AppB提供的Bundle在system_server端被反序列化,之后system_server根據(jù)之前得到的內(nèi)容再序列化并傳遞給AppA。
那么如果對(duì)應(yīng)的傳遞內(nèi)容的序列化和反序列化代碼不一樣,就會(huì)影響到自己以及之后的內(nèi)容的結(jié)果。
傳遞的Bundle對(duì)象中包含一個(gè)重要鍵值對(duì){KEY_INTENT:intent},指定了AppA稍后調(diào)用的Activity。
如果這個(gè)被指定成Setting中的com.android.settings.password.ChooseLockPassword,就可以在不需要原本鎖屏密碼的情況下重新設(shè)置鎖屏密碼。
谷歌在這個(gè)過程中進(jìn)行了檢查,保證Intent中包含的Activity所屬的簽名和AppB一致,并且不是未導(dǎo)出的系統(tǒng)Actiivity。
protected void checkKeyIntent(int authUid, Intent intent) throws SecurityException { long bid = Binder.clearCallingIdentity(); try { PackageManager pm = mContext.getPackageManager(); ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, maccounts.userId); ActivityInfo targetActivityInfo = resolveInfo.activityInfo; int targetUid = targetActivityInfo.applicationInfo.uid; if (!isExportedSystemActivity(targetActivityInfo) && (PackageManager.SIGNATURE_MATCH != pm.checkSignatures(authUid, targetUid))) { String pkgName = targetActivityInfo.packageName; String activityName = targetActivityInfo.name; String tmpl = "KEY_INTENT resolved to an Activity (%s) in a package (%s) that " + "does not share a signature with the supplying authenticator (%s)."; throw new SecurityException( String.format(tmpl, activityName, pkgName, mAccountType)); } } finally { Binder.restoreCallingIdentity(bid); }}
攻擊思路便是在system_server進(jìn)行檢查時(shí)Bundle中的惡意{KEY_INTENT:intent}看不到,但是在重新序列化之后在Setting出現(xiàn),這樣就繞過了檢查。
利用
首先來看看漏洞所在的代碼
public static final Parcelable.Creator<VerifyCredentialResponse> CREATOR = new Parcelable.Creator<VerifyCredentialResponse>() { @Override public VerifyCredentialResponse createFromParcel(Parcel source) { int responseCode = source.readInt(); VerifyCredentialResponse response = new VerifyCredentialResponse(responseCode, 0, null); if (responseCode == RESPONSE_RETRY) { response.setTimeout(source.readInt()); } else if (responseCode == RESPONSE_OK) { int size = source.readInt(); if (size > 0) { byte[] payload = new byte[size]; source.readByteArray(payload); response.setPayload(payload); } } return response; } @Override public VerifyCredentialResponse[] newArray(int size) { return new VerifyCredentialResponse[size]; } }; @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mResponseCode); if (mResponseCode == RESPONSE_RETRY) { dest.writeInt(mTimeout); } else if (mResponseCode == RESPONSE_OK) { if (mPayload != null) { dest.writeInt(mPayload.length); dest.writeByteArray(mPayload); } } }
仔細(xì)閱讀,會(huì)發(fā)現(xiàn)在mResponseCode為RESPONSE_OK時(shí),
如果mPayload為null,那么writeToParcel不會(huì)在末尾寫入0來正確的指示Payload部分的長度。
而在createFromParcel中是需要readInt來獲知的,這個(gè)就帶來了序列化與反序列化過程的不一致。
可以通過精心構(gòu)造的payload來繞過檢查。
難點(diǎn)在于和已經(jīng)有人公開過的CVE-2017-13288和CVE-2017-13315不同,
它們是重新序列化之后會(huì)多出來4個(gè)字節(jié)。這里是重新序列化之后會(huì)少4個(gè)字節(jié)。
】

利用String的結(jié)構(gòu),把惡意intent隱藏在String里。上圖每段注釋的括號(hào)里寫了其所占用的字節(jié)數(shù)。
在第一次反序列化時(shí),VerifyCredentialResponse內(nèi)部的0還在,惡意intent被包裝在第二對(duì)的Key中。
第二對(duì)的值的類型被制定為VAL_NULL,也就是什么都沒有,常量值為-1。
再次序列化時(shí)writeToParcel沒有writeInt(0),所以到達(dá)Setting的Bundle在RESPONSE_OK之后沒有0,原本的String length被視作payload length,調(diào)用readByteArray讀取。
static jbyteArray android_os_Parcel_createByteArray(JNIEnv* env, jclass clazz, jlong nativePtr) { jbyteArray ret = NULL; Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { int32_t len = parcel->readInt32(); // sanity check the stored length against the true data size if (len >= 0 && len <= (int32_t)parcel->dataAvail()) { ret = env->NewByteArray(len); if (ret != NULL) { jbyte* a2 = (jbyte*)env->GetPrimitiveArrayCritical(ret, 0); if (a2) { const void* data = parcel->readInplace(len); memcpy(a2, data, len); env->ReleasePrimitiveArrayCritical(ret, a2, 0); } } } } return ret;}
再次調(diào)用readInt32讀取長度,之后截取數(shù)組內(nèi)容。相應(yīng)的從Payload length開始的指定長度的內(nèi)容都被視作payload。
只要設(shè)置得當(dāng),惡意intent就會(huì)顯露出來成為實(shí)質(zhì)上的第二對(duì)鍵值對(duì)。
那么之前作為第二對(duì)值的VAL_NULL怎么辦?之前提過它的常量值是-1,上一對(duì)惡意intent剛結(jié)束,在這里調(diào)用的是readString這個(gè)函數(shù)。
const char16_t* Parcel::readString16Inplace(size_t* outLen) const{ int32_t size = readInt32(); // watch for potential int overflow from size+1 if (size >= 0 && size < INT32_MAX) { *outLen = size; const char16_t* str = (const char16_t*)readInplace((size+1)*sizeof(char16_t)); if (str != NULL) { return str; } } *outLen = 0; return NULL;}
再次的readInt32,得到-1,直接返回null,長度為0,會(huì)在JNI層中創(chuàng)建一個(gè)空字符串返回到JAVA層。那么就是說:VAL_NULL單獨(dú)作為一個(gè)空字符串被讀取,之后的三個(gè)藍(lán)色塊被視作值。
這里因?yàn)橹蟮淖址?23456,所以string_length是6.
這個(gè)很關(guān)鍵,因?yàn)樵赟ettings這里被readValue視作type,而6正好是VAL_STRING,也即字符串類型。于是ord('1')= 0x31被視作String length正常使用,正常讀取字符串。
至此Settings側(cè)正常讀取完畢,惡意intent被讀取并執(zhí)行。
假String的構(gòu)造
之前略過了包含惡意intent的假String的具體padding過程,這里展開:
String_length(4) + Payload_length(4) + PADDING(Size + 16) + EVIL_INTENT(Size) + PADDING(8)String_length = Payload_length = (4 + 4 + Size + 16 + Size + 8) / 2 – 1 = Size + 15
這里先給出公式,Size在這里就是Evil_intent部分的長度,String_length和Payload_length在Setting側(cè)都被視作payload的長度使用,故相同。
從兩個(gè)視角去審視這個(gè)公式:
- system_server側(cè)
對(duì)于system_server來說,從String_length開始的部分就是單純的一個(gè)字符串,那么它先讀取String_length并套用readString16Inplace中的公式。
它會(huì)從String_length之后讀取$ 2(1 + Size + 15)=2Size + 32 $,正好包括總長。
- Settings側(cè)
對(duì)于Settings來說,從Payload_length之后會(huì)直接截取對(duì)應(yīng)長度的內(nèi)容作為數(shù)組,即Payload_length之后$Size + 15$,
因?yàn)镻arcel底層的操作對(duì)4向上湊整,所以正好露出EVIL_INTENT。
這樣就可以達(dá)成效果。
結(jié)果
POC: https://github.com/FXTi/CVE201713287POC
總結(jié)
在IPC這塊就算谷歌引入了AIDL這種方式來規(guī)定接口,哪怕只是中間所用到的類的序列化過程出現(xiàn)一點(diǎn)失誤都會(huì)造成如此嚴(yán)重的漏洞。
可見安全編程以及代碼審計(jì)的必要性,沒準(zhǔn)以后還會(huì)有類似機(jī)理的漏洞被發(fā)掘出來。
作者:FXTi
轉(zhuǎn)載自 https://www.anquanke.com/post/id/197710