
CVE-2023-45777에 대한 분석 및 익스플로잇, Android 13의 AccountManagerService 내 Intent 검증 우회 — "Lazy Bundle" 완화 조치에도 불구하고
이번에는 Android Security Bulletin에서 CVE-2023-45777에 대한 수정 사항으로 등장한 패치부터 시작해 보겠습니다.```diff diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 7a19d034c2c8..5238595fe2a2 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -4923,7 +4923,7 @@ public class AccountManagerService p.setDataPosition(0); Bundle simulateBundle = p.readBundle(); p.recycle();
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
if (intent != null && intent.getClass() != Intent.class) {
return false;
}
몇몇 사람들은 이 문제에 대해 충분히 궁금해하여 저에게 물어보기도 했습니다. 이전에는 힌트만 알려드렸는데, 이제 이 이슈에 대한 전체 분석 글을 공개합니다.
하지만 먼저 이 패치에서 무슨 일이 일어나고 있는지에 대한 맥락을 살펴보겠습니다.
이것은 [`checkKeyIntent()` 메서드](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4938-4954;drc=47de64a38aa1799cb41f41b2ea0c539ee61de64d)의 변경 사항입니다. 이 메서드는 애플리케이션이 제공한 `Intent`가 시스템이 (시스템 권한을 사용하여) 실행하기에 안전한지 확인하기 위해 여러 검사를 수행합니다.
먼저, 이 메서드는 `checkKeyIntentParceledCorrectly()`를 사용하여 검사 중인 `Bundle`을 직렬화한 다음 다시 역직렬화하고, 그 전에 `Bundle`에서 가져온 `Intent`가 이러한 과정을 거친 후의 `Bundle`의 `Intent`와 일치하는지 확인합니다. `Intent`의 실행은 검증을 수행하는 프로세스가 아닌 다른 시스템 앱 프로세스에서 발생하므로, 이전에는 [`AccountManagerService` 내부의 검증 중에는 안전해 보이지만 다음 프로세스로 전송된 후에는 다른 `Intent`를 포함하는 `Bundle`을 구성하는 것이 가능했습니다](https://github.com/michalbednarski/IntentsLab/issues/2#issuecomment-344365482). 이는 `Bundle`을 다음 프로세스로 보내는 것을 시뮬레이션하여 이러한 상황을 감지합니다.
`checkKeyIntentParceledCorrectly()` 다음에는 `bundle.getParcelable()` 호출이 있는데, 이 패치는 이 호출을 임의의 객체를 생성할 수 있는 더 이상 사용되지 않는 버전에서 역직렬화하려는 객체가 두 번째 매개변수에 지정된 유형인지 검증하는 버전으로 전환합니다.
유형 매개변수가 있는 이 버전은 더 큰 `Parcel`/`Bundle` 강화의 일부로 Android 13에서 도입되었습니다. 특히, Android 13 이전에는 `Bundle`이 프로세스 간에 전송될 때 항목에 접근할 때까지 직렬화된 데이터 전체의 원본 복사본을 유지했으며, 이때 모든 값이 역직렬화되었습니다. 이제 `Bundle`을 수신한 후 어떤 값에 처음 접근하면 `String` 키와 기본 유형의 값만 역직렬화되고, 기본 유형이 아닌 값은 `LazyValue`로 남게 됩니다. `LazyValue`는 직렬화된 데이터의 일부로 길이를 저장하므로 직렬화/역직렬화 로직이 일치하지 않더라도 이러한 불일치가 다른 항목에 영향을 미치지 않도록 보장합니다.
자세히 살펴보기 전에 `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
mPosition 및 mLength는 원본 Parcel에서 전체 LazyValue 데이터의 위치를 설명하며, type과 length를 포함합니다. "length"(앞에 "m"이 없는 것)는 Parcel에 기록된 길이 값을 의미하며 헤더(type 및 length)는 제외합니다.
LazyValue를 포함하는 Bundle이 다른 프로세스로 전달되는 경우, type 및 length 필드를 포함한 전체 LazyValue가 Bundle.mParcelledData에서 대상 Parcel로 그대로 복사됩니다
LazyValue로 표현되는 Bundle 항목에 접근하면, Parcel이 mPosition으로 되감기고 readValue()가 호출됩니다. bundle.getParcelable()에 타입 인수가 전달되면 이는 readValue()로 전파되어, 언파셀링될 타입이 예상된 타입인지 확인할 뿐만 아니라 언파셀링 후 언파셀링된 값의 타입이 예상된 타입인지 검증합니다. 언파셀링 후 LazyValue는 교체되므로, 다음에 Bundle이 Parcel에 기록될 때 값은 writeValue()를 통해 다시 직렬화됩니다.
타입이 지정된 Bundle.get*()/Parcel.read*() 매개변수의 사용은 주로 Parcel.readParcelableList()와 같은 메서드와 관련이 있습니다. 이 메서드는 ArrayList를 반환하며, Java 타입 소거(Type Erasure)로 인해 List<SomeParcelableType> field = parcel.readParcelableList();와 같은 코드를 작성하더라도 <SomeParcelableType> 부분은 런타임에 강제되지 않으므로, 이러한 List에는 시스템에서 사용 가능한 모든 Parcelable 클래스가 포함될 수 있으며, 따라서 시스템에서 사용 가능한 모든 createFromParcel/writeToParcel이 해당 List를 포함하는 타입의 직렬화/역직렬화의 일부로 사용될 수 있습니다.
또한 Android 보안 및 개인정보 보호 팀의 이러한 메커니즘 도입에 관한 프레젠테이션(슬라이드, 비디오)을 확인해 보시기 바랍니다.
그러나 여기서는 반환된 객체의 타입을 명시적으로 확인하기도 하므로 타입이 지정된 버전의 사용이 중복되는 것으로 보입니다. 그렇다면 여기서 무슨 일이 일어나고 있으며 어떤 취약점이 수정되고 있는 것일까요?
처음부터 패치를 다시 살펴보겠습니다.
"intent" 키 아래에서 역직렬화된 값이 Intent인 경우
Intent 객체에서 불일치가 발생할 수 있다면 훨씬 더 큰 문제가 있을 것입니다Intent가 아닌 경우
Bundle 내부에 Intent가 있어야 하지만, Parcelable의 타입은 가능한 불일치보다 더 이른 오프셋에 저장되며 LazyValue의 길이 접두사(length-prefixing)로 인해 writeToParcel/createFromParcel 불일치 시 다음 키-값 쌍을 수정할 수 없습니다.그렇다면 타입 인수 없이 bundle.getParcelable(AccountManager.KEY_INTENT)를 호출하면 여기서 어떤 위험한 일이 발생할 수 있을까요?
[다음 문단에 답이 있습니다. 읽기 전에 추측해 보세요. 제가 퍼소나(fursona)를 가지고 있다면 여기에 예술 작품이 들어갈 자리일 것입니다]
답은 다른 키 아래에 저장되고 다음 프로세스에 그대로 전달될 LazyValue의 원시 데이터를 실제로 수정하는 관련 없는 createFromParcel()을 호출하는 것입니다.
제공된 Parcel에서 실제로 writeInt()를 호출할 수 있는 createFromParcel() 구현이 있습니다.
하지만 이는 writeInt가 실수로 배치되었기 때문이 아니라 제한 없는 리플렉션(reflection) 때문입니다. 특히 PackageParser 내부에는 다음과 같은 코드가 있습니다:```java
final Class cls = (Class) Class.forName(componentName);
final Constructor cons = cls.getConstructor(Parcel.class);
intentsList = new ArrayList<>(N); for (int i = 0; i < N; ++i) { intentsList.add(cons.newInstance(in)); }
We can have `Parcel` object which was passed to `createFromParcel` passed to any available in system `public` constructor that accepts single `Parcel` argument
And then [we have following code](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/PooledStringWriter.java;l=51-56;drc=782d49826862cbdc9d020fc9d85f8a6f64675dcb):```java
public PooledStringWriter(Parcel out) {
mOut = out;
mPool = new HashMap<>();
mStart = out.dataPosition();
out.writeInt(0); // reserve space for final pool size.
}
We've got constructor that calls writeInt(0) on provided Parcel, however there are few things that complicate exploitation
First of all, while it isn't directly visible in source code, immediately after newInstance() is called, a cast is performed and ClassCastException is thrown
I needed something that would during createFromParcel call createFromParcel of another class under try block and then fail to propagate caught Exception
This is part where exploit doesn't actually work on pure AOSP, I've used Samsung specific class
I've included copy of relevant parts of that class in this repo
This repo also includes script that integrates it into AOSP, so for testing you can run it (pass path to your AOSP checkout as argument, e.g. ./make-aosp-buggy.sh /path/to/aosp), revert change described at beginning of writeup and run this exploit against your AOSP build
I've previously used OutputConfiguration class from AOSP for Exception swallowing, before Android 13 swallowing an Exception in createFromParcel() combined with allowing construction of other Parcelable-s is vulnerability in itself, however in case of SemImageClipData Exception swallowing wasn't present on these Android versions
There is however important difference between SemImageClipData and previously used OutputConfiguration: even though SemImageClipData catches an Exception, it still returns non-null object and if it will be later cast to another type, that would trigger ClassCastException which is what we're trying to avoid
Java Type Erasure means that generic methods don't actually know about generic type used by caller. This usually was helping exploitation```java // When we read some List, this actually didn't check if list contains only SomeParcelableType List myList = sourceParcel.readParcelableList();
// Above is why Android 13 has introduced typed methods that enforce type at runtime List myList = sourceParcel.readParcelableList(SomeParcelableType.class);
// If untyped method was used when reading, list can contain non-SomeParcelableType // items and they would be written without errors targetParcel.writeParcelableList(myList, 0);
// However if List contains non-SomeParcelableType item, this would throw during item access // (That however commonly didn't happen if we used Parcelable object only as container in gadget chain) SomeParcelableType myItem = myList.get(0);
이번에는 타입 소거(type erasure)가 우리에게 유리하게 작용하지 않았습니다. 먼저 리플렉션을 통해 실제로 생성자를 호출하는 메서드가 있었습니다.```java
private static <T extends IntentInfo> ArrayList<T> createIntentsList(Parcel in) {
// ...
final ArrayList<T> intentsList;
// ...
intentsList.add(cons.newInstance(in));
// ...
return intentsList;
}
이 메서드는 제네릭 파라미터 T를 가집니다. 호출자가 어떤 파라미터 타입을 사용했는지는 중요하지 않지만, 이 메서드 선언에 <T extends IntentInfo>가 있기 때문에 newInstance() 호출이 있는 줄은 intentsList.add((IntentInfo) cons.newInstance(in));이 됩니다. newInstance()가 Object를 반환하고 ArrayList.add()가 인자로 Object를 받아들임에도 불구하고 말이죠. 이로 인해 해당 호출을 Exception을 삼키는 어떤 Parcelable로 감싸야 할 필요가 생겼습니다.
그런 다음 bundle.getParcelable() 호출이 있습니다.```java
@Deprecated
@Nullable
public T getParcelable(@Nullable String key) {
unparcel();
Object o = getValue(key);
if (o == null) {
return null;
}
try {
return (T) o;
} catch (ClassCastException e) {
typeWarning(key, o, "Parcelable", e);
return null;
}
}
역직렬화 절차는 `getValue()` 호출에 의해 수행되며, 실제로는 `createFromParcel()` 호출로 이어집니다. 만약 거기서 `ClassCastException`이 발생하면 잡히지 않습니다. `getValue()`는 이제 이 키에 대해 [`parcel.readValue()`](https://developer.android.com/reference/android/os/Parcel#readValue(java.lang.ClassLoader))를 통해 역직렬화된 값을 반환합니다.
하지만 `SemImageClipData`를 값으로 넣고 `try`-`catch` 블록 안에서 `T`로 캐스팅을 시도한다면, 이 경우 `T`는 메서드의 제네릭 선언에 명시된 대로 `Parcelable`입니다. 호출자는 이 메서드를 `T`가 `Intent`인 제네릭으로 사용하지만, `getParcelable()`은 이를 알지 못하며 `Intent`로의 캐스팅은 호출자 측에서 발생하므로 `ClassCastException`은 `try` 블록 밖에서 발생합니다.
하지만 `SemImageClipData`를 `Parcelable[]` 배열로 감싸면, `getParcelable()` 내부에서 `T`로의 캐스팅이 `Parcelable[]`을 `Parcelable`로 캐스팅하는 데 실패하여 `try` 블록 안에서 `ClassCastException`이 발생하고, 해당 `Exception`은 로그로 기록된 후 `null`이 반환되어 `checkKeyIntent()`에서 수용됩니다.
# 레이아웃
이제 `Bundle` 내부의 항목들을 정렬하여 `writeToParcel`/`createFromParcel` 주기를 거친 후 그 내용이 우리가 준비한 것이 되도록 해야 합니다.
하지만 트리거가 `createFromParcel`이 이전에 `writeToParcel`이 수행한 것보다 많거나 적은 데이터를 읽는 것인 일반적인 "`Bundle` 펑수이(FengShui)"와 달리, 여기서는 `writeInt(0)`이 역직렬화되지 않은 `LazyValue`의 일부를 덮어씁니다.
다음은 `Bundle.mParcelledData`가 `AccountManagerService`에 의해 처음 역파셀링될 때의 모습입니다 (오프셋은 `system_server`에 연결된 디버거를 통해 `dataPosition()`을 호출하여 얻음).
<table>
<tr><th>오프셋</th><th>값</th><th>비고</th></tr>
<tr><td>0</td><td>3</td><td>키-값 쌍의 수</td></tr>
<tr><td>4</td><td>"intent"</td><td><code>Bundle</code>의 첫 번째 키로, <code>getParcelable(AccountManager.KEY_INTENT)</code>으로 접근되는 키</td></tr>
<tr><td>24</td><td>16</td><td>첫 번째 <code>LazyValue</code>가 여기서 시작되며, 타입은 <code>VAL_PARCELABLEARRAY</code></td></tr>
<tr><td>28</td><td>340</td><td><code>LazyValue</code>의 선언된 길이로, <code>Bundle</code>에서 다음 키를 찾는 데 사용됨. 우리의 <code>LazyValue</code>는 읽힌 후 실제로 이 크기를 갖지 않지만, <code>LazyValue.apply</code>는 <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Parcel.java;l=4501-4505;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">이를 <code>Slog.wtfStack()</code>으로 보고하며</a>, 이는 <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Slog.java;l=230-235;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">예외를 던지지 않습니다</a></td></tr>
<tr><td>32</td><td>1</td><td><code>Parcelable[]</code> 배열의 길이. 배열에는 항목이 하나만 있으며, <code>bundle.getParcelable()</code> 내부의 <code>try</code> 블록 안에서 <code>ClassCastException</code>이 발생하도록 존재함</td></tr>
<tr><td>36</td><td>"com.samsung.android.<br>content.clipboard.data.<br>SemImageClipData"</td><td><code>Parcelable</code> 클래스의 이름. 예외를 삼키는 래퍼 클래스</td></tr>
<tr><td>160</td><td>2</td><td><code>createClipBoardData()</code>에서 사용하는 타입 태그</td></tr>
<tr><td>164</td><td></td><td><code>SemImageClipData</code> 슈퍼클래스 생성자에서 읽는 항목들 (<code>readParcelable()</code> 호출 포함, 그러나 이는 <code>try</code> 블록 밖에서 발생). 실제로 중요하지는 않지만, <code>createFromParcel()</code>의 흥미로운 부분에 도달하기 전에 이들을 통과해야 함</td></tr>
<tr><td>252</td><td></td><td><code>SemImageClipData.readFromSource()</code>에서 읽는 데이터</td></tr>
<tr><td>272</td><td>"android.content.pm.<br>PackageParser$Activity"</td><td><code>mExtraParcelFd = in.readParcelable()</code>로 읽는 <code>Parcelable</code>의 이름. 타입이 일치하지 않지만, 캐스팅이 발생하기 전에 어차피 예외가 발생함</td></tr>
<tr><td>360</td><td></td><td><code>PackageParser$Component</code>의 <code>className</code> 및 <code>metadata</code> 필드</td></tr>
<tr><td>368</td><td>1</td><td><code>createIntentsList()</code>의 항목 수</td></tr>
<tr><td>372</td><td>"android.os.<br>PooledStringWriter"</td><td><code>Class.forName().getConstructor(Parcel.class).newInstance()</code>를 통해 인스턴스화할 클래스의 이름. 이 위치에서 첫 번째 <code>LazyValue</code>가 끝나지만, <code>readValue()</code>가 끝에 도달하지 않았으므로 파싱은 계속됨. 또한 초기 <code>unparcel()</code> 중 <code>Bundle</code>의 두 번째 키로 해석됨</td></tr>
<tr><td>436</td><td>4</td><td>두 번째 <code>LazyValue</code>가 여기서 시작됨. 이 4는 <code>VAL_PARCELABLE</code>로, <code>Parcel.isLengthPrefixed()</code>가 <code>true</code>를 반환함. 이 값은 나중에 <code>PooledStringWriter</code> 생성자에 의해 덮어써지며, 그 후 예외가 발생하고 <code>getParcelable(AccountManager.KEY_INTENT)</code>이 종료됨</td></tr>
<tr><td>440</td><td>240</td><td>타입이 <code>VAL_PARCELABLE</code>로 선언된 <code>LazyValue</code>의 길이. 다음 항목의 위치와 재직렬화 중 대상 <code>Bundle</code>에 복사해야 할 데이터 양을 결정하는 데 사용됨. 이 <code>LazyValue</code>는 실제로 역파셀링되지 않으며 원시 데이터 컨테이너로 사용됨</td></tr>
<tr><td>684</td><td>"1&y~pw"</td><td rowspan="2">세 번째 키/값 쌍. 키는 Java <code>hashCode()</code>가 이전에 사용된 키들보다 높도록 무작위로 생성됨 (<code>ArrayMap</code>에 저장된 항목은 키의 <code>hashCode()</code> 오름차순으로 정렬되며, 이것이 <code>Bundle</code>의 항목이 <code>Parcel</code>에 기록되는 순서임). 이 키-값 쌍은 기록되는 총 쌍 수를 늘리기 위해 여기에만 존재하며, 읽힐 쌍 수가 그 수가 되기 때문임. 실제로 이 쌍은 읽히지 않음</td></tr>
<tr><td>704</td><td>-1 (<code>VAL_NULL</code>)</td></tr>
</table>
그런 다음 `Bundle`이 다시 직렬화되면 다음과 같습니다:
<table>
<tr><th>오프셋</th><th>값</th><th>비고</th></tr>
<tr><td>0</td><td>3</td><td>키-값 쌍의 수</td></tr>
<tr><td>4</td><td>"intent"</td><td><code>Bundle</code>의 첫 번째 키</td></tr>
<tr><td>24</td><td>16</td><td><code>VAL_PARCELABLEARRAY</code>, 이전에 역직렬화된 <code>Parcelable[]</code> 배열이 다시 직렬화되고 있음</td></tr>
<tr><td>28</td><td>196</td><td><code>LazyValue</code>의 길이, 즉 우리가 감싼 <code>SemImageClipData</code> 객체. 이 길이는 내 mock <code>SemImageClipData</code>로 실행한 결과에서 가져온 것이므로, 이 지점부터 제시된 오프셋은 실제 Samsung 기기에서 나타날 값과 일치하지 않지만, 이 <code>LazyValue</code>는 다시 역직렬화되지 않으므로 익스플로잇 실행에는 문제가 없음</td></tr>
<tr><td>224</td><td>"android.os.<br>PooledStringWriter"</td><td><code>Bundle</code>의 두 번째 키</td></tr>
<tr><td>288</td><td>0</td><td>두 번째 <code>LazyValue</code>가 여기서 시작됨. <code>"android.os.PooledStringWriter"</code> 키 아래의 항목은 접근되지 않았으므로 이 <code>LazyValue</code>는 원본 데이터에서 복사되지만, 타입 태그는 <code>PooledStringWriter</code> 생성자에 의한 <code>writeInt(0)</code> 호출로 덮어써졌으며 대상 프로세스에 도달하면 더 이상 <code>LazyValue</code>로 해석되지 않음</td></tr>
<tr><td>292</td><td>240</td><td>이것은 원본 <code>Bundle</code>에서 복사된 두 번째 <code>LazyValue</code>의 길이였지만, 타입 태그가 <code>writeInt(0)</code>으로 덮어써졌으므로 이 값은 이제 <code>VAL_STRING</code>이며 <code>readString()</code>을 통해 읽힘. 이전에는 <code>LazyValue</code>의 경우 길이가 바이트로 표현되었지만, 이제 <code>String</code>의 경우 2바이트 문자로 표현됨. 소스 <code>Parcel</code>에 그만한 데이터가 충분하지 않으므로 <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=2221-2226;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">네이티브 <code>parcel->readString16Inplace()</code>가 길이를 읽은 후 실패</a>하지만, 이는 Java 측에서 예외를 발생시키지 않음</td></tr>
<tr><td>296</td><td>"intent"</td><td><code>Bundle</code>의 "세 번째" 키. 실제로는 첫 번째 키를 덮어씀: <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=651-659;drc=584140c83a456b5de99880b440c2d5dfc3c70506">"intent"는 이전에 본 키보다 작은 <code>hashCode()</code>를 가지므로 <code>ArrayMap.append()</code> 메서드는 값 교체를 허용하는 <code>put()</code>을 사용</a>하며, 그렇지 않으면 <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=667-675;drc=584140c83a456b5de99880b440c2d5dfc3c70506">나중에 <code>validate()</code>에 의해 거부될 중복 키가 발생</a>함</td></tr>
<tr><td>316</td><td>4</td><td><code>VAL_PARCELABLE</code>, 여기서 시작될 실제 <code>Intent</code>를 포함하는 <code>LazyValue</code>가 시작됨</td></tr>
<tr><td>536</td><td>"1&y~pw"</td><td>기록되었지만 3개의 키-값 쌍이 모두 이미 읽혔으므로 읽히지 않는 패딩 항목. <a href="https://cs.android.com/android/_/android/platform/system/tools/aidl/+/96a02f50fdfa4d20aa46ae2dde927257eac46d4a">AIDL 인터페이스와 달리</a> <code>Bundle</code>에는 <code>enforceNoDataAvail()</code> 검사가 수행되지 않음 (하지만 있다고 해도 <a href="https://github.com/michalbednarski/ReparcelBug2/issues/3">예상 길이를 지정하는 더미 항목을 삽입하여 우회할 수 있음</a>)</td></tr>
</table>
# 두 번 발생한 방법
이제 네 개의 패치에 대해 논의해 보겠습니다. 그 중 두 개는 이 글에서 다루는 취약점을 수정합니다.
* CVE-2023-20944 ([게시판](https://source.android.com/docs/security/bulletin/2023-02-01#framework), [패치](https://android.googlesource.com/platform/frameworks/base/+/d0bc9026e2e62e09fa88c1bcbf1dc1c3fb001375%5E%21/)): 제가 발견한 또 다른 취약점입니다. 이 취약점과 유사하게 패치만으로는 어떻게 악용될 수 있는지 명확하지 않지만, <a href="https://konata.github.io/posts/creator-mismatch/">다른 누군가가 알아낸 것 같습니다 (중국어 블로그 게시물)</a>
* CVE-2023-21098 ([게시판](https://source.android.com/docs/security/bulletin/2023-04-01#framework), [패치](https://android.googlesource.com/platform/frameworks/base/+/107e6377328486fca55131ea06ca9d6a3c1585e0%5E%21/)): 여기에 제시된 익스플로잇을 처음 보고한 때입니다. 해당 패치는 또한 Android 13 이전 버전에 적용되는 `checkKeyIntentParceledCorrectly()` 우회에 대한 수정을 도입합니다.
* CVE-2023-35669 ([게시판](https://source.android.com/docs/security/bulletin/2023-09-01#framework), [패치](https://android.googlesource.com/platform/frameworks/base/+/f810d81839af38ee121c446105ca67cb12992fc6%5E%21/)): 이 패치는 제 보고에 대한 응답이 아니지만, CVE-2023-20944이 다루는 것과 동일한 문제를 수정하기 위해 만들어졌다고 생각합니다. 단, `AccountManager.KEY_INTENT`가 `ChooseTypeAndAccountActivity`가 아닌 다른 Activity에 의해 실행되는 경우 (예: <a href="https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Settings/src/com/android/settings/accounts/AddAccountSettings.java;l=95-107;drc=32813a2bef49b172aed89122b4eb50bf14026ddc">`AddAccountSettings`</a>, 처음 버그를 보고할 때 놓친 경우)에 해당합니다. 이 변경은 타입이 지정된 `bundle.getParcelable()` 사용을 타입이 지정되지 않은 사용과 수동 `getClass() != Intent.class` 검사로 대체했으며, 이는 실제로 CVE-2023-21098에 대한 수정을 되돌렸습니다.
* CVE-2023-45777 ([게시판](https://source.android.com/docs/security/bulletin/2023-12-01#framework), [패치](https://android.googlesource.com/platform/frameworks/base/+/f4644b55d36a549710ba35b6fb797ba744807da6%5E%21/)): 이 익스플로잇을 두 번째로 보고한 때입니다. 패치는 수동 `getClass() != Intent.class` 검사를 유지했지만, 그에 더해 타입이 지정된 `bundle.getParcelable()` 사용을 다시 도입했으며, 이는 두 문제를 모두 수정하는 좋은 방법입니다.
동일한 익스플로잇이 CVE-2023-21098과 CVE-2023-45777 모두에 작동하지만, `checkKeyIntentParceledCorrectly()`를 우회한 방식은 다릅니다.
CVE-2023-21098의 경우, <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=3519-3521;drc=cdd30b5c040ba7ebd0a1cc6009183ff602434fc0">검사된 `Bundle`에 `Intent`가 없으면 `checkKeyIntent()`가 실제로 호출되지 않았습니다</a>. `checkKeyIntent()`가 `checkKeyIntentParceledCorrectly()`를 호출하는 것이므로, 원본 `Bundle`에 `Intent`가 포함된 것처럼 보이지 않는 경우 재직렬화된 `Bundle`은 검사되지 않았습니다.
CVE-2023-45777의 경우, `checkKeyIntentParceledCorrectly()`가 올바르게 호출되었지만, <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4921-4929;drc=b0f6558fb36eb76df35c516ec5a65030a34a8734">타입 인자 없이 `getParcelable()` 호출 전에 `writeBundle()`이 발생했습니다 (그때까지 `Bundle`은 내용을 변경하지 않았습니다)</a>.