Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
ReparcelBug2 — # CVE-2021-0928을 통한 Android 12 Beta 설치 앱의 시스템 권한 상승에 대한 분석 및 익스플로잇 `OutputConfiguration`의 `writeToParcel`/`createFromParcel` 직렬화 불일치를 악용하여 설치된 앱에서 시스템 권한으로 상승하는 방법에 대한 분석 및 익스플로잇 | Kitploit
도구/GitHubGitHub/michalbednarski/reparcelbug2
Android SecurityPrivilege EscalationVulnerability AnalysisExploitationMobile SecurityBinary Analysis
GitHubmichalbednarski/reparcelbug2

ReparcelBug2

# CVE-2021-0928을 통한 Android 12 Beta 설치 앱의 시스템 권한 상승에 대한 분석 및 익스플로잇 `OutputConfiguration`의 `writeToParcel`/`createFromParcel` 직렬화 불일치를 악용하여 설치된 앱에서 시스템 권한으로 상승하는 방법에 대한 분석 및 익스플로잇

저장소 보기
123234년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2021-0928, android.hardware.camera2.params.OutputConfiguration의 writeToParcel/createFromParcel 직렬화 불일치

이것은 해당 취약점을 이용한 익스플로잇으로, 설치된 Android 앱에서 Android 설정 앱으로 권한 상승을 수행합니다 (또는 설치된 다른 앱이 AndroidManifest.xml에 선언된 <receiver>로 보낼 수 있는 모든 앱으로 권한 상승이 가능하며, <activity>로 보내는 권한 상승도 가능했지만 여기서는 다루지 않습니다)

이 문제는 원래 Android 12 Developer Preview 3에서 발견했습니다

이 저장소에 있는 익스플로잇 버전은 Android 12 Beta 2 및 3에서 작동합니다

이 취약점은 최초 공식 Android 12 릴리스에서 수정되었습니다

아래 분석 문서는 원래 이 보고서를 완전한 익스플로잇 체인으로 고려해 달라는 요청과 함께 Google에 제출하기 위해 작성되었습니다

작성 시점에 Android 12는 AOSP에서 제공되지 않았습니다 (Android Developer Preview/Beta 릴리스는 오픈 소스가 아닙니다)

설정 앱의 Android 알림 스크린샷: 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의 인사말

Parcel 소개

Android의 대부분의 IPC는 Parcel이라는 클래스를 통해 수행됩니다

Parcel의 기본 사용법은 다음과 같습니다:```java Parcel p = Parcel.obtain(); p.writeInt(1); p.writeString("Hello");

root@kitploit:~
그런 다음 `Parcel`은 [Binder를 통해](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) 다른 프로세스로 전송됩니다. 또는 테스트 목적으로 [`p.setDataPosition(0)`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int))을 호출하여 parcel을 처음 위치로 되감고 읽기를 시작할 수 있습니다:```java
int a = p.readInt(); // a = 1
String b = p.readString(); // b = "Hello"

Parcel은 내부적으로 읽기가 수행되는 위치를 보유한다는 점에 유의해야 합니다. read* 메서드가 이전에 사용된 write* 메서드와 일치하도록 하는 것은 Parcel 클래스 사용자의 책임이며, 그렇지 않으면 이후 읽기는 버퍼의 잘못된 위치에서 수행됩니다.

Parcel은 또한 사용자 정의 객체를 작성하는 기능을 제공하며, 이를 수행하는 권장 방법은 Parcelable 인터페이스를 구현하는 것입니다.

