Writeup and exploit for installed app to system privilege escalation on Android 12 Beta through CVE-2021-0928, a `writeToParcel`/`createFromParcel` serialization mismatch in `OutputConfiguration`

Overview

CVE-2021-0928, writeToParcel/createFromParcel serialization mismatch in android.hardware.camera2.params.OutputConfiguration

This is exploit using that vulnerability for privilege escalation from installed Android app into Android Settings app (or any other app installed app could send to declared in AndroidManifest.xml, privilege escalation by sending to was possible too, although not presented here)

I've found issue originally on Android 12 Developer Preview 3

Exploit version present in this repo works on Android 12 Beta 2 and 3

Vulnerability was fixed in first official Android 12 release

Writeup below was originally written for Google for consideration of this report as complete exploit chain

At time of writing Android 12 was not available in AOSP (Android Developer Preview/Beta releases are not open source)

Screenshot of Android notification from Settings app: Hello from uid=1000(system) gid=1000(system) groups=1000(system),1007(log),1065(reserved_disk),1077(external_storage),3001(net_bt_admin),3002(net_bt),3003(inet),3007(net_bw_acct),9997(everybody) context=u:r:system_app:s0

Introduction to Parcel

Most of IPC on Android is done through class called Parcel

Basic usage of Parcel is as following:

Parcel p = Parcel.obtain();
p.writeInt(1);
p.writeString("Hello");

Then Parcel is sent to another process through Binder. Alternatively for testing one can call p.setDataPosition(0) to rewind parcel to beginning position and start reading:

int a = p.readInt(); // a = 1
String b = p.readString(); // b = "Hello"

It should be noted that parcel internally holds position from which reads are performed. It it responsibility of Parcel class user to ensure that read* methods match previously used write* methods, otherwise subsequent reads will be from wrong positions in buffer

Parcel also provides ability to write custom objects, preferred way to do so is by implementing Parcelable interface

