type
status
date
slug
summary
tags
category
icon
password

0x01 引言

因为去年的某厂商利用其主流APP中的漏洞对Android设备进行大面积的攻击使用了这一漏洞,对这个问题比较感兴趣,阅读一些相关文章后决定结合理解进行整理再做记录。
 

 
 

0x02 前置知识

 

1. Parcel

Parcel是一个用于存储对象序列化表示的容器createFromParcel()方法从Parcel中读取对象的数据并重建对象。
notion image
Parcel 可以支持按照一定顺序写入和读取 intlong等原子数据,也支持 StringIBinder、和 FileDescriptor这些复杂的数据结构。Parcel 中还可以包含Parcelable对象。Parcel主要用于跨进程通信时的数据传输
更详细的关于Parcel的信息:Android 反序列化漏洞攻防史话 - evilpan

2. Parcelable

Parcelable是一个接口,允许对象在应用程序的组件之间进行序列化和传递,或者在不同应用程序之间传递。相比使用Java的Serializable接口,它生成较少的中间对象,从而提高了性能。
要使一个对象在Android中可以Parcelable,需要实现Parcelable接口,并提供writeToParcel()createFromParcel()等方法的实现。writeToParcel()方法负责将对象的数据写入Parcel对象
notion image

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)实现
notion image
notion image

0x03 漏洞分析

 

1. Parcel Mismatch

Parcel Mismatch也即读写不匹配问题,一般情况下序列化和反序列化的数据操作应当是一一对应例如先写一个Int数据,然后写一个字节数组,序列化后发送,接收端应当先读一个Int数据,然后再读一个字节数组,如果读写不匹配,就会导致下一个部分乃至下下个部分或将直到最后一个部分的的数据被读取时被错误的解析。
notion image
下面的Vuln 实际是一个有该问题的Parcelable 类,其中写的时候会写一个int值和一个字节数组,但是读的时候如果字节数组长度为0则不进行读取造成Parcel Mismatch
创建一个Bundle 实例,然后填充数据,完成后序列化Bundle,然后反序列化。
其对应的序列化的二进制数据如下图,其中蓝色区域为Parcelable 值域,0x44开始的四字节为parcel.writeInt(data.length)写入的0,后面的四字节0代表parcel.writeByteArray(data) 并未将任何数据写入数组,即数组长度为0。
notion image
当运行这段代码时就会触发异常,报错为:
java.lang.RuntimeException: Parcel android.os.Parcel@c2ec64: Unmarshalling unknown type code 49 at offset 72 ,意思就是在发序列化时解析错位了,导致将72位置(不包括长度字段和魔术字字段)的49识别为了一个类型code,但是实际是第二个键值对的键的字符串"1"
notion image
💡
如果修改上面的Vuln类,使其字节数组并不为空,size设置为1,内容为0x12,其序列化结果的二进制表现如下图所示。其中蓝色区域为Parcelable 值域。此时不再产生异常,即不存在Parcel Mismatch问题。
notion image
 

2. Self Changing Bundle

Self Changing BundleBundle自修改,回想之前提到的LunachAnyWhere漏洞LaunchAnyWhere漏洞分析 | LLeaves Blog ,该漏洞利用利用过程中进行了两次序列化和两次反序列化,而漏洞的修复就是在第一次反序列化后检查携带的数据中 是否存在{KEY_INTENT:intent}数据,如果存在就会验证签名。如果预先构造一个带有跳转Intent的序列化数据,在中途经过system_server 时使其无法正常检测到该数据,然后发送给AppA,AppA反序列化后解析出了跳转Intent进行利用,则绕开了中途的签名检测。这就是自修改Bundle的一个简单的例子。
notion image
以下面的代码为例,构造序列化数据,使得键值对{"invisible":"I appear from thin air"}在第一次反序列化时不被解析,而在第二次反序列化时被解析。
分析这段代码构造的序列化数据的二进制形式,其中有三个键值对,键区域以绿色表示,值区域以红色表示每个键值对中的值部分的类型用蓝色字体标出中第二个键值对中隐藏数据,实现方式就是通过构造一个字节数组来隐藏(类型code为0xD,也即private static final int VAL_BYTEARRAY = 13)。
  • 当第一次序列化时,原本序列化第一个键值对部分的Parcelable时 除了类名外还需要填充两部分的内容,第一部分为data.length,第二部分为data,具体的填充方法上面叙述过,所以图中黑框位置原本应当是8字节的0 ,但是此时只填充了4字节0,因此会导致两次反序列化结果不一致。
    • notion image
  • 第一次反序列化时,对应的Bundle键值对如下图所示。其中第二个键值对的键ASCII码为0x6 ,因此无法正常显示,在这显示为[ACK]
    • notion image
  • 第二次序列化时,查看其序列化数据的二进制Hex形式,可以发现此时的二进制数据在Parcelable 部分的值中较上一次多填充了4字节的0 ,这才是是正确的填充方式,所以一切就很明了了,就是这部分在干扰解析。
    • notion image
  • 第二次反序列化,由于上次的序列化数据是正确的,但是由于存在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"}到此为止由于也即解析出了三个键值对,所以停止解析,后面的部分不再参与解析。
    • 因此对应的反序列化结果为下图所示
      notion image
 
