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

● CVE-2024-23712 · CVSS 5.5 · Media
Investigación · FlawOpen

CVE-2024-23712: agotamiento de recursos por etiquetas de atribución en AppOps de Android

CVE-2024-23712 (CVSS 5.5; Android lo califica como alta): AppOpsService registraba cualquier etiqueta de atribución de proxy que enviara la app llamante, así que una app normal podía hacer crecer /data/system/appops_accesses.xml sin límite y provocar una denegación de servicio local sin permisos adicionales.

💡 Explicación en Lenguaje Sencillo (ELI5)

La recepción de un edificio lleva un libro de visitas con una página por departamento, y cada mensajero rellena la casilla "acompañado por el departamento". Nadie compara esa casilla con la lista oficial de departamentos. Un mensajero escribe un nombre inventado distinto en cada entrega, miles de veces al día. El libro suma una página nueva por cada nombre y se copia entero al archivo cada noche, hasta que la recepción pasa todo el tiempo copiando páginas. La solución es dejar la casilla en blanco cuando el nombre no está en la lista oficial y limitar cuántos departamentos puede registrar cada empresa.

Conceptos Clave y Términos

AppOpsService
Servicio de system_server que registra qué app usó qué operación protegida (cámara, ubicación, micrófono, etc.), cuándo y en nombre de quién.
Etiqueta de atribución
Etiqueta que una app declara en su manifiesto con <attribution android:tag=...> para atribuir el acceso a datos a una función de la app. La plataforma limita cuántas puede declarar un paquete (MAX_NUM_ATTRIBUTIONS).
Operación de proxy
Un app-op registrado en nombre de otra app, como en una cadena de AttributionSource. El uid, el paquete y la etiqueta de atribución del proxy se guardan con el registro de acceso.
appops_accesses.xml
Archivo de /data/system donde AppOpsService guarda los registros de acceso; scheduleWriteLocked() lo reescribe y se vuelve a leer al arrancar.

Análisis de Causa Raíz

noteOperationUnchecked() y startOperationUnchecked() en AppOpsService.java pasaban el proxyAttributionTag, controlado por quien llama, directamente a AttributedOp.accessed() sin comprobar que la app proxy hubiera declarado esa etiqueta. Cada cadena distinta generaba otro registro de proxy que se quedaba en memoria y se escribía en /data/system/appops_accesses.xml, así que una sola app podía inflar el archivo hasta que system_server agotara memoria y E/S. La corrección anula cualquier etiqueta que isAttributionTagDefined() no encuentre en el manifiesto de ninguno de los dos paquetes y baja MAX_NUM_ATTRIBUTIONS de 10.000 a 1.000.

Flujo de Ataque Paso a Paso

Paso 1

Una app normal actúa como proxy

Una app instalada sin permisos especiales registra app-ops en nombre de otro paquete y pasa su propia etiqueta de atribución de proxy a través de AppOpsManager.

Paso 2

Una etiqueta nueva en cada llamada

La app genera en cada llamada una cadena de etiqueta nueva y no declarada. noteOperationUnchecked() las acepta todas tal cual.

Paso 3

El estado crece sin límite

AttributedOp.accessed() guarda un registro por cada etiqueta de proxy distinta y scheduleWriteLocked() sigue escribiendo el conjunto creciente en appops_accesses.xml.

Paso 4

Denegación de servicio local

system_server dedica cada vez más memoria y E/S de disco a serializar y releer el archivo, y el dispositivo se ralentiza o se bloquea, también tras reiniciar.

Código Fuente: Vulnerable vs. Seguro

IMPLEMENTACIÓN VULNERABLE
// 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;
PARCHE SEGURO Y ROBUSTO
// 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;

Lista de Verificación de Seguridad para Ingeniería

Fuentes