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

● CVE-2024-23712 · CVSS 5.5 · Medium
FlawOpen Security Research

CVE-2024-23712: Android AppOps Attribution Tag Resource Exhaustion

CVE-2024-23712 (CVSS 5.5; Android rates it High): AppOpsService recorded any proxy attribution tag a calling app supplied, so an ordinary app could grow /data/system/appops_accesses.xml without limit and cause a local denial of service, with no extra permissions.

💡 Plain English Explainer (ELI5)

A building's front desk keeps a visitor book with one page per department, and every courier fills in an "escorted by department" box. Nobody checks that box against the building's list of real departments. One courier writes a new made-up department name on every delivery, thousands of times a day. The book gains a fresh page for each name and is recopied into the archive every night until the desk spends all its time copying pages. The fix is to leave the box blank whenever the name is not in the official department list, and to cap how many departments one company may register.

Core Concepts & Subsystem Terms

AppOpsService
The system_server service that records which app used which protected operation (camera, location, microphone and so on), when, and on whose behalf.
Attribution tag
A label an app declares in its manifest with <attribution android:tag=...> so that data access can be attributed to a feature of the app. The platform caps how many a package may declare (MAX_NUM_ATTRIBUTIONS).
Proxy operation
An app-op noted on behalf of another app, as in an AttributionSource chain. The proxy's uid, package and attribution tag are stored with the access record.
appops_accesses.xml
The file in /data/system where AppOpsService persists access records, rewritten by scheduleWriteLocked() and parsed again at boot.

Root Cause Analysis

noteOperationUnchecked() and startOperationUnchecked() in AppOpsService.java passed the caller-controlled proxyAttributionTag straight to AttributedOp.accessed() without checking that the proxy app had declared that tag. Every distinct string produced another proxy record that stayed in memory and was written to /data/system/appops_accesses.xml, so one app could inflate the file until system_server exhausted memory and I/O. The fix nulls any tag that isAttributionTagDefined() cannot find in either package's manifest, and lowers MAX_NUM_ATTRIBUTIONS from 10,000 to 1,000.

Step-by-Step Attack Flow

Step 1

An ordinary app acts as a proxy

An installed app with no special permissions notes app-ops on behalf of another package, passing its own proxy attribution tag through AppOpsManager.

Step 2

A new tag on every call

The app generates a fresh, undeclared attribution tag string for each call. noteOperationUnchecked() accepts each one unchanged.

Step 3

State grows without bound

AttributedOp.accessed() stores a record for every distinct proxy tag and scheduleWriteLocked() keeps persisting the growing set to appops_accesses.xml.

Step 4

Local denial of service

system_server spends ever more memory and disk I/O serialising and reparsing the file, degrading or stalling the device, including after reboot.

Source Code: Flaw vs. Secure Implementation

UNPATCHED FLAW
// 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;
HARDENED SECURE PATCH
// 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;

Engineering & System Hardening Checklist

Sources