type
status
date
slug
summary
tags
category
icon
password
 
 

0x01 引言

漏洞信息:
编号: CVE-2022-20452
类型:Eop
影响范围:AOSP 13
之前有关Android反序列化漏洞-Bundle风水的文章详细的解释了什么是Parcel MisMatch问题,并且结合两个CVE说明了如何进行漏洞利用。
为了解决自修改Bundle的问题,Android引入了LazyBundle机制,具体来说就是不再每次拿到序列化数据时立刻进行反序列化获得全部的对象,而是根据需求进行获取数据,在一定程度上缓解了自修改Bundle的问题。但是由于引入了LazyValue 所以产生了新的问题LeakValue
 

0x02 LazyBundle机制

 

1. LazyBundle机制

为了更加有效的解决self-changing Bundle的利用方式,在Android 13中通过调整了Parcel中数据的布局,引入了Lazy Bundle机制。具体来说就是不再每次拿到序列化数据时立刻进行反序列化获得全部的对象,而是根据需求进行获取数据
  • 如果没有数据获取需求则直接拿原本的序列化数据进行传递,而不需要先反序列化再序列化,即将序列化数据放入mParcelledData
  • 如果想获得其中的部分数据,则先部分反序列化到mMap中,可以通过查parcelable等不定长数据前的长度信息而跳过对应部分数据的反序列化,跳过这部分数据的反序列化的结果时这部分数据以LazyValue 对象保存在mMap对应键值对的值中。
  • 如果想获得具体的对象时才会触发对LazyValue对象的读取
notion image

2. 机制代码分析

可以看到,关键是对LazyValue 长度信息的获取,因此会在原本的数据布局中添加长度信息。在在补丁后的frameworks/base/core/java/android/os/Parcel.java 中说明了添加的LazyValue的具体布局
在从Parcel序列化对象中读取Bundle时会调用readFromParcelInner ,对序列化数据进行解析,如果Parcel存在ReadWriteHelper直接调用initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle) 进行反序列化mMap 赋值,由于开发者知道 parcel 生命周期不可控,因此接着调用了unparcel(true) 对底层的每个数据进行反序列化以解除对Parcel数据的依赖。
如果不存在ReadWriteHelper则不进行反序列化,直接将parcel的序列化数据放入mParcelledData 等待后续反序列化。
💡
Android Parcel提供字符串去重和对象去重
字符串的去重是通过重写 Parcel.ReadWriteHelper 类来实现的。默认的帮助器会直接从 Parcel 中读取字符串 (Parcel.readString())。也可以通过自定义实现来替换这种读取方式,例如,可以预先读取字符串池,并使用 readInt 来获取字符串池中字符串的索引。Parcel 提供了 hasReadWriteHelper() 方法,让调用者能够检测到去重机制是否处于活跃状态,并禁用与其不兼容的特性。
在 Parcel 中,另一个可用的去重机制是对象的压缩(squashing)。首先,需要通过 Parcel.allowSquashing() 来启用压缩功能。然后,当一个支持压缩的类被写入时,它首先会调用 Parcel.maybeWriteSquashed(this)。如果这个方法返回 true,那就意味着这个对象已经被写入到 Parcel 了,现在只写入到以前对象数据的偏移量。否则(如果压缩没有被启用,或者这是对象第一次被写入)它会写入零作为偏移量,以表示对象没有被压缩,并返回 false,告诉调用者他们应该写入实际的对象数据。
这两种去重机制可以帮助 Parcel更有效地存储数据,减少冗余,提高性能,尤其是在处理大量重复字符串或对象的情况下。
如果在之前并没有反序列化,到后面想要读取数据时例如调用getInt()获取Int型数据,或者调用isEmpty()size()等方法时会触发反序列化,调用unparcel() ,进而调用unparcel(/* itemwise */ false) 然后调用initializeFromParcelLocked 反序列化。实际上initializeFromParcelLocked 只会被调用一次,因为在调用结束后序列化数据就被解析保存到mMap 中,相应的mParcelledData 会被设置为null ,到此处还未涉及到LazyValue ,但是在这里initializeFromParcelLocked 参数被设置为true 代表要使用LazyBundle机制
在上面提到initializeFromParcelLocked ,在内部会调用readArrayMap 读取数据到map中,其会根据lazy 来判断是否需要按照Lazy模式读取,如果按照Lazy模式读取则会调用readLazyValue 进行读取,其实现就是通过isLengthPrefixed判断是否属于几种可进行Lazy加载的动态数据类型,如果属于则读取长度信息并且直接setDataPosition跳过,然后new一个LazyValue对象,该过程并不获取数据,而仅保存mSource mPosition mLength mType mLoader 等变量。
如果需要读取对应的值则会调用getValue ,进一步调用getValueAt ,然后回调用LazyValue对象的apply方法,然后在apply方法内部使用setDataPosition 将指针设置到序列化数据的对应位置,然后使用readValue 反序列化并读取数据。
 

