Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
LeakValue — CVE-2022-20452 的漏洞利用,通过 LazyValue 在 recycle() 之后使用 Parcel,在 Android 上实现从已安装应用到系统应用(或其他应用)的权限提升。 | Kitploit
工具/GitHubGitHub/michalbednarski/leakvalue
Android安全权限提升漏洞分析漏洞利用移动安全二进制利用
GitHubmichalbednarski/leakvalue

LeakValue

CVE-2022-20452 的漏洞利用,通过 LazyValue 在 recycle() 之后使用 Parcel,在 Android 上实现从已安装应用到系统应用(或其他应用)的权限提升。

查看仓库
347653年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

Android 13 引入了许多增强功能,以强化 Parcel 序列化机制

这是 Android 安全与隐私团队关于所做改进的演示

这很棒,绝对能消除许多漏洞或使其无法利用。同时,他们描述了如何攻破我之前的利用程序,该程序允许应用将代码加载到其他应用(包括系统应用)中

但现在我带着一个新利用程序回来了,它能以不同的方式实现同样的效果。它依赖于上述 Parcel 强化期间引入的以下漏洞:

  • CVE-2022-20452 (公告, 补丁)
  • CVE-2022-20474 (公告, 补丁)

应用显示文本的截图。标题: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

(另外,应用执行时的 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());

root@kitploit:~
        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());

} }

