flawopen.com/Teardowns/cve-2024-23712-android-appops-attribution-tag-dos
CVE-2024-23712:Android AppOps 归因标签导致的资源耗尽
CVE-2024-23712(CVSS 5.5;Android 评为高危):AppOpsService 会记录调用方应用提供的任意代理归因标签,因此普通应用无需额外权限就能让 /data/system/appops_accesses.xml 无限增长,造成本地拒绝服务。
一栋写字楼的前台有一本访客登记簿,每个部门占一页,每位快递员都要填写“陪同部门”一栏。没人拿这一栏去核对大楼的正式部门名单。有个快递员每送一次就写一个新编的部门名,一天几千次。登记簿为每个名字新开一页,每晚还要整本誊抄进档案,最后前台整天都在抄页。修复办法是:名字不在正式名单上,这一栏就留空;并且限制每家公司最多能登记多少个部门。
核心概念与专有名词
AppOpsServicesystem_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。修复会把 isAttributionTagDefined() 在两个软件包清单中都找不到的标签置为 null,并把 MAX_NUM_ATTRIBUTIONS 从 10,000 降到 1,000。
攻击执行流程分解
普通应用充当代理
一个没有特殊权限的已安装应用代表另一个软件包记录 app-op,并通过 AppOpsManager 传入自己的代理归因标签。
每次调用都用新标签
应用每次调用都生成一个新的、未声明的标签字符串,noteOperationUnchecked() 原样全部接受。
状态无限增长
AttributedOp 为每个不同的代理标签保存一条记录,.accessed()scheduleWriteLocked() 不断把越来越大的集合写入 appops_accesses.xml。
本地拒绝服务
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;
工程与系统安全加固清单
- ✓出厂设备的安全补丁级别应为 2024-04-01 或更高,其中包含 AOSP 提交 6beb68ca。
- ✓在任何系统服务中,把客户端提供的标识符用作存储状态的键之前,先与服务端登记信息(这里是软件包清单)核对。
- ✓为每个持久化结构设置每个客户端的条目硬上限;该补丁把
MAX_NUM_ATTRIBUTIONS从 10,000 降到 1,000。 - ✓编写测试:提交数千个由客户端选择的唯一键,断言存储状态和持久化文件保持在固定大小以内。
- ✓在应用中,把每个归因标签都在清单中声明,并且只为已声明的标签调用
Context创建上下文;未声明的代理标签现在会被丢弃。.createAttributionContext()