回顾 Android 的发展历程,各种反序列化相关的漏洞曾多次被公开披露和修复。
其中最具代表性的当属 Bundle Mismatch 相关的漏洞,它即是深蓝洞察去年发布的 2022 年度最“不可赦”的漏洞利用所揭示的,堪称史上最大规模的在野攻击所利用的核心漏洞。
Google 引入了新机制缓解此类漏洞,就当人们认为一切已经尘埃落定之时,新的绕过还是到来了。
以下为本期深蓝洞察年度安全报告的第五篇。
05
Bundle, 是 Android 用于数据传递的一种特殊的键值对容器类型,通常用于 Android 进程间通信。
Bundle Mismatch 则利用序列化和反序列化不一致的差异绕过系统校验,获取以系统权限启动任意 Activity 的能力(LaunchAnyWhere)。
为了彻底解决 Bundle Mismatch 问题,Google 设计了一套名为 Lazy Bundle 的补丁。该补丁调整了不定长类型的序列化结构,在类型之后、具体数据之前,增加了一个数据块的长度字段。
__Lazy Bundle 的“lazy” 体现在,当 Bundle 反序列化遇到了不定长类型的值时,不直接对具体数据进行读取,而是创建一个 LazyValue 作为值,并记录下该数据在整个 parcel 中的偏移和数据的完整长度,然后跳过相应长度读取下一组键值对。
只有明确需要取出某个 LazyValue 的真实值时,才会跳回到记录的偏移处进行反序列化,最后将 LazyValue 替换为真实值。
AOSP 的源码注释中标注了相应的数据结构和 LazyValue 的参数以便人们理解,其中 mPosition
和 mLength
即 LazyValue 记录的偏移和长度。
Lazy Bundle 于 2022 年推出的 Android 13 中实装。
如此一来,即使某个类型存在 Mismatch,Bundle 其他键值对的内容也不会受影响,缓解了 Bundle Mismatch 的利用。
Bundle Mismatch 利用的作者是 Michal Bednarski(@BednarTildeOne)。自 2017 年起,他在 GitHub 上分享了多种 Android 反序列化漏洞的利用和 writeup。
这一次,他不仅找到了 LazyValue 实现中存在的 UAF 漏洞,随后更是发现了一个可以绕过 Lazy Bundle 缓解措施的漏洞 "TheLastBundleMismatch (CVE-2023-21098、CVE-2023-45777)" ,在 Android 13 和 14 上成功重现了 LaunchAnyWhere 攻击。
通过分析 CVE-2023-45777 的补丁容易发现,TheLastBundleMismatch 漏洞在于 AccountManagerService
读取 intent 字段时并没有显式指定 getParcelable
的类型参数。
_Android 13 弃用了 Parcel 和 Bundle 中原先可以反序列化任意类型的一系列方法,取而代之的是增加了 clazz 参数限制类型的版本。_补丁即是使用更为健壮的函数替换了弃用版本。
`diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 7a19d034c2c8..5238595fe2a2 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -4923,7 +4923,7 @@ public class AccountManagerService p.setDataPosition(0); Bundle simulateBundle = p.readBundle(); p.recycle(); - Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT); + Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class); if (intent != null && intent.getClass() != Intent.class) { return false; } `
该漏洞的利用和 LazyValue 的实现弱点有关。
当一个 Bundle 重新序列化时,尚未被读出的 LazyValue 会使用 appendFrom
直接将其指向的对应数据块(包括开头的 type
和 length
两个字段)复制到新的 parcel 中。
乍一看,这种处理方式似乎没有什么不妥之处,但如果攻击者有能力修改旧 parcel 中 type
和 length
两处数据,让 LazyValue 复制改动后的数据,事情就变得不一样了:
在 Bundle 下一次被反序列化时,修改的 type
可以让这个 LazyValue 不再 “lazy”,从而改变读取后续键值对的位置,读出原本不存在的键,实现 Lazy Bundle 的绕过;修改 length
也可以达到同样的效果。
那么问题就转化为,是否存在这样一个 Parcelable 类型,其在反序列化的过程中会修改原始 parcel 的数据?
这种类型(至少在 AOSP 中)并不存在。
不过 @BednarTildeOne 找到了一个等效的组合:android.os.PooledStringWriter
和 android.content.pm.PackageParser$Activity
。
前者并非 Parcelable 类型,但是却可以接受 Parcel 类型作为构造函数的参数。作为一个 “writer”,它向传入的 parcel 调用了 writeInt(0)
的写入操作。
后者是一个 Parcelable,在反序列化时可以使用反射调用任意类型的构造函数,并传入 parcel 对象,实现了和 PooledStringWriter
的串联。
如果构造数据让写入的 0
覆盖 LazyValue 的 type
,就能让 Bundle 下一次反序列化时将这个 LazyValue 解释成字符串类型,绕过缓解措施。
美中不足的是,PackageParser$Activity
会将反射构造的结果转换为 IntentInfo
类型,不可避免地导致类型转换异常。因此需要寻找另一个包含异常处理的 Parcelable,在它反序列化的 try-catch 流程中进行 PackageParser$Activity
和 PooledStringWriter
的反序列化。
这一次 AOSP 既不存在这样的 Parcelable,也不存在其他的等效组合。这可能要归功于上文提到的 Android 13 新增限制类型的 Parcel 方法,即使有少数几个 Parcelable 使用了异常处理,也无法控制它们去反序列化我们想要的类型。
AOSP 中不存在,不代表其他厂商 OEM 系统中不存在。
在 2022 年度最“不可赦”的漏洞利用 中,我们介绍了一个三星 OEM 的漏洞利用链。这次 @BednarTildeOne 也是在三星中发现了 SemImageClipData
类型完美满足利用要求。
事情结束了吗?
还差最后一小步。
如果直接在 intent 字段放一个 SemImageClipData
类型,反序列化的结果会被 AccountManagerService
强制转换为 Intent
,再次出现异常。bundle.getParcelable
内部实际上有一层类型转换的错误处理,只要把 SemImageClipData
套一层数组成为 Parcelable[]
,函数最终就可返回null
。
至此,整个攻击利用链大功告成,其复杂程度和精妙程度令人叹为观止。
只可惜 SemImageClipData
这种类型不可多得。DARKNAVY 在尝试对国内品牌的旗舰机型复现漏洞时,并不能找到相似的替代品。
考虑到漏洞的核心在于反序列化时对 parcel 的修改操作,我们放弃 PooledStringWriter
和 PackageParser$Activity
的组合拳,去寻找反序列化时直接修改 parcel、并且不会触发异常的类型。
最终我们找到一个 Parcelable,直接调用 unmarshall
修改了整个 parcel 的数据,借此成功完成了复现。
在复现中,我们利用漏洞分别启动了修改密码的页面和 Android 14 的彩蛋页面。这两个页面分别是设置应用和安卓系统本身的未导出 Activity。
该机型最新版本上已经修复此漏洞。值得一提的是,我们利用的这个 Parcelable 并非第一次出现在该品牌的手机中。在其他机型的 Android 12 上,我们就发现过它的存在。
而,TheLastBundleMismatch 在 Android 12 及之前并不能被利用:
原本 parcel 的读取是线性的,如果读取的途中修改了 parcel 数据,也只能在新旧数据之间二选一读到。LazyValue 的出现改变了这种线性,也让这个潜藏多年的类型能够被利用。
在 2014 年,为了修复 LaunchAnyWhere 漏洞,Android 增加了对 intent 的校验,于是 2017 年出现了 Bundle Mismatch 绕过校验。
2022 年新的 Lazy Bundle 修复 Bundle Mismatch,又在一年后被 TheLastBundleMismatch 绕过。
细心的读者可能发现,TheLastBundleMismatch 有两个 CVE 编号。
这是因为,第一次的补丁在修复另外一个漏洞时无意间被去除,导致漏洞死而复生。
旧的攻击面的修复却引入了新的攻击面,只是每次的利用越来越复杂、有越来越多的限制。
我们相信在持续不断的攻防对抗中系统会变得更安全,但:TheLastBundleMismatch 真的会是 "The Last" 吗?
参 考:
[1] https://github.com/michalbednarski/TheLastBundleMismatch
明日,请继续关注《深蓝洞察 | 2023 年度安全报告》第六篇。