Apropos Android-Binder-Überwachungslösung

Bei der Entwicklung von Android-Anwendungen kann man sagen, dass Binder der am häufigsten verwendete IPC-Mechanismus ist. Wir erwägen die Überwachung des IPC-Mechanismus von Binder, im Allgemeinen für die folgenden zwei Zwecke:

  • Caton-Optimierung: Die vollständige Verknüpfung des IPC-Prozesses ist lang und hängt von anderen Prozessen ab. Der Zeitverbrauch ist unkontrollierbar. Der Binder-Aufruf selbst stellt normalerweise externe Funktionen in Form von RPC bereit, sodass wir die Natur dieser Prozesse leichter ignorieren können sein IPC, wenn es verwendet wird. Im Allgemeinen ist das Blockieren des Hauptthreads bei einem synchronen Binder-Aufruf eine typische Ursache für das Einfrieren einer Anwendung.
  • Absturzoptimierung: Während des Binder-Aufrufprozesses können verschiedene abnormale Situationen auftreten. Typischerweise ist der Binder-Puffer erschöpft (klassische TransactionTooLargeException). Die Puffergröße von Binder beträgt etwa 1 MB (beachten Sie, dass der Puffer hier die früheste mmap aus dem globalen gemeinsam genutzten Prozess ist und nicht die Grenze eines einzelnen Binder-Aufrufs), und die Situation asynchroner Aufrufe (einseitig) ist begrenzt und kann nur durchgeführt werden halb so groß wie der Puffer (ca. 512 KB) verwendet werden.

In Anbetracht der Situation, dass nur bestimmte Systemdienste überwacht werden, können wir dank des Designs von ServiceManager und AIDL einfach das Proxy-Objekt ersetzen, das dem aktuellen Prozess entspricht, basierend auf einem dynamischen Proxy, um die Überwachung zu realisieren. Um dabei eine globale Binder-Überwachung zu realisieren, benötigen wir Überlegen Sie, wie Sie Binder abfangen können, der die allgemeine Transaktionsmethode aufruft.

Basierend auf Binder.ProxyTransactListener

Beachten Sie, dass das System unter Android 10 Binder.ProxyTransactListener einführt, der Rückrufe vor und nach Binder-Aufrufen auslöst (innerhalb der transactNative-Methode von BinderProxy). Dem Übermittlungsdatensatz nach zu urteilen, besteht einer der Zwecke der Einführung von ProxyTransactListener darin, SystemUI bei der Überwachung des Binder-Aufrufs des Hauptthreads zu unterstützen.

Der Haken an der Sache ist, dass ProxyTransactListener und die entsprechende Einstellungsschnittstelle ausgeblendet sind und sich das Klassenattribut sTransactListener von BinderProxy in der ausgeblendeten API-Liste befindet. Aber bisher hatten wir immer eine stabile Lösung, um die Einschränkung der versteckten API zu umgehen, sodass wir eine ProxyTransactListener-Instanz basierend auf einem dynamischen Proxy erstellen und diese auf BinderProxy setzen können, um eine globale Überwachung prozessinterner Java-Binder-Aufrufe zu realisieren.

Hier ist eine kleine Erwähnung der versteckten API-Bypass-Lösung. Nachdem das Android 11-System die Metareflexion deaktiviert hat, besteht eine einfache Lösung darin, zuerst einen nativen Thread zu erstellen und ihn dann anzuhängen, um JNIEnv zu erhalten, das in diesem Thread normal verwendet werden kann.

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

Um eine globale versteckte API-Aufhellung zu erreichen. Das Prinzip besteht darin, dass das System beim Zugriff auf die Java-API basierend auf JNI, wenn der Aufrufer nicht im Backtracking-Java-Stack gefunden werden kann, darauf vertraut, dass der Aufruf die versteckte API nicht abfängt. Eine detaillierte Logik finden Sie unter GetJniAccessContext. Daher können wir eine Situation ohne einen Java-Aufrufer konstruieren, indem wir einen nativen Thread erstellen und dann AttachCurrentThread verwenden, um auf die JNI-Schnittstelle zuzugreifen (dies ist auch der Grund, warum der native Thread AttachCurrentThread nicht auf die Anwendungsklasse zugreifen kann und ohne den kein verfügbarer ClassLoader gefunden werden kann Anrufer). Interessanter ist, dass der Regierung die Existenz dieser versteckten API-Hintertür schon seit langem bekannt ist, sie jedoch aufgrund des hohen Änderungsrisikos nicht in die Einschränkungslogik aufgenommen wurde. Ähnliche Diskussionen finden Sie unter Unbekanntem nicht vertrauen Aufrufer beim Zugriff auf versteckte API.

Diese Lösung ist einfach zu implementieren und weist eine gute Kompatibilität auf. Die Hauptnachteile sind:

Es werden nur Android 10 und höher unterstützt, der aktuelle Anteil an Geräten unter Android 10 ist jedoch nicht hoch.
Das Schnittstellendesign von ProxyTransactListener verfügt nicht über einen Datenparameter (von Binder aufgerufene eingehende Daten), daher können wir keine Statistiken über die Größe der übertragenen Daten erstellen. Darüber hinaus kann für Multiprozessanwendungen in der Praxis ein einheitliches AIDL-Objekt als Kommunikationskanal verwendet werden, um eine Schicht des IPC-Frameworks zu kapseln, das die anonyme Übertragung von Binder-Objekten und AIDL-Vorlagencode sowie die Ziellogik des tatsächlichen IPC-Aufrufs abschirmt basiert auf einer einheitlichen Aufrufkonvention, die im Datenparameter gekapselt ist. In diesem Fall können wir die tatsächliche Ziellogik des IPC-Aufrufs tatsächlich nur bestätigen, wenn wir den Datenparameter erhalten (der Stapel ist bedeutungslos, wenn IPC-Aufrufe einheitlich zur Ausführung im Thread-Pool platziert werden).

JNI Hook BinderProxy.transactNative

Tatsächlich geht der Java Binder-Aufruf immer an die JNI-Methode transactNative von BinderProxy. Wir können transactNative basierend auf JNI Hook einbinden, um die globale Überwachung der Vollversion des prozessinternen Java Binder-Aufrufs zu realisieren und auch die vollständigen Parameter abzurufen und Parameter des Binder-Aufrufs. Das Ergebnis zurückgeben.

Hier ist eine kleine Erwähnung von JNI Hook. JNI Hook wird basierend auf der Native-Funktion implementiert, die der Java-JNI-Methode des JNI-Boundary-Hooks entspricht. Die spezifische Implementierung weist weniger Hacks und eine bessere Stabilität auf. Sie kann als gängige Native Hook-Lösung angesehen werden Wird häufig online verwendet. Im Allgemeinen ist die Implementierung von JNI Hook in zwei Schritte unterteilt: Suchen der ursprünglichen nativen Funktion und Ersetzen der nativen Funktion.

  • Das Ersetzen nativer Funktionen ist relativ einfach. Wir können die Abdeckung nativer Funktionen realisieren, indem wir die RegisterNatives-Schnittstelle von JNI aufrufen. (Beachten Sie, dass wir sicherstellen müssen, dass die RegisterNatives des JNI-Hooks später ausgeführt werden, wenn die ursprüngliche JNI-Methode auch über RegisterNatives registriert wird.)
  • Das Finden der ursprünglichen Native-Funktion ist etwas komplizierter und wir müssen uns immer darauf verlassen, dass die ursprüngliche Native-Funktion registriert ist, bevor wir die Native-Funktion finden können.
    • Eine praktikable Lösung besteht darin, eine JNI-Methode manuell zu implementieren, mit der der Offset der Attribute der Native-Funktion berechnet wird, die im tatsächlichen ArtMethod-Objekt (d. h. der tatsächlichen Darstellung der Java-Methode in art) gespeichert sind. Nach Erhalt dieses Offsets kann die ursprüngliche native Funktion basierend auf dem ArtMethod-Objekt der Hooked-JNI-Methode abgerufen werden. Wie erhalte ich das ArtMethod-Objekt? Tatsächlich war jmethodID vor Android 11 der ArtMethod-Zeiger. Nach Android 11 wird jmethodID standardmäßig zu einer indirekten Referenz, aber wir können den ArtMethod-Zeiger weiterhin über die artMethod-Eigenschaft des Java-Methodenobjekts abrufen. Eine detaillierte Einführung finden Sie in einem allgemeinen und supereinfachen Android Java Native-Methoden-Hook, der nicht auf das Hook-Framework angewiesen ist.
    • Eine andere mögliche Lösung besteht darin, die ursprünglichen nativen Funktionen basierend auf der internen Funktion GetNativeMethods von art direkt abzufragen. Mehrere Funktionen von GetNativeMethods werden von art zur Unterstützung von NativeBridge genutzt und auch die Stabilität ist gewährleistet. Eine ausführliche Einführung von NativeBridge finden Sie in der Einführung zum JNI-Aufruf der virtuellen Maschine von NativeBridge für Android ART.

