Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
ThisSeemsWrong — CVE-2024-49746の解説とエクスプロイト:AndroidのParcel::continueWriteによる、後で使用されるファイルディスクリプタのクローズ | Kitploit
ツール/GitHubGitHub/michalbednarski/thisseemswrong
Androidセキュリティエクスプロイトフレームワーク脆弱性分析情報収集ペイロード開発バイナリエクスプロイト
GitHubmichalbednarski/thisseemswrong

ThisSeemsWrong

CVE-2024-49746の解説とエクスプロイト:AndroidのParcel::continueWriteによる、後で使用されるファイルディスクリプタのクローズ

リポジトリを見る
4715211ヶ月前Kitploit レビュー済み

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

この問題の修正は CVE-2024-49746 として公開されました: bulletin, patch

"これは間違っているようです"

上記のタイトルは、Parcel::continueWrite メソッドからのコメントです。このメソッドは実際には Parcel オブジェクトのサイズ変更を担当しており、ユーザーが明示的に要求した場合(例えば setDataSize() を通じて)や、現在のデータ容量が小さすぎるために write メソッドのいずれかを呼び出す場合に、サイズ変更を行います。```cpp status_t Parcel::continueWrite(size_t desired) { // SNIP: Validate desired size // SNIP: Assign kernelFields & rpcFields from variant member of this class // SNIP: Count number of objects (Binder handles and File Descriptors) // that will be present after resize and assign to objectsSize

root@kitploit:~
if (mOwner) {
    // If the size is going to zero, just release the owner's data.
    if (desired == 0) {
        freeData();
        return NO_ERROR;
    }

    // If there is a different owner, we need to take
    // posession.
    uint8_t* data = (uint8_t*)malloc(desired);
    // SNIP: Check if malloc succeeded
    binder_size_t* objects = nullptr;

    if (kernelFields && objectsSize) {
        objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
        // SNIP: Check if calloc succeeded

        // Little hack to only acquire references on objects
        // we will be keeping.
        size_t oldObjectsSize = kernelFields->mObjectsSize;
        kernelFields->mObjectsSize = objectsSize;
        acquireObjects();
        kernelFields->mObjectsSize = oldObjectsSize;
    }
    // SNIP: rpcFields handling for non-/dev/binder Parcels

    if (mData) {
        memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
    }
    if (objects && kernelFields && kernelFields->mObjects) {
        memcpy(objects, kernelFields->mObjects, objectsSize * sizeof(binder_size_t));
    }
    // ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
    if (kernelFields) {
        // TODO(b/239222407): This seems wrong. We should only free FDs when
        // they are in a truncated section of the parcel.
        closeFileDescriptors();
    }
    mOwner(mData, mDataSize, kernelFields ? kernelFields->mObjects : nullptr,
           kernelFields ? kernelFields->mObjectsSize : 0);
    mOwner = nullptr;

    // SNIP: Allocation count tracking
    // SNIP: Assign data and objects to this object
} else if (mData) {
    // SNIP: Resize data owned by this instance of Parcel
} else {
    // SNIP: Allocate initial data for currently empty Parcel
}

return NO_ERROR;

}

root@kitploit:~
[そのコメントが導入された時点](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/)で、`closeFileDescriptors()` の呼び出しは `IPCThreadState::freeBuffer()`(上記のコードで `mOwner()` 関数ポインタを通じて呼び出される)から `continueWrite()` メソッドに移動されましたが、ロジックは以前と同じでした。結局のところ、`Parcel` は Android IPC の核となる部分であり、もしコア IPC が本来閉じてはいけないファイルディスクリプタを閉じていたとしたら、それは明らかな問題になるでしょう。

ここで重要な点に移ります。上記のコードはいつ使用されるのでしょうか?これは、Parcel クラスが Binder ドライバから受信したデータ(その時点では [`/dev/binder` `mmap`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567) 内に存在し、書き込みができない(そのメモリに書き込もうとすると `SIGSEGV` が発生する))の所有権を移動するときに使用されます。つまり、`Parcel` が着信トランザクションデータ([`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) に渡される `data` 引数)または着信リプライ(すなわち、[`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) 呼び出しに `reply` 引数として渡された `Parcel` オブジェクト、`transact()` はその `Parcel` オブジェクト内に参照を設定します)のいずれかである場合です。

実際には、`if (mOwner)` ブロックに入る唯一のケースは、システムが[トランザクションデータを解放するために `setDataSize(0)` を呼び出す](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567)場合ですが、その場合でも `if (desired == 0)` に入り、早期リターンが発生します。正当なシステム使用中に「異なる所有者がいる場合、所有権を取得する必要がある」パスに入るケースはありません。

# 「所有権取得」パスのトリガー

以前のエクスプロイトの1つで、[`createFromParcel()` が、本来読み取るべき `Parcel` に対して `writeInt(0)` を実際に呼び出すことができるケース](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects)を示しました。その修正により、`AccountManagerService` 内での `Intent` 以外の `createFromParcel()` メソッドの実行は防止されましたが、`createFromParcel()` から `writeInt(0)` へのパスはそのまま維持されました。

まとめると、[`PackageParser` 内には以下のコードがあります](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759):```java
final Class<T> cls = (Class<T>) Class.forName(componentName);
final Constructor<T> cons = cls.getConstructor(Parcel.class);

intentsList = new ArrayList<>(N);
for (int i = 0; i < N; ++i) {
    intentsList.add(cons.newInstance(in));
}

したがって、Parcel オブジェクトを createFromParcel に渡し、それをシステム内で利用可能な単一の Parcel 引数を受け取る public コンストラクタに渡すことができます。

また別の場所では、次のコードがあります:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }

root@kitploit:~
したがって、「所有権取得」パスをトリガーするには、`onTransact()` に `data` として渡された `Parcel` 上で任意の `readParcelable` 呼び出しが必要です。このエクスプロイトでは、以前別のエクスプロイトで使用した[同じパス](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them)を利用しています。`readParcelable` 呼び出しによって `PackageParser$Activity.CREATOR.createFromParcel()` が呼び出され、さらにその中で `PooledStringWriter` の名前が読み取られ、コンストラクタが呼び出された後で `Parcel` データが終了します。そのため、`writeInt()` は Parcel を再割り当てする必要があり、これによって「所有権取得」パスに入ります。

ここで注意すべき点ですが、もしその時点で `Parcel` データが終了していなかった場合、`writeInt()` はデータをその場で上書きしようとします。データが `/dev/binder` の `mmap` でバックアップされている場合、これは `SIGSEGV` を引き起こします。

# ファイルディスクリプタサニタイザ

当初のアイデアは、「所有権取得」パスでファイルディスクリプタを閉じ、その後トランザクションの終了時に同じディスクリプタが再度閉じられるようにすることでした。しかし、その間に別のトランザクションで `system_server` 内に別のファイルディスクリプタを配置し、後で自分のファイルディスクリプタを取り戻すというものです。その時点でその FD は別のファイルを指していることになります。

これは古い AOSP バージョンを使用したエミュレータでは機能しましたが、より新しいバージョンで試したところ、[ファイルディスクリプタサニタイザ (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md) によって阻止されました。

特に、[`android-14.0.0_r29` では FDSan のカバレッジが拡張され、`Parcel` 内の FD も対象となりました](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/)。

実際、FDSan が Parcel をカバーした後は、`Parcel` が FD を含んでいる場合、`closeFileDescriptors()` 呼び出しに到達することすらできませんでした。その呼び出しの前には `acquireObjects();` の呼び出しがあります。これは `Binder` ハンドルへの参照を取得します(そのハンドルは後で同じ関数内の `mOwner()` 呼び出しによって解放されます)。しかし、`acquireObjects()` は [FD に FDSan タグを設定](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=175-178;drc=f4c9b48c19f1b040efb35932b322f47e7779cafe) も行います。```cpp
case BINDER_TYPE_FD:
    if (obj.cookie != 0) { // owned
        FdTag(obj.handle, nullptr, who);
    }

問題は、私たちがすでにカーネルから受信したFDをタグ付けしている```cpp // In Parcel::ipcSetDataReference, which assigns this Parcel object to data from kernel (mOwner != null) if (type == BINDER_TYPE_FD) { // FDs from the kernel are always owned FdTag(flat->handle, nullptr, this); }

root@kitploit:~
つまり、二重の`FdTag`(古いタグを指定したままタグを閉じたり変更したりしない)はFDSanエラーを引き起こし、プロセスが中断されます。そして、まだ`closeFileDescriptors()`呼び出しに到達していません。「所有権取得」パスは通常の使用ではデッドコードであるため、このような問題は見過ごされる可能性があります

しかし、`if (obj.cookie != 0)`条件があります。これが偽の場合、`Parcel`内に存在するFDはその`Parcel`によって実際には所有されていないことを意味し、その`Parcel`が存在する限りFDを開いたままにしておくのは`Parcel`ユーザーの責任です。ただし、この`Parcel`はカーネルから来たばかりなので、`cookie`値は実際には元のプロセスからのものであり、`Parcel`が実際に`mOwner`を持っている場合には無関係と見なされます。しかし、「所有権取得」パスは実際にはそれを考慮せず、`cookie`値をそのままコピーします

これらをすべて合わせると、送信側で`cookie`値をゼロに設定することで、閉じられたファイル記述子を参照するが、それらを所有しているとは見なされず、再度閉じることもない`Parcel`を取得できます。これにより、FDSanをトリップするのを回避できますが、二重閉じの悪用経路も排除されます

# ParcelのJava側のトリック

そのようなFDは別の`Parcel`(さらに別のプロセス)に渡される可能性がありますが、このようなぶら下がりFDの作成をトリガーする方法は、リフレクションを介して`PooledStringWriter`を構築し、その後`ArrayList<IntentInfo>`に`add()`しようとすると`ClassCastException`がスローされるというものです

必要なこと:

* `onTransact()`に`data`引数として渡された`Parcel`の末尾にいること
* `PooledStringWriter`を構築し、その後Parcelはぶら下がりFDを持つが、`ClassCastException`もスローされる
* `system_server`内でリークさせたいFDが割り当てられるのを待つ
* その`Parcel`からのFDが、私たちのプロセスに送信される別の`Parcel`にコピーされる

これらの要件をすべて満たすために、新旧両方のトリックの一部を使用する必要があります

## 古いトリック

まずは古いトリックを再確認しましょう。それらのほとんどは、私の[`LazyValue`-using-`Parcel`-after-`recycle()` exploit](https://github.com/michalbednarski/LeakValue)ですでに説明されています

1. [`RemoteViews`クラスは、`Parcel.ReadWriteHelper`が設定された状態で、含まれている`Bundle`の逆シリアル化を実行します](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4)。[`ReadWriteHelper`が設定されている場合、`Bundle`は遅延ではなく先読みで逆シリアル化されます](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545)。また、この時点で注意すべき点として、`RemoteViews`内のその`Bundle`に含まれるすべての`Bundle`も、直接`RemoteViews`内にあるものだけでなく、先読みで逆シリアル化されます
2. [`system_server`内で`Bundle`を逆シリアル化中に`BadParcelableException`がスローされた場合、その`BadParcelableException`は静かにキャッチされ、`Bundle`の内容はクリアされます](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e)。ただし、`createFromParcel`内での書き込みトリガーは`ClassCastException`をスローするため、ここではキャッチされないことに注意してください
3. [`ParceledListSlice`クラスは、逆シリアル化中にシリアル化データで指定されたオブジェクトに対してブロッキング発信Binder呼び出しを行います](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364)。これを使用して逆シリアル化の実行を停止させることができます

これらはすべて今必要ですが、それだけではありません

## 新しいトリック

[AIDLはRPCインターフェース実装を生成するためのツールですが](https://developer.android.com/guide/components/aidl)、それに加えて`Parcelable`構造の実装も生成できます

これらの構造は長さプレフィックス付きであるため、同じ構造の異なるバージョンは、途中にフィールドが追加されない限り、システム内で互換性があります(つまり、一方のバージョンが他方のプレフィックスである場合、バージョンは互換性があります)

AIDLが[`ReceiverInfo`構造](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ReceiverInfo.aidl)に対して生成したコードを見てみましょう:```java
public final void readFromParcel(android.os.Parcel _aidl_parcel)
{
  int _aidl_start_pos = _aidl_parcel.dataPosition();
  int _aidl_parcelable_size = _aidl_parcel.readInt();
  try {
    if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
    if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
    intent = _aidl_parcel.readTypedObject(android.content.Intent.CREATOR);
    if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
    data = _aidl_parcel.readString();
    if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
    extras = _aidl_parcel.readTypedObject(android.os.Bundle.CREATOR);
    // SNIP: Other fields
  } finally {
    if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
      throw new android.os.BadParcelableException("Overflow in the size of parcelable");
    }
    _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
  }
}

このコードにより、残りの2つのトリックを実行できるようになります。どちらも「take possession」パスをトリガーした後にさらなるアクションを実行するために必要です。このパスでは、Parcelの末尾にいる必要があり、ClassCastExceptionがスローされます。

まず、Parcelから長さを読み取り、それを使って書き込まれたバージョンに存在しないフィールドをスキップし、最後にその長さに基づいてParcel内の位置を調整します。そのAIDL Parcelableの位置より前に移動することは許可されていません(Parcelの末尾を超えて移動することは可能ですが、実際には危険なことは何も起こりません)。ただし、既に読み取られたオブジェクトの途中に移動することはできます。そのため、最初の読み取り中にParcelの末尾に到達し、PooledStringWriterを構築してから、このReceiverInfoオブジェクト内の何かの途中に巻き戻すことができます。

これで末尾に到達する問題は解決しましたが、ClassCastExceptionの問題が残っています。しかし、ここにはsetDataPosition()を呼び出す前にオーバーフローがないか検証するfinallyブロックがあります。オーバーフローがある場合、BadParcelableExceptionがスローされます。では、finallyブロックが別の例外が保留中にExceptionをスローするとどうなるでしょうか?finally内でスローされた例外が優先され、以前のExceptionは静かに消えます。これでClassCastExceptionの代わりにBadParcelableExceptionが発生し、Bundleはsystem_server内でのExceptionを避けるためにこれを親切に無視します。

ParcelableListBinderパッチに関する注意

MediaSessionのParcelableListBinderを使用して、system_server内に任意のParcelableを配置し、後でそのオブジェクトを取得しています。

最近、ParcelableListBinderパッチが導入され、それが正確に禁止されました。

このエクスプロイトの観点からは、このパッチは実際には私を止めませんが、それが存在する場合と存在しない場合で異なる処理が必要です。

そのパッチが存在する場合、非QueueItemはsystem_serverが受信するリストから静かに削除されます(Exceptionを引き起こしません)。副作用のために非QueueItemが必要なので、最初のアイテムとして非QueueItemを配置し、その後に実際のQueueItemを続けることができます。これにより、任意のParcelableのデシリアライズは実行できませんが、ファイル記述子(プロセスに渡したいもの)を含むことができるBundleが含まれています。

そのパッチが存在しない場合、その障害はありませんが、同じフローを使用することもできません。上記のケースでは、RemoteViewsとQueueItemの両方を含むリストになるため、ParceledListSliceは混合型リストの転送を拒否します。その場合、漏洩したFDをRemoteViewsインスタンス内に配置する必要があります。

しかし、このパッチについてより興味深いのは、それが導入された理由です。コミットメッセージには「アプリがバックグラウンドから起動できるようにする」とあり、Androidセキュリティ速報はあまり語っていませんが、CVEエントリで有用な情報を見つけることができます。これはNotification.mAllowlistTokenフィールドを参照しており、これは読み取り中に静的フィールドから取得でき、system_server内ではバックグラウンドでのActivity起動を許可するトークンであり、その後そのトークンはNotification.writeToParcel()に書き込まれます。これは、system_serverが任意のParcelableをデシリアライズしてアプリに送り返すすべてのケースが、現在脆弱性になっていることを意味しますか?いずれにせよ、今はそれはただの考えであり、このエクスプロイトではさらに多くのことをしたいと思います。

これらを組み合わせる

このエクスプロイトは、私がこれまでに行った中で最も複雑なParcelableガジェットチェーンを備えていると思います。

  • RemoteViews (1)
    • ReflectionAction (2)
      • Bundle
        • Parcelable[] (3)
          • ReceiverInfo (シーク用、4および10)
            • Intent
              • ComponentName (セグメントA、5)
                • オプションのパディング用ファイル記述子
                • ParceledListSlice (11)
                • ParcelableParcel または QueueItem (12)
              • Bundle (キャッチ用、セグメントB、6)
                • ReceiverInfo (再スロー用、7)
                  • Bundle (8)

上記のリストの「セグメントA」および「B」の注釈は、私のFdLeaker.javaクラスの「START A」/「END A」/「START B」/「END B」コメントの間のブロックを指し、番号は以下のリストのポイントを指します。

上記のツリーは送信者の観点からの階層を説明していますが、受信者の観点からは少し異なります。

  1. ParcelableListBinderからデータを受信しており、最も外側のオブジェクトはRemoteViewsです。
  2. そのRemoteViewsにはネストされたBundleがあります。RemoteViewsはParcel.ReadWriteHelperを設定するため、このBundleとその中のすべてのBundleは熱心に読み取られます。これは、そうしないとReceiverInfo.readFromParcel()から任意のreadParcelableを実行できないため必要です。
  3. Parcelable[]は、ここではRemoteViews内に配置するすべてのParcelableをグループ化するための便利なラッパーです。

では、どのようなファイル記述子を取得できるのでしょうか?

上記の攻撃を高レベルで見てみましょう。

  1. system_server内でファイル記述子が開かれ、それへの参照が記憶され、FDが閉じられます。
  2. 私はsystem_serverに別のファイル記述子を開かせるようにトリガーします。
  3. ステップ1で作成したダングリングファイル記述子が私に送り返されます。

この攻撃には重大な制限があります。攻撃が開始される前に開かれたファイル記述子を取得することはできません。

それでも、まだいくつかの有用なことができる場合があります。

InputChannelを取得する

正直に言うと、これは追加の前提なしで動作させることができた唯一のエクスプロイトのバリエーションです。

入力イベント、つまりタッチスクリーンやキーボードからのイベントは、アプリがsystem_serverからのUNIXソケットを介して受信します。Activityが起動するか、システムに新しいウィンドウを追加すると、新しいInputChannelが作成されます。これは、UNIXソケットペアが作成され、一方の端がアプリに送信され、もう一方がsystem_serverによってイベント送信に使用されることを意味します。

これらのソケットを介して送信される構造のレイアウトは明確に定義されており(32ビットプロセスと64ビットプロセスの間で互換性がある必要があるため)、予期しないシーケンス番号があっても問題は発生しないようです。また、InputChannelはstartActivity()呼び出し後にsystem_server内で割り当てられる唯一のソケットのように見えるため、どのFDがInputChannelのサーバー側ソケットであるかを簡単に判断できます。

「デバイス名」ダイアログが表示された「端末情報」設定画面のスクリーンショット。そこに「キーインジェクションデモ」と入力されている

これはおもちゃの例ですが、権限プロンプトやアプリケーションのインストールを承認したり、メディアプロジェクションやアクセシビリティサービスを有効にしたりすることもできます。

起動中にzygoteへの接続を取得する

これは主に理論的なもので、低速で動作するエミュレーターでこの攻撃を実行できましたが、実際のデバイスでは競合ウィンドウが小さすぎました。

system_serverは起動時に一度だけ/dev/socket/zygoteへの接続を開き、その後はすべてのリクエストがその接続を使用して送信されます。

system_serverの起動中、MediaSessionService(これはsystem_serverとの間でParcelableを送受信するために使用されます)は、zygoteへの接続が確立される前にservicemanagerに公開されます。

そのため、理論的には、アプリがセカンダリプロセスを起動し、system_serverをクラッシュさせ、その後バックグラウンドプロセスからsystem_serverの起動中に攻撃を実行することが可能です。

SensorServiceを通じてzygoteへの接続を取得する

もう一つ別のバグを見つけました。ここにSensorService::createSensorDirectConnection()メソッドがあります。```cpp sp SensorService::createSensorDirectConnection( const String16& opPackageName, int deviceId, uint32_t size, int32_t type, int32_t format, const native_handle *resource) { // SNIP: Reject direct connections when sensor privacy is enabled // SNIP: Irrelevant parameter checks

root@kitploit:~
// check specific to memory type
switch(type) {
    case SENSOR_DIRECT_MEM_TYPE_ASHMEM: { // channel backed by ashmem
        if (resource->numFds < 1) {
            ALOGE("Ashmem direct channel requires a memory region to be supplied");
            android_errorWriteLog(0x534e4554, "70986337");  // SafetyNet
            return nullptr;
        }
        // SNIP: Further validation for SENSOR_DIRECT_MEM_TYPE_ASHMEM
    }
    case SENSOR_DIRECT_MEM_TYPE_GRALLOC:
        // no specific checks for gralloc
        break;
    default:
        ALOGE("Unknown direct connection memory type %d", type);
        return nullptr;
}

native_handle_t *clone = native_handle_clone(resource);
if (!clone) {
    return nullptr;
}
native_handle_set_fdsan_tag(clone);

sp<SensorDirectConnection> conn;
int channelHandle = 0;
if (deviceId == RuntimeSensor::DEFAULT_DEVICE_ID) {
    // SNIP: usual case where sensor belong to this device (not app streaming)
} else {
    auto runtimeSensorCallback = mRuntimeSensorCallbacks.find(deviceId);
    if (runtimeSensorCallback == mRuntimeSensorCallbacks.end()) {
        ALOGE("Runtime sensor callback for deviceId %d not found", deviceId);
    } else {
        int fd = dup(clone->data[0]);
        channelHandle = runtimeSensorCallback->second->onDirectChannelCreated(fd);
    }
}
// SNIP: Return connection

}

root@kitploit:~
`dup(clone->data[0])` 呼び出しがあります。`clone` はリモートプロセスから受信した `native_handle_t` です。ネイティブハンドルは、データ内に特定の数のFDと特定の数のプレーン整数を含みます。その数は、`data` にアクセスする前に、[`numFds` と `numInts` を確認](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e) してユーザーがチェックする必要があります。

ここには「メモリタイプ固有のチェック」セクションもあります。これは、`SENSOR_DIRECT_MEM_TYPE_ASHMEM` の場合にここでチェックされることを確認し、`SENSOR_DIRECT_MEM_TYPE_GRALLOC` の場合、ネイティブハンドルの形式はデバイス固有であり、ここでは検証できないことを示しています。問題は、ランタイムセンサーのタイプは常に `SENSOR_DIRECT_MEM_TYPE_ASHMEM` として扱われることですが、その場合、`SENSOR_DIRECT_MEM_TYPE_GRALLOC` を指定して検証をバイパスできることです。

ただし、このコードに到達できるのは、「ランタイムセンサー」が存在する場合のみです。それが実際にどのような場合に発生するのか確信はありませんが、ユーザーが「Nearby app streaming」を使用する場合だと思います(?)

ただし、テストのために、[`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary) の登録を可能にする小さなクラスを追加しています。次のように使用できます```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'

その後、プロダクションデバイスでsystem uidとしてコードを実行することが可能です。

ファイル記述子のリストを含む長いテキストを表示するアプリ。その末尾には、"device=2 fd=181"、"Found zygote, sending request"、"uid=1000(system) gid=1000(system), groups=1000(system),1065(reserved_disk),3009(readproc) context=u:r:system_app:s0"

ツールをダウンロード
  • PackageParser$Activity (9)
    • PooledStringWriter
  • 最も外側のReceiverInfoには、そのデータの途中に該当する定義済みの長さがありますが、読み取り中はまだ不明であり、Intentはそこから通常通り読み取られます。
  • 後で読み取る必要があるものは、静的ComponentName.readFromParcel()によるreadString呼び出しを通じて読み取られます(ComponentNameを使用するのは、Androidバージョンに関係なくUTF-16のreadStringを使用するためです。このreadStringは文字列がBinderオブジェクトと重なるためnullを返します。そのため、ComponentNameの構築はこのパスから隠されたデータをスキップしましたが、ComponentNameオブジェクトは作成されません)。
  • 2番目のネストされたBundleに入ります。このBundleのデシリアライズの最後にBadParcelableExceptionがキャッチされます。
  • 次に2番目のReceiverInfoに入ります。このReceiverInfoの長さはInteger.MAX_VALUEとして指定されているため、finallyブロックでBadParcelableExceptionをスローし、ClassCastExceptionを静かに破棄します。
  • 3番目のBundle。これは、ReceiverInfoから任意のreadParcelableに到達できるようにするためだけのものです。現在はRemoteViews内にいるため、Bundleは熱心に読み取られます。
  • PackageParser$Activity + PooledStringWriterの組み合わせにより、読み取り元のParcel上でwriteInt(0)呼び出しがトリガーされます。Parcelの末尾にいたため、writeInt()はParcelの容量を拡張する必要があり、「take possession」パスがトリガーされます。この組み合わせはClassCastExceptionも引き起こしますが、上記のステップ7および6で説明したように飲み込まれます。
  • 外側のReceiverInfoの末尾に到達します。ReceiverInfoはそのヘッダーの長さに従って位置をシークし、以前にComponentName内にあったデータの内側に該当します。これらはParcelable[]内の次のアイテムとして読み取られます。
  • ParceledListSliceがあり、これはブロッキングBinderトランザクションを私のプロセスに対して実行します。この時点で、このParcelで定義されたファイル記述子は閉じられていますが、まだ何も興味深いものがその場所を占めていません。このデシリアライズがこの呼び出しからの戻りを待っている間に、システムにいくつかの興味深いファイル記述子を開かせ、それが私に送信されます。
  • ここで、ファイル記述子がこのParcelから取得され、私に送信されます。ParcelableListBinderがアイテムをフィルタリングするかどうかで異なります。 a. ParcelableListBinderがアイテムをフィルタリングしない場合、FDはParcelableParcel内に保存されます。これはBundleと同様にParcel.appendFrom()を使用してParcelデータをそのままコピーしますが、特別なhasReadWriteHelper()ロジックを持たないため、RemoteViewsのBundleの下にあるにもかかわらずそれを実行します。 b. ParcelableListBinderがアイテムをフィルタリングする場合、これはRemoteViewsの終わりです。RemoteViewsオブジェクトはParcelableListBinderによって破棄されますが、副作用はすでに発生しているため、私にとっては問題ありません。ParcelableListBinderが次に受信するアイテムはQueueItemです。これにはMediaDescriptionが含まれており、それがさらにBundleを含んでいます。そこに私の漏洩したFDが保持されています。