3. 总结

简单总结一下就是下面这个图,单单观察就可以发现问题了,上面的分支明确将recycleParcel 属性设置为false ,也就是当前parcel可能被回收,但是还是使用了lazyBundle机制,也即将LazyValue 类型值引入了mMap中。当回收了parcel后再对LazyValue 数据进行访问时就会出现泄露问题,类似于Use-After-Free(UAF)。当然上面的叙述是简化版本,详细的说明需要看漏洞利用一节。
notion image
Android 13通过引入LazyBundle机制可以有效的解决self-changing Bundle的问题,但是如上面所说引入了新的问题,称之为LeakValue

0x03 漏洞分析

 

1. Parcel.recycle

Parcel本身是为了频繁的 IPC传输而设计的,因此对于其分配和释放通常使用手工管理的方式,以避免 Java 堆分配或者 GC 带来的性能损耗。在阅读系统源码或者 AIDL 生成的模版代码时都能发现,Parcel 使用 obtain 进行分配,使用 recycle 进行释放。
内存池中的 Parcel可以看做是一个表头为 sOwnedPool单链表结构obtain 每次从链表头获取一个对象节点,然后将链表头指针后移。如果池中没有空余的对象节点可用则new一个parcel
对于 IPC接收到的 Parcel数据分配方式略有不同,因为这些 Parcelnative层由系统创建,因此使用不同的链表,表头为 sHolderPool静态属性。这些 Parcel 通过构造函数Parcel(long nativePtr)去构建,生命周期由系统管理,即mOwnsNativeParcelObjectfalserecycle则会将回收的对象节点放置在链表头,作为新的链表头,在回收过程中会将两种情况区分处理。
这种手动内存管理为Java带来了类似Use-After-Free的错误的可能。
 

2. LeakValue

上面分析LazyBundle机制代码时说明了漏洞利用点:
机制总结一下就是下面这个图,单单观察就可以发现问题了,上面的分支明确将recycleParcel 属性设置为false ,也就是当前parcel可能被回收,但是还是使用了lazyBundle机制,也即将LazyValue 类型值引入了mMap中。当回收了parcel后再对LazyValue 数据进行访问时就会出现泄露问题,类似于Use-After-Free(UAF)
notion image
Parcel读取 LazyValue之后,将 Parcel 进行释放,而后再读取对应的 LazyValue,此时如果 Parcel被分配并填充了敏感数据,那么通过对LazyValue的读取就可以读出这些敏感内容,从而造成数据泄露。如果泄露的数据来自其他进程,且数据中包含特权的 IBinder 等结构,那么还可能造成提权或者 RCE 的危害。

0x04 漏洞利用

 

1. 利用条件

上面提到,在LazyBundle机制的示意图中上面的支路存在问题,也即parcel.hasReadWriteHelper() == true 时存在问题,除此之外由于使用unparcel(true) 对底层数据进行了反序列化,解除了对parcel的依赖,所以还需要绕过unparcel(true) 的反序列化。所以要想进行漏洞利用需要满足两点:
  • parcel.hasReadWriteHelper() == true
  • 绕过unparcel(true),使对底层的全部或部分数据进行反序列化失败,继续保持LazyValue的形式
对于第一点,可以在系统中寻找有读写HelperParcelable类将其添加到Bundle中进行序列化。RemoteViews类用于在Android中传递小部件到主屏幕等操作中。当读取其中嵌套的Bundle时,RemoteViews类会显式地设置ReadWriteHelper。这样做的原因是RemoteViews启用了压缩(squashing),以便对其中嵌套的ApplicationInfo对象进行去重,但这也可能导致Bundle内部的ApplicationInfo对象被压缩,因此不能延迟读取该Bundle,否则这些被压缩的对象将无法解压回原始状态,即确保Bundle内的数据在传递过程中保持不变,以避免压缩可能导致的数据损坏或错误。
对于第二点,在反序列化过程中对LazyValue的值进行获取时会调用object = ((BiFunction<Class<?>, Class<?>[], ?>) object).apply(clazz, itemTypes) ,如果反序列化失败则会抛出BadParcelableException 异常并且被捕获,如果sShouldDefusetrue 则不再继续抛出异常,防止了程序崩溃,直接返回null
 

