type
status
date
slug
summary
tags
category
icon
password
0x01 引言
漏洞信息:编号:CVE-2022-20452
类型:Eop
影响范围:AOSP 13
为了解决自修改Bundle的问题,Android引入了
LazyBundle
机制,具体来说就是不再每次拿到序列化数据时立刻进行反序列化获得全部的对象,而是根据需求进行获取数据,在一定程度上缓解了自修改Bundle的问题。但是由于引入了LazyValue
所以产生了新的问题LeakValue
0x02 LazyBundle机制
1. LazyBundle机制
为了更加有效的解决
self-changing Bundle
的利用方式,在Android 13
中通过调整了Parcel中数据的布局,引入了Lazy Bundle
机制。具体来说就是不再每次拿到序列化数据时立刻进行反序列化获得全部的对象,而是根据需求进行获取数据- 如果没有数据获取需求则直接拿原本的序列化数据进行传递,而不需要先反序列化再序列化,即将序列化数据放入
mParcelledData
- 如果想获得其中的部分数据,则先部分反序列化到
mMap
中,可以通过查parcelable
等不定长数据前的长度信息而跳过对应部分数据的反序列化,跳过这部分数据的反序列化的结果时这部分数据以LazyValue
对象保存在mMap
对应键值对的值中。
- 如果想获得具体的对象时才会触发对
LazyValue
对象的读取
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)
。当然上面的叙述是简化版本,详细的说明需要看漏洞利用一节。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
数据分配方式略有不同,因为这些 Parcel
在native
层由系统创建,因此使用不同的链表,表头为 sHolderPool
静态属性。这些 Parcel 通过构造函数Parcel(long nativePtr)
去构建,生命周期由系统管理,即mOwnsNativeParcelObject
为 false
。recycle
则会将回收的对象节点放置在链表头,作为新的链表头,在回收过程中会将两种情况区分处理。这种手动内存管理为Java带来了类似
Use-After-Free
的错误的可能。2. LeakValue
上面分析LazyBundle
机制代码时说明了漏洞利用点:机制总结一下就是下面这个图,单单观察就可以发现问题了,上面的分支明确将recycleParcel
属性设置为false
,也就是当前parcel
可能被回收,但是还是使用了lazyBundle
机制,也即将LazyValue
类型值引入了mMap
中。当回收了parcel后再对LazyValue
数据进行访问时就会出现泄露问题,类似于Use-After-Free(UAF)
。
在
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
的形式
对于第一点,可以在系统中寻找有读写
Helper
的Parcelable
类将其添加到Bundle中进行序列化。RemoteViews
类用于在Android
中传递小部件到主屏幕等操作中。当读取其中嵌套的Bundle时,RemoteViews
类会显式地设置ReadWriteHelper
。这样做的原因是RemoteViews
启用了压缩(squashing
),以便对其中嵌套的ApplicationInfo
对象进行去重,但这也可能导致Bundle
内部的ApplicationInfo
对象被压缩,因此不能延迟读取该Bundle
,否则这些被压缩的对象将无法解压回原始状态,即确保Bundle内的数据在传递过程中保持不变,以避免压缩可能导致的数据损坏或错误。对于第二点,在反序列化过程中对
LazyValue
的值进行获取时会调用object = ((BiFunction<Class<?>, Class<?>[], ?>) object).apply(clazz, itemTypes)
,如果反序列化失败则会抛出BadParcelableException
异常并且被捕获,如果sShouldDefuse
为true
则不再继续抛出异常,防止了程序崩溃,直接返回null
2. 漏洞利用
因为该漏洞利用比较复杂,所以漏洞具体细节和POC还是看 ,这里只引用大佬的文章进行记录,后面有精力再回来复现
LeakValue
michalbednarski • Updated Mar 3, 2024
- UAF 漏洞的核心利用目标是获取目标应用的
IApplicationThread
句柄,这通常是应用启动初期使用attachApplication()
传递给system_server
的,主要用于让后者给应用发送四大组件的生命周期回调。通过滥用这个 Binder 句柄可以实现 RCE,参考 CVE-2021-0928 (scheduleReceiver);
- 为了能从
system_server
中泄露任意应用的IApplicationThread
句柄,需要两个条件。首先是有一个接口可以发送 Parcelable 数据并将其取回,AppWidgetHost、Notification.contentView、Mediasession 都可以满足,作者选用的是MediaSession.get/setQueue
接口;
- 其次,要使得 UAF 对象能被包含
IApplicationThread
的 Parcel 复用,需要准确的时机。但是attachApplication
只在应用启动时一个很小的时间窗口中被调用,因此作者寻找其中可能存在的锁去拓展这个窗口;
ParceledListSlice
是一个在 IPC 间传输的特殊数据结构,在其数据量小时可以同步传输,而对于大量的数据,会将其转换为 binder 发送给对方然后进行异步传输;
ActivityManager.moveTaskToFront()
调用时可以提供 ActivityOptions 参数,以 Bundle 形式进行封装,并在system_server
端进行反序列化。因此攻击者可以在 Bundle 中加入ParceledListSlice
类型的数据,从而在反序列化时回调到自身进行阻塞。该函数调用时候持有mGlobalLock
锁,因此可以阻塞 attachApplication 的执行,从而更好地构造 UAF 风水;
- 由于调用了
moveTaskToFront
,种种原因导致无法使用 startActivity 来启动目标应用,因此作者使用 ContentProvider 来启动目标应用。
- 在我们的
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)
除此之外还对漏洞利用过程中的另一个CVE进行了修复
CVE-2022-20474
: Diff - 569c3023f839bca077cd3cccef0a3bef9c31af63^! - platform/frameworks/base - Git at Google (googlesource.com)修复内容就是添加一个判断,防止负数读取前面部分的数据。
0x06 参考
- 作者:LLeaves
- 链接:https://lleavesg.top//article/CVE-2022-20452-LeakValue
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章