1 คะแนน โดย GN⁺ 2024-07-02 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เป็น race condition ใน signal handler ของ OpenSSH เซิร์ฟเวอร์ sshd ซึ่งทำให้ไคลเอนต์ที่ยังไม่ยืนยันตัวตนสามารถก่อให้เกิดการรันโค้ดจากระยะไกลบนเซิร์ฟเวอร์ที่ใช้ค่าตั้งต้นได้ ภายในเวลาจำกัดก่อนยืนยันตัวตน LoginGraceTime
  • ช่องโหว่นี้เป็น regression ของ CVE-2006-5051 และกลับมาอีกครั้งในช่วงตั้งแต่ 8.5p1 ขึ้นไปแต่ต่ำกว่า 9.8p1 หลังคอมมิต OpenSSH 8.5p1 เมื่อเดือนตุลาคม 2020 ได้ลบมาตรการป้องกันออกจาก sigdie()
  • บน Linux ที่ใช้ glibc, syslog() จะเรียก ฟังก์ชันที่ไม่ปลอดภัยต่อ async-signal เช่น malloc() และ free() จึงอาจนำไปสู่ root RCE แบบไม่ต้องยืนยันตัวตนได้ใน privileged code ของ sshd ที่ไม่ได้อยู่ใน sandbox
  • การทดลองทำบนเครื่องเสมือน i386 และเครือข่ายที่ค่อนข้างเสถียรซึ่งมี packet jitter ราว 10ms โดยบน Debian 12.5.0 OpenSSH 9.2p1 ต้องลองเฉลี่ยราว 10,000 ครั้ง และภายใต้เงื่อนไข MaxStartups=100, LoginGraceTime=120 ใช้เวลาประมาณ 6–8 ชั่วโมงกว่าจะได้ root shell
  • OpenSSH แก้ไขแล้วด้วยคอมมิต 81c1099 เมื่อ 6 มิถุนายน 2024 และหากอัปเดตหรือคอมไพล์ใหม่ได้ยาก การตั้ง LoginGraceTime 0 จะป้องกัน RCE ได้ แต่ยังคงมีความเสี่ยง DoS จากการทำให้การเชื่อมต่อ MaxStartups หมดลง

จุดที่ทำให้เกิดช่องโหว่

  • ปัญหาของ OpenSSH sshd เริ่มจาก SIGALRM handler ที่ทำงานก่อนการยืนยันตัวตน
    • หากไคลเอนต์ไม่ยืนยันตัวตนภายใน LoginGraceTime ระบบจะเรียก SIGALRM handler แบบอะซิงโครนัส
    • handler นี้เรียกฟังก์ชันที่ไม่ใช่ async-signal-safe เช่น syslog()
    • ค่าเริ่มต้นคือ LoginGraceTime=120 วินาที และใน OpenSSH เวอร์ชันเก่าจะเป็น 600 วินาที
  • ช่องโหว่นี้เป็น regression ของ CVE-2006-5051
    • CVE-2006-5051 คือ race condition ใน signal handler ของ OpenSSH ก่อน 4.4 ที่ Mark Dowd รายงานในปี 2006
    • ในเดือนตุลาคม 2020 คอมมิต 752250c ของ OpenSSH 8.5p1 ได้ลบ #ifdef DO_LOG_SAFE_IN_SIGHAND ออกจาก sigdie() โดยไม่ตั้งใจ
  • ขอบเขตเวอร์ชันที่ได้รับผลกระทบแบ่งได้ชัดเจน
    • ต่ำกว่า OpenSSH 4.4p1: มีช่องโหว่หากยังไม่ได้ backport แพตช์ที่เกี่ยวข้องกับ CVE-2006-5051 หรือ CVE-2008-4109
    • OpenSSH 4.4p1 ขึ้นไปแต่ต่ำกว่า 8.5p1: sigdie() ถูกเปลี่ยนให้เรียก _exit(1) อย่างปลอดภัย จึงไม่เปราะบางต่อ race condition นี้
    • OpenSSH 8.5p1 ขึ้นไปแต่ต่ำกว่า 9.8p1: กลับมาเปราะบางอีกครั้งหลังลบมาตรการป้องกัน

สภาพแวดล้อมที่ได้รับผลกระทบและข้อยกเว้น

  • เป้าหมายของการโจมตีจากระยะไกลคือ Linux ที่ใช้ glibc
    • syslog() ของ glibc เรียกฟังก์ชันที่ไม่ปลอดภัยต่อ async-signal ภายใน เช่น malloc() และ free()
    • โค้ดที่มีช่องโหว่อยู่ใน privileged code ของ sshd และรันโดยไม่อยู่ใน sandbox พร้อมสิทธิ์เต็ม
    • ส่งผลให้สามารถรันโค้ดระดับ root จากระยะไกลได้โดยไม่ต้องยืนยันตัวตน
  • libc หรือระบบปฏิบัติการอื่นไม่อยู่ในขอบเขตการตรวจสอบนี้
  • OpenBSD ไม่ได้รับผลกระทบ
    • SIGALRM handler ของ OpenBSD เรียก syslog_r() แทน syslog()
    • syslog_r() เป็นเวอร์ชันที่ปลอดภัยต่อ async-signal มากกว่าซึ่ง OpenBSD สร้างขึ้นในปี 2001

