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

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

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

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

工具目录

分类

查看所有分类
Loading categories
ThisSeemsWrong — 针对 CVE-2024-49746 的分析与漏洞利用:Android 的 Parcel::continueWrite 关闭了后续仍会被使用的文件描述符 | Kitploit
工具/GitHubGitHub/michalbednarski/thisseemswrong
Android安全漏洞利用框架漏洞分析信息收集Payload 开发二进制利用
GitHubmichalbednarski/thisseemswrong

ThisSeemsWrong

针对 CVE-2024-49746 的分析与漏洞利用:Android 的 Parcel::continueWrite 关闭了后续仍会被使用的文件描述符

查看仓库
471510个月前Kitploit 审核通过

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

此问题的修复以 CVE-2024-49746 的形式出现:公告,补丁

"这看起来不对"

上面的标题是来自 Parcel::continueWrite 方法 的注释,该方法实际上负责调整 Parcel 对象的大小,无论是用户显式请求时(例如通过 setDataSize()),还是在当前数据容量过小时调用某个 write 方法```cpp status_t Parcel::continueWrite(size_t desired) { // SNIP: Validate desired size // SNIP: Assign kernelFields & rpcFields from variant member of this class // SNIP: Count number of objects (Binder handles and File Descriptors) // that will be present after resize and assign to objectsSize

root@kitploit:~
if (mOwner) {
    // If the size is going to zero, just release the owner's data.
    if (desired == 0) {
        freeData();
        return NO_ERROR;
    }

    // If there is a different owner, we need to take
    // posession.
    uint8_t* data = (uint8_t*)malloc(desired);
    // SNIP: Check if malloc succeeded
    binder_size_t* objects = nullptr;

    if (kernelFields && objectsSize) {
        objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
        // SNIP: Check if calloc succeeded

        // Little hack to only acquire references on objects
        // we will be keeping.
        size_t oldObjectsSize = kernelFields->mObjectsSize;
        kernelFields->mObjectsSize = objectsSize;
        acquireObjects();
        kernelFields->mObjectsSize = oldObjectsSize;
    }
    // SNIP: rpcFields handling for non-/dev/binder Parcels

    if (mData) {
        memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
    }
    if (objects && kernelFields && kernelFields->mObjects) {
        memcpy(objects, kernelFields->mObjects, objectsSize * sizeof(binder_size_t));
    }
    // ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
    if (kernelFields) {
        // TODO(b/239222407): This seems wrong. We should only free FDs when
        // they are in a truncated section of the parcel.
        closeFileDescriptors();
    }
    mOwner(mData, mDataSize, kernelFields ? kernelFields->mObjects : nullptr,
           kernelFields ? kernelFields->mObjectsSize : 0);
    mOwner = nullptr;

    // SNIP: Allocation count tracking
    // SNIP: Assign data and objects to this object
} else if (mData) {
    // SNIP: Resize data owned by this instance of Parcel
} else {
    // SNIP: Allocate initial data for currently empty Parcel
}

return NO_ERROR;

}

root@kitploit:~
[自引入该注释以来](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/),`closeFileDescriptors()` 调用从 `IPCThreadState::freeBuffer()`(在上面的代码中通过 `mOwner()` 函数指针调用)移动到了 `continueWrite()` 方法中,但逻辑与之前相同。毕竟,`Parcel` 是 Android IPC 的核心部分,如果核心 IPC 在不应该关闭文件描述符的时候关闭它们,那显然会是个问题。

这就引出了重要部分:上面的代码在什么时候被使用?当 `Parcel` 类移动从 Binder 驱动接收的数据的所有权时(此时这些数据位于 [`/dev/binder` `mmap`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567),并且无法写入(任何写入该内存的尝试都会导致 `SIGSEGV`)),也就是说 `Parcel` 要么是传入的事务数据(作为 `data` 参数传给 [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))),要么是传入的回复(即作为 `reply` 参数传给 [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) 调用的 `Parcel` 对象,`transact()` 会在该 `Parcel` 对象内设置引用)。

在实践中,我们唯一会进入 `if (mOwner)` 块的情况是系统 [调用 `setDataSize(0)` 来释放事务数据](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567),但在那种情况下我们也会进入 `if (desired == 0)`,它会提前返回。在合法的系统使用过程中,没有任何情况下我们会进入“如果存在不同的所有者,我们需要接管”这条路径。

# 触发“接管”路径

在我之前的一个漏洞利用中,我展示了 [`createFromParcel()` 实际上可以在它本应读取的 `Parcel` 上调用 `writeInt(0)` 的情况](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects)。虽然那里的修复阻止了 `AccountManagerService` 中任何非 `Intent` 的 `createFromParcel()` 方法的执行,但从 `createFromParcel()` 到 `writeInt(0)` 的路径被保留了下来。

回顾一下,[在 `PackageParser` 内部有如下代码](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759):```java
final Class<T> cls = (Class<T>) Class.forName(componentName);
final Constructor<T> cons = cls.getConstructor(Parcel.class);

