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

● CVE-2024-23712 · CVSS 5.5 · Sedang
Riset Keamanan · FlawOpen

CVE-2024-23712: Kehabisan Sumber Daya lewat Tag Atribusi di AppOps Android

CVE-2024-23712 (CVSS 5.5; Android menilainya tinggi): AppOpsService mencatat tag atribusi proxy apa pun yang diberikan aplikasi pemanggil, sehingga aplikasi biasa tanpa izin tambahan dapat membuat /data/system/appops_accesses.xml membengkak tanpa batas dan memicu penolakan layanan lokal.

💡 Penjelasan Sederhana (ELI5)

Resepsionis sebuah gedung kantor menyimpan buku tamu dengan satu halaman per departemen, dan setiap kurir mengisi kolom "didampingi departemen". Tidak ada yang mencocokkan kolom itu dengan daftar resmi departemen. Seorang kurir menulis nama departemen karangan yang baru di setiap pengiriman, ribuan kali sehari. Buku itu menambah halaman untuk setiap nama dan disalin utuh ke arsip setiap malam, sampai resepsionis menghabiskan seluruh waktunya menyalin halaman. Perbaikannya: kosongkan kolom itu bila nama tidak ada di daftar resmi, dan batasi berapa banyak departemen yang boleh didaftarkan satu perusahaan.

Konsep Kunci & Istilah

AppOpsService
Layanan di system_server yang mencatat aplikasi mana memakai operasi terlindungi mana (kamera, lokasi, mikrofon, dan sebagainya), kapan, dan atas nama siapa.
Tag atribusi
Label yang dideklarasikan aplikasi di manifes dengan <attribution android:tag=...> untuk mengaitkan akses data ke fitur tertentu. Platform membatasi jumlah yang boleh dideklarasikan satu paket (MAX_NUM_ATTRIBUTIONS).
Operasi proxy
App-op yang dicatat atas nama aplikasi lain, seperti dalam rantai AttributionSource. Uid, paket, dan tag atribusi proxy disimpan bersama catatan akses.
appops_accesses.xml
File di /data/system tempat AppOpsService menyimpan catatan akses; ditulis ulang oleh scheduleWriteLocked() dan diurai lagi saat booting.

Analisis Akar Masalah (Root Cause)

noteOperationUnchecked() dan startOperationUnchecked() di AppOpsService.java meneruskan proxyAttributionTag yang dikendalikan pemanggil langsung ke AttributedOp.accessed() tanpa memeriksa apakah aplikasi proxy mendeklarasikan tag itu. Setiap string berbeda menghasilkan satu catatan proxy lagi yang tetap di memori dan ditulis ke /data/system/appops_accesses.xml, sehingga satu aplikasi saja dapat menggembungkan file itu sampai system_server kehabisan memori dan I/O. Perbaikannya mengosongkan (null) setiap tag yang tidak ditemukan isAttributionTagDefined() di manifes kedua paket, dan menurunkan MAX_NUM_ATTRIBUTIONS dari 10.000 menjadi 1.000.

Alur Serangan Langkah demi Langkah

Langkah 1

Aplikasi biasa bertindak sebagai proxy

Aplikasi terpasang tanpa izin khusus mencatat app-op atas nama paket lain dan meneruskan tag atribusi proxy miliknya lewat AppOpsManager.

Langkah 2

Tag baru di setiap panggilan

Aplikasi membuat string tag baru yang tidak dideklarasikan di setiap panggilan. noteOperationUnchecked() menerima semuanya apa adanya.

Langkah 3

Status tumbuh tanpa batas

AttributedOp.accessed() menyimpan satu catatan untuk setiap tag proxy yang berbeda, dan scheduleWriteLocked() terus menulis kumpulan yang makin besar ke appops_accesses.xml.

Langkah 4

Penolakan layanan lokal

system_server menghabiskan makin banyak memori dan I/O disk untuk menserialisasi dan mengurai ulang file itu, sehingga perangkat melambat atau macet, bahkan setelah reboot.

Kode Sumber: Rentan vs Aman

IMPLEMENTASI RENTAN
// 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;
PERBAIKAN AMAN & KUAT
// 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;

Daftar Periksa Penguatan Sistem Rekayasa

Sumber