root@kitploit:~
请注意,`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 中取值时,会发生以下情况:

  1. 调用者使用 Bundle 类的众多 get*() 方法之一,例如 新的 getParcelable()(带类型参数)。(新方法和旧方法的流程相同,只是新方法确保 clazz 参数不为 null,而旧方法将其设为 null)
  2. unparcel() 被调用,它会检查该 Bundle 是否具有 mParcelledData(意味着它已从 Parcel 读取,但尚未访问任何值,也未解包键名;如果不是这种情况,跳到第 5 步)
  3. unparcel() 委托给 unparcel(boolean itemwise),调用 ,其中 被设置为 ,即 制作的 副本, 参数被设置为 ,表示传入的 归 所有,可以对其调用

如果 Bundle 在仍包含 LazyValue 时被转发(意味着该特定值未被访问,但该 Bundle 中的其他值被访问了(即调用了 unparcel(),但该条目的 LazyValue.apply() 尚未调用)):

  1. Parcel.writeValue() 检测到 LazyValue,并将写入委托给 LazyValue.writeToParcel()
  2. LazyValue.writeToParcel() 使用 out.appendFrom(source, mPosition, mLength) 从原始 Parcel 复制整个 LazyValue 数据(同样,mPosition 和 mLength 包含 LazyValue 头部,因此这也将复制原始 Parcel 中的 type 和 length)

Parcel.ReadWriteHelper 与 Parcel.readSquashed

(这些细节对于此漏洞利用并不重要,这里只相关的是这些机制存在)

Parcel 另一个有趣的特性是可选的对写入的 String 和对象进行去重

String 的去重是通过重写 Parcel.ReadWriteHelper 类完成的:Parcel.readString() 实际上委托给 ReadWriteHelper,默认帮助器直接从 Parcel 读取 String

Parcel.ReadWriteHelper 的替代实现可以将 readString 调用替换为先读取 String 池,然后使用 readInt 获取池中 String 的索引;然而,这在应用控制的 Parcel 上从未发生过

Parcel 确实提供了 hasReadWriteHelper() 方法,它允许调用者检测这种去重机制是否处于活动状态,并禁用与其不兼容的功能

Parcel 中另一个可用的去重机制是压缩(squashing):

  1. 首先,必须使用 Parcel.allowSquashing() 启用压缩
  2. 然后,当写入支持压缩的类时,它首先调用 Parcel.maybeWriteSquashed(this)。如果该方法返回 true,说明该对象已写入此 Parcel,现在只向 Parcel 写入了对先前对象数据的偏移量。否则(要么没有启用压缩,要么这是该对象第一次被写入),maybeWriteSquashed 会写入零作为偏移量,表示该对象未被压缩,并返回 false 以指示调用者应该写入实际对象数据
  3. 读取时,调用 Parcel.readSquashed,实际读取函数作为 lambda 传递给它。readSquashed 检查 maybeWriteSquashed() 写入的偏移量是否指示该对象的另一个实例已被提前读取:如果是,则返回先前读取的对象,否则调用提供的 lambda 立即读取它

Parcel.recycle() 后使用

在 Java 方面,Parcel 对象可以被回收到池中,也就是说一旦你完成了 Parcel 的使用,你可以对它调用 recycle(),下次有人调用 Parcel.obtain() 时,他们将获得先前回收的 Parcel。这允许减少对象分配和后续垃圾收集的量

另一方面,这种手动内存管理给 Java 带来了类似释放后使用(Use-After-Free)的 bug(尽管有类型安全,不像 C 中通常的 Use-After-Free)

如上所述,Bundle 会创建 Parcel 的副本,如果存在 LazyValue,则不会调用 Parcel.recycle(),但是如果 Parcel.hasReadWriteHelper() 为 true,则情况并非如此,在这种情况下:

  1. 调用 initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle);,这意味着 Bundle 不会回收 Parcel,因为它仍然属于调用者,但是这确实会创建引用原始 Parcel 的 LazyValue,并且这些 LazyValue 的寿命可能超过原始 Parcel 的寿命
  2. 因此,接下来的事情是调用 unparcel(/* itemwise */ true),它将对所有条目使用 getValueAt(),将 Bundle 中存在的所有 LazyValue 替换为实际值

那么,我们能设法让这些 LazyValue 在第 2 步之后存活下来,并将该行为转化为回收后使用(Use-After-Recycle)吗?

如果反序列化失败(例如,无法找到 Parcel 内指定的类名),则会抛出 BadParcelableException,然后被 getValueAt() 捕获。如果 BaseBundle.sShouldDefuse 静态字段为 true,则不会引发异常,执行继续,使 Bundle 包含引用原始 Parcel 的 LazyValue。sShouldDefuse 表示来自 Bundle 的不可用值不应在特定进程中引起异常,并且在 system_server 中被设置为 true

如果原始 Parcel 被回收,之后从它读取的 Bundle 将被写入另一个 Parcel,原始 Parcel 的内容将被复制到目标 Parcel,但此时原始 Parcel 可能已被用于其他目的,并且可能复制来自不相关 IPC 操作的数据

好的,但是我们如何让 Parcel.hasReadWriteHelper() 在我们提供的 Bundle 被反序列化时为 true?

事实证明,RemoteViews 类(通常用于例如将小组件传递给主屏幕)在读取嵌套其中的 Bundle 时显式设置 ReadWriteHelper。这个 ReadWriteHelper 不执行 String 去重,它存在的唯一目的是让 Bundle 跳过将数据复制到辅助 Parcel 的步骤。这样做的原因是 RemoteViews 启用了压缩 以对其中的 ApplicationInfo 对象进行去重,但这也可能导致 Bundle 内的 ApplicationInfo 对象被压缩,因此不能延迟读取该 Bundle,因为那样这些被压缩的对象将无法解压

将 Parcelable 放入 system_server 并检索它们

现在我们希望 system_server 读取我们的 RemoteViews,其中包含 Bundle,Bundle 包含反序列化失败的 LazyValue,然后在另一次 Binder IPC 事务中(稍后)将该对象发送回我们

这或许可以通过一些合法手段实现,例如注册自己为应用小组件宿主(但这需要用户交互来授予我们权限)或发布一个设置了 contentView 的 Notification(但这会导致与其他进程交互和/或对用户可见,我更愿意避免这两种情况)

我决定改为创建 MediaSession 并对其调用 setQueue(List<MediaSession.QueueItem> queue) 以将对象发送到 system_server,然后通过 MediaController 的 List<MediaSession.QueueItem> getQueue() 方法取回它(可以通过 MediaSession.getController() 获取)。虽然这些方法看起来不能接受 RemoteViews,但由于 Java 类型擦除以及底层它们是对 List 使用泛型序列化操作实现的,它们实际上可以

但是,我没有使用这些 SDK 方法,而是手动为基础 Binder 事务写入数据(因为我需要写入并在之后读取格式错误的序列化数据),所以让我们看看这些方法是如何工作的

这两种方法都必须处理队列总大小可能超过 Binder 事务最大大小的事实,因此传输可以被拆分为多个事务

向 system_server 发送“队列”通常如下进行:

  1. MediaSession.setQueue() 首先调用 ISession.getBinderForSetQueue()
  2. 在 system_server 侧,该方法构造并返回 ParcelableListBinder 对象
  3. 之后,MediaSession.setQueue() 调用 ParcelableListBinder.send(),它将列表内容发送到提供的 Binder 中,可能通过多个事务:
    • 第一个事务的开头包含将在列表中的总项目数,然后是第一部分的内容
    • 然后,对于传输的每个项目,写入 1,并通过 Parcel.writeParcelable() 写入实际项目(它会写入正在发送的类的名称,然后调用 发送数据)

另一方面,检索“队列”的方式略有不同:

  1. MediaController.getQueue() 只是调用 ISessionController.getQueue() 并解包接收到的 ParceledListSlice
  2. 在 system_server 侧,getQueue() 只是将 mQueue 包装到 ParceledListSlice 中并返回
  3. 跨多个事务拆分的整个逻辑在 ParceledListSlice.writeToParcel() 和 createFromParcel() 方法内部,特别是,writeToParcel() 在达到安全大小限制时会写入一个 Binder 对象,允许检索后续块

至于为什么它们不同:目前正在努力确保 system_server 不会向其他应用发出同步的 Binder 调用,因为如果这些调用挂起,整个 system_server 可能会挂起。这意味着 system_server 不应接收 ParceledListSlice。虽然存在针对从 system_server 发出传出同步事务的警告代码,但它还不能强制执行,因为 system_server 在某些情况下仍然会进行此类调用,例如实际接收 ParceledListSlice

选择泄漏目标

因此,现在我们有了使 system_server 执行 parcel_that_will_be_sent_to_us.appendFrom(some_recycled_parcel, somewhat_controlled_position, controlled_size) 所需的原语

我们可以随机尝试从系统拉取 Parcel 数据,或者安排事物以获取特定内容

有以下考虑:* 当调用 Parcel.recycle() 时,该 Parcel 的内容会被清除。这意味着,我们希望从中复制数据的 Parcel 不能被 recycle(),这大致意味着我们无法从已结束的 Binder 事务中获取数据

  • 或者,我们可以从系统中存在的某个 Bundle 的某个 Parcel 中获取数据(这包括 Intent 的 extras 和 Activity 的 savedInstanceState)。这些 Parcel 通常根本不会被 recycle()(它们由垃圾收集器清理,不会返回对象池;当对象池耗尽时,Parcel.obtain() 会创建新的 Parcel 对象。当然,我们持有引用的 Parcel 不会被 GC 回收,即使系统对它们没有其他用途)
  • 用于传入 Binder 事务的 Parcel 使用与系统中其他 Parcel 不同的对象池。当进行传出 Binder 事务时,Bundle 会将数据复制到辅助 ,或者应用出于自己的目的使用 ,它们会调用 。另一方面,当有传入的 事务时,,它 。在这两种情况下,之后都会调用 ,并负责 。这意味着漏洞利用必须让 从与我们要泄漏数据的对象池相同的 中读取。在我决定采用特定变体之前,我已经编写了这两种方法,因此你可以在我的 中找到 和 方法。

最终,我决定尝试获取 IApplicationThread Binder。当应用进程启动时,应用会将其发送给 system_server,system_server 用它来告诉应用应该加载哪些组件。

当应用进程首次启动时,它最先做的事情之一就是 通过调用 attachApplication() 将 IApplicationThread 发送到 system_server,而我将从这个事务中获取该 Binder。还有其他地方会将 IApplicationThread 放入 Parcel,例如系统在启动 Activity 时将其传递用于 调用方识别(但我对目标应用何时执行该操作没有太多控制权),或者系统将其作为 Activity 生命周期管理的一部分 发送给应用(但这是通过从 system_server 发出的 单向事务 完成的,与 Parcel.recycle() 竞争并获胜的机会很小)。

话虽如此,在 attachApplication() 事务期间获取 system_server 正在接收的 Binder 也并非易事,有几个问题需要克服。

倒回 Parcel

从 接收 attachApplication() 数据的 Parcel 中获取 IApplicationThread Binder 的第一个问题是,该 Binder 位于 相当早/低的 dataPosition(),远低于我们在 RemoteViews 的 Bundle 中 LazyValue 可能所处的位置。

attachApplication() 事务的数据仅由 RPC 头后跟 IApplicationThread Binder 组成。RPC 头(通过 Parcel.writeInterfaceToken() 写入)由 几个 int 和接口名称 组成,在本例中为 "android.app.IActivityManager"。

与此同时,要读取 RemoteViews 中嵌入的 Bundle,我们需要至少越过以下内容(跳过了一些次要项目):

  • 用于启动 readParcelable 的 项目存在标志
  • Parcelable 的名称:"android.view.RemoteViews"
  • RemoteViews 中存在的相当大的 ApplicationInfo 对象(此外,它必须 不为 null 且 packageName 非 null,否则当我们尝试获取此对象以将其发送回去时,RemoteViews.writeToParcel() 将失败)
  • 最后,我们 到达 ,它 ,后者构造 ,在 之后,最终

现在,在 Bundle 中,我们只需要放入一个 String 键,读取 LazyValue 的操作就会开始,Parcel 中的位置会被记住,但此时它已经远远超过了 IApplicationThread Binder 所在的位置。

当我们到达这一点时,能否将 Parcel 中的位置倒回?换句话说,我们能否让 Parcel.setDataPosition() 被调用,并传入一个指向比当前位置更早位置的值?

事实证明,我们可以,这要归功于 LazyValue 中的另一个 bug。以下是用于读取它的代码:```java public Object readLazyValue(@Nullable ClassLoader loader) { int start = dataPosition(); int type = readInt(); if (isLengthPrefixed(type)) { int objectLength = readInt(); int end = MathUtils.addOrThrow(dataPosition(), objectLength); int valueLength = end - start; setDataPosition(end); return new LazyValue(this, start, valueLength, type, loader); } else { return readValue(type, loader, /* clazz */ null); } }