intentsList = new ArrayList<>(N);
for (int i = 0; i < N; ++i) {
    intentsList.add(cons.newInstance(in));
}

因此,我们可以将传递给 createFromParcel 的 Parcel 对象传递给系统中任何可用的、接受单个 Parcel 参数的 public 构造函数

而在其他地方,我们有以下代码:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }

root@kitploit:~
因此,为了触发“接管”路径,我们需要在传递给 `onTransact()` 的 `Parcel` 上(作为 `data`)进行任意的 `readParcelable` 调用,在本漏洞利用中,我使用的是 [之前在不同漏洞中使用的同一路径](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them)。我让 `readParcelable` 调用 `PackageParser$Activity.CREATOR.createFromParcel()`,而后者又会读取 `PooledStringWriter` 名称并调用其构造函数,之后 `Parcel` 数据结束,因此 `writeInt()` 需要重新分配 Parcel,从而进入我们的“接管”路径

这里值得注意的是,如果那时 `Parcel` 数据没有结束,`writeInt()` 就会尝试就地覆写数据,而在数据由 `/dev/binder` `mmap` 支持的情况下,这将导致 `SIGSEGV`

# File Descriptor Sanitizer

我最初的想法是让“接管”路径关闭文件描述符,之后在事务结束时同样的描述符会被再次关闭,但在这两件事之间,我会在另一个事务中把其他文件描述符放入 `system_server`,稍后再取回我的文件描述符,因为此时该 FD 指向的是不同的文件

