
CVE-2022-20452のエクスプロイト、recycle()後のParcelを使用したLazyValue経由で、Android上でインストールされたアプリからシステムアプリ(または別のアプリ)への特権昇格
Android 13 では、Parcel のシリアライゼーションメカニズムを強化するために多くの機能強化が導入されています。
こちらが Android セキュリティ&プライバシーチームによる機能強化のプレゼンテーションです
これは素晴らしく、多くの脆弱性を排除するか、悪用不可能にしています。また、以前のエクスプロイト(アプリが他のアプリ(システムアプリを含む)にコードをロードできるようにするもの)を破ったことについても説明しています。
しかし今、私は同じことを別の方法で実現する新しいエクスプロイトを持ち帰ってきました。これは、前述の Parcel の強化中に導入された以下の脆弱性に依存しています。
![アプリケーションのテキストを表示するスクリーンショット。タイトル: LeakValue。メインテキスト: Created 6 ValueLeaker-s. Locking ActivityTaskManagerService. Locked ActivityTaskManagerService. Unlocking ActivityTaskManagerService. Unlocked ActivityTaskManagerService. leakedBinders=[android.os.BinderProxy@f06702e]. Leaked interface: 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. 画面下部には2つのボタンがあります: START と MANUAL TESTING](Screenshot_20220723-081920.png)
(アプリ実行時の logcat も参照。エクスプロイトはログにノイズが多い)
Parcel と Parcelable の不一致バグの紹介Android の Parcel クラスはプロセス間通信の基盤です。
オブジェクトは Parcelable インターフェースを実装することで、Parcel に書き込めるようになります。例 (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)を含めることができます。`Bundle`の内容が`Parcel`から読み取られる際、システムで利用可能な任意の`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`内のすべての値が[アンパーセル化](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`の異なる内容を認識する可能性があります。これにより、システム内のクラスの不均衡がすべて[脆弱性](https://github.com/michalbednarski/ReparcelBug)となり、システム内に[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)が存在するためです。
このwriteupでは、そのような、転送後に別の内容を提示するBundleを自己変更Bundleと呼びます。
もう一つ重要な点として、バイト列 (文字列、数値、それらから構成されるオブジェクト) だけでなく、`Parcel`はファイルディスクリプタや`Binder`も含めることができます。`Binder`はRPC呼び出しが可能なオブジェクトであり、あるプロセスが`Binder`オブジェクトを作成し、[`onTransact()`メソッド](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))をオーバーライドします。その後、`Binder`は別のプロセスに渡されます。上のコード例では、`read`/`writeStrongBinder()`呼び出しを使用してParcelに読み書きしていることがわかります。別のプロセスでは、`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では、自己変更Bundleの構築を可能にするようなクラスがシステム内のどこかに存在する問題を解決するために、[`LazyValue`を導入しました](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/)。
現在、`writeValue`が使用される際、書き込まれる値がプリミティブでない場合、[値の長さも`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 があるかどうか(つまり Parcel から読み取られたが、まだ値にアクセスされておらず、キー名が展開されていないことを意味します。そうでない場合は手順 5 にスキップします)をチェックします。unparcel() は unparcel(boolean itemwise) に委譲し、。 は ( が作成した のコピー)に設定され、 パラメータは に設定され、渡された は が所有しており、 を呼び出しても問題ないことを示します。Bundle がまだ LazyValue を含んでいる状態で転送される場合(つまり、その特定の値にはアクセスされなかったが、その Bundle の他の値にはアクセスされた場合(つまり unparcel() が呼び出されたが、そのアイテムの LazyValue.apply() は呼び出されなかった場合)):
LazyValue は Parcel.writeValue() によって検出され、書き込みは委譲されます LazyValue.writeToParcel() へ。LazyValue.writeToParcel() は out.appendFrom(source, mPosition, mLength) を使用して、元の Parcel から LazyValue データ全体をコピーします(繰り返しますが、mPosition と mLength は LazyValue ヘッダーを含むため、元の Parcel から type と length もコピーされます)。Parcel.ReadWriteHelper と Parcel.readSquashed(これらの詳細はこのエクスプロイトにとって重要ではありません。関連するのはこれらのメカニズムが存在することだけです。)
Parcel のもう 1 つの興味深い機能は、書き込まれた String やオブジェクトを重複排除するオプション機能です。
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 に書き込まれたことを意味します。それ以外の場合(squashing が有効でないか、このオブジェクトが初めて書き込まれる場合)、maybeWriteSquashed はオフセットとして 0 を書き込み(オブジェクトが squash されていないことを示す)、呼び出し元に実際のオブジェクトデータを書き込む必要があることを示す false を返します。Parcel.readSquashed が呼び出され、実際の読み取り関数がラムダとして渡されます。readSquashed は、maybeWriteSquashed() によって書き込まれたオフセットが、このオブジェクトの別の出現が以前に読み取られたことを示しているかどうかを確認します。そうであれば、以前に読み取られたオブジェクトが返され、そうでなければ提供されたラムダが呼び出されて今すぐ読み取られます。Parcel.recycle() 後の Use-After-FreeJava サイドでは、Parcel オブジェクトはプールにリサイクルできます。つまり、Parcel を使い終わったら recycle() を呼び出し、次に誰かが Parcel.obtain() を呼び出すと、以前にリサイクルされた Parcel が取得されます。これにより、オブジェクト割り当ての量とその後のガベージコレクションを削減できます。
一方、このような手動メモリ管理は、Java に Use-After-Free に似たバグの可能性をもたらします(ただし、型安全性があるため、C の通常の Use-After-Free とは異なります)。
上記のように、Bundle は Parcel のコピーを作成し、LazyValue が存在する場合は Parcel.recycle() を呼び出しません。ただし、Parcel.hasReadWriteHelper() が true の場合はそうではありません。その場合:
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle); が呼び出され、これは 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 が squashing を有効にして、それにネストされた ApplicationInfo オブジェクトを重複排除するためですが、これにより Bundle 内に存在する ApplicationInfo オブジェクトも squash される可能性があるため、その Bundle の読み取りを延期できません。そうしないと、それらの squash されたオブジェクトが unsquash に失敗するからです。
Parcelable を system_server に配置して取得するでは、system_server に、デシリアライズに失敗する LazyValue を含む Bundle を含む RemoteViews を読み取らせ、後で(別の Binder IPC トランザクションで)そのオブジェクトを私たちに返送させたいとします。
これはおそらく、正規の方法で行うこともできるでしょう。たとえば、自分自身をアプリウィジェットホストとして登録する(ただし、これにはユーザーの操作が必要で許可が与えられます)か、contentView が設定された Notification を投稿する(ただし、これにより他のプロセスとの対話が発生したり、ユーザーに見えたりするため、私は両方を避けたかった)などです。
代わりに、MediaSession を作成し、setQueue(List<MediaSession.QueueItem> queue) を呼び出してオブジェクトを system_server に送信し、後で MediaController(MediaSession.getController() で取得可能)の List<MediaSession.QueueItem> getQueue() メソッド を介して取得することにしました。これらのメソッドは RemoteViews を受け入れられるようには見えませんが、実際には Java の型消去 と、それらが内部的に List に対する汎用シリアライズ操作を使用して実装されているという事実のおかげで可能です。
ただし、これらの SDK メソッドは使用せず、基盤となる Binder トランザクションのデータを手動で書き込んでいます(改ざんされたシリアライズデータを書き込み、後で読み取る必要があるため)。では、これらのメソッドがどのように機能するかを見てみましょう。
これらのメソッドは両方とも、キューの合計サイズが Binder トランザクションの最大サイズを超える可能性があるため、転送を複数のトランザクションに分割できるようにする必要がありました。
system_server への「キュー」の送信は通常、次のように行われます。
MediaSession.setQueue() は最初に ISession.getBinderForSetQueue() を呼び出します。system_server 側では、そのメソッド ParcelableListBinder オブジェクトを構築して返します。MediaSession.setQueue() は ParcelableListBinder.send() を呼び出し、これはリストの内容を提供された Binder に送信し、場合によっては複数のトランザクションにわたって行われます。
1 が書き込まれ、実際のアイテムは Parcel.writeParcelable() を介して書き込まれます(送信するクラスの名前を書き込み、次に を呼び出してデータを送信します)。一方、「キュー」の取得は少し異なります。
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 からの発信同期トランザクションについて警告するコード はありますが、system_server がそのような呼び出しを行うケースがまだあるため(たとえば、実際に ParceledListSlice を受信する など)、まだ強制できるようにはなっていません。
これで、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() されません(ガベージコレクタによってクリーンアップされ、プールに戻りません。プールが枯渇すると、Parcel.obtain() は新しい Parcel オブジェクトを作成します)。もちろん、参照を保持している Parcel は、システムが他に使用しなくなっても GC されることはありません。Binder トランザクションに使用される Parcel は、システム内の他の Parcel とは別のプールを使用します。発信 Binder トランザクションが行われるとき、Bundle がセカンダリ Parcel にデータをコピーするか、アプリが独自の目的で を使用する場合、 が呼び出され、これは を使用します。一方、着信 トランザクションがある場合、、これは を使用します。どちらの場合も、その後 が使用され、 処理が行われます。つまり、エクスプロイトは、データを漏洩させたいプールと同じプールに属する から が読み取るようにする必要があります。特定のバリアントを決定する前に、私は両方のバージョンを書いたので、 に と の両方のメソッドがあります。結局、私は IApplicationThread の Binder を取得しようと試みることにしました。この Binder は、アプリプロセスが開始するときにアプリから system_server に送信され、system_server はそれを使用してアプリケーションにどのコンポーネントをロードすべきかを伝えます。
アプリケーションプロセスが最初に起動すると、最初に行うことの一つは、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() が失敗します)さて、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`は読み取り後にアンパーセルされるため、意図的に`Parcelable`に欠陥のあるものを仕込んで`LazyValue`のままにしておく必要があります。`LazyValue`がある位置に有効なパーセルデータを置くと、それはアンパーセルされ、前述したように長さの不一致は`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. `Bundle`キーは`Parcel.readString()`で読み取られますが、実質的に何でも構いません。無効な長さ(負、または`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`がパーセルデータの最外オブジェクトである場合、適切な場所はありません
次の条件を満たす別の`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`から準備したオブジェクトを読み取るプロセスは次のように進みます:
* [項目存在フラグ](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577) → `readParcelable`開始
* [`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`は2つのキーと値のペアを持つと宣言する。最初の値には負の長さを持つ`LazyValue`があり、それが`Parcel.setDataPosition()`をトリガーして`"android.os.Message"`文字列がある位置に移動する
* 読み取りは2番目のキーと値のペアに進む。キーは`"android.os.Message"`で、`LazyValue`のタイプ、長さ、データは前述の3番目の箇条書きで説明した`int`から取得される。目的の`mPosition`と`mLength`を持つ`LazyValue`を取得できました。やった!
* `LazyValue`が読み取られた後、それらはアンパーセルされる。負のサイズのものは正常にアンパーセルされ、空の`Map`に置き換えられる。一方、もう一方はデシリアライズに失敗するが、その例外はキャッチされ、`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)。しかし、そのマジック値は長さが[ゼロ](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`の読み取りは失敗します。なぜなら、`Binder`は`Parcel`内の特別なオブジェクトであり、読み取るにはアウトオブバンドで注釈を付ける必要があるからです。この問題は、[`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 () {}`ブロックの使用により)。それらのミューテックスの1つを取得してそこで停止すれば、このメソッドも停止します
ここで、この文書ですでに述べられ、この目的に役立ついくつかの点に戻りましょう:
* `Bundle`は、値にアクセスされたときに値のデシリアライズを実行します
* `ParceledListSlice`クラスは、デシリアライズ中に、シリアライズデータ内で指定されたオブジェクトへのブロッキングな発信`Binder`呼び出しを行います
これらすべてをまとめると、`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()`は[後入れ先出し方式](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))で動作します
つまり、他の`Binder`トランザクションが`system_server`に実行されていないときに細工された`LazyValue`を作成すると、`system_server`に着信するトランザクションが1つだけの場合に常に使用される`Parcel`を指す`LazyValue`が得られます(2つの同時着信トランザクションが非スタック順序で開始および終了するまで)
`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()`によって作成された](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804)別々の`ParceledListSlice`にラップされており、`ParceledListSlice`の`Binder`を呼び出して`system_server`にシリアライズさせ、自分のプロセスに送信させることができます
(これを実現する別の方法としては、複数の`MediaSession`を作成することもできます)
# ターゲットアプリプロセスの起動
これで、`attachApplication()`発生時に`IApplicationThread`をキャプチャするために必要なものがすべて揃いましたが、`attachApplication()`を発生させる必要があります
一般に、[他のアプリが操作できるアプリコンポーネントにはいくつかの種類があり](https://developer.android.com/guide/components/fundamentals#Components)、それぞれアプリプロセスを起動する必要があります
私はシステム設定アプリ([システム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)。なぜなら、`ContentProvider`へのバインドは、私が停止している`attachApplication()`が完了するまで終了しないからです。ただし、別のスレッドを開始するという代替手段もありました
(この特定の`ContentProvider`が提供するものは重要ではありません。関連するのは、それに接続を確立できることだけです)
これが Settings アプリのプロセスを開始する方法です。まず、[公式に利用可能な`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. `ActivityManager.killBackgroundProcesses()`を使用して被害アプリケーションプロセスを強制終了する。`attachApplication()`はプロセス起動時にのみ呼び出されるため
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`をロックします。引数として、デシリアライズ時に自分のプロセスへの同期`Binder`トランザクションを実行する`Bundle`を渡します。以降のステップはそのコールバック内から実行されるため、そのロックが保持された状態で行われます
6. `ActivityManagerService`に被害アプリの`ContentProvider`との接続を要求します(名前に「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. 少し`sleep()`して、新しく起動したプロセスが`attachApplication()`の呼び出しを開始する時間を与えます
8. ロックが保持されている間に、以前に作成したすべての`ParceledListSlice`オブジェクトに、残りの内容(最初のトランザクションに収まらなかったもの)を送信するよう要求します。つまり、リサイクルされた`Parcel`を指す`LazyValue`を含むオブジェクトです。次に、`attachApplication()`に渡された`IApplicationThread`の位置に一致するハードコードされたオフセットから`Binder`オブジェクトを読み取ります。今は、受信した`Binder`を`ArrayList`に保存するだけにして、ロックを保持したままの作業を避けます
9. ここで、ステップ5で開始したコールバックから実行するコードは終了です。`ActivityTaskManagerService.mGlobalLock`がロック解除されます
10. `IApplicationThread`の`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)。今回の状況は、前回は`system_server`によって行われた呼び出しの引数の解釈を改ざんしていたのに対し、今回は自分で`scheduleReceiver()`を呼び出している点を除いて同じです
# 補足
このセクションでは、最終的にこのケースでは役に立たなかったものの、知っておく価値のある機能や潜在的なバグについて説明します
## 補足:`Bundle.clear()`ここでは、説明を簡単にするために、後で導入された[特定のコミット](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/)(`Bundle`で`LazyValue`のバッキングに使用される`Parcel`を`Bundle.clear()`を呼び出すことでリサイクルできるようにするもの)を除いた、更新された`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`をリサイクルすべきでないことを示していました。これは、[呼び出し元が`recycleParcel`を`false`に設定して`Parcel`が`Bundle`に所有されていないことを示す場合](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`をリサイクルしない」という意味ではなく、「`Bundle.clear()`が呼び出されるまで`Parcel`のリサイクルを延期する」という意味になります。
つまり、`Parcel.hasReadWriteHelper()`が`true`である`Parcel`で作成された`Bundle`に対して`clear()`が呼び出されると、その`Parcel`がリサイクルされることになりますが、その`Bundle`を作成したコードもその`Parcel`をリサイクルするため、二重の`recycle()`が発生し、これはダブルフリーと同様の動作を引き起こします。次の`Parcel.obtain()`の呼び出しは、同じオブジェクトを2回返すことになります。
ただし、そのような`Bundle`に対して`clear()`を呼び出す方法は見つかりませんでした。
これを最初に書いてから、[`recycle()`の動作が変更され、現在は追加のリサイクルはno-opとなり、`Log.wtf()`によるクラッシュの可能性があります](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))。新しい動作は依然として危険である可能性があると言えます。特に、別のプロセスで発生するデシリアライゼーションをプログラム的に妨害できる場合、二重リサイクルを適切に処理する良い方法はありません。
## 補足: `Binder`呼び出しとミューテックスの再入可能性
`Binder`のあまり知られていない機能として、元のスレッドへの再帰呼び出しのディスパッチをサポートしていることがあります。
つまり、プロセスAが同期`Binder`呼び出しをプロセスBに行い、プロセスBが同じスレッドでそれを処理している間に同期`Binder`呼び出しをプロセスAに行うと、プロセスAでのその呼び出しは、元のプロセスBへの呼び出しの完了を待っている同じスレッドでディスパッチされます。
もう一つは、Javaの`synchronized () {}`セクションは再入可能なミューテックスであり、同じスレッドから2回入ると、許可され、デッドロックにはなりません。
これは理論的には、`ActivityTaskManagerService.mGlobalLock`をロックしたまま、`startActivity(new Intent(Settings.ACTION_SETTINGS))`を使用して設定アプリを起動でき、[`synchronized`ブロック](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`と同じ`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`は非常に特権的なハンドルであるため、それを取得した後の使用はポストエクスプロイテーションと見なします。
このエクスプロイトでは、ターゲットプロセスでコード実行を要求するために直接使用し、その操作へのアクセスが能力(ここでは漏洩した`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`のメソッドに`Binder.getCallingUid()`チェックを追加しても(`scheduleReceiver()`はコードロードを許可する`IApplicationThread`の唯一のメソッドではないため)、`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)。
sourcemParcelledDataBundleParcelrecycleParceltrueParcelBundleinitializeFromParcel は 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()ParcelRemoteViewsmakeOwnedLeakermakeHolderLeakerRemoteViews.readActionsFromParcel()