root@kitploit:~
([AOSP 中的原始代码](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4376-4388;drc=03c34f57c05feecfb090de3917787f049cb5f804),`LazyValue` 构造函数只是将参数赋给字段)

关键在于 `MathUtils.addOrThrow()` 会检查溢出,[但对负值完全没问题](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/MathUtils.java;l=276;drc=290055a76e6ef80dd8ad7bc812d78f4fc0c5be86)

如果我们尝试对一个带有负 `mLength`(由 `valueLength` 参数填充)的 `LazyValue` 执行 `Parcel.writeValue()`,那么[会在 `appendFrom()` 中抛出异常](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4446;drc=03c34f57c05feecfb090de3917787f049cb5f804);但由于我们在读取 `Bundle` 时 `Parcel.hasReadWriteHelper()` 为 `true`,所有 `LazyValue` 在读取后都会被反序列化,因此我们必须有意在其中放入一个错误的 `Parcelable`,使其保持为 `LazyValue`。如果我们把有效的已序列化数据放在 `LazyValue` 所在的位置,它就会被反序列化,而如前所述,长度不匹配只会触发 `logcat` 中的消息。这个特定漏洞利用将类型设置为 `VAL_MAP`,并将键值对数量设为零。在 `logcat` 中读取该值时,我们可以看到以下消息:“`E Parcel  : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP  consumed 4 bytes, but -540 expected.`”

