
Подробный технический анализ и доказательство концепции для Android CVE-2022-20474 — уязвимости несоответствия Bundle, использующей LazyValue с отрицательной длиной для достижения самоизменяющегося поведения Bundle.
Пожалуйста, прочтите это предупреждение: перед чтением этой статьи следует иметь базовое представление об уязвимостях, связанных с несоответствием Bundle. Если вы еще не читали следующие справочные материалы, рекомендуется сначала прочитать их:
Недавно я внимательно изучал статью LeakValue от michalbednarski. В разговоре с Canyie он упомянул, что в этой статье также описывается случай Self-changing Bundle в сценарии LazyValue. Я пошел искать оригинал, и действительно, там был такой абзац, который я упустил, читая статью Михала. В оригинале говорится:
(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)
Михаил, вероятно, имел в виду CVE-2022-20474 (бюллетень, патч). Я взглянул на патч, но функция в ссылке на патч была неполной. Я дополнил её и внимательно изучил:
@@ -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 в коде — это length в LazyValue, но на самом деле это только длина изменяемого объекта, содержащегося в 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 будет прочитан снова.Затем, после долгих размышлений, мы пришли к выводу, что эти факты нифига не полезны! Потому что на их основе можно изменить значение только при чтении, а мы знаем, что основная идея Self-changed Bundle — изменить его после завершения чтения, чтобы обойти проверку безопасности.
Когда мы уже собирались сдаться, мы внезапно заметили некоторые детали в описании патча:
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 — изменить после завершения чтения. Здесь же просто обычное чтение за пределами границ. Похоже, этот путь не работает.
Что если LazyValue не применяется apply в этом процессе? Другими словами, если он продолжает участвовать в IPC как LazyValue, то будет вызвана его функция 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 байт, а из информации о патче следует, что для возникновения уязвимости 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'