2018年4月,Android安全公告公布了CVE-2017-13287漏洞。
与同期披露的其他漏洞一起,同属于框架中Parcelable对象的写入(序列化)与读出(反序列化)的不一致所造成的漏洞。
在刚看到谷歌对于漏洞给出的补丁时一头雾水,
在这里要感谢heeeeen@MS509Team在这个问题上的成果,启发了我的进一步研究。
原理
谷歌在Android中提供了Parcelable作为高效的序列化实现,用来支持IPC调用中多样的对象传递需求。
但是序列化和反序列化的过程依旧依靠程序员编写的代码进行同步。
那么当不同步的时候,漏洞就产生了。
Bundle
传输的时候Parcelable对象按照键值对的形式存储在Bundle内,Bundle内部有一个ArrayMap用hash表进行管理。
反序列化过程如下:
/* package */ void unparcel() { synchronized (this) { final Parcel parcelledData = https://www.solves.com.cn/it/cxkf/ydd/Android/2020-02-16/mParcelledData; int N = parcelledData.readInt(); if (N < 0) { return; } ArrayMapmap = mMap; try { parcelledData.readArrayMapInternal(map, N, mClassLoader); } catch (BadParcelableException e) { } finally { mMap = map; parcelledData.recycle(); mParcelledData = null; } } }
首先读取一个int指示里面有多少对键值对。
/* 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 = https://www.solves.com.cn/it/cxkf/ydd/Android/2020-02-16/readValue(loader); outVal.Append(key, value); N--; } outVal.validate(); }
之后的每一对先是Key的字符串,然后是对应的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();
值内部先是一个int指示值的类型,再存储实际值。
当Bundle被写入Parcel时:
void writeToParcelInner(Parcel parcel, int flags) { final ArrayMap map; synchronized (this) { if (mParcelledData != null) { if (mParcelledData =https://www.solves.com.cn/it/cxkf/ydd/Android/2020-02-16/= 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总共的字节数,再写入魔数,之后是指示键值对数的N,还有相应的键值对。
LaunchAnyWhere
弄明白Bundle的内部结构后,先来看看漏洞触发的地方:
这个流程是AppA在请求添加一个帐号:
AppA请求添加一个帐号
System_server接受到请求,找到可以提供帐号服务的AppB,并发起请求
AppB返回了一个Bundle给系统,系统把Bundle转发给AppA
AccountManagerResponse在AppA的进程空间中调用startActivity(intent)调起一个Activity。