Android 13 引入了许多增强功能,以强化 Parcel 序列化机制
这很棒,绝对能消除许多漏洞或使其无法利用。同时,他们描述了如何攻破我之前的利用程序,该程序允许应用将代码加载到其他应用(包括系统应用)中
但现在我带着一个新利用程序回来了,它能以不同的方式实现同样的效果。它依赖于上述 Parcel 强化期间引入的以下漏洞:
![应用显示文本的截图。标题:LeakValue。正文:创建了 6 个 ValueLeaker。正在锁定 ActivityTaskManagerService。已锁定 ActivityTaskManagerService。正在解锁 ActivityTaskManagerService。已解锁 ActivityTaskManagerService。leakedBinders=[android.os.BinderProxy@f06702e]。泄露接口:android.app.IApplicationThread。请求执行代码。Shellcode 已在 uid=1000 pid=6904 packageName=com.android.settings uid=1000(system) gid=1000(system) groups=1000(system),1007(log),1065(reserved_disk),1077(external_storage),3001(net_bt_admin),3002(net_bt),3003(inet),3007(net_bw_acct),9997(everybody) context=u:r:system_app:s0 中执行。屏幕底部有两个按钮:START 和 MANUAL TESTING](Screenshot_20220723-081920.png)
(另外,应用执行时的 logcat;利用行为在日志中会产生明显噪声)
Parcel 与 Parcelable 不匹配漏洞简介Android 的 Parcel 类是进程间通信的基础
对象可以实现 Parcelable 接口,以便将其写入 Parcel,例如(摘自 AOSP):```java
public class UsbAccessory implements Parcelable {
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
public UsbAccessory createFromParcel(Parcel in) {
String manufacturer = in.readString();
String model = in.readString();
String description = in.readString();
String version = in.readString();
String uri = in.readString();
IUsbSerialReader serialNumberReader = IUsbSerialReader.Stub.asInterface(
in.readStrongBinder());
return new UsbAccessory(manufacturer, model, description, version, uri,
serialNumberReader);
}
};
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(mManufacturer);
parcel.writeString(mModel);
parcel.writeString(mDescription);
parcel.writeString(mVersion);
parcel.writeString(mUri);
parcel.writeStrongBinder(mSerialNumberReader.asBinder());
} }
请注意,`Parcel` 内部会存储执行写入或读取时的位置,`readString()` 会将数据解析为 String 并同时推进位置。该位置可以通过 [`dataPosition()`](https://developer.android.com/reference/android/os/Parcel#dataPosition())/[`setDataPosition()`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)) 手动获取/设置。`Parcelable` 接口的实现必须确保其 `writeToParcel` 和 `createFromParcel` 写入/读取的数据量相同,否则所有后续读取都会从错误的偏移量处获取数据。
[`Bundle`](https://developer.android.com/reference/android/os/Bundle)(可在进程间发送的键值映射)可以包含[可通过 `writeValue()` 写入 Parcel 的各种对象](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=1792-1937)。当从 `Parcel` 读取 `Bundle` 的内容时,系统中可用的任何 `Parcelable` 类都可以被读取。
`Bundle` 通过将整个经 Parcel 序列化的数据的长度写入 `Parcel`,然后仅[将原始 Parcel 的相关部分复制到存储在 `mParcelledData` 中的辅助 Parcel](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=1675-1683) 来延迟对内容的实际解析(例如,这允许 [`Activity.onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle)) 提供在 `system_server` 中不可用的 `Parcelable`,整个 `Bundle` 随后会被原样传递到 `system_server` 并返回,而无需解析内容)。
然而,一旦访问了 `Bundle` 中的任何值,`Bundle` 内的所有值都会被[解包(unparcelled)](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=227-313),并且[每个存在的键值对都会被解析](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=3613-3632)。如果这样的映射包含一个 `Parcelable`,其 `writeToParcel` 和 `createFromParcel` 方法不平衡,并且随后该 `Bundle` 被转发到另一个进程,那么那个进程可能会看到不同的 `Bundle` 内容。这使得所有此类[类中的不匹配成为系统中可利用的漏洞](https://github.com/michalbednarski/ReparcelBug),因为系统中存在一些[将 `Bundle` 检查为安全的位置](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=5037-5046),随后这些 `Bundle` 会被转发到另一个进程。
在这篇 writeup 中,我将这种先呈现一种内容、转发后又呈现另一种内容的 `Bundle` 称为自变化的 `Bundle`。
这里的另一个重要事项是,除了仅字节(字符串、数字以及由上述类型构成的对象)之外,`Parcel` 还可以包含文件描述符(File Descriptors)和 `Binder`。`Binder` 是可以对其发起 RPC 调用的对象,也就是说,一个进程创建 `Binder` 对象并重写 [`onTransact()` 方法](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))。然后 `Binder` 被传递给另一个进程,在上面的示例代码中,你可以看到用于将其读写到 `Parcel` 的 `read`/`writeStrongBinder()` 调用。在另一个进程中,当使用 `readStrongBinder()` 时,会创建一个 `BinderProxy` 对象(隐藏在 [`IBinder` 接口](https://developer.android.com/reference/android/os/IBinder) 后面)。然后那个进程可以对该对象调用 [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)),并且在原始对象中会执行 `onTransact()`。不过,通常人们不会手动编写 `transact()`/`onTransact()`,而是[使用 AIDL](https://developer.android.com/guide/components/aidl) 代替。
# `LazyValue` 登场:自变化 `Bundle` 的终结
由于过去存在大量 `writeToParcel`/`createFromParcel` 不匹配的类,Android 13 [引入 `LazyValue`](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/),从而解决了系统中任何此类类存在即可允许构建自变化 `Bundle` 的问题。
现在,当使用 `writeValue` 时,如果写入的值不是原始类型,[该值的长度也会被写入 `Parcel`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2331-2344;drc=03c34f57c05feecfb090de3917787f049cb5f804)。
当普通应用直接使用 `Parcel.readValue()` 时,[除了一种情况外,其他一切都与之前相同:如果从 `Parcel` 读取的 `length` 与实际读取的数据大小不匹配,则会打印一条警告](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4330-4348;drc=03c34f57c05feecfb090de3917787f049cb5f804)(不过请注意,[`Slog.wtfStack` 永远不会抛出异常](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/Slog.java;l=108-116;drc=23c7543b8e608ebcbb38b952761b54bb56065577))。
而 `Bundle` 现在则改用 [`Parcel.readLazyValue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4350-4420;drc=03c34f57c05feecfb090de3917787f049cb5f804)。
让我们更仔细地看看它是如何工作的:在 `LazyValue` 类中,有一段[很好的注释,解释了 `Parcel` 内 `LazyValue` 数据的结构](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
mSource 是对调用 readLazyValue() 的原始 Parcel 的引用
mPosition 和 mLength 描述整个 LazyValue 数据在原始 Parcel 中的位置和长度,包括 type 和 length
"length"(开头没有 "m")指的是写入 Parcel 的长度值,不包括头部(type 和 length)
那么,当有人(系统或应用)从从 Parcel 读取的 Bundle 中取值时,会发生以下情况: