Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
ThisSeemsWrong — Writeup und Exploit für CVE-2024-49746: Androids Parcel::continueWrite schließt Dateideskriptoren, die später verwendet werden | Kitploit
Tools/GitHubGitHub/michalbednarski/thisseemswrong
Android-SicherheitExploit-FrameworksSchwachstellenanalyseInformationsbeschaffungPayload-EntwicklungBinary-Exploitation
GitHubmichalbednarski/thisseemswrong

ThisSeemsWrong

Writeup und Exploit für CVE-2024-49746: Androids Parcel::continueWrite schließt Dateideskriptoren, die später verwendet werden

Repository anzeigen
4715vor 10 MonatenVon Kitploit geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

Der Fix für dieses Problem erschien als CVE-2024-49746: Bulletin, Patch

„Das scheint falsch zu sein“

Der obige Titel ist der Kommentar aus der Methode Parcel::continueWrite, die tatsächlich für die Größenänderung von Parcel-Objekten verantwortlich ist – entweder wenn sie explizit vom Benutzer angefordert wird (zum Beispiel über setDataSize()) oder wenn eine der write-Methoden aufgerufen wird, während die aktuelle Datenkapazität zu klein ist```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:~
[Als dieser Kommentar eingeführt wurde](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/), wurde der Aufruf von `closeFileDescriptors()` aus der Methode `IPCThreadState::freeBuffer()` (die im obigen Code über den Funktionszeiger `mOwner()` aufgerufen wird) in die Methode `continueWrite()` verschoben, die Logik blieb jedoch dieselbe wie zuvor. Schließlich ist `Parcel` ein zentraler Bestandteil des Android-IPC, und wenn das Kern-IPC Dateideskriptoren schließen würde, die es nicht schließen sollte, wäre das ein offensichtliches Problem.

Was uns zum wichtigen Punkt bringt: Wann wird der obige Code verwendet? Er wird verwendet, wenn die Klasse `Parcel` den Besitz an Daten übernimmt, die vom Binder-Treiber empfangen wurden (die sich zu diesem Zeitpunkt im [`/dev/binder`-`mmap`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567) befinden und nicht beschrieben werden können (jeder Versuch, diesen Speicher zu beschreiben, würde zu `SIGSEGV` führen)), d. h. wenn `Parcel` entweder eingehende Transaktionsdaten ist (`data`-Argument, das an [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) übergeben wird) oder eine eingehende Antwort (d. h. das `Parcel`-Objekt, das an den [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int))-Aufruf als `reply`-Argument übergeben wurde; `transact()` setzt eine Referenz innerhalb dieses `Parcel`-Objekts).

In der Praxis ist der einzige Fall, in dem wir den Block `if (mOwner)` betreten, wenn das System [`setDataSize(0)` aufruft, um Transaktionsdaten freizugeben](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567), aber in diesem Fall würden wir auch `if (desired == 0)` betreten, was eine vorzeitige Rückkehr auslöst. Bei legitimer Systemnutzung gibt es allerdings keinen Fall, in dem wir den Pfad „Wenn es einen anderen Besitzer gibt, müssen wir Besitz ergreifen" betreten.

# Den Pfad „Besitz ergreifen" auslösen

In einem meiner früheren Exploits habe ich [einen Fall gezeigt, in dem `createFromParcel()` tatsächlich `writeInt(0)` auf dem `Parcel` aufrufen kann, aus dem es lesen sollte](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects). Während der Fix dort die Ausführung aller Nicht-`Intent`-`createFromParcel()`-Methoden innerhalb des `AccountManagerService` verhinderte, blieb der Pfad von `createFromParcel()` zu `writeInt(0)` intakt.

Zur Erinnerung: [im `PackageParser` haben wir den folgenden Code](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));
}

Daher können wir ein Parcel-Objekt, das an createFromParcel übergeben wurde, an jeden im System verfügbaren public-Konstruktor übergeben, der ein einzelnes Parcel-Argument akzeptiert.

Und anderswo haben wir folgenden Code:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }

root@kitploit:~
Daher müssen wir, um den „take possession“-Pfad auszulösen, über einen beliebigen `readParcelable`-Aufruf auf dem `Parcel` verfügen, das als `data` an `onTransact()` übergeben wurde. In diesem Exploit verwende ich dafür [denselben Pfad, den ich zuvor in einem anderen verwendet habe](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them). Ich veranlasse, dass `readParcelable` `PackageParser$Activity.CREATOR.createFromParcel()` aufruft, das wiederum den Namen von `PooledStringWriter` liest und dessen Konstruktor aufruft; danach enden die `Parcel`-Daten, sodass `writeInt()` das Parcel neu allozieren muss und damit in unseren „take possession“-Pfad eintritt.

Erwähnenswert ist, dass `writeInt()` versuchen würde, die Daten direkt zu überschreiben, wenn es an dieser Stelle kein Ende der `Parcel`-Daten gäbe, was bei Daten, die durch `/dev/binder`-`mmap` gestützt werden, zu `SIGSEGV` führen würde.

# File Descriptor Sanitizer

Meine ursprüngliche Idee war, dass der „take possession“-Pfad Dateideskriptoren schließt, wonach am Ende der Transaktion dieselben Deskriptoren erneut geschlossen würden; zwischen diesen Vorgängen würde ich jedoch in einer anderen Transaktion einen anderen Dateideskriptor innerhalb von `system_server` platzieren und später meinen Dateideskriptor zurückerhalten, da dieser FD zu diesem Zeitpunkt auf eine andere Datei verweist.

Das funktionierte auf meinem Emulator mit einer alten AOSP-Version. Sobald ich es jedoch mit einer neueren Version versuchte, wurde dieser Plan durch den [File Descriptor Sanitizer (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md) gestoppt.

Insbesondere wurde [in `android-14.0.0_r29` die FDSan-Abdeckung erweitert, um FDs in `Parcel` abzudecken](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/).

Tatsächlich konnte ich, nachdem FDSan Parcel abgedeckt hatte, nicht einmal mehr den `closeFileDescriptors()`-Aufruf erreichen, wenn das `Parcel` FDs enthielt. Vor diesem Aufruf gibt es einen Aufruf von `acquireObjects();`, der wiederum Referenzen auf `Binder`-Handles erwirbt (die später innerhalb dieser Funktion durch den `mOwner()`-Aufruf freigegeben werden). `acquireObjects()` wird jedoch auch [FDSan-Tags für FDs setzen](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);
    }

Die Sache ist die, dass wir bereits vom Kernel empfangene FDs markiert haben.```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:~
Somit führt ein doppeltes `FdTag` (ohne das Tag zu schließen oder zu ändern, während der erwartete alte Tag angegeben wird) zu einem FDSan-Fehler, der den Prozess abbricht, und wir haben noch nicht einmal den `closeFileDescriptors()`-Aufruf erreicht. Da der „take possession“-Pfad bei normaler Nutzung toter Code ist, könnten solche Probleme unbemerkt bleiben.

