
CVE-2021-26708 के पीछे की बग की जांच
CVE-2021-26708 के पीछे के बग की जाँच
यह रिपॉजिटरी CVE-2021-26708 के बारे में एक छोटा लेख है, और यह बग कैसे Use After Free राइट प्रिमिटिव में बदला जा सकता है। यहाँ दिया गया PoC पूरा एक्सप्लॉइट नहीं है, बल्कि सिर्फ मेरा हारनेस है जिसका उपयोग मैंने इस बग की जाँच करने के लिए किया। यह kmalloc-64 कैश से फ्री होने के बाद एक एंट्री का सफलतापूर्वक उपयोग कर सकता है, लेकिन इसमें मेमोरी ग्रूम करने और स्लॉट में कुछ दिलचस्प रखने का कोई कोड नहीं है।
यह @a13xp0p0v द्वारा रिपोर्ट किया गया एक मजेदार बग है। इसने मेरा ध्यान आकर्षित किया क्योंकि पैच बहुत सरल था, बस 5 अलग-अलग स्थानों पर लॉक के बाहर vsk->transport का रेफरेंस प्राप्त करने से रोकना।
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c518adafa39f37858697ac9309c6cf1805581446
नीचे पैच से use-after-free प्रिमिटिव तक जाने की प्रक्रिया का एक संक्षिप्त वॉकथ्रू है, जिसका उपयोग एक्सप्लॉइटेशन के लिए किया जा सकता है। यह वॉकथ्रू उन लोगों के लिए सहायक होना चाहिए जो इस बग का पता लगाना चाहते हैं।
मैंने linux kernel 5.10.13 डाउनलोड किया और ऊपर दिए गए पैच को मैन्युअल रूप से पूर्ववत किया। कर्नेल बनाने और चलाने के बारे में अधिक जानकारी के लिए, निम्नलिखित एक अच्छा संदर्भ है।
https://fedoraproject.org/wiki/Building_a_custom_kernel
मैंने kgdb के साथ कर्नेल की डिबगिंग को सक्षम करने के लिए बूट पैरामीटर भी संशोधित किए। पहले बनाए गए vmlinux फ़ाइल के साथ gdb का उपयोग करके, मेरे पास मुख्य कर्नेल के लिए सभी कर्नेल सिंबल थे, लेकिन किसी भी लोड करने योग्य कर्नेल मॉड्यूल के लिए नहीं। भेद्यता से जुड़ा कोड डिफ़ॉल्ट रूप से लोड नहीं होता था, लेकिन PF_VSOCK परिवार का उपयोग करने पर कर्नेल में लोड हो जाता था (यह इस बात पर निर्भर करता है कि आपने अपना कर्नेल कैसे बनाया)।
लोड किए गए मॉड्यूल के kgdb में सिंबल प्राप्त करने के लिए, मैंने कम से कम एक बार vsock सॉकेट का उपयोग किया, फिर sudo cat /proc/modules | grep vsock का उपयोग करके संबंधित मॉड्यूल के बेस पते प्राप्त किए। gdb में मैं तब कुछ इस तरह करूंगा (gdb) add-symbol-file ./net/vmw_vsock/vsock.ko 0xffffffffc0567000 ताकि gdb को पता चले कि उस ko फ़ाइल के सिंबल मेमोरी में कहाँ हैं। vsock.ko और vmw_vsock_virtio_transport_common.ko दो सबसे अधिक प्रासंगिक थे।
पैच से पीछे की ओर काम करना मजेदार है, क्योंकि भेद्यता शिकार के विपरीत, आप पहले से जानते हैं कि आप सही जगह पर देख रहे हैं। इस मामले में हम पैच से जानते हैं कि sock_lock प्राप्त करने से पहले ट्रांसपोर्ट का एक रेफरेंस सहेजा जाता है। हम सुरक्षित रूप से उम्मीद कर सकते हैं कि भेद्यता ट्रांसपोर्ट के बदलने के कारण है, लेकिन पुराने रेफरेंस का उपयोग किया जाता है।
इस तरह के परिदृश्य में हम उम्मीद करेंगे कि ट्रांसपोर्ट स्वयं एक गतिशील रूप से आवंटित वस्तु होगी जिसे मुक्त किया जा सकता है और रेफरेंस प्राप्त करने और लॉक धारण करने के बीच किसी अन्य वस्तु से बदला जा सकता है। दुर्भाग्य से जब हम अन्य मॉड्यूल द्वारा कार्यान्वित प्रासंगिक ट्रांसपोर्ट के जीवनकाल को ट्रैक करते हैं, तो वे सभी ग्लोबल मेमोरी में प्रतीत होते हैं। इसलिए हम दायरे से बाहर उपयोग की जाने वाली वस्तुओं के लिए कुछ स्तर गहराई से देखने जा रहे हैं।
af_vsock.c में देखने पर, हम दो स्थान पाते हैं जहाँ vsk->transport को संशोधित किया जाता है। vsock_assign_transport और vsock_deassign_transport में। vsock_assign_transport में हम देख सकते हैं कि यदि कोई भिन्न मौजूदा ट्रांसपोर्ट है, तो नया ट्रांसपोर्ट रखने से पहले vsock_deassign_transport को कॉल किया जाता है।
यदि हम vsk->transport->destruct(vsk) कॉल यहाँ की संभावनाओं को देखते हैं, तो हम देखते हैं कि लूपबैक और वर्टीयो दोनों ट्रांसपोर्ट vsk->trans पैरामीटर को kfree करेंगे यहाँ। बिंगो! यदि हम (1) इस कॉल का एक पथ पा सकते हैं जो (2) एक कमजोर फ़ंक्शन के साथ रेस कर सकता है जो vsk->trans तक पहुँचने के लिए नष्ट होने से पहले के ट्रांसपोर्ट रेफरेंस का उपयोग करता है, तो हमारे पास हमारा प्रिमिटिव होगा।
vsock_deassign_transport के पथ की तलाश करते हुए, हम देखते हैं कि इसे vsock_sk_destruct या vsock_assign_transport से कॉल किया जाता है। vsock_sk_destruct को sock->destruct फ़ंक्शन के रूप में सेट किया गया है, और इसलिए __sys_close, या sock_put, sock_close, या vsock_release जैसी विनाश पथ के साथ अन्य उपलब्ध कॉल यहाँ समाप्त हो सकते हैं।
vsock_assign_transport का सबसे अधिक प्रासंगिक पथ vsock_stream_connect के माध्यम से है, लेकिन इसके लिए सॉकेट को कुछ विशिष्ट अवस्थाओं में होना आवश्यक है, और केवल तभी vsock_deassign_transport को कॉल करता है यदि ट्रांसपोर्ट बदल जाता है। और यदि हम NULL नए ट्रांसपोर्ट के साथ समाप्त नहीं होते हैं, तो यह vsk->trans पैरामीटर को बदल देगा।
इससे पहले कि हम यह पता लगाने में बहुत आगे बढ़ें कि मुक्ति के लिए कौन सा पथ हमारे लिए सही है, हम यह निर्धारित करना चाहते हैं कि एक वैध पथ है जो vsk->trans सदस्य का उपयोग एक अमान्य रेफरेंस के साथ करता है जो एक नष्ट किए गए ट्रांसपोर्ट के लिए है। हम व्यवस्थित रूप से हर उस स्थान की जाँच कर सकते हैं जहाँ ट्रांसपोर्ट का उपयोग संभावित अमान्य रेफरेंस के साथ किया जाता है। उन छिद्रों का पता लगाते हुए हम वे पा सकते हैं जहाँ vsk->trans का उपयोग किया जाता है। सबसे अच्छा पथ vsock_stream_setsockopt यहाँ के माध्यम से प्रतीत होता है, जब transport->notify_buffer_size लूपबैक और वर्टीयो ट्रांसपोर्ट के लिए vsk->trans के अंदर एक ऑफ़सेट पर लिखता है यहीं। यदि trans पहले से मुक्त है जब इसका उपयोग किया जाता है, तो हमें kmalloc-64 आवंटन में 0x28 के ऑफ़सेट पर एक u32 का अच्छा लेखन मिलता है।
उस vsock_stream_setsockopt का प्रिमिटिव के रूप में उपयोग एक रेस पर निर्भर करता है, जहाँ ट्रांसपोर्ट के रेफरेंस को प्राप्त करने और sock_lock प्राप्त करने के बीच का समय होता है। यह एक छोटी सी खिड़की है, और वहाँ पहुँचने के लिए बहुत सारे निर्देश हैं। इसलिए यहाँ हम linux की एक शानदार सुविधा का उपयोग कर सकते हैं जिसे userfaultfd कहा जाता है ताकि हम अपने लिए बेहतर मौका बना सकें। यह तंत्र हमें यूजरमोड में पेजफॉल्ट को अपनी सुविधानुसार संभालने देता है।
देखें https://man7.org/linux/man-pages/man2/ioctl_userfaultfd.2.html और https://man7.org/linux/man-pages/man2/userfaultfd.2.html।
इसके साथ हम एक थ्रेड (गेटर) रख सकते हैं जो sock_lock प्राप्त करेगा और फिर कुछ यूजर मेमोरी तक पहुँचेगा और पेज फॉल्ट का कारण बनेगा। हम उस थ्रेड को जब तक चाहें रोके रख सकते हैं (लॉक अभी भी धारण किया हुआ है)। अन्य थ्रेड जो उस लॉक को प्राप्त करने का प्रयास करेंगे, वे तब तक वहीं रहेंगे जब तक हम गेटर को जारी नहीं करते और लॉक को रिलीज़ नहीं करते। हम अपने उस थ्रेड को लाइन कर सकते हैं जो अंत में डिस्ट्रक्ट करेगा, और वह थ्रेड जो अमान्य रेफरेंस का उपयोग करेगा। दोनों sock_lock पर प्रतीक्षा करेंगे, और अब हमारे पास रेस जीतने का एक बड़ा मौका है। यदि डिस्ट्रक्ट करने वाला थ्रेड अगला लॉक प्राप्त करने के लिए चुना जाता है, तो हमारा setsockopt कॉल बाद में पूरा होगा। यह vsk->trans पॉइंटर का उपयोग करेगा, जो मुक्त होने (और बदलने) के बाद है।
यदि इसके बजाय पहले setsockopt कॉल जाता है, तो हम रेस हार जाते हैं, लेकिन पूरी प्रक्रिया को फिर से सुरक्षित रूप से आज़मा सकते हैं।
जब मैं इसे बनाने की कोशिश कर रहा था, तो मैंने कुछ समय गलत रास्ते पर बिताया। मैंने vsock_deassign_transport को क्लोज़ और टाइमआउट के माध्यम से कॉल करने की कोशिश की, लेकिन बहुत सारे रेफरेंस काउंट जाँचों में भाग गया जो वास्तविक विनाश में तब तक देरी करते थे जब तक कि बहुत देर नहीं हो गई।
एक संक्षिप्त टिप्पणी के रूप में, इन पथों को डिबग करना मुश्किल हो सकता है; जैसा कि आप कल्पना कर सकते हैं, सिस्टम कॉल close पर एक ब्रेकपॉइंट बहुत बार लगेगा। भले ही आप केवल उचित थ्रेड में रुकने के लिए सशर्त ब्रेकपॉइंट का उपयोग करें, मशीन धीमी हो जाएगी। इसके चारों ओर एक दिलचस्प रास्ता ebpf का उपयोग ट्रेसपॉइंट के साथ करना है जो केवल स्थितियाँ सही होने पर bpf_trace_printk को कॉल करेंगे। फिर bpf_trace_printk पर एक kgdb ब्रेकपॉइंट रखा जा सकता है, जो हमें सही स्थान के पास ले जाएगा। यह kprobes के साथ काम नहीं करता क्योंकि आप पहले से ही एक ब्रेकपॉइंट हैंडलर में हैं। मुझे लगता है कि ebpf में bpf_trace_kgdb_break कॉल जोड़ना कर्नेल में एक अच्छा जोड़ हो सकता है।
जब मैंने अंततः vsock_assign_transport पथ को देखना शुरू किया, तो यह जल्दी से एक साथ आ गया। आवश्यकताओं को पूरा करने के लिए, हम पहले VM_ADDR_CID_LOCAL से कनेक्ट करते हैं, जब कोई सुनने वाला सर्वर नहीं होता है। यह हमें लूपबैक ट्रांसपोर्ट देगा, लेकिन फिर जब हमारा कनेक्शन टाइम-आउट या विफल होता है, तो हमारी स्थिति SS_UNCONNECTED पर वापस आ जाएगी। यह हमें एक और कनेक्ट करने की अनुमति देता है, जो VM_ADDR_CID_HOST से बड़े पते पर होता है, जो हमारे ट्रांसपोर्ट को बदलने का कारण बनेगा, हमारे मौजूदा ट्रांसपोर्ट को नष्ट करेगा और मुक्ति का कारण बनेगा। यह महत्वपूर्ण है कि हम ऐसा तब करें जब वास्तव में कोई पंजीकृत transport_g2h या transport_h2g न हो, ताकि हमारा नया ट्रांसपोर्ट NULL हो, और हमारा मुक्त रेफरेंस vsk->trans में बना रहे।
उस सब के साथ लाइन में, हमें एक विश्वसनीय use-after-free मिलता है जिसका उपयोग विशेषाधिकार वृद्धि के लिए किया जा सकता है।
यह रिपॉजिटरी केवल हमारे प्रारंभिक use-after-free तक पहुँचने के बारे में है। लेकिन अब हमारे पास kmalloc-64 कैश में एक ऑफ़सेट पर एक मान लिखने का प्रिमिटिव है, जहाँ पहले virtio_vsock_sock आवंटित किया गया था। मैंने वहाँ वॉकथ्रू को रोकने का फैसला किया क्योंकि, क्या, मुझे यहाँ सब कुछ करना है?