flawopen.com/Teardowns/cve-2024-23712-android-appops-attribution-tag-dos
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.
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_serverservice 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
AttributionSourcechain. The proxy's uid, package and attribution tag are stored with the access record. appops_accesses.xml- The file in
/data/systemwhere AppOpsService persists access records, rewritten byscheduleWriteLocked()and parsed again at boot.
Root Cause Analysis
noteOperationUnchecked() and startOperationUnchecked() in AppOpsService.java passed the caller-controlled proxyAttributionTag straight to AttributedOp 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 .accessed()/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
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.
A new tag on every call
The app generates a fresh, undeclared attribution tag string for each call. noteOperationUnchecked() accepts each one unchanged.
State grows without bound
AttributedOp stores a record for every distinct proxy tag and .accessed()scheduleWriteLocked() keeps persisting the growing set to appops_accesses.xml.
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
// 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;
Engineering & System Hardening Checklist
- ✓Ship devices at security patch level 2024-04-01 or later, which includes AOSP commit 6beb68ca.
- ✓In any system service, validate a client-supplied identifier against a server-side registry (here the package manifest) before using it as a key in stored state.
- ✓Put a hard cap on per-client entries in every persisted structure; the patch cut
MAX_NUM_ATTRIBUTIONSfrom 10,000 to 1,000. - ✓Add a test that submits thousands of unique client-chosen keys and asserts that stored state and the persisted file stay within a fixed size.
- ✓In apps, declare every attribution tag in the manifest and create contexts only with
Contextfor declared tags; undeclared proxy tags are now dropped..createAttributionContext()