
एंड्रॉइड CVE-2022-20474 के लिए विस्तृत तकनीकी विश्लेषण और प्रूफ-ऑफ-कॉन्सेप्ट, एक Bundle mismatch भेद्यता जो नकारात्मक लंबाई के साथ LazyValue का शोषण करके self-changing Bundle व्यवहार प्राप्त करती है।
कृपया ध्यान दें, इस लेख को पढ़ने से पहले, आपको 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 (बुलेटिन, पैच) से है। मैंने एक नज़र पैच पर डाली, लेकिन पैच लिंक में फ़ंक्शन पूरा नहीं दिखाया गया था। इसे पूरा करने के बाद ध्यान से देखते हैं:
@@ -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 फिर से पढ़ा जा सकता है।फिर, गहन विचार के बाद, हम सभी सहमत हैं कि इन तथ्यों का! कुछ! भी! उपयोग! नहीं! है! क्योंकि उपरोक्त तथ्यों के आधार पर, केवल 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 बाइट्स, और पैच जानकारी से ज्ञात होता है कि भेद्यता को ट्रिगर करने के लिए objectLength -8 होना चाहिए, तो इस समय mLength = 0 सत्य हो जाता है। दूसरे शब्दों में, इस परिदृश्य में पूरा LazyValue गायब हो जाता है, केवल String Key कॉपी होता है, जिससे एक लापता लेखन होता है, और Self-changed Bundle की शर्त सीधे पूरी हो जाती है।
कारण जानने के बाद, हम प्रतिलिपि बनाना शुरू कर सकते हैं, लेकिन इससे पहले, हमें थोड़ी और विस्तृत जानकारी की आवश्यकता है।