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

● CVE-2024-23712 · CVSS 5.5 · Média
Pesquisa · FlawOpen

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.

💡 Explicação em Linguagem Simples (ELI5)

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_server que 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/system onde o AppOpsService persiste os registros de acesso; é regravado por scheduleWriteLocked() 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.accessed(), 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 /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

Passo 1

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.

Passo 2

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.

Passo 3

O estado cresce sem limite

AttributedOp.accessed() guarda um registro para cada tag de proxy distinta e scheduleWriteLocked() continua gravando o conjunto crescente em appops_accesses.xml.

Passo 4

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

IMPLEMENTAÇÃO VULNERÁVEL
// 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 SEGURO E 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 Verificação de Segurança para Engenharia

Fontes