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

● CVE-2024-23712 · CVSS 5.5 · Средний
Исследования · FlawOpen

CVE-2024-23712: исчерпание ресурсов через теги атрибуции в AppOps Android

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() перезаписывает его, а при загрузке он читается заново.

Анализ первопричины

noteOperationUnchecked() и startOperationUnchecked() в AppOpsService.java передавали контролируемый вызывающим proxyAttributionTag прямо в AttributedOp.accessed(), не проверяя, объявило ли прокси-приложение такой тег. Каждая новая строка порождала ещё одну прокси-запись, которая оставалась в памяти и записывалась в /data/system/appops_accesses.xml, так что одно приложение могло раздувать файл, пока system_server не исчерпает память и ввод-вывод. Исправление обнуляет любой тег, который isAttributionTagDefined() не находит в манифестах обоих пакетов, и снижает MAX_NUM_ATTRIBUTIONS с 10 000 до 1 000.

Пошаговый сценарий атаки

Шаг 1

Обычное приложение выступает прокси

Установленное приложение без особых разрешений фиксирует app-ops от имени другого пакета и передаёт через AppOpsManager собственный тег атрибуции прокси.

Шаг 2

Новый тег при каждом вызове

При каждом вызове приложение генерирует новую необъявленную строку тега. noteOperationUnchecked() принимает их все без изменений.

Шаг 3

Состояние растёт без границ

AttributedOp.accessed() сохраняет запись для каждого нового прокси-тега, а scheduleWriteLocked() снова и снова записывает растущий набор в appops_accesses.xml.

Шаг 4

Локальный отказ в обслуживании

system_server тратит всё больше памяти и дискового ввода-вывода на сериализацию и повторный разбор файла; устройство тормозит или зависает, в том числе после перезагрузки.

Исходный код: Уязвимый vs Защищённый вариант

УЯЗВИМАЯ РЕАЛИЗАЦИЯ
// 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;

Чек-лист по защите системы для инженеров

Источники