(另外,指定负长度的 `LazyValue` 也可以(在不使用本文描述的其他漏洞的情况下)用来创建会自我改变的 `Bundle`,这正是 `LazyValue` 被创造出来所要消除的问题。但那是另一个故事(并且已单独报告给 Google),在这个漏洞利用中,我追求的是更多)

那么我们想要回退多少呢?

在调用 `setDataPosition()` 之后,读取将继续进行到 `Bundle` 中的下一组键值对,所以我们需要选择一个位置,使我们在该位置能拥有:

1. `Bundle` 键,使用 `Parcel.readString()` 读取,可以是几乎任何内容,包括指向无效长度(负值或超过 `Parcel` 总大小),在这种情况下,`readString()` 会返回 `null`,而 `null` 在 `Bundle` 中是有效的键
2. 值类型,必须是 [`isLengthPrefixed()` 返回 `true` 的类型](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4695-4712;drc=03c34f57c05feecfb090de3917787f049cb5f804) 之一
3. 值长度,这也必须是我们可以控制的值,如果长度未对齐或超过源 `Parcel` 的总大小,`Parcel.appendFrom()` 将会失败

那么,考虑到相同的数据已经被读取并且是到达该点所必需的,`Parcel` 中哪个位置可能满足条件呢:

* 不能在 `Parcelable` 的名称(`"android.view.RemoteViews"`)之前,因为空间不够
* 不能在 `Parcelable` 的名称内部,因为我们无法设置类型和长度
* 不能紧接在 `Parcelable` 的名称之后,因为 `RemoteViews` 中的第一项是 `mode`,而我们[必须将其设置为 `MODE_NORMAL` 才能到达我们的代码](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/RemoteViews.java;l=3799;drc=03c34f57c05feecfb090de3917787f049cb5f804)
* 也不能在这之后,因为那已经超过了 `IApplicationThread` `Binder` 所在的位置

嗯,当 `RemoteViews` 是打包数据中最外层的对象时,似乎没有什么好位置

我们需要找到另一个满足以下条件的 `Parcelable`:

1. 在开头或接近开头的位置,我们可以放置任意数据(例如 `int` 或 `String`,它们只是数据,不影响序列化过程)
2. 可以包含 `RemoteViews`(直接包含或通过任意的 `readParcelable` 包含)
3. 完全限定类名不能太长,因为我们在大小上仍然受 `IApplicationThread` 在目标 `Parcel` 中的位置限制

所以我拿了系统中 `Parcelable` 类的列表,按完全限定类名的长度升序排序,并开始检查列表中的项目,看它们是否满足条件 2

这样我就找到了 [`"android.os.Message"`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=afdb23ab6f909c5438fa69aad458a11497cff216),这正是这个漏洞利用所使用的。现在,从 `Parcel` 中读取我们准备好的对象的过程如下:

* [条目存在标志](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577) 用于启动 `readParcelable`
* [`Parcelable` 的名称](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4858;drc=03c34f57c05feecfb090de3917787f049cb5f804):`"android.os.Message"`
* 一些[我们可以设置为任意值的 `int`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=652-656;drc=afdb23ab6f909c5438fa69aad458a11497cff216) 被读入字段
* 我们到达 `readParcelable()` 调用,它会像我们上面描述的那样一路穿过 `RemoteViews`,并在 `Parcel.hasReadWriteHelper` 为 `true` 的情况下开始读取 `Bundle`
* 那个 `Bundle` 声明有两个键值对。在第一个值中,我们有一个负长度的 `LazyValue`,这会触发 `Parcel.setDataPosition()` 定位到 `"android.os.Message"` 字符串所在的位置
* 读取继续到第二个键值对,键是 `"android.os.Message"`,而 `LazyValue` 的类型、长度和数据取自第三个项目符号中描述的 `int`。我得到了我想要的 `mPosition` 和 `mLength` 的 `LazyValue`。太棒了!
* 读取完 `LazyValue` 后,它们会被反序列化。负大小的那个被成功反序列化并替换为空 `Map`,而另一个反序列化失败,但该异常被捕获,`LazyValue` 就留在 `Bundle` 中
* `readParcelable()` 结束了,但这并不是 `Message` 数据的末尾。`Message.readFromParcel()` 现在继续读取回退之后的数据,并看到最初作为 `RemoteViews` 一部分写入的数据。如果此时有任何异常抛出,整个计划就会失败
* 第一个可能的异常是 [存在 `readBundle()` 调用](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=660;drc=afdb23ab6f909c5438fa69aad458a11497cff216)。[`Bundle` 有一个魔法值,如果该值错误,就会抛出异常](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1809-1815;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91)。但是,如果长度为 [零](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1800-1804;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91) 或 [负数](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3323-3327;drc=03c34f57c05feecfb090de3917787f049cb5f804),这个魔法值就不存在,而当 `LazyValue` 数据的长度被设置为我抓取 `IApplicationThread` 所需的值时,情况恰好如此。所以在这里我只是运气好
* 下一个可能的问题是 [`Messenger.readMessengerOrNullFromParcel()` 调用](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=661;drc=afdb23ab6f909c5438fa69aad458a11497cff216)。这实际上是一个包装过的 `Binder` 对象。读取该 `Binder` 会失败,因为 `Binder` 是 `Parcel` 中的特殊对象,必须带外(out-of-band)标注才能被读取。这个问题[会被 `Parcel` 在 native 侧检测并记录日志,但它不会作为错误传播,而是直接返回 `null`](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp;l=2473-2476;drc=8b12e8333bbe17ccf4b30efb825244e1923d3a71)