Speziell für den JNI-Hook BinderProxy.transactNative gibt es vor der Ausführung des ersten Geschäftscodes der Anwendung (attachBaseContext der Anwendung) bereits einen Java-Binder-Aufruf, sodass wir den Binder-Aufruf nicht manuell auslösen müssen, um sicherzustellen, dass BinderProxy. transactNative Native Funktionsregistrierung. Beachten Sie außerdem, dass das transactNative von BinderProxy ebenfalls eine versteckte API ist. Hier müssen auch die Einschränkungen der versteckten API zuerst umgangen werden.

Die Hook-BinderProxy.transactNative-Lösung kann die Anforderungen der Überwachung der globalen Java-Binder-Aufrufe im Prozess gut erfüllen, kann jedoch die nativen Binder-Aufrufe nicht überwachen. Beachten Sie, dass der Unterschied zwischen den Java/Native Binder-Aufrufen hier im Implementierungsort der IPC-Kommunikationslogik liegt und nicht im Implementierungsort der tatsächlichen Geschäftslogik. Bei typischen Audio- und Videoschnittstellen wie MediaCodec wird die eigentliche Binder-Aufrufkapselung in der nativen Schicht implementiert. Wir verwenden Java, um diese Schnittstellen aufzurufen, und der eigentliche Binder-Aufruf kann nicht über BinderProxy.transactNative überwacht werden. Um die globale Binder-Aufrufüberwachung einschließlich Native zu realisieren, müssen wir die Transaktionsfunktion von Native berücksichtigen, die eine untere Ebene von Hook ist.

PLT-Hook BpBinder::transact

Ähnlich wie beim Binder-Schnittstellendesign der Java-Schicht wird der vom Client der nativen Schicht initiierte Binder-Aufruf immer an die Transaktionsfunktion von BpBinder in libbinder.so weitergeleitet. Beachten Sie, dass die Transact-Funktion von BpBinder eine exportierte virtuelle Funktion ist und immer als dynamischer Bindungsaufruf basierend auf dem IBinder-Zeiger der Basisklasse verwendet wird (d. h. andere rufen BpBinder::transact immer basierend auf der virtuellen Funktionstabelle von BpBinder auf). Anstatt sich direkt auf das Symbol BpBinder::transact zu verlassen und die virtuelle Funktionstabelle von BpBinder in libbinder.so enthalten ist, können wir den Aufruf von BpBinder::transact durch libbinder.so direkt per PLT einbinden.

Schauen Sie sich insbesondere die Funktionsdeklaration von BpBinder::transact an:

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

Unter diesen ist status_t eigentlich nur ein Alias ​​für int32_t, aber Parcel ist keine von NDK bereitgestellte Schnittstelle. Wir haben keine Möglichkeit, ein absolut stabiles Layout von Parcel-Objekten zu erhalten. Glücklicherweise basiert die Verwendung von Transact-Funktionen für Parcel auf Referenzen und Zeiger (Referenzen sind in der Assembly-Layer-Implementierung ähnlich wie Zeiger) können wir eine Transaktionsersetzungsfunktion implementieren, ohne auf das Layout des Parcel-Objekts angewiesen zu sein.

Nachdem wir den Aufruf von BpBinder::transact erfolgreich abgefangen haben, müssen wir auch darüber nachdenken, wie wir die benötigten Informationen basierend auf den Aufrufparametern und dem Rückgabewert von transact erhalten.

Für das Binder-Objekt (dh den impliziten Aufrufparameter dieses Zeigers von transact) selbst achten wir normalerweise auf seinen Deskriptor (in Kombination mit dem Codeparameter, um die Ziellogik des tatsächlichen IPC-Aufrufs zu lokalisieren). Hier rufen wir den Export direkt auf Schnittstelle BpBinder::getInterfaceDescriptor, die Can ist.

    virtual const String16&    getInterfaceDescriptor() const;

Noch problematischer ist, dass String16 keine von NDK bereitgestellte Schnittstelle ist und die Funktionsimplementierung, die zum Konvertieren von char16_t*-Zeichen verwendet wird, inline ist.

    inline  const char16_t*     string() const;

Wir können eine ähnliche String16-Klasse nur für Hardcode neu deklarieren. Glücklicherweise ist das Objektlayout von String16, gemessen am Quellcode des Systems, relativ einfach und stabil. Es gibt nur ein privates Attribut mString vom Typ const char16_t* und keine virtuelle Funktion. Etwas wie das:

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

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

Nachdem wir die Zeichenfolge char16_t* erhalten haben, die String16 entspricht, können wir sie direkt über die JNI-Schnittstelle in jstring konvertieren, wenn wir Java zurückrufen.

