
CVE-2024-34740 の解説とエクスプロイト、Android の BinaryXmlSerializer における整数オーバーフローを利用して system_server へのファイル書き込み、その後通常のインストール済みアプリから system_server でのコード実行に至る

ここに記載された問題の修正は、CVE-2024-34740 / A-307288067 として公開されました:
Android system_server 内では、多くのサービスが再起動をまたいだ状態を XML ファイルに保存しています```
$ adb shell su 0 find /data/system -name '*.xml' | sort
/data/system/appops_accesses.xml
/data/system/cachequota.xml
/data/system/device_policies.xml
/data/system/device_policy_state.xml
/data/system/display-manager-state.xml
/data/system/input-manager-state.xml
/data/system/inputmethod/subtypes.xml
/data/system/install_sessions.xml
/data/system/job/jobs_1000.xml
/data/system/job/jobs_10131.xml
/data/system/log-files.xml
/data/system/netpolicy.xml
/data/system/notification_policy.xml
/data/system/overlays.xml
/data/system/packages.xml
/data/system/package-watchdog.xml
/data/system/sensor_privacy_impl.xml
/data/system/sensor_privacy.xml
/data/system/shortcut_service.xml
/data/system/users/0/app_idle_stats.xml
/data/system/users/0/appwidgets.xml
/data/system/users/0/package-restrictions.xml
/data/system/users/0/settings_global.xml
/data/system/users/0/settings_secure.xml
/data/system/users/0/settings_system.xml
/data/system/users/0/wallpaper_info.xml
/data/system/users/0.xml
/data/system/users/userlist.xml
/data/system/watchlist_settings.xml
歴史的には、これらはインデント付きのプレーンテキストXMLファイルであり、開発者が簡単に読めるようになっていました。しかし、[Android 12ではそのフォーマットの新しいバイナリ版が導入され、`system_server`が費やす時間の1.5%がこれらのXML操作に使われていると報告されています](https://android.googlesource.com/platform/frameworks/base/+/4ccea8796991d678ead4399130ec31edf63ff4fa%5E%21/)
このフォーマットはシステム内部でのみ使用され、マジック値 `"ABX\x00"` を持つファイルであることに注意が必要です。これは、APK内部で使用される[フォーマット](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/androidfw/ResourceTypes.cpp;l=1770;drc=d4e49e63519397789d284a03aea5fafc119cb1b0)(`AndroidManifest.xml`、`res/xml/*.xml`、`res/layout/*.xml`など)とは異なります。後者には明示的な「マジック値」はありませんが、通常は `0300 0800` で始まります(これは `type=RES_XML_TYPE` かつ `headerSize=8` の[ヘッダー](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h;l=608;drc=d4e49e63519397789d284a03aea5fafc119cb1b0)です)。
システムがこれらの内部状態XMLファイルのいずれかを読み取る際、ファイル内のマジック値 `"ABX\0"` を使用して、Binary XMLファイル用のパーサーと通常のXMLパーサーのどちらかを選択します([参照](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Xml.java;l=188-192;drc=97a370a95275e79c69e79d7ead11aa38934a5575))。これらのファイルがBinary XMLとして保存されるかどうかは[システムプロパティ](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Xml.java;drc=97a370a95275e79c69e79d7ead11aa38934a5575;l=74?q=Xml.java)によって制御され、デフォルトで有効になっています。
Binary XMLファイルが使用されている場合、例えば `adb shell su 0 abx2xml /data/system/packages.xml -` を通じてその内容を読み取ることができます。
このバイナリフォーマットの利点の一つは、型付きアクセサを提供することです。シリアライザは `attributeInt(String namespace, String name, int value)` メソッドを提供し、値をバイナリ整数として書き込みます。これにより、文字列へのラウンドトリップ(新しい割り当てとその後のガベージコレクション対象となるオブジェクトの生成)を回避します。
直接シリアライズできる別の型はバイト配列です。```java
@Override
public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
mOut.writeInternedUTF(name);
mOut.writeShort(value.length);
mOut.write(value);
return this;
}
類似のメソッド attributeBytesHex も存在し、TYPE_* タグの記述のみが異なります。このタグは abx2xml ツールによってバイト配列を適切な文字列表現に変換するために使用されます。
mOut は FastDataOutput のインスタンスであり、Java の DataOutputStream の機能を提供します。writeByte/writeShort/writeInt/writeUTF/write は標準の DataOutputStream と同じフォーマットを使用します。
Parcel と同様に、書き込み/読み取り中に何かが一致しない場合、その後読み取られるデータは誤ったオフセットから取得されます。ただし、Parcel とは異なり、BinaryXmlSerializer/BinaryXmlPullParser の使用方法の誤りによって、攻撃者が読み取りデータを任意に改ざんできる能力は与えられません(その場合、攻撃者は新しいタグ名/属性名/値を導入できません)。
しかし、BinaryXmlSerializer クラス自体または FastDataOutput 内での誤りは、そうした改ざんを可能にします。
上記のメソッドで、長さが 65536 のバイト配列を書き込もうとした場合、writeShort() を使用して長さを書き込むと、実際には 0 が書き込まれ、その後実際の配列の内容が書き込まれます。
この不一致を悪用するには、attributeBytesBase64 または attributeBytesHex に任意のバイト配列を注入できるファイルを選択し、かつそのファイルの改ざんが攻撃者にとって価値があるものを選ぶ必要があります。
PackageInstaller クラスは、インストール用のパッケージを準備する機能を提供します。特別な権限は不要で、どのアプリでも一時ディレクトリにインストールする新しいAPKを書き込むことができます。インストールに必要なすべてのものが書き込まれたら、インストールアプリは commit() PackageInstaller.Session を実行できます。これは、インストールファイルにそれ以上変更を加えることができなくなり、Session がユーザーの承認または実際のインストールの準備ができたことを意味します。
これらの操作の状態は /data/system/install_sessions.xml に保存されます。インストーラーアプリは、例えば、Package Manager Service が作成した一時ディレクトリに大規模なAPKの半分をダウンロードし、再起動後にダウンロードを再開して残りの半分を書き込み、インストールをコミットすることができます。
可能性の1つは、install_sessions.xml にデータを書き込んでセッションを staged(次の起動後にインストールされる) とマークすることです。
もう1つ、ここで紹介するのは、インストールファイルが準備される一時ディレクトリへのパスを変更することです。openWrite()/openRead() は パストラバーサルがない限り任意の有効なファイル名を受け入れ、そのファイルを stageDir フィールドが指すディレクトリに配置します。このフィールドは XMLから読み取られます。
次に、実際に制御下のバイト配列を attributeBytesBase64() に渡す必要があります。
PackageInstaller.Session は setChecksums() メソッドを提供しています。
system_server 側では、提供された Checksum は必要に応じて呼び出し元の署名に対して検証され、その後 mChecksums に格納されます。
install_sessions.xml が書き込まれる際、checksum.getValue() が writeByteArrayAttribute に渡され、それがさらに attributeBytesBase64() に渡されます。