温馨提示,阅读本文前,应当对Bundle Mismatch相关漏洞有初步了解,以下参考资料假如您还没有读过,建议先阅读一下:
最近正仔细学习michalbednarski的LeakValue文章。在与Canyie讨论的时候,他说在这篇文章中还提到了一种在LazyValue场景下Self-changing Bundle的情况,然后我就去翻找原文,果真有这么一段,而我在读Michal的文章的时候直接漏掉了。原文中如下说到:
(Also
LazyValuewith negative length specified can be used (without using other bugs described in this writeup) to create self-changingBundle, the thingLazyValuewas 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,但是补丁链接中函数并没有很完整,补全它之后再仔细看一下:
@@ -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的布局格式如下:
/**
* | 4B | 4B |
* mSource = Parcel{... | type | length | object | ...}
* a b c d
* length = d - c
* mPosition = a
* mLength = d - a
*/
基于上述的内容,我们可以得到如下事实:
mLength代表整个LazyValue的长度,mLength = objectLength + 8字节。objectLength应该大于等于0。LazyValue对象中只保存了mLength,而没保存objectLength,因为LazyValue作内存拷贝的时候是基于整个对象来做拷贝的。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吗?
@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函数:
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的条件直接满足。
知道了原因,我们就能开始复现了,不过在这之前,我们还需要亿点点细节。
static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L'
private static final int BUNDLE_MAGIC_NATIVE = 0x4C444E44; // 'B' 'N' 'D' 'N'
我们都知道,Bundle的内存布局中大致如下所示:
/**
* | 4B | 4B | 4B |
* Bundle{| length | MAGIC | size | Key | Value | Key | Value | ...}
*
*/
MAGIC是Bundle在内存布局中的魔数,可以BUNDLE_MAGIC或者BUNDLE_MAGIC_NATIVE,这两者最重要的区别是BUNDLE_MAGIC会让Key-Value在反序列化完成后,进行重新排序,如以下代码所示:
/**
* 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值。
/**
* a
* ArrayMap{| Key1 | LazyValue1 | FakeKey2 | Value2 | Key3 | Value3 |}
*
*/