
Pixel7/8 Pro के लिए Android 14 कर्नेल शोषण
यह लेख Mali GPU के भीतर दो कर्नल कमजोरियों का गहन विश्लेषण प्रदान करता है, जो डिफ़ॉल्ट एप्लिकेशन सैंडबॉक्स से पहुंच योग्य हैं, जिन्हें मैंने स्वतंत्र रूप से पहचाना और Google को रिपोर्ट किया। इसमें एक कर्नल एक्सप्लॉइट शामिल है जो मनमाना कर्नल r/w क्षमताएं प्राप्त करता है। परिणामस्वरूप, यह SELinux को अक्षम करता है और निम्नलिखित Android 14 संस्करणों पर चलने वाले Google Pixel 7 और 8 Pro मॉडल पर रूट तक विशेषाधिकार बढ़ाता है:
google/husky/husky:14/UD1A.231105.004/11010374:user/release-keysgoogle/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keysgoogle/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keysgoogle/panther/panther:14/UP1A.231105.003/11010452:user/release-keys (m4b4 (Marcel) द्वारा)यह एक्सप्लॉइट दो कमजोरियों का लाभ उठाता है: gpu_pixel_handle_buffer_liveness_update_ioctl ioctl कमांड में अधूरे पैच के परिणामस्वरूप एक इंटीजर ओवरफ्लो, और टाइमलाइन स्ट्रीम संदेश बफ़र्स के भीतर एक सूचना रिसाव।
Google ने gpu_pixel_handle_buffer_liveness_update_ioctl ioctl कमांड में एक इंटीजर ओवरफ्लो को इस कमिट में संबोधित किया। पहले, जब मैंने इस मुद्दे की रिपोर्ट की, तो मैंने सोचा कि बग पहले वर्णित पैच में किसी समस्या के कारण था। रिपोर्ट की समीक्षा करने के बाद, मुझे एहसास हुआ कि कमजोरी का मेरा विश्लेषण गलत था। पैच के अधूरे होने की मेरी पहली धारणा के बावजूद, यह प्रभावी रूप से गणना में अंडरफ्लो को हल और रोकता है। इससे मुझे संदेह हुआ कि परिवर्तन प्रोडक्शन बिल्ड में लागू नहीं किया गया था। हालाँकि, मैं गणना में अंडरफ्लो पैदा कर सकता हूँ, लेकिन ओवरफ्लो पैदा करना संभव नहीं है। यह सुझाव देता है कि ioctl कमांड को आंशिक रूप से ठीक किया गया है, हालाँकि ऊपर दिखाए गए पैच के साथ नहीं। IDA को देखने से पता चला कि प्रोडक्शन रिलीज़ में एक और अधूरा पैच भेजा गया था, और यह पैच mali gpu कर्नल मॉड्यूल की किसी भी git branch में मौजूद नहीं है।
यह कमजोरी पहली बार नवीनतम Android संस्करण में खोजी गई और 19 नवंबर, 2023 को रिपोर्ट की गई। Google ने बाद में मुझे सूचित किया कि उन्होंने पहले ही इसे आंतरिक रूप से पहचान लिया था और दिसंबर Android सुरक्षा बुलेटिन में इसे CVE-2023-48409 निर्दिष्ट कर दिया था, इसे एक डुप्लिकेट मुद्दे के रूप में लेबल किया। हालाँकि मैं यह सत्यापित करने में सक्षम था कि बग को मेरी रिपोर्ट से महीनों पहले आंतरिक रूप से पहचान लिया गया था, (लगभग 30 अगस्त की कमिट तिथि के आधार पर) फिर भी भ्रम बना हुआ है। विशेष रूप से, यह अजीब है कि सबसे हालिया डिवाइसों के अक्टूबर और नवंबर के सुरक्षा पैच स्तर (SPL) अभी भी इस कमजोरी से प्रभावित थे —मैंने इनसे पहले के संस्करणों की जाँच नहीं की है। इसलिए, मैं निर्णायक रूप से यह निर्धारित करने में असमर्थ हूँ कि क्या यह वास्तव में एक डुप्लिकेट मुद्दा था और क्या उपयुक्त पैच वास्तव में मेरे सबमिशन से पहले दिसंबर के लिए निर्धारित किया गया था या इस कमजोरी को संबोधित करने में कोई अनदेखी थी।
वैसे भी, इस बग को शक्तिशाली बनाने वाली बात निम्नलिखित है:
info.live_ranges पूरी तरह से उपयोगकर्ता-नियंत्रित है।info.live_ranges पॉइंटर buff कर्नल पते की शुरुआत से पहले एक मनमाना ऑफसेट पर हो सके।यह कमजोरी DeCxt::RasterizeScaleBiasData() बफ़र अंडरफ्लो कमजोरी के साथ समानताएं साझा करती है, जिसे मैंने 2022 में iOS 15 कर्नल में खोजा और एक्सप्लॉइट किया था।
Mali GPU एक कस्टम timeline stream लागू करता है जिसे जानकारी एकत्र करने, उसे क्रमबद्ध करने और बाद में एक विशिष्ट प्रारूप का पालन करते हुए रिंग बफ़र में लिखने के लिए डिज़ाइन किया गया है। उपयोगकर्ता kbase_api_tlstream_acquire ioctl कमांड को कॉल करके फ़ाइल डिस्क्रिप्टर प्राप्त कर सकते हैं, जो उन्हें इस रिंग बफ़र से पढ़ने में सक्षम बनाता है। संदेशों का प्रारूप इस प्रकार है:
एक पैकेट हेडर
एक संदेश आईडी
एक क्रमबद्ध संदेश बफ़र, जहां विशिष्ट सामग्री संदेश आईडी पर निर्भर करती है।
उदाहरण के लिए, __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait फ़ंक्शन kbase_kcpu_command_queue और dma_fence कर्नल पॉइंटर्स को संदेश बफ़र में क्रमबद्ध करता है, जिसके परिणामस्वरूप कर्नल पॉइंटर्स यूज़र स्पेस प्रक्रिया में लीक हो जाते हैं।```c
void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait(
struct kbase_tlstream *stream,
const void *kcpu_queue,
const void *fence
)
{
const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT;
const size_t msg_size = sizeof(msg_id) + sizeof(u64)
+ sizeof(kcpu_queue)
+ sizeof(fence)
;
char *buffer;
unsigned long acq_flags;
size_t pos = 0;
buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);
pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id)); pos = kbasep_serialize_timestamp(buffer, pos); pos = kbasep_serialize_bytes(buffer, pos, &kcpu_queue, sizeof(kcpu_queue)); pos = kbasep_serialize_bytes(buffer, pos, &fence, sizeof(fence));
kbase_tlstream_msgbuf_release(stream, acq_flags); }
प्रूफ ऑफ कॉन्सेप्ट एक्सप्लॉइट `kbase_kcpu_command_queue` ऑब्जेक्ट का पता `KBASE_TL_KBASE_NEW_KCPUQUEUE` संदेश आईडी की निगरानी करके लीक करता है, जिसे `kbasep_kcpu_queue_new` फ़ंक्शन द्वारा प्रेषित किया जाता है जब भी कोई नया kcpu कतार ऑब्जेक्ट आवंटित किया जाता है।
Google ने मुझे सूचित किया कि यह भेद्यता मार्च 2023 में रिपोर्ट की गई थी और उनके सुरक्षा बुलेटिन में [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01) निर्दिष्ट की गई थी। फिर भी, मैं अक्टूबर और नवंबर के लिए सुरक्षा पैच स्तर (SPL) के साथ वितरित नवीनतम Pixel उपकरणों पर इस समस्या को दोहराने में सक्षम था, जो दर्शाता है कि सुधार सही ढंग से या बिल्कुल भी लागू नहीं किया गया था। बाद में, Google ने दिसंबर के सुरक्षा अद्यतन बुलेटिन में बिना श्रेय दिए इस समस्या का तुरंत समाधान किया, और बाद में मुझे बताया कि इस मुद्दे को डुप्लिकेट माना गया था। हालाँकि, इस मुद्दे को डुप्लिकेट के रूप में चिह्नित करने का औचित्य संदिग्ध बना हुआ है।
## शोषण
---
तो मेरे पास दो दिलचस्प भेद्यताएँ हैं। पहली आवंटित ~buff~ पते से पहले आने वाले किसी भी 16-बाइट संरेखित कर्नेल पते की सामग्री को संशोधित करने की एक शक्तिशाली क्षमता प्रदान करती है। दूसरी भेद्यता कर्नेल मेमोरी के भीतर वस्तुओं के संभावित स्थानों के बारे में संकेत प्रदान करती है।
### buffer_count और live_ranges_count मानों पर नोट्स
`buffer_count` और `live_ranges_count` फ़ील्ड्स पर पूर्ण नियंत्रण के साथ, मेरे पास लक्षित स्लैब और उस सटीक ऑफसेट को चुनने की सुविधा है जिस पर मैं लिखना चाहता हूँ। हालाँकि, `buffer_count` और `live_ranges_count` के लिए मान चुनने के लिए कई बाधाओं और कारकों के कारण सावधानीपूर्वक विचार करने की आवश्यकता है:
- दोनों मान संबंधित हैं, और ओवरफ्लो केवल तभी होगा जब सभी नई शुरू की गई जाँचों को बायपास किया जाए।
- ऋणात्मक ऑफसेट के 16-बाइट संरेखित होने की आवश्यकता किसी भी चयनित स्थान पर लिखने की क्षमता को प्रतिबंधित करती है। हालाँकि, यह सामान्यतः कोई महत्वपूर्ण बाधा नहीं है।
- बड़े ऑफसेट का चयन करने से बड़ी मात्रा में डेटा मेमोरी के उन क्षेत्रों में लिखा जाता है जो इच्छित लक्ष्य नहीं हो सकते हैं। उदाहरण के लिए, यदि आवंटन आकार `0x3004` तक ओवरफ्लो हो जाता है, तो `live_ranges` पॉइंटर `buff` ऑब्जेक्ट के आवंटित स्थान से `-0x4000` बाइट्स पर सेट हो जाएगा। `copy_from_user` फ़ंक्शन तब `update->live_ranges_count` गुणा 4 की गणना के आधार पर `0x7004` बाइट्स लिखेगा। परिणामस्वरूप, यह ऑपरेशन `live_ranges` पॉइंटर और `buff` आवंटन के बीच के मेमोरी क्षेत्र को उपयोगकर्ता-नियंत्रित डेटा से अधिलेखित कर देगा। इसलिए, यह ध्यानपूर्वक सुनिश्चित करना आवश्यक है कि उस सीमा के भीतर कोई महत्वपूर्ण सिस्टम ऑब्जेक्ट गलती से अधिलेखित न हो। चूँकि इस ऑपरेशन में `copy_from_user` कॉल शामिल है, कोई व्यक्ति संवेदनशील स्थानों पर डेटा लिखे जाने से रोकने के लिए उपयोगकर्ता स्रोत बफर के बाद अवांछित मेमोरी क्षेत्र को जानबूझकर अन-मैप करके `EFAULT` उत्पन्न करने पर विचार कर सकता है। हालाँकि, यह दृष्टिकोण अप्रभावी है, क्योंकि यदि `raw_copy_from_user` फ़ंक्शन विफल हो जाता है, तो यह गंतव्य कर्नेल बफर में शेष बाइट्स को शून्य कर देगा। यह व्यवहार यह सुनिश्चित करने के लिए लागू किया गया है कि किसी त्रुटि के कारण आंशिक प्रतिलिपि के मामले में, शेष कर्नेल बफर में अप्रारंभीकृत डेटा न हो।```c
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
unsigned long res = n;
might_fault();
if (!should_fail_usercopy() && likely(access_ok(from, n))) {
instrument_copy_from_user(to, from, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
return res;
}
इसे ध्यान में रखते हुए, हमें ओवरराइट करने के लिए ऑब्जेक्ट और लिखने के लिए डेटा का सावधानीपूर्वक चयन करना होगा।
चूँकि मैं इस दुर्भाग्यपूर्ण जाँच में फँसा हुआ हूँ, मेरी रणनीति एक ऐसी वस्तु (object) की पहचान करना है कि यदि उसे शून्य (null) कर दिया जाए, तो कोई अवांछित परिणाम उत्पन्न न हो। लेकिन, उस पर पहुँचने से पहले, एक और मुद्दा है जिससे निपटना है। क्या आपको याद है जब मैंने पिछले भाग में कहा था कि मैं कोई भी आवंटन आकार (allocation size) चुन सकता हूँ और इस प्रकार अपने आवंटन बफर की सेवा के लिए कोई भी सामान्य प्रयोजन स्लैब कैश आवंटक (general purpose slab cache allocator) चुन सकता हूँ? यह सही नहीं है, और इसका कारण फिर से copy_from_user है! यह CONFIG_HARDENED_USERCOPY मिटिगेशन के कारण है। यह उस आकार को निर्दिष्ट करने से रोकता है जो संबंधित स्लैब कैश आकार को पूरा नहीं करता है, जहाँ कर्नेल गंतव्य बफर (इस मामले में) हीप ऑब्जेक्ट के रूप में मेल खाता है। यह निर्धारित करता है कि बफर का पेज स्लैब पेज है या नहीं, और यदि ऐसा है, तो यह मेल खाते हुए kmem_cache->size को प्राप्त करता है और यह निर्धारित करता है कि उपयोगकर्ता द्वारा आपूर्ति किया गया आकार उससे अधिक नहीं होगा; अन्यथा, आकार बेमेल होने के कारण कर्नेल क्रैश हो जाता है। दूसरे शब्दों में, मैं उन ऑब्जेक्ट्स को लक्षित नहीं कर सकता जो सामान्य प्रयोजन आवंटक (general purpose allocator) से संबंधित हैं, लेकिन फिर भी मैं उन ऑब्जेक्ट्स को लक्षित कर सकता हूँ जिनका आकार बड़ा है (यानी जो सीधे पेज आवंटक द्वारा सेवित होते हैं)।
सबसे पहला विचार जो मन में आया वह pipe_buffer तकनीक का उपयोग करना था, जो मनमानी रीड/राइट प्रिमिटिव (arbitrary read/write primitives) प्राप्त करने के लिए एक अत्यंत सुरुचिपूर्ण तकनीक है। मैं तकनीक के विवरण में नहीं जाऊँगा, लेकिन पाठकों को Interrupt Labs का यह शानदार ब्लॉग पढ़ने के लिए प्रोत्साहित किया जाता है। पाइप ऑब्जेक्ट का निर्माण करते समय, pipe_buffer ऑब्जेक्ट प्रारंभ में 16 तत्वों की एक सरणी (array) में बनाया जाता है; हालाँकि, सरणी के आकार को fcntl(F_SETPIPE_SZ) का उपयोग करके समायोजित किया जा सकता है। इसलिए, pipe_buffer सरणी आवंटन को इस प्रकार समायोजित किया जा सकता है कि इसे पेज आवंटक से सेवित किया जा सके, जिससे यह हमला करने के लिए एक आदर्श लक्ष्य वस्तु बन जाती है।
pipe_buffer ऑब्जेक्ट को लक्ष्य उम्मीदवार के रूप में चुनने के बाद, कर्नेल r/w प्राप्त करने की दिशा में अगला कदम अंडरफ्लो भेद्यता के साथ इसकी सामग्री को ओवरराइट करना है, जो मुझे किसी भी मेमोरी स्थान से/तक पढ़ने/लिखने की अनुमति देगा जिसका पेज pipe_buffer->page फ़ील्ड को ओवरराइट कर रहा है।
चूँकि भेद्यता मुझे मनमाना डेटा लिखने की अनुमति देती है, मैं 'pipe_buffer' की पूरी सामग्री को नियंत्रित कर सकता हूँ, जिसमें इसका पेज फ़ील्ड भी शामिल है, और ऐसा करने के लिए, मुझे भेद्य kbuff ऑब्जेक्ट से पहले pipe_buffer सरणी आवंटित करनी होगी और उन्हें एक-दूसरे के बगल में होना चाहिए।
मैंने कर्नेल मेमोरी पर बहुत सारे kbase_kcpu_command_queue ऑब्जेक्ट्स का स्प्रे किया और फिर उनके बाद बहुत सारे pipe_buffer सरणियाँ रखीं।
मैं pipe_buffer सरणियों का उपयोग अकेले स्प्रे के प्राथमिक स्रोत के रूप में नहीं कर सकता, क्योंकि pipe_max_size द्वारा लगाई गई सीमा है। इसलिए, मैंने kbase_kcpu_command_queue ऑब्जेक्ट के साथ स्प्रे शुरू करने का निर्णय लिया। इस ऑब्जेक्ट को चुनने के दो कारण थे: इसका आवंटन आकार 0x38C8 है, इसलिए इसे पेज आवंटक द्वारा नियंत्रित किया जाता है, और मैं इसका कर्नेल पता सूचना कर्नेल लीक बग का उपयोग करके निश्चित रूप से प्राप्त कर सकता हूँ, जिससे यह स्प्रे करने के लिए एक अच्छा ऑब्जेक्ट और लक्षित करने के लिए भी एक अच्छा ऑब्जेक्ट बन जाता है (जैसा कि हम अगले भाग में देखेंगे)।
जैसा कि पहले उल्लेख किया गया है, मैंने pipe_buffer सरणी आवंटन का आकार बढ़ाने के लिए fcntl(F_SETPIPE_SZ) का उपयोग किया ताकि इसे पेज आवंटक द्वारा सेवित किया जा सके। अधिक स्पष्ट रूप से, मैंने आवंटन आकार को ==0x4000 bytes (4 * PAGE_SIZE)== चुना ताकि यह kbase_kcpu_command_queue आवंटनों के अनुरूप हो।
pipe_buffer का ठीक से उपयोग करने के लिए, एक पेज पते की आवश्यकता होती है। उस kbase_kcpu_command_queue ऑब्जेक्ट के कर्नेल पते की पहचान करने में सक्षम होना, जिसे मैं जानबूझकर बना और नष्ट कर सकता हूँ, इसे उपयोग करने के लिए एक अच्छा उम्मीदवार बनाता है, और इसके मेल खाते struct page को virt_to_page . का उपयोग करके प्राप्त किया जा सकता है।
तो pipe_buffer ऑब्जेक्ट इस प्रकार है:```c
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
जैसा कि पहले उल्लेख किया गया है, `page` फ़ील्ड में एक मान्य पेज पता शामिल होना चाहिए। `offset` और `len` फ़ील्ड `PAGE_SIZE` से अधिक नहीं होने चाहिए, अन्यथा pipe head/tail काउंटरों को बढ़ा देगा, जिसके परिणामस्वरूप एक नए `pipe_buffer` ऑब्जेक्ट का उपयोग होगा और नकली pipe buffer पर नियंत्रण समाप्त हो जाएगा।
इसके अलावा, `flags` को `PIPE_BUF_FLAG_CAN_MERGE` होना चाहिए, ताकि बाद के `pipe_write` कॉल, head counter को आँख मूंदकर बढ़ाने और अगले pipe buffer का उपयोग करने के बजाय, पहले जाँच करें कि क्या वर्तमान `pipe_buffer` में लिखने के अनुरोध को समायोजित करने के लिए पर्याप्त जगह है, और यदि है, तो वे केवल उसी pipe buffer में `len` फ़ील्ड में संग्रहीत मान से शुरू करके डेटा जोड़ देंगे।
`pipe_buf_confirm` पर डिवाइस को क्रैश होने से बचाने के लिए, जिसे `pipe_write` और `pipe_read`’ द्वारा कॉल किया जाता है, `ops` पॉइंटर को भी एक मान्य कर्नेल पता होना चाहिए जिसमें `ops->confirm` फ़ील्ड _NULL_ पर सेट हो। मैं आसानी से लीक हुए `kbase_kcpu_command_queue` ऑब्जेक्ट के भीतर एक ऐसे ऑफसेट का उपयोग कर सकता हूँ जो NULL है और किसी भी परिस्थिति में नहीं बदलेगा।
### अंडरफ्लो के लिए इष्टतम ऑफसेट मान चुनना
जबकि `buff` ,`kbase_kcpu_command_queue` और `pipe_buffer` के आवंटन आकार ~0x4000~ बाइट्स हैं, मैंने बफर को **0x8000** बाइट्स के साथ अंडरफ्लो करना चुना। क्यों ?
आइए संक्षेप में देखें कि रीड और राइट ऑपरेशन के दौरान `pipe_buffers` को कैसे अपडेट किया जाता है। मान लें कि हम `pipe_buffer` को इस तरह आकार दे सकते हैं:```c
struct pipe_buffer {
.page = virt_to_page(addr),
.offset = 0,
.len = 0x40,
.ops = kcpu_addr + 0x50,
.flags = PIPE_BUF_FLAG_CAN_MERGE,
unsigned long private = 0
};
जबकि यह बग इस object की सामग्री पर मनमाना नियंत्रण देने की क्षमता देता है, यह केवल एक बार ऐसा करता है क्योंकि अंडरफ्लो हुआ object ioctl कॉल समाप्त होते ही तुरंत मुक्त कर दिया जाता है। यह वास्तव में एक समस्या पैदा करता है क्योंकि मुझे pipe_buffer object को फिर से उपयोग योग्य बनाने के लिए इसे मैन्युअल रूप से अपडेट करना पड़ता है, क्योंकि प्रत्येक pipe read/write ऑपरेशन:
.page फ़ील्ड अपडेट नहीं होती; यह वही रहती है, और जब बफर खाली होता है, तो इसे रिलीज़ कर दिया जाता है, जो मैं नहीं चाहता क्योंकि .ops फ़ील्ड सही ढंग से सेट नहीं है।pipe_buffer रीड ऑपरेशन पर .offset फ़ील्ड को अपडेट करता है, इसलिए मैं उसी मेमोरी क्षेत्र को दोबारा नहीं पढ़ सकता।pipe_buffer में लिखा गया डेटा बफर में .len मान से शुरू होकर जोड़ा जाएगा (यह मानते हुए कि PIPE_BUF_FLAG_CAN_MERGE फ़्लैग सेट है) और .len तदनुसार अपडेट होगा। अर्थात, हम डेटा को एक ही सटीक पते पर दो बार नहीं लिख सकते।परिणामस्वरूप, जब तक मैं प्रत्येक read या write ऑपरेशन के बाद pipe_buffer को ठीक से अपडेट नहीं करता, मैं एक ही समय में एक ही pipe से पढ़ और लिख नहीं सकता। इसीलिए 0x8000 बाइट्स के साथ अंडरफ्लो करना कहीं अधिक व्यावहारिक है, क्योंकि एक single pipe_buffer को अधिलेखित करने के बजाय, मैं दो अलग-अलग pipes objects के दो अलग-अलग pipe_buffer instances को अधिलेखित करूँगा: एक read के लिए और दूसरा write ऑपरेशन के लिए माना जाएगा।```c
#define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */
pipe_read = (struct pipe_buffer *)( ptr); pipe_read->page = virt_to_page(ta->kcpu_kaddr); pipe_read->offset = 0; pipe_read->len = 0xfff; pipe_read->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_read->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_read->private = 0;
pipe_write = (struct pipe_buffer )( ptr + 0x4000); pipe_write->page = virt_to_page(ta->kcpu_kaddr); pipe_write->offset = 0; pipe_write->len = 0; / This is the starting position of the pipe_write */ pipe_write->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_write->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_write->private = 0;
The `pipe_read` एक नकली पाइप बफर है जिसका उपयोग लक्षित पेज से `.offset = 0` से शुरू करके `0xfff` बाइट्स तक डेटा पढ़ने के लिए किया जाएगा, जबकि `pipe_write` एक नकली `pipe_buffer` है जिसका उपयोग `.len = 0` से शुरू करके `0xfff` बाइट्स तक डेटा लिखने के लिए किया जाएगा।
यह भी दोबारा बताना बहुत महत्वपूर्ण है कि `PAGE_SIZE` से अधिक बाइट्स लिखने पर पाइप हेड काउंटर को बढ़ाने के लिए मजबूर होगा, जिससे नए सिरे से आवंटित `pipe_buffer` उपयोग में आएगा और हमारे नकली `pipe_write` पर नियंत्रण खत्म हो जाएगा। दूसरी ओर, `fake_read` बफर को खाली करने (उसमें से `0xfff` डेटा पढ़ने) से कर्नेल को `ops→release` कॉल करके वास्तविक पेज को रिलीज़ करने का निर्देश मिलता है, जिससे कर्नेल क्रैश हो जाता है क्योंकि मेरे पास अभी भी कर्नेल टेक्स्ट पता नहीं है।
हालाँकि मैं पाइप रीड और राइट ऑपरेशनों को अलग करने में सफल रहा ताकि एक पाइप छोर पर लिखना दूसरे पाइप बफर में हस्तक्षेप न करे और न ही विपरीत दिशा में, फिर भी मैंने मूल समस्या हल नहीं की है: पाइप बफर को विश्वसनीय रूप से कैसे अपडेट किया जाए? स्पष्ट उत्तर जो दिमाग में आया वह था प्रत्येक पाइप रीड या राइट कॉल के बाद स्प्रे प्रक्रिया को बार-बार दोहराना। और यह कोई मतलब नहीं रखता क्योंकि इसका एक्सप्लॉइट विश्वसनीयता पर महत्वपूर्ण प्रभाव पड़ता। अगले भाग में, मैं लक्ष्य को दो उप-लक्ष्यों में विभाजित करूँगा: शुरुआत में, मैं केवल `.page` फ़ील्ड पर ध्यान केंद्रित करूँगा, और उसके बाद `.len/.offset` फ़ील्ड पर।
### pipe_buffer→page फ़ील्ड को संशोधित करना
मुझे आश्चर्य हुआ कि मुझे `.page` को बिल्कुल भी अपडेट करने की आवश्यकता नहीं है, क्योंकि मैं `pipe_buffer→page` को लीक हुए `kbase_kcpu_command_queue` के पेज पते पर इंगित करने के लिए ओवरराइट कर सकता हूँ। इसलिए, **मुझे बस `kbase_kcpu_command_queue` ऑब्जेक्ट को रिलीज़ करना है और इसे एक नए `pipe_buffer` ऑब्जेक्ट के साथ ओवरलैप करना है। हाँ! अब मेरे पास एक `pipe_buffer→page` है जो एक वैध `pipe_buffer` ऑब्जेक्ट की ओर इंगित करता है!**
`kbase_kcpu_command_queue` को `pipe_buffer` से बदलने से हमें एक वैध पाइप बफर में हेरफेर करने की क्षमता मिलती है, बिना नियमित रूप से `.page` फ़ील्ड को अपडेट किए। हालाँकि, मुझे अभी भी `.len` और `.offset` फ़ील्ड से निपटना है।
### pipe_buffer→len/offset फ़ील्ड को संशोधित करना
जैसा कि मैंने पहले बताया है, पाइप रीड/राइट करने से `.len` और `.offset` फ़ील्ड अपडेट हो जाती हैं, जिससे उसी पेज पर बाद के रीड/राइट ऑपरेशन अनुपयोगी हो जाते हैं, भले ही वे दो अलग-अलग पाइपों पर किए जाएँ। यहाँ एक और ट्रिक है: **`.len/.offset` फ़ील्ड को छुए बिना भी डेटा पढ़ने/लिखने की एक तकनीक है!**। और इसे `pipe_read/write` पर `copy_page_from_iter` और `copy_page_to_iter` कॉल्स को फॉल्ट करके प्राप्त किया जा सकता है! हाँ, बिल्कुल `copy_to/from_user` की तरह, `copy_page_to/from_iter` यूज़र-स्पेस से/में डेटा कॉपी करता है जो `iov_iter` संरचना के माध्यम से पारित किया जाता है, और इसे फॉल्ट किया जा सकता है।
पिछले उदाहरण को जारी रखते हुए, यदि हम किसी पते पर 8 बाइट डेटा लिखना चाहते हैं, तो प्रदान किए गए यूज़र स्पेस बफर का आकार 8 होना चाहिए, उसके बाद मेमोरी का एक अनमैप्ड या गैर-पठनीय क्षेत्र होना चाहिए, और फिर `9` को `write` सिस्टम कॉल में आकार तर्क के रूप में पास करें, जो उस डेटा की मात्रा को दर्शाता है जिसे हम लिखना चाहते हैं। यह ऑपरेशन 8 बाइट्स लिखेगा और _नौवें_ बाइट पर विफल हो जाएगा क्योंकि उसे एक अनमैप्ड/अपठनीय मेमोरी स्थान मिलता है। परिणामस्वरूप, डेटा प्रभावी रूप से गंतव्य कर्नेल बफर में लिखा जा चुका होता है और `.len` फ़ील्ड संशोधित नहीं हुई होती है। `pipe_write` कर्नेल फ़ंक्शन बिना `buf->len` फ़ील्ड को अपडेट किए ही वापस लौट जाएगा।```c
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
ret = pipe_buf_confirm(pipe, buf);
if (ret)
goto out;
ret = copy_page_from_iter(buf->page, offset, chars, from);
if (unlikely(ret < chars)) {
ret = -EFAULT;
goto out;
}
buf->len += ret;
if (!iov_iter_count(from))
goto out;
}
यही बात रीड ऑपरेशन्स पर भी लागू होती है; अगर हम 8 बाइट्स पढ़ना चाहते हैं, तो बफर के नौवें बाइट को अपठनीय बना दें और फिर केवल यह दावा करें कि हम 9 बाइट्स पढ़ना चाहते हैं, डेटा बिना .offset फ़ील्ड को बदले यूज़र बफर में कॉपी हो जाएगा।
परिणामस्वरूप, हम बिना बार-बार स्प्रे प्रक्रिया से गुज़रे किसी भी कर्नेल मेमोरी एड्रेस पर असीमित रीड/राइट ऑपरेशन करने में सक्षम हैं।
अब जब मेरे पास एक मज़बूत आर्बिट्रेरी रीड/राइट प्रिमिटिव है, मैंने Interrupt Labs ब्लॉग पोस्ट में बताई गई तकनीक का उपयोग करके कर्नेल टेक्स्ट प्रारंभिक एड्रेस निर्धारित करने के लिए VMEMMAP_START ऐरे में सभी struct page को देखा। फिर मुझे एहसास हुआ कि Android November Security Updates में init_task को नल कर दिया गया है, इसलिए मैंने इसके बजाय kthreadd_task का उपयोग किया। kthreadd_task कर्नेल एड्रेस होने से मैं task->tasks सूची को ट्रैवर्स कर सका और अपना स्वयं का current टास्क कर्नेल एड्रेस प्राप्त कर सका, फिर रूट विशेषाधिकार प्राप्त करने के लिए cred संरचना को शून्य कर दिया।
बाद में, मुझे एहसास हुआ कि सभी पेज एड्रेस को स्कैन करना अनावश्यक था क्योंकि मेरे पास पहले से ही pipe_buffer ऑब्जेक्ट से anon_pipe_buf_ops कर्नेल टेक्स्ट एड्रेस था। इस जानकारी के साथ, मैं कर्नेल टेक्स्ट बेस एड्रेस का अनुमान लगा सकता था, जिससे KASLR को प्रभावी ढंग से बायपास किया जा सकता था।
एक्सप्लॉइट SELinux को भी अक्षम करता है, कर्नेल टेक्स्ट बेस एड्रेस के साथ, मुझे केवल selinux_state ग्लोबल संरचना का स्थान खोजना होगा और फिर .enforcing मान को शून्य करना होगा।
रिपोर्ट के साथ आया प्रूफ ऑफ कॉन्सेप्ट Pixel 7 और 8 Pro डिवाइसों पर Android 14 के साथ October और November ASBs पर परीक्षण किया गया, जिसमें लगभग 100% सफलता दर हासिल हुई। यह उल्लेख करना भी महत्वपूर्ण है कि कुछ हार्डकोडेड ऑफसेट्स के उपयोग के कारण यह एक्सप्लॉइट अन्य डिवाइसों पर सीधे (out of the box) काम नहीं करेगा। किसी नए डिवाइस के लिए समर्थन जोड़ने हेतु, निम्नलिखित प्रदान करना आवश्यक है:
kthreadd_task ऑफसेट।selinux_state ऑफसेट।task_struct->cred, task_struct->pid और task_struct->tasks संरचना ऑफसेट।anon_pipe_buf_ops ऑफसेट।एक्सप्लॉइट को स्टैंडअलोन बाइनरी के रूप में संकलित करने के लिए निम्न कमांड का उपयोग करें, फिर इसे चलाने के लिए adb shell का उपयोग करें:```sh
$ aarch64-linux-androidXX-clang++ -static-libstdc++ -w -Wno-c++11-narrowing -DUSE_STANDALONE -o poc poc.cpp -llog
$ adb push poc /data/local/tmp/
$ adb shell /data/local/tmp/poc
आप इस निर्देशिका को इसमें एम्बेड करके Android Studio ऐप के माध्यम से भी एक्सप्लॉइट चला सकते हैं और cmake फ़ाइल में `-w -Wno-c++11-narrowing` जोड़कर अनावश्यक C++ चेतावनियों को अक्षम करना सुनिश्चित करें।
### डेमो```shell
$ adb logcat |grep -i EXPLOIT
11-28 16:04:12.500 7989 7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc
11-28 16:04:15.563 7989 7989 E EXPLOIT : [+] Got the kcpu_id (0) kernel address = 0xffffff8901390000 from context (0x0)
11-28 16:04:18.441 7989 7989 E EXPLOIT : [+] Got the kcpu_id (255) kernel address = 0xffffff89b0bf8000 from context (0xff)
11-28 16:04:18.442 7989 7989 E EXPLOIT : [+] Found corrupted pipe with size 0xfff
11-28 16:04:18.442 7989 7989 E EXPLOIT : [+] SUCCESS! we have a fake pipe_buffer (0)!
11-28 16:04:18.444 7989 7989 E EXPLOIT : 10 00 39 01 89 FF FF FF 10 00 39 01 89 FF FF FF | ..9.......9.....
11-28 16:04:18.444 7989 7989 E EXPLOIT : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
11-28 16:04:18.444 7989 7989 E EXPLOIT : 00 B0 CD 12 C0 FF FF FF 00 00 00 00 00 00 00 00 | ................
11-28 16:04:18.444 7989 7989 E EXPLOIT : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................
11-28 16:04:18.445 7989 7989 E EXPLOIT : [+] Freeing kcpu_id = 0 (0xffffff8901390000)
11-28 16:04:18.446 7989 7989 E EXPLOIT : [+] Allocating 61 pipes with 256 slots
11-28 16:04:18.462 7989 7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer
11-28 16:04:18.463 7989 7989 E EXPLOIT : 40 AB BA 26 FE FF FF FF 00 00 00 00 30 00 00 00 | @..&........0...
11-28 16:04:18.463 7989 7989 E EXPLOIT : 70 37 8D F1 DA FF FF FF 10 00 00 00 00 00 00 00 | p7..............
11-28 16:04:18.463 7989 7989 E EXPLOIT : 00 00 00 00 00 00 00 00 | ........
11-28 16:04:18.463 7989 7989 E EXPLOIT : [+] pipe_buffer {.page = 0xfffffffe26baab40, .offset = 0x0, .len = 0x30, ops = 0xffffffdaf18d3770}
11-28 16:04:18.463 7989 7989 E EXPLOIT : [+] kernel base = 0xffffffdaf0010000, kthreadd_task = 0xffffff8002da3780 selinux_state = 0xffffffdaf28a3168
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c80
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 0
11-28 16:04:20.097 7989 7989 E EXPLOIT : [+] Successfully disabled SELinux
11-28 16:04:20.102 7989 7989 E EXPLOIT : [+] Cleanup ... OK