此问题的修复已作为 CVE-2025-22441 发布:公告 补丁 后续修复
ApplicationInfoApplicationInfo 是定义已安装应用各种信息(最显著的是加载资源和代码的 apk 文件路径)的结构体
通常它由系统传递给应用,但有时也存在非系统调用方可以提供自己的 ApplicationInfo 的情况。例如,过去 bindBackupAgent() 方法中就存在漏洞,攻击者可以在参数中传入自己的 ApplicationInfo 对象,其中包含 uid 和 sourceDir 值,而这些值并未与系统中实际安装的应用进行核对,因为该方法本应由 system_server 内部调用,却暴露给了 adb shell
不过这次,我仔细研究了 RemoteViews 中的 ApplicationInfo 字段
RemoteViews 是描述可来自另一个进程的视图的对象。最显著的是它用于主屏幕小部件,提供小部件的应用构建 RemoteViews,然后由主屏幕进程进行"应用"
使用 RemoteViews 的其他场景包括通知(由 SystemUI 进程应用)和自动填充对话框(由自动填充服务提供,由 system_server 应用)
RemoteViews.mApplication 字段通过 Parcel 进行序列化,因此可能来自远程进程,并且每当应用 RemoteViews 时,它会被以下方法使用 (代码片段来源):```java
private Context getContextForResourcesEnsuringCorrectCachedApkPaths(Context context) {
if (mApplication != null) {
if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
&& context.getPackageName().equals(mApplication.packageName)) {
return context;
}
try {
LoadedApk.checkAndUpdateApkPaths(mApplication);
return context.createApplicationContext(mApplication,
Context.CONTEXT_RESTRICTED);
} catch (NameNotFoundException e) {
Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found");
}
}
return context;
}
这里最有趣的是 `LoadedApk.checkAndUpdateApkPaths()` 调用,因为这是一个静态方法,会修改一些全局状态 [(代码片段来源)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=2275-2302;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)```java
public static void checkAndUpdateApkPaths(ApplicationInfo expectedAppInfo) {
// Get the LoadedApk from the cache
ActivityThread activityThread = ActivityThread.currentActivityThread();
if (activityThread == null) {
Log.e(TAG, "Cannot find activity thread");
return;
}
checkAndUpdateApkPaths(activityThread, expectedAppInfo, /* cacheWithCode */ true);
checkAndUpdateApkPaths(activityThread, expectedAppInfo, /* cacheWithCode */ false);
}
private static void checkAndUpdateApkPaths(ActivityThread activityThread,
ApplicationInfo expectedAppInfo, boolean cacheWithCode) {
String expectedCodePath = expectedAppInfo.getCodePath();
LoadedApk loadedApk = activityThread.peekPackageInfo(
expectedAppInfo.packageName, /* includeCode= */ cacheWithCode);
// If there is load apk cached, or if the cache is valid, don't do anything.
if (loadedApk == null || loadedApk.getApplicationInfo() == null
|| loadedApk.getApplicationInfo().getCodePath().equals(expectedCodePath)) {
return;
}
// Duplicate framework logic
List<String> oldPaths = new ArrayList<>();
LoadedApk.makePaths(activityThread, expectedAppInfo, oldPaths);
// Force update the LoadedApk instance, which should update the reference in the cache
loadedApk.updateApplicationInfo(expectedAppInfo, oldPaths);
}
让我们来讨论一下这些方法中发生了什么
首先,我们调用三参数的 checkAndUpdateApkPaths,其中 cacheWithCode 分别被设置为 true 和 false。LoadedApk 对象可以以两种模式构造,要么 mIncludeCode 为 true,要么为 false,在 SDK 层面这对应着 Context 是否设置了 CONTEXT_INCLUDE_CODE 标志
该方法首先会使用 ActivityThread.peekPackageInfo(),它会返回已缓存的 LoadedApk 实例,因此如果应用之前没有以匹配的 packageName 和 includeCode 构造过 LoadedApk,peekPackageInfo() 将返回 null,而 checkAndUpdateApkPaths() 不会执行任何操作
再回看 RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths(),那里有 context.createApplicationContext(mApplication, Context.CONTEXT_RESTRICTED),其中没有指定 CONTEXT_INCLUDE_CODE 标志(传入该方法的上下文参数也是如此),因此该方法将始终使用 mIncludeCode=false 的 LoadedApk
因此,对于 RemoteViews 来说,更新带代码的版本是不必要的,因为 RemoteViews 始终使用不带代码的 Context。提交信息只是说有两个地方可能缓存了 ApplicationInfo,我认为移除带代码的版本更新在这里不会引起问题(因为 checkAndUpdateApkPaths() 仅被 AppWidgetHostView 和 RemoteViews 使用)。不过,反对这一点的理由是,要避免各种缓存不同步的可能性,而且虽然移除 cacheWithCode=true 的调用消除了此漏洞最严重的影响,但它并不能完全修复问题,因为仅修改资源(而非代码)对攻击者来说可能仍然有价值
LoadedApk.updateApplicationInfo()到目前为止展示的代码仅用于 RemoteViews(小部件、通知等),但现在我们将进入 LoadedApk.updateApplicationInfo(),该方法也用于在安装新的 split 之后更新正在运行的应用进程(例如在使用 Play Feature Delivery 按需交付时)
现在,来自 RemoteViews 的不可信 ApplicationInfo 对象将被传递给 updateApplicationInfo(),让我们看看该方法做了什么 (代码片段来源)```java
public void updateApplicationInfo(@NonNull ApplicationInfo aInfo,
@Nullable List oldPaths) {
if (!setApplicationInfo(aInfo)) {
return;
}
我们来看一下这个 `setApplicationInfo()` [(代码片段来源)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=392-422;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)```java
private boolean setApplicationInfo(ApplicationInfo aInfo) {
if (mApplicationInfo != null && mApplicationInfo.createTimestamp > aInfo.createTimestamp) {
Slog.w(TAG, "New application info for package " + aInfo.packageName
+ " is out of date with TS " + aInfo.createTimestamp + " < the current TS "
+ mApplicationInfo.createTimestamp);
return false;
}
// Snip: assign fields such as mAppDir and mResDir on this object from aInfo
return true;
}
createTimestamp 字段通常被设置为 SystemClock.uptimeMillis(),但由于该对象来自攻击者,这意味着攻击者可以提供未来的值,从而阻止后续的 updateApplicationInfo() 调用执行