สมมติฐานของงานวิจัยการโจมตีจากระยะไกล

  • การใช้ race condition นี้จากระยะไกลต้องแก้ปัญหา 3 อย่าง
    • ต้องมี code path ที่ทำให้ sshd อยู่ในสถานะไม่สอดคล้องกันเมื่อ SIGALRM แทรกเข้ามาในจังหวะที่เหมาะสม
    • ต้องเข้าถึง code path นั้นและเพิ่มโอกาสให้ถูกขัดจังหวะในเวลาที่ถูกต้อง
    • ต้องสามารถจับจังหวะดังกล่าวได้แม้อยู่บนสภาพแวดล้อมเครือข่ายระยะไกล
  • งานวิจัยเริ่มจากสภาพแวดล้อม i386 ของ OpenSSH รุ่นเก่าก่อน แล้วค่อยขยายไปยังเวอร์ชันใหม่ แทนที่จะชนกับกลไกป้องกันสมัยใหม่ทันที
  • เงื่อนไขการทดลองมีข้อจำกัดชัดเจน
    • ทดสอบกับ เครื่องเสมือน เท่านั้น ไม่ใช่ bare-metal server
    • เครือข่ายเป็นลิงก์ที่ค่อนข้างเสถียร มี packet jitter ประมาณ 10ms
    • หลายส่วนของ exploit ยังสามารถปรับปรุงได้อีก
    • เริ่มงาน exploit สำหรับ amd64 แล้ว แต่ยากกว่ามากเพราะ ASLR แข็งแรงกว่า

การทดลองกับ OpenSSH รุ่นเก่า

  • Debian 3.0r6, OpenSSH 3.4p1

    • เป้าหมายคือ SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 ซึ่งเป็นสภาพแวดล้อมของ Debian 3.0r6 ในปี 2005
    • Debian รุ่นนี้เป็นรุ่นแรกที่เปิดใช้ privilege separation เป็นค่าตั้งต้น และมีการใช้แพตช์ช่องโหว่หลักในยุคนั้นแล้ว
    • การโจมตีอาศัยการขัดจังหวะ free() และ สถานะ heap ที่ไม่สอดคล้องกัน
      • ขัดจังหวะการเรียก free() ในโค้ด parsing ของ public key ด้วย SIGALRM
      • จากนั้นอาศัยสถานะ heap ที่ไม่สอดคล้องกันใน free() อีกจุดหนึ่งภายใน packet_close()
    • glibc 2.2.5 ยังไม่มีการเสริมความแข็งแกร่งต่อเทคนิค unlink() ของ Solar Designer
    • การโจมตีเขียนทับ __free_hook เพื่อเปลี่ยนทิศทางการทำงานไปยังตำแหน่ง shellcode บน heap
    • Debian รุ่นนี้ไม่มีทั้ง ASLR และ NX
    • หลังปรับปรุงการจับจังหวะแล้ว ต้องลองเฉลี่ยราว 10,000 ครั้ง
    • ภายใต้ MaxStartups=10, LoginGraceTime=600 ใช้เวลาเฉลี่ยประมาณ 1 สัปดาห์กว่าจะได้ remote root shell
  • Ubuntu 6.06.1, OpenSSH 4.2p1

    • เป้าหมายคือ SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3 ซึ่งเป็นสภาพแวดล้อมของ Ubuntu 6.06.1 ในปี 2006
    • เป็น Ubuntu เวอร์ชันสุดท้ายที่ยังคงเปราะบางต่อ CVE-2006-5051
    • glibc 2.3.6 จะจับ mandatory lock เมื่อเข้าใช้ฟังก์ชันตระกูล malloc ทำให้วิธีขัดจังหวะ malloc แล้วไปโจมตีด้วยการเรียก malloc อีกครั้งจบลงที่ deadlock
    • เส้นทางโจมตีสุดท้ายใช้ PAM
      • pam_start() ตั้งค่า pointer ส่วนกลาง sshpam_handle ของ sshd
      • หาก _pam_add_handler() ถูกขัดจังหวะ อาจเหลือฟิลด์ next ที่ยังไม่ถูกกำหนดค่า
      • เมื่อ pam_end() ถูกเรียกจาก SIGALRM handler ก็อาจส่ง arbitrary pointer ให้ free() ได้
    • เทคนิค unlink() แบบเก่าของ glibc ถูกป้องกันแล้ว จึงใช้ House of Mind fastbin เวอร์ชันจาก Malloc Maleficarum
    • ชี้ fake arena ไปที่ .got.plt ของ sshd และเขียนทับ entry ของ _exit() ให้เป็นตำแหน่ง heap shellcode
    • heap ของ Ubuntu รุ่นนี้รันโค้ดได้ตามค่าตั้งต้น
    • ต้องลองเฉลี่ยราว 10,000 ครั้ง
    • ภายใต้ MaxStartups=10, LoginGraceTime=120 ใช้เวลาเฉลี่ยประมาณ 1–2 วันกว่าจะได้ remote root shell
    • หากโชคร้าย ผู้โจมตีอาจทำให้การเชื่อมต่อ MaxStartups ทั้ง 10 เส้น deadlock ก่อนจะได้ root shell