Eine weitere häufig verwendete Information ist die Datengröße. Wir können die exportierte Schnittstelle Parcel::dataSize direkt aufrufen, um sie abzurufen. Beachten Sie, dass der Datenparameter der Transact-Funktion eine Paketreferenz ist. Wir deklarieren direkt eine leere Klasse, um den Datenparameter zu akzeptieren, und übernehmen dann die Adresse der erhaltenen Daten, damit der Compiler normalerweise die Konvertierung von der Referenz in den Zeiger abschließen kann. Etwas wie das:

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);

Beachten Sie außerdem, dass bei Java-Binder-Aufrufen BpBinder::transact innerhalb von BinderProxy.transactNative aufgerufen wird. Wir können JNI-Hook und PLT-Hook kombinieren, um Java-Binder basierend auf JNI-Hook aufzurufen. Der vollständige Java-Parameter ist für uns praktisch, um weitere Aufgaben zu erledigen Verarbeitung direkt basierend auf dem Java-Parameter im Java-Callback.

Eine Sache noch

Das Abfangen von Binder-Aufrufen ist nur der erste Schritt bei der Überwachung und, was noch wichtiger ist, wie man auf dieser Basis Daten verarbeitet, um Probleme zu erkennen und zu lokalisieren.

Die beiden oben genannten klassischen Probleme: zeitraubendes Einfrieren des IPC und übermäßige Übertragungsdatenabstürze können erkannt werden, indem die zeitaufwändigen Transaktionen vor und nach dem Anruf gezählt werden und die Größe der übertragenen Daten vor dem Anruf ermittelt wird. In Bezug auf die Positionierung sind der Stapel sowie der Deskriptor und Code des aktuellen Binder-Aufrufs wertvollere Informationen.

Wenn Sie das Framework noch nicht beherrschen und in kürzester Zeit ein umfassendes Verständnis davon erlangen möchten, können Sie auf „Android Framework Core Knowledge Points“ verweisen , das Folgendes umfasst: Init, Zygote, SystemServer, Binder, Handler, AMS , PMS, Launcher... ... und andere Wissenspunktdatensätze.

„Framework Core Knowledge Point Zusammenfassungshandbuch“ :https://qr18.cn/AQpN4J

Teil des Implementierungsprinzips des Handler-Mechanismus:
1. Makrotheoretische Analyse und Analyse des Nachrichtenquellcodes
2. Analyse des MessageQueue-Quellcodes
3. Analyse des Looper-Quellcodes
4. Analyse des Handler-Quellcodes
5. Zusammenfassung

Binder-Prinzip:
1. Wissenspunkte, die vor dem Erlernen von Binder verstanden werden müssen
2. Binder-Mechanismus in ServiceManager
3. Systemdienst-Registrierungsprozess
4. ServiceManager-Startprozess
5. Systemdienst-Erfassungsprozess
6. Java-Binder-Initialisierung
7. Java Der Registrierungsprozess des Systems Dienstleistungen in Binder

Zygote:

  1. Der Startvorgang des Android-Systems und der Startvorgang von Zygote
  2. Der Startvorgang des Bewerbungsprozesses

AMS-Quellcode-Analyse:

  1. Management des Aktivitätslebenszyklus
  2. onActivityResult-Ausführungsprozess
  3. Detaillierte Erläuterung des Aktivitätsstapelmanagements in AMS

Ausführlicher PMS-Quellcode:

1. PMS-Startprozess und Ausführungsprozess
2. APK-Installations- und Deinstallations-Quellcode-Analyse
3. Intent-Filter-Matching-Struktur in PMS

WMS:
1. Die Geburt von WMS
2. Die wichtigen Mitglieder von WMS und der Hinzufügungsprozess von Window
3. Der Löschprozess von Window

„Android Framework-Studienhandbuch“:https://qr18.cn/AQpN4J

  1. Boot-Init-Prozess
  2. Starten Sie den Zygote-Prozess beim Booten
  3. Starten Sie den SystemServer-Prozess beim Booten
  4. Binder-Treiber
  5. AMS-Startvorgang
  6. Der Startvorgang des PMS
  7. Startvorgang des Launchers
  8. Die vier Hauptkomponenten von Android
  9. Android-Systemdienst – Verteilungsprozess des Eingabeereignisses
  10. Analyse des Quellcodes des zugrunde liegenden Android-Rendering-Bildschirmaktualisierungsmechanismus
  11. Analyse des Android-Quellcodes in der Praxis

Supongo que te gusta

Origin blog.csdn.net/weixin_61845324/article/details/132569785
Recomendado
Clasificación