1 คะแนน โดย GN⁺ 2024-08-10 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Google ค้นพบ CVE-2023-2163 ใน eBPF verifier ด้วย Buzzer และยืนยันว่าช่องโหว่นี้สามารถถูกใช้เพื่อยกระดับสิทธิ์แบบ local และหลบหนีออกจากคอนเทนเนอร์ได้
  • eBPF ขยายความสามารถของ kernel ได้ขณะรันไทม์ แต่เนื่องจากรัน bytecode ใด ๆ ได้ด้วยสิทธิ์ระดับสูง การ ตรวจสอบโดย verifier ก่อนโหลดจึงเป็นแกนหลักด้านความปลอดภัย
  • ปัญหาเกิดจาก path pruning ของ verifier ตัดสินผิดว่าเส้นทางการทำงานที่จริง ๆ แล้วแตกต่างกัน มีสถานะเทียบเท่ากับเส้นทางที่ปลอดภัยแล้ว
  • exploit อาศัยความแตกต่างที่ register ซึ่ง verifier มองว่าเป็น 0 แต่ตอนรันไทม์มีค่าอื่น เพื่อทำให้ eBPF stack pointer ปนเปื้อน และนำไปสู่การอ่าน/เขียนตามอำเภอใจและการ หลบเลี่ยง KASLR
  • patch ใช้วิธีทำเครื่องหมาย imprecise register ที่ส่งผลต่อ precise register ให้เป็น precise ด้วย และหลังจากนั้นไม่พบปัญหาเพิ่มเติมจากกลยุทธ์ fuzzing pointer arithmetic แบบเดียวกัน

eBPF verifier ขยายพื้นผิวการโจมตีของ kernel

  • eBPF เป็นเทคโนโลยีที่ช่วยให้ขยายความสามารถของ Linux kernel ได้ขณะรันไทม์ โดยไม่ต้องใช้ kernel module ที่ซับซ้อน
  • โปรแกรม eBPF เขียนด้วย bytecode แบบกำหนดเอง และจะผ่านการตรวจสอบความปลอดภัยก่อนรันเมื่อเกิดเหตุการณ์บางอย่าง
    • ตัวอย่างที่พบบ่อยคือโปรแกรม eBPF ที่รันเมื่อมีการเรียก syscall บางรายการ
  • โครงสร้างที่อนุญาตให้รันโค้ดใด ๆ ด้วยระดับสิทธิ์สูง ทำให้ พื้นผิวการโจมตี ของ kernel เพิ่มขึ้นอย่างมาก
  • ก่อนโหลด โปรแกรมต้องผ่าน verifier และ verifier จะตรวจสอบว่าสมมติฐานด้านความปลอดภัยของ eBPF เป็นไปตามเงื่อนไขหรือไม่
  • หากช่องโหว่ใน verifier ถูกใช้ประโยชน์ โดยทั่วไปจะนำไปสู่ การยกระดับสิทธิ์แบบ local หรือการหลบหนีออกจากคอนเทนเนอร์ในสภาพแวดล้อมคอนเทนเนอร์

Buzzer และการ fuzzing pointer arithmetic

  • Google สร้าง Buzzer เพื่อ audit โค้ด eBPF verifier แบบอัตโนมัติ
  • Buzzer เป็น fuzzer ที่สร้างโปรแกรม eBPF ที่ถูกต้องตามไวยากรณ์จำนวนมาก และสามารถกำหนดกลยุทธ์เพื่อกระตุ้น logical bug ได้
  • ในการค้นพบ CVE-2023-2163 มีการใช้ กลยุทธ์ pointer arithmetic
    • สร้าง header ที่ initialize register ด้วยค่าตามอำเภอใจ
    • สร้างลำดับคำสั่ง arithmetic และ jump แบบสุ่ม
    • เลือก register แบบสุ่มแล้วนำไปบวกกับ pointer ของ eBPF map element
    • บันทึกค่า magic ลงใน element นั้น
    • หากไม่พบค่าที่บันทึกไว้จาก user space อาจมีการเขียนนอกขอบเขตเกิดขึ้น
  • CVE-2023-2163 ที่ Buzzer พบอยู่ในตรรกะ path pruning ของ eBPF และนำไปสู่ exploit ที่ใช้ได้ทั้งกับการหลบหนีออกจากคอนเทนเนอร์และการยกระดับสิทธิ์แบบ local