การทดลองบน Debian 12.5.0, OpenSSH 9.2p1

  • syslog() และเส้นทาง malloc ของ glibc

    • เป้าหมายคือ SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2 ซึ่งเป็นสภาพแวดล้อม current stable ของ Debian 12.5.0 ในปี 2024
    • สภาพแวดล้อมนี้เปราะบางต่อ regression ของ CVE-2006-5051
    • SIGALRM handler ของเวอร์ชันนี้ไม่เรียก packet_close() หรือ pam_end() แต่ไปตามเส้นทาง syslog()
      • grace_alarm_handler() เรียก sigdie()
      • sigdie() เรียก syslog() ผ่าน sshlogv() และ do_log()
    • syslog() ของ Debian glibc 2.36 จะเรียก malloc ในการเรียกครั้งแรก
      • มีการเรียก __tzfile_read() ผ่านเส้นทาง __localtime64_r()
      • fopen() เรียก malloc(304) สำหรับโครงสร้าง FILE
      • และเรียก malloc(4096) สำหรับ internal read buffer
    • glibc malloc ตั้งแต่หลังเดือนตุลาคม 2017 จะไม่จับ mandatory lock ในกรณี single-threaded
    • จึงเกิดโอกาสใช้ประโยชน์จากการแข่งขันของ malloc ได้ในโปรเซสแบบ single-threaded อย่าง sshd
  • เงื่อนไข ASLR และข้อจำกัดของ i386

    • สภาพแวดล้อม Debian 12.5.0 i386 มี จุดอ่อนของ ASLR
    • PIE, heap, ไลบรารีส่วนใหญ่ และ stack ของ sshd โดยทั่วไปจะถูกสุ่มตำแหน่ง
    • แต่ glibc เองจะถูกแมปที่ 0xb7200000 หรือ 0xb7400000 เสมอ
    • จึงเดาตำแหน่ง glibc ได้ด้วยโอกาสครึ่งหนึ่ง
    • exploit นี้สมมติว่า glibc ถูกแมปที่ 0xb7400000
    • เพราะที่อยู่นี้พบได้บ่อยกว่า 0xb7200000 เล็กน้อย
  • สถานะ heap ที่ไม่สอดคล้องกันและการใช้ประโยชน์จากโครงสร้าง FILE

    • เส้นทาง malloc ที่เลือกคือเส้นทาง split ซึ่งแบ่ง free chunk ขนาดใหญ่เป็นสองส่วน
    • จะได้ chunk ที่ส่งคืนและ remainder chunk
    • หาก SIGALRM แทรกหลังจาก remainder chunk ถูกเชื่อมเข้า unsorted list แต่ก่อนกำหนดค่า size field จะเกิดความไม่สอดคล้องของ heap
    • ผู้โจมตีควบคุม size field ของ remainder chunk ได้ผ่านข้อมูลตกค้างจาก heap allocation ก่อนหน้า
    • ทำให้ remainder chunk ดูใหญ่กว่าความจริงและซ้อนทับกับ heap chunk อื่นได้
    • เมื่อ malloc ใน SIGALRM handler ใช้ chunk นี้ก็จะทำให้หน่วยความจำ heap เสียหาย
    • เป้าหมายคือ โครงสร้าง FILE ที่ fopen() ภายใน __tzfile_read() จัดสรรไว้บน heap
      • ด้วย heap corruption ที่จำกัด สามารถเขียนทับ _vtable_offset ได้ 1 ไบต์
      • ทำให้ฟังก์ชัน libio ของ glibc ไปอ่าน vtable pointer จาก offset อื่นแทนตำแหน่งปกติ
      • ผู้โจมตีควบคุม fake vtable pointer และ _codecvt pointer ที่ตำแหน่งนั้นได้ผ่านข้อมูลตกค้างจาก heap allocation ก่อนหน้า
    • บน i386 glibc เทคนิคนี้ทำให้เรียก arbitrary function pointer __fct ระหว่าง __fread_unlocked() ได้
    • ดูเหมือน amd64 glibc จะไม่ใช้ _vtable_offset
  • heap layout และ race window 27 จุด

    • race window ขนาดเล็กเพียงจุดเดียวไม่พอจะชนะ race condition นี้ จึงสร้าง คู่ large hole และ small hole 27 คู่
    • ถ้าเป็น 28 คู่จะเกิน PACKET_MAX_SIZE 256KB
    • แพ็กเก็ตสุดท้ายจะบังคับลำดับ malloc(~4KB), malloc(304), malloc(~4KB), malloc(304)
    • หากการ split large hole แต่ละจุดถูกขัดจังหวะในช่วงที่เหมาะสม fopen() ใน SIGALRM handler จะนำ small hole ที่สอดคล้องกันไปจัดสรรเป็นโครงสร้าง FILE
    • ใช้โค้ด parsing ของ public key ใน sshd เพื่อสร้าง heap layout
      • ผ่านเส้นทาง cert_parse() และ cert_free() เพื่อทำลำดับ malloc() และ free() ที่ใกล้เคียงกับอิสระ
      • เนื่องจากหา memory leak ไม่พบ จึงใช้ tcache chunk เป็น barrier chunk ชั่วคราว
    • ส่ง public key packet 5 ประเภท
      • a/ ทำให้ heap allocation ที่ควบคุมไม่ได้เข้าไปอยู่ใน tcache chunk
      • b/ สร้าง large/small hole 27 คู่และ barrier chunk
      • c/ เขียน fake header, fake footer, fake vtable, และ _codecvt pointer ไว้ล่วงหน้า
      • d/ จัดสรรและคืนค่าสตริงขนาดใหญ่เกือบ 256KB เพื่อย้าย hole จาก unsorted list ไปยัง malloc bin ต่าง ๆ
      • e/ บังคับลำดับ malloc สุดท้ายเพื่อเปิด small race window ทั้ง 27 จุด
  • กลยุทธ์จับจังหวะสำหรับเวอร์ชันใหม่

    • กลยุทธ์ timing แบบอิง feedback ที่ใช้กับเวอร์ชันเก่า ใช้ไม่ได้กับ OpenSSH 9.2p1
    • การ parse public key ชุดที่ห้าใช้เวลาประมาณ 10ms ทำให้ large race window กว้างเกินไป
    • user_specific_delay() ที่เพิ่มเข้ามาใน OpenSSH 7.8p1 ทำให้การตอบสนองล่าช้าได้สูงสุดราว 9ms จึงทำลาย feedback แบบเดิม
    • กลยุทธ์ใหม่เปรียบเทียบเวลาตอบกลับของ intentional error สองชนิด
      • ส่ง packet ที่ทำให้เกิดข้อผิดพลาดก่อนเริ่ม parse public key
      • ส่ง packet ที่ทำให้เกิดข้อผิดพลาดหลัง parse public key เสร็จ
      • ใช้ส่วนต่างของเวลาตอบกลับทั้งสองเพื่อวัดเวลา parse public key ครั้งสุดท้าย
    • ด้วยวิธีนี้สามารถชนะ race condition ได้ที่ค่าเฉลี่ยราว 10,000 ครั้ง
    • ภายใต้ MaxStartups=100, LoginGraceTime=120 ใช้เวลาเฉลี่ย 3–4 ชั่วโมงเพื่อชนะ race condition
    • และเนื่องจาก ASLR ต้องใช้เวลาเฉลี่ย 6–8 ชั่วโมงกว่าจะได้ remote root shell

