
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` की ओर ले जाएगा
# फ़ाइल डिस्क्रिप्टर सैनिटाइज़र
मेरा प्रारंभिक विचार "take possession" पथ को फ़ाइल डिस्क्रिप्टर बंद करने के लिए था, जिसके बाद लेन-देन के अंत में वही डिस्क्रिप्टर फिर से बंद हो जाएंगे, लेकिन उन घटनाओं के बीच मैं `system_server` के अंदर एक अन्य लेन-देन में एक और फ़ाइल डिस्क्रिप्टर रखूंगा और बाद में अपना फ़ाइल डिस्क्रिप्टर वापस प्राप्त करूंगा, क्योंकि वह FD उस बिंदु पर एक अलग फ़ाइल को संदर्भित करता है
यह पुराने AOSP संस्करण का उपयोग करके मेरे एमुलेटर पर काम कर गया, हालांकि जब मैंने अधिक हाल के संस्करण के साथ प्रयास किया तो वह योजना [फ़ाइल डिस्क्रिप्टर सैनिटाइज़र (FDSan)](https://android.googlesource.com/platform/bionic/+/refs/heads/main/docs/fdsan.md) द्वारा रोक दी गई
विशेष रूप से, [`android-14.0.0_r29` में FDSan कवरेज को बढ़ाकर `Parcel` के अंदर FD को कवर करने के लिए किया गया](https://android.googlesource.com/platform/frameworks/native/+/7772039cc5084247450f6113d9a18eca17f672aa%5E!/)
वास्तव में, जब FDSan ने Parcel को कवर किया, तो मैं `Parcel` में FD होने पर `closeFileDescriptors()` कॉल तक पहुँचने में भी सक्षम नहीं था। उस कॉल से पहले, `acquireObjects();` का कॉल होता है, जो `Binder` हैंडल के संदर्भ प्राप्त करता है (जो बाद में उस फ़ंक्शन के भीतर `mOwner()` कॉल द्वारा जारी किए जाते हैं), हालांकि `acquireObjects()` [FD के लिए FDSan टैग भी सेट करेगा](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=175-178;drc=f4c9b48c19f1b040efb35932b322f47e7779cafe):```cpp
case BINDER_TYPE_FD:
if (obj.cookie != 0) { // owned
FdTag(obj.handle, nullptr, who);
}
बात यह है कि हम पहले से ही कर्नेल से प्राप्त FD को टैग कर चुके हैं```cpp // In Parcel::ipcSetDataReference, which assigns this Parcel object to data from kernel (mOwner != null) if (type == BINDER_TYPE_FD) { // FDs from the kernel are always owned FdTag(flat->handle, nullptr, this); }
तो, डबल-`FdTag` (बिना बंद किए या टैग बदले जबकि अपेक्षित पुराना टैग निर्दिष्ट किया गया है) से FDSan त्रुटि उत्पन्न होती है, जो प्रक्रिया को समाप्त कर देती है, और हम `closeFileDescriptors()` कॉल तक पहुँच भी नहीं पाते। चूंकि सामान्य उपयोग के दौरान "कब्ज़ा लेने" का पथ मृत कोड है, ऐसी समस्याएं अनजान रह सकती हैं
हालांकि, हम `if (obj.cookie != 0)` शर्त देख सकते हैं। यदि यह गलत है तो इसका मतलब है कि `Parcel` के अंदर मौजूद FD वास्तव में उस `Parcel` के स्वामित्व में नहीं है और यह `Parcel` उपयोगकर्ता की जिम्मेदारी है कि वे उन्हें तब तक खुला रखें जब तक वह `Parcel` मौजूद है। लेकिन चूंकि यह `Parcel` अभी-अभी कर्नेल से आया है, `cookie` मान वास्तव में मूल प्रक्रिया से आते हैं और इन्हें अप्रासंगिक माना जाता है जब `Parcel` के पास वास्तव में `mOwner` हो। लेकिन "कब्ज़ा लेने" का पथ वास्तव में इस पर विचार नहीं करता और केवल `cookie` मानों को कॉपी करेगा
इन सबको एक साथ जोड़ते हुए, प्रेषक पक्ष पर `cookie` मानों को शून्य पर सेट करके, हम एक ऐसा `Parcel` प्राप्त कर सकते हैं जो बंद किए गए फ़ाइल डिस्क्रिप्टर को संदर्भित करता है, लेकिन उन्हें अपना स्वामित्व भी नहीं मानता, जिसका अर्थ है कि वह उन्हें फिर से बंद नहीं करेगा। इससे हम FDSan को ट्रिगर करने से बच सकते हैं, हालांकि यह डबल-क्लोज़ शोषण पथों को भी समाप्त कर देता है
# Parcel के Java पक्ष पर चालें
ऐसे FD अभी भी किसी अन्य `Parcel` (और फिर किसी अन्य प्रक्रिया) को पास किए जा सकते हैं, हालांकि ऐसे लटकते FD बनाने का हमारा तरीका रिफ्लेक्शन के माध्यम से `PooledStringWriter` के निर्माण को शामिल करता है, जिसके बाद जब हम इसे `ArrayList<IntentInfo>` में `add()` करने का प्रयास करते हैं तो `ClassCastException` फेंकी जाती है
हमें यह करना होगा:
* `onTransact()` को `data` तर्क के रूप में पारित किए गए `Parcel` के अंत में हों
* `PooledStringWriter` का निर्माण करें, जिसके बाद Parcel में लटकते FD होंगे, लेकिन `ClassCastException` भी फेंकी जाएगी
* उन FD के लिए प्रतीक्षा करें जिन्हें हम लीक करना चाहते हैं, `system_server` के अंदर आवंटित किए जाएं
* उस `Parcel` से FD को किसी अन्य `Parcel` में कॉपी किया जाए जो हमारी प्रक्रिया को भेजा जाएगा
इन सभी आवश्यकताओं को पूरा करने के लिए मुझे पुरानी और नई दोनों तरकीबों का उपयोग करना होगा
## पुरानी तरकीबें
आइए पुरानी तरकीबों पर दोबारा गौर करके शुरू करते हैं, उनमें से अधिकांश को मेरे [`LazyValue`-using-`Parcel`-after-`recycle()` exploit](https://github.com/michalbednarski/LeakValue) में पहले ही वर्णित किया जा चुका है
1. [`RemoteViews` वर्ग `Parcel.ReadWriteHelper` सेट के साथ निहित `Bundle` का डिसेरियलाइज़ेशन करता है](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/RemoteViews.java;l=2287-2300;drc=9b2e54f25456f2726ab1a15e6b6dc19395a3b5b4)। [जब `ReadWriteHelper` सेट होता है तो `Bundle`-s को आलसी (lazily) के बजाय उत्सुकता से (eagerly) डिसेरियलाइज़ किया जाता है](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=1886-1896;drc=e1841f84f41213879e1f1b45ad4300b96970e545)। यह भी ध्यान देने योग्य है कि उस `Bundle` के अंदर `RemoteViews` के अंतर्गत मौजूद सभी `Bundle`-s भी उत्सुकता से डिसेरियलाइज़ किए जाएंगे, न कि केवल वह जो सीधे `RemoteViews` के अंदर है
2. [यदि `system_server` के अंदर `Bundle` को डिसेरियलाइज़ करते समय `BadParcelableException` फेंकी जाती है, तो वह `BadParcelableException` चुपचाप पकड़ ली जाएगी और `Bundle` की सामग्री साफ़ कर दी जाएगी](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/BaseBundle.java;l=479-485;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e)। ध्यान दें कि हमारा write-in-`createFromParcel` ट्रिगर `ClassCastException` फेंकता है जो यहाँ नहीं पकड़ा जाएगा
3. [`ParceledListSlice` वर्ग डिसेरियलाइज़ेशन के दौरान सीरियलाइज़्ड डेटा में निर्दिष्ट ऑब्जेक्ट के लिए ब्लॉकिंग आउटगोइंग Binder कॉल करता है](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=95-102;drc=e220b578ebc0885a28b83c95cb9ec78581bd8364), जिसका उपयोग हम डिसेरियलाइज़ेशन निष्पादन को रोकने के लिए कर सकते हैं
अब इन सभी की आवश्यकता होगी, लेकिन यही सब कुछ नहीं है
## नई तरकीबें
[AIDL RPC इंटरफ़ेस कार्यान्वयन उत्पन्न करने का उपकरण है](https://developer.android.com/guide/components/aidl), हालाँकि इसके अतिरिक्त यह `Parcelable` संरचना कार्यान्वयन भी उत्पन्न करने में सक्षम है
ये संरचनाएँ लंबाई-उपसर्ग (length-prefixed) होती हैं, इसलिए एक ही संरचना के विभिन्न संस्करण सिस्टम में तब तक संगत होते हैं जब तक बीच में कोई फ़ील्ड नहीं जोड़ी जाती है (अर्थात, संस्करण संगत होते हैं यदि एक संस्करण दूसरे का उपसर्ग है)
आइए देखें कि [`ReceiverInfo` संरचना](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/ReceiverInfo.aidl) के लिए AIDL ने क्या कोड उत्पन्न किया है:```java
public final void readFromParcel(android.os.Parcel _aidl_parcel)
{
int _aidl_start_pos = _aidl_parcel.dataPosition();
int _aidl_parcelable_size = _aidl_parcel.readInt();
try {
if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
intent = _aidl_parcel.readTypedObject(android.content.Intent.CREATOR);
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
data = _aidl_parcel.readString();
if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
extras = _aidl_parcel.readTypedObject(android.os.Bundle.CREATOR);
// SNIP: Other fields
} finally {
if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
throw new android.os.BadParcelableException("Overflow in the size of parcelable");
}
_aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
}
}
यह कोड हमें शेष दो तरकीबें करने की अनुमति देगा, जिनकी हमें "take possession" पथ को ट्रिगर करने के बाद आगे की कार्रवाई करने के लिए आवश्यकता होगी, जिसके लिए हमें Parcel के अंत में होना होगा और यह ClassCastException फेंकता है
सबसे पहले, हम Parcel से लंबाई पढ़ रहे हैं, इसका उपयोग उन फ़ील्ड्स को छोड़ने के लिए करते हैं जो लिखे गए संस्करण में मौजूद नहीं हैं और अंत में हम उस लंबाई के आधार पर Parcel के भीतर स्थिति को समायोजित करेंगे। हमें उस AIDL Parcelable की स्थिति से पहले जाने की अनुमति नहीं है (और Parcel के अंत से आगे जाना संभव है, हालांकि इससे वास्तव में कुछ खतरनाक नहीं होता)। हालांकि हम पहले से पढ़ी गई वस्तु के बीच में जा सकते हैं, ताकि हम पहली बार पढ़ने के दौरान Parcel के अंत तक पहुंच सकें, PooledStringWriter का निर्माण कर सकें, फिर इस ReceiverInfo ऑब्जेक्ट के अंदर किसी चीज़ के बीच में वापस आ सकें
यह Parcel-के-अंत-होने की समस्या का ध्यान रखता है, अभी भी ClassCastException की समस्या बाकी है। यहाँ हमारे पास हालांकि एक finally ब्लॉक है जो setDataPosition() को कॉल करने से पहले यह मान्य करता है कि कोई अतिप्रवाह नहीं है। यदि अतिप्रवाह होता है तो BadParcelableException फेंका जाएगा। अब, क्या होता है यदि finally ब्लॉक एक अपवाद फेंकता है जबकि दूसरा लंबित हो? finally के अंदर फेंका गया अपवाद प्राथमिकता लेता है, पिछला अपवाद चुपचाप गायब हो जाता है। अब ClassCastException के बजाय हमारे पास BadParcelableException है, जिसे Bundle मददगार रूप से अनदेखा करता है ताकि system_server के भीतर अपवादों से बचा जा सके
ParcelableListBinder पैच पर ध्यान देंहम MediaSession के ParcelableListBinder का उपयोग करके system_server के अंदर मनमाना Parcelable डाल रहे हैं और बाद में उस ऑब्जेक्ट को वापस प्राप्त कर रहे हैं
हाल ही में ParcelableListBinder पैच आया था जो ठीक यही रोकता है
इस शोषण के दृष्टिकोण से, यह पैच वास्तव में मुझे नहीं रोकता, लेकिन मुझे इसकी उपस्थिति और अनुपस्थिति को अलग-अलग तरीके से संभालने की आवश्यकता है
यदि वह पैच मौजूद है, तो गैर-QueueItem को system_server द्वारा प्राप्त सूची से चुपचाप हटा दिया जाता है (अपवाद पैदा नहीं करते)। चूँकि हमें साइड इफेक्ट के लिए गैर-QueueItem की आवश्यकता है, हम गैर-QueueItem को पहली वस्तु के रूप में रख सकते हैं और फिर उसके बाद एक वास्तविक QueueItem रख सकते हैं, जो हमें मनमाना Parcelable डीसेरियलाइज़ेशन करने की अनुमति नहीं देता, हालांकि इसमें एक Bundle होता है जिसमें फ़ाइल डिस्क्रिप्टर हो सकते हैं (जिन्हें हम अपनी प्रक्रिया में पास करना चाहेंगे)
यदि वह पैच अनुपस्थित है तो हमारे सामने वह बाधा नहीं है, हालांकि हम उसी प्रवाह का उपयोग भी नहीं कर सकते। चूँकि ऊपर के मामले में हमारे पास RemoteViews और QueueItem दोनों वाली सूची होगी, इसलिए ParceledListSlice मिश्रित-प्रकार की सूची के स्थानांतरण से इनकार कर देगा। उस मामले में मुझे अपने लीक हुए FD को RemoteViews इंस्टेंस के अंदर रखना होगा
हालांकि इस पैच के बारे में अधिक दिलचस्प बात यह है कि इसे क्यों लागू किया गया, कमिट संदेश में "एप्लिकेशन को पृष्ठभूमि से शुरू करने की अनुमति देना" का उल्लेख है और जबकि Android सुरक्षा बुलेटिन बहुत अधिक नहीं कहता, हम CVE प्रविष्टि में उपयोगी जानकारी पा सकते हैं, जो Notification.mAllowlistToken फ़ील्ड को संदर्भित करता है, जो पढ़ने के दौरान स्थैतिक फ़ील्ड से लिया जा सकता है, जो system_server के अंदर पृष्ठभूमि गतिविधि प्रारंभ करने की अनुमति देने वाला टोकन है और बाद में वह टोकन Notification.writeToParcel() में लिखा जाएगा। क्या इसका मतलब है कि अब वे सभी मामले जहाँ system_server मनमाना Parcelable डीसेरियलाइज़ करता है और वापस एप्लिकेशन को भेजता है, कमजोरियाँ हैं? वैसे भी, अभी के लिए यह सिर्फ एक विचार है, इस शोषण में मैं वैसे भी और अधिक करना चाहता हूँ
मुझे लगता है कि यह शोषण अब तक की सबसे जटिल Parcelable गैजेट श्रृंखला प्रस्तुत करता है:
RemoteViews (1)
ReflectionAction (2)
Bundle
Parcelable[] (3)
ReceiverInfo (खोज के लिए, 4 और 10)
Intent
ComponentName (सेगमेंट A, 5)
ParceledListSlice (11)ParcelableParcel या QueueItem (12)Bundle (पकड़ने के लिए, सेगमेंट B, 6)
ReceiverInfo (पुनः फेंकने के लिए, 7)
Bundle (8)
उपरोक्त सूची में "सेगमेंट A" और "B" टिप्पणियाँ मेरी FdLeaker.java क्लास में "START A"/"END A"/"START B"/"END B" टिप्पणियों के बीच के ब्लॉक को संदर्भित करती हैं, संख्याएँ नीचे की सूची में बिंदुओं को संदर्भित करती हैं
उपरोक्त पेड़ प्रेषक के दृष्टिकोण से पदानुक्रम का वर्णन करता है, प्राप्तकर्ता के दृष्टिकोण से यह थोड़ा अलग दिखता है:
ParcelableListBinder से डेटा प्राप्त कर रहे हैं, सबसे बाहरी ऑब्जेक्ट RemoteViews हैRemoteViews में एक नेस्टेड Bundle है। RemoteViews Parcel.ReadWriteHelper सेट करेगा ताकि यह Bundle और उसके भीतर के सभी Bundle-s को उत्सुकता से पढ़ा जाए। यह आवश्यक है क्योंकि अन्यथा हम ReceiverInfo.readFromParcel() से मनमाना readParcelable निष्पादित नहीं कर पाएंगेParcelable[] यहाँ केवल एक सुविधा रैपर है ताकि उन सभी Parcelable-s को समूहित किया जा सके जिन्हें मैं RemoteViews के अंदर डाल रहा हूँआइए ऊपर के हमले को उच्च स्तर से देखें:
system_server के भीतर खोला जाता है, इसका एक संदर्भ याद रखा जाता है और FD बंद कर दिया जाता हैsystem_server को एक अलग फ़ाइल डिस्क्रिप्टर खोलने के लिए ट्रिगर करता हूँइस हमले की एक महत्वपूर्ण सीमा है: हम उन फ़ाइल डिस्क्रिप्टर को नहीं पकड़ सकते जो हमारे हमले के शुरू होने से पहले खोले गए थे
हालांकि अभी भी कुछ उपयोगी चीज़ें हैं जो हम कर सकते हैं
InputChannel को पकड़नासच कहूँ तो, यह एकमात्र शोषण संस्करण है जिसे मैं बिना अतिरिक्त मान्यताओं के काम करवा पाया हूँ
इनपुट इवेंट, यानी टच स्क्रीन और कीबोर्ड से आने वाले इवेंट, system_server से UNIX सॉकेट के माध्यम से एप्लिकेशन द्वारा प्राप्त किए जाते हैं। जब Activity शुरू होती है या सिस्टम में नई विंडो जोड़ी जाती है, तो एक नया InputChannel बनाया जाता है, जिसका अर्थ है कि UNIX सॉकेट जोड़ी बनाई जाती है और एक सिरा एप्लिकेशन को भेजा जाता है और दूसरे का उपयोग system_server द्वारा इवेंट भेजने के लिए किया जाता है
इन सॉकेट्स पर भेजी जाने वाली संरचनाओं का लेआउट अच्छी तरह से परिभाषित है (क्योंकि वे 32-बिट और 64-बिट प्रक्रियाओं के बीच संगत होनी चाहिए) और ऐसा लगता है कि अप्रत्याशित अनुक्रम संख्याओं से कुछ भी परेशान नहीं होता। इसके अलावा, InputChannel system_server के अंदर startActivity() कॉल के बाद आवंटित एकमात्र सॉकेट प्रतीत होता है, इसलिए यह आसानी से निर्धारित किया जा सकता है कि कौन सा FD InputChannel का सर्वर-साइड सॉकेट है