Allerdings können wir die Bedingung `if (obj.cookie != 0)` sehen. Wenn diese falsch ist, bedeutet das, dass der in der `Parcel` vorhandene FD nicht wirklich von dieser `Parcel` besessen wird und es in der Verantwortung des `Parcel`-Nutzers liegt, sie offen zu halten, solange diese `Parcel` existiert. Da diese `Parcel` jedoch gerade vom Kernel gekommen ist, stammen die `cookie`-Werte tatsächlich vom ursprünglichen Prozess und gelten als irrelevant, wenn die `Parcel` tatsächlich `mOwner` hat. Aber der „take possession“-Pfad berücksichtigt das nicht wirklich und kopiert einfach die `cookie`-Werte.

Wenn wir all das zusammennehmen, können wir durch Setzen der `cookie`-Werte auf Null auf der Senderseite eine `Parcel` erhalten, die auf geschlossene Dateideskriptoren verweist, aber nicht davon ausgeht, sie zu besitzen, was bedeutet, dass sie sie nicht erneut schließen wird. Das erlaubt uns, FDSan nicht auszulösen, eliminiert aber auch alle Double-Close-Ausnutzungspfade.

# Tricks auf der Java-Seite von Parcel

Solche FDs könnten zwar noch an eine andere `Parcel` übergeben werden (und dann an einen anderen Prozess), aber unser Weg, die Erstellung solcher hängenden FDs auszulösen, beinhaltet die Konstruktion eines `PooledStringWriter` per Reflection, wonach eine `ClassCastException` geworfen wird, wenn wir versuchen, ihn zu einer `ArrayList<IntentInfo>` hinzuzufügen.

Wir müssten:

