
# CVE-2021-0928을 통한 Android 12 Beta 설치 앱의 시스템 권한 상승에 대한 분석 및 익스플로잇 `OutputConfiguration`의 `writeToParcel`/`createFromParcel` 직렬화 불일치를 악용하여 설치된 앱에서 시스템 권한으로 상승하는 방법에 대한 분석 및 익스플로잇
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의 대부분의 IPC는 Parcel이라는 클래스를 통해 수행됩니다
Parcel의 기본 사용법은 다음과 같습니다:```java Parcel p = Parcel.obtain(); p.writeInt(1); p.writeString("Hello");
그런 다음 `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<>();
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);
}
};
}
위에서 볼 수 있듯이, 쓰기 중에는 `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`를 전달하는 것입니다.