flawopen.com/Teardowns/cve-2024-23712-android-appops-attribution-tag-dos
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.
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_serverque 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/systemdonde 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 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 .accessed()/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
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.
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.
El estado crece sin límite
AttributedOp guarda un registro por cada etiqueta de proxy distinta y .accessed()scheduleWriteLocked() sigue escribiendo el conjunto creciente en appops_accesses.xml.
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
// 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;
// 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
- ✓Distribuya los dispositivos con nivel de parche de seguridad 2024-04-01 o posterior, que incluye el commit AOSP 6beb68ca.
- ✓En cualquier servicio del sistema, valide un identificador enviado por el cliente contra un registro del lado del servidor (aquí, el manifiesto del paquete) antes de usarlo como clave de estado almacenado.
- ✓Ponga un límite estricto de entradas por cliente en toda estructura persistida; el parche redujo
MAX_NUM_ATTRIBUTIONSde 10.000 a 1.000. - ✓Añada una prueba que envíe miles de claves únicas elegidas por el cliente y compruebe que el estado almacenado y el archivo persistido no superan un tamaño fijo.
- ✓En las apps, declare todas las etiquetas de atribución en el manifiesto y cree contextos con
Contextsolo para etiquetas declaradas; las etiquetas de proxy no declaradas ahora se descartan..createAttributionContext()