type
status
date
slug
summary
tags
category
icon
password
0x01 漏洞分析
该漏洞在Android 12 Developer Preview 3版本是上被发现,并在Android 12 release中就已经完成了修复。安全公告:Android 安全公告 - 2021 年 11 月 | Android 开源项目
1. Android广播
BroadcastReceiver
(广播接收器) 是Android APP的四大组件之一,APP可以通过在AndroidManifest.xml
文件中添加一条<receiver>
的声明来完成组件的声明或者通过动态注册的方式进行广播接收器的注册。其他APP通过构造一个Intent
并通过sendBroadcast
方法即可发送广播,实际上进行了进程间的通信,广播接收器则可以接收到广播和数据。当应用程序进程最初启动时调用
IActivityManager.attachApplication()
,以此来传递IApplicationThread
句柄,系统使用该句柄获得应用程序的控制权。当系统希望在应用程序进程中执行清单注册的
BroadcastReceiver
时,它使用IApplicationThread
句柄调用scheduleReceiver
方法。进而通过Binder
的transact
实现跨进程调用。下跨进程通过onTransact
处理请求,需要跨进程传递的参数都会被放到同一个Parcel对象中,按顺序依次读取各种参数,包括Intent
ActivityInfo
等。其中
ActivityInfo
包含有关要执行的组件的信息。此参数中传递的数据包括加载处理接收的广播的Java
类的文件路径。这时候你可能已经猜到这个新的漏洞路径是什么了:调用
sendBroadcast
传递一个Intent
,当系统尝试调用scheduleReceiver
时,将导致在调用scheduleReceiver
的应用程序中看到被篡改的ActivityInfo
。需要注意的是,这条新的漏洞路径仅在Android 12中可行,因为先前没有办法将任意的
Parcelable
放入Intent
中(Intent通过putExtras添加不算,因为这样会将数据放入Bundle中,Bundle序列化后被写入mParcelData
,在读取时不发生反序列化)而漏洞作者是用了一个在 Android 12
中加入的 Intent.ClipData
字段去实现的,该字段原本用于携带剪切板数据。sendMessage
后会有handleReceiver
进行处理。handleReceiver
调用getPackageInfoNoCheck
方法拿到packageInfo
,并在其中获取到需要拉起的receiver
对象并调用其onReceive
方法来完成BroadcastReceiver
组件的启动。继续深入
getPackageInfoNoCheck
,调用了getPackageInfo
漏洞利用思路就是通过
intent
参数的反序列化数据残留,间接地修改 info
参数,因为应用 B 会使用 ActivityInfo
中的数据去实例化代码,具体来说就是应用会通过传入的 ActivityInfo
构造 LoadedApk
,其中会使用内部的 zipPaths
创建 ClassLoader
,攻击者如果可以控制这个字段,就能实现针对应用 B 的任意代码执行。2. Parcel MisMatch Point
OutputConfiguration
类,其中有个序列化的字段sensorPixelModesUsed
是ArrayList<Integer>
类型,在反序列化该字段时,使用的是Parcel.readList
方法,其调用链路是:
Parcel.readList
Parcel.readListInternal
Parcel.readValue
最终是使用readValue
读取List
中每个元素的值。因此实际上可以读出任何readValue
支持的类型,比如Parcelable
、IBinder
等,并不局限于Integer
。仅仅进行反序列化并不会出现任何问题,只不过在使用具体的元素时,如果我们实际读取的类型无法转换为整数,就会出现ClassCastException
异常。
OutputConfiguration
在反序列化时用try-catch
包裹了createFromParcel
方法,这意味着如果在反序列化过程中抛出异常,对象的读取会提前结束,但之前写入的数据不会被读取,造成数据长度不匹配。在这个漏洞场景中,我们并不会使用这个数组的元素,因此我们可以指定任意的序列化类。又由于我们需要使目标类在序列化/反序列化过程产生不匹配,那么就需要找到一个类,使得该类可以在 system_server
中成功反序列化,但是在应用 B 中出现ClassNotFoundException
异常。0x02 漏洞利用
漏洞利用思路就是通过
intent
参数的反序列化数据残留,间接地修改 info
参数,因为应用 B 会使用 ActivityInfo
中的数据去实例化代码,具体来说就是应用会通过传入的 ActivityInfo
构造 LoadedApk
,其中会使用内部的 zipPaths
创建 ClassLoader
,攻击者如果可以控制这个字段,就能实现针对应用 B 的任意代码执行。接着上文
Parcel MisMatch Point
继续:- 上文提到为了让系统服务能序列化但应用不能反序列化,可以使用只在系统服务中存在的类,如
PackageManagerException
。但需要额外通过不指定ClassLoader
的readList
来绕过类加载约束,因为如果指定了ClassLoader
,那么应用B就可以在类加载器中找到对于的类,从而不会抛出异常,而如果没有指定ClassLoader,会默认只在BOOTCLASSPATH
中搜索类,而不会去system_server
类路径中查找,从而使system_server
也反序列化失败。原作者的利用是使用了PackageManagerException
,这是一个Serializable
类而不是Parcelable
。反序列化Serializable
对象时,会使用ObjectInputStream
,它在反序列化类时,会自动从堆栈轨迹中选择最近的非空非BootClassLoader的ClassLoader
。这里由于在系统服务的onTransact
等方法在堆栈中,ObjectInputStream
会优先使用包含system_server
类的ClassLoader
,所以可以成功加载到系统服务中定义的可序列化类。但在应用中反序列化该对象时,由于堆栈中没有系统服务的类,会导致ClassNotFoundException
。
- 实际利用中由于读取使用了带
classLoader
的readList
,因此到readSerializable
时loader
变量不为null
,从而不会调用resolveClass
在堆栈轨迹中寻找最近的loader,而是直接使用forName
加载类,从而导致直接抛出异常,因为当Class.forName
无法找到类时,它会抛出异常而不是返回null
。解决方案是将Serializable
再套一层Parcelable
,使用不带ClassLoader
的readList
去进行反序列化。这里选用的是WindowContainerTransaction
类。因此有了下面的构造对象方法。
这个类加载器在堆栈trace中存在,是因为在
ActivityManagerService
中重写的onTransact
方法存在,而不是Binder#execTransact()
、IActivityManager$Stub#onTransact()
(由AIDL生成),以及所有使用的Parcelable
类中的方法存在。- 目标是在
Intent
对象中触发反序列化长度不匹配,因为Intent会在系统服务的onTransact
方法中作为第一个参数传递,这样就可以修改第二个参数ActivityInfo
。但是上文提到这条攻击链路仅在Android12
中可行,因为先前没有办法将任意的Parcelable
放入Intent
中。在Android12-bete
中的在ClipData$Item
中新增了一个字段mActivityInfo
,在ClipData
构造方法中通过in.readTypedObject(ActivityInfo.CREATOR)
读取。
ActivityInfo
继承自ComponentInfo
,并具有applicationInfo
字段。在ActivityInfo(Parcel source)
构造函数中,无法直接放置自定义Parcelable
对象,但是ApplicationInfo
中有一个字段splitDependencies
,它是SparseArray<int[]>
类型。在读取splitDependencies
时,使用了readSparseArray
方法,该方法又使用readValue
方法来读取SparseArray
。可以将OutputConfiguration
放置在splitDependencies
中。
- 但后面还有一些
readString
调用,为了完全控制不匹配后的未读数据,可以OutputConfiguration
中放一个Bundle
类型的原始数据,然后用ZenPolicy
类将OutputConfiguration
包裹起来,以在反序列化时跳过Bundle
的前三个int
字段VAL_BUNDLE
、长度和魔术字)。这样当触发PackageManagerException
时会停止继续正常反序列化,而留下了Bundle
数据,再由ZenPolicy
反序列化过程中后三个readInt
读取Bundle
字段的三个int
字段,并且将后续内容暴露出来,_arg114 = (ActivityInfo) ActivityInfo.CREATOR.createFromParcel(data);
继续读取剩下的Bundle
数据作为ActivityInfo
数据
handleReceiver
调用了getPackageInfoNoCheck
,并传入了可以被攻击者控制的ApplicationInfo
。getPackageInfoNoCheck
会根据ApplicationInfo
生成一个LoadedApk
实例。这里攻击者传入一个新的packageName
,以确保会创建新实例。然后调用ContextImpl.getClassLoader
,首次调用会代理到LoadedApk
的getClassLoader
,然后会调用createOrUpdateClassLoaderLocked
。createOrUpdateClassLoaderLocked
通过makePaths
生成类加载的路径zipPaths
,其中主要使用了ApplicationInfo
中的sourceDir
信息。攻击者控制的ApplicationInfo
将sourceDir
设置为攻击者的apk路径。 因此makePaths
将生成包含攻击者apk
的zipPaths
,交给createClassLoader
加载。 最终BroadcastReceiver
会从攻击者的apk
中加载类并执行,实现了代码执行。
0x03 漏洞修复
这个漏洞本身对终端用户的影响不是很大,毕竟只在Android 12 Preview
版本中就修复了。但通过这个漏洞,Google 引入了许多修复和缓释方案,直接影响了后续的漏洞挖掘和利用思路。首先针对漏洞本身,修复方案为:
- 对上述类去除隐式的异常处理,修复读写不一致的问题;
- 使用
readIntArray
而不是readList/readValue
去读取数据,消灭类型擦除的副作用;
- 防止
ClipData.mActivityInfo
写入Parcel
,除非显式指定。这消除了向 Intent 写入任意 Parcelable 的一个攻击链路;另外,在 Andorid 13 中,引入了更强的反序列化缓释方案:
- 新增了一个
readListInternal
方法的重载,增加额外的Class
参数,显式指定读取列表的元素的类型,并且将原来的方法标记为@Deprecated
;
- 新增了
Parcel.enforceNoDataAvail
方法,用于确保反序列化结束后,Parcel 中不再存在多余的数据;回想上一节中 Bundle 风水的利用,实际上第三个元素在第二次反序列化中是多出来的,因此这个修改会导致上述 Bundle 风水的失败;当然也有一些绕过的手法,比如通过更复杂的风水使得第二次反序列化能够到 Parcel 的末尾即可;
LazyBundle patch
,在 LazyBundle 实现中,Parcelable
、List
等类型会在序列化数据的元素开头单独存储长度信息。不过,这个 patch 并不会影响非 Bundle 造成的反序列化漏洞,比如这个漏洞。
0x04 参考
- 作者:LLeaves
- 链接:https://lleavesg.top//article/CVE-2021-0928
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章