Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
ThisSeemsWrong — CVE-2024-49746에 대한 라이트업 및 익스플로잇: 이후에 사용되는 파일 디스크립터를 닫는 Android의 Parcel::continueWrite | Kitploit
도구/GitHubGitHub/michalbednarski/thisseemswrong
Android SecurityExploit FrameworksVulnerability AnalysisInformation GatheringPayload DevelopmentBinary Exploitation
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()` 호출은 (위 코드에서 `mOwner()` 함수 포인터를 통해 호출되는) `IPCThreadState::freeBuffer()`에서 `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`은 수신 트랜잭션 데이터(`onTransact()`에 전달되는 `data` 인자)이거나 수신 reply(`transact()` 호출에 `reply` 인자로 전달된 `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. }

따라서 "take possession" 경로를 트리거하려면 `onTransact()`에 `data`로 전달된 `Parcel`에 대해 임의의 `readParcelable` 호출이 필요합니다. 이 익스플로잇에서는 [이전에 다른 익스플로잇에서 사용했던 동일한 경로](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them)를 사용합니다. `readParcelable`이 `PackageParser$Activity.CREATOR.createFromParcel()`을 호출하게 하고, 이어서 `PooledStringWriter` 이름을 읽고 생성자를 호출한 후 `Parcel` 데이터가 끝나므로, `writeInt()`가 Parcel을 재할당해야 하며 우리의 "take possession" 경로로 진입하게 됩니다.

여기서 주목할 점은, 그 시점에 `Parcel` 데이터의 끝이 아니었다면 `writeInt()`는 데이터를 제자리에서 덮어쓰려 시도할 것이고, `/dev/binder` `mmap`으로 뒷받침되는 데이터의 경우 `SIGSEGV`가 발생했을 것입니다.

# 파일 디스크립터 샌나타이저

제 초기 아이디어는 "take possession" 경로가 파일 디스크립터를 닫도록 하고, 그 후 트랜잭션이 끝날 때 동일한 디스크립터가 다시 닫히도록 하는 것이었습니다. 그러나 그 사이에 다른 트랜잭션으로 `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` (예상되는 이전 태그를 지정하면서 태그를 닫거나 변경하지 않음)는 FDSan 오류를 유발하고, 이는 프로세스를 중단시키며, 우리는 아직 `closeFileDescriptors()` 호출에 도달하지도 못했습니다. "소유권 이전( take possession )" 경로는 정상적인 사용 중에는 죽은 코드(dead code)이므로 이러한 문제는 눈에 띄지 않을 수 있습니다
도구 다운로드