* Am Ende der `Parcel` sein, die als `data`-Argument an `onTransact()` übergeben wurde
* Die Konstruktion von `PooledStringWriter` durchführen, wonach die Parcel hängende FDs haben wird, aber auch eine `ClassCastException` geworfen wird
* Warten, bis die FDs, die wir leaken möchten, innerhalb von `system_server` alloziert werden
* Die FDs aus dieser `Parcel` in eine andere `Parcel` kopieren lassen, die an unseren Prozess gesendet wird

Um alle diese Anforderungen zu erfüllen, werde ich sowohl einige der alten als auch der neuen Tricks einsetzen müssen.

## Alte Tricks

Beginnen wir mit einem erneuten Blick auf die alten Tricks, die meisten davon wurden bereits in meinem [`LazyValue`-using-`Parcel`-after-`recycle()` Exploit](https://github.com/michalbednarski/LeakValue) beschrieben.

1. [Die Klasse `RemoteViews` führt die Deserialisierung des enthaltenen `Bundle` mit gesetztem `Parcel.ReadWriteHelper` durch](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4). [Wenn `ReadWriteHelper` gesetzt ist, werden `Bundle`-s nicht eager, sondern lazy deserialisiert](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545). An dieser Stelle ist auch erwähnenswert, dass alle `Bundle`-s, die in diesem `Bundle` unter `RemoteViews` enthalten sind, ebenfalls eager deserialisiert werden, nicht nur dasjenige, das sich direkt in `RemoteViews` befindet.
2. [Wenn beim Deserialisieren eines `Bundle` innerhalb von `system_server` eine `BadParcelableException` geworfen wird, wird diese `BadParcelableException` stillschweigend abgefangen und der Inhalt des `Bundle` wird geleert](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e). Beachte jedoch, dass unser Trigger, der in `createFromParcel` schreibt, eine `ClassCastException` wirft, die hier nicht abgefangen wird.
3. [Die Klasse `ParceledListSlice` führt während der Deserialisierung einen blockierenden ausgehenden Binder-Aufruf an das in den serialisierten Daten angegebene Objekt aus](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364), den wir verwenden können, um die Deserialisierungsausführung anzuhalten.

All diese werden jetzt benötigt, aber das ist noch nicht alles.

## Neue Tricks

[AIDL ist ein Werkzeug zum Generieren von RPC-Schnittstellenimplementierungen](https://developer.android.com/guide/components/aidl), aber darüber hinaus ist es auch in der Lage, Implementierungen von `Parcelable`-Strukturen zu generieren.

Diese Strukturen sind längenpräfixiert, sodass verschiedene Versionen derselben Struktur innerhalb des Systems kompatibel sind, solange keine Felder in der Mitte hinzugefügt werden (das heißt, Versionen sind kompatibel, wenn eine Version ein Präfix der anderen ist).

Werfen wir einen Blick darauf, welchen Code AIDL für die [Struktur `ReceiverInfo`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ReceiverInfo.aidl) generiert hat:```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);
  }
}

Dieser Code ermöglicht uns die zwei verbleibenden Tricks, die wir beide benötigen, um nach dem Auslösen des „take possession“-Pfads weitere Aktionen auszuführen, der erfordert, dass wir uns am Ende des Parcel befinden, und eine ClassCastException auslöst.

Zuerst lesen wir die Länge aus dem Parcel und verwenden sie, um Felder zu überspringen, die in der geschriebenen Version nicht vorhanden sind. Am Ende passen wir die Position innerhalb des Parcel basierend auf dieser Länge an. Es ist uns nicht erlaubt, uns vor die Position dieses AIDL-Parcelable zu bewegen (und das Bewegen über das Ende des Parcel hinaus ist möglich, tut aber nichts Gefährliches). Wir können uns jedoch in die Mitte eines bereits gelesenen Objekts bewegen, sodass wir während des ersten Lesens das Ende des Parcel erreichen, einen PooledStringWriter konstruieren lassen und dann in die Mitte von etwas zurückspulen können, das sich innerhalb dieses ReceiverInfo-Objekts befindet.

Damit ist das Problem gelöst, am Ende des Parcel zu sein; es bleibt noch das Problem der ClassCastException. Hier haben wir jedoch einen finally-Block, der vor dem Aufruf von setDataPosition() prüft, ob kein Überlauf vorliegt. Wenn ein Überlauf vorliegt, wird eine BadParcelableException geworfen. Was passiert nun, wenn der finally-Block eine Exception wirft, während bereits eine andere ansteht? Eine im finally-Block geworfene Exception hat Vorrang; die vorherige Exception verschwindet stillschweigend. Statt einer ClassCastException haben wir nun eine BadParcelableException, die Bundle freundlicherweise ignoriert, um Exception-s innerhalb des zu vermeiden.

Hinweis zum ParcelableListBinder-Patch

Wir verwenden den ParcelableListBinder von MediaSession, um ein beliebiges Parcelable in den system_server zu legen und dieses Objekt später zurückzuholen.

Vor Kurzem gab es einen ParcelableListBinder-Patch, der genau das verbietet.

Aus Sicht dieses Exploits stoppt mich dieser Patch nicht wirklich, aber ich muss unterschiedlich damit umgehen, je nachdem ob er vorhanden ist oder nicht.

Wenn dieser Patch vorhanden ist, werden Nicht-QueueItem-Elemente stillschweigend aus der vom system_server empfangenen Liste entfernt (sie verursachen keine Exception-s). Da wir für die Seiteneffekte ein Nicht-QueueItem benötigen, können wir ein Nicht-QueueItem als erstes Element setzen und ihm ein echtes QueueItem folgen lassen. Das erlaubt uns zwar keine beliebige Parcelable-Deserialisierung, enthält aber ein Bundle, das File Descriptors enthalten kann (die wir an unseren Prozess übergeben haben möchten).

Wenn dieser Patch nicht vorhanden ist, haben wir dieses Hindernis nicht, können aber auch nicht denselben Ablauf verwenden. Denn im obigen Fall hätten wir eine Liste mit sowohl RemoteViews als auch QueueItem, sodass ParceledListSlice die Übertragung einer gemischten Liste verweigern würde. In diesem Fall muss ich unsere geleakten FDs in die RemoteViews-Instanz legen.

Interessanter an diesem Patch ist jedoch der Grund, warum er eingeführt wurde. Die Commit-Message erwähnt „allowing apps to start from background“, und während der Android Security Bulletin nicht viel sagt, können wir im CVE-Eintrag nützliche Informationen finden, der sich auf das Notification.mAllowlistToken-Feld bezieht, das beim Lesen aus einem statischen Feld übernommen werden kann, das innerhalb des system_server ein Token ist, das Activity-Starts aus dem Hintergrund erlaubt und später in Notification.writeToParcel() geschrieben würde. Heißt das, dass jetzt alle Fälle, in denen system_server beliebige Parcelable deserialisiert und sie an die App zurücksendet, Schwachstellen sind? Wie auch immer, das ist vorerst nur ein Gedanke – in diesem Exploit möchte ich ohnehin mehr erreichen.

Alles zusammensetzen

Ich denke, dieser Exploit enthält die komplexeste Parcelable-Gadget-Kette, die ich je gebaut habe:

  • RemoteViews (1)
    • ReflectionAction (2)
      • Bundle
        • Parcelable[] (3)
          • ReceiverInfo (für Seek, 4 und 10)
            • Intent
              • ComponentName (Segment A, 5)
                • Optionale File Descriptors als Padding
                • ParceledListSlice (11)
                • ParcelableParcel oder QueueItem (12)
              • Bundle (für Catch, Segment B, 6)
                • ReceiverInfo (für rethrow, 7)
                  • Bundle (8)

Die Anmerkungen „segment A“ und „B“ in der obigen Liste beziehen sich auf Blöcke zwischen den Kommentaren "START A"/"END A"/"START B"/"END B" in meiner FdLeaker.java-Klasse; die Zahlen beziehen sich auf die Punkte in der folgenden Liste.

Der obige Baum beschreibt die Hierarchie aus Sicht des Senders. Aus Sicht des Empfängers sieht er jedoch etwas anders aus:

  1. Wir empfangen Daten vom ParcelableListBinder; das äußerste Objekt ist RemoteViews.
  2. In diesem RemoteViews befindet sich ein verschachteltes Bundle. RemoteViews wird Parcel.ReadWriteHelper setzen, sodass dieses Bundle und alle Bundle-s darin sofort gelesen werden. Das ist notwendig, weil wir sonst kein beliebiges readParcelable aus ReceiverInfo.readFromParcel() ausführen könnten.
  3. Parcelable[] ist hier nur ein praktischer Wrapper, um alle Parcelable-s zu gruppieren, die ich in lege.

Also, welche File Descriptors können wir nehmen?

Schauen wir uns den obigen Angriff einmal grob an:

  1. Ein File Descriptor wird innerhalb des system_server geöffnet, eine Referenz darauf wird gemerkt, und der FD wird geschlossen.
  2. Ich veranlasse den system_server, einen anderen File Descriptor zu öffnen.
  3. Der im Schritt 1 erzeugte Dangling File Descriptor wird an mich zurückgesendet.

Dieser Angriff hat eine erhebliche Einschränkung: Wir können keine File Descriptors greifen, die geöffnet wurden, bevor unser Angriff begonnen hat.

Es gibt jedoch noch ein paar nützliche Dinge, die wir tun könnten.

InputChannel abgreifen

Um ehrlich zu sein, das ist die einzige Exploit-Variante, die ich ohne zusätzliche Annahmen zum Laufen gebracht habe.

Eingabeereignisse, also Ereignisse von Touchscreen und Tastatur, empfängt die App über einen UNIX-Socket vom system_server. Wenn eine Activity startet oder dem System ein neues Fenster hinzufügt, wird ein neuer InputChannel erstellt, was wiederum bedeutet, dass ein UNIX-Socketpaar erzeugt wird und eines der Enden an die App gesendet wird, während das andere von system_server zum Senden von Ereignissen verwendet wird.

Das Layout der über diese Sockets gesendeten Strukturen ist wohldefiniert (da sie zwischen 32-Bit- und 64-Bit-Prozessen kompatibel sein müssen), und es scheint, als würde sich niemand über unerwartete Sequenznummern beschweren. Außerdem scheint InputChannel der einzige Socket zu sein, der nach dem startActivity()-Aufruf innerhalb des system_server allokiert wird, sodass leicht bestimmt werden kann, welcher FD der serverseitige Socket des InputChannel ist.

Screenshot des Einstellungsbildschirms „Über das Telefon“ mit dem Dialog „Gerätename“, in den „key injection demo“ eingegeben wird

Obwohl dies ein Spielzeugbeispiel ist, könnten wir damit auch Berechtigungsabfragen oder App-Installationen genehmigen, Media Projection oder den Accessibility-Dienst aktivieren.

Verbindung zu zygote beim Booten abgreifen

Dieses ist größtenteils theoretisch. Ich konnte diesen Angriff auf einem langsam laufenden Emulator durchführen, auf einem echten Gerät war das Race-Fenster jedoch zu klein.

Der system_server öffnet die Verbindung zu /dev/socket/zygote nur einmal beim Start. Danach werden alle Anforderungen über diese Verbindung gesendet.

Während des Bootens des system_server wird MediaSessionService (das zum Senden und Empfangen von Parcelable-s an/von den system_server verwendet wird) beim servicemanager publiziert, bevor die Verbindung zu zygote hergestellt wird.

Daher ist es theoretisch möglich, dass eine App einen sekundären Prozess startet, den system_server abstürzen lässt und dann aus diesem Hintergrundprozess während des Starts des system_server einen Angriff durchführt.

Verbindung zu zygote über SensorService abgreifen

Es gibt also einen weiteren Bug, den ich gefunden habe. Hier ist die SensorService::createSensorDirectConnection()-Methode```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:~
Wir haben einen `dup(clone->data[0])`-Aufruf. `clone` ist ein `native_handle_t`, der vom Remote-Prozess empfangen wurde. Der native Handle enthält eine bestimmte Anzahl von FDs und eine bestimmte Anzahl von einfachen Ganzzahlen in `data`, deren Anzahl der Nutzer des native Handle überprüfen sollte, indem er [in `numFds` und `numInts` schaut](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e), bevor er auf `data` zugreift.

Hier haben wir sogar einen Abschnitt „check specific to memory type“, der überprüft, dass für `SENSOR_DIRECT_MEM_TYPE_ASHMEM` dies hier überprüft wird, für `SENSOR_DIRECT_MEM_TYPE_GRALLOC` das Format des native Handle gerätespezifisch ist und hier nicht validiert werden kann. Die Sache ist, dass für „runtime sensors“ der Typ immer als `SENSOR_DIRECT_MEM_TYPE_ASHMEM` behandelt wird, aber wir `SENSOR_DIRECT_MEM_TYPE_GRALLOC` angeben können, um die Validierung in diesem Fall zu umgehen.

Dieser Code ist jedoch nur erreichbar, wenn „runtime sensors“ vorhanden sind. Ich bin mir nicht sicher, in welchem Fall das wirklich passiert, ich denke, es ist, wenn der Benutzer „Nearby app streaming“ verwendet (?)

Für Tests füge ich allerdings eine kleine Klasse hinzu, die die Registrierung eines [`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary) ermöglicht, du kannst es über```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'

Danach ist es möglich, Code als System-UID auf dem Produktionsgerät auszuführen

App zeigt langen Text mit einer Liste von Dateideskriptoren, an deren Ende wir haben, "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"

Tool herunterladen
system_server
  • PackageParser$Activity (9)
    • PooledStringWriter
RemoteViews
  • Das äußerste ReceiverInfo hat eine definierte Länge, die in die Mitte seiner Daten fällt; während des Lesens ist das jedoch noch nicht bekannt, und Intent wird normal daraus gelesen.
  • Das, was wir später lesen müssen, wird durch einen readString-Aufruf gelesen, der von statischem ComponentName.readFromParcel() gemacht wird (wir verwenden ComponentName, weil es unabhängig von der Android-Version UTF-16-readString verwendet; dieses readString gibt trotzdem null zurück, weil diese Zeichenkette Binder-Objekte überlappt. Während die Konstruktion von ComponentName also Daten übersprungen hat, die für diesen Durchlauf verborgen sind, wird das ComponentName-Objekt nicht erstellt).
  • Wir betreten das zweite verschachtelte Bundle; am Ende der Deserialisierung dieses Bundle-s wird eine BadParcelableException abgefangen.
  • Dann betreten wir das zweite ReceiverInfo. Dieses ReceiverInfo hat die Länge Integer.MAX_VALUE und wird daher im finally-Block eine BadParcelableException werfen, wodurch die ClassCastException stillschweigend verworfen wird.
  • Drittes Bundle. Das dient nur dazu, ein beliebiges readParcelable von ReceiverInfo erreichen zu können, was wir jetzt tun können, da wir uns innerhalb von RemoteViews befinden und das Bundle sofort gelesen wird.
  • Die Kombination aus PackageParser$Activity und PooledStringWriter löst einen writeInt(0)-Aufruf auf dem Parcel aus, das gerade gelesen wird. Da wir uns am Ende des Parcel befanden, muss writeInt() die Kapazität des Parcel erweitern, was den „take possession“-Pfad auslöst. Diese Kombination führt auch zu einer ClassCastException, die wie in den Schritten 7 und 6 oben beschrieben verschluckt wird.
  • Wir erreichen das Ende des äußeren ReceiverInfo. ReceiverInfo positioniert sich gemäß der Länge in seinem Header neu und fällt in Daten, die sich zuvor in ComponentName befanden und die als nächste Elemente im Parcelable[] gelesen werden.
  • Hier gibt es ein ParceledListSlice, das eine blockierende Binder-Transaktion an meinen Prozess durchführt. Zu diesem Zeitpunkt sind die in diesem Parcel enthaltenen File Descriptors geschlossen, aber noch hat nichts Interessantes ihren Platz eingenommen. Während diese Deserialisierung auf die Rückkehr von diesem Aufruf wartet, kann ich das System dazu bringen, einige interessante File Descriptors zu öffnen, die mir dann gesendet werden.
  • Dies ist der Teil, in dem File Descriptors aus diesem Parcel entnommen werden, um an mich gesendet zu werden. Es unterscheidet sich danach, ob ParcelableListBinder Elemente filtert oder nicht. a. Wenn ParcelableListBinder keine Elemente filtert, werden die FDs innerhalb von ParcelableParcel gespeichert. Dieses kopiert ähnlich wie Bundle die Daten des Parcel unverändert mit Parcel.appendFrom(), hat aber keine spezielle hasReadWriteHelper()-Logik, sodass es das tut, obwohl es sich unter dem RemoteViews-Bundle befindet. b. Wenn ParcelableListBinder Elemente filtert, ist dies das Ende von RemoteViews; das RemoteViews-Objekt wird von ParcelableListBinder verworfen, aber das ist für mich in Ordnung, da die Seiteneffekte bereits eingetreten sind. Das nächste von ParcelableListBinder empfangene Element ist QueueItem, das eine MediaDescription enthält, die wiederum ein Bundle enthält, in dem meine geleakten FDs aufbewahrt werden.