在使用旧版 AOSP 的模拟器上这个方法有效,但当我尝试更新版本时,这个计划被 [File Descriptor Sanitizer (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md) 阻止了

具体来说,[在 `android-14.0.0_r29` 中,FDSan 的覆盖范围扩大到涵盖 `Parcel` 内的 FD](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/)

事实上,在 FDSan 覆盖 Parcel 之后,当 `Parcel` 包含 FD 时,我甚至无法到达 `closeFileDescriptors()` 调用。在该调用之前,会有对 `acquireObjects();` 的调用,它会获取对 `Binder` 句柄的引用(这些引用稍后会在该函数内由 `mOwner()` 调用释放),然而 `acquireObjects()` 还会 [为 FD 设置 FDSan 标签](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=175-178;drc=f4c9b48c19f1b040efb35932b322f47e7779cafe):```cpp
case BINDER_TYPE_FD:
    if (obj.cookie != 0) { // owned
        FdTag(obj.handle, nullptr, who);
    }

问题在于我们已经对从内核接收到的 FD 打了标签```cpp // In Parcel::ipcSetDataReference, which assigns this Parcel object to data from kernel (mOwner != null) if (type == BINDER_TYPE_FD) { // FDs from the kernel are always owned FdTag(flat->handle, nullptr, this); }

root@kitploit:~
因此,双重 `FdTag`(在不关闭或更改 tag 的情况下指定预期的旧 tag)会导致 FDSan 错误,从而中止进程,而且我们甚至还没有执行到 `closeFileDescriptors()` 调用。由于“接管”路径在正常使用期间是死代码,此类问题可能不会被注意到

然而,我们可以看到 `if (obj.cookie != 0)` 条件。如果该条件为假,则意味着 `Parcel` 中的 FD 并非真正由该 `Parcel` 拥有,只要该 `Parcel` 存在,保持这些 FD 打开就是 `Parcel` 使用者的责任。但由于这个 `Parcel` 刚从内核传来,`cookie` 值实际上来自原始进程,并且当 `Parcel` 确实具有 `mOwner` 时,这些值被认为无关紧要。但“接管”路径实际上并未考虑这一点,而只是复制 `cookie` 值

综合以上所有内容,通过在发送方将 `cookie` 值设为零,我们可以得到一个引用已关闭文件描述符的 `Parcel`,但它并不认为自己拥有这些描述符,这意味着它不会再次关闭它们。这让我们能够避免触发 FDSan,但也消除了所有双重关闭的利用路径

# Parcel 的 Java 端技巧

这类 FD 仍然可以传递给另一个 `Parcel`(然后再传给另一个进程),然而我们触发这种悬空 FD 创建的方式涉及通过反射构造 `PooledStringWriter`,之后当我们尝试将其 `add()` 到 `ArrayList<IntentInfo>` 时,会抛出 `ClassCastException`

我们需要:

* 位于作为 `data` 参数传递给 `onTransact()` 的 `Parcel` 的末尾
* 构造 `PooledStringWriter`,之后 Parcel 将拥有悬空 FD,但也会抛出 `ClassCastException`
* 等待我们想要泄露的 FD 在 `system_server` 中被分配
* 让来自该 `Parcel` 的 FD 被复制到某个将发送给我们进程的其他 `Parcel` 中

为了满足所有这些要求,我需要同时使用一些旧技巧和新技巧

## 旧技巧

让我们从回顾旧技巧开始,其中大多数已经在我此前发布的 [使用 `LazyValue` 在 `recycle()` 后操作 `Parcel` 的利用](https://github.com/michalbednarski/LeakValue) 中描述过

1. [`RemoteViews` 类在设置了 `Parcel.ReadWriteHelper` 的情况下,对其包含的 `Bundle` 执行反序列化](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4)。[当设置 `ReadWriteHelper` 时,`Bundle` 不会被立即反序列化,而是惰性反序列化](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545)。另外值得注意的是,在 `RemoteViews` 下,该 `Bundle` 中包含的所有 `Bundle` 也会被立即反序列化,而不仅仅是直接位于 `RemoteViews` 内部的那一个
2. [如果在 `system_server` 内反序列化 `Bundle` 时抛出 `BadParcelableException`,该 `BadParcelableException` 将被静默捕获,并且 `Bundle` 内容将被清除](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e)。不过请注意,我们在 `createFromParcel` 中写入的触发器抛出的是 `ClassCastException`,它不会在这里被捕获
3. [`ParceledListSlice` 类在反序列化期间会对序列化数据中指定的对象进行阻塞的外向 Binder 调用](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364),我们可以利用它来拖延反序列化的执行

现在这些都会用到,但这还不是全部

## 新技巧

[AIDL 是用于生成 RPC 接口实现的工具](https://developer.android.com/guide/components/aidl),但除此之外,它还能够生成 `Parcelable` 结构实现

这些结构带有长度前缀,因此在系统内,同一结构的不同版本是兼容的,只要没有在中间添加字段(也就是说,如果一个版本是另一个版本的前缀,则版本兼容)

让我们看看 AIDL 为 [`ReceiverInfo` 结构](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ReceiverInfo.aidl) 生成了什么代码:```java
public final void readFromParcel(android.os.Parcel _aidl_parcel)
{
  int _aidl_start_pos = _aidl_parcel.dataPosition();
  int _aidl_parcelable_size = _aidl_parcel.readInt();
  try {
    if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
    if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
    intent = _aidl_parcel.readTypedObject(android.content.Intent.CREATOR);
    if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
    data = _aidl_parcel.readString();
    if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
    extras = _aidl_parcel.readTypedObject(android.os.Bundle.CREATOR);
    // SNIP: Other fields
  } finally {
    if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
      throw new android.os.BadParcelableException("Overflow in the size of parcelable");
    }
    _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
  }
}

这段代码让我们能够完成剩余的两个技巧,在触发“take possession”路径后,我们需要这两个技巧来执行后续操作;该路径要求我们位于 Parcel 末尾,并会抛出 ClassCastException。

首先,我们从 Parcel 中读取长度,使用它跳过在写入版本中不存在的字段,最后我们将根据该长度调整 Parcel 内的位置。我们不允许移动到该 AIDL Parcelable 的位置之前(移动到 Parcel 末尾之后是可能的,但实际上不会造成任何危险)。然而,我们可以移动到已读取对象的中间,因此我们可以在第一次读取时到达 Parcel 末尾,让 PooledStringWriter 被构造出来,然后回退到该 ReceiverInfo 对象内部某个数据的中间。

这样就解决了位于 Parcel 末尾的问题,但仍存在 ClassCastException 问题。不过,这里有一个 finally 块,它在调用 setDataPosition() 之前会验证是否没有溢出。如果发生溢出,将抛出 BadParcelableException。那么,当另一个 Exception 处于待处理状态时,如果 finally 块抛出 Exception,会发生什么?在 finally 内部抛出的异常具有优先权,之前的 Exception 会静默消失。现在我们得到的不是 ClassCastException,而是 BadParcelableException,Bundle 会很贴心地忽略它,以避免在 system_server 中出现 Exception。

关于 ParcelableListBinder 补丁的说明

我们使用 MediaSession 的 ParcelableListBinder 将任意 Parcelable 放入 system_server,之后再将那个对象取回。

最近有一个 ParcelableListBinder 补丁,它恰恰禁止了这种行为。

从这个漏洞利用的角度来看,这个补丁实际上并不能阻止我,但我需要区分它存在和不存在两种情况来处理。

如果该补丁存在,非 QueueItem 项会从 system_server 接收到的列表中被静默移除(不会引发 Exception)。由于我们需要非 QueueItem 来产生副作用,我们可以将非 QueueItem 放在第一项,后面再跟一个真正的 QueueItem。这样虽然不允许我们执行任意 Parcelable 反序列化,但其中包含的 Bundle 可以包含文件描述符(我们正是希望这些描述符被传递给我们的进程)。

如果该补丁不存在,我们就不会遇到这个障碍,但我们也不能使用同样的流程。因为在上述情况下,列表中会同时包含 RemoteViews 和 QueueItem,因此 ParceledListSlice 会拒绝传输混合类型的列表。在这种情况下,我需要将泄漏的 FD 放入 RemoteViews 实例中。

不过,更有意思的是这个补丁被引入的原因。提交信息提到“允许应用从后台启动”,而且虽然 Android 安全公告没有说明太多,我们可以从 CVE 条目中找到有用信息,它指向 Notification.mAllowlistToken 字段,该字段 在读取时可以从静态字段中获取,而 在 system_server 内部,它是允许后台启动 Activity 的令牌,之后 该令牌会在 Notification.writeToParcel() 中被写入。这是否意味着,现在所有 system_server 反序列化任意 Parcelable 并将其发送回应用的情况都成了漏洞?无论如何,目前这只是一个想法,在这个漏洞利用中我无论如何都想做更多。

整合起来

我认为这个漏洞利用拥有我迄今为止做过的最复杂的 Parcelable gadget 链:

  • RemoteViews (1)
    • ReflectionAction (2)
      • Bundle
        • Parcelable[] (3)
          • ReceiverInfo (用于 seek,4 和 10)
            • Intent
              • ComponentName (段 A,5)
                • 可选的填充文件描述符
                • ParceledListSlice (11)
                • ParcelableParcel 或 QueueItem (12)
              • Bundle (用于 catch,段 B,6)
                • ReceiverInfo (用于 rethrow,7)
                  • Bundle (8)

上面列表中的 “segment A” 和 “B” 标注指的是我的 FdLeaker.java 类中 “START A”/“END A”/“START B”/“END B” 注释之间的代码块;数字则指向下面列表中的各个点。

上面的树从发送者的角度描述了层次结构,而从接收者的角度来看,情况略有不同:

  1. 我们从 ParcelableListBinder 接收数据,最外层的对象是 RemoteViews。
  2. 在那个 RemoteViews 中嵌套着一个 Bundle。RemoteViews 会 设置 Parcel.ReadWriteHelper,因此这个 Bundle 以及其中所有的 Bundle 都会被急切地读取。这是必要的,否则我们将无法从 ReceiverInfo.readFromParcel() 执行任意的 readParcelable。
  3. 这里的 Parcelable[] 只是一个便捷包装,用于把我要放入 RemoteViews 的所有 Parcelable 分组。

那么,我们能获取哪些文件描述符?

让我们从高层次来看一下上述攻击:

  1. 在 system_server 内打开一个文件描述符,记住它的引用,然后关闭该 FD。
  2. 我触发 system_server 打开另一个不同的文件描述符。
  3. 我在第 1 步中创建的悬空文件描述符被发送回给我。

这个攻击有一个重大的局限性:我们无法获取在攻击开始之前就已经打开的文件描述符。

不过,仍然有少数有用的事情可以做。

获取 InputChannel

老实说,这是我在没有额外假设的情况下唯一能使其工作的漏洞利用变体。

输入事件,也就是来自触摸屏和键盘的事件,由应用通过来自 system_server 的 UNIX 套接字接收。当 Activity 启动或向系统添加新窗口时,会创建一个新的 InputChannel,这反过来意味着 会创建一个 UNIX 套接字对,并且 其中一端会被发送给应用,另一端则由 system_server 用于发送事件。

通过这些套接字发送的结构布局有明确定义(因为它们必须在 32 位和 64 位进程之间兼容),而且看起来意外的序列号不会引起任何问题。此外,InputChannel 似乎是 startActivity() 调用之后在 system_server 内分配的唯一套接字,因此可以很容易地确定哪个 FD 是 InputChannel 的服务端套接字。

“关于手机”设置屏幕的截图,带有“设备名称”对话框,其中输入了“key injection demo”

虽然这只是一个玩具示例,但我们也可以批准权限提示或应用安装,启用媒体投影或无障碍服务。

启动期间获取与 zygote 的连接

这个想法基本是理论性的。我能在运行缓慢的模拟器上实施此攻击,但在真机上竞争窗口(race window)太小了。

system_server 只在启动时打开一次到 /dev/socket/zygote 的连接,之后所有请求都通过该连接发送。

在 system_server 启动期间,MediaSessionService(用于与 system_server 之间收发 Parcelable)会在与 zygote 建立连接之前发布到 servicemanager。

因此,理论上应用可以启动一个辅助进程,让 system_server 崩溃,然后从那个后台进程在 system_server 启动期间执行攻击。

通过 SensorService 获取与 zygote 的连接

我还发现了另一个 bug,这是 SensorService::createSensorDirectConnection() 方法```cpp sp SensorService::createSensorDirectConnection( const String16& opPackageName, int deviceId, uint32_t size, int32_t type, int32_t format, const native_handle *resource) { // SNIP: Reject direct connections when sensor privacy is enabled // SNIP: Irrelevant parameter checks

root@kitploit:~
// check specific to memory type
switch(type) {
    case SENSOR_DIRECT_MEM_TYPE_ASHMEM: { // channel backed by ashmem
        if (resource->numFds < 1) {
            ALOGE("Ashmem direct channel requires a memory region to be supplied");
            android_errorWriteLog(0x534e4554, "70986337");  // SafetyNet
            return nullptr;
        }
        // SNIP: Further validation for SENSOR_DIRECT_MEM_TYPE_ASHMEM
    }
    case SENSOR_DIRECT_MEM_TYPE_GRALLOC:
        // no specific checks for gralloc
        break;
    default:
        ALOGE("Unknown direct connection memory type %d", type);
        return nullptr;
}

native_handle_t *clone = native_handle_clone(resource);
if (!clone) {
    return nullptr;
}
native_handle_set_fdsan_tag(clone);

sp<SensorDirectConnection> conn;
int channelHandle = 0;
if (deviceId == RuntimeSensor::DEFAULT_DEVICE_ID) {
    // SNIP: usual case where sensor belong to this device (not app streaming)
} else {
    auto runtimeSensorCallback = mRuntimeSensorCallbacks.find(deviceId);
    if (runtimeSensorCallback == mRuntimeSensorCallbacks.end()) {
        ALOGE("Runtime sensor callback for deviceId %d not found", deviceId);
    } else {
        int fd = dup(clone->data[0]);
        channelHandle = runtimeSensorCallback->second->onDirectChannelCreated(fd);
    }
}
// SNIP: Return connection

}

root@kitploit:~
我们有一个 `dup(clone->data[0])` 调用。`clone` 是从远程进程接收到的 `native_handle_t`。native handle 在 data 中包含一定数量的 FD 和一定数量的普通整数,native handle 的使用者在访问 `data` 之前,应通过[查看 `numFds` 和 `numInts`](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e)来检查这些数量。

这里甚至还有一个“特定于内存类型的检查”部分,它会检查,对于 `SENSOR_DIRECT_MEM_TYPE_ASHMEM`,这里会进行检查;对于 `SENSOR_DIRECT_MEM_TYPE_GRALLOC`,native handle 的格式是设备特定的,无法在这里验证。问题在于,对于“运行时传感器”,其类型总是被视为 `SENSOR_DIRECT_MEM_TYPE_ASHMEM`,但在这种情况下,我们可以指定 `SENSOR_DIRECT_MEM_TYPE_GRALLOC` 来绕过验证。

然而,这段代码只有在存在“运行时传感器”时才可达。我不确定这具体会发生在什么情况下,我认为是当用户使用“Nearby app streaming”时(?)

不过为了测试,我添加了一个小型类,允许注册 [`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary),你可以通过它来```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'

此后,就可以在生产设备上以 system uid 执行代码。

应用显示带有文件描述符列表的长文本,其末尾有:"device=2 fd=181", "Found zygote, sending request", "uid=1000(system) gid=1000(system), groups=1000(system),1065(reserved_disk),3009(readproc) context=u:r:system_app:s0"

下载工具
  • PackageParser$Activity (9)
    • PooledStringWriter
  • 最外层的 ReceiverInfo 定义的长度落在其数据中间,不过在读取时这一点还未知,Intent 会正常地从它里面读出。
  • 我们之后需要读取的内容,是通过 静态 ComponentName.readFromParcel() 发起的 readString 调用读取的 (使用 ComponentName 是因为无论 Android 版本如何,它都使用 UTF-16 的 readString;该 readString 仍会返回 null,因为这个字符串与 Binder 对象重叠,所以尽管 ComponentName 的构造已经跳过了这一轮中隐藏的数据,ComponentName 对象并未被创建)。
  • 我们进入第二个嵌套的 Bundle,在反序列化这个 Bundle 结束时,会捕获一个 BadParcelableException。
  • 然后我们进入第二个 ReceiverInfo。这个 ReceiverInfo 的长度被指定为 Integer.MAX_VALUE,因此会在 finally 块中抛出 BadParcelableException,从而静默丢弃 ClassCastException。
  • 第三个 Bundle。这只是为了能够从 ReceiverInfo 触达任意的 readParcelable。现在我们位于 RemoteViews 内部,所以 Bundle 会被急切地读取,因此可以做到这一点。
  • PackageParser$Activity + PooledStringWriter 组合会在正在被读取的 Parcel 上触发 writeInt(0) 调用。由于我们位于 Parcel 末尾,writeInt() 必须扩展 Parcel 容量,从而触发“take possession”路径。这个组合还会导致 ClassCastException,但正如上面第 7 步和第 6 步所述,它会被吞掉。
  • 我们到达外层 ReceiverInfo 的末尾,ReceiverInfo 会根据其头部的长度将位置定位到之前 ComponentName 内部的数据中间,这些数据会作为 Parcelable[] 中的下一项被读取。
  • 这里有一个 ParceledListSlice,它会向我的进程发起一次阻塞式 Binder 事务。此时这个 Parcel 中定义的文件描述符已经被关闭,但还没有任何有趣的东西取代它们。在此反序列化等待这次调用返回期间,我可以让系统打开一些有趣的文件描述符,然后它们会被发送给我。
  • 这一部分是从这个 Parcel 中取出文件描述符并发送给我的地方。ParcelableListBinder 是否过滤条目,处理方式有所不同。 a. 如果 ParcelableListBinder 不过滤条目,FD 会被保存在 ParcelableParcel 中。ParcelableParcel 与 Bundle 类似,使用 Parcel.appendFrom() 逐字复制 Parcel 数据,但它没有特殊的 hasReadWriteHelper() 逻辑,所以即使位于 RemoteViews 的 Bundle 之下,它也会这样做。 b. 如果 ParcelableListBinder 过滤条目,那么这就是 RemoteViews 的结尾,RemoteViews 对象会被 ParcelableListBinder 丢弃,但这对我来说没问题,因为副作用已经发生。ParcelableListBinder 接收到的下一个条目是 QueueItem,它包含 MediaDescription,而 MediaDescription 又 包含 Bundle,我泄漏的 FD 就保存在这个 Bundle 中。