
Analyse technique détaillée et preuve de concept pour CVE-2022-20474 sur Android, une vulnérabilité d'inadéquation de Bundle exploitant LazyValue avec une longueur négative pour obtenir un comportement de Bundle auto-modifiable.
Note : avant de lire cet article, il est conseillé d'avoir une compréhension de base des vulnérabilités de décalage de Bundle. Si vous n'avez pas encore lu les documents suivants, veuillez d'abord les consulter :
Récemment, j'étudie attentivement l'article LeakValue de michalbednarski. En discutant avec Canyie, il a mentionné que cet article évoquait également un cas de Bundle auto-modifié dans le scénario LazyValue. Je suis donc allé chercher le texte original, et effectivement il y avait ce passage, que j'avais directement omis lors de ma lecture de l'article de Michal. L'original dit :
(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 faisait probablement référence à CVE-2022-20474 (bulletin, patch). J'ai jeté un coup d'œil au patch, mais la fonction dans le lien du patch n'était pas très complète. Je l'ai complétée et examinée attentivement :
@@ -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 dans le code correspond à length du LazyValue. En fait, ce n'est que la longueur de l'objet variable contenu dans LazyValue, tandis que la longueur totale du LazyValue doit être contrôlée par le champ mLength, c'est-à-dire valueLength dans le code, qui est passé à mLength dans le constructeur de LazyValue.
Comparons avec la disposition de LazyValue :
/**
* | 4B | 4B |
* mSource = Parcel{... | type | length | object | ...}
* a b c d
* length = d - c
* mPosition = a
* mLength = d - a
*/
Sur la base de ce qui précède, nous pouvons obtenir les faits suivants :
mLength représente la longueur totale du LazyValue ; mLength = objectLength + 8 octets.objectLength doit être supérieur ou égal à 0.LazyValue ne stocke que mLength, pas objectLength, car la copie mémoire de LazyValue est basée sur l'objet entier.LazyValue soit à nouveau lu.Ensuite, après mûre réflexion, nous sommes arrivés à la conclusion que ces faits ne sont d'aucune utilité ! Car, sur la base des faits ci-dessus, une seule modification peut être effectuée lors de la lecture. Or, nous savons que l'idée centrale d'un Bundle auto-modifié est de modifier après la lecture, afin de contourner les vérifications de sécurité.
Au moment où nous étions sur le point d'abandonner, nous avons soudainement remarqué quelques détails dans la description du patch :
Addresses a security vulnerability where a (-8) length object would cause dataPosition to be reset back to the start of the value, and be re-read again.
objectLength anormalIl y est mentionné que lorsque objectLength vaut -8, des problèmes se posent, ce qui nous donne des indices supplémentaires. Dans ce cas, LazyValue peut-il encore être appliqué normalement ?
@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;
}
On voit que le readValue réel commence à mPosition, puis lit LazyType et objectLength, avant d'entrer dans le flux normal de lecture de Value. Par exemple, un Parcelable doit lire ClassName, puis exécuter createFromParcel. Une fois la lecture terminée, il n'y a aucune différence avec un Key-Value normal, et cela n'affecte pas la sérialisation ultérieure. Reconsidérons : l'idée centrale d'un Bundle auto-modifié est de modifier après la lecture. Ici, ce n'est qu'une simple lecture hors limites, donc cette direction semble inefficace.
Alors, que se passe-t-il si LazyValue n'est pas appliqué pendant ce processus, c'est-à-dire s'il continue à participer à l'IPC en tant que LazyValue ? Dans ce cas, sa fonction writeToParcel est appelée :
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);
}