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

● CVE-2024-23712 · CVSS 5.5 · Moyenne
Recherche · FlawOpen

CVE-2024-23712 : épuisement des ressources par étiquettes d'attribution dans AppOps d'Android

CVE-2024-23712 (CVSS 5.5 ; Android la classe élevée) : AppOpsService enregistrait n'importe quelle étiquette d'attribution de proxy fournie par l'app appelante, si bien qu'une app ordinaire pouvait faire grossir /data/system/appops_accesses.xml sans limite et provoquer un déni de service local, sans permission supplémentaire.

💡 Explication en Langage Simple (ELI5)

L'accueil d'un immeuble tient un registre des visites, une page par service, et chaque coursier remplit la case « accompagné par le service ». Personne ne compare cette case à la liste officielle des services. Un coursier écrit un nom de service inventé différent à chaque livraison, des milliers de fois par jour. Le registre gagne une page par nom et il est recopié en entier aux archives chaque nuit, jusqu'à ce que l'accueil ne fasse plus que recopier des pages. Le correctif : laisser la case vide dès que le nom n'est pas dans la liste officielle, et plafonner le nombre de services qu'une entreprise peut déclarer.

Concepts Clés et Termes

AppOpsService
Service de system_server qui enregistre quelle app a utilisé quelle opération protégée (caméra, position, micro, etc.), quand et pour le compte de qui.
Étiquette d'attribution
Libellé qu'une app déclare dans son manifeste avec <attribution android:tag=...> pour rattacher un accès aux données à une fonctionnalité. La plateforme plafonne le nombre qu'un paquet peut déclarer (MAX_NUM_ATTRIBUTIONS).
Opération de proxy
Un app-op noté pour le compte d'une autre app, comme dans une chaîne AttributionSource. L'uid, le paquet et l'étiquette d'attribution du proxy sont stockés avec l'enregistrement d'accès.
appops_accesses.xml
Fichier de /data/system où AppOpsService conserve les enregistrements d'accès ; scheduleWriteLocked() le réécrit et il est relu au démarrage.

Analyse de Cause Racine

noteOperationUnchecked() et startOperationUnchecked() dans AppOpsService.java transmettaient le proxyAttributionTag, contrôlé par l'appelant, directement à AttributedOp.accessed() sans vérifier que l'app proxy avait déclaré cette étiquette. Chaque chaîne distincte créait un nouvel enregistrement de proxy, gardé en mémoire et écrit dans /data/system/appops_accesses.xml : une seule app pouvait gonfler le fichier jusqu'à épuiser la mémoire et les E/S de system_server. Le correctif annule toute étiquette que isAttributionTagDefined() ne trouve dans le manifeste d'aucun des deux paquets et abaisse MAX_NUM_ATTRIBUTIONS de 10 000 à 1 000.

Déroulement de l'Attaque Étape par Étape

Étape 1

Une app ordinaire joue le proxy

Une app installée, sans permission particulière, note des app-ops pour le compte d'un autre paquet en transmettant sa propre étiquette d'attribution de proxy via AppOpsManager.

Étape 2

Une nouvelle étiquette à chaque appel

L'app génère à chaque appel une nouvelle chaîne d'étiquette non déclarée. noteOperationUnchecked() les accepte toutes telles quelles.

Étape 3

L'état grossit sans limite

AttributedOp.accessed() stocke un enregistrement par étiquette de proxy distincte et scheduleWriteLocked() continue d'écrire l'ensemble croissant dans appops_accesses.xml.

Étape 4

Déni de service local

system_server consacre toujours plus de mémoire et d'E/S disque à sérialiser et relire le fichier, ce qui ralentit ou bloque l'appareil, y compris après redémarrage.

Code Source : Vulnérable vs Sécurisé

IMPLÉMENTATION VULNÉRABLE
// 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;
PATCH SÉCURISÉ ET ROBUSTE
// 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;

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources