关联漏洞
描述
PoC of CVE-2022-20474
介绍
# 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 | 被排到了最后 |
最后欣赏一下通过模拟的输出图:

文件快照
[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
备注
1. 建议优先通过来源进行访问。
2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。