Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
regreSSHion — هذا هو POC الذي كتبته لـ CVE-2024-6387 | Kitploit
أدوات/GitHubGitHub/teamos-hub/regresshion
تحليل الثغرات الأمنيةالاستغلالاختبار الاختراقالفريق الأحمرأداة الوصول عن بعداستغلال الملفات الثنائية
GitHubteamos-hub/regresshion

regreSSHion

هذا هو POC الذي كتبته لـ CVE-2024-6387

عرض المستودع
12منذ 2 سنواتلم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

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)

  • النظرية
  • التطبيق العملي
  • التوقيت SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, from 2006)
  • النظرية، المحاولة الأولى
  • النظرية، المحاولة الثانية
  • التطبيق العملي
  • التوقيت SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, from 2024)
  • النظرية
  • التطبيق العملي
  • التوقيت نحو استغلال amd64 التصحيحات والتخفيف شكر وتقدير الخط الزمني

======================================================================== ملخص

root@kitploit:~
كل ما يتطلبه الأمر هو قفزة إيمان
    -- 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:

https://lcamtuf.coredump.cx/signals.txt

ومع ذلك، واجهنا فورًا ثلاث مشاكل رئيسية:

  • من وجهة نظر نظرية، يجب أن نجد مسارًا برمجيًا مفيدًا، إذا قُطع في الوقت المناسب بواسطة 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:

https://bugzilla.mindrot.org/show_bug.cgi?id=3690

لذلك قررنا الاتصال بمطوّري OpenSSH فورًا (لإعلامهم بأن هذا التوقف التام ناتج عن ثغرة قابلة للاستغلال)، وأوقفنا عملنا على amd64، وبدأنا في كتابة هذه النشرة.

======================================================================== SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, from 2005)


النظرية

root@kitploit:~
لكن هذا ليس من طبيعتي، أنا أتحرر
    -- The Interrupters, "Haven't Seen the Last of Me"

معالج SIGALRM في هذا الإصدار من OpenSSH يستدعي packet_close()، الذي يستدعي buffer_free()، الذي يستدعي xfree() وبالتالي free()، وهي ليست آمنة للإشارات غير المتزامنة:


