此问题的修复已作为 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() 调用执行
回到 updateApplicationInfo() (代码片段来源)```java
final List newPaths = new ArrayList<>();
makePaths(mActivityThread, aInfo, newPaths);
final List addedPaths = new ArrayList<>(newPaths.size());
// Snip: populate addedPaths with items that are in newPaths and not in oldPaths (passed in argument) synchronized (mLock) { createOrUpdateClassLoaderLocked(addedPaths);
`addedPaths` 列表的构建方式如下:在安装新的 split 时,`oldPaths` 会包含安装新 split 之前所使用的路径列表,而 `addedPaths` 会包含需要添加到现有 `ClassLoader` 的 `.apk` 文件列表;然而,在 `checkAndUpdateApkPaths()` 的情况下,`oldPaths` 将由完全相同的 `ApplicationInfo` 对象构建,因此 `addedPaths` 将为空,攻击者无法在此处向现有 `ClassLoader` 添加新路径
`createOrUpdateClassLoaderLocked()` 是一个较长的方法,但这里只有少数几件事值得关注:
* [如果 `mIncludeCode` 为 `false`,唯一做的事情就是创建 `ClassLoader`,而不引用任何 `apk`/`dex` 文件](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=968-990;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)
* 最危险的事情是使用刚刚通过攻击者控制的 `ApplicationInfo` 设置的路径来创建新的 `ClassLoader`,但这只会在 [`mDefaultClassLoader` 为 `null`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1005;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1) 时发生,这意味着这是该 `LoadedApk` 实例上第一次调用 `createOrUpdateClassLoaderLocked()`(`mDefaultClassLoader` 字段指的是在应用 [`AppComponentFactory.instantiateClassLoader()`](https://developer.android.com/reference/android/app/AppComponentFactory#instantiateClassLoader(java.lang.ClassLoader,%20android.content.pm.ApplicationInfo)) 之前所使用的 `ClassLoader` 实例)
* 还有[向该 `ClassLoader` 的原生库搜索路径添加新路径](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1050-1058;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1),但要利用这一点,受害应用需要执行 `System.loadLibrary()`,而这通常并不存在
* 还有[将 `addedPaths` 参数中的 `apk`/`dex` 路径添加到现有 `ClassLoader`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1060-1065;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1),然而在来自 `RemoteViews` 的代码路径上,`addedPaths` 将为空
再次回到 `updateApplicationInfo()`,该方法仍然做了一件相关的事情,即[用使用新 `mResDir` 值的实例替换 `LoadedApk.mResources`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=382;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)。需要注意的是,这是一个新实例,而不是更新已经存在的实例。新创建的 `Context` 将返回/使用这个新的 `Resources` 实例,但任何已经创建的 `Context` 将继续使用旧的 `Resources`
# 影响总结
总结以上各节,每当受害进程应用 `RemoteViews` 时,此漏洞允许:
* 替换该进程内新创建的 Activity 的 [`Resources`(本地化字符串、布局等)](https://developer.android.com/guide/topics/resources/providing-resources)
* 追加 `System.loadLibrary()` 所使用的原生库搜索路径,但要利用这一点需要受害应用调用 `System.loadLibrary()` 并传入通常不存在的库名称,这不太可能发生
* 如果受害进程使用了 `createPackageContext(CONTEXT_INCLUDE_CODE)`,但没有在该 `Context` 上调用 [`getClassLoader()`](https://developer.android.com/reference/android/content/Context#getClassLoader()),则可以加载任意 Java 代码;然而,由于加载代码正是使用 `CONTEXT_INCLUDE_CODE` 标志的原因,这种情况自然发生的可能性也很低
现在,虽然在这种情况下加载 Java 代码的机会不太可能自然发生,但我能够触发它
# 加载 `WebView`
在现代 Android 版本中,`WebView` 不是系统的一部分,而是从[系统配置](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/res/res/xml/config_webview_packages.xml)中定义的普通 `apk` 加载的,并且[要么是系统应用,要么其签名与系统内定义的签名匹配](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java;l=688-705;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928)
该 `apk` 的加载是通过[创建新的 `Context`,并传递 `Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY` 标志](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/webkit/WebViewFactory.java;l=521;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928)来完成的
现在让我们看一下该方法的调用者 [(代码片段来源)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/webkit/WebViewFactory.java;l=541-563;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928)```java
// Overall snip: try/catch/finally, Trace, logging and timing measurement
webViewContext = getWebViewContextAndSetProvider();
if (android.content.res.Flags.registerResourcePaths()) {
Resources.registerResourcePaths(webViewContext.getPackageName(),
webViewContext.getApplicationInfo());
} else {
// Snip: old resource update path, won't be used on latest Android builds
}
ClassLoader clazzLoader = webViewContext.getClassLoader();
getWebViewContextAndSetProvider() 是创建 Context 的方法,Resources.registerResourcePaths() 是我们唯一的机会来引入延迟,以便在另一个线程中调用 checkAndUpdateApkPaths() 并加载任意 Java 代码,因为 webViewContext.getClassLoader() 是窗口的终点,此时 LoadedApk 的 mIncludeCode 为 true 且 mDefaultClassLoader 为 null
Resources.registerResourcePaths() 会到达 appendLibAssetsLocked(),该方法会遍历单例的 mResourceImpls 字段,其中包含已加载到该进程的所有资源,因此可能包含使用通过 RemoteViews 植入的 ApplicationInfo 构建的资源
我最初的想法是在 ApplicationInfo 中放入大量具有相同 hashCode() 的 overlay 路径,这样通过 createNewResourceKeyIfNeeded() 调用去重时会很慢,虽然在使用解释器时(例如使用调试器或在刚安装的应用中)这是一个显著的延迟,但当运行时完成优化后,哈希冲突引入的延迟并不显著,然而出现了另一个减速原因:由于这些 overlay 路径没有指向现有文件,对于每个加载失败的 overlay,都会打印一条带有堆栈跟踪的日志消息,这实际上确实导致了减速,使得利用这个竞态条件变得可行
应用可以在通知中将 RemoteViews 传递给 SystemUI。我的利用应用请求 POST_NOTIFICATIONS 权限,我认为这是合理的用户交互,尽管某些通知可能可以绕过该要求
通常 SystemUI 不使用 WebView,事实上它甚至无法使用,因为 SystemUI 使用设备受保护存储(即不受锁屏凭据保护的存储),而 WebView 实现不允许这样做
不过这个利用允许我修改 Resources,所以我首先修改了SlicePermissionActivity 使用的布局,使其包含 <WebView /> 元素,然后启动该 Activity,这会在 SystemUI 主线程上触发 WebView 初始化
然后我必须在另一个线程上调用 RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths(),以便替换用于加载代码的路径
通常发布通知涉及 SystemUI 进程中的多个线程,包括主线程(因此在 WebView 初始化期间我无法发布另一个通知),然而 RemoteViews 的应用发生在单独的线程上,我可以通过在 RemoteViews 中包含 ImageView 并要求它从我的 ContentProvider 加载图像来挂起该操作
这个利用通过替换 SlicePermissionActivity 中使用的布局来加载 WebView。如果无法更新代码,攻击 SlicePermissionActivity 仍然有价值,因为攻击者可以替换布局以完全隐藏原始消息,例如显示更新日志,将允许按钮替换为“知道了”,并将拒绝按钮替换为空字符串使其实际上不可见。虽然 Slices 框架已被弃用,但仍然存在设置切片,例如可以在无需用户交互的情况下更改移动数据设置
SystemUI 中另一个重要的权限提示是 Media Projection 确认,但该提示恰好不易受攻击,因为它为 Dialog 使用应用上下文
Preference 片段也很有趣,因为你可以定义由 Preference 启动的 Intent,但在 SystemUI 的情况下,只有与 SystemUI Tuner 和 Demo Mode 相关的 Preference Activity,但这些在与显示通知不同的进程中运行