Here is example implementation of Parcelable interface (irrelevant code removed, WindowContainerTransaction class is used in exploit as part of gadget chain, however there isn't anything wrong with it)

package android.window;
public final class WindowContainerTransaction implements Parcelable {
    private final ArrayMap<IBinder, Change> mChanges = new ArrayMap<>();
    private final ArrayList<HierarchyOp> mHierarchyOps = new ArrayList<>();

    private WindowContainerTransaction(Parcel in) {
        in.readMap(mChanges, null /* loader */);
        in.readList(mHierarchyOps, null /* loader */);
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeMap(mChanges);
        dest.writeList(mHierarchyOps);
    }

    @NonNull
    public static final Creator<WindowContainerTransaction> CREATOR =
            new Creator<WindowContainerTransaction>() {
                @Override
                public WindowContainerTransaction createFromParcel(Parcel in) {
                    return new WindowContainerTransaction(in);
                }
            };
}

As can be seen above, writeToParcel() method is used during writing. Then while reading CREATOR.createFromParcel() factory method is called. It is responsibility of Parcelable implementation to ensure that createFromParcel reads same amount of data as was written by writeToParcel, otherwise all subsequent reads from that Parcel will read data from wrong offset

Such class can be written to/read from Parcel through:

  • Directly calling obj.writeToParcel(parcel, 0) / obj = WindowContainerTransaction.CREATOR.createFromParcel(), this is often used when type of class is known, for example when Parcelable has field with different Parcelable or in code generated by AIDL when defined RPC method has Parcelable as argument
  • Through Parcel.writeParcelable/readParcelable. writeParcelable first writes name of class and then calls writeToParcel method from Parcelable interface. readParcelable reads written class name, finds class with that name in provided ClassLoader or BOOTCLASSPATH if null was provided. Once class is found it's static field CREATOR is used to obtain Parcelable.Creator instance which is factory used to read that class. It is important to note that when readParcelable method is used it can read any Parcelable available in class path as name of object to be created is read from same Parcel
  • readParcelable is used by many other Parcel methods, for example readList seen in example above reads elements through readValue, which is most generic method of transferring objects in Parcel and one of ways it uses is through readParcelable. Also in above example due to Java's Type Erasure ArrayList mHierarchyOps field can actually contain any objects supported by Parcel, not only those compatible with type specified in generic type declaration

writeToParcel/createFromParcel mismatches

As noted above it is responsibility of Parcelable interface implementation to ensure that createFromParcel reads same amount of data from Parcel as matching writeToParcel has previously written. Whenever there is in BOOTCLASSPATH a Parcelable which can violate that contract it creates a vulnerability as it allows for following scenario:

  1. An evil application sends to system_server a Bundle OR Parcelable containing faulty Parcelable instance along with specifically constructed data that will be actually read in step 3 but passed verbatim during step 2
  2. system_server verifies Bundle is safe and then forwards it OR system_server passes provided Parcelable to AIDL method that also has critical data passed in next parameter (if data received in that parameter could be modified that would cause security issue)
  3. Another app receives data from system_server and trusts it, however due to faulty serialization data that it actually sees differs from data system_server intended to send

I've used "OR" in above steps as these steps describe both an old exploit variant which leads to starting arbitrary Activity which I've published in 2017 (on the left side of "OR") and a new variant which I'll describe here in next section

How BroadcastReceiver is executed in app

From the point of application developer using APIs available in Android SDK the way BroadcastReceiver works is that one application calls sendBroadcast (although often apps want to receive Broadcasts from system, not app) and then broadcasted Intent is matched to defined in AndroidManifest.xml, when that happens system starts process of receiving application, instantiates BroadcastReceiver subclass as defined in attribute and then calls onReceive method

Let's take a look at communication with system_server happening in process receiving broadcast:

  • When application process is initially started it calls IActivityManager.attachApplication(), by doing so it passes IApplicationThread handle which is used by system to tell application process what to do
  • When system wants to execute manifest-registered BroadcastReceiver in application process, it calls scheduleReceiver method using IApplicationThread described in previous point. This method has multiple arguments but here most important ones are first two:
    1. Intent intent, which was previously passed to system when sendBroadcast() was called
    2. ActivityInfo info, which contains information about component that has to be executed. Value to this parameter is taken by system from Package Manager Service. Most importantly data passed in this parameter includes path to file from which Java class handling received broadcast will be loaded

At this point you probably can guess what this new exploit path is: call sendBroadcast() passing an Intent that will cause that when system tries to call scheduleReceiver it'll cause that application in which scheduleReceiver is invoked will see tampered ActivityInfo

It should be noted that this new exploit path became viable in Android 12 as previously there was no way to put arbitrary Parcelables in Intent (Intent extras don't count as they are put into Bundle which has its whole length written into Parcel and is read as single blob, so extras cannot cause misinterpretation of Intent object containing them)

Triggering writeToParcel/createFromParcel mismatch

Most of the time writeToParcel/createFromParcel mismatches are cases where in one of these methods one of the fields is forgotten or written twice, in such case sending such object will always trigger mismatch. (Most of the time that happens when object while Parcelable, isn't really used across processes, otherwise that would be quickly noticed during normal usage)

This time however that wasn't the case and triggering mismatch isn't obvious

Let's take a look at vulnerable class (original was here, lines marked // New in Android 12 were manually added as they weren't present in AOSP at time of writing) (Here's commit originally introducing vulnerability, however it was published after Android 12 was released)

mSurfaces; private final int mRotation; private final int mSurfaceGroupId; private final int mSurfaceType; private final Size mConfiguredSize; private final int mConfiguredFormat; private final int mConfiguredDataspace; private final int mConfiguredGenerationId; private final boolean mIsDeferredConfig; private boolean mIsShared; private String mPhysicalCameraId; private boolean mIsMultiResolution; // New in Android 12 private ArrayList mSensorPixelModesUsed; // New in Android 12 }">
package android.hardware.camera2.params;

public final class OutputConfiguration implements Parcelable {
    private OutputConfiguration(@NonNull Parcel source) {
        int rotation = source.readInt();
        int surfaceSetId = source.readInt();
        int surfaceType = source.readInt();
        int width = source.readInt();
        int height = source.readInt();
        boolean isDeferred = source.readInt() == 1;
        boolean isShared = source.readInt() == 1;
        ArrayList<Surface> surfaces = new ArrayList<Surface>();
        source.readTypedList(surfaces, Surface.CREATOR);
        String physicalCameraId = source.readString();
        boolean isMultiResolution = source.readInt() == 1; // New in Android 12
        ArrayList<Integer> sensorPixelModesUsed = new ArrayList<Integer>(); // New in Android 12
        source.readList(sensorPixelModesUsed, Integer.class.getClassLoader()); // New in Android 12

		// SNIP: copy values from variables set above to fields of this class
    }

    public static final @android.annotation.NonNull Parcelable.Creator<OutputConfiguration> CREATOR =
            new Parcelable.Creator<OutputConfiguration>() {
        @Override
        public OutputConfiguration createFromParcel(Parcel source) {
            try {
                OutputConfiguration outputConfiguration = new OutputConfiguration(source);
                return outputConfiguration;
            } catch (Exception e) {
                Log.e(TAG, "Exception creating OutputConfiguration from parcel", e);
                return null;
            }
        }

        @Override
        public OutputConfiguration[] newArray(int size) {
            return new OutputConfiguration[size];
        }
    };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        if (dest == null) {
            throw new IllegalArgumentException("dest must not be null");
        }
        dest.writeInt(mRotation);
        dest.writeInt(mSurfaceGroupId);
        dest.writeInt(mSurfaceType);
        dest.writeInt(mConfiguredSize.getWidth());
        dest.writeInt(mConfiguredSize.getHeight());
        dest.writeInt(mIsDeferredConfig ? 1 : 0);
        dest.writeInt(mIsShared ? 1 : 0);
        dest.writeTypedList(mSurfaces);
        dest.writeString(mPhysicalCameraId);
        dest.writeInt(mIsMultiResolution ? 1 : 0); // New in Android 12
        dest.writeList(mSensorPixelModesUsed); // New in Android 12
    }

    private ArrayList<Surface> mSurfaces;
    private final int mRotation;
    private final int mSurfaceGroupId;
    private final int mSurfaceType;
    private final Size mConfiguredSize;
    private final int mConfiguredFormat;
    private final int mConfiguredDataspace;
    private final int mConfiguredGenerationId;
    private final boolean mIsDeferredConfig;
    private boolean mIsShared;
    private String mPhysicalCameraId;
    private boolean mIsMultiResolution; // New in Android 12
    private ArrayList<Integer> mSensorPixelModesUsed; // New in Android 12
}

So whats wrong here and why this change introduces vulnerability? As I've said while discussing WindowContainerTransaction example Parcelable, readList can actually fill list with any object supported by Parcel, not only ones matching generic declaration (ArrayList ). However since we're just using this class as part of serialization gadget chain and not actually using it that field won't be used for anything else than reading and writing to Parcel (and attempts to use elements from ArrayList containing elements mismatching its generic declaration would only lead to ClassCastException anyway), that isn't problem in itself

In this class there's also a try-catch within createFromParcel, which means that if during read an Exception is thrown, reading of OutputConfiguration will be stopped and reading of object containing OutputConfiguration will proceed. When that happens whole OutputConfiguration will be written to Parcel but it'll be read only to point at which Exception happened. This creates mismatch as unconsumed data written within OutputConfiguration.writeToParcel will actually be read by object that was calling OutputConfiguration.CREATOR.createFromParcel

Now, combination of these two (allowing nesting arbitrary objects supported by Parcel and wrapping that within try-catch without rethrow) gives ability to construct Parcelable that can be written by system_server and later be read in a way that is controlled by app that initially constructed Parcelable being forwarded by system_server

Ok, so in order to construct such Parcelable now we need to find something to be put in mSensorPixelModesUsed that will be successfully read in system_server (as this object is being received from attacker app through Parcel), successfully written by system_server and then will fail to unparcel and throw an Exception in victim app

One of ways to do so is to use class that is present within system_server but not in apps, so that attempting to deserialize it would lead to ClassNotFoundException. I cannot however pick a Parcelable from system_server as readParcelable without explicitly specified ClassLoader will only search BOOTCLASSPATH which won't contain system_server specific classes. Solution to that problem is to use one of Serializable classes as ObjectInputStream will pick ClassLoader from first non-BOOTCLASSPATH method in stack trace

I've picked PackageManagerException, however before we use it there's one more thing we need to do. In OutputConfiguration constructor when readList is called loader argument is explicitly set to Integer.class.getClassLoader(). That loader value is propagated to readValue(), then to readSerializable() and within readSerializable() if loader parameter isn't null it is used instead of resolveClass from ObjectInputStream (that c != null check does nothing because when Class.forName doesn't find class it'll throw exception instead of returning null). The way around is quite simple though, we just need to wrap PackageManagerException in some Parcelable that does readList without specifying ClassLoader. This is where described above WindowContainerTransaction class comes in

So, at this point we have following object:

  • OutputConfiguration
    • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
      • mHierarchyOps.get(0) = PackageManagerException

Now such object can successfully deserialized within system_server: when WindowContainerTransaction calls readList it'll try finding PackageManagerException class using system servers ClassLoader (not BootClassLoader), as it can find it in stack trace. That class loader happens to be present within stack trace because while all of following methods weren't from system server class path: Binder#execTransact(), IActivityManager$Stub#onTransact() generated by AIDL and methods from all used Parcelable classes, there was method declared within system server in stack trace: an overridden onTransact within ActivityManagerService. Therefore system_server can read and later write such object to Parcel and when target app attempts reading it PackageManagerException class won't be available and therefore ClassNotFoundException will be thrown, wrapped into RuntimeException and then caught by OutputConfiguration CREATOR

So we triggered this mismatch. Well, not really yet at this point because exception was caught when no unread data was left by OutputConfiguration.writeToParcel, but we can easily add another item to mSensorPixelModesUsed List and that item will be written through Parcel.writeValue and left unread after reading OutputConfiguration

Putting that in Intent

As noted above I'll want to trigger mismatch from Intent object, as it will be passed by system_server to an AIDL method which has Intent in first parameter and execution information in second parameter, so that serialization/deserialization of Intent passed in first parameter lead to modification of value in second parameter

In Intent.readFromParcel() all values are read through dedicated typed methods so there we cannot specify custom Parcelable class

Within Intent though, there's nested ClipData and since Android 12 in ClipData$Item there's a new field ActivityInfo mActivityInfo (was not present in AOSP at time of initial writing, here's commit introducing that field, this field is read through in.readTypedObject(ActivityInfo.CREATOR) inside ClipData(Parcel in) constructor)

Then within ActivityInfo(Parcel source) constructor again there isn't way to put custom Parcelable, but as ActivityInfo extends from ComponentInfo it has applicationInfo field

Finally within ApplicationInfo there's SparseArray splitDependencies field, which is read through readSparseArray, which in turn uses readValue to read SparseArray items

At this point we could place OutputConfiguration within splitDependencies, however reading splitDependencies is followed by few readString8() calls and it'd be nice to have full control over unconsumed data after mismatch happens so we can directly place empty strings there and not worry about different interpretation of unconsumed data

To do so, first we need to put some raw data container within OutputConfiguration.mSensorPixelModesUsed that will be written through writeValue. I've chosen Bundle. That way in unconsumed data we'll have left:

  1. writeValue VAL_BUNDLE tag
  2. Length of raw data (this link also applies to remaining items in this list)
  3. BUNDLE_MAGIC
  4. Raw data passed verbatim through Parcel.appendFrom

So we have three Parcel.writeInt items we'd have unconsumed, we can get rid of them by wrapping OutputConfiguration within some Parcelable that while reading it reads arbitrary Parcelable value followed by three ints. I've found that in ZenPolicy CREATOR

To sum up we've got following object hierarchy (that is present in system_server and which it attempts passing to scheduleReceiver)

  • Intent
    • mClipData = ClipData
      • mItems.get(0).mActivityInfo = ActivityInfo
        • applicationInfo = ApplicationInfo
          • splitDependencies.get(0) = ZenPolicy
            • mVisualEffects.get(0) = OutputConfiguration
              • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
                • mHierarchyOps.get(0) = PackageManagerException
              • mSensorPixelModesUsed.get(1) = Bundle

That is written by system_server. Then receiving application reads everything up to (and including readSerializable data of) PackageManagerException normally, however after Serializable data for PackageManagerException are read an exception is thrown and reading of everything below OutputConfiguration is cancelled, leaving Bundle unread. Reading proceeds to ZenPolicy which consumes three ints that precede raw data within Bundle. Then ApplicationInfo reading proceeds with reading data that were previously raw data passed verbatim in Bundle. Reading of that raw data will continue with remaining objects in this stack (ApplicationInfo, ActivityInfo, ClipData and Intent) and then that raw data will be used for reading next handleReceiver method parameter

What then happens within handleReceiver

As I've just said below now remaining scheduleReceiver parameters are read from buffer controlled by attacker.

Let's take a look at what happens once that method is invoked.

First scheduleReceiver packs values from all arguments and uses sendMessage() to pass execution to main thread

Next, on main thread handleReceiver is called

handleReceiver calls getPackageInfoNoCheck, passing it ApplicationInfo which it received as part of ActivityInfo which was passed to scheduleReceiver argument

getPackageInfo checks if package with given name is already present in cache and if not it constructs new LoadedApk instance, passing it ApplicationInfo object received earlier (Since attacker wants to cause new LoadedApk to be constructed a packageName of package that wasn't earlier seen in this process is used)

Then ContextImpl.getClassLoader() method is used, which at first run delegates to mPackageInfo.getClassLoader(), with mPackageInfo being a LoadedApk constructed in previous paragraph

Then there's createOrUpdateClassLoaderLocked, which calls makePaths to populate zipPaths with paths to be used in ClassLoader, then they are joined and assigned to zip variable and that is passed to createClassLoader

makePaths fills zipPaths using information from ApplicationInfo, most importantly this includes sourceDir. Attacker application makes injected ApplicationInfo with sourceDir set to path to own apk, therefore receiver class will be actually loaded from attacker apk. This directly leads to execution of attacker-controlled code within application receiving broadcast

Note on hidden API checks

There was one more thing that needed to be bypassed: hidden API checks. These were never meant to be security boundary (as application can always use NDK and call underlying syscall directly), but in this case they were bypassed by constructing crafted ClipData by manually writing data to Parcel and then using readParcelable. Such ClipData could be then normally attached to Intent and then passed to sendBroadcast() so sending broadcast itself was done using only public APIs

Fixes

The above writeup was originally sent to Google and looks like they've made use of it as there are multiple fixes resulting from it (I think, I have no proof about direct causality)

Released with Android 12:

Present only on master branch at time of writing, not in released versions, probably will appear in Android 13 (not in 12L):

You might also like...
Android app to test various cryptography algorithm.
Android app to test various cryptography algorithm.

CryptographyLesson Introduction This android app shows how cryptographic algorithm works. You can encrypt or decrypt messages and try different algori

📱 Android client app for the AryKey 🔑
📱 Android client app for the AryKey 🔑

Android application that prepares an hardware device via USB serial port with a specific password generated deterministically based on three (3) inputs: the App we want to Unlock, the User ID used for login (typically an email address) and the PIN (6 numeric digits) we want to associate with previous inputs.

Appshark is a static taint analysis platform to scan vulnerabilities in an Android app.
Appshark is a static taint analysis platform to scan vulnerabilities in an Android app.

Document Index 1.overview 2.startup 3.how to write rules 4.how to find compliance problems use appshark 5.a path traversal game 6.argument 7.engine co

BlackDex is an Android unpack tool, it supports Android 5.0~12 and need not rely to any environment. BlackDex can run on any Android mobile phones or emulators, you can unpack APK File in several seconds.
BlackDex is an Android unpack tool, it supports Android 5.0~12 and need not rely to any environment. BlackDex can run on any Android mobile phones or emulators, you can unpack APK File in several seconds.

BlackDex is an Android unpack tool, it supports Android 5.0~12 and need not rely to any environment. BlackDex can run on any Android mobile phones or emulators, you can unpack APK File in several seconds.

A simple library that can help you detect if you app is modded or tampered with
A simple library that can help you detect if you app is modded or tampered with

Android Tamper Detector A simple library that can help you detect if you app is modded or tampered with. This adds a security level that makes it diff

This app should provide a common interface to fetch the estimated time of arrival for parcels

ETA-App This app should provide a common interface to fetch the estimated time of arrival for parcels. It will integrate with several backend systems

Keepass2Android is a password manager app.

Keepass2Android What is Keepass2Android? Keepass2Android is a password manager app. It allows to store and retrieve passwords and other sensitive info

Tiny app to enforce security policies of your device
Tiny app to enforce security policies of your device

Sentry Enforce security policies. Tiny app to enforce security policies of your device. It can: limit the maximum number of failed password attempts d

Comments
  • Re: One could construct data that will cause at end seek to final position

    Re: One could construct data that will cause at end seek to final position

    Hello,

    Very impressive find, can I ask one question Re: "although I think in many cases one could construct data that will cause at end seek to final position so this one isn't strong mitigation"

    Can you elaborate on that a bit more? Did you mean there are other parcelable classes can be used as the gadget to put in the PoC Parcel, that would do setPos when deserializing?

    Thanks!

    opened by M3hh 4
Owner
null
Finds players using brownmen exploit

Lambda Plugin SDK This project in an example to show how a proper plugin for Lambda Client is set up. The advantage of plugins for a utility mod is th

notperry1234567890 9 Oct 10, 2021
This app will show grid overlay over whole system which helps you to verify your excellent app design.

GridWichterle for Android This app will show grid overlay over whole system which helps you to verify your excellent app design. Download: What is the

Inmite s.r.o. 408 Dec 29, 2022
🔓 Kotlin version of the popular google/easypermissions wrapper library to simplify basic system permissions logic on Android M or higher.

?? Kotlin version of the popular google/easypermissions wrapper library to simplify basic system permissions logic on Android M or higher.

Madalin Valceleanu 327 Dec 30, 2022
Android Malware (Analysis | Scoring) System

An Obfuscation-Neglect Android Malware Scoring System Quark-Engine is also bundled with Kali Linux, BlackArch. A trust-worthy, practical tool that's r

Quark-Engine 999 Dec 20, 2022
The Spigot plugin counterpart of the overly complex SSN.gg authentication system

Atreus The Spigot plugin counterpart of the overly complex SSN.gg authentication system. Building Make sure you have both Maven and JDK installed (ver

servidor sem nome 3 Dec 16, 2022
A simple android app that parses its own signature and displays it

SigDisplayer Usage Download the release APK or clone the repository and compile yourself. Sign the APK with your preferred keystore. Install and open

Jonah 5 Oct 18, 2022
MiHawk 🦅👁️ is simple and secure 🔒 Android Library to store and retrieve pair of key-value data with encryption , internally it use jetpack DataStore Preferences 💽 to store data.

MiHawk MiHawk ?? ??️ is simple and secure ?? Android Library to store and retrieve pair of key-value data with encryption , internally it use jetpack

Nedal Hasan Ibrahem 5 Sep 3, 2022
a version of the official Android openssl setup to build standalone for use in app

OpenSSL on the Android platform. --- The code in this directory is based on $OPENSSL_VERSION in the file openssl.version. See patches/README for more

Guardian Project 371 Dec 8, 2022
Analyze any Android/Java based app or game

ClassyShark Introduction ClassyShark is a standalone binary inspection tool for Android developers. It can reliably browse any Android executable and

Google 7.2k Jan 3, 2023
A android app for encrypting apk

A android app for encrypting apk

FlyingYu 124 Jan 5, 2023