302 grace_alarm_handler(int sig) 303 { ... 307 packet_close();

329 packet_close(void) 330 { ... 341 buffer_free(&input); 342 buffer_free(&output); 343 buffer_free(&outgoing_packet); 344 buffer_free(&incoming_packet);

35 buffer_free(Buffer *buffer) 36 { 37 memset(buffer->buf, 0, buffer->alloc); 38 xfree(buffer->buf); 39 }

51 xfree(void *ptr) 52 { 53 if (ptr == NULL) 54 fatal("xfree: NULL pointer given as argument"); 55 free(ptr); 56 }

وبالتالي، بدأنا في قراءة كود malloc الخاص بـ glibc في هذا الإصدار من Debian (2.2.5)، لمعرفة ما إذا كان يمكن مقاطعة استدعاء أول لـ free() بواسطة SIGALRM واستغلاله أثناء استدعاء ثانٍ لـ free() داخل معالج SIGALRM (في الأسطر 341-344 أعلاه). ولأن malloc في هذا glibc غير محصّن ضد تقنية unlink() التي ابتكرها Solar Designer في 2000، سرعان ما رصدنا مسارًا برمجيًا مثيرًا للاهتمام في chunk_free() (الذي يُستدعى داخليًا بواسطة free()):


1028 struct malloc_chunk 1029 { 1030 INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). / 1031 INTERNAL_SIZE_T size; / Size in bytes, including overhead. / 1032 struct malloc_chunk fd; /* double links -- used only if free. / 1033 struct malloc_chunk bk; 1034 };

2516 #define unlink(P, BK, FD)
2517 {
2518 BK = P->bk;
2519 FD = P->fd;
2520 FD->bk = BK;
2521 BK->fd = FD;
2522 } \

3160 chunk_free(arena ar_ptr, mchunkptr p) .... 3164 { 3165 INTERNAL_SIZE_T hd = p->size; / its head field / .... 3177 sz = hd & ~PREV_INUSE; 3178 next = chunk_at_offset(p, sz); 3179 nextsz = chunksize(next); .... 3230 if (!(inuse_bit_at_offset(next, nextsz))) / consolidate forward / 3231 { .... 3241 unlink(next, bck, fwd); .... 3244 } 3245 else 3246 set_head(next, nextsz); / clear inuse bit */ .... 3251 frontlink(ar_ptr, p, sz, idx, bck, fwd);

لاستغلال هذا المسار البرمجي، نرتّب كومة 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().


التطبيق العملي

root@kitploit:~
الآن يستولون على الأمور ويحكمون بالكامل
    -- The Interrupters, "Liberty"

لشن هذا الهجوم ضد sshd، نقاطع استدعاءً لـ free() داخل كود تحليل sshd لمفتاح DSA العام (أي السطر 144 أدناه هو free(chunk_Y) الخاص بنا) ونستغله أثناء أحد استدعاءات free() في packet_close() (أي أحد الأسطر 341-344 أعلاه هو free(chunk_X) الخاص بنا):


136 buffer_get_bignum2(Buffer *buffer, BIGNUM *value) 137 { 138 u_int len; 139 u_char *bin = buffer_get_string(buffer, &len); ... 143 BN_bin2bn(bin, len, value); 144 xfree(bin); 145 }

في البداية، لم نتمكن أبدًا من الفوز بحالة التسابق هذه (أي مقاطعة استدعاء free() في السطر 144 في الوقت المناسب). في النهاية، أدركنا أنه يمكننا تحسين فرصنا في الفوز بهذا السباق بشكل كبير: كود تحليل مفتاح DSA العام يسمح لنا باستدعاء free() أربع مرات (في الأسطر 704-707 أدناه)، كما يسمح لنا sshd بمحاولة ست عمليات مصادقة مستخدم (AUTH_FAIL_MAX)؛ إذا تمت مقاطعة أي من استدعاءات free() الأربعة والعشرين هذه في الوقت المناسب، عندها نحقق لاحقًا تنفيذ كود عن بُعد داخل معالج SIGALRM.


678 key_from_blob(u_char *blob, int blen) 679 { ... 693 switch (type) { ... 702 case KEY_DSA: 703 key = key_new(type); 704 buffer_get_bignum2(&b, key->dsa->p); 705 buffer_get_bignum2(&b, key->dsa->q); 706 buffer_get_bignum2(&b, key->dsa->g); 707 buffer_get_bignum2(&b, key->dsa->pub_key);

مع هذا التحسين، فزنا أخيرًا بحالة التسابق بعد حوالي شهر: كنا سعداء (وقمنا برقصة قشرة root)، لكننا شعرنا أيضًا أن هناك مجالًا للتحسين.


التوقيت

root@kitploit:~
لا تقلق، فقط انتظر وانظر
    -- 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 عن بُعد.

======================================================================== SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 (Ubuntu 6.06.1, from 2006)


النظرية، المحاولة الأولى

root@kitploit:~
أنام عندما تبدأ الشمس بالشروق
    -- 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):


104 void _pam_free_data(pam_handle_t *pamh, int status) 105 { 106 struct pam_data *last; 107 struct pam_data *data; ... 112 data = pamh->data; 113 114 while (data) { 115 last = data; 116 data = data->next; 117 if (last->cleanup) { 118 last->cleanup(pamh, last->data, status);

كانت هذه ستشكّل استغلالة بسيطة للغاية؛ لكننا وللأسف أغفلنا تمامًا أن pam_set_data() يمكن استدعاؤها فقط من وحدات PAM: إذا قاطعناها بـ SIGALRM، فإن pamh->caller_is يظل _PAM_CALLED_FROM_MODULE، وفي هذه الحالة يعود pam_end() فورًا، دون استدعاء _pam_free_data() إطلاقًا. العودة إلى المربع الأول.


Theory, take two

root@kitploit:~
Not giving up, it's not what we do
    -- The Interrupters, "Title Holder"

لاحظنا أنه في السطر 601 أدناه، يمرر sshd مؤشرًا إلى مؤشره العام sshpam_handle مباشرةً إلى pam_start() (التي تُستدعى مرة واحدة لكل اتصال):


202 static pam_handle_t *sshpam_handle = NULL;


584 sshpam_init(Authctxt *authctxt) 585 { ... 600 sshpam_err = 601 pam_start(SSHD_PAM_SERVICE, user, &store_conv, &sshpam_handle);

لذلك قررنا أن ننظر إلى pam_start() نفسها: إذا قوطعت بواسطة SIGALRM، فقد تترك البنية التي يشير إليها sshpam_handle في حالة غير متناسقة، ويمكن عندها استغلال ذلك داخل معالج SIGALRM، عند استدعاء "pam_end(sshpam_handle, sshpam_err)".


18 int pam_start ( .. 22 pam_handle_t **pamh) 23 { .. 32 if ((*pamh = calloc(1, sizeof(**pamh))) == NULL) { ... 110 if ( _pam_init_handlers(*pamh) != PAM_SUCCESS ) {

319 int _pam_init_handlers(pam_handle_t *pamh) 320 { ... 398 retval = _pam_parse_conf_file(pamh, f, pamh->service_name, PAM_T_ANY

66 static int _pam_parse_conf_file(pam_handle_t *pamh, FILE *f .. 73 { ... 252 res = _pam_add_handler(pamh, must_fail, other

581 int _pam_add_handler(pam_handle_t *pamh ... 585 { ... 755 the_handlers = (other) ? &pamh->handlers.other : &pamh->handlers.conf; ... 767 handler_p = &the_handlers->authenticate; ... 874 if ((*handler_p = malloc(sizeof(struct handler))) == NULL) { ... 886 (*handler_p)->next = NULL;

في السطر 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) أدناه:


11 int pam_end(pam_handle_t *pamh, int pam_status) 12 { .. 31 if ((ret = _pam_free_handlers(pamh)) != PAM_SUCCESS) {

925 int _pam_free_handlers(pam_handle_t *pamh) 926 { ... 954 _pam_free_handlers_aux(&(pamh->handlers.conf.authenticate));

1009 void _pam_free_handlers_aux(struct handler **hp) 1010 { 1011 struct handler *h = *hp; 1012 struct handler last; .... 1015 while (h) { 1016 last = h; 1017 _pam_drop(h->argv); / This is all alocated in a single chunk */ 1018 h = h->next; 1019 memset(last, 0, sizeof(*last)); 1020 free(last); 1021 }

نظرًا لأن 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:

https://seclists.org/bugtraq/2005/Oct/118


Practice

root@kitploit:~
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 نافذة سباق صغيرة.


Timing

root@kitploit:~
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 على أي حال.

======================================================================== SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 (Debian 12.5.0, from 2024)


Theory

root@kitploit:~
Now you're ready, take the demons head on
    -- The Interrupters, "Be Gone"

معالج SIGALRM في هذا الإصدار من OpenSSH لا يستدعي packet_close() ولا pam_end()؛ في الواقع، يستدعي دالة واحدة مثيرة للاهتمام فقط، وهي syslog():


358 grace_alarm_handler(int sig) 359 { ... 370 sigdie("Timeout before authentication for %s port %d", 371 ssh_remote_ipaddr(the_active_state), 372 ssh_remote_port(the_active_state));

96 #define sigdie(...) sshsigdie(FILE, func, LINE, 0, SYSLOG_LEVEL_ERROR, NULL, VA_ARGS)

451 sshsigdie(const char *file, const char *func, int line, int showfunc, 452 LogLevel level, const char *suffix, const char *fmt, ...) 453 { ... 457 sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL, 458 suffix, fmt, args);

464 sshlogv(const char *file, const char *func, int line, int showfunc, 465 LogLevel level, const char *suffix, const char *fmt, va_list args) 466 { ... 489 do_log(level, forced, suffix, fmt2, args);

337 do_log(LogLevel level, int force, const char *suffix, const char *fmt, 338 va_list args) 339 { ... 419 syslog(pri, "%.500s", fmtbuf);

سؤالان رئيسيان إذن: هل تستدعي syslog() في glibc هذا الإصدار من Debian (2.36) دوالًا غير آمنة في سياق الإشارات غير المتزامنة مثل malloc() وfree()؟ وإذا كان الجواب نعم، فهل ما يزال هذا glibc يأخذ قفلًا إلزاميًا عند الدخول إلى دوال عائلة malloc؟

  • لحسن حظنا نحن المهاجمين، الإجابة على سؤالنا الأول هي نعم؛ إذا وفقط إذا كانت استدعاء syslog() داخل معالج SIGALRM هو أول استدعاء على الإطلاق إلى syslog()، فإن __localtime64_r() (التي تستدعيها syslog()) تستدعي malloc(304) لتخصيص بنية FILE (عند السطر 166) وتستدعي malloc(4096) لتخصيص مخزن قراءة داخلي (عند السطر 186):

28 __localtime64_r (const __time64_t *t, struct tm *tp) 29 { 30 return __tz_convert (*t, 1, tp);

567 __tz_convert (__time64_t timer, int use_localtime, struct tm *tp) 568 { ... 577 tzset_internal (tp == &_tmbuf && use_localtime);

367 tzset_internal (int always) 368 { ... 405 __tzfile_read (tz, 0, NULL);

105 __tzfile_read (const char *file, size_t extra, char **extrap) 106 { ... 109 FILE *f; ... 166 f = fopen (file, "rce"); ... 186 if (__builtin_expect (__fread_unlocked ((void *) &tzhead, sizeof (tzhead), 187 1, f) != 1, 0)

ملاحظة: نظرًا لأننا لا نتحكم في أي شيء يتعلق بهذه التخصيصات عبر 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):


1449 #define set_head(p, s) ((p)->mchunk_size = (s))

3765 _int_malloc (mstate av, size_t bytes) 3766 { .... 3798 nb = checked_request2size (bytes); .... 4295 size = chunksize (victim); .... 4300 remainder_size = size - nb; .... 4316 remainder = chunk_at_offset (victim, nb); .... 4320 bck = unsorted_chunks (av); 4321 fwd = bck->fd; .... 4324 remainder->bk = bck; 4325 remainder->fd = fwd; 4326 bck->fd = remainder; 4327 fwd->bk = remainder; .... 4337 set_head (victim, nb | PREV_INUSE | 4338 (av != &main_arena ? NON_MAIN_ARENA : 0)); 4339 set_head (remainder, remainder_size | PREV_INUSE); .... 4343 void *p = chunk2mem (victim); .... 4345 return p;

  • إذا قوطع هذا المسار البرمجي بواسطة 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().


Practice

root@kitploit:~
I wanted it perfect, no wrinkles in it
    -- The Interrupters, "In the Mirror"

لتنفيذ هذا الهجوم ضد الطفل المميز لـ sshd، لنتخيل أولًا تخطيط الكومة التالي (حيث "XXX" هي قطع "حاجزة" تسمح لنا بصنع ثقوب في الكومة؛ مثل قطع صغيرة مسرَّبة من الذاكرة):

---|----------------------------------------------|---|------------|---

XXXlarge holeXXXsmall holeXXX
~8KB320B
  • قبل وقت قصير من استلام sshd لإشارة SIGALRM، نخصص عبر malloc() قطعة بحجم ~4KB تقسم الثقب الكبير ~8KB إلى قطعتين أصغر:

---|-----------------------|----------------------|---|------------|---

XXXlarge allocated chunkfree remainder chunkXXXsmall holeXXX
~4KB~4KB320B
  • لكن إذا قوطع هذا الاستدعاء إلى 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 الأقدم أننا لن نفوز أبدًا في حالة السباق هذه في معالج الإشارة إذا كانت نافذة السباق الكبيرة لدينا تحتوي على نافذة سباق صغيرة واحدة فقط. وبالتالي، نفّذنا الاستراتيجية التالية، بناءً على تخطيط الكومة التالي:

---|------------|---|------------|---|------------|---|------------|---

XXXlarge hole 1XXXsmall hole 1XXXlarge hole 2XXXsmall hole 2...
~8KB320B~8KB320B

آخر حزمة نرسلها إلى 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 لتنفيذ تسلسلات تعسفية من استدعاءات malloc() و free() (عند السطرين 1805 و573):

1754 cert_parse(struct sshbuf *b, struct sshkey *key, struct sshbuf *certbuf) 1755 { .... 1797 while (sshbuf_len(principals) > 0) { .... 1805 if ((ret = sshbuf_get_cstring(principals, &principal, .... 1820 key->cert->principals[key->cert->nprincipals++] = principal; 1821 }

562 cert_free(struct sshkey_cert *cert) 563 { ... 572 for (i = 0; i < cert->nprincipals; i++) 573 free(cert->principals[i]);

  • لم نتمكن من العثور على تسرب ذاكرة لكتل «الحاجز» الصغيرة لدينا؛ بدلاً من ذلك، نستخدم كتل tcache (التي لا يتم تحريرها حقًا أبدًا، لأن بت inuse الخاص بها لا يُمسح أبدًا) ككتل «حاجز» مؤقتة.

لتحقيق هذا التخطيط للكومة بشكل موثوق، نرسل خمس حزم مختلفة من المفاتيح العامة إلى 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).


التوقيت

root@kitploit:~
نفد وقتنا
    -- 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).


88 userauth_pubkey(struct ssh *ssh, const char method) 89 { ... 138 if (pktype == KEY_UNSPEC) { 139 / this is perfectly legal */ 140 verbose_f("unsupported public key algorithm: %s", pkalg); 141 goto done; 142 } 143 if ((r = sshkey_from_blob(pkblob, blen, &key)) != 0) { 144 error_fr(r, "parse key"); 145 goto done; 146 } ... 151 if (key->type != pktype) { 152 error_f("type mismatch for decoded key " 153 "(received %d, expected %d)", key->type, pktype); 154 goto done; 155 }

مع هذا التغيير في الاستراتيجية، يستغرق الفوز في حالة السباق حوالي 10,000 محاولة في المتوسط؛ أي مع قبول 100 اتصال (MaxStartups) كل 120 ثانية (LoginGraceTime)، يستغرق الفوز في حالة السباق حوالي 3-4 ساعات في المتوسط، وحوالي 6-8 ساعات للحصول على صدفة جذر عن بُعد (بسبب ASLR).

======================================================================== نحو استغلال amd64

root@kitploit:~
ما خطتك للغد؟
    -- 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

======================================================================== التصحيحات والتخفيف

root@kitploit:~
لقد جاءت العاصفة ومضت
    -- The Interrupters, "Good Things"

في 6 يونيو 2024، تم إصلاح حالة السباق في معالج الإشارة هذه بواسطة الالتزام 81c1099 («إضافة تسهيل إلى sshd(8) لمعاقبة سلوكيات العملاء الإشكالية المحددة»)، والذي نقل الكود غير الآمن للإشارات غير المتزامنة من معالج SIGALRM في sshd إلى عملية الاستماع في sshd، حيث يمكن التعامل معه بشكل متزامن:

https://github.com/openssh/openssh-portable/commit/81c1099d22b81ebfd20a334ce986c4f753b0db29

نظرًا لأن هذا الإصلاح جزء من التزام كبير (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;

root@kitploit:~
    va_start(args, fmt);
    sshlogv(file, func, line, showfunc, SYSLOG_LEVEL_FATAL,
        suffix, fmt, args);
    va_end(args);

#endif _exit(1); }

أخيرًا، إذا تعذر تحديث sshd أو إعادة ترجمته، يمكن إصلاح حالة السباق في معالج الإشارة هذه ببساطة عن طريق ضبط LoginGraceTime على 0 في ملف الإعداد. وهذا يجعل sshd عرضة لرفض الخدمة (استنفاد جميع اتصالات MaxStartups)، لكنه يجعله آمنًا من تنفيذ الكود عن بُعد المعروض في هذه النشرة الأمنية.

======================================================================== شكر وتقدير

نشكر مطوري OpenSSH على عملهم المتميز وتعاونهم الوثيق في هذا الإصدار. ونشكر أيضًا القائمة البريدية distros@openwall. وأخيرًا، نهدي هذه النشرة الأمنية إلى Sophia d'Antoine.

======================================================================== الجدول الزمني

2024-05-19: تواصلنا مع مطوري OpenSSH. تبع ذلك تكرارات متتالية من التصحيحات ومراجعات التصحيحات.

2024-06-20: تواصلنا مع القائمة البريدية distros@openwall.

2024-07-01: تاريخ الإصدار المنسق.

تنزيل الأداة