2. 漏洞利用

因为该漏洞利用比较复杂,所以漏洞具体细节和POC还是看
LeakValue
michalbednarskiUpdated Mar 3, 2024
,这里只引用大佬的文章进行记录,后面有精力再回来复现
  1. UAF 漏洞的核心利用目标是获取目标应用的 IApplicationThread 句柄,这通常是应用启动初期使用 attachApplication() 传递给 system_server 的,主要用于让后者给应用发送四大组件的生命周期回调。通过滥用这个 Binder 句柄可以实现 RCE,参考 CVE-2021-0928 (scheduleReceiver);
  1. 为了能从 system_server 中泄露任意应用的 IApplicationThread 句柄,需要两个条件。首先是有一个接口可以发送 Parcelable 数据并将其取回,AppWidgetHost、Notification.contentView、Mediasession 都可以满足,作者选用的是 MediaSession.get/setQueue 接口;
  1. 其次,要使得 UAF 对象能被包含 IApplicationThread的 Parcel 复用,需要准确的时机。但是 attachApplication 只在应用启动时一个很小的时间窗口中被调用,因此作者寻找其中可能存在的锁去拓展这个窗口;
  1. ParceledListSlice是一个在 IPC 间传输的特殊数据结构,在其数据量小时可以同步传输,而对于大量的数据,会将其转换为 binder 发送给对方然后进行异步传输;
  1. ActivityManager.moveTaskToFront() 调用时可以提供 ActivityOptions 参数,以 Bundle 形式进行封装,并在 system_server端进行反序列化。因此攻击者可以在 Bundle 中加入 ParceledListSlice 类型的数据,从而在反序列化时回调到自身进行阻塞。该函数调用时候持有 mGlobalLock 锁,因此可以阻塞 attachApplication 的执行,从而更好地构造 UAF 风水;
  1. 由于调用了 moveTaskToFront,种种原因导致无法使用 startActivity 来启动目标应用,因此作者使用 ContentProvider 来启动目标应用。
  1. 在我们的 UAF LazyValue Parcel 被成功占位后,对应的 IApplicationThread 实际上在比较靠前的位置,但我们的 LazyValue 所在位置比较靠后因此无法读取到对应 binder。解决方案是可以找其他占位对象,不过作者这里利用了一个新的漏洞 CVE-2022-20474,即 readLazyValue 方法中 objectLenth 只检查了证书溢出,却没检查负数的情况,因此设置负数的 prefixLength 实际上可以读取 Parcel 前方的数据,从而绕过该限制。
其他还有亿点细节,就需要动手复现才能真正理解了。理论上该漏洞可以影响安全补丁在 2022-11 之前的 Android 13 设备。 攻击的目标应用可以是任意应用,因此可以选择 Settings.apk,其 UID=system,攻击成功后就相当于获取到了 system 权限。
 

0x05 漏洞修复

修复主要针对initializeFromParcelLocked 调用readArrayMap时的参数进行修改,将lazy 从默认为true修改为recycleParcel ,即如果存在读写Helper,则直接不使用LazyBundle机制。除此之外还删除了unparcel(true) ,因为在这种情况下不涉及LazyValue,所以无需再次unparcel(true)
notion image
 
除此之外还对漏洞利用过程中的另一个CVE进行了修复CVE-2022-20474Diff - 569c3023f839bca077cd3cccef0a3bef9c31af63^! - platform/frameworks/base - Git at Google (googlesource.com)
修复内容就是添加一个判断,防止负数读取前面部分的数据。
notion image
 

0x06 参考

  1. Android Parcel 系列系列漏洞整理 (一) | 失眠想睡觉的blog
  1. michalbednarski/LeakValue: Exploit for CVE-2022-20452
  1. Android 反序列化漏洞攻防史话 - evilpan
  1. Bundle数据结构和反序列化分析
  1. 深入理解Android Parcel-安卓-安卓笔记本
 
De1CTF-2020-Pwn-BroadCastTestAndroid反序列化漏洞-Bundle风水
  • Twikoo