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

● CVE-2024-23712 · CVSS 5.5 · मध्यम
सुरक्षा अनुसंधान · FlawOpen

CVE-2024-23712: Android AppOps में एट्रिब्यूशन टैग से संसाधन समाप्ति

CVE-2024-23712 (CVSS 5.5; Android इसे उच्च मानता है): AppOpsService कॉल करने वाले ऐप का दिया कोई भी प्रॉक्सी एट्रिब्यूशन टैग दर्ज कर लेता था, इसलिए एक साधारण ऐप बिना अतिरिक्त अनुमति के /data/system/appops_accesses.xml को असीमित रूप से बढ़ाकर लोकल डिनायल ऑफ़ सर्विस कर सकता था।

💡 आसान भाषा में (ELI5)

एक दफ़्तर की इमारत के रिसेप्शन पर आगंतुक रजिस्टर रखा जाता है, जिसमें हर विभाग का एक पन्ना है, और हर कूरियर वाला "किस विभाग के साथ" खाना भरता है। कोई भी इस खाने को इमारत के असली विभागों की सूची से नहीं मिलाता। एक कूरियर वाला हर डिलीवरी पर एक नया मनगढ़ंत विभाग लिखता है, दिन में हज़ारों बार। रजिस्टर हर नाम के लिए नया पन्ना जोड़ता है और हर रात पूरा रजिस्टर अभिलेखागार में दोबारा उतारा जाता है, जब तक रिसेप्शन का सारा समय पन्ने उतारने में ही न जाने लगे। सुधार यह है कि नाम आधिकारिक सूची में न हो तो खाना खाली छोड़ दें, और हर कंपनी कितने विभाग दर्ज कर सकती है इसकी सीमा तय करें।

इस पेज के मुख्य शब्द

AppOpsService
system_server की सेवा जो दर्ज करती है कि किस ऐप ने कौन-सा सुरक्षित ऑपरेशन (कैमरा, लोकेशन, माइक्रोफ़ोन आदि) कब और किसकी ओर से इस्तेमाल किया।
एट्रिब्यूशन टैग
वह लेबल जो ऐप अपने मैनिफ़ेस्ट में <attribution android:tag=...> से घोषित करता है, ताकि डेटा एक्सेस को ऐप की किसी सुविधा से जोड़ा जा सके। प्लेटफ़ॉर्म सीमित करता है कि एक पैकेज कितने घोषित कर सकता है (MAX_NUM_ATTRIBUTIONS)।
प्रॉक्सी ऑपरेशन
किसी दूसरे ऐप की ओर से दर्ज किया गया app-op, जैसे AttributionSource चेन में। प्रॉक्सी का uid, पैकेज और एट्रिब्यूशन टैग एक्सेस रिकॉर्ड के साथ सहेजे जाते हैं।
appops_accesses.xml
/data/system की वह फ़ाइल जिसमें AppOpsService एक्सेस रिकॉर्ड सहेजता है; scheduleWriteLocked() इसे दोबारा लिखता है और बूट के समय इसे फिर पढ़ा जाता है।

मूल कारण विश्लेषण (Root Cause)

AppOpsService.java में noteOperationUnchecked() और startOperationUnchecked() कॉलर के नियंत्रण वाले proxyAttributionTag को सीधे AttributedOp.accessed() को दे देते थे, यह जाँचे बिना कि प्रॉक्सी ऐप ने वह टैग घोषित किया है या नहीं। हर अलग स्ट्रिंग एक और प्रॉक्सी रिकॉर्ड बनाती थी जो मेमोरी में रहता और /data/system/appops_accesses.xml में लिखा जाता, इसलिए एक ही ऐप फ़ाइल को तब तक फुला सकता था जब तक system_server की मेमोरी और I/O खत्म न हो जाए। सुधार उस हर टैग को null कर देता है जो isAttributionTagDefined() को दोनों में से किसी पैकेज के मैनिफ़ेस्ट में न मिले, और MAX_NUM_ATTRIBUTIONS को 10,000 से घटाकर 1,000 कर देता है।

हमले का चरण-दर-चरण प्रवाह

चरण 1

साधारण ऐप प्रॉक्सी बनता है

बिना किसी विशेष अनुमति वाला इंस्टॉल ऐप किसी दूसरे पैकेज की ओर से app-op दर्ज करता है और AppOpsManager के ज़रिए अपना प्रॉक्सी एट्रिब्यूशन टैग भेजता है।

चरण 2

हर कॉल पर नया टैग

ऐप हर कॉल पर एक नई, अघोषित टैग स्ट्रिंग बनाता है। noteOperationUnchecked() हर एक को बिना बदले स्वीकार कर लेता है।

चरण 3

स्टेट असीमित बढ़ती है

AttributedOp.accessed() हर अलग प्रॉक्सी टैग के लिए एक रिकॉर्ड सहेजता है और scheduleWriteLocked() बढ़ते सेट को बार-बार appops_accesses.xml में लिखता रहता है।

चरण 4

लोकल डिनायल ऑफ़ सर्विस

system_server फ़ाइल को सीरियलाइज़ करने और दोबारा पढ़ने में लगातार ज़्यादा मेमोरी और डिस्क I/O खर्च करता है, जिससे डिवाइस धीमा हो जाता है या अटक जाता है, रीबूट के बाद भी।

सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन

कमज़ोर कार्यान्वयन
// 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;

इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट

स्रोत