flawopen.com/Teardowns/cve-2024-23712-android-appops-attribution-tag-dos
CVE-2024-23712: исчерпание ресурсов через теги атрибуции в AppOps Android
CVE-2024-23712 (CVSS 5.5; Android оценивает как высокую): AppOpsService записывал любой тег атрибуции прокси, переданный вызывающим приложением, поэтому обычное приложение без дополнительных разрешений могло бесконечно раздувать /data/system/appops_accesses.xml и вызвать локальный отказ в обслуживании.
На ресепшене офисного здания ведут журнал посетителей — по странице на каждый отдел, — и каждый курьер заполняет графу «сопровождает отдел». Никто не сверяет эту графу со списком настоящих отделов. Один курьер при каждой доставке пишет новое выдуманное название отдела, тысячи раз в день. Журнал заводит новую страницу под каждое название и каждую ночь целиком переписывается в архив, пока ресепшен не начинает заниматься только переписыванием. Исправление: оставлять графу пустой, если названия нет в официальном списке, и ограничить число отделов, которые может зарегистрировать одна компания.
Ключевые понятия и термины
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.
Пошаговый сценарий атаки
Обычное приложение выступает прокси
Установленное приложение без особых разрешений фиксирует app-ops от имени другого пакета и передаёт через AppOpsManager собственный тег атрибуции прокси.
Новый тег при каждом вызове
При каждом вызове приложение генерирует новую необъявленную строку тега. noteOperationUnchecked() принимает их все без изменений.
Состояние растёт без границ
AttributedOp сохраняет запись для каждого нового прокси-тега, а .accessed()scheduleWriteLocked() снова и снова записывает растущий набор в appops_accesses.xml.
Локальный отказ в обслуживании
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;
Чек-лист по защите системы для инженеров
- ✓Выпускайте устройства с уровнем исправлений безопасности 2024-04-01 или новее — он включает коммит AOSP 6beb68ca.
- ✓В любом системном сервисе проверяйте переданный клиентом идентификатор по серверному реестру (здесь — манифесту пакета), прежде чем использовать его как ключ хранимого состояния.
- ✓Задайте жёсткий предел записей на клиента в каждой сохраняемой структуре; патч снизил
MAX_NUM_ATTRIBUTIONSс 10 000 до 1 000. - ✓Добавьте тест, который отправляет тысячи уникальных ключей, выбранных клиентом, и проверяет, что хранимое состояние и файл не превышают фиксированного размера.
- ✓В приложениях объявляйте каждый тег атрибуции в манифесте и создавайте контексты через
Contextтолько для объявленных тегов; необъявленные прокси-теги теперь отбрасываются..createAttributionContext()