Android バインダー監視ソリューションについての話

Android アプリケーション開発において、Binder は最も一般的な IPC メカニズムであると言えます。一般に次の 2 つの目的で、Binder の IPC メカニズムを監視することを検討します。

  • Caton の最適化: IPC プロセスの完全なリンクは長く、他のプロセスに依存しており、時間の消費は制御できません。通常、Binder 呼び出し自体は RPC の形式で外部機能を提供するため、IPC プロセスの性質を無視しやすくなります。使用時の IPC 。一般に、同期 Binder 呼び出しでメイン スレッドをブロックすることは、アプリケーション ラグの一般的な原因です。
  • クラッシュの最適化: Binder 呼び出しプロセス中にさまざまな異常な状況が発生する可能性があり、通常は Binder バッファーが枯渇します (従来の TransactionTooLargeException)。Binder のバッファ サイズは約 1M (ここでのバッファは、単一の Binder 呼び出しの制限ではなく、グローバル共有プロセスからの最も古い mmap であることに注意してください)、非同期呼び出し (一方向) の状況は制限されており、バッファの半分のサイズ (約 512K) が使用されます。

特定のシステム サービスのみを監視する状況を考慮すると、ServiceManager と AIDL の設計のおかげで、動的プロキシに基づいて現在のプロセスに対応する Proxy オブジェクトを置き換えるだけで監視を実現できます。プロセス内のグローバルなバインダー監視を実現するには、次のことが必要です。一般的なtransactメソッドを呼び出すBinderをインターセプトする方法を検討します。

Binder.ProxyTransactListener に基づく

Android 10 のシステムでは、Binder.ProxyTransactListener が導入されており、Binder 呼び出しの前後にコールバックがトリガーされることに注意してください (BinderProxy の transactNative メソッド内)。送信記録から判断すると、ProxyTransactListener 導入の目的の 1 つは、メイン スレッドの Binder 呼び出しを監視する SystemUI をサポートすることです。

問題は、ProxyTransactListener と対応する設定インターフェイスが非表示になっており、BinderProxy のクラス属性 sTransactListener が非表示 API リストに含まれていることです。しかし、これまでのところ、隠し API の制限を回避する安定したソリューションが常に存在するため、動的プロキシに基づいて ProxyTransactListener インスタンスを作成し、それを BinderProxy に設定して、インプロセス Java Binder 呼び出しのグローバル監視を実現できます。

Android 11 システムでメタリフレクションが無効になった後の簡単な解決策は、まずネイティブ スレッドを作成し、それをアタッチして JNIEnv を取得することです (このスレッドで通常どおり使用できます)。

VMRuntime.getRuntime().setHiddenApiExemptions(new String[]{"L"}); 

グローバルな非表示 API ホワイトニングを実現します。原則として、システムが JNI に基づいて Java API にアクセスするときに、バックトラッキング Java スタックで呼び出し元が見つからない場合、システムはその呼び出しを信頼して、非表示の API をインターセプトしないということです。詳細なロジックについては、「GetJniAccessContext」を参照してください。したがって、ネイティブ スレッドを作成し、AttachCurrentThread を作成して JNI インターフェイスにアクセスすることで、Java 呼び出し元なしの状況を構築できます (これが、ネイティブ スレッド AttachCurrentThread がアプリケーション クラスにアクセスできず、使用可能な ClassLoader が見つからない理由でもあります)。発信者)。さらに興味深いのは、政府はこの隠れた API バックドアの存在を長い間認識していましたが、変更のリスクが高いため、制限ロジックに含まれていなかったことです。同様の議論については、「不明な情報を信頼しない」を参照してください。非表示の API にアクセスするときの呼び出し元。

このソリューションは実装が簡単で、互換性も優れていますが、主な欠点は次のとおりです。

