Goal Reached Thanks to every supporter — we hit 100%!

Goal: 1000 CNY · Raised: 1000 CNY

100.0%

CVE-2022-20474 PoC — Google Pixel 安全漏洞

Source
Associated Vulnerability
Title:Google Pixel 安全漏洞 (CVE-2022-20474)
Description:Google Pixel是美国谷歌(Google)公司的一款智能手机。 Google Pixel 存在安全漏洞。目前尚无此漏洞的相关信息,请随时关注CNNVD或厂商公告。
Description
PoC of CVE-2022-20474
Readme




# CVE-2022-20474

### 前言

最近正仔细学习[michalbednarski](https://github.com/michalbednarski)的[LeakValue文章](https://github.com/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](https://source.android.com/docs/security/bulletin/2022-12-01#framework), [patch](https://android.googlesource.com/platform/frameworks/base/+/569c3023f839bca077cd3cccef0a3bef9c31af63^!/)),我瞅了一眼patch:

```diff
@@ -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`长度,在`LazyValue`对象中记录为`mLength`。

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

```java
    		 /**
         *                      |   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吗?

```java
 				@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`和`LazyLength`,然后就进入正常的`Value`读取流程了,例如`Parcelable`需要读取`ClassName`,然后执行`createFromParcel`,读完了之后普通的`Key-Value`没有任何区别,也不会对后续的序列化产生影响。再度回顾一下,`Self-changed Bundle`的核心思想是read完成后再修改,这里只不过是一个普通的越界读取而已,看来这个方向行不通。

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

```java
     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`的条件直接满足。

### 复现

| 值              | 说明                                                         |
| --------------- | ------------------------------------------------------------ |
| "Cxxsheng"      | 第一个key                                                    |
| 4               | 会读取两轮:第一轮代表`VAL_PARCELABLE`;第二轮变成第二个Key的String Length |
| -8              | 会读取两轮:第一轮代表`LazyValue`的`objectLength`,会导致读取指针前移,导致两轮读取;第二轮变成第二个Key 的String Value |
| 0               | 第二个Key 的String Value                                     |
| 0               | 第二个Key 的String Value                                     |
| 1               | `VAL_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                                      |
| 13              | `VAL_BYTEARRAY`                                              |
| LazyValue的长度 | 计算得出                                                     |
| ByteArray的长度 | 计算得出                                                     |
| ByteArray       | 其中包含了了恶意的Key-Value,即`Intent.EXTRA_INTENT`和`Intent` |

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

```txt
Bundle[{Cxxsheng=Supplier{VAL_PARCELABLE@28+0},[一段乱码]=[恶意的ByteArray], [一段乱码]=0}]
```

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

| 值                                  | 说明                                                         |
| :---------------------------------- | :----------------------------------------------------------- |
| "Cxxsheng"                          | 第一个key                                                    |
| 11                                  | VAL_LIST                                                     |
| 32                                  | 第一个Value的长度,后面的合不合法已经都不重要(反正不会去apply这个LazyValue),这个直接指到ByteArray中恶意Intent的前面 |
| 0                                   | LazyValue中的值                                              |
| 0                                   | LazyValue中的值                                              |
| number1                             | LazyValue中的值                                              |
| number2                             | LazyValue中的值                                              |
| 0                                   | LazyValue中的值                                              |
| 13                                  | LazyValue中的值                                              |
| LazyValue的长度                     | LazyValue中的值                                              |
| ByteArray的长度                     | LazyValue中的值                                              |
| ByteArray开始/`Intent.EXTRA_INTENT` | 第二个Key                                                    |
| Intent                              | 第二个Value                                                  |
| 第三个Key-Value                     | 被排到了最后                                                 |

最后欣赏一下通过模拟的输出图:
![description](https://raw.githubusercontent.com/cxxsheng/CVE-2022-20474/refs/heads/main/img.png)

File Snapshot

[4.0K] /data/pocs/bc73ad5b2c1709de4e2b4648aa66002269575a9e ├── [4.0K] app │   ├── [ 980] build.gradle │   ├── [ 750] proguard-rules.pro │   └── [4.0K] src │   ├── [4.0K] androidTest │   │   └── [4.0K] java │   │   └── [4.0K] com │   │   └── [4.0K] cxxsheng │   │   └── [4.0K] cve_2022_20474 │   │   └── [ 768] ExampleInstrumentedTest.java │   ├── [4.0K] main │   │   ├── [ 936] AndroidManifest.xml │   │   ├── [4.0K] java │   │   │   └── [4.0K] com │   │   │   └── [4.0K] cxxsheng │   │   │   └── [4.0K] cve_2022_20474 │   │   │   └── [6.2K] MainActivity.java │   │   └── [4.0K] res │   │   ├── [4.0K] drawable │   │   │   ├── [5.5K] ic_launcher_background.xml │   │   │   └── [1.7K] ic_launcher_foreground.xml │   │   ├── [4.0K] layout │   │   │   └── [ 805] activity_main.xml │   │   ├── [4.0K] mipmap-anydpi │   │   │   ├── [ 343] ic_launcher_round.xml │   │   │   └── [ 343] ic_launcher.xml │   │   ├── [4.0K] mipmap-hdpi │   │   │   ├── [2.8K] ic_launcher_round.webp │   │   │   └── [1.4K] ic_launcher.webp │   │   ├── [4.0K] mipmap-mdpi │   │   │   ├── [1.7K] ic_launcher_round.webp │   │   │   └── [ 982] ic_launcher.webp │   │   ├── [4.0K] mipmap-xhdpi │   │   │   ├── [3.8K] ic_launcher_round.webp │   │   │   └── [1.9K] ic_launcher.webp │   │   ├── [4.0K] mipmap-xxhdpi │   │   │   ├── [5.8K] ic_launcher_round.webp │   │   │   └── [2.8K] ic_launcher.webp │   │   ├── [4.0K] mipmap-xxxhdpi │   │   │   ├── [7.6K] ic_launcher_round.webp │   │   │   └── [3.8K] ic_launcher.webp │   │   ├── [4.0K] values │   │   │   ├── [ 147] colors.xml │   │   │   ├── [ 76] strings.xml │   │   │   └── [ 408] themes.xml │   │   ├── [4.0K] values-night │   │   │   └── [ 332] themes.xml │   │   └── [4.0K] xml │   │   ├── [ 478] backup_rules.xml │   │   └── [ 551] data_extraction_rules.xml │   └── [4.0K] test │   └── [4.0K] java │   └── [4.0K] com │   └── [4.0K] cxxsheng │   └── [4.0K] cve_2022_20474 │   └── [ 388] ExampleUnitTest.java ├── [ 163] build.gradle ├── [4.0K] gradle │   ├── [ 939] libs.versions.toml │   └── [4.0K] wrapper │   ├── [ 58K] gradle-wrapper.jar │   └── [ 230] gradle-wrapper.properties ├── [1.2K] gradle.properties ├── [5.6K] gradlew ├── [2.7K] gradlew.bat ├── [ 74K] img.png ├── [ 11K] README.md └── [ 537] settings.gradle 31 directories, 37 files
Shenlong Bot has cached this for you
Remarks
    1. It is advised to access via the original source first.
    2. If the original source is unavailable, please email f.jinxu#gmail.com for a local snapshot (replace # with @).
    3. Shenlong has snapshotted the POC code for you. To support long-term maintenance, please consider donating. Thank you for your support.