path pruning และ bug ใน precise tracking

  • eBPF verifier จำลองเส้นทางการทำงานที่เป็นไปได้เพื่อตรวจสอบว่าโปรแกรมสามารถรันได้อย่างปลอดภัยหรือไม่
  • เมื่อไม่แน่ใจค่า register ใน conditional branch verifier จะพยายามตามการทำงานของทุกสถานะที่เป็นไปได้
  • ยิ่งมี conditional jump มาก จำนวนเส้นทางการทำงานจะเพิ่มขึ้นแบบทวีคูณ และส่งผลเสียต่อประสิทธิภาพการโหลดโปรแกรมเข้า kernel ด้วย
  • เพื่อลดปัญหานี้ นักพัฒนา eBPF จึงนำ path pruning มาใช้
    • หาก verifier รับประกันได้ว่าสถานะที่เทียบเท่ากับสถานะหนึ่งไปถึงคำสั่ง exit ได้อย่างปลอดภัยแล้ว ก็จะไม่สำรวจเส้นทางนั้นต่อ
  • เพื่อให้ pruning มีประสิทธิภาพยิ่งขึ้น จึงใช้แนวคิด precise tracking ร่วมด้วย
    • หาก register เกี่ยวข้องกับการคำนวณ pointer arithmetic หรือถูกส่งเป็นค่าคงที่ให้ helper function จะถูกทำเครื่องหมายเป็น precise
    • verifier ต้องสำรวจทุกสถานะที่เกี่ยวข้องกับ register นั้น
  • ใน CVE-2023-2163 นั้น r9 ซึ่งส่งผลต่อความแม่นยำของ r6 ไม่ถูกทำเครื่องหมายอย่างถูกต้อง
    • verifier สมมติว่า r9 ไม่ได้มีส่วนต่อ preciseness ของ r6
    • หลังจากตัดสินว่าเส้นทางก่อนหน้าสามารถไปถึง exit ได้อย่างปลอดภัยด้วย r6 แล้ว ก็ถือว่าสถานะที่เหลือเทียบเท่ากันและ pruning ทิ้ง
    • ตอนรันไทม์ เส้นทาง 1:2:4:6 ถูกเรียกใช้ และที่ 6 นั้น r6 ซึ่ง verifier มองว่าเป็น 0 จริง ๆ แล้วอาจมีค่าอื่นและถูกใช้ในการคำนวณ pointer arithmetic ได้

การอ่าน/เขียนตามอำเภอใจและการหลบเลี่ยง KASLR

  • โค้ด exploit เผยแพร่ไว้ใน Google security research repository
  • การพัฒนา exploit อาศัยงานของ @chompie และ @_manfp อย่างสำคัญ
  • ภาพรวมคือเริ่มจากได้ การอ่าน/เขียนตามอำเภอใจ ก่อน จากนั้นค้นหา credentials ของ process แล้ว patch uid และ pointer fs_struct เพื่อยกระดับสิทธิ์
  • ขั้นแรกทำให้ค่า register ที่ปนเปื้อนกลายเป็น 1
    • จากการวิเคราะห์ด้วยมือ ค่า r6 ตอนรันไทม์คือ 0x400 และ verifier มองค่านี้เป็น 0
    • ใช้คำสั่ง r6 >>= 10 เพื่อสร้างค่าที่ต้องการคือ 1
  • จากนั้นใช้ helper function bpf_skb_load_bytes_relative เพื่อทำให้ pointer ใน eBPF stack ปนเปื้อน
    • verifier ตัดสินว่า len เป็น 8 แต่ตอนรันไทม์จริง len กลายเป็น 9 เพราะค่า r6
    • ผลคือเขียน 9 ไบต์แทนที่จะเป็น 8 ไบต์ และทำให้ไบต์แรกของค่าที่ stack offset -32 ปนเปื้อน
  • เมื่อจัดการ stack pointer ที่ปนเปื้อนได้ ก็สามารถรั่วไหล eBPF map pointer ได้
    • อ่านค่า R2 และ R3 จาก user space แล้วตรวจสอบว่า R2 กลายเป็น 0xBACA หรือไม่
    • หากเงื่อนไขนี้ตรง R3 จะเป็นค่ารั่วไหลของ map pointer
    • การรั่วไหล eBPF map pointer นำไปสู่สถานะที่หลบเลี่ยง KASLR ได้แล้ว
  • ด้วยกลยุทธ์เดียวกัน หากเขียนทับพื้นที่ stack ต่อเนื่องทั้งหมดด้วย pointer ที่ต้องการ แทนที่จะเป็นเพียง byte เดียว ก็จะทำให้สามารถอ่าน/เขียนตามอำเภอใจได้
    • ในมุมมองของ verifier เป็นการจัดการ BPF stack แต่จริง ๆ แล้วสามารถอ่านและเขียนหน่วยความจำ kernel ได้

