flawopen.com/Teardowns/cve-2024-23712-android-appops-attribution-tag-dos

● CVE-2024-23712 · CVSS 5.5 · Mittel
Sicherheitsforschung · FlawOpen

CVE-2024-23712: Ressourcenerschöpfung durch Attribution-Tags in Android AppOps

CVE-2024-23712 (CVSS 5.5; Android stuft sie als hoch ein): AppOpsService speicherte jedes Proxy-Attribution-Tag, das eine aufrufende App mitgab. Eine gewöhnliche App konnte /data/system/appops_accesses.xml so unbegrenzt wachsen lassen und ohne zusätzliche Berechtigungen einen lokalen Denial of Service auslösen.

💡 Einfache Erklärung (ELI5)

Der Empfang eines Bürogebäudes führt ein Besucherbuch mit einer Seite pro Abteilung, und jeder Kurier trägt ein Feld „begleitet von Abteilung“ aus. Niemand gleicht dieses Feld mit der offiziellen Abteilungsliste ab. Ein Kurier schreibt bei jeder Lieferung einen neuen erfundenen Abteilungsnamen hinein, tausendfach am Tag. Das Buch bekommt für jeden Namen eine neue Seite und wird jede Nacht komplett ins Archiv kopiert, bis der Empfang nur noch Seiten kopiert. Die Lösung: Das Feld bleibt leer, wenn der Name nicht auf der offiziellen Liste steht, und jede Firma darf nur begrenzt viele Abteilungen anmelden.

Kernkonzepte & Begriffe

AppOpsService
Dienst im system_server, der festhält, welche App welche geschützte Operation (Kamera, Standort, Mikrofon usw.) wann und in wessen Auftrag genutzt hat.
Attribution-Tag
Kennzeichnung, die eine App im Manifest mit <attribution android:tag=...> deklariert, um Datenzugriffe einer Funktion der App zuzuordnen. Die Plattform begrenzt, wie viele ein Paket deklarieren darf (MAX_NUM_ATTRIBUTIONS).
Proxy-Operation
Ein App-Op, der im Auftrag einer anderen App protokolliert wird, etwa in einer AttributionSource-Kette. UID, Paket und Attribution-Tag des Proxys werden mit dem Zugriffseintrag gespeichert.
appops_accesses.xml
Datei in /data/system, in der AppOpsService Zugriffseinträge speichert; scheduleWriteLocked() schreibt sie neu, beim Booten wird sie erneut eingelesen.

Ursachenanalyse

noteOperationUnchecked() und startOperationUnchecked() in AppOpsService.java reichten das vom Aufrufer kontrollierte proxyAttributionTag direkt an AttributedOp.accessed() weiter, ohne zu prüfen, ob die Proxy-App dieses Tag deklariert hatte. Jede neue Zeichenkette erzeugte einen weiteren Proxy-Eintrag, der im Speicher blieb und nach /data/system/appops_accesses.xml geschrieben wurde. Eine einzige App konnte die Datei so aufblähen, bis system_server Speicher und I/O erschöpfte. Der Fix setzt jedes Tag auf null, das isAttributionTagDefined() in keinem der beiden Manifeste findet, und senkt MAX_NUM_ATTRIBUTIONS von 10.000 auf 1.000.

Schritt-für-Schritt Angriffsablauf

Schritt 1

Eine gewöhnliche App agiert als Proxy

Eine installierte App ohne besondere Berechtigungen protokolliert App-Ops im Auftrag eines anderen Pakets und übergibt über AppOpsManager ihr eigenes Proxy-Attribution-Tag.

Schritt 2

Bei jedem Aufruf ein neues Tag

Die App erzeugt bei jedem Aufruf eine neue, nicht deklarierte Tag-Zeichenkette. noteOperationUnchecked() übernimmt jede unverändert.

Schritt 3

Der Zustand wächst unbegrenzt

