flawopen.com/Teardowns/cve-2024-23712-android-appops-attribution-tag-dos
CVE-2024-23712: Ressourcenerschöpfung durch Attribution-Tags in Android AppOps
CVE-2024-23712 (CVSS 5.5; Android stuft sie als hoch ein): AppOpsService speicherte jedes Proxy-Attribution-Tag, das eine aufrufende App mitgab. Eine gewöhnliche App konnte /data/system/appops_accesses.xml so unbegrenzt wachsen lassen und ohne zusätzliche Berechtigungen einen lokalen Denial of Service auslösen.
Der Empfang eines Bürogebäudes führt ein Besucherbuch mit einer Seite pro Abteilung, und jeder Kurier trägt ein Feld „begleitet von Abteilung“ aus. Niemand gleicht dieses Feld mit der offiziellen Abteilungsliste ab. Ein Kurier schreibt bei jeder Lieferung einen neuen erfundenen Abteilungsnamen hinein, tausendfach am Tag. Das Buch bekommt für jeden Namen eine neue Seite und wird jede Nacht komplett ins Archiv kopiert, bis der Empfang nur noch Seiten kopiert. Die Lösung: Das Feld bleibt leer, wenn der Name nicht auf der offiziellen Liste steht, und jede Firma darf nur begrenzt viele Abteilungen anmelden.
Kernkonzepte & Begriffe
AppOpsService- Dienst im
system_server, der festhält, welche App welche geschützte Operation (Kamera, Standort, Mikrofon usw.) wann und in wessen Auftrag genutzt hat. Attribution-Tag- Kennzeichnung, die eine App im Manifest mit
<attribution android:tag=...>deklariert, um Datenzugriffe einer Funktion der App zuzuordnen. Die Plattform begrenzt, wie viele ein Paket deklarieren darf (MAX_NUM_ATTRIBUTIONS). Proxy-Operation- Ein App-Op, der im Auftrag einer anderen App protokolliert wird, etwa in einer
AttributionSource-Kette. UID, Paket und Attribution-Tag des Proxys werden mit dem Zugriffseintrag gespeichert. appops_accesses.xml- Datei in
/data/system, in der AppOpsService Zugriffseinträge speichert;scheduleWriteLocked()schreibt sie neu, beim Booten wird sie erneut eingelesen.
Ursachenanalyse
noteOperationUnchecked() und startOperationUnchecked() in AppOpsService.java reichten das vom Aufrufer kontrollierte proxyAttributionTag direkt an AttributedOp weiter, ohne zu prüfen, ob die Proxy-App dieses Tag deklariert hatte. Jede neue Zeichenkette erzeugte einen weiteren Proxy-Eintrag, der im Speicher blieb und nach .accessed()/data/system/appops_accesses.xml geschrieben wurde. Eine einzige App konnte die Datei so aufblähen, bis system_server Speicher und I/O erschöpfte. Der Fix setzt jedes Tag auf null, das isAttributionTagDefined() in keinem der beiden Manifeste findet, und senkt MAX_NUM_ATTRIBUTIONS von 10.000 auf 1.000.
Schritt-für-Schritt Angriffsablauf
Eine gewöhnliche App agiert als Proxy
Eine installierte App ohne besondere Berechtigungen protokolliert App-Ops im Auftrag eines anderen Pakets und übergibt über AppOpsManager ihr eigenes Proxy-Attribution-Tag.
Bei jedem Aufruf ein neues Tag
Die App erzeugt bei jedem Aufruf eine neue, nicht deklarierte Tag-Zeichenkette. noteOperationUnchecked() übernimmt jede unverändert.
Der Zustand wächst unbegrenzt
AttributedOp legt für jedes neue Proxy-Tag einen Eintrag an, und .accessed()scheduleWriteLocked() schreibt die wachsende Menge immer wieder nach appops_accesses.xml.
Lokaler Denial of Service
system_server verbraucht immer mehr Speicher und Datenträger-I/O, um die Datei zu serialisieren und neu einzulesen; das Gerät wird langsam oder hängt, auch nach einem Neustart.
Quellcode: Verwundbar vs. Sicher
// 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;
Checkliste für Engineering & Systemsicherheit
- ✓Geräte mit Sicherheitspatch-Level 2024-04-01 oder neuer ausliefern; es enthält den AOSP-Commit 6beb68ca.
- ✓In jedem Systemdienst vom Client gelieferte Kennungen gegen ein serverseitiges Register (hier das Paketmanifest) prüfen, bevor sie als Schlüssel in gespeichertem Zustand dienen.
- ✓Für jede persistierte Struktur eine harte Obergrenze an Einträgen pro Client setzen; der Patch senkte
MAX_NUM_ATTRIBUTIONSvon 10.000 auf 1.000. - ✓Einen Test schreiben, der Tausende eindeutiger, vom Client gewählter Schlüssel sendet und prüft, dass gespeicherter Zustand und persistierte Datei unter einer festen Größe bleiben.
- ✓In Apps jedes Attribution-Tag im Manifest deklarieren und Kontexte mit
Contextnur für deklarierte Tags erzeugen; nicht deklarierte Proxy-Tags werden jetzt verworfen..createAttributionContext()