
Exploit for CVE-2022-20452, privilege escalation on Android from installed app to system app (or another app) via LazyValue using Parcel after recycle()
Android 13 introduces many enhancements in order to harden Parcel serialization mechanism
Here's presentation from Android Security and Privacy team on enhancements made
That is great, definitely eliminates or makes unexploitable many vulnerabilities. Also they describe breaking my previous exploit, allowing apps to load their code into other apps (including system ones)
But now I am back with new exploit that achieves the same, although in different way. It relies on following vulnerabilities that were introduced during aforementioned Parcel hardening:
![Screenshot of application displaying text. Title: LeakValue. Main text: Created 6 ValueLeaker-s. Locking ActivityTaskManagerService. Locked ActivityTaskManagerService. Unlocking ActivityTaskManagerService. Unlocked ActivityTaskManagerService. leakedBinders=[android.os.BinderProxy@f06702e]. Leaked interface: android.app.IApplicationThread. Requesting code execution. Shellcode has been executed in uid=1000 pid=6904 packageName=com.android.settings 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. At bottom of screen there are two buttons: START and MANUAL TESTING](Screenshot_20220723-081920.png)
(Also logcat from app execution, exploitation is noisy in logs)
Parcel and Parcelable mismatch bugsAndroid's Parcel class is base of communication between processes
Objects can implement Parcelable interface in order to allow writing them to Parcel, for example (copied from AOSP):
public class UsbAccessory implements Parcelable {
public static final Parcelable.Creator<UsbAccessory> CREATOR =
new Parcelable.Creator<UsbAccessory>() {
public UsbAccessory createFromParcel(Parcel in) {
String manufacturer = in.readString();
String model = in.readString();
String description = in.readString();
String version = in.readString();
String uri = in.readString();
IUsbSerialReader serialNumberReader = IUsbSerialReader.Stub.asInterface(
in.readStrongBinder());
return new UsbAccessory(manufacturer, model, description, version, uri,
serialNumberReader);
}
};
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(mManufacturer);
parcel.writeString(mModel);
parcel.writeString(mDescription);
parcel.writeString(mVersion);
parcel.writeString(mUri);
parcel.writeStrongBinder(mSerialNumberReader.asBinder());
}
}
Note that Parcel internally stores position at which write or read is performed, readString() parses data into String as well as advances position. That position can be manually get/set through dataPosition()/setDataPosition(). Implementations of Parcelable interface must ensure that their writeToParcel and createFromParcel write/read same amount of data, otherwise all subsequent reads will get data from wrong offsets
Bundle (key-value map that can be sent across processes) can contain variety of objects that can be written to Parcel through writeValue(). When contents of Bundle are read from Parcel, any Parcelable class available in system there can be read
Bundle defers actual parsing of contents by having length of whole parcelled data written into Parcel and then just copying relevant part of original Parcel to secondary Parcel stored in mParcelledData (this allows for example Activity.onSaveInstanceState() to provide Parcelables which are not available in system_server, whole Bundle is then passed to system_server and back verbatim without parsing contents)
Once however any value in Bundle was accessed, all values inside Bundle were unparcelled and every present key-value pair was parsed. If such map contained Parcelable which had unbalanced writeToParcel and createFromParcel methods and later such Bundle was forwarded to another process, that another process could see different contents of Bundle. This made all such mismatches in classes available in system vulnerabilities as there are places in system where Bundle is inspected to be safe and then forwarded to another process
In this writeup I'm calling such Bundle which presents one contents and then other after being forwarded a self-changing Bundle
Another important thing here is that besides just bytes (Strings, numbers, objects made of above), Parcel can also contain File Descriptors and Binders. Binders are objects on which one can make RPC call, that is one process creates Binder object and overrides onTransact() method. Then Binder is passed to another process, in example code above you can see read/writeStrongBinder() calls used to read and write it to Parcel. In another process, when readStrongBinder() is used a BinderProxy object is created (hidden behind IBinder interface). Then that another process can call on that object and in original object will be executed. Usually through, one doesn't manually write / but
LazyValue, the end of self-changing BundlesSince in the past there were many cases of classes with writeToParcel/createFromParcel mismatch, Android 13 solves problem of any such class being present anywhere in system allowing construction of self-changing Bundle by introducing LazyValue
Now, when writeValue is used, if value being written is not primitive, length of value is written to Parcel as well
When normal app directly uses Parcel.readValue(), everything happens as before except a warning is printed if length read from Parcel doesn't match size of actually read data (Note though that Slog.wtfStack never throws)
Bundle however, now uses Parcel.readLazyValue() instead
Lets take closer look at how it works: in LazyValue class we have nice comment explaining structure of LazyValue data inside Parcel:
| 4B | 4B |
mSource = Parcel{... | type | length | object | ...}
a b c d
length = d - c
mPosition = a
mLength = d - a
mSource is reference to original Parcel on which readLazyValue() was called
mPosition and mLength describe location of whole LazyValue data in original Parcel, including type and length
"length" (without "m" at beginning) refers to length value as written to Parcel and excludes header (type and length)
So here is what happens when someone (either system or app) takes value from Bundle that was read from Parcel:
get*() methods of Bundle class, for example new getParcelable() with type argument (Flow will be same for both new and old methods, just new methods ensure that clazz argument isn't null while legacy ones set it to null)unparcel() is called, which will check if this Bundle has mParcelledData (meaning it was read from Parcel but no value was accessed yet and key names were not unpacked yet, if that isn't the case skip to step 5.)unparcel() delegates to unparcel(boolean itemwise), , is set to , a copy of that has made and parameter is set to to indicate that passed is owned by and it is okay to call on itIf Bundle is being forwarded while it still contains LazyValue (meaning that this particular value was not accessed, but some other value from that Bundle was (that is unparcel() was called, but LazyValue.apply() for that item wasn't)):
LazyValue is detected by Parcel.writeValue() and write is delegated to LazyValue.writeToParcel()LazyValue.writeToParcel() uses out.appendFrom(source, mPosition, mLength) to copy whole LazyValue data from original Parcel (again, mPosition and mLength include LazyValue header, so this also copies type and length from original )Parcel.ReadWriteHelper and Parcel.readSquashed(Details of these are not important for this exploit, only relevant thing here is that these mechanisms exist)
Another interesting feature of Parcel is optional ability to deduplicate written Strings and objects
The deduplication of Strings is done by overriding Parcel.ReadWriteHelper class: Parcel.readString() actually delegates to ReadWriteHelper and default helper directly reads String from Parcel
Alternate implementation of Parcel.ReadWriteHelper can replace readString calls with reading pool of Strings beforehand and using readInt to get indexes of Strings in pool; that however is never done with app-controlled Parcels
Parcel does offer hasReadWriteHelper() method, which allows callers detect presence of such deduplication mechanism being active and disable features incompatible with it
Other deduplication mechanism available in Parcel is squashing:
Parcel.allowSquashing()Parcel.maybeWriteSquashed(this). If that method returned true it means that object was already written to this Parcel and now only offset to previous object data was written to Parcel. Otherwise (either squashing is not enabled or this is first time this object is written) maybeWriteSquashed writes zero as offset to indicate that object isn't squashed and returns false to indicate to caller that they should write actual object dataParcel.readSquashed is called and actual read function is passed to it as lambda. readSquashed checks if offset written by indicates that another occurrence of object was read earlier: if yes then previously read object is returned, otherwise provided lambda is called to read it nowParcel.recycle()On Java side Parcel objects can be recycled into pool, that is once you're done with Parcel you call recycle() on it and next time someone calls Parcel.obtain() they'll get previously recycled Parcel. This allows reducing amount of object allocations and subsequent Garbage Collection
On the other hand, such manual memory management brings possibility of Use-After-Free-like bugs into Java (although with type safety, unlike usual Use-After-Free in C)
As noted above, Bundle creates copy of Parcel and won't call Parcel.recycle() if LazyValue is present, that however is not the case if Parcel.hasReadWriteHelper() is true, in that case:
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle); is called, this means that Bundle won't recycle Parcel as it still belongs to caller, however this does create LazyValues which refer to original Parcel and could outlive original Parcels lifetimeunparcel(/* itemwise */ true), which will use getValueAt() on all items to replace all LazyValues present in Bundle with actual valuesNow, can we make these LazyValues survive step 2. and turn that behavior into Use-After-Recycle?
If deserialization fails (for example class with name specified inside Parcel could not be found), a BadParcelableException is thrown and then caught by getValueAt(). If BaseBundle.sShouldDefuse static field is true, an exception isn't raised and execution proceeds leaving Bundle containing LazyValue referring to original Parcel. sShouldDefuse indicates that unavailable values from Bundle shouldn't cause exceptions in particular process and is set to true in
If original Parcel gets recycled and after that the Bundle read from it will be written to another Parcel, contents of original Parcel will be copied to destination Parcel, but at that point original Parcel could be reused for something else and data from unrelated IPC operation could be copied
Okay, but how do we get Parcel.hasReadWriteHelper() to be true while Bundle provided by us is being deserialized?
Turns out that RemoteViews class (normally used for example for passing widgets to home screen) explicitly sets ReadWriteHelper when reading Bundles nested in it. This ReadWriteHelper doesn't do String deduplication and is present only to cause Bundle to skip copying data to secondary Parcel. The reason for that being done is that RemoteViews enables squashing in order to deduplicate ApplicationInfo objects nested in it, but this could also cause ApplicationInfo objects present inside Bundle to be squashed, so reading of that cannot be deferred because then those squashed objects would fail to unsquash
Parcelables in system_server and retrieving themSo now we want system_server to read our RemoteViews containing Bundle containing LazyValue that fails to deserialize and later (in another Binder IPC transaction) send that object back to us
It probably could be done through some legitimate means, such as registering ourselves as app widget host (but that would require user interaction to grant us permission) or posting a Notification with contentView set (but that would cause interaction with other processes and/or be visible to user and I preferred to avoid both of these)
I've decided instead to create MediaSession and call setQueue(List<MediaSession.QueueItem> queue) on it to send object to system_server and later get it back through List<MediaSession.QueueItem> getQueue() method of MediaController (which can be retrieved through MediaSession.getController()). While these methods don't look like they could accept RemoteViews, they actually do thanks to Java Type Erasure and fact that under the hood they are implemented using generic serialization operations on List
However, I'm not using these SDK methods, I'm manually writing data for underlying Binder transactions (because I need to write and later read malformed serialized data), so lets take a look at how these methods work
Both of these methods had to take care of fact that total size of queue might exceed maximum size of Binder transaction so transfer can be split into multiple transactions
Sending "queue" to system_server normally goes as follows:
MediaSession.setQueue() first calls ISession.getBinderForSetQueue()system_server side that methods constructs and returns ParcelableListBinder objectMediaSession.setQueue() calls ParcelableListBinder.send() which will send list contents into provided Binder, possibly over multiple transactions:
1 written and actual item is written through (which writes name of class being sent and then calls to send data)Retrieving "queue" on the other hand, goes bit differently:
MediaController.getQueue() just calls ISessionController.getQueue() and unwraps received ParceledListSlicesystem_server side, getQueue() just wraps mQueue into ParceledListSlice and returns itParceledListSlice.writeToParcel() and createFromParcel() methods, in particular, writeToParcel() upon reaching safe size limit will write object that allows retrieving next chunksAs to why these are different: there is an ongoing effort to make sure that system_server doesn't make outgoing synchronous Binder calls to other apps, because if these calls would hang that could hang whole system_server. This means system_server shouldn't be receiving ParceledListSlices. While there is code that warns about outgoing synchronous transactions from system_server, it couldn't yet be made enforcing because there are still cases where system_server does make such calls, for example by actually receiving ParceledListSlice
So now we have primitives needed to make system_server do parcel_that_will_be_sent_to_us.appendFrom(some_recycled_parcel, somewhat_controlled_position, controlled_size)
We could either randomly attempt pulling Parcel data from system or arrange stuff to take something specific
There are following considerations:
Parcel.recycle() is called, contents of that Parcel are cleared. This means that Parcel from which we would like to have data copied from must not be recycle()d, which approximately means that we can't take data from Binder transaction that has finishedParcel of some Bundle present in system (this includes Intent extras and savedInstanceState of Activity). These are usually not recycle()d at all (they are cleaned by Garbage Collector and don't return to pool, when pool gets depleted Parcel.obtain() creates new Parcel objects. Of course s to which we're holding reference won't be GCed, even if system has no other use for them)In the end I've decided to attempt grabbing IApplicationThread Binder, which is sent by app to system_server when app process starts and system_server uses it to tell application which components it should load
When application process initially starts, one of first things it does is sending IApplicationThread to system_server through call to attachApplication() and this is transaction from which I'll be grabbing that Binder from. There are other places where IApplicationThread is being put in Parcel, such as being passed for caller identification by system when starting activity (but I didn't have much control over when target application does that) or being sent by system to application as part of Activity lifecycle management (but this is done in oneway transaction outgoing from system_server and chances of winning race against Parcel.recycle() would be slim)
That being said, grabbing Binder that is being received by system_server during attachApplication() transaction is also nontrivial and there were few problems to overcome
ParcelFirst problem with grabbing IApplicationThread Binder from Parcel from which data for attachApplication() are received is that this Binder is at quite early/low dataPosition(), much lower than our LazyValue in Bundle in RemoteViews could be
Data for attachApplication() transaction consist just of RPC header followed by IApplicationThread Binder. RPC header (written through Parcel.writeInterfaceToken()) consist of few ints and name of interface, in this case "android.app.IActivityManager"
Meanwhile to read Bundle embedded in RemoteViews we'd need to get past at least (few minor items are skipped):
readParcelableParcelable: "android.view.RemoteViews"ApplicationInfo object present in RemoteViews (also it must not be null and have non-null packageName or RemoteViews.writeToParcel() will fail when we'll try to get this object to be sent back)Now in Bundle, we just need to put String key and reading of LazyValue starts, position in Parcel is remembered, but at this point its way past position IApplicationThread Binder would be
Can we perhaps upon reaching this point rewind position in Parcel? In other words could we have Parcel.setDataPosition() called with value pointing to earlier position than current one?
Turn out, we can, thanks to another bug in LazyValue. This is code used for reading it:
public Object readLazyValue(@Nullable ClassLoader loader) {
int start = dataPosition();
int type = readInt();
if (isLengthPrefixed(type)) {
int objectLength = readInt();
int end = MathUtils.addOrThrow(dataPosition(), objectLength);
int valueLength = end - start;
setDataPosition(end);
return new LazyValue(this, start, valueLength, type, loader);
} else {
return readValue(type, loader, /* clazz */ null);
}
}
(Original in AOSP, LazyValue constructor just assigns parameters to fields)
The thing is MathUtils.addOrThrow() checks for overflow, but is perfectly fine with negative values
If we'd try doing Parcel.writeValue() on LazyValue with negative mLength (filled from valueLength parameter) then that would throw on appendFrom(), however since we're during read of Bundle with Parcel.hasReadWriteHelper() being true all LazyValues are unparcelled after reading and we had to intentionally put faulty Parcelable inside it make it stay as LazyValue. If we put valid parcelled data at position where LazyValue is, it'll be unparcelled and as noted earlier mismatched length will only trigger message in logcat. This particular exploit sets type to and number of key-value pairs to zero. In upon reading that value we can see following message: ""
(Also LazyValue with negative length specified can be used (without using other bugs described in this writeup) to create self-changing Bundle, the thing LazyValue was created to eliminate. But that is another story (and separately reported to Google), in this exploit I'm aiming for more)
So how much do we want to rewind?
After setDataPosition() call happens, reading will proceed to next key-value pair in Bundle, so we need to pick position where we'll have:
Parcel.readString(), can be pretty much anything, including pointing at invalid length (negative or exceeding total Parcel size), in that case readString() would return null which is valid key in BundleisLengthPrefixed() returns trueParcel.appendFrom() will fail if length is not aligned or exceeds total size of source ParcelSo what position in Parcel that could be it could be considering the same data were already read and are necessary to reach this point:
Parcelable ("android.view.RemoteViews"), because there's not enough spaceParcelable, because we're unable to set type and lengthParcelable, because first thing in RemoteViews is mode which we must set to MODE_NORMAL to reach our codeIApplicationThread Binder isHmm, there isn't any good place when RemoteViews is outermost object in parcelled data
We need to find some other Parcelable that:
ints or Strings that are just data and don't affect serialization process)RemoteViews (either directly or via arbitrary readParcelable)IApplicationThread stays in target ParcelSo I've taken list of Parcelable classes in system, sorted it by ascending length of fully qualified class name and began checking items on that list to see if they fulfill condition 2
That way I've reached to "android.os.Message", which is what this exploit uses. Now process of reading item our prepared object from Parcel goes as follows:
readParcelableParcelable: "android.os.Message"ints that we can set to whatever values we want are read into fieldsreadParcelable() call, which goes all the way we described above through RemoteViews and starts reading Bundle with Parcel.hasReadWriteHelper being trueBundle declares to have two key-value pairs. In first value we have LazyValue with negative length, that triggers to position where isattachApplication()Okay, so in previous step we've successfully created object that will allow us grabbing IApplicationThread object while attachApplication() method is running
The thing is, that method completes quickly and our chances in fair race against its completion would be rather slim
That method however, does acquire few mutexes (Through use of Java's synchronized () {} blocks), if we get to acquire one of such mutexes and stall there, this method would stall as well
Now lets return to few things that were already said in this writeup and will become useful for this purpose:
Bundle performs deserialization of values in it when these values are accessedParceledListSlice class which during deserialization will make blocking outgoing Binder call to object specified inside serialized dataAdding all those things together: If we find in system_server place where contents of Bundle provided by app are accessed under mutex which is also used by attachApplication(), we'll be able to stall attachApplication() until Binder transaction made to our process finishes
ActivityOptions is class describing various parameters related to Activity start (for example animation). Unlike other classes describing parameters passed to system_server, this one doesn't implement Parcelable but instead provides method for converting it to Bundle
On system_server side, that Bundle is converted back to ActivityOptions, triggering deserialization. I've found place where that operation is being done while ActivityTaskManagerService.mGlobalLock mutex is held in ActivityTaskManagerService.moveTaskToFront()
So I call ActivityManager.moveTaskToFront(), passing Bundle that contains ParceledListSlice instead of value with expected type. That ParceledListSlice makes Binder call to my process and until I return from that call the ActivityTaskManagerService.mGlobalLock mutex will remain locked
LazyValues pointing to different ParcelsParcel.recycle() and Parcel.obtain() work in Last-In-First-Out manner
This means, that if I create rigged LazyValue when no other Binder transaction to system_server is running, I'll get LazyValue that will point to Parcel that is always used when there is only one transaction incoming to system_server (until it happens that two concurrent transactions incoming to system_server start and finish in non-stack order)
As I don't have control over what other transactions are incoming to system_server, in order to improve exploit reliability I've created multiple LazyValues pointing to various Parcels
Since I have ability to trigger synchronous Binder transaction to my process from system_server, I've used that ability to create LazyValue at various levels of recursion between my process and system_server (although this time I did so without holding a global mutex)
So:
LazyValuesystem_server, system_server calls me back
LazyValuesystem_server, system_server calls me back
LazyValuesystem_server, system_server calls me back
Then once I've got enough LazyValues I finish doing that, return from all of these calls and all Parcels which were reserved by these calls get recycle()d
Each of LazyValues I've made is wrapped in separate ParceledListSlice created by getQueue() and I can call ParceledListSlice Binder to make system_server serialize it and send it to my process
(Alternative way of doing that would be creating multiple MediaSessions)
Now we have everything needed to capture IApplicationThread from attachApplication() when it happens, but we still need to make attachApplication() happen
In general there is a few of types of app components other app can interact with, each of which requires app process to be started
I wanted to start system Settings app (which runs under system uid and therefore has access to everything behind Android permissions)
Initially I've attempted to launch it through startActivity(), however when I've tried that process wasn't started until I've released ActivityTaskManagerService lock. Details on why that was case are in "Additional note: Binder calls and mutexes reentrancy" section, but as a solution I've decided request system for ContentProvider from that app instead of Activity. This had additional advantage of avoiding interference with my UI
I haven't used official ContentResolver API exposed by SDK, but instead I've used system internal one, as I needed asynchronous API because binding to ContentProvider wouldn't finish until attachApplication() which I'm hanging, although starting another thread could be alternative
(It doesn't matter what this particular ContentProvider offer, only relevant thing is that I can establish connection to it)
So that is how I start process of Settings app. I'm ensuring it isn't already running in first place by using officially available ActivityManager.killBackgroundProcesses() method
Primitives are now described, so now here's how it all works together (this is pretty much transcription of MainActivity.doAllStuff() method from this exploit):
Property.of(), which I haven't seen elsewhere)ContentProvider we established to in step 6 during previous execution. We have to do it as otherwise ActivityManager.killBackgroundProcesses() won't consider target process to be "background" and won't kill itActivityManager.killBackgroundProcesses(), as attachApplication() is called only at process startupsystem_server to create bunch of objects containing LazyValue pointing to Parcel that is later recycled. I get ParceledListSlice Binder reference for each object containing and I can make transaction to it to trigger system to write it back. Each of object creation is done at different depths of calls between and my app to make it likely that each of these s will have dangling reference to different objectIApplicationThreadAs noted earlier IApplicationThread Binder, is sent by app to system_server when app process starts and then system_server uses it to tell application which components it should load
It is assumed that this object is only passed to system_server and therefore there are no Binder.getCallingUid()-based checks there, so we can just directly call methods offered by that interface
I've described in my previous writeup on how I get code execution by manipulating scheduleReceiver() arguments. Now situation is same except this time I'm calling scheduleReceiver() myself while then I was tampering with interpretation of arguments of call made by system_server
In these section I'm describing few things that in the end didn't turn out to be useful in this case, although they may be features worth being aware of or are potential bugs
Bundle.clear()For simplicity, I've here described updated Bundle without one commit that was later introduced, that allows Parcel used in Bundle for backing LazyValues to be recycled by calling Bundle.clear()
As noted in commit message, it is tracked if Bundle is copied and in that case clear() won't recycle the Parcel
However that commit also changes semantics of recycleParcel parameter/variable of BaseBundle.initializeFromParcelLocked()
Previously recycleParcel being false indicated that Parcel shouldn't be recycled, either because caller set recycleParcel to false to indicate that Parcel isn't owned by Bundle or it was set to false based on result of parcelledData.readArrayMap()
Now reasons recycleParcel could be false are same, however interpretation of that changed, now that doesn't mean "don't recycle this Parcel", it means "defer recycling of Parcel until Bundle.clear() call"
This means that if clear() would be called on Bundle created with Parcel.hasReadWriteHelper() being true this would led to Parcel being recycled, while code invoking creation of that Bundle would also recycle that Parcel, leading to double-recycle(), which leads to similar behavior as double-free: next calls to Parcel.obtain() would return same object twice
However, I haven't found way to have clear() called on such Bundle
Since I've originally written this, behavior of recycle() was changed and now additional recycle is no-op with possible crash through Log.wtf() (depending on configuration, but never crashing system_server). I'd say that new behavior still might be dangerous, especially when we have ability to programmatically stall deserialization happening in other process, but there really isn't good way to handle double-recycle
Binder calls and mutexes reentrancyA not very well known feature of Binder is that it supports dispatching recursive calls to original thread
That is if process A makes synchronous Binder call to process B and then process B while handling it on same thread makes synchronous Binder call to process A, that call in process A will be dispatched in same thread that is waiting for original call to process B to complete
Other thing is that synchronized () {} sections in Java are reentrant mutexes, which means that if you enter it twice from same thread, it will let you in and won't deadlock
This means that in theory while we're keeping ActivityTaskManagerService.mGlobalLock locked, we still could start Settings app using startActivity(new Intent(Settings.ACTION_SETTINGS)) and we'd successfully enter synchonized block that we're stalling, however starting that Activity also involves Task creation, which involves calling notifyTaskCreated(), which posts message to DisplayThread, and handling of that attempts acquiring lock we're stalling from another thread. So until release , thread will remain blocked. Later, procedure of starting involves . All of that means that in this case app process won't be started until we release lock and the reason we were holding that lock in first place was to keep transaction from finishing so we could grab handles from it, but in this case that transaction wouldn't actually start
Even if we launch Activity that will be part of same Task as current one (that is, we'd launch different Activity from Settings app, one that doesn't specify android:launchMode="singleTask"), that procedure will still involve notifyTaskDescriptionChanged(), which has same impact here as notifyTaskCreated()
So while my thread could call into methods which are using synchronized (ActivityTaskManagerService.mGlobalLock) {}, starting new app process after startActivity() involved use of that lock from different thread and that wasn't useful in this case, so I've opted to trigger start of app process through ContentProvider instead
IApplicationThreadIApplicationThread is very privileged handle, so I consider making use of it after obtaining it a post-exploitation
In this exploit I've used it directly to request code execution in target process, taking advantage of fact that access to that operation is gated by capability (possession of Binder object, which we here leaked) and not by Binder.getCallingUid()
Adding Binder.getCallingUid() check in ApplicationThread.scheduleReceiver() (which we used here to request code execution) and other methods of ApplicationThread (as scheduleReceiver() isn't only method in IApplicationThread allowing code loading) still wouldn't prevent using IApplicationThread to load code into process of other app, as attacker could pass leaked IApplicationThread in place of own one to attachApplication()
Besides loading code into process, having IApplicationThread allows performing grantUriPermission() using privileges of process to which that handle belongs to
onTransact()transact()onTransact()sourcemParcelledDataParcelBundlerecycleParceltrueParcelBundleinitializeFromParcel calls recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader) in order to read key-value map contents. Keys are Strings and values are read using readLazyValue(), creating LazyValue objects for values of types which are written along length prefix. readArrayMap() returns value indicating if it is okay to recycle Parcel. If there were any LazyValue objects present recycleParcel is set to false and Parcel to which LazyValues refer won't be recycled (there is an exception to that, but it is not relevant here, I'll describe it in "Additional note: Bundle.clear()" section)unparcel() is done, mMap is set (not null) and maps String keys to either actual values if they are ready or LazyValue objectsgetValue() is called, which maps key (String) to index (int) and passes it to getValueAt()LazyValue.apply() rewinds Parcel to position of LazyValue.mPosition and calls normal Parcel.readValue() about which I've already saidLazyValue is replaced in mMap, so that next Bundle.get*() call for same key will directly return value and LazyValue deserialization won't be repeated. When Bundle is forwarded that value will be serialized again instead of having original data copied verbatim (however after forwarded Bundle is read, that value will be LazyValue again and any possible writeToParcel/createFromParcel mismatches won't be able affect other values)ParcelmaybeWriteSquashed()system_serverBundleParcel.writeParcelable()Parcelable.writeToParcelBinder transaction size, 0 is written to indicate that there are no more items in this transaction and next items will be sent in another transactionParcelableListBinder has received number of elements that was specified in first transaction, it invokes lambda passed to its constructor, which in this case assigns retrieved list to MediaSessionRecord.mQueueBinderParceledListSlice is read from Parcel, it reads first part directly from Parcel and then if not all elements were written inline, it calls Binder that was written to Parcel in order to retrieve these itemsParcelParcels used for incoming Binder transactions use separate pool than other Parcels in system. When an outgoing Binder transaction is being made, Bundle copies data to secondary Parcel, or an app uses Parcel for their own purposes, they call Parcel.obtain(), which uses Parcel.sOwnedPool. On the other hand, when there's an incoming Binder transaction, system calls Parcel.obtain(long obj), which uses Parcel.sHolderPool. In both cases Parcel.recycle() is used afterwards and takes care of returning Parcel object into appropriate pool. This means exploit must have RemoteViews read from Parcel belonging to same pool as we'd like to leak data from. Before I've decided on particular variant I've written both, so you can find both makeOwnedLeaker and makeHolderLeaker methods in my ValueLeakerMaker classRemoteViews.readActionsFromParcel()VAL_MAPlogcatE Parcel : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP consumed 4 bytes, but -540 expected.Parcel.setDataPosition()"android.os.Message"String"android.os.Message" and LazyValue type, length and data are taken from ints described in third bullet. I've got LazyValue with mPosition and mLength I wanted. Hooray!LazyValues are read they are unparcelled. The one with negative size gets successfully unparcelled and replaced with empty Map, while the other fails deserialization, but that exception is caught and LazyValue just stays in BundlereadParcelable() finishes, but that isn't end of Message data. Message.readFromParcel() is now continuing reading data after rewind and sees data which were initially written as part of RemoteViews. If anything throws exception at this point whole plan gets foiledreadBundle() call. Bundle has magic value and if it is wrong an exception will be thrown. That magic value however isn't present if length is zero or negative and that happened to be the case when length of LazyValue data was set to value I've needed to grab IApplicationThread. So I just got lucky hereMessenger.readMessengerOrNullFromParcel() call. This is actually wrapped Binder object. Reading of that Binder fails, because Binder is special object in Parcel and it must be annotated out-of-band to be read. This problem is detected and logged by Parcel on native side, however this isn't propagated as error and null is simply returnedLazyValueBinderLazyValuesystem_serverLazyValueParcelActivityTaskManagerService.mGlobalLock by making call to ActivityTaskManagerService.moveTaskToFront() passing in argument Bundle that upon deserialization performs synchronous Binder transaction to my process. Next steps are done from that callback and therefore are done with that lock heldActivityManagerService for connection with ContentProvider of victim app (note no "Task" in name, ActivityTaskManagerService is class focused mostly on handling Activity components of apps, while ActivityManagerService handles other app components (as well as overall process startup), this split happened in Android 10, previously both handling of Activity and other app components was in ActivityManagerService)sleep() a little bit to give newly launched process time to start calling attachApplication()ParceledListSlice objects to send their remaining contents (that didn't fit in initial transaction), that is objects containing LazyValue pointing to recycled Parcel. Then from hardcoded offset matching position of IApplicationThread passed to attachApplication() I read Binder object. Right now I'm only saving received Binders in ArrayList to avoid doing too much with lock heldActivityTaskManagerService.mGlobalLock becomes unlockedIApplicationThread Binder. Now I can simply use it to load my code into victim app as described in next sectionActivityTaskManagerService.mGlobalLockDisplayThreadActivityattachApplication()