💡
为了更好的解析序列化二进制数据,写了一个简单的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被吞掉,暴露出后面的键值对。
notion image
notion image
💡
这里还添加了Intent的额外参数confirm_credentials = false ,能够绕过PIN验证而直接修改密码。
启动App将会直接导致绕过Pin验证而修改设备密码,修复方法则是将writeLong改为writeInt
notion image
 
 

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未被读出,而被反序列化为下一个键的键长度,然后后面的内容都会偏移,导致后面的恶意键值对暴露出来。
所以构造恶意的序列化数据,示意图如下图
notion image
notion image
notion image
修复方法上面已经说过了if (numChains > 0) { 改为if (numChains >= 0) { 避免读写不一致问题。
💡
在构造过程中如果会遇到hashcode的问题,需要谨慎构造键值对的key值,因为Bundle是一个类似于HashMap的结果,会根据键值对的键计算的hashcode而存储调整顺序,所以需要仔细构造Key值,避免顺序被调整,造成无法正常反序列化的问题。

 

0x05 漏洞修复

漏洞的修复思路有三种:
  • 修复不匹配的读写,虽然有效但是仅限于当前发现的不匹配漏洞,攻击者依旧可以寻找其他不匹配问题进行漏洞利用
  • 修复TOCTOU 漏洞本身,即确保检查和使用的反序列化对象是相同的,但这种修复方案也无法未能从根本上解决问题,因为攻击者也可以通过寻找其他的可攻击点进行绕过。
    • notion image
  • 完善Bundle的设计,使其具有更好的安全性。
    • 虽然 Bundle 本身是 ArrayMap结构,但在反序列化时候即便只需要获取其中一个 key,也需要把整个 Bundle 反序列化一遍。这其中的主要原因在于序列化数据中每个元素的大小是不固定的,且由元素的类型决定,如果不解析完前面的所有数据,就不知道目标元素在什么地方。
      为此在 21 年左右,AOSP 中针对 Bundle 提交了一个称为 LazyBundle(9ca6a5)的 patch。其主要思想为针对一些长度不固定的自定义类型,比如 ParcelableSerializableList等结构或容器会在序列化时将对应数据的大小添加到头部。这样在反序列化时遇到这些类型的数据,可以仅通过检查头部去选择性跳过这些元素的解析,而此时 sMap 中对应元素的值会设置为 LazyValue,在实际用到这些值的时候再去对特定数据进行反序列化。
      这个 patch 可以在一定程度上缓释针对 Bundle 风水的攻击,而且在提升系统健壮性也有所助益因为即便对于损坏的 Parcel 数据,如果接收方没有使用到对应的字段,就可以避免异常的发生。对于之前的 Bundle 解析策略,哪怕只调用了 size 方法,也会触发所有元素的解析从而导致异常。 在这个 patch 中 unparcel 还增加了一个 boolean参数 itemwise如果为 true 则按照传统方式解析每个元素,否则就会跳过 LazyValue 的解析。
      notion image
 

0x06 参考

  1. 再谈Parcelable反序列化漏洞和Bundle mismatch
  1. Android parcels: the bad, the good and the better - Introducing Android’s Safer Parcel
  1. Android 反序列化漏洞攻防史话 - evilpan
  1. Parcelable为什么速度优于 Serializable
  1. Bundle Fengshui - Android's self changing Bundle
  1. CVE-2023-20963 WorkSource Parcelable反序列化漏洞分析 – 小路的博客
 
CVE-2022-20452-LeakValueLaunchAnyWhere漏洞分析
  • Twikoo