
CVE-2024-49746 के लिए राइटअप और एक्सप्लॉइट: Android का Parcel::continueWrite उन File Descriptors को बंद करता है जो बाद में उपयोग किए जाते हैं
इस समस्या का समाधान CVE-2024-49746 के रूप में सामने आया: बुलेटिन, पैच
उपरोक्त शीर्षक Parcel::continueWrite विधि से एक टिप्पणी है, जो वास्तव में Parcel ऑब्जेक्ट्स का आकार बदलने के लिए जिम्मेदार है, या तो जब उपयोगकर्ता द्वारा स्पष्ट रूप से अनुरोध किया जाता है (उदाहरण के लिए setDataSize() के माध्यम से) या जब वर्तमान डेटा क्षमता बहुत छोटी होने पर किसी write विधि को कॉल किया जाता है।```cpp
status_t Parcel::continueWrite(size_t desired)
{
// SNIP: Validate desired size
// SNIP: Assign kernelFields & rpcFields from variant member of this class
// SNIP: Count number of objects (Binder handles and File Descriptors)
// that will be present after resize and assign to objectsSize
if (mOwner) {
// If the size is going to zero, just release the owner's data.
if (desired == 0) {
freeData();
return NO_ERROR;
}
// If there is a different owner, we need to take
// posession.
uint8_t* data = (uint8_t*)malloc(desired);
// SNIP: Check if malloc succeeded
binder_size_t* objects = nullptr;
if (kernelFields && objectsSize) {
objects = (binder_size_t*)calloc(objectsSize, sizeof(binder_size_t));
// SNIP: Check if calloc succeeded
// Little hack to only acquire references on objects
// we will be keeping.
size_t oldObjectsSize = kernelFields->mObjectsSize;
kernelFields->mObjectsSize = objectsSize;
acquireObjects();
kernelFields->mObjectsSize = oldObjectsSize;
}
// SNIP: rpcFields handling for non-/dev/binder Parcels
if (mData) {
memcpy(data, mData, mDataSize < desired ? mDataSize : desired);
}
if (objects && kernelFields && kernelFields->mObjects) {
memcpy(objects, kernelFields->mObjects, objectsSize * sizeof(binder_size_t));
}
// ALOGI("Freeing data ref of %p (pid=%d)", this, getpid());
if (kernelFields) {
// TODO(b/239222407): This seems wrong. We should only free FDs when
// they are in a truncated section of the parcel.
closeFileDescriptors();
}
mOwner(mData, mDataSize, kernelFields ? kernelFields->mObjects : nullptr,
kernelFields ? kernelFields->mObjectsSize : 0);
mOwner = nullptr;
// SNIP: Allocation count tracking
// SNIP: Assign data and objects to this object
} else if (mData) {
// SNIP: Resize data owned by this instance of Parcel
} else {
// SNIP: Allocate initial data for currently empty Parcel
}
return NO_ERROR;
}
[जब वह टिप्पणी पेश की गई थी](https://android.googlesource.com/platform/frameworks/native/+/53b6ffe5af3951e8784c451ef8c4ff19f3d6b196%5E!/), `closeFileDescriptors()` कॉल को `IPCThreadState::freeBuffer()` (जो उपरोक्त कोड में `mOwner()` फंक्शन पॉइंटर के माध्यम से कॉल किया जाता है) से `continueWrite()` मेथड में ले जाया गया, हालाँकि तर्क पहले जैसा ही था। आखिरकार, `Parcel` Android IPC का मुख्य भाग है और यदि कोर IPC फ़ाइल डिस्क्रिप्टर बंद कर रहा था तो यह स्पष्ट समस्या होनी चाहिए न कि छिपी हुई
जो हमें महत्वपूर्ण भाग तक लाता है: उपरोक्त कोड का उपयोग कब किया जाता है? इसका उपयोग तब किया जाता है जब Parcel वर्ग Binder ड्राइवर से प्राप्त डेटा का स्वामित्व स्थानांतरित करता है (जो उस समय [`/dev/binder` `mmap`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/ProcessState.cpp;l=587-592;drc=187efe18e3de6258af0230198c881915cc695567) में रहता है और इसमें लिखा नहीं जा सकता (उस मेमोरी में लिखने के किसी भी प्रयास से `SIGSEGV` होगा)), अर्थात `Parcel` या तो इनकमिंग ट्रांजेक्शन डेटा होता है (`data` आर्गुमेंट जो [`onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) को पास किया जाता है) या इनकमिंग रिप्लाई (अर्थात, `Parcel` ऑब्जेक्ट जो [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) कॉल में `reply` आर्गुमेंट के रूप में पास किया गया था, `transact()` उस `Parcel` ऑब्जेक्ट के अंदर रेफरेंस सेट करता है)
व्यवहार में, हम `if (mOwner)` ब्लॉक में केवल तभी प्रवेश करेंगे जब सिस्टम [ट्रांजेक्शन डेटा को रिलीज़ करने के लिए `setDataSize(0)` कॉल करता है](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/IPCThreadState.cpp;l=1483-1488;drc=187efe18e3de6258af0230198c881915cc695567), लेकिन उस स्थिति में हम `if (desired == 0)` में भी प्रवेश करेंगे जो जल्दी लौट जाता है। वैध सिस्टम उपयोग के दौरान ऐसा कोई मामला नहीं है जहां हम "यदि कोई भिन्न मालिक है, तो हमें कब्ज़ा लेना होगा" पथ में प्रवेश करें
# "कब्ज़ा लेने" पथ को ट्रिगर करना
अपने पिछले एक्सप्लॉइट में मैंने [वह मामला दिखाया था जहाँ `createFromParcel()` वास्तव में उस `Parcel` पर `writeInt(0)` कॉल कर सकता है जिससे उसे पढ़ना चाहिए](https://github.com/michalbednarski/TheLastBundleMismatch#side-effects)। जबकि वहाँ के फिक्स ने `AccountManagerService` के अंदर किसी भी गैर-`Intent` `createFromParcel()` विधियों के निष्पादन को रोक दिया, `createFromParcel()` से `writeInt(0)` वाला पथ बरकरार रखा गया
पुनर्कथन के लिए, [`PackageParser` के अंदर हमारे पास निम्नलिखित कोड है](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/PackageParser.java;l=7789-7795;drc=7d3ffbae618e9e728644a96647ed709bf39ae759):```java
final Class<T> cls = (Class<T>) Class.forName(componentName);
final Constructor<T> cons = cls.getConstructor(Parcel.class);
intentsList = new ArrayList<>(N);
for (int i = 0; i < N; ++i) {
intentsList.add(cons.newInstance(in));
}
इसलिए, हमारे पास Parcel ऑब्जेक्ट हो सकता है, जो createFromParcel में पारित किया गया था, सिस्टम में उपलब्ध किसी भी public कंस्ट्रक्टर को पारित किया जा सकता है जो एकल Parcel तर्क स्वीकार करता है
और अन्यत्र हमारे पास निम्नलिखित कोड है:```java public PooledStringWriter(Parcel out) { mOut = out; mPool = new HashMap<>(); mStart = out.dataPosition(); out.writeInt(0); // reserve space for final pool size. }
इसलिए, "take possession" पथ को ट्रिगर करने के लिए, हमें `onTransact()` पर `data` के रूप में पारित `Parcel` पर मनमाना `readParcelable` कॉल की आवश्यकता है, इस एक्सप्लॉइट में मैं इसके लिए [वही पथ उपयोग कर रहा हूँ जो मैंने पहले एक अलग में उपयोग किया था](https://github.com/michalbednarski/LeakValue#putting-parcelables-in-system_server-and-retrieving-them)। मेरे पास `readParcelable` कॉल `PackageParser$Activity.CREATOR.createFromParcel()` है, जो बदले में `PooledStringWriter` नाम पढ़ता है और उसका कंस्ट्रक्टर कॉल करता है और उसके बाद `Parcel` डेटा समाप्त हो जाता है, इसलिए `writeInt()` को Parcel को पुनः आवंटित करने की आवश्यकता होती है, जो हमारे "take possession" पथ में प्रवेश करता है
यहाँ ध्यान देने योग्य है, यदि उस बिंदु पर `Parcel` डेटा का अंत नहीं होता, तो `writeInt()` डेटा को उसी स्थान पर अधिलेखित करने का प्रयास करेगा, जो `/dev/binder` `mmap` द्वारा समर्थित डेटा के मामले में `SIGSEGV` की ओर ले जाएगा
# फ़ाइल डिस्क्रिप्टर सैनिटाइज़र