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

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

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 关闭了后续仍会被使用的文件描述符

查看仓库
4715611个月前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

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;

}

[自引入该注释以来](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. }

因此,为了触发“接管”路径,我们需要在传递给 `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); }

因此,双重 `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 端技巧
下载工具