जबकि यह एक खिलौना उदाहरण है, हम अनुमति प्रॉम्प्ट या एप्लिकेशन इंस्टॉलेशन को भी अनुमोदित कर सकते हैं, मीडिया प्रोजेक्शन या एक्सेसिबिलिटी सर्विस सक्षम कर सकते हैं
zygote से कनेक्शन पकड़नायह ज्यादातर सैद्धांतिक है, मैं धीमी गति से चलने वाले एम्यूलेटर पर यह हमला करने में सक्षम था, हालांकि वास्तविक डिवाइस पर रेस विंडो बहुत छोटी थी
system_server /dev/socket/zygote से कनेक्शन केवल एक बार स्टार्टअप पर खोलता है, उसके बाद सभी अनुरोध उस कनेक्शन का उपयोग करके भेजे जाते हैं
system_server बूट के दौरान, MediaSessionService (जिसका उपयोग system_server को/से Parcelable-s भेजने और प्राप्त करने के लिए किया जाता है) को zygote से कनेक्शन स्थापित होने से पहले servicemanager में प्रकाशित किया जाता है
इसलिए, सैद्धांतिक रूप से, एप्लिकेशन के लिए द्वितीयक प्रक्रिया शुरू करना, system_server को क्रैश करना और फिर उस पृष्ठभूमि प्रक्रिया से system_server स्टार्टअप के दौरान हमला करना संभव है
SensorService के माध्यम से zygote से कनेक्शन पकड़नातो एक और बग है जो मुझे मिला, यहाँ SensorService::createSensorDirectConnection() विधि है```cpp
sp SensorService::createSensorDirectConnection(
const String16& opPackageName, int deviceId, uint32_t size, int32_t type, int32_t format,
const native_handle *resource) {
// SNIP: Reject direct connections when sensor privacy is enabled
// SNIP: Irrelevant parameter checks
// check specific to memory type
switch(type) {
case SENSOR_DIRECT_MEM_TYPE_ASHMEM: { // channel backed by ashmem
if (resource->numFds < 1) {
ALOGE("Ashmem direct channel requires a memory region to be supplied");
android_errorWriteLog(0x534e4554, "70986337"); // SafetyNet
return nullptr;
}
// SNIP: Further validation for SENSOR_DIRECT_MEM_TYPE_ASHMEM
}
case SENSOR_DIRECT_MEM_TYPE_GRALLOC:
// no specific checks for gralloc
break;
default:
ALOGE("Unknown direct connection memory type %d", type);
return nullptr;
}
native_handle_t *clone = native_handle_clone(resource);
if (!clone) {
return nullptr;
}
native_handle_set_fdsan_tag(clone);
sp<SensorDirectConnection> conn;
int channelHandle = 0;
if (deviceId == RuntimeSensor::DEFAULT_DEVICE_ID) {
// SNIP: usual case where sensor belong to this device (not app streaming)
} else {
auto runtimeSensorCallback = mRuntimeSensorCallbacks.find(deviceId);
if (runtimeSensorCallback == mRuntimeSensorCallbacks.end()) {
ALOGE("Runtime sensor callback for deviceId %d not found", deviceId);
} else {
int fd = dup(clone->data[0]);
channelHandle = runtimeSensorCallback->second->onDirectChannelCreated(fd);
}
}
// SNIP: Return connection
}
हमारे पास `dup(clone->data[0])` कॉल है। `clone` एक `native_handle_t` है जो रिमोट प्रक्रिया से प्राप्त होता है। नेटिव हैंडल में डेटा के अंदर निश्चित संख्या में FDs और निश्चित संख्या में सादे पूर्णांक (प्लेन इंटीजर) होते हैं, जिनकी मात्रा नेटिव हैंडल के उपयोगकर्ता को `data` तक पहुँचने से पहले [`numFds` और `numInts` में देखकर](https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/cutils/native_handle.h;l=37-38;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e) जांचनी चाहिए।
यहाँ हमारे पास "मेमोरी प्रकार के लिए विशिष्ट जांच" नामक अनुभाग भी है, जो जाँचता है कि `SENSOR_DIRECT_MEM_TYPE_ASHMEM` के लिए यह यहाँ जाँचा जाता है, `SENSOR_DIRECT_MEM_TYPE_GRALLOC` के लिए नेटिव हैंडल का प्रारूप डिवाइस-विशिष्ट है और यहाँ मान्य नहीं किया जा सकता। बात यह है कि "रनटाइम सेंसर" के लिए प्रकार को हमेशा `SENSOR_DIRECT_MEM_TYPE_ASHMEM` माना जाता है, लेकिन हम `SENSOR_DIRECT_MEM_TYPE_GRALLOC` निर्दिष्ट करके उस मामले में सत्यापन को बायपास कर सकते हैं।
हालाँकि यह कोड केवल तभी पहुँच योग्य है जब "रनटाइम सेंसर" मौजूद हों। मुझे यकीन नहीं है कि वास्तव में ऐसा किस स्थिति में होता है, मुझे लगता है कि यह तब होता है जब उपयोगकर्ता "Nearby app streaming" का उपयोग करता है (?)
परीक्षण के लिए, हालाँकि, मैं एक छोटी क्लास जोड़ रहा हूँ जो [`VirtualDevice`](https://developer.android.com/reference/android/companion/virtual/package-summary) के पंजीकरण की अनुमति देती है, आप इसका उपयोग कर सकते हैं```sh
adb shell 'CLASSPATH=$(pm path com.example.thisseemswrong | cut -d: -f2) app_process / com.example.thisseemswrong.VirtualDeviceReg'
उसके बाद, उत्पादन उपकरण पर system uid के रूप में कोड निष्पादित करना संभव है