จาก map leak ไปจนถึง root shell

  • หลังจากเกิดการรั่วไหลของ map pointer แล้ว exploit แทบไม่ต่างจาก exploit ของ Chompie มากนัก และยืมโค้ดบางส่วนมาใช้
  • ขั้นตอนคร่าว ๆ มีดังนี้
    • ค้นหาสตริง init_pid_ns ซ้ำ ๆ ใน kstrtab
    • ค้นหา symbol ใน ksymtab ที่อ้างอิงสตริงนั้น เพื่อให้ได้ address ของ struct init_pid_ns
    • เดิน radix tree เพื่อหา entry ที่ field comm ตรงกับชื่อไฟล์ executable ของ exploit ที่กำลังรันอยู่
      • หากรันอยู่ในคอนเทนเนอร์ PID ไม่ใช่ heuristic ที่เชื่อถือได้ จึงไม่ใช้เฉพาะ PID เพียงอย่างเดียว
    • patch uid ให้เป็น 0 และ patch pointer fs_struct
    • หาก patch fs_struct ให้มีค่าเดียวกับ pointer ที่ PID 1 อ้างอิงอยู่ จะสามารถมองเห็นระบบไฟล์ของ host ได้เมื่อรันอยู่ในคอนเทนเนอร์
    • เรียก system("/bin/bash") เพื่อให้ได้ root shell
  • โค้ดที่เผยแพร่บน GitHub ทำงานเป็นการหลบหนีออกจากคอนเทนเนอร์ได้เฉพาะบน Linux บางเวอร์ชันเท่านั้น
  • บน Ubuntu และบาง distribution offset ของ data structure ที่ถูกเขียนทับแตกต่างกัน จึงทำงานได้เพียงการยกระดับสิทธิ์แบบ local
  • หากต้องการให้ทำงานได้บน Linux distribution ใด ๆ จำเป็นต้องปรับแต่งโค้ด