# 阻塞 `attachApplication()`

好了,在上一步中,我们成功创建了一个对象,它允许我们在 `attachApplication()` 方法运行时抓取 `IApplicationThread` 对象

问题是,该方法完成得很快,我们公平地与之竞赛并赶在其完成前的机会相当渺茫

然而,该方法确实会获取几个互斥锁(通过使用 Java 的 `synchronized () {}` 块)。如果我们能获取其中一个互斥锁并阻塞在那里,该方法也会被阻塞

现在让我们回到本文中已经说过的几点,它们将对这一目的有用:

* 当 `Bundle` 中的值被访问时,`Bundle` 会对其执行反序列化
* 有一个 `ParceledListSlice` 类,在反序列化过程中,它会向序列化数据中指定的对象发起阻塞式的传出 `Binder` 调用

将所有这些放在一起:如果我们在 `system_server` 中找到这样一个位置,即应用提供的 `Bundle` 的内容在 `attachApplication()` 也使用的互斥锁下被访问,那么我们就能阻塞 `attachApplication()`,直到向我们的进程发起的 `Binder` 事务完成

[`ActivityOptions`](https://developer.android.com/reference/android/app/ActivityOptions) 是描述与 `Activity` 启动相关的各种参数(例如动画)的类。与其他描述传给 `system_server` 的参数的类不同,它没有实现 `Parcelable`,而是提供了将其转换为 `Bundle` 的方法

在 `system_server` 端,那个 [`Bundle` 被转换回 `ActivityOptions`,触发反序列化](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityOptions.java;l=1096-1204;drc=03c34f57c05feecfb090de3917787f049cb5f804)。我找到了一个地方,[该操作在持有 `ActivityTaskManagerService.mGlobalLock` 互斥锁时于 `ActivityTaskManagerService.moveTaskToFront()` 中执行](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=2088;drc=03c34f57c05feecfb090de3917787f049cb5f804)

所以我调用 [`ActivityManager.moveTaskToFront()`](https://developer.android.com/reference/android/app/ActivityManager#moveTaskToFront(int,%20int,%20android.os.Bundle)),传入一个包含 `ParceledListSlice` 而非预期类型值的 `Bundle`。那个 [`ParceledListSlice` 会向我的进程发起 `Binder` 调用](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=82-105;drc=23c7543b8e608ebcbb38b952761b54bb56065577),并且在从这个调用返回之前,`ActivityTaskManagerService.mGlobalLock` 互斥锁将一直保持锁定

# 创建指向不同 `Parcel` 的多个 `LazyValue`

`Parcel.recycle()` 和 `Parcel.obtain()` 以[后进先出](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))的方式工作

这意味着,如果在没有其他 `Binder` 事务进入 `system_server` 时创建一个特制的 `LazyValue`,我将得到一个指向某个 `Parcel` 的 `LazyValue`,而这个 `Parcel` 在只有一个事务进入 `system_server` 时总是会被用到(除非发生两个并发进入 `system_server` 的事务以非栈顺序开始和结束)

由于我无法控制其他哪些事务正在进入 `system_server`,为了提高漏洞利用的可靠性,我创建了多个指向不同 `Parcel` 的 `LazyValue`

由于我有能力让 `system_server` 向我的进程触发同步的 `Binder` 事务,我利用这一能力在我的进程与 `system_server` 之间的不同递归层级创建 `LazyValue`(不过这一次我这样做时没有持有全局互斥锁)

所以:

* 我创建 `LazyValue`
* 我触发对 `system_server` 的调用,`system_server` 回叫我
    * 我创建 `LazyValue`
    * 我触发对 `system_server` 的调用,`system_server` 回叫我
        * 我创建 `LazyValue`
        * 我触发对 `system_server` 的调用,`system_server` 回叫我
            * ...

然后,一旦我获得了足够的 `LazyValue`,我就结束这个过程,从所有这些调用中返回,所有被这些调用保留的 `Parcel` 都会被 `recycle()`

我创建的每个 `LazyValue` 都包装在独立的[由 `getQueue()` 创建的 `ParceledListSlice`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804) 中,我可以调用 `ParceledListSlice` 的 `Binder`,让 `system_server` 将其序列化并发送到我的进程

(另一种做法是创建多个 `MediaSession`)

# 启动目标应用进程

现在我们拥有了在 `attachApplication()` 发生时从中捕获 `IApplicationThread` 所需的一切,但我们仍然需要让 `attachApplication()` 发生

一般来说,[其他应用可以与应用组件交互的类型有几种](https://developer.android.com/guide/components/fundamentals#Components),每种都需要启动应用进程

我想启动系统设置应用(它[在 system uid 下运行](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=5;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74),因此可以访问 [Android 权限背后的所有内容](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityManager.java;l=3948-3952;drc=16c119018a80b8630c7736a5c6dc35ebef130c5b))

最初我尝试通过 `startActivity()` 启动它,但当我尝试时,进程直到我释放 `ActivityTaskManagerService` 锁后才启动。具体原因详见“附加说明:`Binder` 调用与互斥锁的可重入性”一节,但作为解决方案,我决定向系统请求该应用的 [`ContentProvider`](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=4031-4034;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74),而不是 `Activity`。这还有一个额外的好处,就是避免干扰我的 UI

我没有使用 [SDK 公开的官方 `ContentResolver` API](https://developer.android.com/reference/android/content/ContentResolver),而是使用了[系统内部的 API,因为我需要异步 API](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IActivityManager.aidl;l=153-154;drc=3d7be31a284d295af4c118c5675896e6fcc28907),因为绑定到 `ContentProvider` 在我挂起的 `attachApplication()` 完成之前不会结束,尽管启动另一个线程也可以作为替代方案

(这个特定的 `ContentProvider` 提供什么并不重要,唯一相关的是我可以与它建立连接)

这就是我启动设置应用进程的方式。我首先确保它尚未在运行,方法是使用[官方可用的 `ActivityManager.killBackgroundProcesses()` 方法](https://developer.android.com/reference/android/app/ActivityManager#killBackgroundProcesses(java.lang.String))

# 将所有部分整合在一起

现在基础组件已经描述完毕,下面就是它们如何协同工作(这基本上是本漏洞利用中 `MainActivity.doAllStuff()` 方法的转录):

1. 启用隐藏 API 访问(隐藏 API 不是安全边界,并且[已经有公开可用的绕过方法](https://www.xda-developers.com/bypass-hidden-apis/),不过这里我使用了基于 [`Property.of()`](https://developer.android.com/reference/android/util/Property#of(java.lang.Class%3CT%3E,%20java.lang.Class%3CV%3E,%20java.lang.String)) 的方法,这是我未曾见过其他人在用的)
2. (仅当我们在第一次尝试后重新运行漏洞利用时)释放我们在上一次执行中于第 6 步建立的与 `ContentProvider` 的连接。我们必须这样做,否则 `ActivityManager.killBackgroundProcesses()` 不会认为目标进程处于“后台”,也不会将其杀死
3. 使用 `ActivityManager.killBackgroundProcesses()` 杀死目标应用进程,因为 `attachApplication()` 只在进程启动时被调用
4. 请求 `system_server` 创建一组包含指向之后会被回收的 `Parcel` 的 `LazyValue` 对象。对于每个包含 `LazyValue` 的对象,我获得一个 `ParceledListSlice` 的 `Binder` 引用,并且可以向其发起 `Binder` 事务以触发系统将其写回。每个 `LazyValue` 对象的创建都在 `system_server` 与我的应用之间的[相互递归](https://en.wikipedia.org/wiki/Mutual_recursion)调用的不同深度完成,以便使这些 `LazyValue` 中的每一个都更有可能对不同 `Parcel` 对象存在悬空引用
5. 我通过调用 `ActivityTaskManagerService.moveTaskToFront()` 并传入一个 `Bundle` 参数来锁住 `ActivityTaskManagerService.mGlobalLock`,该 `Bundle` 在反序列化时会向我的进程执行同步的 `Binder` 事务。接下来的步骤是在该回调中完成的,因此是在持有该锁的情况下完成的
6. 我请求 `ActivityManagerService` 与目标应用的 `ContentProvider` 建立连接(注意名称中没有“`Task`”,`ActivityTaskManagerService` 主要专注于处理应用的 `Activity` 组件,而 `ActivityManagerService` 处理其他[应用组件](https://developer.android.com/guide/components/fundamentals#Components)(以及整体进程启动),这个[拆分发生在 Android 10 中,此前 `Activity` 和其他应用组件的处理都在 `ActivityManagerService` 中](https://android.googlesource.com/platform/frameworks/base/+/595070969de0a7334d251d5448b641e856e052bc))
7. 我 `sleep()` 一小会儿,让新启动的进程有时间开始调用 `attachApplication()`
8. 在锁仍然被持有时,我请求所有之前创建的 `ParceledListSlice` 对象发送它们的剩余内容(最初事务中放不下的部分),也就是包含指向已回收 `Parcel` 的 `LazyValue` 的对象。然后,从与传给 `attachApplication()` 的 `IApplicationThread` 位置匹配的硬编码偏移量处,我读取 `Binder` 对象。目前我只是将收到的 `Binder` 保存到 `ArrayList` 中,以避免在持有锁的情况下做太多事情
9. 这是我在第 5 步启动的回调中执行的代码的结束。`ActivityTaskManagerService.mGlobalLock` 被解锁
10. 我已经获得了 `IApplicationThread` 的 `Binder`。现在我可以直接使用它,按照下一节所述将我的代码加载到目标应用中

# 我如何使用 `IApplicationThread`

如前所述,[`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl;drc=45f4b4aaa5fd8af2d0c685b2fef8acf75ed37452) `Binder` 是应用进程启动时由应用发送给 `system_server` 的,然后 `system_server` 使用它来告诉应用应该加载哪些组件

人们假设这个对象只会被传给 `system_server`,因此其中没有基于 `Binder.getCallingUid()` 的检查,所以我们可以直接调用该接口提供的方法

在我之前的文章中,我[描述了如何通过操纵 `scheduleReceiver()` 的参数来实现代码执行](https://github.com/michalbednarski/ReparcelBug2#what-then-happens-within-handlereceiver)。现在的情况是一样的,只不过这次是我自己调用 `scheduleReceiver()`,而那时我是在篡改 `system_server` 发起的调用的参数解释

# 附加说明

在本节中,我要描述几件事,最终在这种情况下它们没有派上用场,尽管它们可能是值得注意的功能,或者是潜在的 bug

## 附加说明:`Bundle.clear()`为简单起见,我在这里描述的是更新后的 `Bundle`,没有包含[后来引入的一个提交,该提交允许将 `Bundle` 中用于支持 `LazyValue`s 的 `Parcel` 通过调用 `Bundle.clear()` 回收](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/)

正如提交消息中所指出的,如果 `Bundle` 被复制,则会进行跟踪;在这种情况下,`clear()` 不会回收 `Parcel`。

不过,该提交也改变了 [`BaseBundle.initializeFromParcelLocked()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=408-457;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1) 的 `recycleParcel` 参数/变量的语义。

此前,`recycleParcel` 为 `false` 表示不应回收 `Parcel`,要么是因为[调用者将 `recycleParcel` 设为 `false` 以表示 `Parcel` 并非由 `Bundle` 拥有](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1842-1847;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1),要么是因为[根据 `parcelledData.readArrayMap()` 的结果将其设为 `false`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=441;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)。

现在,`recycleParcel` 可能为 `false` 的原因相同,但其含义已经改变:现在它不再表示“不要回收这个 `Parcel`”,而是表示“将 `Parcel` 的回收推迟到调用 `Bundle.clear()` 时”。

这意味着,如果在 `Parcel.hasReadWriteHelper()` 为 `true` 时创建的 `Bundle` 上调用 `clear()`,将导致 `Parcel` 被回收;而调用该 `Bundle` 创建过程的代码也会回收那个 `Parcel`,从而造成双重 `recycle()`,其行为类似于双重释放:接下来对 `Parcel.obtain()` 的调用会两次返回同一个对象。

不过,我尚未找到在这样一个 `Bundle` 上调用 `clear()` 的方法。

自从我最初写下这些内容以来,[`recycle()` 的行为发生了变化,现在多余的回收调用是空操作,但可能通过 `Log.wtf()` 导致崩溃](https://android.googlesource.com/platform/frameworks/base/+/64ff38669a0e1f945b54c4c62ed9316282a6588d%5E%21/)([具体取决于配置,但绝不会让 `system_server` 崩溃](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8619-8648;drc=ab23aee04d50b9bdabd52481e77265e976558056))。我认为新行为仍然可能很危险,尤其是当我们能够以编程方式阻塞另一个进程中正在进行的反序列化时,但确实没有好的方法来处理双重回收。

## 补充说明:`Binder` 调用与互斥锁的可重入性

`Binder` 一个不太为人知的特性是,它支持将递归调用分派回原始线程。

也就是说,如果进程 A 向进程 B 发起同步 `Binder` 调用,然后进程 B 在同一线程上处理该调用时又向进程 A 发起同步 `Binder` 调用,那么进程 A 中的这个调用将在等待原始调用(对进程 B)完成的同一个线程中被分派处理。

另一件事是,Java 中的 `synchronized () {}` 块是可重入互斥锁,这意味着如果同一线程两次进入该块,它仍然会允许你进入,而不会死锁。

这意味着,理论上在我们保持 `ActivityTaskManagerService.mGlobalLock` 锁定的情况下,我们仍然可以使用 `startActivity(new Intent(Settings.ACTION_SETTINGS))` 启动 Settings 应用,并且能够成功进入[我们正在阻塞的 `synchonized` 块](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=626;drc=8854e6eb5960c1a9b233fd0fa6e36a366b2f802d)。然而,启动那个 `Activity` 还涉及 `Task` 的创建,而 `Task` 的创建会调用[`notifyTaskCreated()`,它会发送消息至](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=424-429;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) [`DisplayThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/DisplayThread.java;l=25-31;drc=92b9365f9e1ea5d735e8acb06f790604036ee547),而[对该消息的处理](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=207-208;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)会尝试[从另一个线程获取我们所阻塞的那把锁](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=320;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)。因此,在释放 `ActivityTaskManagerService.mGlobalLock` 之前,`DisplayThread` 线程将一直处于阻塞状态。后来,启动 `Activity` 的过程还涉及[向同一线程发送消息以启动应用进程](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=4659-4664;drc=c76db81ef9d400ffed200f69c7bbda923cdb941c)。这一切意味着,在这种情况下,应用进程只有在我们释放锁之后才会启动;而我们最初持有该锁的原因,正是为了阻止 `attachApplication()` 事务完成,以便从中获取句柄,但在这种情况下,该事务实际上根本不会开始执行。

即使我们启动的 `Activity` 与当前 `Task` 属于同一个 `Task`(也就是说,我们从 Settings 应用启动另一个 `Activity`,一个未指定 `android:launchMode="singleTask"` 的 `Activity`),该过程仍然会涉及 [`notifyTaskDescriptionChanged()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=444-450;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f),它在这里产生的影响与 `notifyTaskCreated()` 相同。

因此,尽管我的线程可以调用那些使用 `synchronized (ActivityTaskManagerService.mGlobalLock) {}` 的方法,但在 `startActivity()` 之后启动新的应用进程需要从另一个线程使用该锁,而这一点在这种情况下没有用处,所以我选择改为通过 `ContentProvider` 来触发应用进程的启动。

## 补充说明:`IApplicationThread` 的其他用法

`IApplicationThread` 是一个特权句柄,因此我认为在获得它之后加以利用属于后渗透(post-exploitation)行为。

在这个漏洞利用中,我直接使用它来请求在目标进程中执行代码,利用的事实是:对该操作的访问由能力(即拥有 `Binder` 对象,我们在此泄露了它)控制,而不是由 `Binder.getCallingUid()` 控制。

即使在 [`ApplicationThread.scheduleReceiver()`(我们在此用它来请求代码执行)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=985-997;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) 以及 `ApplicationThread` 的其他方法中加入 `Binder.getCallingUid()` 检查(因为 `scheduleReceiver()` 并不是 `IApplicationThread` 中唯一允许加载代码的方法),仍然无法阻止使用 `IApplicationThread` 将代码加载到其他应用的进程中,因为攻击者可以将泄露的 `IApplicationThread` 传递给 `attachApplication()`,以取代自己的句柄。

除了将代码加载到进程之外,拥有 `IApplicationThread` 还允许[以该句柄所属进程的权限调用 `grantUriPermission()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5631-5632;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)。
下载工具
initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative);
source
mParcelledData
Bundle
Parcel
recycleParcel
true
Parcel
Bundle
Parcel.recycle()
  • initializeFromParcel calls recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader) 以读取键值映射内容。键是 String,值是使用 readLazyValue() 读取的,对于带有长度前缀写入的值类型,会创建 LazyValue 对象。readArrayMap() 返回一个值,指示是否可以回收 Parcel。如果存在任何 LazyValue 对象,recycleParcel 将被设置为 false,并且 LazyValue 引用的 Parcel 不会被回收(有一个例外,但与此处无关,我将在“附加说明:Bundle.clear()”部分中描述)
  • unparcel() 完成后,mMap 被设置(非 null),并将 String 键映射到实际值(如果已准备就绪)或 LazyValue 对象
  • 之后,调用 getValue(),它将键(String)映射到索引(int)并将其传递给 getValueAt()
  • getValueAt() 通过 instanceof BiFunction 检测 LazyValue 并调用 apply() 将其反序列化
  • LazyValue.apply() 将 Parcel 重置到 LazyValue.mPosition 的位置,并调用普通的 Parcel.readValue(),这一点我已经介绍过
  • 反序列化成功后,LazyValue 被替换到 mMap 中,这样下次对同一键调用 Bundle.get*() 时将直接返回值,不会重复进行 LazyValue 反序列化。当 Bundle 被转发时,该值将被重新序列化,而不是按原样复制原始数据(但是,在读取转发的 Bundle 之后,该值将再次成为 LazyValue,任何可能的 writeToParcel/createFromParcel 不匹配都不会影响其他值)
  • Parcelable.writeToParcel
  • 如果我们接近 Binder 事务大小的限制,则写入 0,表示此事务中没有更多项目,下一个项目将在另一个事务中发送
  • 一旦 ParcelableListBinder 接收到第一个事务中指定的元素数量,它就会调用传给其构造函数的 lambda,在这种情况下,将检索到的列表分配给 MediaSessionRecord.mQueue
  • 从 Parcel 读取 ParceledListSlice 时,它首先直接从 Parcel 读取第一部分,然后如果并非所有元素都内联写入,则调用写入 Parcel 中的 Binder 来检索这些项目
  • Parcel
    Parcel
    Parcel.obtain(),它使用 Parcel.sOwnedPool
    Binder
    系统会调用 Parcel.obtain(long obj)
    使用 Parcel.sHolderPool
    Parcel.recycle()
    将 Parcel 对象返回到相应的对象池
    RemoteViews
    Parcel
    ValueLeakerMaker 类
    makeOwnedLeaker
    makeHolderLeaker
    RemoteViews.readActionsFromParcel()
    调用 getActionFromParcel()
    ReflectionAction
    读取常见的 BaseReflectionAction 参数
    构造 Bundle