Android 10 以降のみがサポートされていますが、現時点では Android 10 未満のデバイスの割合は高くありません。
ProxyTransactListener のインターフェイス設計にはデータ パラメーター (Binder によって呼び出される受信データ) がないため、送信されたデータのサイズに関する統計を作成できません。さらに、マルチプロセス アプリケーションの場合、実際には、匿名の Binder オブジェクト転送と AIDL テンプレート コード、および実際の IPC 呼び出しのターゲット ロジックを保護する IPC フレームワークの層をカプセル化するための通信チャネルとして、統合 AIDL オブジェクトが使用される場合があります。データパラメータにカプセル化された統一呼び出し規約に基づいています。この場合、実際に IPC 呼び出しの実際のターゲット ロジックを確認できるのは、データ パラメーターを取得した場合のみです (IPC 呼び出しがスレッド プールに均一に配置されて実行される場合、スタックは意味がありません)。

JNI フック BinderProxy.transactNative

実際、Java Binder 呼び出しは常に BinderProxy の JNI メソッド transactNative に行きます。JNI フックに基づいて transactNative をフックして、インプロセス Java Binder 呼び出しの完全バージョンのグローバル監視を実現し、完全なパラメータも取得できます。 Binder 呼び出しのパラメータとその結果を返します。

ここで JNI フックについて少し説明します。JNI フックは、JNI 境界フックの Java JNI メソッドに対応するネイティブ関数に基づいて実装されています。特定の実装はハックが少なく、安定性が優れています。一般的なネイティブ フック ソリューションとみなすことができます。オンラインでよく使われます。一般に、JNI フックの実装は、元のネイティブ関数の検索とネイティブ関数の置き換えの 2 つのステップに分かれています。

  • ネイティブ関数の置き換えは比較的簡単で、JNI の RegisterNatives インターフェイスを呼び出すことでネイティブ関数のカバレッジを実現できます。(元の JNI メソッドも RegisterNatives を通じて登録されている場合は、JNI フックの RegisterNatives が後で実行されるようにする必要があることに注意してください)
  • 元のネイティブ関数を見つけるのは少し複雑で、ネイティブ関数を見つける前に、登録されている元のネイティブ関数に依存する必要があります。
    • 実現可能な解決策は、JNI メソッドを手動で実装することです。このメソッドは、実際の ArtMethod オブジェクト (つまり、アートにおける Java メソッドの実際の表現) に格納されているネイティブ関数の属性のオフセットを計算するために使用されます。このオフセットを取得した後、Hooked JNI メソッドの ArtMethod オブジェクトに基づいて元のネイティブ関数を取得できます。ArtMethod オブジェクトを取得するにはどうすればよいですか? 実際、Android 11 より前では、jmethodID は ArtMethod ポインターでしたが、Android 11 以降では、jmethodID はデフォルトで間接参照になりますが、引き続き Java Method オブジェクトの artMethod プロパティを通じて ArtMethod ポインターを取得できます。詳細な紹介については、Hook フレームワークに依存する必要のない、一般的で非常にシンプルな Android Java ネイティブ メソッド Hook を参照してください。
    • もう 1 つの実現可能な解決策は、内部関数 GetNativeMethods に基づいて元のネイティブ関数を直接クエリすることです。GetNativeMethods のいくつかの関数は、NativeBridge をサポートするためにアートで使用されており、安定性も保証されています。NativeBridge の詳細な紹介については、Android ART 仮想マシン JNI 呼び出し用の NativeBridge の紹介を参照してください。

JNI フック BinderProxy.transactNative に特有ですが、アプリケーションのビジネス コードの最初の行 (アプリケーションのattachBaseContext) が実行される前に、すでに Java Binder 呼び出しが存在するため、BinderProxy を確実にするために Binder 呼び出しを手動でトリガーする必要はありません。 transactNative ネイティブ関数の登録。さらに、BinderProxy の transactNative も隠し API であることに注意してください。ここでも最初に隠し API の制限をバイパスする必要があります。

