
هذا هو POC الذي كتبته لـ CVE-2024-6387
Qualys Security Advisory
regreSSHion: تنفيذ تعليمات برمجية عن بُعد في خادم OpenSSH على أنظمة Linux المبنية على glibc (CVE-2024-6387)
ملخص SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, from 2005)
كل ما يتطلبه الأمر هو قفزة إيمان
-- The Interrupters, "Leap of Faith"
ملاحظة أولية: OpenSSH هو أحد أكثر البرمجيات أمانًا في العالم؛ هذه الثغرة هي مجرد زلّة واحدة في تنفيذ شبه خالٍ من العيوب. ويُعد تصميمها القائم على الدفاع المتعمّق ورمزها البرمجي نموذجًا ومصدر إلهام، ونشكر مطوّري OpenSSH على عملهم المثالي.
اكتشفنا ثغرة (حالة تسابق في معالج الإشارات) في خادم OpenSSH (sshd): إذا لم يقم العميل بالمصادقة خلال LoginGraceTime ثانية (120 افتراضيًا، 600 في إصدارات OpenSSH القديمة)، عندها يتم استدعاء معالج SIGALRM الخاص بـ sshd بشكل غير متزامن، لكن معالج الإشارة هذا يستدعي دوالًا مختلفة ليست آمنة للإشارات غير المتزامنة (على سبيل المثال، syslog()). تؤثر حالة التسابق هذه على sshd في تكوينه الافتراضي.
عند التحقيق، أدركنا أن هذه الثغرة هي في الواقع ارتداد لـ CVE-2006-5051 ("حالة تسابق في معالج الإشارات في OpenSSH قبل 4.4 تتيح للمهاجمين عن بُعد التسبب في رفض الخدمة (تعطل)، وربما تنفيذ كود عشوائي")، والتي أُبلغ عنها في 2006 بواسطة Mark Dowd.
تم إدخال هذا الارتداد في أكتوبر 2020 (OpenSSH 8.5p1) عبر الالتزام 752250c ("revised log infrastructure for OpenSSH")، والذي أزال عن طريق الخطأ "#ifdef DO_LOG_SAFE_IN_SIGHAND" من sigdie()، وهي دالة يتم استدعاؤها مباشرة بواسطة معالج SIGALRM الخاص بـ sshd. بعبارة أخرى:
OpenSSH < 4.4p1 معرّض لحالة التسابق هذه في معالج الإشارات، إذا لم يتم تصحيحه بنقل التصحيح الخاص بـ CVE-2006-5051، أو لم يتم تصحيحه ضد CVE-2008-4109، الذي كان تصحيحًا غير صحيح لـ CVE-2006-5051؛
4.4p1 <= OpenSSH < 8.5p1 غير معرّض لحالة التسابق هذه في معالج الإشارات (لأن "#ifdef DO_LOG_SAFE_IN_SIGHAND" الذي أُضيف إلى sigdie() بواسطة تصحيح CVE-2006-5051 حوّل هذه الدالة غير الآمنة إلى استدعاء آمن _exit(1))؛
8.5p1 <= OpenSSH < 9.8p1 معرّض مرة أخرى لحالة التسابق هذه في معالج الإشارات (لأن "#ifdef DO_LOG_SAFE_IN_SIGHAND" أُزيل عن طريق الخطأ من sigdie()).
هذه الثغرة قابلة للاستغلال عن بُعد على أنظمة Linux المبنية على glibc، حيث تستدعي syslog() نفسها دوالًا غير آمنة للإشارات غير المتزامنة (على سبيل المثال، malloc() و free()): تنفيذ كود عن بُعد غير مصادَق به بصلاحية root، لأنها تؤثر على الكود المميّز الخاص بـ sshd، وهو غير معزول ويعمل بصلاحيات كاملة. لم نتحقق من أي libc أو نظام تشغيل آخر؛ لكن OpenBSD ليس معرضًا بشكل ملحوظ، لأن معالج SIGALRM الخاص به يستدعي syslog_r()، وهي نسخة أكثر أمانًا للإشارات غير المتزامنة من syslog() ابتكرتها 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 يكون فيه فصل الامتيازات مفعّلاً افتراضيًا ويكون مصححًا ضد جميع الثغرات الحرجة في تلك الحقبة (خاصة CVE-2003-0693 و CVE-2002-0640).
لاستغلال هذا الإصدار عن بُعد، نقاطع استدعاءً لـ free() بواسطة SIGALRM (داخل كود تحليل المفاتيح العامة في sshd)، ونترك الكومة في حالة غير متناسقة، ونستغل هذه الحالة غير المتناسقة أثناء استدعاء آخر لـ free()، داخل معالج SIGALRM.
في تجاربنا، يستغرق الأمر حوالي 10,000 محاولة في المتوسط للفوز بحالة التسابق هذه؛ أي مع 10 اتصالات (MaxStartups) مقبولة كل 600 ثانية (LoginGraceTime)، يستغرق الأمر حوالي أسبوع في المتوسط للحصول على قشرة root عن بُعد.
ثانيًا، "SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3"، من "ubuntu-6.06.1-server-i386.iso": هذا هو آخر إصدار من Ubuntu لا يزال معرضًا لـ CVE-2006-5051 ("حالة تسابق في معالج الإشارات في OpenSSH قبل 4.4").
لاستغلال هذا الإصدار عن بُعد، نقاطع استدعاءً لـ pam_start() بواسطة SIGALRM، ونترك أحد هياكل PAM في حالة غير متناسقة، ونستغل هذه الحالة غير المتناسقة أثناء استدعاء لـ pam_end()، داخل معالج SIGALRM.
في تجاربنا، يستغرق الأمر حوالي 10,000 محاولة في المتوسط للفوز بحالة التسابق هذه؛ أي مع 10 اتصالات (MaxStartups) مقبولة كل 120 ثانية (LoginGraceTime)، يستغرق الأمر حوالي 1-2 يوم في المتوسط للحصول على قشرة root عن بُعد.
أخيرًا، "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2"، من "debian-12.5.0-i386-DVD-1.iso": هذا هو إصدار Debian المستقر الحالي، وهو معرض لارتداد CVE-2006-5051.
لاستغلال هذا الإصدار عن بُعد، نقاطع استدعاءً لـ malloc() بواسطة SIGALRM (داخل كود تحليل المفاتيح العامة في sshd)، ونترك الكومة في حالة غير متناسقة، ونستغل هذه الحالة غير المتناسقة أثناء استدعاء آخر لـ malloc()، داخل معالج SIGALRM (بتعبير أدق، داخل syslog()).
في تجاربنا، يستغرق الأمر حوالي 10,000 محاولة في المتوسط للفوز بحالة التسابق هذه، أي حوالي 3-4 ساعات مع 100 اتصال (MaxStartups) مقبول كل 120 ثانية (LoginGraceTime). في النهاية، يستغرق الأمر حوالي 6-8 ساعات في المتوسط للحصول على قشرة root عن بُعد، لأننا لا نستطيع تخمين عنوان glibc بشكل صحيح إلا في نصف المرات (بسبب ASLR).
لا يزال هذا البحث عملاً قيد التقدم:
استهدفنا الأجهزة الافتراضية فقط، وليس الخوادم الفعلية، عبر رابط شبكة مستقر في الغالب (~10ms تذبذب في الحزم)؛
نحن مقتنعون بأن الجوانب المختلفة لاستغلالاتنا يمكن تحسينها بشكل كبير؛
بدأنا العمل على استغلال amd64، وهو أصعب بكثير بسبب قوة ASLR الأكبر.
بعد أيام قليلة من بدء عملنا على amd64، لاحظنا تقرير الخطأ التالي (في نظام Bugzilla العام لـ OpenSSH)، حول توقف تام في معالج SIGALRM الخاص بـ sshd:
لذلك قررنا الاتصال بمطوّري OpenSSH فورًا (لإعلامهم بأن هذا التوقف التام ناتج عن ثغرة قابلة للاستغلال)، وأوقفنا عملنا على amd64، وبدأنا في كتابة هذه النشرة.
لكن هذا ليس من طبيعتي، أنا أتحرر
-- The Interrupters, "Haven't Seen the Last of Me"
معالج SIGALRM في هذا الإصدار من OpenSSH يستدعي packet_close()، الذي يستدعي buffer_free()، الذي يستدعي xfree() وبالتالي free()، وهي ليست آمنة للإشارات غير المتزامنة:
وبالتالي، بدأنا في قراءة كود malloc الخاص بـ glibc في هذا الإصدار من Debian (2.2.5)، لمعرفة ما إذا كان يمكن مقاطعة استدعاء أول لـ free() بواسطة SIGALRM واستغلاله أثناء استدعاء ثانٍ لـ free() داخل معالج SIGALRM (في الأسطر 341-344 أعلاه). ولأن malloc في هذا glibc غير محصّن ضد تقنية unlink() التي ابتكرها Solar Designer في 2000، سرعان ما رصدنا مسارًا برمجيًا مثيرًا للاهتمام في 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 قد وُسمت بالفعل كحرة (لأن بِت PREV_INUSE في chunk_Z تم مسحه في السطر 3246) لكنها لم تُربط بعد في قائمتها المرتبطة المزدوجة (في السطر 3251): بعبارة أخرى، لا يزال مؤشرا fd و bk في chunk_Y يحتويان على بيانات المستخدم (بيانات يتحكم فيها المهاجم).
ثانيًا، إذا قام (داخل معالج SIGALRM) packet_close() باستدعاء free(chunk_X)، عندها يتم الدخول إلى كتلة الكود في الأسطر 3230-3244 (لأن chunk_Y موسومة كحرة) ويتم إلغاء ربط chunk_Y عبر unlink() (في السطر 3241): وهي بدائية تُسمى aa4bmo (كتابة مرآتية عشوائية تقريبًا لأربع بايتات)، لأن مؤشري fd و bk في chunk_Y لا يزالان تحت سيطرة المهاجم. لمزيد من المعلومات حول تقنية unlink() وبدائية aa4bmo:
https://www.openwall.com/articles/JPEG-COM-Marker-Vulnerability#exploit http://phrack.org/issues/61/6.html#article
أخيرًا، باستخدام بدائية aa4bmo هذه، نستبدل مؤشر دالة __free_hook في glibc (هذا الإصدار القديم من Debian لا يحتوي على ASLR ولا NX) بعنوان شيل كود الخاص بنا في الكومة، مما يحقق تنفيذ كود عن بُعد أثناء استدعاء free() التالي في packet_close().
الآن يستولون على الأمور ويحكمون بالكامل
-- The Interrupters, "Liberty"
لشن هذا الهجوم ضد sshd، نقاطع استدعاءً لـ free() داخل كود تحليل sshd لمفتاح DSA العام (أي السطر 144 أدناه هو free(chunk_Y) الخاص بنا) ونستغله أثناء أحد استدعاءات free() في packet_close() (أي أحد الأسطر 341-344 أعلاه هو free(chunk_X) الخاص بنا):
في البداية، لم نتمكن أبدًا من الفوز بحالة التسابق هذه (أي مقاطعة استدعاء free() في السطر 144 في الوقت المناسب). في النهاية، أدركنا أنه يمكننا تحسين فرصنا في الفوز بهذا السباق بشكل كبير: كود تحليل مفتاح DSA العام يسمح لنا باستدعاء free() أربع مرات (في الأسطر 704-707 أدناه)، كما يسمح لنا sshd بمحاولة ست عمليات مصادقة مستخدم (AUTH_FAIL_MAX)؛ إذا تمت مقاطعة أي من استدعاءات free() الأربعة والعشرين هذه في الوقت المناسب، عندها نحقق لاحقًا تنفيذ كود عن بُعد داخل معالج SIGALRM.
مع هذا التحسين، فزنا أخيرًا بحالة التسابق بعد حوالي شهر: كنا سعداء (وقمنا برقصة قشرة root)، لكننا شعرنا أيضًا أن هناك مجالًا للتحسين.
لا تقلق، فقط انتظر وانظر
-- The Interrupters, "Haven't Seen the Last of Me"
لذلك نفذنا استراتيجية توقيت ثلاثية الأبعاد التالية:
لا ننتظر حتى اللحظة الأخيرة لإرسال حزمة مفتاح DSA العام (الكبيرة نسبيًا) إلى sshd: بدلاً من ذلك، نرسل الحزمة كاملة ما عدا بايت واحد (البايت الأخير) قبل وقت طويل من LoginGraceTime، ونرسل البايت الأخير في اللحظة الأخيرة تمامًا، لتقليل آثار تأخيرات الشبكة. (ونعطّل خوارزمية Nagle.)
نتتبع متوسط زمن الرحلة ذهابًا وإيابًا (عن طريق إرسال حزم تنتج استجابة من sshd بانتظام)، ونتتبع الفرق بين اللحظة التي نتوقع فيها إغلاق اتصالنا بواسطة sshd (أساسًا اللحظة التي نستلم فيها أول بايت من لافتة sshd، مضافًا إليها LoginGraceTime) واللحظة التي يُغلق فيها اتصالنا فعليًا بواسطة sshd، ونعدّل توقيتنا وفقًا لذلك (أي اللحظة التي نرسل فيها آخر بايت من حزمة DSA الخاصة بنا).
تتيح لنا هذه الفروق الزمنية تتبع انحرافات الساعة وتأخيرات الشبكة، والتي تُظهر أنماطًا يمكن التنبؤ بها بمرور الوقت: جرّبنا الانحدار الخطي والخطي المُجزّأ (spline)، لكن في النهاية، لم ينجح شيء أفضل من إعادة استخدام القياس الأكثر حداثة ببساطة. ربما قد يحقق التعلم العميق نتائج أفضل؛ نترك هذا كتمرين للقارئ المهتم.
الأهم من ذلك، نزيد فرصنا في الفوز بحالة التسابق هذه عن طريق ضبط توقيتنا ببطء من خلال ردود فعل لا إرادية من sshd:
إذا تلقينا استجابة (SSH2_MSG_USERAUTH_FAILURE) لحزمة مفتاح DSA العام الخاصة بنا، فهذا يعني أننا أرسلناها مبكرًا جدًا (كان لدى sshd الوقت لاستلام حزمتنا في الطفل غير المميّز، وتحليلها، وإرسالها إلى الطفل المميّز، وتحليلها هناك، وإرسال استجابة طوال الطريق إلينا)؛
إذا لم نتمكن حتى من إرسال آخر بايت من حزمة DSA الخاصة بنا، فهذا يعني أننا انتظرنا طويلاً (كان sshd قد استلم بالفعل SIGALRM وأغلق اتصالنا)؛
إذا تمكنا من إرسال آخر بايت من حزمة DSA الخاصة بنا، ولم نتلقَ أي استجابة قبل أن يغلق sshd اتصالنا، فهذا يعني أن توقيتنا كان دقيقًا بشكل معقول.
تتيح لنا هذه التغذية الراجعة استهداف ما نسميه نافذة السباق "الكبيرة": إصابة هذه النافذة لا تضمن الفوز بحالة التسابق، لكن داخل هذه النافذة الكبيرة توجد نوافذ السباق "الصغيرة" الأربع والعشرون (داخل استدعاءات free() الأربعة والعشرين) التي، إذا أُصيبت، تضمن الفوز بحالة التسابق.
مع هذه التحسينات، يستغرق الأمر حوالي 10,000 محاولة في المتوسط للفوز بحالة التسابق هذه؛ أي مع 10 اتصالات (MaxStartups) مقبولة كل 600 ثانية (LoginGraceTime)، يستغرق الأمر حوالي أسبوع في المتوسط للحصول على قشرة root عن بُعد.
أنام عندما تبدأ الشمس بالشروق
-- The Interrupters, "Alien"
معالج SIGALRM في هذا الإصدار من OpenSSH لم يعد يستدعي packet_close()؛ علاوة على ذلك، فإن glibc الخاص بهذا الإصدار من Ubuntu (2.3.6) يأخذ دائمًا قفلًا إلزاميًا عند الدخول إلى دوال عائلة malloc (حتى لو كان أحادي الخيط مثل sshd)، مما يمنعنا من مقاطعة استدعاء لإحدى دوال malloc ثم استغلاله لاحقًا أثناء استدعاء آخر لهذه الدوال (ستتوقف دائمًا عن العمل). يجب أن نجد حلاً آخر.
يذكر CVE-2006-5051 تحريرًا مزدوجًا في GSSAPI، لكن GSSAPI (أو Kerberos) غير مفعّل افتراضيًا، لذا لا يبدو هذا جذابًا للغاية. من ناحية أخرى، PAM مفعّل افتراضيًا، ويتم استدعاء pam_end() بواسطة معالج SIGALRM الخاص بـ sshd (وهي، بالطبع، ليست آمنة للإشارات غير المتزامنة). لذلك بحثنا عن دالة PAM يمكنها، إذا قاطعتها SIGALRM في الوقت المناسب، أن تترك الهياكل الداخلية لـ PAM في حالة غير متناسقة، قابلة للاستغلال أثناء pam_end() في معالج SIGALRM. وجدنا 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 (مؤشر دالة) لم تتم تهيئته بعد (نظرًا لأن استدعاء malloc() في السطر 57 لا يهيئ ذاكرته). إذا استطعنا التحكم في 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() إطلاقًا. العودة إلى المربع الأول.
Not giving up, it's not what we do
-- 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() فورًا بتعيين sshpam_handle الخاص بـ sshd إلى قطعة ذاكرة مخصصة عبر calloc()؛ وهذا آمن، لأن calloc() يهيئ هذه الذاكرة إلى صفر. من ناحية أخرى، إذا قوطعت _pam_add_handler() (التي تُستدعى عدة مرات بواسطة pam_start()) بواسطة SIGALRM بعد السطر 874 ولكن قبل السطر 886، فإن بنية مخصصة عبر malloc() تُربط في pamh، ولكن حقل next الخاص بها لم تتم تهيئته بعد. إذا استطعنا التحكم في next (من خلال مخلفات تخصيصات الكومة السابقة)، فسنتمكن من تمرير مؤشر عشوائي إلى free() أثناء استدعاء pam_end() (داخل معالج SIGALRM)، عند السطر 1020 (والسطر 1017) أدناه:
نظرًا لأن malloc من glibc في هذا الإصدار من Ubuntu أصبح محصّنًا بالفعل ضد تقنية unlink() القديمة، قررنا تحويل free() العشوائي لدينا إلى تقنية House of Mind من Malloc Maleficarum (نسخة fastbin): نحرر قطعة NON_MAIN_ARENA الخاصة بنا، ونوجّه arena الوهمية إلى .got.plt الخاص بـ sshd (هذا الإصدار من Ubuntu يطبّق ASLR على sshd لكن لا يطبّق PIE)، ونستبدل مدخل _exit() بعنوان شيلكود لدينا في الكومة (كومة هذا الإصدار من Ubuntu ما تزال قابلة للتنفيذ افتراضيًا). لمزيد من المعلومات حول Malloc Maleficarum:
I learned everything the hard way
-- The Interrupters, "The Hard Way"
لتنفيذ هذا الهجوم ضد sshd، واجهنا في البداية ثلاث مشاكل:
تتطلب تقنية House of Mind تخزين المؤشر إلى arena الوهمية لدينا عند العنوان 0x08100000 في الكومة؛ ولكن هل يمكننا تخزين بيانات يتحكم فيها المهاجم عند هذا العنوان المرتفع؟ نظرًا لأن sshd يستدعي pam_start() في بداية مصادقة المستخدم تمامًا، فإننا لا نتحكم في أي شيء سوى اسم المستخدم نفسه؛ ولحسن الحظ، يسمح لنا اسم مستخدم بطول ~128KB (أقصر من DEFAULT_MMAP_THRESHOLD) بتخزين بياناتنا الخاصة عند العنوان 0x08100000.
يجب ألا يكون حقل الحجم (size) لقطعة NON_MAIN_ARENA الوهمية كبيرًا جدًا (لتجاوز الفحوصات الأمنية في free())، أي يجب أن يحتوي على بايتات صفرية. لكن اسم المستخدم الطويل لدينا هو سلسلة تنتهي بصفر ولا يمكن أن تحتوي على بايتات صفرية؛ ولحسن الحظ تذكّرنا أن _pam_free_handlers_aux() تصفّر البنى التي تحررها free() (السطر 1019 أعلاه): لذلك نقوم "بتصحيح" حقل الحجم لقطعتنا الوهمية عبر memset(0) من هذا القبيل، وعندها فقط نحررها عبر free().
يجب أن ننجو من عدة استدعاءات إلى free() (عند السطرين 1017 و1020 أعلاه) قبل تحرير قطعة NON_MAIN_ARENA الوهمية. نحول هذه الاستدعاءات إلى عمليات عديمة الأثر (no-op) عبر توجيهها إلى قطع IS_MMAPPED وهمية: يستدعي free() الدالة munmap_chunk()، التي تستدعي munmap()، والتي تفشل لأن هذه القطع الوهمية IS_MMAPPED غير محاذاة؛ وهذا فعليًا عديم الأثر، لأن إخفاقات assert() غير مُطبَّقة في glibc لهذا الإصدار من Ubuntu.
أخيرًا، يسمح لنا اسم المستخدم الطويل أيضًا بالتحكم في حقل next الذي قد يكون غير مهيأ في 20 بنية مختلفة (من خلال مخلفات النسخ المؤقتة لاسم المستخدم الطويل)، لأن pam_start() تستدعي _pam_add_handler() عدة مرات؛ أي أن نافذة السباق الواسعة لدينا تحتوي على 20 نافذة سباق صغيرة.
Same tricks they used before
-- The Interrupters, "Divide Us"
لهذا الهجوم ضد Ubuntu 6.06.1، أعدنا ببساطة استخدام استراتيجية التوقيت التي استخدمناها ضد Debian 3.0r6: يتطلب الأمر حوالي 10000 محاولة في المتوسط للفوز بشرط السباق، ومع 10 اتصالات (MaxStartups) مقبولة كل 120 ثانية (LoginGraceTime)، يستغرق الأمر في المتوسط من يوم إلى يومين للحصول على شل جذر عن بُعد.
ملاحظة: نظرًا لأن glibc لهذا الإصدار من Ubuntu يأخذ دائمًا قفلًا إلزاميًا عند الدخول إلى دوال عائلة malloc، فقد يواجه مهاجم غير محظوظ طريقًا مسدودًا في جميع اتصالات MaxStartups العشرة قبل الحصول على شل جذر؛ لم نحاول الالتفاف على هذه المشكلة لأن هدفنا النهائي كان استغلال إصدار حديث من OpenSSH على أي حال.
Now you're ready, take the demons head on
-- The Interrupters, "Be Gone"
معالج SIGALRM في هذا الإصدار من OpenSSH لا يستدعي packet_close() ولا pam_end()؛ في الواقع، يستدعي دالة واحدة مثيرة للاهتمام فقط، وهي syslog():
سؤالان رئيسيان إذن: هل تستدعي syslog() في glibc هذا الإصدار من Debian (2.36) دوالًا غير آمنة في سياق الإشارات غير المتزامنة مثل malloc() وfree()؟ وإذا كان الجواب نعم، فهل ما يزال هذا glibc يأخذ قفلًا إلزاميًا عند الدخول إلى دوال عائلة malloc؟
ملاحظة: نظرًا لأننا لا نتحكم في أي شيء يتعلق بهذه التخصيصات عبر malloc() (لا ترتيبها، ولا أحجامها، ولا محتوياتها)، اعتبرنا أن "rce" عند السطر 166 هو فأل خير كنا في أمس الحاجة إليه.
ولحسن حظنا أيضًا، الإجابة على سؤالنا الثاني هي لا؛ فمنذ أكتوبر 2017، لم تعد دوال malloc في glibc تأخذ أي قفل عندما يكون البرنامج أحادي الخيط (مثل 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
عمليًا، في حالة sshd على i386، يتم تعشية كل تعيين ذاكرة بشكل طبيعي (PIE الخاص بـ sshd، والكومة، ومعظم المكتبات، والمكدس)، لكن glibc نفسه يُعيَّن دائمًا إما عند العنوان 0xb7200000 أو عند العنوان 0xb7400000؛ بعبارة أخرى، يمكننا تخمين عنوان glibc بشكل صحيح في نصف المرات (ثمن صغير ندفعه مقابل هزيمة ASLR). نفترض في استغلالنا أن glibc مُعيَّن عند العنوان 0xb7400000، لأنه أكثر شيوعًا قليلًا من 0xb7200000.
سؤالنا التالي هو: أي مسارات برمجية داخل دوال malloc في glibc، إذا قوطعت بواسطة SIGALRM في الوقت المناسب، تترك الكومة في حالة غير متناسقة، قابلة للاستغلال أثناء أحد استدعاءات malloc() داخل معالج SIGALRM؟
وجدنا العديد من المسارات البرمجية المثيرة للاهتمام (والمفاجئة!)، لكن المسار الذي اخترناه يتضمن أحجامًا نسبية فقط، وليس عناوين مطلقة (على عكس مسارات برمجية مختلفة داخل unlink_chunk() مثلًا)؛ قد يكون هذا الاختلاف حاسمًا لاستغلال مستقبلي على amd64. هذا المسار، داخل malloc()، يقسم قطعة حرة كبيرة (victim) إلى قطعتين أصغر؛ تُعاد القطعة الأولى إلى مستدعي malloc() (عند السطر 4345)، بينما تُربط القطعة الثانية (remainder) في قائمة غير مرتبة من القطع الحرة (عند السطرين 4324-4327):
إذا قوطع هذا المسار البرمجي بواسطة SIGALRM بعد السطر 4327 ولكن قبل السطر 4339، فإن قطعة remainder من هذا التقسيم تكون قد ارتبطت بالفعل في القائمة غير المرتبة للقطع الحرة (الأسطر 4324-4327)، لكن حقل الحجم (mchunk_size) الخاص بها لم تتم تهيئته بعد (السطر 4339).
إذا استطعنا التحكم في حقل الحجم الخاص بها (من خلال مخلفات تخصيصات الكومة السابقة)، فسنتمكن من جعل قطعة remainder هذه أكبر وتتداخل مع قطع كومة أخرى، وبالتالي إفساد ذاكرة الكومة عندما يتم تخصيص هذه القطعة الموسّعة المتداخلة عبر malloc() والكتابة إليها في النهاية (داخل معالج SIGALRM).
سؤالنا الأخير إذن هو: نظرًا لأننا لا نتحكم في أي شيء يتعلق باستدعاءات malloc() داخل معالج SIGALRM، فما الذي يمكننا استبداله في الكومة لتحقيق تنفيذ كود عشوائي قبل أن يستدعي sshd الدالة _exit() (في sshsigdie())؟
نظرًا لأن __tzfile_read() (داخل معالج SIGALRM) تخصص بنية FILE عبر malloc() في الكومة (عند السطر 166 أعلاه)، ونظرًا لأن بنى FILE لها تاريخ طويل من إساءة الاستخدام لتنفيذ كود عشوائي، قررنا توجيه إفساد الكومة لدينا نحو بنية FILE هذه. لكن هذا أسهل قولًا من فعلًا: إفساد الكومة لدينا محدود للغاية، وبنى FILE أصبحت محصّنة بشكل كبير على مر السنين (بواسطة IO_validate_vtable() وPTR_DEMANGLE() مثلًا).
في النهاية، ابتكرنا التقنية التالية (التي تبدو خاصة بـ glibc على i386 -- فـ glibc على amd64 لا يبدو أنه يستخدم _vtable_offset إطلاقًا):
من خلال إفساد الكومة المحدود لدينا، نستبدل حقل _vtable_offset (محرفًا موقعًا واحدًا) من بنية FILE الخاصة بـ __tzfile_read()؛
وستبحث دوال libio في glibc بالتالي عن مؤشر vtable لبنية FILE هذه (مؤشر إلى مصفوفة من مؤشرات الدوال) عند إزاحة غير صفرية (_vtable_offset الذي استبدلناه)، بدلًا من الإزاحة الصفرية الافتراضية؛
يمكننا نحن (المهاجمين) بسهولة التحكم في مؤشر vtable الوهمي هذا (من خلال مخلفات تخصيصات الكومة السابقة)، لأن بنية FILE حول هذه الإزاحة لا تتم تهيئتها صراحةً بواسطة fopen()؛
لتجاوز الفحوصات الأمنية في glibc، يجب أن يشير مؤشر vtable الوهمي لدينا إلى مكان ما داخل قسم __libc_IO_vtables: قررنا توجيهه إلى vtable الخاص بتيارات المحارف العريضة، _IO_wfile_jumps (أي إلى 0xb761b740، لأننا نفترض أن glibc مُعيَّن عند العنوان 0xb7400000)؛
ونتيجة لذلك، تستدعي __fread_unlocked() (عند السطر 186 أعلاه) الدالة _IO_wfile_underflow() (بدلًا من _IO_file_underflow())، والتي تستدعي مؤشر دالة (__fct) يأتي أساسًا من بنية يكون مؤشرها (_codecvt) حقلًا آخر من حقول بنية FILE؛
يمكننا نحن (المهاجمين) بسهولة التحكم في مؤشر _codecvt هذا (من خلال مخلفات تخصيصات الكومة السابقة، لأن هذا الحقل من بنية FILE لا تتم تهيئته صراحةً بواسطة fopen())، مما يسمح لنا أيضًا بالتحكم في مؤشر الدالة __fct.
باختصار، من خلال استبدال بايت واحد (_vtable_offset) من بنية FILE المخصصة عبر malloc() بواسطة fopen()، يمكننا استدعاء مؤشر الدالة __fct الخاص بنا وتنفيذ كود عشوائي أثناء __fread_unlocked().
I wanted it perfect, no wrinkles in it
-- 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 من هذا التقسيم تكون قد ارتبطت بالفعل في القائمة غير المرتبة للقطع الحرة، لكن حقل الحجم الخاص بها يخضع لسيطرتنا (من خلال مخلفات تخصيصات الكومة السابقة)، وتتداخل قطعة 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()، مما يقسم الكتلة المتبقية الموسّعة إلى جزأين (مخزن القراءة 4KB وكتلة متبقية صغيرة):
---|-----------------------|----------------------|---|------------|--- XXX| large allocated chunk | |XXX| FILE |XXX ---|-----------------------|----------------------|---|--|---------|--- | ~4KB |<--------------------------->|<------->| 4KB read buffer remainder
لذلك نكتب فوق أجزاءٍ من بنية FILE بالترويسة الداخلية لهذه الكتلة المتبقية الصغيرة: بشكل أكثر دقة، نكتب فوق _vtable_offset الخاص بـ FILE بالبايت الثالث من حقل bk الخاص بهذه الترويسة، وهو مؤشر إلى القائمة غير المرتبة للكتل الحرة، 0xb761d7f8 (أي نكتب فوق _vtable_offset بـ 0x61);
ثم، كما هو موضح في القسم الفرعي «النظرية»، تستدعي __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 هذا الانقسام الأول في الوقت المناسب، فإن fopen() داخل معالج SIGALRM تخصص الفراغ الصغير 1 لبنية FILE الخاصة بها عبر malloc()، ونحقق تنفيذ كود تعسفي كما هو موضح أعلاه؛
إذا لم يحدث ذلك، فإننا نخصص الفراغ الصغير 1 بأنفسنا عبر malloc() باستخدام أول malloc(304)، و:
2/ استدعاء malloc(~4KB) الثاني لدينا يقسم الفراغ الكبير 2 إلى جزأين:
إذا قاطع SIGALRM هذا الانقسام الثاني في الوقت المناسب، فإن fopen() داخل معالج SIGALRM تخصص الفراغ الصغير 2 لبنية FILE الخاصة بها عبر malloc()، ونحقق تنفيذ كود تعسفي كما هو موضح أعلاه؛
إذا لم يحدث ذلك، فإننا نخصص الفراغ الصغير 2 بأنفسنا عبر malloc() باستخدام ثاني malloc(304)، إلخ.
تمكنا من إنشاء 27 زوجًا من هذه الفراغات الكبيرة والصغيرة في كومة sshd (28 سيتجاوز PACKET_MAX_SIZE، أي 256KB): أصبحت نافذة السباق الكبيرة لدينا الآن تحتوي على 27 نافذة سباق صغيرة! كان تحقيق هذا التخطيط المعقد للكومة مؤلمًا للغاية ويستغرق وقتًا طويلاً، لكن أبرز نتيجتين هما:
لتحقيق هذا التخطيط للكومة بشكل موثوق، نرسل خمس حزم مختلفة من المفاتيح العامة إلى sshd (يمكن إرسال الحزم a/ إلى d/ قبل SIGALRM بوقت طويل؛ ويمكن أيضًا إرسال معظم الحزمة e/ قبل SIGALRM بوقت طويل، ولكن يجب إرسال آخر بايت منها في اللحظة الأخيرة تمامًا):
a/ نخصص عبر malloc() ونحرر مجموعة متنوعة من كتل tcache، لضمان أن تخصيصات الكومة التي لا نتحكم فيها تنتهي في هذه الكتل tcache ولا تتعارض مع تخطيط الكومة الدقيق لدينا.
b/ نخصص عبر malloc() ونحرر كتلًا بأحجام مختلفة، لإنشاء أزواجنا الـ 27 من الفراغات الكبيرة والصغيرة (وكتل «الحاجز» المقابلة).
c/ نخصص عبر malloc() ونحرر كتلًا بحجم ~4KB وكتلًا بحجم 320B، من أجل:
كتابة الترويسة المزيفة (حقل الحجم الكبير) للكتلة المتبقية الموسّعة المحتملة لدينا، في منتصف فراغاتنا الكبيرة؛
كتابة التذييل المزيف للكتلة المتبقية الموسّعة المحتملة لدينا، في نهاية فراغاتنا الصغيرة (لاجتياز الفحوصات الأمنية في glibc)؛
كتابة مؤشرات vtable المزيفة ومؤشرات _codecvt الخاصة بنا، داخل فراغاتنا الصغيرة (التي تُعد بنيات FILE محتملة).
d/ نخصص عبر malloc() ونحرر سلسلة كبيرة جدًا (حوالي 256KB)، لضمان إزالة فراغاتنا الكبيرة والصغيرة من القائمة غير المرتبة للكتل الحرة ووضعها في صناديق malloc الخاصة بها.
e/ نجبر sshd على تنفيذ تسلسلنا النهائي من استدعاءات malloc() (malloc(~4KB)، malloc(304)، malloc(~4KB)، malloc(304)، إلخ)، لفتح نوافذ السباق الصغيرة الـ 27 لدينا.
قد لاحظ القراء المهتمون أننا لم نتناول بعد (حرفيًا ومجازيًا) مشكلة _codecvt. في الواقع، _codecvt هو مؤشر إلى بنية (_IO_codecvt) تحتوي على مؤشر إلى بنية (__gconv_step) تحتوي على مؤشر الدالة __fct الذي يسمح لنا بتنفيذ كود تعسفي. للتحكم بشكل موثوق في __fct عبر _codecvt، نوجه _codecvt ببساطة إلى أحد صناديق malloc الخاصة بـ glibc، الذي يحتوي بشكل ملائم على مؤشر إلى إحدى كتلنا الحرة في الكومة، والتي تحتوي على مؤشر دالة __fct الخاص بنا إلى كود glibc التعسفي (جميع عناوين glibc هذه معروفة لنا، لأننا نفترض أن glibc مُعيّن على العنوان 0xb7400000).
نفد وقتنا
-- The Interrupters, "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 محاولة في المتوسط؛ أي مع قبول 100 اتصال (MaxStartups) كل 120 ثانية (LoginGraceTime)، يستغرق الفوز في حالة السباق حوالي 3-4 ساعات في المتوسط، وحوالي 6-8 ساعات للحصول على صدفة جذر عن بُعد (بسبب ASLR).
ما خطتك للغد؟
-- The Interrupters, "Take Back the Power"
قررنا استهداف Rocky Linux 9 (مشتق من Red Hat Enterprise Linux 9)، من "Rocky-9.4-x86_64-minimal.iso"، لسببين:
إصدار OpenSSH الخاص به (8.7p1) معرّض لحالة السباق هذه في معالج الإشارة، و glibc الخاص به مُعيّن دائمًا عند مضاعف 2MB (بسبب ضعف ASLR الذي نوقش في القسم الفرعي السابق «النظرية»)، مما يجعل عمليات الكتابة الجزئية فوق المؤشرات أكثر قوة بكثير؛
دالة syslog() (التي ليست آمنة للإشارات غير المتزامنة ولكن يتم استدعاؤها بواسطة معالج SIGALRM في sshd) في إصدار glibc هذا (2.34) تستدعي داخليًا __open_memstream()، التي تخصص بنية FILE في الكومة عبر malloc()، وتستدعي أيضًا calloc() و realloc() و free() (وهو ما يمنحنا بعض الحرية التي نحتاجها بشدة).
مع وجود إفساد للكومة كبدائية، وبنيتَي FILE مخصصتين في الكومة عبر malloc()، و21 بتًا ثابتًا في عناوين glibc، نعتقد أن حالة السباق في معالج الإشارة هذه قابلة للاستغلال على amd64 (ربما ليس في ~6-8 ساعات، ولكن نأمل في أقل من أسبوع). فقط الوقت سيخبرنا.
ملاحظة جانبية: اكتشفنا أن Ubuntu 24.04 لا يعيد توزيع العشوائية في ASLR لعمليات sshd الفرعية (يتم توزيع العشوائية مرة واحدة فقط، عند الإقلاع)؛ وقد تتبعنا هذا إلى التصحيح أدناه، الذي يعطّل rexec_flag في sshd. هذه فكرة سيئة عمومًا، لكن في الحالة الخاصة لحالة السباق في معالج الإشارة هذه، فإنها تمنع استغلال sshd: syslog() داخل معالج SIGALRM لا تستدعي أيًا من دوال malloc، لأنها ليست أبدًا أول استدعاء لـ syslog().
https://git.launchpad.net/ubuntu/+source/openssh/tree/debian/patches/systemd-socket-activation.patch
لقد جاءت العاصفة ومضت
-- The Interrupters, "Good Things"
في 6 يونيو 2024، تم إصلاح حالة السباق في معالج الإشارة هذه بواسطة الالتزام 81c1099 («إضافة تسهيل إلى sshd(8) لمعاقبة سلوكيات العملاء الإشكالية المحددة»)، والذي نقل الكود غير الآمن للإشارات غير المتزامنة من معالج SIGALRM في sshd إلى عملية الاستماع في sshd، حيث يمكن التعامل معه بشكل متزامن:
نظرًا لأن هذا الإصلاح جزء من التزام كبير (81c1099)، فوق التزام أكبر للدفاع المتعمق (03e3de4، «بدء عملية تقسيم sshd إلى ملفات تنفيذية منفصلة»)، فقد يثبت صعوبة نقل الإصلاح إلى الإصدارات الأقدم. في هذه الحالة، يمكن إصلاح حالة السباق في معالج الإشارة نفسها عن طريق إزالة أو التعليق على الكود غير الآمن للإشارات غير المتزامنة من دالة sshsigdie()؛ على سبيل المثال:
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: تاريخ الإصدار المنسق.