ความคืบหน้าของ exploit บน amd64

  • เลือก Rocky Linux 9 เป็นเป้าหมายสำหรับ amd64
    • อิมเมจเป้าหมายคือ Rocky-9.4-x86_64-minimal.iso
    • OpenSSH 8.7p1 เปราะบางต่อ race condition ใน signal handler นี้
    • glibc ถูกแมปที่เป็นพหุคูณของ 2MB เพราะจุดอ่อนของ ASLR ทำให้ partial pointer overwrite มีพลังมากขึ้น
  • syslog() ของ glibc 2.34 บน Rocky Linux 9 เรียก __open_memstream() ภายใน
    • มีการ malloc() โครงสร้าง FILE บน heap
    • และยังเรียก calloc(), realloc(), free() เปิดช่องเพิ่มเติม
  • จาก heap corruption primitive, โครงสร้าง FILE สองตัวที่ถูกจัดสรรบน heap และบิตคงที่ 21 บิตของที่อยู่ glibc จึงเชื่อว่าสามารถใช้ประโยชน์บน amd64 ได้เช่นกัน
    • คาดว่าใช้เวลานานกว่า 6–8 ชั่วโมงของ i386 แต่ยังน่าจะน้อยกว่า 1 สัปดาห์
  • ยังมีข้อสังเกตแยกสำหรับ Ubuntu 24.04
    • Ubuntu 24.04 ไม่สุ่ม ASLR ใหม่ให้ child ของ sshd ทุกครั้ง แต่สุ่มเพียงครั้งเดียวตอนบูต
    • ต้นเหตุสืบไปถึง systemd-socket-activation.patch ที่ปิด rexec_flag
    • โดยทั่วไปถือว่าเป็นทางเลือกที่ไม่ดี แต่สำหรับช่องโหว่นี้กลับขัดขวางการโจมตี เพราะ syslog() ใน SIGALRM handler ไม่ใช่การเรียก syslog() ครั้งแรก จึงไม่เรียกฟังก์ชัน malloc
    • แพตช์ที่เกี่ยวข้อง: https://git.launchpad.net/ubuntu/+source/…

แพตช์และมาตรการบรรเทา

  • OpenSSH แก้ race condition นี้แล้วด้วยคอมมิต 81c1099 เมื่อ 6 มิถุนายน 2024
    • 81c1099: เพิ่มความสามารถให้ sshd(8) ลงโทษ client behavior ที่มีปัญหา
    • ย้ายโค้ดที่ไม่ปลอดภัยต่อ async-signal ออกจาก SIGALRM handler ของ sshd ไปยัง listener process เพื่อจัดการแบบ synchronous
  • การแก้ไขนี้พึ่งพาคอมมิตใหญ่ 81c1099 และคอมมิต defense-in-depth ที่ใหญ่กว่าอีกตัวคือ 03e3de4 จึงอาจ backport ได้ยาก
  • หาก backport ได้ยาก สามารถลบหรือคอมเมนต์โค้ด async-signal-unsafe ใน sshsigdie() เพื่อให้เหลือเพียงการเรียก _exit(1)
  • หากอัปเดตหรือคอมไพล์ใหม่ไม่ได้ สามารถตั้งค่า LoginGraceTime เป็น 0 ในไฟล์คอนฟิกได้
    • การตั้งค่านี้ป้องกันการรันโค้ดจากระยะไกลตาม advisory นี้ได้
    • แต่จะยังเปราะบางต่อ DoS จากการทำให้การเชื่อมต่อ MaxStartups หมดลง

กำหนดการเปิดเผย

  • 2024-05-19: ติดต่อผู้พัฒนา OpenSSH และมีการวนแก้แพตช์กับรีวิวต่อจากนั้น
  • 2024-06-20: ติดต่อ distros@openwall
  • 2024-07-01: เปิดเผยตาม coordinated release date

