type
status
date
slug
summary
tags
category
icon
password
0x01 引言0x02 前置知识1. Parcel2. Parcelable3. Bundle0x03 漏洞分析1. Parcel Mismatch2. Self Changing Bundle0x04 漏洞利用-结合LaunchAnyWhere1. CVE-2017-13288 2. CVE-2023-209630x05 漏洞修复0x06 参考
0x01 引言
因为去年的某厂商利用其主流APP中的漏洞对
Android
设备进行大面积的攻击使用了这一漏洞,对这个问题比较感兴趣,阅读一些相关文章后决定结合理解进行整理再做记录。0x02 前置知识
1. Parcel
Parcel
是一个用于存储对象序列化表示的容器。createFromParcel()
方法从Parcel
中读取对象的数据并重建对象。Parcel
可以支持按照一定顺序写入和读取 int
、long
等原子数据,也支持 String
、IBinder
、和 FileDescriptor
这些复杂的数据结构。Parcel
中还可以包含Parcelable
对象。Parcel
主要用于跨进程通信时的数据传输更详细的关于
Parcel
的信息:Android 反序列化漏洞攻防史话 - evilpan2. Parcelable
Parcelable
是一个接口,允许对象在应用程序的组件之间进行序列化和传递,或者在不同应用程序之间传递。相比使用Java的Serializable
接口,它生成较少的中间对象,从而提高了性能。要使一个对象在
Android
中可以Parcelable
,需要实现Parcelable
接口,并提供writeToParcel()
和createFromParcel()
等方法的实现。writeToParcel()
方法负责将对象的数据写入Parcel
对象3. Bundle
Bundle
是Android提供的一种数据结构,用于在Activity、Fragment和Service等组件之间传递数据。它是一个键值对的集合,可以将各种数据类型(如字符串、整数、布尔值、Parcelable对象等)存储在其中。Bundle
序列化时会调用writeToParcelInner
,写入一个值占4字节用于序列化完成后填充长度,然后写入BUNDLE_MAGIC
,即Bundle
魔术字,然后调用writeArrayMapInternal
将Bundle内键值对写入Parcel
对象。写入规则是先写入Bundle内键值对的数量,然后逐个写入键值对
writeString(val.keyAt(i));
writeValue(val.valueAt(i));
对于键值对中的键的写入会调用
android_os_Parcel_writeString16
,先写入四字节的长度,然后对于每个字符以2个字节的宽度写入。对于键值对中的值则通过writeValue
写入,先写入四字节的类型信息,然后写入具体的值。因此对于序列化的Bundle,其内存布局应当为下图所示
其中Length字段不包括Length字段本身和Bundle Magic字段,因此会较实际序列化后二进制数据长度小8B
另外对于String数据,包括键值对中的键,都会进行四字节对齐,对齐原则是:String字符串后会最后有两字节的
00 00
表征字符串的结束,在计算对齐需要补充的字节数时会将结尾的2字节00 00
也考虑进去, 如果此时未四字节对齐则再补充两字节00 00
,否则无需补充。注意:结尾的两字节是填充对齐完成后添加,此时刚好会对齐。详见status_t writeString16(const char16_t* str, size_t len)
实现0x03 漏洞分析
1. Parcel Mismatch
Parcel Mismatch
也即读写不匹配问题,一般情况下序列化和反序列化的数据操作应当是一一对应的,例如先写一个Int数据,然后写一个字节数组,序列化后发送,接收端应当先读一个Int数据,然后再读一个字节数组,如果读写不匹配,就会导致下一个部分乃至下下个部分或将直到最后一个部分的的数据被读取时被错误的解析。下面的
Vuln
实际是一个有该问题的Parcelable
类,其中写的时候会写一个int
值和一个字节数组,但是读的时候如果字节数组长度为0
则不进行读取造成Parcel Mismatch
创建一个
Bundle
实例,然后填充数据,完成后序列化Bundle,然后反序列化。其对应的序列化的二进制数据如下图,其中蓝色区域为
Parcelable
值域,0x44开始的四字节为parcel.writeInt(data.length)
写入的0,后面的四字节0代表parcel.writeByteArray(data)
并未将任何数据写入数组,即数组长度为0。当运行这段代码时就会触发异常,报错为:
java.lang.RuntimeException: Parcel android.os.Parcel@c2ec64: Unmarshalling unknown type code 49 at offset 72
,意思就是在发序列化时解析错位了,导致将72
位置(不包括长度字段和魔术字字段)的49
识别为了一个类型code
,但是实际是第二个键值对的键的字符串"1"
。如果修改上面的Vuln类,使其字节数组并不为空,size设置为
1
,内容为0x12
时,其序列化结果的二进制表现如下图所示。其中蓝色区域为Parcelable
值域。此时不再产生异常,即不存在Parcel Mismatch
问题。2. Self Changing Bundle
Self Changing Bundle
即Bundle自修改,回想之前提到的LunachAnyWhere漏洞LaunchAnyWhere漏洞分析 | LLeaves Blog ,该漏洞利用利用过程中进行了两次序列化和两次反序列化,而漏洞的修复就是在第一次反序列化后检查携带的数据中 是否存在{KEY_INTENT:intent}
数据,如果存在就会验证签名。如果预先构造一个带有跳转Intent的序列化数据,在中途经过system_server
时使其无法正常检测到该数据,然后发送给AppA,AppA反序列化后解析出了跳转Intent进行利用,则绕开了中途的签名检测。这就是自修改Bundle的一个简单的例子。以下面的代码为例,构造序列化数据,使得键值对
{"invisible":"I appear from thin air"}
在第一次反序列化时不被解析,而在第二次反序列化时被解析。分析这段代码构造的序列化数据的二进制形式,其中有三个键值对,键区域以绿色表示,值区域以红色表示,每个键值对中的值部分的类型用蓝色字体标出。中第二个键值对中隐藏数据,实现方式就是通过构造一个字节数组来隐藏(类型code为
0xD
,也即private static final int VAL_BYTEARRAY = 13
)。- 当第一次序列化时,原本序列化第一个键值对部分的
Parcelable
时 除了类名外还需要填充两部分的内容,第一部分为data.length
,第二部分为data
,具体的填充方法上面叙述过,所以图中黑框位置原本应当是8
字节的0
,但是此时只填充了4
字节0
,因此会导致两次反序列化结果不一致。
- 第一次反序列化时,对应的Bundle键值对如下图所示。其中第二个键值对的键ASCII码为
0x6
,因此无法正常显示,在这显示为[ACK]
- 第二次序列化时,查看其序列化数据的二进制Hex形式,可以发现此时的二进制数据在
Parcelable
部分的值中较上一次多填充了4
字节的0
,这才是是正确的填充方式,所以一切就很明了了,就是这部分在干扰解析。
- 第二次反序列化,由于上次的序列化数据是正确的,但是由于存在
Parcel Mismatch
问题,而data.length = 0
所以不会通过readByteArray
读取字节数组,所以在解析的时候会将多填充的第二部分的四字节0解析为下一个键值对的键的长度,即0,继而将01 00 00 00
四字节解析为该键值对的键,06 00 00 00
解析为键值对的值的类型VAL_LONG
,将0D 00 00 00 50 00 00 00
解析为一个long
类型的数字,到此这个键值对解析完成,再往后解析就会出现隐藏的键值对,即09 00 00 00
为下一个键值对键的长度,再往后正常解析得到{"invisible":"I appear from thin air"}
,到此为止由于也即解析出了三个键值对,所以停止解析,后面的部分不再参与解析。
因此对应的反序列化结果为下图所示
为了更好的解析序列化二进制数据,写了一个简单的010 editor模板,能够实现基本数据类型解析
0x04 漏洞利用-结合LaunchAnyWhere
上面的自修改
Bundle
部分提到了利用自修改Bundle
实现LunachAnyWhere
漏洞,主要利用自修改Bundle
绕过system_server
对序列化数据中Intent
的检测。但是有一个问题:在进程之间发送时数据时,Android官方建议不要使用自定义
Parcelable
。如果将一个自定义 Parcelable
对象从一个应用发送到另一个应用,则需要确保发送和接收的应用上都存在版本完全相同的自定义类。通常情况下,这可能是在两个应用中都会使用的通用库。如果应用尝试向系统发送自定义 Parblelable
,则可能会发生错误,因为系统无法对其不了解的类进行解组,同时由于LaunchAnyWhere
的反序列化过程是由system_server
完成,所以只能找系统已知的Parcelable
。所以需要找系统本身存在的Parcelable
接口是否存在Parcel MisMatch
问题,而不能自己构造存在问题的Parcel MisMatch
部分
Parcel MisMatch
问题CVE如下所示1. CVE-2017-13288
影响范围
Android 8.0 - 8.1
问题在于
android.bluetooth.le.PeriodicAdvertisingReport
类,可以看到在writeToParcel
中通过dest.writeLong(txPower)
写了一个Long类型数据(8B),但是在readFromParcel
读取时是由txPower = in.readInt()
读取一个Int类型数据(4B),造成Parcel MisMatch
问题。那么就可以利用读写之间的
4B
差来隐藏Intent
数据,下图是三个不同角色拿到序列化数据进行操作的视角,可以看到System_server
拿到序列化数据(System_server_read
)后未能检测出以KEY_INTENT
为键的键值对,然后进行第二次序列化得到System_server_write
的数据,而Settings
拿到后(Settings_read
)由于读写不一致,顺利解析出以KEY_INTENT
为键的键值对,具体而言Settings
拿到序列化数据后解析时会将txPower
的后四个字节解析为rss
的值,从而导致后面解析都存在4B的偏移,解析到字节数组的长度时拿到的数据其实是flag = 1
,也就是说字节数组长度被解析为1,由于字节数组补齐4B,所以data.length被吞掉,暴露出后面的键值对。这里还添加了Intent的额外参数
confirm_credentials = false
,能够绕过PIN验证而直接修改密码。启动App将会直接导致绕过Pin验证而修改设备密码,修复方法则是将
writeLong
改为writeInt
2. CVE-2023-20963
影响范围
AOSP versions 11, 12, 12L, 13 with security patch levels prior to March 2023.
查看其修复内容为if (numChains > 0) {
改为if (numChains >= 0) {
Parcelable
反序列化类漏洞中,size
处理的问题是比较常见的,这个也不例外。在第一次read的时候,攻击者使得numChains
读取到int
值为1,然后进到里面readParcelableList
直接放一个-1
代表null
,这样在new ArrayList
的时候就给mChains
创建了一个长度为1的ArrayList
,但是并未add
任何成员,这里面读取了2个int
,8个字节。但是在第二次序列化时
writeToParcel
mChains.size实际是0,而非1,因此会先写入一个Int型的0,writeParcelableList
也会写入一个ArrayList
的长度0,不过this.mChains里面并没有内容,所以不会再继续写入。因此写入了两个Int型的0 。第二次反序列化时读取时先读取到
numChains = 0
,从而不继续读下去,导致第二个Int型0未被读出,而被反序列化为下一个键的键长度,然后后面的内容都会偏移,导致后面的恶意键值对暴露出来。所以构造恶意的序列化数据,示意图如下图
修复方法上面已经说过了
if (numChains > 0) {
改为if (numChains >= 0) {
避免读写不一致问题。在构造过程中如果会遇到
hashcode
的问题,需要谨慎构造键值对的key值,因为Bundle
是一个类似于HashMap
的结果,会根据键值对的键计算的hashcode
而存储调整顺序,所以需要仔细构造Key值,避免顺序被调整,造成无法正常反序列化的问题。0x05 漏洞修复
漏洞的修复思路有三种:
- 修复不匹配的读写,虽然有效但是仅限于当前发现的不匹配漏洞,攻击者依旧可以寻找其他不匹配问题进行漏洞利用
- 修复
TOCTOU
漏洞本身,即确保检查和使用的反序列化对象是相同的,但这种修复方案也无法未能从根本上解决问题,因为攻击者也可以通过寻找其他的可攻击点进行绕过。
- 完善Bundle的设计,使其具有更好的安全性。
虽然Bundle
本身是ArrayMap
结构,但在反序列化时候即便只需要获取其中一个 key,也需要把整个 Bundle 反序列化一遍。这其中的主要原因在于序列化数据中每个元素的大小是不固定的,且由元素的类型决定,如果不解析完前面的所有数据,就不知道目标元素在什么地方。为此在 21 年左右,AOSP 中针对 Bundle 提交了一个称为LazyBundle(9ca6a5)
的 patch。其主要思想为针对一些长度不固定的自定义类型,比如Parcelable
、Serializable
、List
等结构或容器,会在序列化时将对应数据的大小添加到头部。这样在反序列化时遇到这些类型的数据,可以仅通过检查头部去选择性跳过这些元素的解析,而此时sMap
中对应元素的值会设置为LazyValue
,在实际用到这些值的时候再去对特定数据进行反序列化。这个 patch 可以在一定程度上缓释针对 Bundle 风水的攻击,而且在提升系统健壮性也有所助益,因为即便对于损坏的 Parcel 数据,如果接收方没有使用到对应的字段,就可以避免异常的发生。对于之前的 Bundle 解析策略,哪怕只调用了 size 方法,也会触发所有元素的解析从而导致异常。 在这个 patch 中 unparcel 还增加了一个boolean
参数itemwise
,如果为 true 则按照传统方式解析每个元素,否则就会跳过 LazyValue 的解析。
0x06 参考
- 作者:LLeaves
- 链接:https://lleavesg.top//article/Bundle-Fengshui
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章