这次我们从 Android 安全公告 中作为 CVE-2023-45777 修复而出现的补丁开始:```diff 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;
}
Few people were puzzled by it enough to ask me, previously I've replied to them with some hints and now I'm publishing full writeup for this issue
But first lets provide some context about what is going on in this patch
This is change in [`checkKeyIntent()` method](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4938-4954;drc=47de64a38aa1799cb41f41b2ea0c539ee61de64d). This method performs multiple checks to ensure that `Intent` provided by application is safe for system to launch (using privileges of system)
First, this method uses `checkKeyIntentParceledCorrectly()` which serializes and deserializes again `Bundle` which we're checking and checks if `Intent` taken from `Bundle` before that matches `Intent` from `Bundle` after such cycle. Since launch of `Intent` happens in other system app processes than one which performs validation, it was previously [possible to construct `Bundle`-s which appeared safe during validation inside `AccountManagerService`, but contained different `Intent` after being sent to next process](https://github.com/michalbednarski/IntentsLab/issues/2#issuecomment-344365482). This simulates sending `Bundle` to next process in order to detect such situations.
After `checkKeyIntentParceledCorrectly()` we have `bundle.getParcelable()` call, which this patch switches from deprecated version that could construct any object to one that validates that object that is about to be deserialized is of type which was specified in second parameter
That version with type parameter was introduced in Android 13, as part of larger `Parcel`/`Bundle` hardening. In particular, before Android 13 when `Bundle` was sent between processes, it kept raw copy of whole serialized data until any item was accessed, at which point every value was deserialized. Now when any value is accessed for first time after `Bundle` has been received, only `String` keys and the values of primitive types are deserialized, while non-primitive values are left as `LazyValue`-s, which have their length stored as part of serialized data in order to ensure that even when serialization/deserialization logic is mismatched, such mismatches won't affect other entries
Before we dive in, lets have a look at `LazyValue`: In it's source code [we've got nice comment explaining it's data structure](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4392-4399;drc=03c34f57c05feecfb090de3917787f049cb5f804)```
| 4B | 4B |
mSource = Parcel{... | type | length | object | ...}
a b c d
length = d - c
mPosition = a
mLength = d - a
mPosition 和 mLength 描述了整个 LazyValue 数据在原始 Parcel 中的位置,包括 type 和 length。“length”(开头没有“m”)指的是写入 Parcel 的长度值,且不包含头部(type 和 length)
如果包含 LazyValue 的 Bundle 被转发到另一个进程,整个 LazyValue(包括 type 和 length 字段)会从 Bundle.mParcelledData 原样复制到目标 Parcel
当访问由 LazyValue 表示的 Bundle 项时,Parcel 会被回退到 mPosition 并调用 readValue()。如果向 bundle.getParcelable() 传入了类型参数,该参数会传递给 readValue(),后者既会确保即将反序列化的类型是预期的类型,也会在反序列化后验证反序列化出的值类型是否为预期类型。反序列化后 LazyValue 会被替换,因此下次将 Bundle 写入 Parcel 时,该值会再次通过 writeValue() 进行序列化
使用带类型的 Bundle.get*()/Parcel.read*() 参数主要与诸如 Parcel.readParcelableList() 之类的方法相关,该方法返回 ArrayList,并且由于 Java 类型擦除,即使你写了类似 List<SomeParcelableType> field = parcel.readParcelableList(); 的代码,<SomeParcelableType> 部分在运行时也不会被强制检查,因此这样的 List 可以包含系统中可用的任何 Parcelable 类,从而系统中所有可用的 createFromParcel/writeToParcel 都可能被用作包含此类 List 的类型的序列化/反序列化的一部分
你可能还想查看 Android 安全与隐私团队关于引入这些机制的演示(幻灯片,视频)
不过在这里,使用带类型的版本似乎是多余的,因为我们也显式检查了返回对象的类型。那么这里到底发生了什么,修复的又是什么漏洞呢?
再看一下开头的补丁
"intent" 键下反序列化的值是一个 Intent
Intent 对象触发不匹配,那我们将面临更大的问题Intent
Bundle 被发送到另一个进程后在其中包含一个 Intent,但 Parcelable 的类型保存在任何可能的不匹配偏移量之前,并且 LazyValue 的长度前缀机制阻止了我们在 writeToParcel/createFromParcel 不匹配的情况下修改后续的键值对那么,这里调用不带类型参数的 bundle.getParcelable(AccountManager.KEY_INTENT) 可能做什么危险的事情呢?
[答案在下一段,先猜一猜再继续读。如果我有一个兽设,这里会是放一些艺术图的地方]
答案是调用不相关的 createFromParcel(),它实际上修改了存储在不同键下的 LazyValue 的原始数据,而这些数据将被原样传递给下一个进程
我们有一个 createFromParcel() 实现,它实际上可以在提供的 Parcel 上调用 writeInt()
但这并不是因为 writeInt 被错误地放置,而是因为不受限制的反射。特别是在 PackageParser 内部有以下代码:```java
final Class cls = (Class) Class.forName(componentName);
final Constructor cons = cls.getConstructor(Parcel.class);
intentsList = new ArrayList<>(N); for (int i = 0; i < N; ++i) { intentsList.add(cons.newInstance(in)); }
We can have `Parcel` object which was passed to `createFromParcel` passed to any available in system `public` constructor that accepts single `Parcel` argument
And then [we have following code](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/PooledStringWriter.java;l=51-56;drc=782d49826862cbdc9d020fc9d85f8a6f64675dcb):```java
public PooledStringWriter(Parcel out) {
mOut = out;
mPool = new HashMap<>();
mStart = out.dataPosition();
out.writeInt(0); // reserve space for final pool size.
}
我们有一个构造函数,它会对传入的 Parcel 调用 writeInt(0),但有几个因素使利用变得复杂。
首先,虽然在源代码中并不直接可见,但在调用 newInstance() 之后,会立即执行一次类型转换,并抛出 ClassCastException。
我需要某种东西,能在 createFromParcel 期间于 try 块内调用另一个类的 createFromParcel,然后失败并传播捕获到的异常。
这部分是漏洞利用在纯 AOSP 上无法实际生效的地方,我使用了三星特有的类。