Hook BinderProxy.transactNative ソリューションは、プロセス内のグローバル Java Binder 呼び出しを監視するニーズを十分に満たすことができますが、Native Binder 呼び出しを監視することはできません。ここでの Java とネイティブ バインダー呼び出しの違いは、実際のビジネス ロジックの実装場所ではなく、IPC 通信ロジックの実装場所にあることに注意してください。MediaCodec などの一般的なオーディオおよびビデオ インターフェイスの場合、実際の Binder 呼び出しのカプセル化はネイティブ層に実装されており、これらのインターフェイスの呼び出しには Java を使用しており、実際の Binder 呼び出しは BinderProxy.transactNative を通じて監視できません。Nativeを含めたグローバルなBinder呼び出し監視を実現するには、Hookの下位層であるNativeのトランザクション機能を考慮する必要があります。

PLT フック BpBinder::transact

Java 層の Binder インターフェイス設計と同様に、ネイティブ層のクライアントによって開始された Binder 呼び出しは、常に libbinder.so 内の BpBinder のトランザクション関数に送られます。BpBinder の transact 関数はエクスポートされた仮想関数であり、常に基本クラス IBinder ポインターに基づく動的バインディング呼び出しとして使用されることに注意してください (つまり、他の関数は常に BpBinder の仮想関数テーブルに基づいて BpBinder::transact を呼び出します)。 、シンボル BpBinder::transact に直接依存する代わりに、BpBinder の仮想関数テーブルは libbinder.so 内にあります)、libbinder.so による BpBinder::transact の呼び出しを直接 PLT フックできます。

具体的には、BpBinder::transact の関数宣言を見てください。

    // NOLINTNEXTLINE(google-default-arguments)
    virtual status_t    transact(   uint32_t code,
                                    const Parcel& data,
                                    Parcel* reply,
                                    uint32_t flags = 0) final;

その中で、status_t は実際には int32_t のエイリアスにすぎませんが、Parcel は NDK によって公開されるインターフェイスではありません。Parcel オブジェクトの完全に安定したレイアウトを取得する方法はありません。幸いなことに、Parcel のトランザクション関数の使用は参照に基づいており、ポインター (アセンブリー層での参照の実装はポインターに似ています) を使用すると、Parcel オブジェクトのレイアウトに依存せずにトランザクション置換関数を実装できます。

BpBinder::transact の呼び出しを正常にインターセプトした後、呼び出しパラメータと transact の戻り値に基づいて必要な情報を取得する方法も考慮する必要があります。

Binder オブジェクト (つまり、トランザクションの暗黙的な呼び出しパラメーター、このポインター) 自体については、通常、その記述子 (実際の IPC 呼び出しのターゲット ロジックを特定するコード パラメーターと組み合わせたもの) に注意を払います。ここでは、エクスポートされたオブジェクトを直接呼び出します。インターフェイス BpBinder::getInterfaceDescriptor は Can です。

    virtual const String16&    getInterfaceDescriptor() const;

さらに厄介なのは、String16 が NDK によって公開されるインターフェイスではなく、char16_t* 文字を変換するために使用される関数実装がインラインであることです。

    inline  const char16_t*     string() const;

ハードコード用に同様の String16 クラスを再宣言することしかできません。幸いなことに、システムのソース コードから判断すると、String16 のオブジェクト レイアウトは比較的単純で安定しており、const char16_t* 型のプライベート属性 mString が 1 つだけあり、仮想関数はありません。このようなもの:

class String16 {
public:
    [[nodiscard]] inline const char16_t *string() const;
private:
    const char16_t* mString;
};

inline const char16_t* String16::string() const {
    return mString;
}

String16 に対応する char16_t* 文字列を取得した後、Java にコールバックするときに JNI インターフェイスを直接使用して、それを jstring に変換できます。

