安卓漏洞 CVE 2017-13287 复现详解( 二 )

攻击思路便是在system_server进行检查时Bundle中的恶意{KEY_INTENT:intent}看不到,但是在重新序列化之后在Setting出现,这样就绕过了检查 。
 
利用 
首先来看看漏洞所在的代码
public static final Parcelable.Creator<VerifyCredentialResponse> CREATOR= new Parcelable.Creator<VerifyCredentialResponse>() {@Overridepublic 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;}@Overridepublic VerifyCredentialResponse[] newArray(int size) {return new VerifyCredentialResponse[size];}};@Overridepublic 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);}}}仔细阅读,会发现在mResponseCode为RESPONSE_OK时,
 
如果mPayload为null,那么writeToParcel不会在末尾写入0来正确的指示Payload部分的长度 。
 
而在createFromParcel中是需要readInt来获知的,这个就带来了序列化与反序列化过程的不一致 。
 
可以通过精心构造的payload来绕过检查 。
 
难点在于和已经有人公开过的CVE-2017-13288和CVE-2017-13315不同,
 
它们是重新序列化之后会多出来4个字节 。这里是重新序列化之后会少4个字节 。
 

安卓漏洞 CVE 2017-13287 复现详解

文章插图
 
利用String的结构,把恶意intent隐藏在String里 。上图每段注释的括号里写了其所占用的字节数 。
 
在第一次反序列化时,VerifyCredentialResponse内部的0还在,恶意intent被包装在第二对的Key中 。
第二对的值的类型被制定为VAL_NULL,也就是什么都没有,常量值为-1 。
 
再次序列化时writeToParcel没有writeInt(0),所以到达Setting的Bundle在RESPONSE_OK之后没有0,原本的String length被视作payload length,调用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 sizeif (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 = https://www.isolves.com/it/cxkf/ydd/Android/2020-02-16/parcel->readInplace(len);memcpy(a2, data, len);env->ReleasePrimitiveArrayCritical(ret, a2, 0);}}}}return ret;}再次调用readInt32读取长度,之后截取数组内容 。相应的从Payload length开始的指定长度的内容都被视作payload 。
 
只要设置得当,恶意intent就会显露出来成为实质上的第二对键值对 。
 
那么之前作为第二对值的VAL_NULL怎么办?之前提过它的常量值是-1,上一对恶意intent刚结束,在这里调用的是readString这个函数 。
const char16_t* Parcel::readString16Inplace(size_t* outLen) const{int32_t size = readInt32();// watch for potential int overflow from size+1if (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,会在JNI层中创建一个空字符串返回到JAVA层 。那么就是说:VAL_NULL单独作为一个空字符串被读取,之后的三个蓝色块被视作值 。
 
这里因为之后的字符串是123456,所以string_length是6.
 
这个很关键,因为在Settings这里被readValue视作type,而6正好是VAL_STRING,也即字符串类型 。于是ord('1')= 0x31被视作String length正常使用,正常读取字符串 。
 
至此Settings侧正常读取完毕,恶意intent被读取并执行 。
 
假String的构造 
之前略过了包含恶意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


推荐阅读