1 ความคิดเห็น

 
GN⁺ 2024-07-02
ความคิดเห็นจาก Hacker News
  • น่าสนใจที่ดูเหมือนว่าแพตช์แก้ RCE จะถูก “ปะปน” เผยแพร่ต่อสาธารณะไปแล้วเกือบหนึ่งเดือนก่อน
    เมื่อเปิดใช้ PerSourcePenalties, sshd(8) จะเฝ้าดูสถานะการจบการทำงานของโปรเซสเซสชันลูกก่อนการยืนยันตัวตน และบันทึกเงื่อนไขอย่างการยืนยันตัวตนล้มเหลวซ้ำ ๆ หรือ sshd แครช เป็นโทษต่อที่อยู่ไคลเอนต์เป็นระยะเวลาหนึ่ง
    https://github.com/openssh/openssh-portable/commit/81c1099d2...
    ดูค่อนข้างฉลาด เพราะแทนที่จะเป็นแพตช์ที่ทำวิศวกรรมย้อนกลับได้และบอกอะไรบางอย่างแก่ผู้โจมตี มันดูเหมือนมีผลข้างเคียงในการเปลี่ยนโครงสร้างไบนารีเพื่อตัดช่องโหว่เฉพาะตัวหนึ่งออก และยังช่วยบรรเทา ตระกูล exploit ทั้งหมดนั้นด้วย

    • นั่นไม่ใช่แพตช์แก้ RCE; แพตช์แก้ RCE จริง ๆ อยู่ตรงนี้: https://news.ycombinator.com/item?id=40843865
      การเปลี่ยนแปลงข้างต้นเป็นฟีเจอร์ที่เคยประกาศไว้เพื่อจัดการกับการเชื่อมต่อขยะ และเพียงแค่ช่วยบรรเทาช่องโหว่นี้ด้วยการทำให้ชนะ race condition ได้ยากขึ้น
      การพูดคุยก่อนหน้านี้: https://news.ycombinator.com/item?id=40610621
    • สงสัยว่าแพตช์นี้ถูกนำไปรวมในดิสทริบิวชันต่าง ๆ หรือถูกดึงไปใช้แล้วหรือยัง
    • น่าสนใจที่คอมเมนต์นี้ผิดและถูกแก้ไขทันทีด้านล่างแล้ว แต่ก็ยังอยู่ บนสุดนาน 2 วัน
      สงสัยว่าผู้คนอ่านแค่คอมเมนต์แรกของเธรด กดโหวต แล้วจากไปพร้อมความเข้าใจผิดหรือเปล่า
  • มีข้อความตอนหนึ่งใน release notes ของ OpenSSH ที่น่าสนใจ
    “มีการสาธิตการใช้ประโยชน์สำเร็จบนระบบ Linux/glibc 32 บิตที่เปิด ASLR แล้ว ภายใต้เงื่อนไขในห้องแล็บ การโจมตีต้องรักษาการเชื่อมต่อแบบต่อเนื่องให้ถึงค่าสูงสุดที่เซิร์ฟเวอร์อนุญาตโดยเฉลี่ย 6–8 ชั่วโมง เชื่อว่าเป็นไปได้บนระบบ 64 บิตด้วย แต่ยังไม่ได้พิสูจน์ การโจมตีลักษณะนี้มีแนวโน้มว่าจะถูกปรับปรุงให้ดีขึ้นได้”
    https://www.openssh.com/releasenotes.html

  • เมื่อดู diff [1] ที่ทำให้บั๊กถูกใส่เข้ามา ตามการวิเคราะห์แล้ว ปัญหาคือ sigdie() เดิมถูกครอบด้วย #ifdef DO_LOG_SAFE_IN_SIGHAND แต่เมื่อรีแฟกเตอร์เป็น sshsigdie() ที่เรียก sshlogv() โดยตรง #ifdef กลับหายไป
    อะไรจะช่วยป้องกันได้? ควรมีคนดู pull request มากกว่านี้หรือเปล่า? น่าทึ่งที่ซอฟต์แวร์ซึ่งทั้งโลกพึ่งพาเพื่อการเชื่อมต่อที่ปลอดภัย ดูเหมือนว่าถูกดูแลโดยคนสองคน [2] ในทางปฏิบัติ
    [1] https://github.com/openssh/openssh-portable/commit/752250caa...
    [2] https://github.com/openssh/openssh-portable/graphs/contribut...

    • หลังเหตุการณ์แล้ว การพูดว่าควรทำอะไรเพื่อป้องกันนั้นเป็นเรื่องง่าย
      ในกรณีนี้ หากมีคอมเมนต์อธิบายว่าทำไมต้องมี #ifdef ก็น่าจะช่วยได้ เช่น “โค้ดตรงนี้ต้อง ปลอดภัยต่อสัญญาณแบบอะซิงโครนัส และสถานะของล็อกอาจไม่แน่นอน” อะไรทำนองนั้น
      แต่พูดตามตรง getrlimit ก็ไม่ได้อยู่ในรายการนี้เช่นกัน: https://man7.org/linux/man-pages/man7/signal-safety.7.html
      ถึงอย่างนั้น ถ้าโค้ดที่มีคอมเมนต์เกี่ยวกับความปลอดภัยต่อสัญญาณแบบอะซิงโครนัสถูกลบหรือแก้ไข ก็มีโอกาสที่จะสะดุดตาในการรีวิว โค้ดที่อ้างถึงมีเพียง SAFE_IN_SIGHAND เท่านั้นที่พอจะบอกเป็นนัยว่าโค้ดนี้ต้องปลอดภัยเมื่ออยู่ใน signal handler
    • OpenBSD รีแฟกเตอร์ระบบให้ใช้ฟังก์ชัน syslog ที่ reentrant และปลอดภัยต่อสัญญาณแบบอะซิงโครนัสแล้ว ดังนั้นผู้เขียนโค้ดนี้อาจแค่สันนิษฐานว่าการเปลี่ยนแปลงนี้ปลอดภัย
      พวกเขาอาจลืมหรือไม่รู้ว่า บนแพลตฟอร์มอื่นที่นักพัฒนา OpenBSD ssh ไม่ได้อ้างว่าสนับสนุนจริง ๆ นั้น ยังคงใช้ ฟังก์ชันที่ไม่ปลอดภัยต่อสัญญาณแบบอะซิงโครนัส อยู่
    • มันเป็นโอเพนซอร์ส ถ้ารู้สึกว่าทำได้ดีกว่า ก็ฟอร์กได้เต็มที่
      คุณไม่มีสิทธิ์โดยชอบธรรมที่จะได้รับอะไรจากนักพัฒนาโอเพนซอร์ส พวกเขาก็ทำผิดพลาดได้ และจะมี maintainer หรือ reviewer กี่คนก็เป็นสิ่งที่พวกเขาตัดสินใจเองได้
      https://gist.github.com/richhickey/1563cddea1002958f96e7ba95...
    • ประโยคที่ว่า “ซอฟต์แวร์ซึ่งทั้งโลกพึ่งพาเพื่อการเชื่อมต่อที่ปลอดภัย ถูกดูแลโดยคนสองคนในทางปฏิบัติ” ทำให้นึกถึง xkcd ตอนนี้อย่างเลี่ยงไม่ได้: https://xkcd.com/2347/
    • มีหลายวิธีที่อาจป้องกันได้
      1. ใช้ภาษาโปรแกรมที่ดีพอซึ่งไม่อนุญาตให้ตั้งค่าฟังก์ชันใด ๆ เป็น signal handler ได้ตามอำเภอใจ ใน libc ทั่วไปนั้นไม่ปลอดภัยอย่างชัดเจน และในภาษาที่ปลอดภัยอย่าง Rust หรือ Java ก็ทำแบบนั้นไม่ได้
      2. ใช้ libc ที่ออกแบบมาดี ซึ่งเมื่อเรียกฟังก์ชันที่ไม่ปลอดภัยต่อสัญญาณแบบอะซิงโครนัส จะทำให้เกิดอย่างมากแค่ deadlock ไม่ใช่ memory corruption เรื่องนี้ทำได้ค่อนข้างง่ายหากมองโค้ดที่รันภายในสัญญาณเหมือนเป็นเธรดแยกต่างหากในแง่ของการเข้าถึง thread-local storage และถ้าไม่มี global mutex หรือสามารถกลับไปรันโค้ดที่ถูกขัดจังหวะขณะถือ mutex ต่อได้ ก็ยังหลีกเลี่ยง deadlock ได้ด้วย
      3. ใช้ความคิดตอนเปลี่ยนโค้ดและตอนอนุมัติ อย่าทำแบบคนที่ลบ #ifdef โดยไม่มีเหตุผลรองรับเหมือนใน [1]
      4. ใช้ซอฟต์แวร์ที่เรียบง่ายและออกแบบมาดี ซึ่งเขียนโดยโปรแกรมเมอร์เก่ง ๆ แทน OpenSSH
  • บันทึกประจำรุ่นก็น่าอ่านเช่นกัน: https://www.openssh.com/releasenotes.html
    จริง ๆ แล้วนี่เป็นรูปแบบหนึ่งของ บั๊ก race condition ของสัญญาณ ที่น่าสนใจ ตามรายงานช่องโหว่ระบุว่า “OpenBSD ไม่ได้มีช่องโหว่เป็นพิเศษ เพราะ handler ของ SIGALRM เรียก syslog_r() ซึ่งเป็นเวอร์ชันของ syslog() ที่ปลอดภัยกว่าสำหรับสัญญาณแบบ asynchronous ที่ OpenBSD สร้างขึ้นในปี 2001”
    กล่าวคือ มาตรการลดผลกระทบด้านความปลอดภัยของสัญญาณทำให้นักพัฒนา OpenBSD ใส่โค้ดที่ไม่ใช่โค้ดเล็กน้อยไว้ใน signal handler และเมื่อโค้ดนั้นถูกพอร์ตไปยังระบบอื่นก็กลายเป็นไม่ปลอดภัย หากมีการ refactor เพื่อลดโค้ดใน signal handler ให้เหลือน้อยที่สุดตามภูมิปัญญาทั่วไปและธรรมเนียมของโค้ด Unix ก็น่าจะหลีกเลี่ยงบั๊กนี้ได้

    • Theo de Raadt ได้ให้ข้อสังเกตที่ค่อนข้างสมเหตุสมผลเกี่ยวกับการป้องกันบั๊กนี้และบั๊กที่คล้ายกัน: signal handler ใด ๆ ไม่ควรเรียกฟังก์ชันที่ไม่ใช่ system call ที่ปลอดภัยต่อสัญญาณ
      เพราะเมื่อเวลาผ่านไป เป็นเรื่องง่ายเกินไปที่จะมีการเรียกที่ไม่ปลอดภัยต่อสัญญาณแบบ asynchronous ปะปนเข้ามาที่ใดที่หนึ่งใน transitively-called functions และก็ไม่ได้ชัดเจนเสมอไปว่าเส้นทางนั้นเข้าถึงได้จากบริบทของสัญญาณ
    • มีความเป็นไปได้สูงว่าผู้ดูแลระบบรุ่นใหม่หรืออินเทิร์นจำนวนไม่น้อยที่ต้องแพตช์ช่องโหว่นี้ ยังไม่เกิดด้วยซ้ำตอนที่ OpenBSD นำวิธีแก้นี้มาใช้
  • หลังจากอัปเกรดอินสแตนซ์ OpenSSH ของผมแล้ว ผมเห็นว่ามันลิงก์กับ musl ไม่ใช่ glibc เลยลองดูว่า syslog(3) ของ musl มีการ allocate ด้วยหรือไม่ และจึงถูก exploit ได้ง่ายในลักษณะเดียวกันหรือเปล่า
    ดูเหมือนจะไม่ใช่: https://github.com/bminor/musl/blob/master/src/misc/syslog.c
    สิ่งที่อยู่ในนั้นทั้งหมดคือสแตกหรือ static variables ที่ใช้ล็อกเพื่อป้องกันการ re-enter ส่วนการเรียก {d,sn,vsn}printf() ใน musl ก็ไม่ได้ allocate แต่ใน glibc อาจทำได้ ผมพลาดอะไรไปไหม?

    • การยืนยันจาก Rich: https://fosstodon.org/@musl/112711796005712271
    • ถ้าการประเมินเรื่อง allocation ถูกต้อง กรณีเลวร้ายที่สุดน่าจะเป็นแค่เกิด deadlock เพราะล็อกไม่ใช่แบบ recursive
      ถึงอย่างนั้น หาก deadlock เกิดใน sigalrm ก็อาจขัดขวางการเคลียร์ connection และนำไปสู่ denial of service ได้
  • มีแพตช์สำหรับ FreeBSD ออกมาแล้ว
    ยังไม่ชัดเจนว่าได้รับผลกระทบหรือไม่ การ exploit ที่เป็นที่รู้จักทำได้เฉพาะใน glibc และ FreeBSD ไม่ได้ใช้ glibc แต่เลือกทางปลอดภัยไว้ก่อนดีกว่า
    https://www.freebsd.org/security/advisories/FreeBSD-SA-24:04...

  • ตามรายงาน หากไม่สามารถอัปเดตหรือคอมไพล์ sshd ใหม่ได้ การตั้งค่า LoginGraceTime เป็น 0 ในไฟล์คอนฟิกเพียงอย่างเดียวก็สามารถแก้ race condition ของ signal handler นี้ได้
    ในกรณีนี้ sshd จะเสี่ยงต่อ denial of service ที่ทำให้ connection ทั้งหมดตาม MaxStartups ถูกใช้จนหมด แต่จะปลอดภัยจาก remote code execution ที่ระบุในคำแนะนำนี้
    ดังนั้นการตั้ง LoginGraceTime 0 ใน sshd_config ดูเหมือนจะเป็นการบรรเทาผลกระทบได้

    • เดี๋ยวก่อน ใน https://www.man7.org/linux/man-pages/man5/sshd_config.5.html ระบุว่าค่า 0 หมายถึงไม่มี time limit
      ถ้าอย่างนั้นไม่ยิ่งแย่กว่าเดิมหรือ?
    • วิธีเลี่ยงที่สมจริงกว่าอาจเป็นการตั้ง grace time ให้ยาวพอ หรือในทางกลับกันปรับจำนวน connection สูงสุด เพื่อให้ความน่าจะเป็นที่การโจมตีจะสำเร็จไกลออกไปในอนาคตจนไม่คุ้มที่จะลอง
    • การ cold restart sshd ทุกชั่วโมงจะช่วยลดโอกาสถูก exploit หรือทำให้ยากขึ้นได้ไหม?
  • มีแพตช์สำหรับ Debian 12 ออกมาแล้ว และ Debian 11 ไม่ได้รับผลกระทบ
    https://security-tracker.debian.org/tracker/CVE-2024-6387

    • Focal (20.04) ดูเหมือนจะไม่ใช่เวอร์ชันที่ได้รับผลกระทบ ส่วน Jammy (22.04) ดูเหมือนจะได้รับผลกระทบ
    • เมื่อกี้ผมรัน apt update และ upgrade บนเซิร์ฟเวอร์ Debian 12 และแพ็กเกจที่ถูกอัปเกรดมีเพียงแพ็กเกจ OpenSSH เท่านั้น
    • ยืนยันว่า Pi OS bullseye ก็มี openssh ที่อัปเดตเข้ามาแล้ว
  • เป็นการค้นพบที่ยอดเยี่ยมจริง ๆ
    แม้ผมไม่ได้อยู่ในสถานะที่ทำงานด้านนี้โดยตรง แต่ในงานวิจัยความปลอดภัย มักรู้สึกได้ถึงบรรยากาศว่า ถ้าจะ “ชนะ” ไม่ใช่แค่หาปัญหาเดี่ยว ๆ แล้วแก้หรือรับรางวัล แต่ต้องหา ทั้งเชน ที่นำไปสู่การเข้าถึงจากระยะไกลได้
    ผมรู้สึกว่าการเจอช่องโหว่เดี่ยว ๆ อย่างเช่น memory corruption หนึ่งจุด หรือ sandbox escape หนึ่งจุด ก็น่าจะเพียงพอแล้วไม่ใช่หรือ ตอนนี้มีปัญหาเล็ก ๆ มากเกินไป จนบางทีถ้าจะให้ผู้คนมองอย่างจริงจังหรือจ่าย bug bounty ก็อาจต้องสาธิตการแฮ็กที่ต่อไปได้จนสุดทาง

    • มีนักวิจัยความปลอดภัยที่อยากเข้าวงการจำนวนมาก ซึ่งพบปัญหาที่ไม่สามารถนำไปใช้โจมตีได้ แล้วก็เรียกร้องหมายเลข CVE การยอมรับ หรือแม้กระทั่งรางวัล
      ตัวอย่างเช่น หากแอปหนึ่ง crash เมื่อรับอินพุตที่เชื่อถือผิดพลาด แต่โดยลักษณะของแอปนั้นไม่ได้ตั้งใจให้เปิดเผยต่อผู้ไม่หวังดี และในความเป็นจริงก็แทบไม่มีทางเกิดขึ้น คนส่วนใหญ่ก็จะมองว่าเป็นแค่บั๊ก ไม่ใช่บั๊กด้านความปลอดภัย แก้ได้ก็ดี แต่ไม่ใช่ระดับเดียวกัน และเรื่องแบบนี้ก็หาเจอได้ไม่ยากนัก
      ดังนั้นจึงจำเป็นต้องแยกแยะระหว่างบั๊กความปลอดภัย “ของจริง” อย่างกรณีนี้ กับบั๊กที่ไม่มีผลกระทบด้านความปลอดภัย และการ พิสูจน์ว่าสามารถนำปัญหาไปใช้โจมตีได้ จึงสำคัญมาก
      บั๊กที่ไม่มีผลกระทบด้านความปลอดภัยคงมีได้ไม่รู้จบ ดังนั้นข้อกำหนดให้พิสูจน์แบบนี้คงยังไม่หายไปในเร็ว ๆ นี้
    • ถ้ามองอีกมุม สมมติว่าผมสร้างไลบรารี serialization/deserialization ที่จะมีช่องโหว่หากป้อนข้อมูลที่ไม่น่าเชื่อถือเข้าไป
      นี่เป็นไปตามการออกแบบ และผู้ใช้สามารถ serialize และ deserialize อะไรก็ได้ รวมถึง lambda function ไลบรารีของผมตั้งใจให้ใช้ประมวลผลข้อมูลจากแหล่งที่เชื่อถือได้เท่านั้น
      เท่าที่ผมรู้ ไม่มีใครใช้ไลบรารีนี้เพื่อประมวลผลข้อมูลที่ไม่น่าเชื่อถือ มีไลบรารียอดนิยมตัวหนึ่งใช้ไลบรารีของผมในการอ่านไฟล์ตั้งค่า แต่พวกเขามองว่าไฟล์ตั้งค่าเป็นข้อมูลที่เชื่อถือได้ และการไปควบคุมว่าคนอื่นใช้ไลบรารีของผมอย่างไรก็ไม่ใช่งานของผม
      ในกรณีแบบนี้ ควรลงทะเบียน CVE ระดับความสำคัญสูงสุดโดยอ้างว่าโปรเจกต์ของผมมี ช่องโหว่ Remote Code Execution หรือไม่?
    • ผมเคยอยู่ในฝั่งผู้รายงานมาก่อน และ “ช่องโหว่ที่นำไปใช้โจมตีได้” กับ “จุดอ่อนด้านความปลอดภัยที่สักวันอาจนำไปสู่ช่องโหว่ที่โจมตีได้” นั้นต่างกันมาก
      รางวัลมักจ่ายให้กับประเภทแรกเสมอ ส่วนรายงานประเภทที่สอง หากไม่มี proof of concept หรือการพิสูจน์ว่านำไปใช้โจมตีได้ บางครั้งกลับอาจทำลายชื่อเสียงหรือสัญญาณความน่าเชื่อถือด้วยซ้ำ
      จุดอ่อนที่ยังไม่สามารถนำไปโจมตีได้จนกว่าจะมีเงื่อนไขบางอย่างครบถ้วน แทบจะมีอยู่เสมอ แม้แต่ในการแข่งขันอย่าง Pwn2Own ก็ยังมักเห็นการเชื่อมช่องโหว่หลายจุดเข้าด้วยกันจนยึดอุปกรณ์ได้ในท้ายที่สุด และหลายกรณีก็ยังคงไม่ถูกแพตช์อยู่นานหลายปี นักวิจัยบางคนก็เก็บจุดอ่อนเหล่านั้นไว้นานเพื่อเพิ่มผลกระทบให้มากที่สุด
      น่าเศร้า แต่นี่คือความจริง
    • ตามสุภาษิตวงการความปลอดภัยก็คือ: POC || GTFO
    • ผู้ซื้อจ่ายเงินให้ผลลัพธ์ ส่วน vendor ก็จ่ายเงินให้ลิงก์แต่ละจุดในเชนเหมือนกัน
  • บันทึกการเผยแพร่ของ OpenSSH: https://www.openssh.com/txt/release-9.8
    แพตช์ขั้นต่ำสำหรับผู้ที่ไม่สามารถหรือไม่ต้องการอัปเกรด: https://marc.info/?l=oss-security&m=171982317624594&w=2

    • “เชื่อกันว่าการโจมตีบนระบบ 64 บิตก็เป็นไปได้เช่นกัน แต่ ณ เวลานี้ยังไม่ได้รับการพิสูจน์”