
CVE-2022-20452용 익스플로잇: recycle() 후 Parcel을 사용하는 LazyValue를 통해 Android에서 설치된 앱에서 시스템 앱(또는 다른 앱)으로의 권한 상승
Android 13은 Parcel 직렬화 메커니즘을 강화하기 위해 많은 개선 사항을 도입합니다.
Android 보안 및 개인정보 보호 팀이 만든 개선 사항에 대한 프레젠테이션은 여기 있습니다
훌륭한 일이며, 많은 취약점을 확실히 제거하거나 악용 불가능하게 만듭니다. 또한 그들은 앱이 다른 앱(시스템 앱 포함)에 자신의 코드를 로드할 수 있게 해주는 이전 익스플로잇을 무력화시키는 방법을 설명합니다.
하지만 이제 저는 같은 결과를 다른 방식으로 달성하는 새로운 익스플로잇을 가지고 돌아왔습니다. 이 익스플로잇은 앞서 언급한 Parcel 강화 과정에서 도입된 다음 취약점에 의존합니다:
![텍스트를 표시하는 애플리케이션의 스크린샷. 제목: LeakValue. 본문: ValueLeaker-s 6개를 생성했습니다. ActivityTaskManagerService 잠금 중. ActivityTaskManagerService 잠금 완료. ActivityTaskManagerService 잠금 해제 중. ActivityTaskManagerService 잠금 해제 완료. leakedBinders=[android.os.BinderProxy@f06702e]. 유출된 인터페이스: android.app.IApplicationThread. 코드 실행을 요청 중입니다. 셸코드가 다음에서 실행되었습니다: uid=1000 pid=6904 packageName=com.android.settings 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. 화면 하단에는 두 개의 버튼이 있습니다: START 및 MANUAL TESTING](Screenshot_20220723-081920.png)
(앱 실행 중 logcat도 첨부했습니다. 익스플로잇은 로그에서 쉽게 드러납니다)
Parcel 및 Parcelable 불일치 버그 소개Android의 Parcel 클래스는 프로세스 간 통신의 기초입니다.
객체는 Parcel에 기록할 수 있도록 Parcelable 인터페이스를 구현할 수 있습니다. 예를 들어 (AOSP에서 복사):```java
public class UsbAccessory implements Parcelable {
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
public UsbAccessory createFromParcel(Parcel in) {
String manufacturer = in.readString();
String model = in.readString();
String description = in.readString();
String version = in.readString();
String uri = in.readString();
IUsbSerialReader serialNumberReader = IUsbSerialReader.Stub.asInterface(
in.readStrongBinder());
return new UsbAccessory(manufacturer, model, description, version, uri,
serialNumberReader);
}
};
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(mManufacturer);
parcel.writeString(mModel);
parcel.writeString(mDescription);
parcel.writeString(mVersion);
parcel.writeString(mUri);
parcel.writeStrongBinder(mSerialNumberReader.asBinder());
} }
참고로 `Parcel`은 내부적으로 쓰기 또는 읽기가 수행되는 위치를 저장하며, `readString()`은 데이터를 String으로 파싱하는 동시에 위치를 전진시킵니다. 이 위치는 [`dataPosition()`](https://developer.android.com/reference/android/os/Parcel#dataPosition())/[`setDataPosition()`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int))을 통해 수동으로 가져오거나 설정할 수 있습니다. `Parcelable` 인터페이스의 구현체는 `writeToParcel`과 `createFromParcel`이 동일한 양의 데이터를 쓰고 읽도록 보장해야 합니다. 그렇지 않으면 이후의 모든 읽기가 잘못된 오프셋에서 데이터를 가져오게 됩니다.
[`Bundle`](https://developer.android.com/reference/android/os/Bundle)(프로세스 간에 전송할 수 있는 키-값 맵)에는 [`writeValue()`를 통해 `Parcel`에 쓸 수 있는 다양한 객체](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=1792-1937)가 포함될 수 있습니다. `Parcel`에서 `Bundle`의 내용을 읽을 때, 해당 시스템에서 사용 가능한 모든 `Parcelable` 클래스를 읽을 수 있습니다.
`Bundle`은 전체 parcel된 데이터의 길이를 `Parcel`에 기록한 다음 [원래 `Parcel`의 관련 부분을 `mParcelledData`에 저장된 보조 `Parcel`로 복사](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=1675-1683)함으로써 실제 내용 파싱을 지연시킵니다. (예를 들어 [`Activity.onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle))가 `system_server`에서 사용할 수 없는 `Parcelable`을 제공할 수 있는데, 이 경우 전체 `Bundle`은 내용을 파싱하지 않은 채 그대로 `system_server`에 전달되었다가 다시 돌아옵니다.)
하지만 `Bundle`의 어떤 값에 한 번이라도 접근하게 되면, `Bundle` 내부의 모든 값은 [unparcel되고](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=227-313) [존재하는 모든 키-값 쌍이 파싱됩니다](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=3613-3632). 그러한 맵에 `writeToParcel`과 `createFromParcel` 메서드가 균형을 이루지 못하는 `Parcelable`이 포함되어 있었고 나중에 해당 `Bundle`이 다른 프로세스로 전달되었다면, 그 다른 프로세스는 `Bundle`의 다른 내용을 볼 수 있었습니다. 시스템에는 `Bundle`이 안전한지 검사된 후 다른 프로세스로 전달되는 [곳](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=5037-5046)이 있기 때문에, 시스템에서 사용 가능한 클래스들의 [불일치가 시스템 취약점](https://github.com/michalbednarski/ReparcelBug)이 되었습니다.
이 글에서는 하나의 내용을 보여준 후 전달된 뒤에는 다른 내용을 보여주는 이런 `Bundle`을 자가변형 `Bundle`(self-changing `Bundle`)이라고 부르겠습니다.
여기서 또 중요한 점은, `Parcel`은 단순한 바이트(String, 숫자, 위 요소들로 만들어진 객체) 외에도 파일 디스크립터(File Descriptor)와 `Binder`를 포함할 수 있다는 것입니다. `Binder`는 RPC 호출을 할 수 있는 객체입니다. 즉, 한 프로세스가 `Binder` 객체를 만들고 [`onTransact()` 메서드](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))를 재정의합니다. 그런 다음 `Binder`가 다른 프로세스로 전달됩니다. 위 예제 코드에서 `Parcel`에 대해 읽기/쓰기에 사용된 `read`/`writeStrongBinder()` 호출을 볼 수 있습니다. 다른 프로세스에서 `readStrongBinder()`를 사용하면 `BinderProxy` 객체가 생성됩니다([`IBinder` 인터페이스](https://developer.android.com/reference/android/os/IBinder) 뒤에 숨겨져 있습니다). 그러면 그 다른 프로세스는 해당 객체에 대해 [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))를 호출할 수 있고, 원래 객체에서는 `onTransact()`가 실행됩니다. 보통은 `transact()`/`onTransact()`를 수동으로 작성하지 않고 [대신 AIDL을 사용](https://developer.android.com/guide/components/aidl)합니다.
# `LazyValue`의 등장, 자가변형 `Bundle`의 종말
과거에는 `writeToParcel`/`createFromParcel` 불일치가 있는 클래스가 많았습니다. Android 13은 [`LazyValue`를 도입](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/)하여, 시스템 어디에나 존재할 수 있는 그러한 클래스로 인해 자가변형 `Bundle`을 만들 수 있던 문제를 해결합니다.
이제 `writeValue`가 사용될 때, 작성되는 값이 원시 타입(primitive)이 아니면 [값의 길이도 `Parcel`에 기록](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2331-2344;drc=03c34f57c05feecfb090de3917787f049cb5f804)됩니다.
일반 앱이 `Parcel.readValue()`를 직접 사용하면, `Parcel`에서 읽은 `length`가 실제로 읽은 데이터의 크기와 일치하지 않을 때 경고가 출력된다는 점을 제외하면 [모든 것이 이전과 동일하게 동작](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4330-4348;drc=03c34f57c05feecfb090de3917787f049cb5f804)합니다. (단, [`Slog.wtfStack`은 절대 예외를 던지지 않는다](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/Slog.java;l=108-116;drc=23c7543b8e608ebcbb38b952761b54bb56065577)는 점에 유의하세요.)
하지만 `Bundle`은 이제 대신 [`Parcel.readLazyValue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4350-4420;drc=03c34f57c05feecfb090de3917787f049cb5f804)를 사용합니다.
작동 방식을 좀 더 자세히 살펴보겠습니다. `LazyValue` 클래스에는 `Parcel` 내부의 `LazyValue` 데이터 구조를 설명하는 [좋은 주석](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4392-4399;drc=03c34f57c05feecfb090de3917787f049cb5f804)이 있습니다:```
| 4B | 4B |
mSource = Parcel{... | type | length | object | ...}
a b c d
length = d - c
mPosition = a
mLength = d - a
mSource는 readLazyValue()가 호출된 원본 Parcel에 대한 참조입니다.
mPosition과 mLength는 원본 Parcel에서 LazyValue 데이터 전체의 위치를 나타내며, 여기에는 type과 length가 포함됩니다.
"length"(앞에 "m"이 없는 것)는 Parcel에 쓰인 길이 값을 의미하며 헤더(type 및 length)는 제외됩니다.
그래서 누군가(시스템 또는 앱) Parcel에서 읽은 Bundle에서 값을 가져올 때 다음과 같은 일이 발생합니다:
Bundle 클래스의 다양한 get*() 메서드 중 하나를 사용합니다. 예를 들어 타입 인수를 사용하는 새로운 getParcelable() 같은 경우입니다. (흐름은 새로운 메서드와 이전 메서드 모두 동일하며, 새로운 메서드는 clazz 인수가 null이 아닌지 확인하지만 이전 메서드는 null로 설정합니다.)unparcel()이 호출됩니다. 이 메서드는 이 Bundle에 mParcelledData가 있는지 확인합니다. (mParcelledData가 있다는 것은 Parcel에서 읽어왔지만 아직 값에 접근하지 않았고 키 이름도 풀리지 않았음을 의미합니다. 그렇지 않으면 5단계로 건너뜁니다.)unparcel()은 에 위임하며, . 는 이 만든 의 복사본인 로 설정되고, 매개변수는 로 설정되어 전달된 이 소유이며 해당 에 대해 을 호출해도 괜찮음을 나타냅니다.Bundle이 여전히 LazyValue를 포함한 채 전달되는 경우(즉, 이 특정 값에는 접근하지 않았지만 해당 Bundle의 다른 값에는 접근한 경우, 다시 말해 unparcel()은 호출되었지만 그 항목에 대한 LazyValue.apply()는 호출되지 않은 경우):
Parcel.writeValue()가 LazyValue를 감지하고 쓰기를 LazyValue.writeToParcel()로 위임합니다.LazyValue.writeToParcel()는 out.appendFrom(source, mPosition, mLength)를 사용해 원본 Parcel에서 LazyValue 데이터 전체를 복사합니다. (다시 말하지만, mPosition과 mLength에는 LazyValue 헤더가 포함되므로 원본 Parcel에서 type과 length도 함께 복사됩니다.)Parcel.ReadWriteHelper 및 Parcel.readSquashed(이들의 세부 사항은 이 익스플로잇에 중요하지 않습니다. 여기서 관련된 것은 이러한 메커니즘이 존재한다는 것뿐입니다.)
Parcel의 또 다른 흥미로운 기능은 작성된 String과 객체를 선택적으로 중복 제거(deduplicate)할 수 있는 기능입니다.
String 중복 제거는 Parcel.ReadWriteHelper 클래스를 재정의하여 수행됩니다. Parcel.readString()은 실제로 ReadWriteHelper에 위임하며, 기본 헬퍼는 Parcel에서 직접 String을 읽습니다.
Parcel.ReadWriteHelper의 대체 구현은 readString 호출을 미리 String 풀을 읽어두고 readInt를 사용해 풀에서 String의 인덱스를 가져오는 방식으로 대체할 수 있습니다. 그러나 이는 앱이 제어하는 Parcel에서는 절대 수행되지 않습니다.
Parcel은 hasReadWriteHelper() 메서드를 제공합니다. 이 메서드를 통해 호출자는 이러한 중복 제거 메커니즘이 활성화되어 있는지 감지하고 이와 호환되지 않는 기능을 비활성화할 수 있습니다.
Parcel에서 사용할 수 있는 또 다른 중복 제거 메커니즘은 스쿼싱(squashing)입니다:
Parcel.allowSquashing()으로 스쿼싱을 활성화해야 합니다.Parcel.maybeWriteSquashed(this)를 호출합니다. 이 메서드가 true를 반환하면 해당 객체가 이미 이 Parcel에 기록되었으며 이제 이전 객체 데이터에 대한 오프셋만 Parcel에 기록되었음을 의미합니다. 그렇지 않은 경우(스쿼싱이 활성화되지 않았거나 이 객체가 처음 기록되는 경우) maybeWriteSquashed는 객체가 스쿼싱되지 않았음을 나타내기 위해 오프셋으로 0을 쓰고, 호출자가 실제 객체 데이터를 써야 함을 나타내기 위해 false를 반환합니다.Parcel.readSquashed가 호출되고 실제 읽기 함수가 람다로 전달됩니다. readSquashed는 maybeWriteSquashed()가 쓴 오프셋이 객체의 다른 항목이 이전에 읽혔음을 나타내는지 확인합니다. 그렇다면 이전에 읽은 객체를 반환하고, 그렇지 않으면 제공된 람다를 호출하여 지금 읽습니다.Parcel.recycle() 후 사용Java 쪽에서 Parcel 객체는 풀로 재활용될 수 있습니다. 즉, Parcel 사용이 끝나면 recycle()을 호출하고, 다음에 누군가 Parcel.obtain()을 호출하면 이전에 재활용된 Parcel을 받게 됩니다. 이렇게 하면 객체 할당 횟수와 이후 가비지 컬렉션(Garbage Collection)을 줄일 수 있습니다.
반면에 이러한 수동 메모리 관리는 Java에 Use-After-Free와 유사한 버그가 발생할 가능성을 가져옵니다. (C의 일반적인 Use-After-Free와 달리 타입 안전성은 있지만요.)
위에서 언급했듯이 Bundle은 Parcel의 복사본을 만들고 LazyValue가 있으면 Parcel.recycle()을 호출하지 않습니다. 그러나 Parcel.hasReadWriteHelper()가 true인 경우에는 그렇지 않습니다. 그 경우:
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle);가 호출됩니다. 이는 Parcel이 여전히 호출자 소유이므로 Bundle이 Parcel을 재활용하지 않음을 의미합니다. 그러나 이렇게 하면 원본 Parcel을 참조하는 LazyValue가 생성되며, 이는 원본 Parcel의 수명보다 오래 존재할 수 있습니다.unparcel(/* itemwise */ true) 호출입니다. 이 호출은 모든 항목에 대해 getValueAt()을 사용해 Bundle에 존재하는 모든 LazyValue를 실제 값으로 교체합니다.그렇다면 이러한 LazyValue가 2단계에서도 살아남게 만들 수 있을까요? 그리고 그 동작을 Use-After-Recycle로 전환할 수 있을까요?
역직렬화가 실패하면(예: Parcel 내부에 지정된 이름의 클래스를 찾을 수 없는 경우) BadParcelableException이 발생하고 이것은 getValueAt()에 의해 포착됩니다. BaseBundle.sShouldDefuse 정적 필드가 true이면 예외가 발생하지 않고 실행이 계속되어 Bundle에 원본 Parcel을 참조하는 LazyValue가 남게 됩니다. sShouldDefuse는 특정 프로세스에서 Bundle에서 사용할 수 없는 값으로 인해 예외가 발생하지 않아야 함을 나타내며 system_server에서 true로 설정됩니다.
원본 Parcel이 재활용되고 그 후에 그 Parcel에서 읽은 Bundle이 다른 Parcel에 쓰이게 되면 원본 Parcel의 내용이 대상 Parcel에 복사됩니다. 하지만 그 시점에 원본 Parcel은 다른 용도로 재사용될 수 있으며, 관련 없는 IPC 작업의 데이터가 복사될 수 있습니다.
좋습니다. 그런데 우리가 제공한 Bundle이 역직렬화되는 동안 Parcel.hasReadWriteHelper()가 true가 되게 하려면 어떻게 해야 할까요?
알고 보니 RemoteViews 클래스(보통 예를 들어 위젯을 홈 화면에 전달하는 데 사용됨)는 그 안에 중첩된 Bundle을 읽을 때 ReadWriteHelper를 명시적으로 설정합니다. 이 ReadWriteHelper는 String 중복 제거를 수행하지 않으며, 오직 Bundle이 보조 Parcel로 데이터를 복사하는 것을 건너뛰게 하기 위해서만 존재합니다. 이렇게 하는 이유는 RemoteViews가 스쿼싱을 활성화하여 그 안에 중첩된 ApplicationInfo 객체를 중복 제거하기 때문이지만, 이로 인해 Bundle 내부에 있는 ApplicationInfo 객체도 스쿼싱될 수 있습니다. 따라서 해당 Bundle의 읽기를 지연시킬 수 없습니다. 지연시키면 스쿼싱된 객체들의 스쿼싱 해제가 실패할 수 있기 때문입니다.
system_server에 Parcelable 넣고 다시 가져오기이제 우리는 system_server가 역직렬화에 실패하는 LazyValue를 포함한 Bundle을 담은 RemoteViews를 읽고, 나중에(다른 Binder IPC 트랜잭션에서) 그 객체를 우리에게 다시 보내주기를 원합니다.
아마도 앱 위젯 호스트로 등록하는 것이나 contentView가 설정된 Notification을 게시하는 것 같은 합법적인 방법으로도 가능할 것입니다. (하지만 전자는 권한을 부여받기 위해 사용자 상호작용이 필요하고, 후자는 다른 프로세스와의 상호작용을 유발하거나 사용자에게 보일 수 있기 때문에 두 가지 모두 피하고 싶었습니다.)
대신 MediaSession을 만들고 여기에 setQueue(List<MediaSession.QueueItem> queue)를 호출하여 객체를 system_server로 보낸 다음, 나중에 MediaController의 List<MediaSession.QueueItem> getQueue() 메서드를 통해 다시 가져오기로 결정했습니다. (MediaController는 MediaSession.getController()를 통해 얻을 수 있습니다.) 이 메서드들은 RemoteViews를 받을 수 있을 것처럼 보이지는 않지만, 실제로는 Java Type Erasure와 내부적으로 List에 대한 일반 직렬화 작업으로 구현된다는 사실 덕분에 가능합니다.
하지만 저는 이런 SDK 메서드를 사용하지 않고 기본 Binder 트랜잭션용 데이터를 직접 작성합니다(형식이 잘못된 직렬화 데이터를 썼다가 나중에 읽어야 하기 때문입니다). 그러니 이 메서드들이 어떻게 동작하는지 살펴보겠습니다.
이 두 메서드 모두 큐의 전체 크기가 Binder 트랜잭션의 최대 크기를 초과할 수 있으므로 전송이 여러 트랜잭션으로 나뉠 수 있다는 점을 처리해야 했습니다.
system_server로 "queue"를 보내는 과정은 일반적으로 다음과 같습니다:
MediaSession.setQueue()는 먼저 ISession.getBinderForSetQueue()를 호출합니다.system_server 쪽에서 그 메서드는 ParcelableListBinder 객체를 생성해 반환합니다.MediaSession.setQueue()는 ParcelableListBinder.send()를 호출합니다. 이 메서드는 목록 내용을 제공된 Binder로 보내며, 여러 트랜잭션으로 나뉠 수도 있습니다.
1이 쓰이고 실제 항목은 Parcel.writeParcelable()을 통해 작성됩니다. (이 메서드는 전송되는 클래스의 이름을 쓴 다음 을 호출하여 데이터를 보냅니다.)반면에 "queue"를 가져오는 과정은 조금 다릅니다:
MediaController.getQueue()는 단지 ISessionController.getQueue()를 호출하고 수신된 ParceledListSlice를 언랩합니다.system_server 쪽에서 getQueue()는 단지 mQueue를 ParceledListSlice로 감싸서 반환합니다.ParceledListSlice.writeToParcel() 및 createFromParcel() 메서드 안에 있습니다. 특히 writeToParcel()은 안전한 크기 한도에 도달하면 다음 청크를 가져올 수 있는 Binder 객체를 기록합니다.왜 이 둘이 다른지 설명하자면, system_server가 다른 앱으로 나가는 동기식 Binder 호출을 하지 않도록 하려는 노력이 계속 진행 중입니다. 이러한 호출이 중단되면 system_server 전체가 중단될 수 있기 때문입니다. 즉, system_server는 ParceledListSlice를 수신해서는 안 됩니다. system_server에서 나가는 동기식 트랜잭션에 대해 경고하는 코드가 있지만 아직 강제로 적용되지는 못했습니다. 예를 들어 실제로 ParceledListSlice를 수신하는 경우처럼 system_server가 여전히 그러한 호출을 하는 경우가 있기 때문입니다.
이제 system_server가 parcel_that_will_be_sent_to_us.appendFrom(some_recycled_parcel, somewhat_controlled_position, controlled_size)를 수행하도록 만드는 데 필요한 프리미티브를 갖추게 되었습니다.
시스템에서 Parcel 데이터를 무작위로 가져오려 시도하거나, 특정 데이터를 얻기 위해 상황을 조작할 수 있습니다.
다음은 고려 사항들입니다:* Parcel.recycle()이 호출되면 해당 Parcel의 내용이 지워집니다. 즉, 데이터를 복사해 오려는 Parcel은 recycle()되면 안 된다는 뜻이며, 대략적으로 말해 이미 끝난 Binder 트랜잭션에서 데이터를 가져올 수 없다는 의미입니다.
Bundle의 Parcel에서 데이터를 가져올 수 있습니다 (여기에는 Intent extras와 Activity의 savedInstanceState가 포함됩니다). 이러한 객체는 일반적으로 전혀 recycle()되지 않습니다 (Garbage Collector에 의해 정리되며 풀로 돌아가지 않습니다. 풀이 고갈되면 Parcel.obtain()이 새 Parcel 객체를 생성합니다. 물론 참조를 보유하고 있는 Parcel은 시스템에 더 이상 용도가 없더라도 GC되지 않습니다).Binder 트랜잭션에 사용되는 Parcel은 시스템의 다른 Parcel과 별도의 풀을 사용합니다. 나가는 Binder 트랜잭션이 만들어질 때 Bundle이 데이터를 보조 Parcel에 복사하거나, 앱이 자신의 목적을 위해 을 사용할 때 . 반면 들어오는 트랜잭션이 있을 때는 , 이 호출은 . 두 경우 모두 이후 이 사용되며 합니다. 즉, 익스플로잇은 데이터를 유출하려는 대상과 동일한 풀에 속한 에서 가 읽히도록 해야 합니다. 특정 변형을 결정하기 전에 저는 둘 다 작성했으므로, 제 에서 와 메서드를 모두 찾을 수 있습니다.결국 저는 앱 프로세스가 시작될 때 앱이 system_server로 보내는 IApplicationThread Binder를 가로채는 것을 시도하기로 결정했습니다. system_server는 이 Binder를 사용해 애플리케이션에 어떤 컴포넌트를 로드해야 하는지 알려줍니다.
애플리케이션 프로세스가 처음 시작될 때, 가장 먼저 하는 일 중 하나는 attachApplication() 호출을 통해 IApplicationThread를 system_server로 보내는 것입니다. 그리고 이 트랜잭션이 제가 그 Binder를 가로챌 트랜잭션입니다. IApplicationThread가 Parcel에 담기는 다른 곳도 있습니다. 예를 들어 시스템이 액티비티를 시작할 때 호출자 식별을 위해 전달하는 경우가 있습니다 (하지만 대상 애플리케이션이 그런 작업을 수행하는 시점을 제가 제어할 수는 없었습니다). 또는 Activity 생명주기 관리의 일부로 시스템이 애플리케이션에 보내는 경우도 있습니다 (하지만 이는 system_server에서 나가는 oneway 트랜잭션으로 수행되며, Parcel.recycle()과의 경쟁에서 이길 가능성은 희박합니다).
그렇긴 하지만, attachApplication() 트랜잭션 중 system_server가 수신하는 Binder를 가로채는 것도 결코 간단하지 않으며, 해결해야 할 몇 가지 문제가 있었습니다.
Parcel 되감기attachApplication()의 데이터가 수신되는 Parcel에서 IApplicationThread Binder를 가로채는 데 있어 첫 번째 문제는 이 Binder가 상당히 이른/낮은 dataPosition()에 있다는 점입니다. 이는 RemoteViews의 Bundle 안에 있는 우리의 LazyValue가 위치할 수 있는 위치보다 훨씬 낮습니다.
attachApplication() 트랜잭션의 데이터는 RPC 헤더 뒤에 IApplicationThread Binder가 이어지는 구성으로만 이루어져 있습니다. RPC 헤더는 Parcel.writeInterfaceToken()을 통해 작성되며, 몇 개의 int와 인터페이스 이름으로 구성됩니다. 이 경우 이름은 "android.app.IActivityManager"입니다.
한편, RemoteViews에 포함된 Bundle을 읽으려면 최소한 다음 항목들을 지나쳐야 합니다 (몇 가지 사소한 항목은 건너뜁니다):
readParcelable을 시작하기 위한 아이템 존재 플래그Parcelable의 이름: "android.view.RemoteViews"RemoteViews에 있는 ApplicationInfo 객체 (또한 이 객체는 null이 아니고 packageName이 null이 아닌 상태여야 하며, 그렇지 않으면 이 객체를 다시 보내기 위해 가져오려고 할 때 RemoteViews.writeToParcel()이 실패합니다)RemoteViews.readActionsFromParcel()에 도달하며, 이 함수는 을 호출하고, 이는 을 구성합니다. 이는 공통 마지막으로 .이제 Bundle에서 String 키만 넣으면 LazyValue 읽기가 시작되고, Parcel에서의 위치가 기억됩니다. 하지만 이 시점의 위치는 IApplicationThread Binder가 있었을 위치를 한참 지나 있습니다.
이 지점에 도달하면 Parcel의 위치를 되감을 수 있을까요? 다시 말해 현재 위치보다 더 이른 위치를 가리키는 값으로 Parcel.setDataPosition()을 호출할 수 있을까요?
알고 보니 LazyValue의 또 다른 버그 덕분에 가능합니다. 다음은 이를 읽는 데 사용되는 코드입니다:```java
public Object readLazyValue(@Nullable ClassLoader loader) {
int start = dataPosition();
int type = readInt();
if (isLengthPrefixed(type)) {
int objectLength = readInt();
int end = MathUtils.addOrThrow(dataPosition(), objectLength);
int valueLength = end - start;
setDataPosition(end);
return new LazyValue(this, start, valueLength, type, loader);
} else {
return readValue(type, loader, /* clazz */ null);
}
}
([AOSP의 원본](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4376-4388;drc=03c34f57c05feecfb090de3917787f049cb5f804), `LazyValue` 생성자는 매개변수를 필드에 할당할 뿐입니다)
문제는 `MathUtils.addOrThrow()`가 오버플로를 검사하지만, [음수 값에 대해서는 전혀 문제가 없다는 점입니다](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/MathUtils.java;l=276;drc=290055a76e6ef80dd8ad7bc812d78f4fc0c5be86)
음수 `mLength`(`valueLength` 매개변수에서 채워진)를 가진 `LazyValue`에 `Parcel.writeValue()`를 시도하면 [`appendFrom()`에서 예외가 발생](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4446;drc=03c34f57c05feecfb090de3917787f049cb5f804)하겠지만, `Parcel.hasReadWriteHelper()`가 `true`인 상태에서 `Bundle`을 읽는 중이므로 모든 `LazyValue`는 읽은 후 unparcelled 되며, 우리는 의도적으로 잘못된 `Parcelable`을 그 안에 넣어 `LazyValue`로 남아 있게 해야 했습니다. `LazyValue`가 있는 위치에 유효한 parcelled 데이터를 넣으면 unparcelled 되고, 앞서 언급했듯이 길이 불일치는 `logcat`에 메시지만 남깁니다. 이 특정 익스플로잇은 타입을 `VAL_MAP`으로, 키-값 쌍의 수를 0으로 설정합니다. `logcat`에서 해당 값을 읽을 때 다음 메시지를 볼 수 있습니다: "`E Parcel : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP consumed 4 bytes, but -540 expected.`"
(또한 음수 길이가 지정된 `LazyValue`는 (이 글에서 설명하는 다른 버그를 사용하지 않고도) 자기 스스로 변경되는 `Bundle`을 만드는 데 사용될 수 있습니다. `LazyValue`는 이를 없애기 위해 만들어진 것입니다. 하지만 그것은 다른 이야기이고(별도로 Google에 보고했습니다), 이 익스플로잇에서는 더 많은 것을 노리고 있습니다.)
그렇다면 우리는 얼마나 되감고 싶은 걸까요?
`setDataPosition()` 호출이 발생한 후에는 읽기가 `Bundle`의 다음 키-값 쌍으로 진행되므로, 다음 조건을 만족하는 위치를 골라야 합니다:
1. `Parcel.readString()`을 사용해 읽는 `Bundle` 키는 거의 무엇이든 될 수 있습니다. 잘못된 길이(음수 또는 전체 `Parcel` 크기 초과)를 가리키는 경우도 포함되며, 그 경우 `readString()`은 `null`을 반환하는데, 이는 `Bundle`에서 유효한 키입니다.
2. 값 타입. 이 값은 [`isLengthPrefixed()`가 `true`를 반환하는 타입](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4695-4712;drc=03c34f57c05feecfb090de3917787f049cb5f804) 중 하나여야 합니다.
3. 값 길이. 이것 역시 우리가 제어하는 값이어야 합니다. `Parcel.appendFrom()`은 길이가 정렬되지 않았거나 소스 `Parcel`의 전체 크기를 초과하면 실패합니다.
그렇다면 `Parcel`에서 그런 위치가 될 수 있는 곳은 어디일까요? 이 지점에 도달하는 데 필요한 동일한 데이터가 이미 읽힌 상태라는 점을 고려하면:
* `Parcelable` 이름(`"android.view.RemoteViews"`) 앞은 안 됩니다. 공간이 충분하지 않기 때문입니다.
* `Parcelable` 이름 내부는 안 됩니다. 타입과 길이를 설정할 수 없기 때문입니다.
* `Parcelable` 이름 바로 뒤는 안 됩니다. `RemoteViews`의 첫 번째 항목은 `mode`이며, 우리 코드에 도달하려면 [이를 `MODE_NORMAL`로 설정해야 하기 때문입니다](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/RemoteViews.java;l=3799;drc=03c34f57c05feecfb090de3917787f049cb5f804).
* 그 이후도 안 됩니다. `IApplicationThread` `Binder`가 있는 지점을 이미 지나쳤기 때문입니다.
흠, `RemoteViews`가 parcelled 데이터의 최외곽 객체인 경우 좋은 위치가 없습니다.
다음 조건을 충족하는 다른 `Parcelable`을 찾아야 합니다:
1. 시작 부분 또는 그 근처에 임의 데이터를 넣을 수 있는 곳이 있어야 합니다 (예: 직렬화 과정에 영향을 주지 않는 단순 데이터인 `int` 또는 `String`)
2. `RemoteViews`를 포함할 수 있어야 합니다 (직접 또는 임의의 `readParcelable`을 통해)
3. 정규화된 클래스 이름이 너무 길지 않아야 합니다. 여전히 대상 `Parcel`에서 `IApplicationThread`가 위치한 지점에 의해 크기 제한을 받기 때문입니다.
그래서 시스템의 `Parcelable` 클래스 목록을 가져와 정규화된 클래스 이름 길이의 오름차순으로 정렬한 다음, 조건 2를 충족하는지 확인하기 위해 목록의 항목을 검사하기 시작했습니다.
그렇게 해서 이 익스플로잇이 사용하는 [`"android.os.Message"`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=afdb23ab6f909c5438fa69aad458a11497cff216)에 도달했습니다. 이제 우리가 준비한 객체를 `Parcel`에서 읽는 과정은 다음과 같습니다:
* [`readParcelable()`을 시작하기 위한 항목 존재 플래그](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577)
* [`Parcelable` 이름](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4858;drc=03c34f57c05feecfb090de3917787f049cb5f804): `"android.os.Message"`
* [우리가 원하는 값으로 설정할 수 있는 몇 개의 `int`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=652-656;drc=afdb23ab6f909c5438fa69aad458a11497cff216)가 필드로 읽힙니다.
* `readParcelable()` 호출에 도달합니다. 이 호출은 위에서 설명한 대로 `RemoteViews`를 거쳐 진행되며, `Parcel.hasReadWriteHelper`가 `true`인 상태에서 `Bundle` 읽기를 시작합니다.
* 그 `Bundle`은 두 개의 키-값 쌍을 가진다고 선언합니다. 첫 번째 값에는 음수 길이를 가진 `LazyValue`가 있어, `Parcel.setDataPosition()`이 `"android.os.Message"` `String`이 있는 위치로 이동하게 됩니다.
* 읽기는 두 번째 키-값 쌍으로 진행됩니다. 키는 `"android.os.Message"`이고, `LazyValue`의 타입, 길이, 데이터는 세 번째 글머리 기호에서 설명한 `int`들에서 가져옵니다. 제가 원하는 `mPosition`과 `mLength`를 가진 `LazyValue`를 얻게 되었습니다. 만세!
* `LazyValue`들을 읽은 후에는 unparcelled 됩니다. 음수 크기를 가진 것은 성공적으로 unparcelled 되어 빈 `Map`으로 대체되고, 다른 하나는 역직렬화에 실패하지만 그 예외는 catch되어 `LazyValue`는 그냥 `Bundle`에 남아 있습니다.
* `readParcelable()`이 끝나지만, 그것이 `Message` 데이터의 끝은 아닙니다. `Message.readFromParcel()`은 이제 되감기 이후의 데이터를 계속 읽으며, 처음에 `RemoteViews`의 일부로 쓰였던 데이터를 보게 됩니다. 만약 이 시점에서 어떤 예외라도 발생하면 전체 계획이 무산됩니다.
* 첫 번째로 가능한 예외는 [`readBundle()` 호출](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=660;drc=afdb23ab6f909c5438fa69aad458a11497cff216)입니다. [`Bundle`에는 매직 값이 있고, 그것이 잘못되면 예외가 발생합니다](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1809-1815;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91). 그러나 그 매직 값은 길이가 [0](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1800-1804;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91)이거나 [음수](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3323-3327;drc=03c34f57c05feecfb090de3917787f049cb5f804)인 경우 존재하지 않으며, `IApplicationThread`를 잡는 데 필요한 값으로 `LazyValue` 데이터의 길이를 설정했을 때가 바로 그 경우였습니다. 그래서 여기서는 운이 좋았습니다.
* 다음으로 가능한 문제는 [`Messenger.readMessengerOrNullFromParcel()` 호출](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=661;drc=afdb23ab6f909c5438fa69aad458a11497cff216)입니다. 이것은 실제로 래핑된 `Binder` 객체입니다. `Binder`는 `Parcel`에서 특별한 객체이므로 읽으려면 대역 외(out-of-band)로 주석이 지정되어야 하기 때문에 해당 `Binder` 읽기는 실패합니다. 이 문제는 [네이티브 측 `Parcel`에서 감지되어 로그로 남지만, 오류로 전파되지는 않고 단순히 `null`이 반환됩니다](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp;l=2473-2476;drc=8b12e8333bbe17ccf4b30efb825244e1923d3a71).
# `attachApplication()` 지연시키기
좋습니다. 이전 단계에서 우리는 `attachApplication()` 메서드가 실행되는 동안 `IApplicationThread` 객체를 잡을 수 있게 해주는 객체를 성공적으로 만들었습니다.
문제는 그 메서드가 빠르게 완료된다는 것이며, 공정한 경쟁으로 그 완료에 맞서 이길 가능성은 꽤 희박하다는 것입니다.
하지만 그 메서드는 몇 개의 뮤텍스를 획득합니다(Java의 `synchronized () {}` 블록을 통해). 그러한 뮤텍스 중 하나를 획득하여 그곳에서 지연시킬 수 있다면, 이 메서드도 함께 지연될 것입니다.
이제 이 글에서 이미 언급했고 이 목적에 유용하게 쓰일 몇 가지로 돌아가 보겠습니다:
* `Bundle`은 값에 접근할 때 그 안의 값에 대한 역직렬화를 수행합니다.
* 역직렬화 중에 직렬화된 데이터 안에 지정된 객체로 차단형 발신 `Binder` 호출을 수행하는 `ParceledListSlice` 클래스가 있습니다.
이 모든 것을 종합하면: `system_server`에서 앱이 제공한 `Bundle`의 내용이 `attachApplication()`에서도 사용되는 뮤텍스 아래에서 접근되는 곳을 찾을 수 있다면, 우리 프로세스로 이루어진 `Binder` 트랜잭션이 끝날 때까지 `attachApplication()`을 지연시킬 수 있습니다.
[`ActivityOptions`](https://developer.android.com/reference/android/app/ActivityOptions)는 `Activity` 시작과 관련된 다양한 매개변수(예: 애니메이션)를 설명하는 클래스입니다. `system_server`에 전달되는 매개변수를 설명하는 다른 클래스들과 달리, 이 클래스는 `Parcelable`을 구현하지 않고 대신 `Bundle`로 변환하는 메서드를 제공합니다.
`system_server` 쪽에서는 해당 [`Bundle`이 다시 `ActivityOptions`로 변환되면서 역직렬화가 촉발됩니다](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityOptions.java;l=1096-1204;drc=03c34f57c05feecfb090de3917787f049cb5f804). 저는 [그 작업이 `ActivityTaskManagerService.moveTaskToFront()`에서 `ActivityTaskManagerService.mGlobalLock` 뮤텍스가 보유된 상태로 수행되는 곳](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=2088;drc=03c34f57c05feecfb090de3917787f049cb5f804)을 찾았습니다.
그래서 저는 [`ActivityManager.moveTaskToFront()`](https://developer.android.com/reference/android/app/ActivityManager#moveTaskToFront(int,%20int,%20android.os.Bundle))를 호출하면서, 예상된 타입의 값 대신 `ParceledListSlice`를 포함하는 `Bundle`을 전달합니다. 그 [`ParceledListSlice`는 제 프로세스로 `Binder` 호출을 수행하고](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=82-105;drc=23c7543b8e608ebcbb38b952761b54bb56065577), 그 호출에서 반환하기 전까지 `ActivityTaskManagerService.mGlobalLock` 뮤텍스는 계속 잠긴 상태로 유지됩니다.
# 서로 다른 `Parcel`을 가리키는 여러 `LazyValue` 만들기
`Parcel.recycle()`과 `Parcel.obtain()`은 [후입선출(Last-In-First-Out) 방식](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))으로 동작합니다.
즉, `system_server`로 가는 다른 `Binder` 트랜잭션이 실행되고 있지 않을 때 조작된 `LazyValue`를 만들면, `system_server`로 들어오는 트랜잭션이 하나뿐일 때 항상 사용되는 `Parcel`을 가리키는 `LazyValue`를 얻게 됩니다 (스택 순서가 아닌 순서로 시작하고 끝나는 두 개의 동시 트랜잭션이 `system_server`로 들어오는 경우가 발생할 때까지).
`system_server`로 들어오는 다른 트랜잭션이 무엇인지 제어할 수 없으므로, 익스플로잇의 신뢰성을 높이기 위해 다양한 `Parcel`을 가리키는 여러 `LazyValue`를 만들었습니다.
`system_server`에서 내 프로세스로 동기식 `Binder` 트랜잭션을 촉발할 수 있기 때문에, 그 능력을 사용하여 내 프로세스와 `system_server` 사이의 다양한 재귀 깊이에서 `LazyValue`를 만들었습니다 (이번에는 전역 뮤텍스를 보유하지 않은 상태로 했지만요).
그래서:
* `LazyValue`를 만듭니다
* `system_server`에 호출을 촉발하면 `system_server`가 나에게 다시 호출합니다
* `LazyValue`를 만듭니다
* `system_server`에 호출을 촉발하면 `system_server`가 나에게 다시 호출합니다
* `LazyValue`를 만듭니다
* `system_server`에 호출을 촉발하면 `system_server`가 나에게 다시 호출합니다
* ...
그런 다음 충분한 수의 `LazyValue`를 확보하면 그 작업을 끝내고, 이 모든 호출에서 반환하면 이 호출들에 의해 예약되었던 모든 `Parcel`이 `recycle()`됩니다.
제가 만든 각 `LazyValue`는 [`getQueue()`가 생성한 별도의 `ParceledListSlice`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804)에 래핑되어 있으며, `ParceledListSlice` `Binder`를 호출하여 `system_server`가 그것을 직렬화해 내 프로세스로 보내게 할 수 있습니다.
(이를 대체하는 방법은 여러 `MediaSession`을 만드는 것이었을 것입니다.)
# 대상 앱 프로세스 시작하기
이제 `attachApplication()`이 발생할 때 `IApplicationThread`를 포착하는 데 필요한 모든 것을 갖추었지만, 여전히 `attachApplication()`이 발생하도록 만들어야 합니다.
일반적으로 [다른 앱이 상호 작용할 수 있는 몇 가지 유형의 앱 구성 요소](https://developer.android.com/guide/components/fundamentals#Components)가 있으며, 각각은 앱 프로세스가 시작되어야 합니다.
저는 시스템 설정(Settings) 앱을 시작하고 싶었습니다 (이 앱은 [system uid로 실행되며](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=5;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74), 따라서 [Android 권한 뒤에 있는 모든 것에 접근할 수 있습니다](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityManager.java;l=3948-3952;drc=16c119018a80b8630c7736a5c6dc35ebef130c5b)).
처음에는 `startActivity()`를 통해 실행하려고 시도했지만, 그렇게 했을 때 `ActivityTaskManagerService` 잠금을 해제하기 전까지 프로세스가 시작되지 않았습니다. 그 이유에 대한 자세한 내용은 "추가 참고: `Binder` 호출과 뮤텍스 재진입" 섹션에 있지만, 해결책으로 `Activity` 대신 그 앱의 [`ContentProvider`](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=4031-4034;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74)를 시스템에 요청하기로 결정했습니다. 이것은 내 UI와의 간섭을 피할 수 있는 추가적인 장점도 있었습니다.
저는 [SDK에서 제공하는 공식 `ContentResolver` API](https://developer.android.com/reference/android/content/ContentResolver)를 사용하지 않고 대신 [시스템 내부 API](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IActivityManager.aidl;l=153-154;drc=3d7be31a284d295af4c118c5675896e6fcc28907)를 사용했습니다. 제가 중단시키고 있는 `attachApplication()`이 끝나기 전에는 `ContentProvider`에 바인딩하는 것이 완료되지 않을 것이기 때문에 비동기 API가 필요했기 때문입니다. 물론 다른 스레드를 시작하는 것도 대안이 될 수 있었습니다.
(이 특정 `ContentProvider`가 무엇을 제공하는지는 중요하지 않습니다. 관련된 유일한 것은 그것에 연결을 설정할 수 있다는 것입니다.)
이것이 제가 설정 앱의 프로세스를 시작하는 방법입니다. 먼저 [공식적으로 제공되는 `ActivityManager.killBackgroundProcesses()` 메서드](https://developer.android.com/reference/android/app/ActivityManager#killBackgroundProcesses(java.lang.String))를 사용하여 이미 실행 중이지 않은지 확인합니다.
# 모든 것을 종합하기
기본 요소들은 이제 설명되었으므로, 이제 모든 것이 어떻게 함께 동작하는지 설명하겠습니다 (이것은 이 익스플로잇의 `MainActivity.doAllStuff()` 메서드를 거의 그대로 옮긴 것입니다):
1. 숨겨진 API 접근을 활성화합니다 (숨겨진 API는 보안 경계가 아니며 [이미 공개적으로 사용 가능한 우회 방법](https://www.xda-developers.com/bypass-hidden-apis/)이 있습니다. 다만 여기서는 다른 곳에서는 본 적이 없는 [`Property.of()`](https://developer.android.com/reference/android/util/Property#of(java.lang.Class%3CT%3E,%20java.lang.Class%3CV%3E,%20java.lang.String))에 기반한 방법을 사용했습니다)
2. (첫 번째 시도 후 익스플로잇을 다시 실행하는 경우에만) 이전 실행 중 6단계에서 설정한 `ContentProvider` 연결을 해제합니다. 그렇게 해야 합니다. 그렇지 않으면 `ActivityManager.killBackgroundProcesses()`가 대상 프로세스를 "백그라운드"로 간주하지 않아 종료하지 않기 때문입니다.
3. `attachApplication()`은 프로세스 시작 시에만 호출되므로 `ActivityManager.killBackgroundProcesses()`를 사용하여 피해자 앱 프로세스를 종료합니다.
4. `system_server`에 나중에 재활용되는 `Parcel`을 가리키는 `LazyValue`를 포함하는 객체들을 여러 개 생성하도록 요청합니다. `LazyValue`를 포함하는 각 객체에 대해 `ParceledListSlice` `Binder` 참조를 얻고, `Binder` 트랜잭션을 수행하여 시스템이 이를 다시 쓰도록 촉발할 수 있습니다. 각 `LazyValue` 객체 생성은 `system_server`와 내 앱 사이의 [상호 재귀적](https://en.wikipedia.org/wiki/Mutual_recursion) 호출의 서로 다른 깊이에서 수행되며, 이를 통해 각 `LazyValue`가 서로 다른 `Parcel` 객체에 대한 댕글링 참조를 가질 가능성을 높입니다.
5. `ActivityTaskManagerService.moveTaskToFront()`를 호출하여 `ActivityTaskManagerService.mGlobalLock`을 잠급니다. 인자로 전달하는 `Bundle`은 역직렬화 시 내 프로세스로 동기식 `Binder` 트랜잭션을 수행합니다. 다음 단계들은 그 콜백에서 수행되므로 해당 잠금이 보유된 상태에서 실행됩니다.
6. 피해자 앱의 `ContentProvider`에 대한 연결을 `ActivityManagerService`에 요청합니다 (이름에 "`Task`"가 없다는 점에 유의하세요. `ActivityTaskManagerService`는 주로 앱의 `Activity` 구성 요소 처리를 담당하는 클래스이고, `ActivityManagerService`는 다른 [앱 구성 요소](https://developer.android.com/guide/components/fundamentals#Components) (및 전반적인 프로세스 시작)를 처리합니다. 이 [분할은 Android 10에서 일어났으며, 이전에는 `Activity`와 다른 앱 구성 요소 처리가 모두 `ActivityManagerService`에 있었습니다](https://android.googlesource.com/platform/frameworks/base/+/595070969de0a7334d251d5448b641e856e052bc))
7. 새로 시작된 프로세스가 `attachApplication()`을 호출하기 시작할 시간을 주기 위해 잠시 `sleep()`합니다.
8. 잠금이 여전히 보유된 상태에서, 이전에 생성한 모든 `ParceledListSlice` 객체에 초기 트랜잭션에 맞지 않았던 나머지 내용, 즉 재활용된 `Parcel`을 가리키는 `LazyValue`를 포함하는 객체를 보내도록 요청합니다. 그런 다음 `attachApplication()`에 전달된 `IApplicationThread`의 위치와 일치하는 하드코딩된 오프셋에서 `Binder` 객체를 읽습니다. 지금은 잠금을 보유한 상태에서 너무 많은 작업을 피하기 위해 수신한 `Binder`들을 `ArrayList`에 저장만 합니다.
9. 이것은 5단계에서 시작된 콜백에서 수행하는 코드의 끝입니다. `ActivityTaskManagerService.mGlobalLock`이 잠금 해제됩니다.
10. `IApplicationThread` `Binder`를 얻었습니다. 이제 다음 섹션에서 설명하는 것처럼 이 Binder를 사용하여 피해자 앱에 내 코드를 로드할 수 있습니다.
# `IApplicationThread` 사용하기
앞서 언급했듯이 [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl;drc=45f4b4aaa5fd8af2d0c685b2fef8acf75ed37452) `Binder`는 앱 프로세스가 시작될 때 앱이 `system_server`로 보내며, 그런 다음 `system_server`는 이를 사용하여 앱에 어떤 구성 요소를 로드해야 하는지 지시합니다.
이 객체는 `system_server`에만 전달된다고 가정하므로 거기에는 `Binder.getCallingUid()` 기반 검사가 없습니다. 따라서 해당 인터페이스가 제공하는 메서드를 직접 호출할 수 있습니다.
저는 [이전 글에서 `scheduleReceiver()` 인자를 조작하여 코드 실행을 얻는 방법을 설명했습니다](https://github.com/michalbednarski/ReparcelBug2#what-then-happens-within-handlereceiver). 지금 상황은 동일하지만, 이번에는 제가 직접 `scheduleReceiver()`를 호출한다는 점이 다릅니다. 그때는 `system_server`가 수행한 호출의 인자 해석을 조작하고 있었습니다.
# 추가 참고 사항
이 섹션에서는 결국 이 경우에 유용하지 않은 것으로 판명 났지만, 알아둘 가치가 있거나 잠재적인 버그일 수 있는 몇 가지를 설명합니다.
## 추가 참고: `Bundle.clear()`단순화를 위해, 여기서는 [나중에 도입되어 `Bundle`에서 `LazyValue`를 뒷받침하는 `Parcel`을 `Bundle.clear()` 호출로 재활용할 수 있게 하는 커밋](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/) 하나를 제외하고 업데이트된 `Bundle`을 설명했습니다.
커밋 메시지에서 언급했듯이, `Bundle`이 복사되었는지 추적되며, 그 경우 `clear()`는 `Parcel`을 재활용하지 않습니다.
하지만 그 커밋은 [`BaseBundle.initializeFromParcelLocked()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=408-457;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)의 `recycleParcel` 매개변수/변수의 의미도 변경합니다.
이전에는 `recycleParcel`이 `false`라는 것은 `Parcel`을 재활용해서는 안 된다는 뜻이었습니다. [호출자가 `Parcel`이 `Bundle`의 소유가 아님을 나타내기 위해 `recycleParcel`을 `false`로 설정](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1842-1847;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)했거나, [`parcelledData.readArrayMap()`의 결과에 따라 `false`로 설정](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=441;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)되었기 때문입니다.
이제 `recycleParcel`이 `false`일 수 있는 이유는 동일하지만, 그 해석은 바뀌었습니다. 더 이상 "이 `Parcel`을 재활용하지 말라"는 뜻이 아니라 "`Parcel`의 재활용을 `Bundle.clear()` 호출 때까지 연기하라"는 뜻입니다.
이것은 `Parcel.hasReadWriteHelper()`가 `true`인 상태로 생성된 `Bundle`에 `clear()`가 호출되면 `Parcel`이 재활용되는 반면, 해당 `Bundle`의 생성을 호출한 코드도 그 `Parcel`을 재활용하여 이중 `recycle()`이 발생할 수 있음을 의미합니다. 이는 double-free와 유사한 동작을 초래합니다. 즉, 이후 `Parcel.obtain()` 호출은 같은 객체를 두 번 반환하게 됩니다.
하지만 그러한 `Bundle`에 `clear()`가 호출되도록 할 방법은 찾지 못했습니다.
이 글을 원래 작성한 이후, [`recycle()`의 동작이 변경되어 이제 추가 recycle은 `Log.wtf()`를 통한 크래시 가능성이 있는 no-op이 되었습니다](https://android.googlesource.com/platform/frameworks/base/+/64ff38669a0e1f945b54c4c62ed9316282a6588d%5E%21/) ([구성에 따라 다르지만 `system_server`를 절대 충돌시키지는 않습니다](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8619-8648;drc=ab23aee04d50b9bdabd52481e77265e976558056)). 새 동작은 여전히 위험할 수 있다고 생각합니다. 특히 다른 프로세스에서 진행 중인 역직렬화를 프로그래밍 방식으로 지연시킬 수 있는 능력이 있을 때 더욱 그렇습니다. 하지만 이중 recycle을 처리할 좋은 방법은 실제로 없습니다.
## 추가 참고: `Binder` 호출과 뮤텍스 재진입
`Binder`의 그다지 잘 알려지지 않은 기능 중 하나는 재귀 호출을 원래 스레드로 디스패치하는 것을 지원한다는 것입니다.
즉, 프로세스 A가 프로세스 B에 동기식 `Binder` 호출을 하고, 프로세스 B가 같은 스레드에서 이를 처리하는 동안 프로세스 A에 동기식 `Binder` 호출을 하면, 프로세스 A에서의 그 호출은 프로세스 B로의 원래 호출이 완료되기를 기다리는 바로 그 스레드에서 디스패치됩니다.
또한 Java의 `synchronized () {}` 블록은 재진입 가능한 뮤텍스입니다. 즉, 같은 스레드에서 두 번 진입해도 허용되며 교착 상태가 발생하지 않습니다.
이는 이론적으로 `ActivityTaskManagerService.mGlobalLock`을 잠근 채로도 `startActivity(new Intent(Settings.ACTION_SETTINGS))`를 사용해 설정 앱을 시작하고 우리가 지연시키고 있는 [`synchonized` 블록](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=626;drc=8854e6eb5960c1a9b233fd0fa6e36a366b2f802d)에 성공적으로 진입할 수 있음을 의미합니다. 하지만 해당 `Activity`를 시작하는 것은 `Task` 생성도 수반하며, 이는 [메시지를 게시하는 `notifyTaskCreated()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=424-429;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)를 [`DisplayThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/DisplayThread.java;l=25-31;drc=92b9365f9e1ea5d735e8acb06f790604036ee547)로 호출하는 것을 포함하고, [그 처리](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=207-208;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)는 [우리가 다른 스레드에서 지연시키고 있는 락을 획득](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=320;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)하려 시도합니다. 따라서 `ActivityTaskManagerService.mGlobalLock`을 해제할 때까지 `DisplayThread` 스레드는 차단된 상태로 유지됩니다.
그 후 `Activity`를 시작하는 절차는 [앱 프로세스를 시작하기 위해 같은 스레드에 메시지를 게시](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=4659-4664;drc=c76db81ef9d400ffed200f69c7bbda923cdb941c)하는 것을 수반합니다. 이 모든 것은, 이 경우 락을 해제할 때까지 앱 프로세스가 시작되지 않는다는 것을 의미합니다. 그리고 그 이유는 우리가 처음에 그 락을 잡고 있던 것이 `attachApplication()` 트랜잭션이 끝나지 않도록 하여 그 핸들을 가져오려는 것이었기 때문이지만, 이 경우 그 트랜잭션은 실제로 시작되지 않을 것입니다.
현재 것과 같은 `Task`의 일부가 될 `Activity`를 시작하더라도(즉, 설정 앱에서 `android:launchMode="singleTask"`를 지정하지 않은 다른 `Activity`를 실행하는 경우), 그 절차에는 여전히 [`notifyTaskDescriptionChanged()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=444-450;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)가 포함되며, 이는 여기서 `notifyTaskCreated()`와 동일한 영향을 미칩니다.
그래서 내 스레드가 `synchronized (ActivityTaskManagerService.mGlobalLock) {}`을 사용하는 메서드를 호출할 수는 있었지만, `startActivity()` 이후 새 앱 프로세스를 시작하는 것은 다른 스레드에서 그 락을 사용하는 것을 수반했고, 이 경우에는 유용하지 않았습니다. 따라서 대신 `ContentProvider`를 통해 앱 프로세스 시작을 트리거하는 방식을 선택했습니다.
## 추가 참고: `IApplicationThread`를 사용하는 다른 방법
`IApplicationThread`는 매우 권한이 높은 핸들이므로, 이를 획득한 후 사용하는 것은 post-exploitation으로 간주합니다.
이 익스플로잇에서는 이 핸들을 직접 사용하여 대상 프로세스에서 코드 실행을 요청했습니다. 해당 작업에 대한 접근이 `Binder.getCallingUid()`가 아니라 capability(여기서 유출한 `Binder` 객체의 소유)에 의해 제어된다는 점을 이용한 것입니다.
`Binder.getCallingUid()` 검사를 [`ApplicationThread.scheduleReceiver()` (여기서 코드 실행을 요청하는 데 사용)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=985-997;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) 및 `ApplicationThread`의 다른 메서드에 추가한다고 해도, `IApplicationThread`를 사용해 다른 앱의 프로세스에 코드를 로드하는 것을 막을 수는 없습니다. `scheduleReceiver()`는 코드 로딩을 허용하는 `IApplicationThread`의 유일한 메서드가 아니며, 공격자는 유출된 `IApplicationThread`를 자신의 것 대신 `attachApplication()`에 전달할 수 있기 때문입니다.
프로세스에 코드를 로드하는 것 외에도, `IApplicationThread`를 보유하면 [해당 핸들이 속한 프로세스의 권한을 사용하여 `grantUriPermission()`을 수행](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5631-5632;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)할 수 있습니다.
unparcel(boolean itemwise)sourceBundleParcelmParcelledDatarecycleParceltrueParcelBundleParcelinitializeFromParcel는 키-값 맵 내용을 읽기 위해 recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader)를 호출합니다. 키는 String이고 값은 readLazyValue()를 사용해 읽힙니다. 길이 접두어와 함께 쓰이는 유형의 값에 대해 LazyValue 객체가 생성됩니다. readArrayMap()은 Parcel을 재활용해도 되는지 여부를 나타내는 값을 반환합니다. LazyValue 객체가 하나라도 있으면 recycleParcel은 false로 설정되고 LazyValue들이 참조하는 Parcel은 재활용되지 않습니다. (이에 대한 예외가 있지만 여기서는 관련이 없습니다. "추가 참고: Bundle.clear()" 섹션에서 설명하겠습니다.)unparcel()이 완료되면 mMap이 설정되고(null이 아님), String 키는 준비된 경우 실제 값 또는 LazyValue 객체에 매핑됩니다.getValue()가 호출되는데, 키(String)를 인덱스(int)로 매핑하고 이를 getValueAt()에 전달합니다.LazyValue.apply()는 Parcel을 LazyValue.mPosition 위치로 되감은 다음 일반 Parcel.readValue()를 호출합니다. 이에 대해서는 이미 설명했습니다.LazyValue는 mMap에서 교체됩니다. 따라서 같은 키에 대한 다음 Bundle.get*() 호출은 값을 직접 반환하고 LazyValue 역직렬화가 반복되지 않습니다. Bundle이 전달될 때 해당 값은 원본 데이터를 그대로 복사하는 대신 다시 직렬화됩니다. (단, 전달된 Bundle을 읽은 후에는 그 값이 다시 LazyValue가 되며, 가능한 writeToParcel/createFromParcel 불일치가 다른 값에는 영향을 미칠 수 없습니다.)Parcelable.writeToParcelBinder 트랜잭션 크기 한도에 가까워지면 0이 기록되어 이 트랜잭션에는 더 이상 항목이 없으며 다음 항목들은 다른 트랜잭션에서 전송됨을 나타냅니다.ParcelableListBinder가 첫 번째 트랜잭션에서 지정된 수의 요소를 수신하면 생성자에 전달된 람다를 호출합니다. 이 경우 그 람다는 수신된 목록을 MediaSessionRecord.mQueue에 할당합니다.ParceledListSlice가 Parcel에서 읽힐 때 먼저 첫 번째 부분을 Parcel에서 직접 읽고, 모든 요소가 인라인으로 기록되지 않은 경우 해당 항목들을 가져오기 위해 Parcel에 기록된 Binder를 호출합니다.ParcelBinderParcel.recycle()ParcelRemoteViewsmakeOwnedLeakermakeHolderLeakergetActionFromParcel()