もう 1 つの一般的に使用される情報は、データのデータ サイズです。エクスポートされたインターフェイス Parcel::dataSize を直接呼び出して取得できます。transact 関数の data パラメーターは Parcel 参照であることに注意してください。data パラメーターを受け入れるために空のクラスを直接宣言し、取得したデータのアドレスを取得します。これにより、コンパイラーは通常、参照からポインターへの変換を完了できます。このようなもの:

class Parcel {};

// size_t dataSize() const;
typedef size_t(*ParcelDataSize)(const Parcel *);

// virtual status_t transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) = 0;
status_t HijackedTransact(void *thiz, uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags);

ParcelDataSize g_parcel_data_size = nullptr;
auto data_size = g_parcel_data_size(&data);

さらに、Java バインダー呼び出しの場合、BpBinder::transact が BinderProxy.transactNative 内で呼び出されることに注意してください。JNI フックと PLT フックを組み合わせて、JNI フックに基づいて Java バインダーを呼び出すことができます。完全な Java パラメーターは、さらに実行するのに便利です。 Java コールバックの Java パラメータに基づいて直接処理します。

もう一つ

Binder 呼び出しのインターセプトは監視の最初のステップにすぎません。さらに重要なのは、問題を発見して特定するためにこれに基づいてデータ処理を行う方法です。

前述の 2 種類の古典的な問題、つまり IPC に時間がかかるフリーズと過剰な送信データのクラッシュは、通話の前後で時間のかかるトランザクションをカウントし、通話前に送信データのサイズを取得することで発見できます。位置決めの問題では、現在の Binder 呼び出しのスタック、記述子、およびコードの方が貴重な情報です。

まだフレームワークをマスターしておらず、短期間でフレームワークを完全に理解したい場合は、「Android Framework Core Knowledge Points」を参照してください。これには、Init、Zygote、SystemServer、Binder、Handler、AMS が含まれています。 、PMS、ランチャー... ...およびその他のナレッジ ポイントのレコード。

「フレームワークコアナレッジポイント概要マニュアル」https://qr18.cn/AQpN4J

ハンドラー メカニズムの実装原理の一部:
1. マクロ理論分析とメッセージ ソース コード分析
2. MessageQueue ソース コード分析
3. ルーパー ソース コード分析
4. ハンドラー ソース コード分析
5. まとめ

Binder の原則:
1. Binder を学習する前に理解しておく必要がある知識ポイント
2. ServiceManager のバインダーの仕組み
3. システム サービスの登録プロセス
4. ServiceManager の起動プロセス
5. システム サービスの取得プロセス
6. Java Binder の初期化
7. Java システムの登録プロセスバインダーのサービス

受精卵:

  1. Androidシステムの起動処理とZygoteの起動処理
  2. アプリケーションプロセスの起動プロセス

AMS ソースコード分析:

  1. アクティビティのライフサイクル管理
  2. onActivityResult実行処理
  3. AMSのアクティビティスタック管理の詳細説明

詳細な PMS ソース コード:

1. PMSの起動プロセスと実行プロセス
2. APKのインストールとアンインストールのソースコード解析
3. PMSのインテントフィルタマッチング構造

WMS:
1. WMS の誕生
2. WMS の主要メンバーと Window の追加プロセス
3. Window の削除プロセス

『Androidフレームワーク学習マニュアル』:https://qr18.cn/AQpN4J

  1. ブート初期化プロセス
  2. 起動時に Zygote プロセスを開始する
  3. 起動時に SystemServer プロセスを開始する
  4. バインダードライバー
  5. AMSの起動プロセス
  6. PMSの起動プロセス
  7. ランチャーの起動プロセス
  8. Android の 4 つの主要コンポーネント
  9. Androidシステムサービス - 入力イベントの配信処理
  10. Android の基盤となるレンダリング画面更新メカニズムのソース コード分析
  11. Android のソースコード解析の実践

おすすめ

転載: blog.csdn.net/weixin_61845324/article/details/132569785