flawopen.com/Teardowns/cve-2024-23712-android-appops-attribution-tag-dos
CVE-2024-23712: esgotamento de recursos por tags de atribuição no AppOps do Android
CVE-2024-23712 (CVSS 5.5; o Android classifica como alta): o AppOpsService registrava qualquer tag de atribuição de proxy enviada pelo app chamador, então um app comum podia fazer /data/system/appops_accesses.xml crescer sem limite e causar negação de serviço local, sem permissões extras.
A recepção de um prédio mantém um livro de visitas com uma página por departamento, e todo entregador preenche o campo "acompanhado pelo departamento". Ninguém confere esse campo com a lista oficial de departamentos. Um entregador escreve um nome inventado diferente a cada entrega, milhares de vezes por dia. O livro ganha uma página nova para cada nome e é copiado inteiro para o arquivo toda noite, até que a recepção passa o tempo todo copiando páginas. A correção é deixar o campo em branco sempre que o nome não estiver na lista oficial e limitar quantos departamentos uma empresa pode registrar.
Conceitos Centrais e Termos
AppOpsService- Serviço do
system_serverque registra qual app usou qual operação protegida (câmera, localização, microfone etc.), quando e em nome de quem. Tag de atribuição- Rótulo que um app declara no manifesto com
<attribution android:tag=...>para atribuir o acesso a dados a um recurso do app. A plataforma limita quantas um pacote pode declarar (MAX_NUM_ATTRIBUTIONS). Operação de proxy- Um app-op registrado em nome de outro app, como numa cadeia de
AttributionSource. O uid, o pacote e a tag de atribuição do proxy são guardados com o registro de acesso. appops_accesses.xml- Arquivo em
/data/systemonde o AppOpsService persiste os registros de acesso; é regravado porscheduleWriteLocked()e lido de novo na inicialização.
Análise de Causa Raiz
noteOperationUnchecked() e startOperationUnchecked() em AppOpsService.java passavam o proxyAttributionTag, controlado pelo chamador, direto para AttributedOp, sem verificar se o app proxy havia declarado essa tag. Cada string distinta gerava mais um registro de proxy que ficava na memória e era gravado em .accessed()/data/system/appops_accesses.xml, então um único app podia inflar o arquivo até o system_server esgotar memória e E/S. A correção anula qualquer tag que isAttributionTagDefined() não encontre no manifesto de nenhum dos dois pacotes e reduz MAX_NUM_ATTRIBUTIONS de 10.000 para 1.000.
Fluxo de Ataque Passo a Paso
Um app comum age como proxy
Um app instalado, sem permissões especiais, registra app-ops em nome de outro pacote e passa a própria tag de atribuição de proxy pelo AppOpsManager.
Uma tag nova a cada chamada
O app gera uma string de tag nova e não declarada em cada chamada. noteOperationUnchecked() aceita todas sem alteração.
O estado cresce sem limite
AttributedOp guarda um registro para cada tag de proxy distinta e .accessed()scheduleWriteLocked() continua gravando o conjunto crescente em appops_accesses.xml.
Negação de serviço local
O system_server gasta cada vez mais memória e E/S de disco serializando e relendo o arquivo, deixando o aparelho lento ou travado, inclusive depois de reiniciar.
Código-Fonte: Vulnerável 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 Verificação de Segurança para Engenharia
- ✓Distribua aparelhos com nível de patch de segurança 2024-04-01 ou posterior, que inclui o commit AOSP 6beb68ca.
- ✓Em qualquer serviço de sistema, valide um identificador enviado pelo cliente contra um registro do lado do servidor (aqui, o manifesto do pacote) antes de usá-lo como chave de estado armazenado.
- ✓Imponha um limite rígido de entradas por cliente em toda estrutura persistida; o patch reduziu
MAX_NUM_ATTRIBUTIONSde 10.000 para 1.000. - ✓Crie um teste que envie milhares de chaves únicas escolhidas pelo cliente e verifique que o estado armazenado e o arquivo persistido ficam abaixo de um tamanho fixo.
- ✓Nos apps, declare todas as tags de atribuição no manifesto e crie contextos com
Contextapenas para tags declaradas; tags de proxy não declaradas agora são descartadas..createAttributionContext()