Patch และการตรวจสอบภายหลัง

  • การวิเคราะห์สาเหตุและ patch ของ CVE-2023-2163 ดูได้จาก kernel mailing list
  • การแก้ไขใช้วิธีทำเครื่องหมาย imprecise register ของ operation ที่ส่งผลต่อ precise register ให้เป็น precise
  • ยังไม่ชัดเจนว่าการแก้ไขนี้ส่งผลต่อประสิทธิภาพของ eBPF verifier หรือไม่
  • แม้จะรันกลยุทธ์ fuzzing pointer arithmetic แบบเดียวกันต่อไป แต่ก็ไม่พบปัญหาเพิ่มเติม
  • Buzzer ยังอยู่ระหว่างพัฒนาต่อเนื่อง และรับ contribution จากชุมชนโอเพนซอร์สผ่าน GitHub repo

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

 
GN⁺ 2024-08-10
ความคิดเห็นจาก Hacker News
  • บนแพลตฟอร์มที่ใช้ eBPF กันบ่อยที่สุด ตั้งแต่แรกแล้ว โค้ดที่ไม่มีสิทธิ์ไม่สามารถโหลดโปรแกรม eBPF ได้ ดังนั้นผลกระทบของบั๊กใน verifier จึงมักไม่รุนแรงมากนัก
    บั๊กแบบนี้สุดท้ายคือช่องโหว่จาก root → ring0 ซึ่งไม่ได้หมายความว่าไม่สำคัญ แต่สำหรับงานฝั่งเซิร์ฟเวอร์โดยทั่วไปถือเป็นการแลกเปลี่ยนที่ยอมรับได้
    โดยเฉพาะอย่างยิ่ง ประวัติการยกระดับสิทธิ์ในเครื่องของเคอร์เนลผ่าน eBPF ถือว่าค่อนข้างดีเมื่อเทียบกับเคอร์เนลทั้งหมด และคุณค่าหลักของ verifier ในสภาพแวดล้อม eBPF ปัจจุบันคือช่วยให้โปรแกรม eBPF ที่เขียนผิดพลาดทำเคอร์เนลล่มโดยไม่ตั้งใจได้ยากขึ้น
    เรื่องนี้แทบใช้ไม่ได้เลยกับ โมดูลเคอร์เนลที่โหลดได้ ทั่วไป

    • PoC เขียน eBPF map ด้วยพอยน์เตอร์ที่อยู่นอกขอบเขต แต่ถ้าเป็นแค่ ข้อผิดพลาดในการติดตามช่วงค่าของสเกลาร์ ก็ดูเหมือนจะสามารถใช้โจมตีผ่านโปรแกรม BPF แบบไม่ขยายที่โหลดได้ผ่าน seccomp ได้เช่นกัน
      ในกรณีนี้ บนแพลตฟอร์มส่วนใหญ่ไม่จำเป็นต้องมีสิทธิ์
      และถ้ามี user namespace แบบไม่ต้องมีสิทธิ์ ก็สามารถทำให้ตัวเองเป็น “root” ได้ ดังนั้น root → ring0 ก็กลายเป็นปัญหาที่มีข้อจำกัดน้อยลง
      นี่เป็นรูปแบบที่เห็นซ้ำ ๆ ใน PoC ของบั๊ก eBPF ที่ออกมาหลังจากดิสโทรต่าง ๆ เคยเปิดใช้สิ่งนี้แล้วส่วนใหญ่ก็ปิดกลับไป
    • อย่าลืมด้วยว่าสามารถให้ CAP_BPF กับคอนเทนเนอร์ได้
      เมื่อเครื่องมืออย่าง Cilium ใหญ่ขึ้น เส้นทางโจมตีที่เจาะเข้าไปยังสภาพแวดล้อมคอนเทนเนอร์ที่มี cap_bpf ก็ยิ่งเป็นจริงมากขึ้น
    • การแก้บั๊กของ verifier สำคัญ เพราะเป็นเงื่อนไขเบื้องต้นในการทำให้ การใช้งาน eBPF แบบไม่ต้องมีสิทธิ์ ปลอดภัย
  • “Uno no es ninguno” หากแปลตามตัวอักษรคือ “หนึ่งไม่ใช่ความไม่มี” หรือใกล้เคียงกับ “One is not none”

    • ในภาษาสเปน การปฏิเสธซ้อน มักไม่ได้เป็นการปฏิเสธซ้อนจริง ๆ
      ตัวอย่างเช่น “ที่นี่ไม่มีอะไรเลย” จะพูดว่า “no hay nada aquí” ซึ่งถ้าแปลทีละคำจะดูเหมือน “ไม่ใช่ว่าไม่มีอะไรที่นี่”
      Royal Spanish Academy ก็อธิบายไว้เช่นนี้:

      https://www.rae.es/espanol-al-dia/doble-negacion-no-vino-nad...

      สิ่งที่เรียกว่า “การปฏิเสธซ้อน” เกิดจาก การสอดคล้องของคำปฏิเสธ ซึ่งในภาษาสเปนและภาษาโรมานซ์อื่น ๆ ต้องเกิดขึ้นในบางสถานการณ์ ทำให้คำวิเศษณ์ no และองค์ประกอบอื่นที่มีความหมายปฏิเสธปรากฏร่วมกันในประโยคเดียว
      การมี “คำปฏิเสธ” สองตัวพร้อมกันไม่ได้ทำให้ความหมายเชิงปฏิเสธของประโยคหักล้างกัน

  • ตอนที่เคยลองใช้ eBPF เมื่อก่อน มันยังมีพลังในการสื่อความไม่พอสำหรับงานที่ต้องการทำ
    เลยสงสัยจริง ๆ ว่าการเพิ่ม ความซับซ้อนในพื้นที่เคอร์เนล เพื่อแลกกับความยืดหยุ่นที่จำกัดนั้นคุ้มค่าหรือไม่
    ถ้าใช้เพื่อกรองแพ็กเก็ตก็เข้าใจได้ แต่การเอาไปใช้กับงานอื่นอย่าง sandboxing ดูไม่ค่อยน่าเชื่อ

    • สำหรับงานแบบนี้ก็มีเทคโนโลยีอื่นอย่าง DTrace ด้วย
      ตัวเลือกของเคอร์เนลไม่ใช่ eBPF หรือไม่มีอะไรเลย แต่เป็น eBPF หรือสิ่งอื่นที่คล้ายกัน
      คุณอาจไม่ได้ใช้มันมากนัก แต่ก็มีคนที่ใช้มันตลอดทั้งวัน
      เหมือนเคยได้ยินว่าวิศวกร FAANG รันโปรแกรมแบบนี้หลายสิบ หรืออาจถึงหลายร้อยตัว บนเซิร์ฟเวอร์ทุกเครื่องตลอดเวลา และยังไม่นับการใช้งานครั้งเดียวด้วย
      FAANG ยังจ้างนักพัฒนาเคอร์เนลเฉพาะทางด้วย ดังนั้นก็เท่ากับพวกเขาเป็นคนให้ทุนกับความซับซ้อนที่ตัวเองใช้นี้
      ผมเองก็เคยแก้ปัญหาด้วย eBPF
      เป็นปัญหาที่ถ้าไม่มี eBPF แทบจะแก้ไม่ได้สำหรับคนที่ไม่ใช่ผู้เชี่ยวชาญเคอร์เนล และแม้จะไม่จำเป็นบ่อย แต่เมื่อจำเป็นก็ไม่มีอะไรมาทดแทนได้
      ในบางกรณี แม้แต่ผู้เชี่ยวชาญเคอร์เนลก็ต้องเลือกระหว่างใช้ eBPF หรือ ดูแลแพตช์เคอร์เนลแบบกำหนดเอง ไปตลอด
    • ไดรเวอร์โหมดเคอร์เนลที่โหลดได้ แบบดั้งเดิมจะดีกว่าแพตช์หรือ eBPF ไหม?
      รู้ว่ามันไม่ปลอดภัย แต่คนที่ใช้มันก็รู้ว่าพลังอันยิ่งใหญ่มาพร้อมกับความรับผิดชอบ
  • “Uno no es ninguno” ดูเหมือนควรแปลว่า “One is not none”

    https://bughunters.google.com/blog/6303226026131456/a-deep-d...

    • แต่กลับถูกแปลว่า “One is none”
      เป็น การปฏิเสธซ้อน ที่ขึ้นชื่อว่าสร้างความลำบากให้ผู้พูดภาษาต่างประเทศ รวมถึงผมด้วย

      https://spanish.stackexchange.com/questions/26777/how-does-d...

    • ถ้าแปลตรงตัวก็ใช่ แต่ในภาษาสเปน การปฏิเสธซ้อน กลับมักใช้เป็นการปฏิเสธธรรมดาอย่างน่าประหลาด

    • อาจจะแปลประมาณว่า “one ain't nothin'” จะตรงกว่า

  • บ้านเรามีสำนวนว่า “เม่นในกางเกง”
    ต่อให้มีประโยชน์แค่ไหน สิ่งนี้ก็ดูไม่ได้ถูกเขียนขึ้นอย่าง ปลอดภัยและรอบคอบ นัก

    • เมื่อมีประสบการณ์มากขึ้น ก็จะรู้ว่าต่อให้ทำอย่างปลอดภัยและรอบคอบแล้ว ความผิดพลาดก็ยังเกิดขึ้นได้