PackageParser$Activity (9)
PooledStringWriterReceiverInfo में एक परिभाषित लंबाई है जो उसके डेटा के बीच में पड़ती है, हालांकि पढ़ने के दौरान यह अभी ज्ञात नहीं है और Intent को इससे सामान्य रूप से पढ़ा जाता हैComponentName.readFromParcel() द्वारा किए गए readString कॉल के माध्यम से पढ़ा जाता है (Android संस्करण की परवाह किए बिना UTF-16 readString का उपयोग करने के लिए ComponentName का उपयोग करते हुए, वह readString अभी भी null लौटाएगा क्योंकि वह स्ट्रिंग Binder ऑब्जेक्ट्स को ओवरलैप करती है, इसलिए जबकि ComponentName का निर्माण इस पास से छिपे डेटा को छोड़ चुका है, ComponentName ऑब्जेक्ट नहीं बनताBundle में प्रवेश करते हैं, इस Bundle के डीसेरियलाइज़ेशन के अंत में एक BadParcelableException पकड़ा जाएगाReceiverInfo में प्रवेश करते हैं। इस ReceiverInfo की लंबाई Integer.MAX_VALUE के रूप में निर्दिष्ट है और इसलिए अंततः ब्लॉक में BadParcelableException फेंकेगा, चुपचाप ClassCastException को त्याग देगाBundle। यह केवल ReceiverInfo से मनमाना readParcelable तक पहुँचने में सक्षम होने के लिए है, जो अब हम कर सकते हैं क्योंकि हम RemoteViews के अंदर हैं इसलिए Bundle उत्सुकता से पढ़ा जाता हैPackageParser$Activity + PooledStringWriter संयोजन उस Parcel पर writeInt(0) कॉल को ट्रिगर करता है जिससे पढ़ा जा रहा है। जैसे ही हम Parcel के अंत में थे, writeInt() को Parcel की क्षमता का विस्तार करना होगा, जो "take possession" पथ को ट्रिगर करता है। यह संयोजन ClassCastException की ओर भी ले जाता है, जिसे ऊपर चरण 7 और 6 में वर्णित अनुसार निगल लिया जाता हैReceiverInfo के अंत तक पहुँचते हैं, ReceiverInfo अपने हेडर में लंबाई के अनुसार स्थिति की तलाश करता है और उस डेटा के अंदर आता है जो पहले ComponentName के अंदर था, जिसे Parcelable[] में अगली वस्तुओं के रूप में पढ़ा जाता हैParceledListSlice है जो मेरी प्रक्रिया के लिए एक अवरुद्ध Binder लेन-देन करता है। इस बिंदु पर इस Parcel में परिभाषित फ़ाइल डिस्क्रिप्टर बंद कर दिए गए थे, लेकिन अभी तक उनके स्थान पर कुछ भी दिलचस्प नहीं आया है। जब यह डीसेरियलाइज़ेशन इस कॉल से वापसी की प्रतीक्षा करता है, मैं सिस्टम को कुछ दिलचस्प फ़ाइल डिस्क्रिप्टर खोलने के लिए बना सकता हूँ जो फिर मुझे भेजे जाएँगेParcelableListBinder आइटम को फ़िल्टर करता है या नहीं
a. यदि ParcelableListBinder आइटम को फ़िल्टर नहीं करता, तो FD को ParcelableParcel के अंदर सहेजा जाता है, जो Bundle के समान ही Parcel.appendFrom() का उपयोग करके Parcel डेटा को शब्दशः कॉपी करता है, लेकिन इसमें विशेष hasReadWriteHelper() तर्क नहीं है इसलिए यह RemoteViews Bundle के अधीन होने के बावजूद ऐसा करता है
b. यदि ParcelableListBinder आइटम को फ़िल्टर करता है, तो यह RemoteViews का अंत है, RemoteViews ऑब्जेक्ट को ParcelableListBinder द्वारा त्याग दिया जाता है, लेकिन यह मेरे लिए ठीक है क्योंकि साइड इफेक्ट पहले ही हो चुके हैं। ParcelableListBinder द्वारा प्राप्त अगली वस्तु QueueItem है, जिसमें MediaDescription होता है, जिसमें बदले में Bundle होता है, जहाँ मेरे लीक हुए FD रखे जाते हैं