
यह CVE-2024-6387 के लिए मेरे द्वारा लिखा गया POC है।
Qualys सुरक्षा सलाह
regreSSHion: OpenSSH के सर्वर में RCE, glibc-आधारित Linux सिस्टम पर (CVE-2024-6387)
सारांश SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, 2005 से)
बस विश्वास की एक छलांग ही काफी है
-- The Interrupters, "Leap of Faith"
प्रारंभिक टिप्पणी: OpenSSH दुनिया के सबसे सुरक्षित सॉफ़्टवेयर में से एक है; यह भेद्यता एक अन्यथा लगभग त्रुटिहीन कार्यान्वयन में एक छोटी सी चूक है। इसका defense-in-depth डिज़ाइन और कोड एक आदर्श और प्रेरणा हैं, और हम OpenSSH के डेवलपर्स को उनके अनुकरणीय कार्य के लिए धन्यवाद देते हैं।
हमने OpenSSH के सर्वर (sshd) में एक भेद्यता (सिग्नल हैंडलर रेस कंडीशन) खोजी है: यदि कोई क्लाइंट LoginGraceTime सेकंड (डिफ़ॉल्ट रूप से 120, पुराने OpenSSH संस्करणों में 600) के भीतर प्रमाणित नहीं करता है, तो sshd का SIGALRM हैंडलर अतुल्यकालिक रूप से कॉल होता है, लेकिन यह सिग्नल हैंडलर कई ऐसे फ़ंक्शन कॉल करता है जो async-signal-safe नहीं हैं (उदाहरण के लिए, syslog())। यह रेस कंडीशन sshd को उसकी डिफ़ॉल्ट कॉन्फ़िगरेशन में प्रभावित करती है।
जाँच करने पर, हमें एहसास हुआ कि यह भेद्यता वास्तव में CVE-2006-5051 की प्रतिगमन (regression) है ("OpenSSH 4.4 से पहले सिग्नल हैंडलर रेस कंडीशन दूरस्थ हमलावरों को सेवा से वंचित (क्रैश) करने और संभवतः मनमाना कोड निष्पादित करने की अनुमति देता है"), जिसे 2006 में Mark Dowd ने रिपोर्ट किया था।
यह प्रतिगमन अक्टूबर 2020 (OpenSSH 8.5p1) में कमिट 752250c ("OpenSSH के लिए संशोधित लॉग इंफ्रास्ट्रक्चर") द्वारा पेश किया गया था, जिसने गलती से sigdie() से एक "#ifdef DO_LOG_SAFE_IN_SIGHAND" हटा दिया, जो एक ऐसा फ़ंक्शन है जिसे sshd के SIGALRM हैंडलर द्वारा सीधे कॉल किया जाता है। दूसरे शब्दों में:
OpenSSH < 4.4p1 इस सिग्नल हैंडलर रेस कंडीशन के लिए संवेदनशील है, यदि CVE-2006-5051 के विरुद्ध बैकपोर्ट-पैच नहीं किया गया है, या CVE-2008-4109 के विरुद्ध पैच नहीं किया गया है, जो CVE-2006-5051 का गलत सुधार था;
4.4p1 <= OpenSSH < 8.5p1 इस सिग्नल हैंडलर रेस कंडीशन के लिए संवेदनशील नहीं है (क्योंकि CVE-2006-5051 के पैच द्वारा sigdie() में जोड़ा गया "#ifdef DO_LOG_SAFE_IN_SIGHAND" इस असुरक्षित फ़ंक्शन को सुरक्षित _exit(1) कॉल में बदल देता है);
8.5p1 <= OpenSSH < 9.8p1 इस सिग्नल हैंडलर रेस कंडीशन के लिए फिर से संवेदनशील है (क्योंकि "#ifdef DO_LOG_SAFE_IN_SIGHAND" गलती से sigdie() से हटा दिया गया था)।
यह भेद्यता glibc-आधारित Linux सिस्टम पर दूरस्थ रूप से शोषणीय है, जहाँ syslog() स्वयं async-signal-असुरक्षित फ़ंक्शन (उदाहरण के लिए, malloc() और free()) कॉल करता है: एक बिना प्रमाणीकरण के रूट के रूप में दूरस्थ कोड निष्पादन, क्योंकि यह sshd के विशेषाधिकार प्राप्त कोड को प्रभावित करता है, जो सैंडबॉक्स नहीं है और पूर्ण विशेषाधिकारों के साथ चलता है। हमने किसी अन्य libc या ऑपरेटिंग सिस्टम की जाँच नहीं की है; लेकिन OpenBSD विशेष रूप से संवेदनशील नहीं है, क्योंकि इसका SIGALRM हैंडलर syslog_r() को कॉल करता है, जो syslog() का अधिक async-signal-सुरक्षित संस्करण है, जिसे OpenBSD ने 2001 में बनाया था।
इस भेद्यता का दूरस्थ रूप से शोषण करने के लिए (हमारी जानकारी के अनुसार, CVE-2006-5051 पहले कभी सफलतापूर्वक शोषित नहीं किया गया है), हमने एक दूरदर्शी पेपर, "Delivering Signals for Fun and Profit" से प्रेरणा ली, जिसे 2001 में Michal Zalewski ने प्रकाशित किया था:
फिर भी, हमें तुरंत तीन बड़ी समस्याओं का सामना करना पड़ा:
सैद्धांतिक दृष्टिकोण से, हमें एक उपयोगी कोड पथ खोजना होगा, जो यदि SIGALRM द्वारा सही समय पर बाधित होता है, तो sshd को असंगत स्थिति में छोड़ देता है, और फिर हमें SIGALRM हैंडलर के अंदर इस असंगत स्थिति का शोषण करना होगा।
व्यावहारिक दृष्टिकोण से, हमें sshd में इस उपयोगी कोड पथ तक पहुँचने का एक तरीका खोजना होगा, और इसे सही समय पर बाधित करने की अपनी संभावनाओं को अधिकतम करना होगा।
समय-निर्धारण दृष्टिकोण से, हमें दूरस्थ रूप से इस उपयोगी कोड पथ को सही समय पर बाधित करने की अपनी संभावनाओं को और बढ़ाने का एक तरीका खोजना होगा।
इन तीन समस्याओं पर ध्यान केंद्रित करने के लिए, बिना तुरंत सभी आधुनिक ऑपरेटिंग सिस्टम सुरक्षाओं (विशेष रूप से, ASLR और NX) से लड़े बिना, हमने पहले पुराने OpenSSH संस्करणों को i386 पर शोषित करने का निर्णय लिया, और फिर इस अनुभव के आधार पर, हाल के संस्करणों को:
पहला, "SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3", "debian-30r6-dvd-i386-binary-1_NONUS.iso" से: यह पहला Debian संस्करण है जिसमें विशेषाधिकार पृथक्करण (privilege separation) डिफ़ॉल्ट रूप से सक्षम है और जो उस युग की सभी गंभीर भेद्यताओं (विशेष रूप से, CVE-2003-0693 और CVE-2002-0640) के विरुद्ध पैच किया गया है।
इस संस्करण का दूरस्थ रूप से शोषण करने के लिए, हम SIGALRM के साथ free() कॉल को बाधित करते हैं (sshd के सार्वजनिक-कुंजी पार्सिंग कोड के अंदर), हीप को असंगत स्थिति में छोड़ देते हैं, और SIGALRM हैंडलर के अंदर free() की एक और कॉल के दौरान इस असंगत स्थिति का शोषण करते हैं।
हमारे प्रयोगों में, इस रेस कंडीशन को जीतने के लिए औसतन ~10,000 प्रयास लगते हैं; अर्थात, प्रति 600 सेकंड (LoginGraceTime) में 10 कनेक्शन (MaxStartups) स्वीकार किए जाने पर, एक दूरस्थ रूट शेल प्राप्त करने में औसतन ~1 सप्ताह लगता है।
दूसरा, "SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3", "ubuntu-6.06.1-server-i386.iso" से: यह अंतिम Ubuntu संस्करण है जो अभी भी CVE-2006-5051 ("OpenSSH 4.4 से पहले सिग्नल हैंडलर रेस कंडीशन") के लिए संवेदनशील है।
इस संस्करण का दूरस्थ रूप से शोषण करने के लिए, हम SIGALRM के साथ pam_start() कॉल को बाधित करते हैं, PAM की एक संरचना को असंगत स्थिति में छोड़ देते हैं, और SIGALRM हैंडलर के अंदर pam_end() कॉल के दौरान इस असंगत स्थिति का शोषण करते हैं।
हमारे प्रयोगों में, इस रेस कंडीशन को जीतने के लिए औसतन ~10,000 प्रयास लगते हैं; अर्थात, प्रति 120 सेकंड (LoginGraceTime) में 10 कनेक्शन (MaxStartups) स्वीकार किए जाने पर, एक दूरस्थ रूट शेल प्राप्त करने में औसतन ~1-2 दिन लगते हैं।
अंत में, "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2", "debian-12.5.0-i386-DVD-1.iso" से: यह वर्तमान Debian स्थिर संस्करण है, और यह CVE-2006-5051 की प्रतिगमन के लिए संवेदनशील है।
इस संस्करण का दूरस्थ रूप से शोषण करने के लिए, हम SIGALRM के साथ malloc() कॉल को बाधित करते हैं (sshd के सार्वजनिक-कुंजी पार्सिंग कोड के अंदर), हीप को असंगत स्थिति में छोड़ देते हैं, और SIGALRM हैंडलर के अंदर malloc() की एक और कॉल के दौरान (अधिक सटीक रूप से, syslog() के अंदर) इस असंगत स्थिति का शोषण करते हैं।
हमारे प्रयोगों में, इस रेस कंडीशन को जीतने के लिए औसतन ~10,000 प्रयास लगते हैं, इसलिए प्रति 120 सेकंड (LoginGraceTime) में 100 कनेक्शन (MaxStartups) स्वीकार किए जाने पर ~3-4 घंटे लगते हैं। अंततः, एक दूरस्थ रूट शेल प्राप्त करने में औसतन ~6-8 घंटे लगते हैं, क्योंकि हम केवल आधे समय glibc का पता सही ढंग से अनुमान लगा सकते हैं (ASLR के कारण)।
यह शोध अभी भी कार्य प्रगति पर है:
हमने केवल वर्चुअल मशीनों को लक्षित किया है, बेयर-मेटल सर्वरों को नहीं, ज्यादातर स्थिर नेटवर्क लिंक पर (~10ms पैकेट जिटर);
हमें विश्वास है कि हमारे एक्सप्लॉइट्स के विभिन्न पहलुओं में काफी सुधार किया जा सकता है;
हमने amd64 एक्सप्लॉइट पर काम शुरू कर दिया है, जो मजबूत ASLR के कारण बहुत कठिन है।
amd64 पर अपना काम शुरू करने के कुछ दिनों बाद, हमने OpenSSH के सार्वजनिक Bugzilla में निम्नलिखित बग रिपोर्ट देखी, जो sshd के SIGALRM हैंडलर में डेडलॉक के बारे में थी:
इसलिए हमने OpenSSH के डेवलपर्स से तुरंत संपर्क करने का निर्णय लिया (उन्हें बताने के लिए कि यह डेडलॉक एक शोषणीय भेद्यता के कारण होता है), हमने अपना amd64 काम रोक दिया, और यह सलाह (advisory) लिखना शुरू कर दिया।
लेकिन यह मेरी तरह नहीं है, मैं आज़ाद हो रहा हूँ
-- The Interrupters, "Haven't Seen the Last of Me"
इस OpenSSH संस्करण का SIGALRM हैंडलर packet_close() कॉल करता है, जो buffer_free() कॉल करता है, जो xfree() और इस प्रकार free() कॉल करता है, जो async-signal-safe नहीं है:
परिणामस्वरूप, हमने इस Debian के glibc (2.2.5) का malloc कोड पढ़ना शुरू किया, यह देखने के लिए कि क्या free() की पहली कॉल को SIGALRM द्वारा बाधित किया जा सकता है और SIGALRM हैंडलर के अंदर free() की दूसरी कॉल के दौरान शोषित किया जा सकता है (ऊपर पंक्तियाँ 341-344)। क्योंकि इस glibc का malloc, 2000 में Solar Designer द्वारा अग्रणी की गई unlink() तकनीक के विरुद्ध कठोर नहीं है, हमने तुरंत chunk_free() (जो आंतरिक रूप से free() द्वारा कॉल किया जाता है) में एक दिलचस्प कोड पथ देखा:
इस कोड पथ का शोषण करने के लिए, हम sshd के हीप को निम्नलिखित लेआउट के साथ व्यवस्थित करते हैं (chunk_X, chunk_Y और chunk_Z मेमोरी के malloc() किए गए चंक हैं, और p, s, f, b उनके prev_size, size, fd और bk फ़ील्ड हैं):
-----|---+---------------|---+---------------|---+---------------|----- ... |p|s|f|b| chunk_X |p|s|f|b| chunk_Y |p|s|f|b| chunk_Z | ... -----|---+---------------|---+---------------|---+---------------|----- |<------------->| user data
पहला, यदि free(chunk_Y) की कॉल को SIGALRM द्वारा पंक्ति 3246 के बाद लेकिन पंक्ति 3251 से पहले बाधित किया जाता है, तो chunk_Y पहले से ही मुक्त (free) के रूप में चिह्नित है (क्योंकि chunk_Z का PREV_INUSE बिट पंक्ति 3246 पर साफ़ हो जाता है) लेकिन यह अभी तक अपनी doubly-linked सूची में जुड़ा नहीं है (पंक्ति 3251 पर): दूसरे शब्दों में, chunk_Y के fd और bk पॉइंटर्स में अभी भी उपयोगकर्ता डेटा (हमलावर-नियंत्रित डेटा) होता है।
दूसरा, यदि (SIGALRM हैंडलर के अंदर) packet_close() free(chunk_X) कॉल करता है, तो पंक्तियों 3230-3244 पर कोड ब्लॉक में प्रवेश किया जाता है (क्योंकि chunk_Y मुक्त के रूप में चिह्नित है) और chunk_Y को unlink() किया जाता है (पंक्ति 3241 पर): एक तथाकथित aa4bmo प्रिमिटिव (लगभग मनमाना 4 बाइट मिरर किया गया ओवरराइट), क्योंकि chunk_Y के fd और bk पॉइंटर्स अभी भी हमलावर-नियंत्रित हैं। unlink() तकनीक और aa4bmo प्रिमिटिव के बारे में अधिक जानकारी के लिए:
https://www.openwall.com/articles/JPEG-COM-Marker-Vulnerability#exploit http://phrack.org/issues/61/6.html#article
अंतिम, इस aa4bmo प्रिमिटिव के साथ हम glibc के __free_hook फ़ंक्शन पॉइंटर को (इस पुराने Debian संस्करण में ASLR नहीं है, न ही NX) हीप में अपने शेलकोड के पते के साथ अधिलेखित करते हैं, इस प्रकार packet_close() में free() की अगली कॉल के दौरान दूरस्थ कोड निष्पादन प्राप्त करते हैं।
अब वे कब्ज़ा कर रहे हैं और उन्हें पूर्ण नियंत्रण मिल गया है
-- The Interrupters, "Liberty"
sshd के विरुद्ध इस हमले को आरूढ़ करने के लिए, हम DSA सार्वजनिक कुंजी के sshd के पार्सिंग कोड के अंदर free() की एक कॉल को बाधित करते हैं (अर्थात, नीचे पंक्ति 144 हमारा free(chunk_Y) है) और इसे packet_close() में free() कॉलों में से एक के दौरान शोषित करते हैं (अर्थात, ऊपर पंक्तियों 341-344 में से एक हमारा free(chunk_X) है):
हालाँकि, शुरू में हम इस रेस कंडीशन को कभी जीतने में सक्षम नहीं थे (अर्थात, सही समय पर पंक्ति 144 पर free() कॉल को बाधित करना)। आखिरकार, हमें एहसास हुआ कि हम इस रेस को जीतने की अपनी संभावनाओं में काफी सुधार कर सकते हैं: DSA सार्वजनिक-कुंजी पार्सिंग कोड हमें free() को चार बार कॉल करने की अनुमति देता है (नीचे पंक्तियाँ 704-707), और इसके अलावा sshd हमें छह उपयोगकर्ता प्रमाणीकरण (AUTH_FAIL_MAX) का प्रयास करने की अनुमति देता है; यदि इन 24 free() कॉलों में से कोई एक सही समय पर बाधित होती है, तो बाद में हम SIGALRM हैंडलर के अंदर दूरस्थ कोड निष्पादन प्राप्त करते हैं।
इस सुधार के साथ, हमने आखिरकार ~1 महीने के बाद रेस कंडीशन जीत ली: हम खुश थे (और रूट-शेल वाला नृत्य किया), लेकिन हमने यह भी महसूस किया कि सुधार की अभी भी गुंजाइश थी।
चिंता मत करो, बस इंतज़ार करो और देखो
-- The Interrupters, "Haven't Seen the Last of Me"
इसलिए हमने निम्नलिखित तिहरी टाइमिंग रणनीति लागू की:
हम sshd को अपना (काफी बड़ा) DSA सार्वजनिक-कुंजी पैकेट भेजने के लिए अंतिम क्षण तक प्रतीक्षा नहीं करते हैं: इसके बजाय, हम LoginGraceTime से काफी पहले पूरा पैकेट माइनस एक बाइट (अंतिम बाइट) भेजते हैं, और बहुत अंतिम बाइट अंतिम क्षण में भेजते हैं, ताकि नेटवर्क विलंब के प्रभावों को कम किया जा सके। (और हम Nagle एल्गोरिथ्म को अक्षम करते हैं।)
हम माध्यिका राउंड-ट्रिप समय का ट्रैक रखते हैं (नियमित रूप से ऐसे पैकेट भेजकर जो sshd से प्रतिक्रिया उत्पन्न करते हैं), और उस क्षण के बीच के अंतर का ट्रैक रखते हैं जब हम उम्मीद कर रहे हैं कि हमारा कनेक्शन sshd द्वारा बंद किया जाएगा (मूल रूप से वह क्षण जब हमें sshd के बैनर का पहला बाइट प्राप्त होता है, प्लस LoginGraceTime) और वह क्षण जब हमारा कनेक्शन वास्तव में sshd द्वारा बंद किया जाता है, और तदनुसार अपनी टाइमिंग समायोजित करते हैं (अर्थात, वह क्षण जब हम अपने DSA पैकेट का अंतिम बाइट भेजते हैं)।
ये समय अंतर हमें घड़ी तिरछापन (clock skews) और नेटवर्क विलंबों को ट्रैक करने की अनुमति देते हैं, जो समय के साथ पूर्वानुमानित पैटर्न दिखाते हैं: हमने रैखिक और स्पलाइन प्रतिगमन के साथ प्रयोग किया, लेकिन अंत में, हाल के माप का सरल रूप से पुन: उपयोग करने से बेहतर कुछ भी काम नहीं किया। संभवतः, डीप लर्निंग और भी बेहतर परिणाम दे सकती है; इसे इच्छुक पाठक के लिए एक अभ्यास के रूप में छोड़ दिया गया है।
अधिक महत्वपूर्ण बात, हम sshd से अनैच्छिक फीडबैक के माध्यम से अपनी टाइमिंग को धीरे-धीरे समायोजित करके इस रेस कंडीशन को जीतने की अपनी संभावनाओं को और बढ़ाते हैं:
यदि हमें अपने DSA सार्वजनिक-कुंजी पैकेट के जवाब में एक प्रतिक्रिया (SSH2_MSG_USERAUTH_FAILURE) प्राप्त होती है, तो हमने इसे बहुत जल्दी भेजा था (sshd के पास हमारे पैकेट को अनविशेषाधिकृत चाइल्ड में प्राप्त करने, उसे पार्स करने, विशेषाधिकृत चाइल्ड को भेजने, वहाँ पार्स करने और हमें वापस प्रतिक्रिया भेजने का समय था);
यदि हम अपने DSA पैकेट का अंतिम बाइट भी नहीं भेज सकते हैं, तो हमने बहुत लंबा इंतज़ार किया (sshd पहले ही SIGALRM प्राप्त कर चुका है और हमारा कनेक्शन बंद कर चुका है);
यदि हम अपने DSA पैकेट का अंतिम बाइट भेज सकते हैं, और sshd के हमारा कनेक्शन बंद करने से पहले कोई प्रतिक्रिया प्राप्त नहीं होती है, तो हमारी टाइमिंग उचित रूप से सटीक थी।
यह फीडबैक हमें उस 'बड़ी' रेस विंडो को लक्षित करने की अनुमति देता है जिसे हम कहते हैं: इसे हिट करना इस बात की गारंटी नहीं देता कि हम रेस कंडीशन जीत जाएंगे, लेकिन इस बड़ी विंडो के अंदर 24 'छोटी' रेस विंडो (24 free() कॉलों के अंदर) हैं, जो हिट होने पर गारंटी देती हैं कि हम रेस कंडीशन जीतते हैं।
इन सुधारों के साथ, इस रेस कंडीशन को जीतने के लिए औसतन ~10,000 प्रयास लगते हैं; अर्थात, प्रति 600 सेकंड (LoginGraceTime) में 10 कनेक्शन (MaxStartups) स्वीकार किए जाने पर, एक दूरस्थ रूट शेल प्राप्त करने में औसतन ~1 सप्ताह लगता है।
जब सूरज उगने लगता है तो मैं सोता हूँ
-- The Interrupters, "Alien"
इस OpenSSH संस्करण का SIGALRM हैंडलर अब packet_close() कॉल नहीं करता है; इसके अलावा, इस Ubuntu के glibc (2.3.6) में malloc परिवार के फ़ंक्शनों में प्रवेश करते समय हमेशा एक अनिवार्य लॉक लेता है (चाहे वह sshd की तरह सिंगल-थ्रेडेड ही क्यों न हो), जो हमें malloc फ़ंक्शनों में से एक की कॉल को बाधित करने और बाद में इन फ़ंक्शनों की एक और कॉल के दौरान इसका शोषण करने से रोकता है (वे हमेशा डेडलॉक करेंगे)। हमें एक और समाधान खोजना होगा।
CVE-2006-5051 में GSSAPI में डबल-फ्री का उल्लेख है, लेकिन GSSAPI (या Kerberos) डिफ़ॉल्ट रूप से सक्षम नहीं है, इसलिए यह बहुत आकर्षक नहीं लगता। दूसरी ओर, PAM डिफ़ॉल्ट रूप से सक्षम है, और pam_end() sshd के SIGALRM हैंडलर द्वारा कॉल किया जाता है (और, निश्चित रूप से, async-signal-safe नहीं है)। इसलिए हमने एक ऐसा PAM फ़ंक्शन खोजा, जो यदि SIGALRM द्वारा सही समय पर बाधित होता है, तो PAM की आंतरिक संरचनाओं को एक असंगत स्थिति में छोड़ देगा, जो SIGALRM हैंडलर में pam_end() के दौरान शोषणीय हो। हमें pam_set_data() मिला:
33 int pam_set_data( 34 pam_handle_t *pamh, .. 37 void (*cleanup)(pam_handle_t *pamh, void *data, int error_status)) 38 { 39 struct pam_data *data_entry; .. 57 } else if ((data_entry = malloc(sizeof(*data_entry)))) { .. 65 data_entry->next = pamh->data; 66 pamh->data = data_entry; .. 74 data_entry->cleanup = cleanup; ------------------------------------------------------------------------यदि यह फ़ंक्शन SIGALRM द्वारा पंक्ति 66 के बाद लेकिन पंक्ति 74 के पहले बाधित होता है, तो data_entry पहले से ही PAM की संरचनाओं (pamh) में जुड़ा होता है, लेकिन इसका cleanup फ़ील्ड (एक फ़ंक्शन पॉइंटर) अभी तक आरंभीकृत नहीं होता है (क्योंकि पंक्ति 57 पर malloc() इसकी मेमोरी को आरंभीकृत नहीं करता)। यदि हम cleanup को नियंत्रित करने में सक्षम हैं (पिछले हीप आवंटनों से बचे हुए अवशेषों के माध्यम से), तो हम pam_end() (SIGALRM हैंडलर के अंदर) द्वारा _pam_free_data() को कॉल करने पर (पंक्ति 118 पर) मनमाना कोड निष्पादित कर सकते हैं:
यह एक अत्यंत सरल शोषण होता; दुर्भाग्य से, हम पूरी तरह से अनदेखा कर गए कि pam_set_data() को केवल PAM मॉड्यूल से ही कॉल किया जा सकता है: यदि हम इसे SIGALRM से बाधित करते हैं, तो pamh->caller_is अभी भी _PAM_CALLED_FROM_MODULE होता है, जिस स्थिति में pam_end() बिना कभी _pam_free_data() को कॉल किए तुरंत लौट आता है। वापस ड्रॉइंग बोर्ड पर।
हार नहीं मानना, यह हमारा तरीका नहीं है
-- The Interrupters, "Title Holder"
हमने देखा कि, नीचे पंक्ति 601 पर, sshd अपने वैश्विक sshpam_handle पॉइंटर का एक पॉइंटर सीधे pam_start() को पास करता है (जिसे प्रत्येक कनेक्शन पर एक बार कॉल किया जाता है):
इसलिए हमने स्वयं pam_start() की जाँच करने का निर्णय लिया: यदि SIGALRM से बाधित होता है, तो यह sshpam_handle द्वारा इंगित संरचना को एक असंगत स्थिति में छोड़ सकता है, जिसका फिर SIGALRM हैंडलर के अंदर, जब "pam_end(sshpam_handle, sshpam_err)" कॉल किया जाता है, शोषण किया जा सकता है।
पंक्ति 32 पर, pam_start() तुरंत sshd के sshpam_handle को calloc() द्वारा आवंटित मेमोरी के एक हिस्से पर सेट करता है; यह सुरक्षित है, क्योंकि calloc() इस मेमोरी को शून्य से आरंभीकृत करता है। दूसरी ओर, यदि _pam_add_handler() (जिसे pam_start() द्वारा कई बार कॉल किया जाता है) SIGALRM द्वारा पंक्ति 874 के बाद लेकिन पंक्ति 886 के पहले बाधित होता है, तो एक malloc() द्वारा आवंटित संरचना pamh में जुड़ जाती है, लेकिन इसका next फ़ील्ड अभी तक आरंभीकृत नहीं होता है। यदि हम next को नियंत्रित करने में सक्षम हैं (पिछले हीप आवंटनों से बचे हुए अवशेषों के माध्यम से), तो हम pam_end() (SIGALRM हैंडलर के अंदर) को कॉल के दौरान, नीचे पंक्ति 1020 (और पंक्ति 1017) पर, free() के लिए एक मनमाना पॉइंटर पास कर सकते हैं:
चूँकि इस Ubuntu के glibc की malloc पुरानी unlink() तकनीक के विरुद्ध पहले से ही सुरक्षित है, हमने अपने मनमाने free() को Malloc Maleficarum के House of Mind (fastbin संस्करण) में बदलने का निर्णय लिया: हम अपने स्वयं के NON_MAIN_ARENA चंक को free() करते हैं, अपने नकली arena को sshd के .got.plt पर इंगित करते हैं (इस Ubuntu के sshd में ASLR है लेकिन PIE नहीं), और हीप में अपने शेलकोड के पते के साथ _exit() की प्रविष्टि को अधिलेखित करते हैं (इस Ubuntu का हीप डिफ़ॉल्ट रूप से अभी भी निष्पादन योग्य है)। Malloc Maleficarum पर अधिक जानकारी के लिए:
मैंने सब कुछ कठिन तरीके से सीखा
-- The Interrupters, "The Hard Way"
sshd के विरुद्ध यह हमला करने के लिए, हमें शुरू में तीन समस्याओं का सामना करना पड़ा:
House of Mind के लिए हमें अपने नकली arena के पॉइंटर को हीप में पते 0x08100000 पर संग्रहीत करना आवश्यक है; लेकिन क्या हम इतने ऊँचे पते पर हमलावर-नियंत्रित डेटा संग्रहीत कर सकते हैं? चूँकि sshd उपयोगकर्ता प्रमाणीकरण की शुरुआत में ही pam_start() को कॉल करता है, हम उपयोगकर्ता नाम के अलावा किसी भी चीज़ को नियंत्रित नहीं करते हैं; सौभाग्य से, ~128KB लंबाई का एक उपयोगकर्ता नाम (DEFAULT_MMAP_THRESHOLD से छोटा) हमें पते 0x08100000 पर अपना स्वयं का डेटा संग्रहीत करने की अनुमति देता है।
हमारे नकली NON_MAIN_ARENA चंक का size फ़ील्ड बहुत बड़ा नहीं होना चाहिए (free() की सुरक्षा जाँचों को पार करने के लिए); अर्थात्, इसमें null बाइट्स होने चाहिए। लेकिन हमारा लंबा उपयोगकर्ता नाम एक null-समाप्त स्ट्रिंग है जिसमें null बाइट्स नहीं हो सकते; सौभाग्य से हमें याद आया कि _pam_free_handlers_aux() उन संरचनाओं को शून्य कर देता है जिन्हें यह free() करता है (ऊपर पंक्ति 1019): इसलिए हम अपने नकली चंक के size फ़ील्ड को इस तरह के memset(0) से "पैच" करते हैं, और उसके बाद ही इसे free() करते हैं।
हमें अपने नकली NON_MAIN_ARENA चंक के free() से पहले free() के कई कॉलों (ऊपर पंक्तियाँ 1017 और 1020) से बचना होगा। हम इन free() को नकली IS_MMAPPED चंकों की ओर इंगित करके no-op में बदल देते हैं: free() munmap_chunk() को कॉल करता है, जो munmap() को कॉल करता है, जो विफल हो जाता है क्योंकि ये नकली IS_MMAPPED चंक गलत संरेखित हैं; प्रभावी रूप से यह एक no-op है, क्योंकि इस Ubuntu के glibc में assert() विफलताएँ लागू नहीं की जाती हैं।
अंत में, हमारा लंबा उपयोगकर्ता नाम हमें 20 विभिन्न संरचनाओं के संभावित रूप से अप्रारंभीकृत next फ़ील्ड को भी नियंत्रित करने की अनुमति देता है (हमारे लंबे उपयोगकर्ता नाम की अस्थायी प्रतियों से बचे हुए अवशेषों के माध्यम से), क्योंकि pam_start() _pam_add_handler() को कई बार कॉल करता है; अर्थात्, हमारी बड़ी दौड़ विंडो में 20 छोटी दौड़ विंडो होती हैं।
वही तरकीबें जो उन्होंने पहले इस्तेमाल की थीं
-- The Interrupters, "Divide Us"
Ubuntu 6.06.1 के विरुद्ध इस हमले के लिए, हमने केवल उस समय-निर्धारण रणनीति का पुन: उपयोग किया जिसे हमने Debian 3.0r6 के विरुद्ध उपयोग किया था: दौड़ की स्थिति जीतने में औसतन ~10,000 प्रयास लगते हैं, और प्रति 120 सेकंड (LoginGraceTime) में 10 कनेक्शन (MaxStartups) स्वीकार किए जाने पर, रिमोट रूट शेल प्राप्त करने में औसतन ~1-2 दिन लगते हैं।
नोट: चूँकि इस Ubuntu के glibc में malloc परिवार के फ़ंक्शनों में प्रवेश करते समय हमेशा एक अनिवार्य लॉक लिया जाता है, एक अभागा हमलावर रूट शेल प्राप्त करने से पहले सभी 10 MaxStartups कनेक्शनों को डेडलॉक कर सकता है; हमने इस समस्या के आसपास काम करने का प्रयास नहीं किया क्योंकि हमारा अंतिम लक्ष्य वैसे भी एक आधुनिक OpenSSH संस्करण का शोषण करना था।
अब तुम तैयार हो, राक्षसों का सामना करो
-- The Interrupters, "Be Gone"
इस OpenSSH संस्करण का SIGALRM हैंडलर न तो packet_close() कॉल करता है और न ही pam_end(); वास्तव में यह केवल एक दिलचस्प फ़ंक्शन कॉल करता है, syslog():
हमारे दो प्रमुख प्रश्न हैं: क्या इस Debian के glibc (2.36) का syslog() async-सिग्नल-असुरक्षित फ़ंक्शन जैसे malloc() और free() को कॉल करता है? और यदि हाँ, तो क्या यह glibc अभी भी malloc परिवार के फ़ंक्शनों में प्रवेश करते समय एक अनिवार्य लॉक लेता है?
नोट: चूँकि हम इन malloc() आवंटनों के बारे में किसी भी चीज़ को नियंत्रित नहीं करते हैं (न उनका क्रम, न उनके आकार, न उनकी सामग्री), हमने पंक्ति 166 पर "rce" को एक अत्यंत आवश्यक शुभ संकेत के रूप में लिया।
और सौभाग्य से हमारे लिए, हमारे दूसरे प्रश्न का उत्तर नहीं है; अक्टूबर 2017 के बाद से, glibc के malloc फ़ंक्शन सिंगल-थ्रेडेड (जैसे sshd) होने पर कोई भी लॉक नहीं लेते हैं:
https://sourceware.org/git?p=glibc.git;a=commit;h=a15d53e2de4c7d83bda251469d92a3c7b49a90db https://sourceware.org/git?p=glibc.git;a=commit;h=3f6bb8a32e5f5efd78ac08c41e623651cc242a89 https://sourceware.org/git?p=glibc.git;a=commit;h=905a7725e9157ea522d8ab97b4c8b96aeb23df54
इसके अलावा, इस Debian संस्करण में निम्नलिखित बेहतरीन ब्लॉग पोस्टों में वर्णित ASLR कमजोरी है (क्रमशः Justin Miller और Mathias Krause द्वारा):
https://zolutal.github.io/aslrnt/ https://grsecurity.net/toolchain_necromancy_past_mistakes_haunting_aslr
ठोस रूप से, i386 पर sshd के मामले में, प्रत्येक मेमोरी मैपिंग सामान्य रूप से यादृच्छिक होती है (sshd का PIE, हीप, अधिकांश लाइब्रेरी, स्टैक), लेकिन glibc स्वयं हमेशा या तो पते 0xb7200000 पर या पते 0xb7400000 पर मैप किया जाता है; दूसरे शब्दों में, हम आधे समय glibc के पते का सही अनुमान लगा सकते हैं (ASLR को पराजित करने के लिए एक छोटी सी कीमत)। अपने शोषण में हम मानते हैं कि glibc पते 0xb7400000 पर मैप किया गया है, क्योंकि यह 0xb7200000 की तुलना में थोड़ा अधिक सामान्य है।
हमारा अगला प्रश्न है: glibc के malloc फ़ंक्शनों के अंदर कौन से कोड पथ, यदि सही समय पर SIGALRM द्वारा बाधित होते हैं, तो हीप को एक असंगत स्थिति में छोड़ देते हैं, जो SIGALRM हैंडलर के अंदर malloc() कॉलों में से एक के दौरान शोषण योग्य होती है?
हमें कई दिलचस्प (और आश्चर्यजनक!) कोड पथ मिले, लेकिन जिसे हमने चुना है उसमें केवल सापेक्ष आकार शामिल हैं, निरपेक्ष पते नहीं (उदाहरण के लिए, unlink_chunk() के अंदर विभिन्न कोड पथों के विपरीत); यह अंतर भविष्य के amd64 शोषण के लिए महत्वपूर्ण साबित हो सकता है। यह कोड पथ, malloc() के अंदर, एक बड़े मुक्त चंक (victim) को दो छोटे चंकों में विभाजित करता है; पहला चंक malloc() के कॉलर को लौटाया जाता है (पंक्ति 4345 पर) और दूसरा चंक (remainder) मुक्त चंकों की एक अनसॉर्टेड सूची में जोड़ दिया जाता है (पंक्तियाँ 4324-4327):
यदि यह कोड पथ SIGALRM द्वारा पंक्ति 4327 के बाद लेकिन पंक्ति 4339 के पहले बाधित होता है, तो इस विभाजन का remainder चंक पहले से ही मुक्त चंकों की अनसॉर्टेड सूची में जुड़ा होता है (पंक्तियाँ 4324-4327), लेकिन इसका size फ़ील्ड (mchunk_size) अभी तक आरंभीकृत नहीं होता है (पंक्ति 4339)।
यदि हम इसके size फ़ील्ड को नियंत्रित करने में सक्षम हैं (पिछले हीप आवंटनों से बचे हुए अवशेषों के माध्यम से), तो हम इस remainder चंक को बड़ा कर सकते हैं और इसे अन्य हीप चंकों के साथ अतिव्यापी बना सकते हैं, और इस प्रकार जब यह बढ़ा हुआ, अतिव्यापी remainder चंक अंततः malloc() द्वारा आवंटित और लिखा जाता है (SIGALRM हैंडलर के अंदर), तो हीप मेमोरी को दूषित कर सकते हैं।
हमारा अंतिम प्रश्न है: यह देखते हुए कि हम SIGALRM हैंडलर के अंदर malloc() कॉलों के बारे में किसी भी चीज़ को नियंत्रित नहीं करते हैं, sshd द्वारा _exit() (sshsigdie() में) कॉल करने से पहले मनमाना कोड निष्पादन प्राप्त करने के लिए हम हीप में क्या अधिलेखित कर सकते हैं?
चूँकि __tzfile_read() (SIGALRM हैंडलर के अंदर) हीप में एक FILE संरचना malloc() द्वारा आवंटित करता है (ऊपर पंक्ति 166 पर), और चूँकि FILE संरचनाओं के दुरुपयोग का मनमाना कोड निष्पादन के लिए एक लंबा इतिहास है, हमने अपने हीप भ्रष्टाचार को इसी FILE संरचना पर लक्षित करने का निर्णय लिया। हालाँकि, यह कहना जितना आसान है, करना उतना ही कठिन है: हमारा हीप भ्रष्टाचार बहुत सीमित है, और FILE संरचनाओं को वर्षों में काफी मजबूत किया गया है (उदाहरण के लिए, IO_validate_vtable() और PTR_DEMANGLE() द्वारा)।
आखिरकार, हमने निम्नलिखित तकनीक विकसित की (जो i386 glibc के लिए विशिष्ट प्रतीत होती है -- amd64 glibc _vtable_offset का उपयोग बिल्कुल नहीं करता प्रतीत होता है):
अपने सीमित हीप भ्रष्टाचार के साथ, हम __tzfile_read() की FILE संरचना के _vtable_offset फ़ील्ड (एक एकल हस्ताक्षरित char) को अधिलेखित करते हैं;
glibc के libio फ़ंक्शन इसलिए इस FILE संरचना के vtable पॉइंटर (फ़ंक्शन पॉइंटरों की एक सरणी का पॉइंटर) को डिफ़ॉल्ट शून्य ऑफसेट के बजाय एक गैर-शून्य ऑफसेट (हमारे अधिलेखित _vtable_offset) पर खोजेंगे;
हम (हमलावर) इस नकली vtable पॉइंटर को आसानी से नियंत्रित कर सकते हैं (पिछले हीप आवंटनों से बचे हुए अवशेषों के माध्यम से), क्योंकि इस ऑफसेट के आसपास की FILE संरचना fopen() द्वारा स्पष्ट रूप से आरंभीकृत नहीं होती है;
glibc की सुरक्षा जाँचों को पार करने के लिए, हमारे नकली vtable पॉइंटर को __libc_IO_vtables अनुभाग में कहीं इंगित करना चाहिए: हमने इसे वाइड-कैरेक्टर स्ट्रीम के लिए vtable, _IO_wfile_jumps पर इंगित करने का निर्णय लिया (अर्थात्, 0xb761b740 पर, क्योंकि हम मानते हैं कि glibc पते 0xb7400000 पर मैप किया गया है);
परिणामस्वरूप, __fread_unlocked() (ऊपर पंक्ति 186 पर) _IO_file_underflow() के बजाय _IO_wfile_underflow() को कॉल करता है, जो एक फ़ंक्शन पॉइंटर (__fct) को कॉल करता है जो मूल रूप से एक संरचना से आता है जिसका पॉइंटर (_codecvt) FILE संरचना का एक और फ़ील्ड है;
हम (हमलावर) इस _codecvt पॉइंटर को आसानी से नियंत्रित कर सकते हैं (पिछले हीप आवंटनों से बचे हुए अवशेषों के माध्यम से, क्योंकि FILE संरचना का यह फ़ील्ड fopen() द्वारा स्पष्ट रूप से आरंभीकृत नहीं होता है), जो हमें __fct फ़ंक्शन पॉइंटर को नियंत्रित करने की भी अनुमति देता है।
संक्षेप में, fopen() द्वारा malloc() आवंटित FILE संरचना के एक एकल बाइट (_vtable_offset) को अधिलेखित करके, हम अपने स्वयं के __fct फ़ंक्शन पॉइंटर को कॉल कर सकते हैं और __fread_unlocked() के दौरान मनमाना कोड निष्पादित कर सकते हैं।
मैं इसे उत्तम चाहता था, इसमें कोई शिकन नहीं
-- The Interrupters, "In the Mirror"
sshd के विशेषाधिकार प्राप्त चाइल्ड के विरुद्ध यह हमला करने के लिए, आइए पहले निम्नलिखित हीप लेआउट की कल्पना करें ("XXX" "बैरियर" चंक हैं जो हमें हीप में छेद बनाने की अनुमति देते हैं; उदाहरण के लिए, छोटे मेमोरी-लीक चंक):
---|----------------------------------------------|---|------------|---
| XXX | large hole | XXX | small hole | XXX |
|---|---|---|---|---|
| ~8KB | 320B |
---|-----------------------|----------------------|---|------------|---
| XXX | large allocated chunk | free remainder chunk | XXX | small hole | XXX |
|---|---|---|---|---|---|
| ~4KB | ~4KB | 320B |
लेकिन यदि यह malloc() SIGALRM द्वारा पंक्ति 4327 के बाद लेकिन पंक्ति 4339 के पहले बाधित होता है, तो इस विभाजन का remainder चंक पहले से ही मुक्त चंकों की अनसॉर्टेड सूची में जुड़ा होता है, लेकिन इसका size फ़ील्ड हमारे नियंत्रण में होता है (पिछले हीप आवंटनों से बचे हुए अवशेषों के माध्यम से), और यह कृत्रिम रूप से बढ़ाया गया remainder चंक निम्नलिखित छोटे छेद के साथ अतिव्यापी हो जाता है:---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | real remainder chunk |XXX| small hole |XXX ---|-----------------------|----------------------|---|------------|--- | ~4KB |<------------------------------------->| artificially enlarged remainder chunk
जब SIGALRM हैंडलर syslog() और इसलिए __tzfile_read() को कॉल करता है, fopen() अपनी FILE संरचना के लिए छोटे छेद को malloc() करता है, और __fread_unlocked() एक 4KB रीड बफर malloc() करता है, जिससे बड़ा किया गया remainder chunk दो भागों में विभाजित हो जाता है (4KB रीड बफर और एक छोटा remainder chunk):
---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | |XXX| FILE |XXX ---|-----------------------|----------------------|---|--|---------|--- | ~4KB |<--------------------------->|<------->| 4KB read buffer remainder
इसलिए हम FILE संरचना के कुछ हिस्सों को इस छोटे remainder chunk के आंतरिक हेडर से अधिलेखित करते हैं: अधिक सटीक रूप से, हम FILE के _vtable_offset को इस हेडर के bk फ़ील्ड के तीसरे बाइट से अधिलेखित करते हैं, जो मुक्त चंक्स की अवर्गीकृत सूची के लिए एक पॉइंटर है, 0xb761d7f8 (अर्थात, हम _vtable_offset को 0x61 से अधिलेखित करते हैं);
फिर, जैसा कि "Theory" उपधारा में समझाया गया है, __fread_unlocked() _IO_wfile_underflow() को कॉल करता है (_IO_file_underflow() के बजाय), जो हमारे स्वयं के __fct फ़ंक्शन पॉइंटर को कॉल करता है (हमारे स्वयं के _codecvt पॉइंटर के माध्यम से) और हमारा मनमाना कोड निष्पादित करता है।
नोट: हमने अभी तक यह नहीं समझाया है कि नियंत्रित _codecvt पॉइंटर से नियंत्रित __fct फ़ंक्शन पॉइंटर तक विश्वसनीय रूप से कैसे पहुँचा जाए; हम ऐसा करेंगे, लेकिन पहले हमें एक अधिक गंभीर समस्या हल करनी होगी।
वास्तव में, हमने पुराने OpenSSH संस्करणों पर अपने काम से सीखा कि यदि हमारी बड़ी रेस विंडो में केवल एक छोटी रेस विंडो हो तो हम इस सिग्नल हैंडलर रेस कंडीशन को कभी नहीं जीत पाएँगे। परिणामस्वरूप, हमने निम्नलिखित हीप लेआउट पर आधारित निम्नलिखित रणनीति लागू की:
---|------------|---|------------|---|------------|---|------------|---
| XXX | large hole 1 | XXX | small hole 1 | XXX | large hole 2 | XXX | small hole 2 | ... |
|---|---|---|---|---|---|---|---|---|
| ~8KB | 320B | ~8KB | 320B |
हम sshd को भेजने वाला अंतिम पैकेट (SIGALRM की डिलीवरी से कुछ समय पहले) sshd को malloc() कॉल्स का निम्नलिखित क्रम करने के लिए बाध्य करता है: malloc(~4KB), malloc(304), malloc(~4KB), malloc(304), आदि।
1/ हमारा पहला malloc(~4KB) बड़े छेद 1 को दो भागों में विभाजित करता है:
यदि यह पहला विभाजन सही समय पर SIGALRM द्वारा बाधित होता है, तो SIGALRM हैंडलर के अंदर fopen() छोटे छेद 1 को अपनी FILE संरचना के लिए malloc() करता है, और हम ऊपर बताए अनुसार मनमाना कोड निष्पादन प्राप्त करते हैं;
यदि नहीं, तो हम छोटे छेद 1 को स्वयं अपने पहले malloc(304) से malloc() करते हैं, और:
2/ हमारा दूसरा malloc(~4KB) बड़े छेद 2 को दो भागों में विभाजित करता है:
यदि यह दूसरा विभाजन सही समय पर SIGALRM द्वारा बाधित होता है, तो SIGALRM हैंडलर के अंदर fopen() छोटे छेद 2 को अपनी FILE संरचना के लिए malloc() करता है, और हम ऊपर बताए अनुसार मनमाना कोड निष्पादन प्राप्त करते हैं;
यदि नहीं, तो हम छोटे छेद 2 को स्वयं अपने दूसरे malloc(304) से malloc() करते हैं, आदि।
हम sshd के हीप में ऐसे बड़े और छोटे छेदों के 27 जोड़े बनाने में सक्षम थे (28, PACKET_MAX_SIZE, 256KB से अधिक हो जाता): हमारी बड़ी रेस विंडो में अब 27 छोटी रेस विंडो शामिल हैं! इस जटिल हीप लेआउट को प्राप्त करना अत्यंत कष्टदायक और समय लेने वाला था, लेकिन दो मुख्य आकर्षण हैं:
इस हीप लेआउट को विश्वसनीय रूप से प्राप्त करने के लिए, हम sshd को पाँच अलग-अलग पब्लिक-की पैकेट भेजते हैं (पैकेट a/ से d/ SIGALRM से बहुत पहले भेजे जा सकते हैं; पैकेट e/ का अधिकांश भाग भी SIGALRM से बहुत पहले भेजा जा सकता है, लेकिन इसका अंतिम बाइट अंतिम क्षण में ही भेजा जाना चाहिए):
a/ हम विभिन्न प्रकार के tcache चंक्स को malloc() और free() करते हैं, ताकि यह सुनिश्चित हो सके कि हमारे नियंत्रण में नहीं होने वाले हीप आवंटन इन tcache चंक्स में समाप्त हों और हमारे सावधानीपूर्वक बनाए गए हीप लेआउट में हस्तक्षेप न करें।
b/ हम विभिन्न आकारों के चंक्स को malloc() और free() करते हैं, ताकि हमारे 27 जोड़े बड़े और छोटे छेद (और संबंधित "barrier" चंक्स) बन सकें।
c/ हम ~4KB चंक्स और 320B चंक्स को malloc() और free() करते हैं, ताकि:
हमारे संभावित रूप से बड़े किए गए remainder chunk का नकली हेडर (बड़े आकार का फ़ील्ड), हमारे बड़े छेदों के बीच में लिखा जा सके;
हमारे संभावित रूप से बड़े किए गए remainder chunk का नकली फुटर, हमारे छोटे छेदों के अंत में लिखा जा सके (glibc की सुरक्षा जाँचों को पास करने के लिए);
हमारे नकली vtable और _codecvt पॉइंटर, हमारे छोटे छेदों (जो संभावित FILE संरचनाएँ हैं) में लिखे जा सकें।
d/ हम एक बहुत बड़ा स्ट्रिंग (लगभग 256KB) malloc() और free() करते हैं, ताकि यह सुनिश्चित हो सके कि हमारे बड़े और छोटे छेद मुक्त चंक्स की अवर्गीकृत सूची से हटा दिए जाएँ और उनके संबंधित malloc बिन्स में रख दिए जाएँ।
e/ हम sshd को हमारे अंतिम malloc() कॉल्स का क्रम (malloc(~4KB), malloc(304), malloc(~4KB), malloc(304), आदि) करने के लिए बाध्य करते हैं, ताकि हमारी 27 छोटी रेस विंडो खुल जाएँ।
चौकस पाठकों ने ध्यान दिया होगा कि हमने अभी भी _codecvt की समस्या का समाधान नहीं किया है (शाब्दिक और आलंकारिक रूप से)। वास्तव में, _codecvt एक संरचना (_IO_codecvt) के लिए एक पॉइंटर है जिसमें एक संरचना (__gconv_step) के लिए एक पॉइंटर होता है जिसमें __fct फ़ंक्शन पॉइंटर होता है जो हमें मनमाना कोड निष्पादित करने की अनुमति देता है। _codecvt के माध्यम से __fct को विश्वसनीय रूप से नियंत्रित करने के लिए, हम बस _codecvt को glibc के malloc बिन्स में से एक की ओर इंगित करते हैं, जिसमें सुविधाजनक रूप से हीप में हमारे एक मुक्त चंक का पॉइंटर होता है, जिसमें मनमाने glibc कोड के लिए हमारा अपना __fct फ़ंक्शन पॉइंटर होता है (ये सभी glibc पते हमें ज्ञात हैं, क्योंकि हम मानते हैं कि glibc 0xb7400000 पते पर मैप किया गया है)।
हमारा समय समाप्त हो रहा है
-- द इंटरप्टर्स, "As We Live"
जैसे ही हमने इस तीसरे एक्सप्लॉइट को लागू किया, यह स्पष्ट हो गया कि हम उस टाइमिंग रणनीति को दोबारा इस्तेमाल नहीं कर सकते जो हमने दो पुराने OpenSSH संस्करणों के खिलाफ इस्तेमाल की थी: हम इस नई रेस कंडीशन को कभी नहीं जीत रहे थे। आखिरकार, हम समझ गए कि क्यों:
sshd को हमारे पाँचवें और अंतिम पब्लिक की (ऊपर पैकेट e/) को पार्स करने में लंबा समय लगता है (~10ms); दूसरे शब्दों में, हमारी बड़ी रेस विंडो बहुत बड़ी है (हमारी 27 छोटी रेस विंडो भूसे के ढेर में सुइयों की तरह हैं)।
user_specific_delay() जो हाल ही में पेश की गई थी (OpenSSH 7.8p1) sshd के हमारे अंतिम पब्लिक-की पैकेट के प्रति प्रतिक्रिया को ~9ms तक विलंबित करती है और इसलिए हमारी फीडबैक-आधारित टाइमिंग रणनीति को नष्ट कर देती है।
परिणामस्वरूप, हमने एक पूरी तरह से अलग टाइमिंग रणनीति विकसित की:
समय-समय पर, हम अपना अंतिम पब्लिक-की पैकेट एक छोटी सी गलती के साथ भेजते हैं जो त्रुटि प्रतिक्रिया उत्पन्न करती है (नीचे पंक्तियाँ 138-142), ठीक उस sshkey_from_blob() कॉल से पहले जो हमारी पब्लिक की को पार्स करती है;
समय-समय पर, हम अपना अंतिम पब्लिक-की पैकेट एक और छोटी सी गलती के साथ भेजते हैं जो त्रुटि प्रतिक्रिया उत्पन्न करती है (नीचे पंक्तियाँ 151-155), ठीक उस sshkey_from_blob() कॉल के बाद जो हमारी पब्लिक की को पार्स करती है;
इन दो प्रतिक्रिया समयों के बीच का अंतर वह समय है जो sshd को हमारी अंतिम पब्लिक की को पार्स करने में लगता है, और यह हमें अपने अंतिम पैकेटों के प्रसारण को सटीक रूप से समयबद्ध करने की अनुमति देता है (यह सुनिश्चित करने के लिए कि sshd के पास SIGALRM की डिलीवरी से पहले हमारी पब्लिक की को अनविशेषाधिकार प्राप्त चाइल्ड में पार्स करने, इसे विशेषाधिकार प्राप्त चाइल्ड को भेजने, और वहाँ इसे पार्स करना शुरू करने का समय हो)।
इस रणनीति में बदलाव के साथ, रेस कंडीशन जीतने में औसतन ~10,000 प्रयास लगते हैं; अर्थात, प्रति 120 सेकंड (LoginGraceTime) में 100 कनेक्शन (MaxStartups) स्वीकार होने पर, रेस कंडीशन जीतने में औसतन ~3-4 घंटे लगते हैं, और (ASLR के कारण) दूरस्थ रूट शेल प्राप्त करने में ~6-8 घंटे लगते हैं।
कल के लिए तुम्हारी योजना क्या है?
-- द इंटरप्टर्स, "Take Back the Power"
हमने दो कारणों से "Rocky-9.4-x86_64-minimal.iso" से Rocky Linux 9 (एक Red Hat Enterprise Linux 9 व्युत्पन्न) को लक्षित करने का निर्णय लिया:
इसका OpenSSH संस्करण (8.7p1) इस सिग्नल हैंडलर रेस कंडीशन के प्रति संवेदनशील है और इसका glibc हमेशा 2MB के गुणक पर मैप किया जाता है (पिछले "Theory" उपधारा में चर्चा की गई ASLR कमजोरी के कारण), जो आंशिक पॉइंटर अधिलेखन को अधिक शक्तिशाली बनाता है;
इस glibc संस्करण (2.34) का syslog() फ़ंक्शन (जो async-signal-unsafe है लेकिन sshd के SIGALRM हैंडलर द्वारा कॉल किया जाता है) आंतरिक रूप से __open_memstream() को कॉल करता है, जो हीप में एक FILE संरचना malloc() करता है, और calloc(), realloc(), और free() को भी कॉल करता है (जो हमें कुछ अत्यंत आवश्यक स्वतंत्रता देता है)।
हीप भ्रष्टाचार को एक प्रिमिटिव के रूप में, हीप में malloc() की गई दो FILE संरचनाओं, और glibc के पतों में 21 निश्चित बिट्स के साथ, हम मानते हैं कि यह सिग्नल हैंडलर रेस कंडीशन amd64 पर शोषण योग्य है (संभवतः ~6-8 घंटों में नहीं, लेकिन उम्मीद है कि एक सप्ताह से कम समय में)। केवल समय बताएगा।
पार्श्व टिप्पणी: हमने पता लगाया कि Ubuntu 24.04 अपने sshd चाइल्ड्स के ASLR को पुनः रैंडमाइज़ नहीं करता (यह केवल एक बार, बूट समय पर रैंडमाइज़ किया जाता है); हमने इसे नीचे दिए गए पैच तक ट्रैक किया, जो sshd के rexec_flag को बंद कर देता है। यह सामान्य रूप से एक बुरा विचार है, लेकिन इस सिग्नल हैंडलर रेस कंडीशन के विशेष मामले में, यह sshd को शोषण योग्य होने से रोकता है: SIGALRM हैंडलर के अंदर syslog() किसी भी malloc फ़ंक्शन को कॉल नहीं करता, क्योंकि यह कभी भी syslog() की पहली कॉल नहीं होती।
https://git.launchpad.net/ubuntu/+source/openssh/tree/debian/patches/systemd-socket-activation.patch
तूफान आया और चला गया
-- द इंटरप्टर्स, "Good Things"
6 जून, 2024 को, इस सिग्नल हैंडलर रेस कंडीशन को कमिट 81c1099 ("Add a facility to sshd(8) to penalise particular problematic client behaviours") द्वारा ठीक किया गया, जिसने async-signal-unsafe कोड को sshd के SIGALRM हैंडलर से sshd की लिसनर प्रक्रिया में स्थानांतरित कर दिया, जहाँ इसे समकालिक रूप से संभाला जा सकता है:
क्योंकि यह फिक्स एक बड़े कमिट (81c1099) का हिस्सा है, जो एक और भी बड़े defense-in-depth कमिट (03e3de4, "Start the process of splitting sshd into separate binaries") के ऊपर है, इसे बैकपोर्ट करना कठिन साबित हो सकता है। उस स्थिति में, सिग्नल हैंडलर रेस कंडीशन को स्वयं sshsigdie() फ़ंक्शन से async-signal-unsafe कोड को हटाकर या कमेंट करके ठीक किया जा सकता है; उदाहरण के लिए:
sshsigdie(const char *file, const char *func, int line, int showfunc, LogLevel level, const char *suffix, const char *fmt, ...) { #if 0 va_list args;
va_start(args, fmt);
sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL,
suffix, fmt, args);
va_end(args);
अंत में, यदि sshd को अपडेट या पुनर्संकलित नहीं किया जा सकता है, तो इस सिग्नल हैंडलर रेस कंडीशन को कॉन्फ़िगरेशन फ़ाइल में LoginGraceTime को केवल 0 पर सेट करके ठीक किया जा सकता है। यह sshd को सेवा से वंचित करने (सभी MaxStartups कनेक्शनों की समाप्ति) के प्रति संवेदनशील बनाता है, लेकिन यह इसे इस सलाह में प्रस्तुत दूरस्थ कोड निष्पादन से सुरक्षित बनाता है।
हम OpenSSH के डेवलपर्स को इस रिलीज़ पर उनके उत्कृष्ट कार्य और घनिष्ठ सहयोग के लिए धन्यवाद देते हैं। हम distros@openwall को भी धन्यवाद देते हैं। अंत में, हम इस सलाह को Sophia d'Antoine को समर्पित करते हैं।
2024-05-19: हमने OpenSSH के डेवलपर्स से संपर्क किया। पैच और पैच समीक्षाओं के क्रमिक पुनरावृत्तियाँ हुईं।
2024-06-20: हमने distros@openwall से संपर्क किया।
2024-07-01: समन्वित रिलीज़ तिथि।