AttributedOp.accessed() legt für jedes neue Proxy-Tag einen Eintrag an, und scheduleWriteLocked() schreibt die wachsende Menge immer wieder nach appops_accesses.xml.

Schritt 4

Lokaler Denial of Service

system_server verbraucht immer mehr Speicher und Datenträger-I/O, um die Datei zu serialisieren und neu einzulesen; das Gerät wird langsam oder hängt, auch nach einem Neustart.

Quellcode: Verwundbar vs. Sicher

VERWUNDBARE IMPLEMENTIERUNG
// services/core/java/com/android/server/appop/AppOpsService.java (before 2024-04-01)
private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String packageName,
        @Nullable String attributionTag, int proxyUid, String proxyPackageName,
        @Nullable String proxyAttributionTag, @OpFlags int flags,
        boolean shouldCollectAsyncNotedOp, @Nullable String message,
        boolean shouldCollectMessage) {
    // (uid and package verification elided)

    // BUG: proxyAttributionTag comes from the calling app and is never checked
    // against the <attribution> tags that app declared. Each new string becomes
    // another attributed entry, held in memory and persisted to
    // /data/system/appops_accesses.xml.
    synchronized (this) {
        final Ops ops = getOpsLocked(uid, packageName, attributionTag,
                pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
        final Op op = getOpLocked(ops, code, uid, true);
        final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
        attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag,
                uidState.getState(), flags);
        scheduleWriteLocked();
    }
    return new SyncNotedAppOp(AppOpsManager.MODE_ALLOWED, code, attributionTag, packageName);
}

// services/core/java/com/android/server/pm/pkg/component/ParsedAttributionImpl.java
/** Maximum amount of attributions per package */
static final int MAX_NUM_ATTRIBUTIONS = 10000;
GEHÄRTETER SICHERHEITS-PATCH
// services/core/java/com/android/server/appop/AppOpsService.java (2024-04-01, AOSP 6beb68ca)
private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String packageName,
        @Nullable String attributionTag, int proxyUid, String proxyPackageName,
        @Nullable String proxyAttributionTag, @OpFlags int flags,
        boolean shouldCollectAsyncNotedOp, @Nullable String message,
        boolean shouldCollectMessage) {
    // (uid and package verification elided)

    // FIX: drop a proxy attribution tag that neither package declared, so a
    // caller can no longer mint unlimited entries. startOperationUnchecked()
    // gets the same check.
    if (proxyAttributionTag != null
            && !isAttributionTagDefined(packageName, proxyPackageName, proxyAttributionTag)) {
        proxyAttributionTag = null;
    }
    synchronized (this) {
        final Ops ops = getOpsLocked(uid, packageName, attributionTag,
                pvr.isAttributionTagValid, pvr.bypass, /* edit */ true);
        final Op op = getOpLocked(ops, code, uid, true);
        final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag);
        attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag,
                uidState.getState(), flags);
        scheduleWriteLocked();
    }
    return new SyncNotedAppOp(AppOpsManager.MODE_ALLOWED, code, attributionTag, packageName);
}

private boolean isAttributionTagDefined(@Nullable String packageName,
        @Nullable String proxyPackageName, @Nullable String attributionTag) {
    if (packageName == null) {
        return false;
    } else if (attributionTag == null) {
        return true;
    }
    PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class);
    if (proxyPackageName != null) {
        AndroidPackage proxyPkg = pmInt.getPackage(proxyPackageName);
        if (proxyPkg != null && isAttributionInPackage(proxyPkg, attributionTag)) {
            return true;
        }
    }
    AndroidPackage pkg = pmInt.getPackage(packageName);
    return isAttributionInPackage(pkg, attributionTag);
}

// services/core/java/com/android/server/pm/pkg/component/ParsedAttributionImpl.java
/** Maximum amount of attributions per package */
static final int MAX_NUM_ATTRIBUTIONS = 1000;

Checkliste für Engineering & Systemsicherheit

Quellen