다음은 Parcelable 인터페이스 구현의 예입니다(관련 없는 코드는 제거되었으며, WindowContainerTransaction 클래스는 가젯 체인의 일부로 익스플로잇에서 사용되지만, 여기에는 문제가 없습니다).```java package android.window; public final class WindowContainerTransaction implements Parcelable { private final ArrayMap<IBinder, Change> mChanges = new ArrayMap<>(); private final ArrayList mHierarchyOps = new ArrayList<>();

root@kitploit:~
private WindowContainerTransaction(Parcel in) {
    in.readMap(mChanges, null /* loader */);
    in.readList(mHierarchyOps, null /* loader */);
}

@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
    dest.writeMap(mChanges);
    dest.writeList(mHierarchyOps);
}

@NonNull
public static final Creator<WindowContainerTransaction> CREATOR =
        new Creator<WindowContainerTransaction>() {
            @Override
            public WindowContainerTransaction createFromParcel(Parcel in) {
                return new WindowContainerTransaction(in);
            }
        };

}

root@kitploit:~
위에서 볼 수 있듯이, 쓰기 중에는 `writeToParcel()` 메서드가 사용됩니다. 그런 다음 읽을 때 `CREATOR.createFromParcel()` 팩토리 메서드가 호출됩니다. `Parcelable` 구현은 `createFromParcel`이 `writeToParcel`이 기록한 것과 동일한 양의 데이터를 읽도록 보장해야 하며, 그렇지 않으면 해당 `Parcel`에서 이후의 모든 읽기가 잘못된 오프셋에서 데이터를 읽게 됩니다.

이러한 클래스는 다음과 같은 방법으로 `Parcel`에 쓰거나 `Parcel`에서 읽을 수 있습니다:

* `obj.writeToParcel(parcel, 0)` / `obj = WindowContainerTransaction.CREATOR.createFromParcel()`을 직접 호출하는 방법. 이는 클래스 유형이 알려진 경우에 자주 사용됩니다. 예를 들어 `Parcelable`에 다른 `Parcelable` 유형의 필드가 있거나, AIDL로 생성된 코드에서 정의된 RPC 메서드가 `Parcelable`을 인자로 가질 때 사용됩니다.
* `Parcel.writeParcelable`/`readParcelable`을 통한 방법. [`writeParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=1909;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3)은 먼저 클래스 이름을 기록한 다음 `Parcelable` 인터페이스의 `writeToParcel` 메서드를 호출합니다. [`readParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3282;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3)은 기록된 클래스 이름을 읽고, 제공된 `ClassLoader`에서 해당 이름의 클래스를 찾거나, null이 제공된 경우 `BOOTCLASSPATH`에서 찾습니다. 클래스를 찾으면 해당 클래스의 정적 필드 `CREATOR`를 사용하여 [`Parcelable.Creator`](https://developer.android.com/reference/android/os/Parcelable.Creator) 인스턴스를 얻습니다. 이 인스턴스는 해당 클래스를 읽는 데 사용되는 팩토리입니다. `readParcelable` 메서드를 사용할 때 생성할 객체의 이름이 동일한 `Parcel`에서 읽히므로 클래스 경로에 있는 모든 `Parcelable`을 읽을 수 있다는 점에 유의해야 합니다.
* `readParcelable`은 다른 많은 `Parcel` 메서드에서도 사용됩니다. 예를 들어 위 예제에서 볼 수 있는 `readList`는 `readValue`를 통해 요소를 읽습니다. `readValue`는 `Parcel`에서 객체를 전송하는 가장 일반적인 메서드이며, 그 중 하나의 방식이 `readParcelable`을 통한 것입니다. 또한 위 예제에서 Java의 Type Erasure로 인해 `ArrayList<HierarchyOp> mHierarchyOps` 필드에는 실제로 제네릭 타입 선언에 지정된 타입과 호환되는 객체뿐만 아니라 Parcel이 지원하는 모든 객체가 포함될 수 있습니다.

# `writeToParcel`/`createFromParcel` 불일치

위에서 언급했듯이 `Parcelable` 인터페이스 구현은 `createFromParcel`이 `writeToParcel`이 이전에 기록한 것과 동일한 양의 데이터를 `Parcel`에서 읽도록 보장할 책임이 있습니다. `BOOTCLASSPATH`에 이 계약을 위반할 수 있는 `Parcelable`이 있으면 취약점이 발생하며, 다음과 같은 시나리오가 가능해집니다:

1. 악성 애플리케이션이 `system_server`에 결함이 있는 `Parcelable` 인스턴스와 함께 특별히 구성된 데이터를 포함하는 `Bundle` 또는 `Parcelable`을 보냅니다. 이 데이터는 3단계에서 실제로 읽히지만 2단계에서는 그대로 전달됩니다.
2. `system_server`는 `Bundle`이 안전한지 확인한 후 전달하거나, 제공된 `Parcelable`을 다음 매개변수에 중요한 데이터도 함께 전달되는 AIDL 메서드에 전달합니다(해당 매개변수에서 수신된 데이터가 수정될 수 있다면 보안 문제가 발생할 수 있음).
3. 다른 앱이 `system_server`로부터 데이터를 수신하고 이를 신뢰하지만, 결함이 있는 직렬화로 인해 실제로 보는 데이터는 `system_server`가 보내려고 의도한 데이터와 다릅니다.

위 단계에서 "OR"을 사용한 이유는 이러한 단계가 [2017년에 공개한 임의 Activity 시작으로 이어지는 기존 익스플로잇 변형](https://github.com/michalbednarski/ReparcelBug)("OR"의 왼쪽)과 다음 섹션에서 설명할 새로운 변형을 모두 설명하기 때문입니다.

# 앱에서 `BroadcastReceiver`가 실행되는 방식

Android SDK에서 사용 가능한 API를 사용하는 애플리케이션 개발자의 관점에서 [`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts)가 작동하는 방식은 다음과 같습니다. 한 애플리케이션이 [`sendBroadcast`](https://developer.android.com/reference/android/content/Context#sendBroadcast(android.content.Intent))를 호출하면(하지만 앱은 종종 시스템이 아닌 앱으로부터 Broadcast를 수신하려는 경우가 많음) 브로드캐스트된 `Intent`가 `AndroidManifest.xml`에 정의된 `<receiver>`와 매칭됩니다. 이때 시스템은 수신 애플리케이션의 프로세스를 시작하고, `<receiver android:name>` 속성에 정의된 `BroadcastReceiver` 하위 클래스를 인스턴스화한 다음 [`onReceive`](https://developer.android.com/reference/android/content/BroadcastReceiver#onReceive(android.content.Context,%20android.content.Intent)) 메서드를 호출합니다.

브로드캐스트를 수신하는 프로세스에서 발생하는 `system_server`와의 통신을 살펴보겠습니다:

* 애플리케이션 프로세스가 처음 시작되면 [ `IActivityManager.attachApplication()`을 호출](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7340;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c)합니다. 이를 통해 시스템이 애플리케이션 프로세스에 수행할 작업을 지시하는 데 사용하는 [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl) 핸들을 전달합니다.
* 시스템이 애플리케이션 프로세스에서 매니페스트에 등록된 `BroadcastReceiver`를 실행하려고 할 때, 이전 지점에서 설명한 `IApplicationThread`를 사용하여 [`scheduleReceiver`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=950;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c) 메서드를 호출합니다. 이 메서드에는 여러 인자가 있지만 여기서 가장 중요한 것은 처음 두 개입니다:
  1. `Intent intent` - 이전에 `sendBroadcast()`가 호출될 때 시스템에 전달된 Intent입니다.
  2. `ActivityInfo info` - 실행해야 할 컴포넌트에 대한 정보를 포함합니다. 이 매개변수의 값은 시스템이 Package Manager Service에서 가져옵니다. 가장 중요한 것은 이 매개변수에 전달된 데이터에 수신된 브로드캐스트를 처리하는 Java 클래스가 로드될 파일의 경로가 포함된다는 점입니다.

이 시점에서 이 새로운 익스플로잇 경로가 무엇인지 짐작할 수 있을 것입니다: `sendBroadcast()`를 호출할 때 시스템이 `scheduleReceiver`를 호출하려고 할 때 `scheduleReceiver`가 호출되는 애플리케이션이 변조된 `ActivityInfo`를 보게 만드는 `Intent`를 전달하는 것입니다.

이 새로운 익스플로잇 경로는 Android 12에서 가능해졌다는 점에 유의해야 합니다. 이전에는 `Intent`에 임의의 `Parcelable`을 넣을 방법이 없었기 때문입니다([Intent extras](https://developer.android.com/reference/android/content/Intent#putExtra(java.lang.String,%20android.os.Parcelable))는 `Bundle`에 넣어지며, `Bundle`의 전체 길이가 Parcel에 기록되고 [단일 blob으로 읽히므로](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1671-1681;drc=5d123b67756dffcfdebdb936ab2de2b29c799321) extras는 이를 포함하는 `Intent` 객체의 잘못된 해석을 유발할 수 없습니다).

# `writeToParcel`/`createFromParcel` 불일치 트리거

대부분의 경우 `writeToParcel`/`createFromParcel` 불일치는 이러한 메서드 중 하나에서 필드 중 하나가 누락되거나 두 번 기록되는 경우입니다. 이 경우 해당 객체를 보내면 항상 불일치가 트리거됩니다. (대부분의 경우 객체가 `Parcelable`이지만 실제로 프로세스 간에 사용되지 않을 때 발생하며, 그렇지 않으면 정상적인 사용 중에 빠르게 발견될 것입니다.)

하지만 이번에는 그렇지 않았고 불일치를 트리거하는 것이 명확하지 않습니다.

취약한 클래스를 살펴보겠습니다([원본은 여기](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/hardware/camera2/params/OutputConfiguration.java;drc=46c390a1c695e2dc458cb889e40559f259f60aed)에 있으며, `// New in Android 12`로 표시된 줄은 작성 시점에 AOSP에 없었으므로 수동으로 추가되었습니다) ([취약점을 처음 도입한 커밋은 여기](https://android.googlesource.com/platform/frameworks/base/+/a69b1bc58b0838e06deefb190e226774a34671e6%5E%21/#F10)에 있지만, Android 12 출시 이후에 게시되었습니다)```java
package android.hardware.camera2.params;

public final class OutputConfiguration implements Parcelable {
    private OutputConfiguration(@NonNull Parcel source) {
        int rotation = source.readInt();
        int surfaceSetId = source.readInt();
        int surfaceType = source.readInt();
        int width = source.readInt();
        int height = source.readInt();
        boolean isDeferred = source.readInt() == 1;
        boolean isShared = source.readInt() == 1;
        ArrayList<Surface> surfaces = new ArrayList<Surface>();
        source.readTypedList(surfaces, Surface.CREATOR);
        String physicalCameraId = source.readString();
        boolean isMultiResolution = source.readInt() == 1; // New in Android 12
        ArrayList<Integer> sensorPixelModesUsed = new ArrayList<Integer>(); // New in Android 12
        source.readList(sensorPixelModesUsed, Integer.class.getClassLoader()); // New in Android 12

		// SNIP: copy values from variables set above to fields of this class
    }

    public static final @android.annotation.NonNull Parcelable.Creator<OutputConfiguration> CREATOR =
            new Parcelable.Creator<OutputConfiguration>() {
        @Override
        public OutputConfiguration createFromParcel(Parcel source) {
            try {
                OutputConfiguration outputConfiguration = new OutputConfiguration(source);
                return outputConfiguration;
            } catch (Exception e) {
                Log.e(TAG, "Exception creating OutputConfiguration from parcel", e);
                return null;
            }
        }

        @Override
        public OutputConfiguration[] newArray(int size) {
            return new OutputConfiguration[size];
        }
    };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        if (dest == null) {
            throw new IllegalArgumentException("dest must not be null");
        }
        dest.writeInt(mRotation);
        dest.writeInt(mSurfaceGroupId);
        dest.writeInt(mSurfaceType);
        dest.writeInt(mConfiguredSize.getWidth());
        dest.writeInt(mConfiguredSize.getHeight());
        dest.writeInt(mIsDeferredConfig ? 1 : 0);
        dest.writeInt(mIsShared ? 1 : 0);
        dest.writeTypedList(mSurfaces);
        dest.writeString(mPhysicalCameraId);
        dest.writeInt(mIsMultiResolution ? 1 : 0); // New in Android 12
        dest.writeList(mSensorPixelModesUsed); // New in Android 12
    }

    private ArrayList<Surface> mSurfaces;
    private final int mRotation;
    private final int mSurfaceGroupId;
    private final int mSurfaceType;
    private final Size mConfiguredSize;
    private final int mConfiguredFormat;
    private final int mConfiguredDataspace;
    private final int mConfiguredGenerationId;
    private final boolean mIsDeferredConfig;
    private boolean mIsShared;
    private String mPhysicalCameraId;
    private boolean mIsMultiResolution; // New in Android 12
    private ArrayList<Integer> mSensorPixelModesUsed; // New in Android 12
}

그래서 여기서 무엇이 잘못되었고 왜 이 변경이 취약점을 도입하는가? WindowContainerTransaction 예제 Parcelable을 논의하면서 말했듯이, readList는 실제로 Parcel이 지원하는 모든 객체로 리스트를 채울 수 있으며, 제네릭 선언(ArrayList<Integer>)과 일치하는 객체만 채우는 것이 아니다. 그러나 우리는 이 클래스를 직렬화 가젯 체인의 일부로만 사용하고 실제로 사용하지 않기 때문에, 해당 필드는 Parcel에 읽고 쓰는 것 외에는 아무것도 사용되지 않을 것이다(그리고 제네릭 선언과 일치하지 않는 요소를 포함하는 ArrayList의 요소를 사용하려는 시도는 어차피 ClassCastException을 초래할 뿐이다). 그 자체로는 문제가 되지 않는다.

이 클래스에는 createFromParcel 내에 try-catch도 있는데, 이는 읽는 동안 Exception이 발생하면 OutputConfiguration 읽기가 중지되고 OutputConfiguration을 포함하는 객체의 읽기가 계속된다는 것을 의미한다. 그렇게 되면 전체 OutputConfiguration이 Parcel에 기록되지만 Exception이 발생한 지점까지만 읽힌다. 이는 OutputConfiguration.writeToParcel 내에 기록된 소비되지 않은 데이터가 실제로 OutputConfiguration.CREATOR.createFromParcel을 호출한 객체에 의해 읽히게 되므로 불일치가 발생한다.

이제 이 두 가지를 결합하면(Parcel이 지원하는 임의의 객체를 중첩할 수 있게 하고, 이를 재throw 없이 try-catch로 감싸는 것) system_server에 의해 기록되고 나중에 system_server에 의해 전달되는 Parcelable을 처음 구성한 앱에 의해 제어되는 방식으로 읽힐 수 있는 Parcelable을 구성할 수 있다.

좋아, 그럼 이제 그러한 Parcelable을 구성하기 위해 mSensorPixelModesUsed에 넣을 무언가를 찾아야 한다. 그것은 system_server에서 성공적으로 읽히고(이 객체는 공격자 앱에서 Parcel을 통해 수신되므로), system_server에 의해 성공적으로 기록되고, 그 다음 피해자 앱에서 unparcel에 실패하여 Exception을 던질 것이다.

이를 수행하는 한 가지 방법은 system_server에는 존재하지만 앱에는 없는 클래스를 사용하여 역직렬화를 시도하면 ClassNotFoundException이 발생하도록 하는 것이다. 그러나 명시적으로 ClassLoader를 지정하지 않은 readParcelable은 BOOTCLASSPATH만 검색하므로 system_server 특정 클래스를 포함하지 않기 때문에 system_server에서 Parcelable을 선택할 수 없다. 이 문제에 대한 해결책은 ObjectInputStream이 스택 추적에서 첫 번째 비-BOOTCLASSPATH 메서드에서 ClassLoader를 선택하므로 Serializable 클래스 중 하나를 사용하는 것이다.

PackageManagerException을 선택했지만, 사용하기 전에 한 가지 더 해야 할 일이 있다. OutputConfiguration 생성자에서 readList가 호출될 때 loader 인수가 명시적으로 Integer.class.getClassLoader()로 설정된다. 해당 loader 값은 readValue()로 전파되고, 그 다음 readSerializable()로 전달되며, readSerializable() 내에서 loader 매개변수가 null이 아니면 ObjectInputStream의 resolveClass 대신 사용된다(그 검사는 이 클래스를 찾지 못하면 null을 반환하는 대신 예외를 던지기 때문에 아무것도 하지 않는다). 해결 방법은 매우 간단하다. 을 를 지정하지 않고 를 수행하는 일부 로 감싸기만 하면 된다. 이것이 위에서 설명한 클래스가 등장하는 지점이다.

그래서 이 시점에서 우리는 다음과 같은 객체를 가지고 있다:

  • OutputConfiguration
    • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
      • mHierarchyOps.get(0) = PackageManagerException

이제 이러한 객체는 system_server 내에서 성공적으로 역직렬화될 수 있다: WindowContainerTransaction이 readList를 호출할 때 시스템 서버의 ClassLoader( BootClassLoader가 아님)를 사용하여 PackageManagerException 클래스를 찾으려고 시도하는데, 스택 추적에서 찾을 수 있기 때문이다. 해당 클래스 로더는 스택 추적에 존재하는데, 다음 메서드들이 시스템 서버 클래스 경로에서 온 것이 아니지만(Binder#execTransact(), AIDL에 의해 생성된 IActivityManager$Stub#onTransact(), 사용된 모든 Parcelable 클래스의 메서드), 스택 추적에 시스템 서버 내에 선언된 메서드가 있었기 때문이다: ActivityManagerService 내의 재정의된 onTransact. 따라서 system_server는 이러한 객체를 읽고 나중에 에 기록할 수 있으며, 대상 앱이 이를 읽으려고 시도할 때 클래스를 사용할 수 없으므로 이 발생하고,

그래서 우리는 이 불일치를 트리거했다. 음, 아직 이 시점에서는 실제로 그렇지 않다. 왜냐하면 OutputConfiguration.writeToParcel에 의해 읽히지 않은 데이터가 남지 않았을 때 예외가 포착되었기 때문이다. 그러나 우리는 mSensorPixelModesUsed List에 다른 항목을 쉽게 추가할 수 있으며, 해당 항목은 Parcel.writeValue를 통해 기록되고 OutputConfiguration을 읽은 후 읽히지 않은 채로 남게 된다.

Intent에 넣기

위에서 언급했듯이 Intent 객체에서 불일치를 트리거하고 싶다. 왜냐하면 system_server가 첫 번째 매개변수에 Intent가 있고 두 번째 매개변수에 실행 정보가 있는 AIDL 메서드에 이를 전달할 것이기 때문이다. 따라서 첫 번째 매개변수에 전달된 Intent의 직렬화/역직렬화가 두 번째 매개변수의 값 수정으로 이어질 것이다.

Intent.readFromParcel()에서 모든 값은 전용 타입 메서드를 통해 읽히므로 사용자 정의 Parcelable 클래스를 지정할 수 없다.

그러나 Intent 내에는 중첩된 ClipData가 있으며, Android 12부터 ClipData$Item에는 새로운 필드 ActivityInfo mActivityInfo가 있다(초기 작성 당시 AOSP에는 없었으며, 해당 필드를 도입한 커밋은 여기 있다). 이 필드는 ClipData(Parcel in) 생성자 내에서 in.readTypedObject(ActivityInfo.CREATOR)를 통해 읽힌다.

그런 다음 ActivityInfo(Parcel source) 생성자 내에서도 사용자 정의 Parcelable을 넣을 방법이 없지만, ActivityInfo가 ComponentInfo를 확장하므로 applicationInfo 필드가 있다.

마지막으로 ApplicationInfo에는 SparseArray<int[]> splitDependencies 필드가 있으며, 이는 readSparseArray를 통해 읽히고, 이는 다시 readValue를 사용하여 SparseArray 항목을 읽는다.

이 시점에서 splitDependencies 내에 OutputConfiguration을 배치할 수 있지만, splitDependencies를 읽은 후에는 몇 개의 readString8() 호출이 이어지며, 불일치가 발생한 후 소비되지 않은 데이터에 대한 완전한 제어권을 갖는 것이 좋을 것이다. 그래서 소비되지 않은 데이터의 다른 해석에 대해 걱정하지 않고 직접 빈 문자열을 배치할 수 있다.

이를 위해 먼저 OutputConfiguration.mSensorPixelModesUsed 내에 writeValue를 통해 기록될 일부 원시 데이터 컨테이너를 배치해야 한다. 나는 Bundle을 선택했다. 그렇게 하면 소비되지 않은 데이터에 다음이 남게 된다:

  1. writeValue VAL_BUNDLE 태그
  2. 원시 데이터의 길이(이 링크는 이 목록의 나머지 항목에도 적용됨)
  3. BUNDLE_MAGIC
  4. Parcel.appendFrom을 통해 그대로 전달된 원시 데이터

그래서 우리는 소비되지 않은 세 개의 Parcel.writeInt 항목이 있다. OutputConfiguration을 읽는 동안 임의의 Parcelable 값을 읽은 다음 세 개의 int를 읽는 일부 Parcelable로 감싸서 이를 제거할 수 있다. 나는 ZenPolicy CREATOR에서 이를 찾았다.

요약하면 우리는 다음과 같은 객체 계층 구조를 가지고 있다(system_server에 존재하며 scheduleReceiver에 전달하려고 시도하는 것):

  • Intent
    • mClipData = ClipData
      • mItems.get(0).mActivityInfo = ActivityInfo
        • applicationInfo = ApplicationInfo
          • splitDependencies.get(0) = ZenPolicy
            • mVisualEffects.get(0) = OutputConfiguration
              • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
                • mHierarchyOps.get(0) = PackageManagerException
              • mSensorPixelModesUsed.get(1) = Bundle

이것은 system_server에 의해 기록된다. 그런 다음 수신 애플리케이션은 PackageManagerException의 readSerializable 데이터까지(포함) 모든 것을 정상적으로 읽지만, PackageManagerException에 대한 Serializable 데이터가 읽힌 후 예외가 발생하고 OutputConfiguration 아래의 모든 항목 읽기가 취소되어 Bundle이 읽히지 않은 채로 남는다. 읽기는 ZenPolicy로 진행되며, Bundle 내의 원시 데이터 앞에 오는 세 개의 int를 소비한다. 그런 다음 ApplicationInfo 읽기는 이전에 Bundle에서 그대로 전달된 원시 데이터였던 데이터를 읽는 것으로 진행된다. 해당 원시 데이터의 읽기는 이 스택의 나머지 객체(ApplicationInfo, ActivityInfo, ClipData 및 )로 계속된 다음 해당 원시 데이터는 다음 메서드 매개변수를 읽는 데 사용된다.

그런 다음 handleReceiver 내에서 무슨 일이 발생하는가

방금 말했듯이 이제 나머지 scheduleReceiver 매개변수는 공격자가 제어하는 버퍼에서 읽힌다.

해당 메서드가 호출되면 어떤 일이 발생하는지 살펴보자.

먼저 scheduleReceiver는 모든 인수의 값을 패킹하고 sendMessage()를 사용하여 실행을 메인 스레드로 전달한다.

다음으로 메인 스레드에서 handleReceiver가 호출된다.

handleReceiver는 getPackageInfoNoCheck를 호출하고, scheduleReceiver 인수로 전달된 ActivityInfo의 일부로 수신한 ApplicationInfo를 전달한다.

getPackageInfo는 주어진 이름의 패키지가 이미 캐시에 있는지 확인하고, 없으면 새 LoadedApk 인스턴스를 구성하고, 이전에 수신한 ApplicationInfo 객체를 전달한다(공격자가 새 LoadedApk가 구성되도록 하려면 이 프로세스에서 이전에 본 적 없는 패키지의 packageName이 사용된다).

그런 다음 ContextImpl.getClassLoader() 메서드가 사용되며, 첫 실행 시 mPackageInfo.getClassLoader()에 위임하고, mPackageInfo는 LoadedApk이며 이전 단락에서 구성된 것이다.

그런 다음 createOrUpdateClassLoaderLocked가 있으며, 이는 makePaths를 호출하여 zipPaths를 채우고 ClassLoader에 사용될 경로를 채운 다음, 이들을 결합하여 zip 변수에 할당하고 createClassLoader에 전달한다.

makePaths는 ApplicationInfo의 정보를 사용하여 zipPaths를 채우며, 가장 중요하게는 sourceDir이 포함된다. 공격자 애플리케이션은 sourceDir이 자신의 apk 경로로 설정된 주입된 ApplicationInfo를 만들므로, 수신자 클래스는 실제로 공격자 apk에서 로드된다. 이는 직접적으로 브로드캐스트를 수신하는 애플리케이션 내에서 공격자가 제어하는 코드의 실행으로 이어진다.

숨겨진 API 검사에 대한 참고 사항

우회해야 할 것이 한 가지 더 있었다: 숨겨진 API 검사. 이는 보안 경계로 의도된 적이 없지만(애플리케이션이 항상 NDK를 사용하여 기본 syscall을 직접 호출할 수 있으므로), 이 경우에는 Parcel에 데이터를 수동으로 기록한 다음 readParcelable을 사용하여 조작된 ClipData를 구성함으로써 우회되었다. 이러한 ClipData는 그런 다음 정상적으로 Intent에 첨부되고 sendBroadcast()에 전달될 수 있으므로 브로드캐스트 전송 자체는 공개 API만 사용하여 수행되었다.

수정 사항

위의 분석은 원래 Google에 보내졌으며, 그 결과로 여러 수정 사항이 있는 것으로 보인다(직접적인 인과 관계에 대한 증거는 없지만).

Android 12와 함께 출시됨:

  • OutputConfiguration 및 관련 클래스의 예외 삼키기가 제거됨
  • OutputConfiguration#mSensorPixelModesUsed는 더 이상 writeValue를 통해 기록되지 않음
  • ClipData#mActivityInfo는 쓰기 중에 명시적으로 요청되지 않는 한 더 이상 Parcel에 기록되지 않음 (따라서 Intent는 더 이상 BOOTCLASSPATH의 임의의 Parcelable을 포함할 수 없으며, 이 악용 기술이 제거됨)

작성 시점에 master 브랜치에만 존재하며 출시 버전에는 없으며, 아마도 Android 13에 나타날 것임(12L에는 아님):

  • Parcel에 항목 유형을 확인하는 새로운 List 읽기 메서드가 있으며 타입 없는 버전은 더 이상 사용되지 않는 것으로 표시됨
  • Parcel에 읽지 않은 데이터가 남아 있지 않은지 확인하는 새로운 메서드 Parcel#enforceNoDataAvail()가 있으며, AIDL이 RPC 호출 인수를 읽은 후 사용할 것으로 보인다. 일반적으로 내 익스플로잇은 Parcel에서 모든 데이터를 읽은 후 나머지는 무시된다는 사실에 의존했는데, 이는 더 이상 그렇지 않을 것이다. 그러나 많은 경우 끝에 최종 위치로 seek하는 데이터를 구성할 수 있다고 생각하므로 이것은 강력한 완화책이 아니다. 어쨌든 때때로 이러한 문제가 자연적으로 감지되지 않고 발생하므로 이를 포착할 수 있을 것이다. 이에 대한 추가 논의는 issue #3에 있음
  • Bundle의 모든 항목은 길이가 별도로 저장됨. 이것은 2014년부터 Google에 비공개로 버그를 보고해 온 전체 버그 클래스를 사실상 종료시킨다. 2017년에 게시된 설명 및 약 1년 후의 코드 자체. 올바르게 계산한다면 버그 클래스의 수명은 8년이 될 것이다(솔직히 이것이 많은 것인지 잘 모르겠지만, 여전히 Bundle이 아닌 익스플로잇 변형이 있을 수 있다. 예를 들어 정확히 이것과 같은 것(물론 정확히 이것은 이미 수정되었지만)).
도구 다운로드
c != null
Class.forName
PackageManagerException
ClassLoader
readList
Parcelable
WindowContainerTransaction
Parcel
PackageManagerException
ClassNotFoundException
RuntimeException으로 감싸진 다음
OutputConfiguration CREATOR에 의해 포착된다
Intent
handleReceiver