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


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

文章插图
 
 
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.isolves.com/it/cxkf/ydd/Android/2020-02-16/mParcelledData;int N = parcelledData.readInt();if (N < 0) {return;}ArrayMap map = 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.isolves.com/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指示值的类型,再存储实际值 。
【安卓漏洞 CVE 2017-13287 复现详解】当Bundle被写入Parcel时:
void writeToParcelInner(Parcel parcel, int flags) {final ArrayMap<String, Object> map;synchronized (this) {if (mParcelledData != null) {if (mParcelledData =https://www.isolves.com/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的内部结构后,先来看看漏洞触发的地方:
安卓漏洞 CVE 2017-13287 复现详解

文章插图
 
这个流程是AppA在请求添加一个帐号:
  1. AppA请求添加一个帐号
  2. System_server接受到请求,找到可以提供帐号服务的AppB,并发起请求
  3. AppB返回了一个Bundle给系统,系统把Bundle转发给AppA
  4. AccountManagerResponse在AppA的进程空间中调用startActivity(intent)调起一个Activity 。
 
在第4步中,如果AppA的权限较高,比如Settings,那么AppA可以调用正常App无法调用的未导出Activity 。
 
并且在第3步中,AppB提供的Bundle在system_server端被反序列化,之后system_server根据之前得到的内容再序列化并传递给AppA 。
 
那么如果对应的传递内容的序列化和反序列化代码不一样,就会影响到自己以及之后的内容的结果 。
 
传递的Bundle对象中包含一个重要键值对{KEY_INTENT:intent},指定了AppA稍后调用的Activity 。
 
如果这个被指定成Setting中的com.android.settings.password.ChooseLockPassword,就可以在不需要原本锁屏密码的情况下重新设置锁屏密码 。
 
谷歌在这个过程中进行了检查,保证Intent中包含的Activity所属的签名和AppB一致,并且不是未导出的系统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);}}


推荐阅读