พบและแก้ไขช่องโหว่ใน eBPF ของ Linux kernel ได้อย่างไร
(bughunters.google.com)- 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
- จากการวิเคราะห์ด้วยมือ ค่า r6 ตอนรันไทม์คือ
- จากนั้นใช้ helper function
bpf_skb_load_bytes_relativeเพื่อทำให้ pointer ใน eBPF stack ปนเปื้อน- verifier ตัดสินว่า
lenเป็น 8 แต่ตอนรันไทม์จริงlenกลายเป็น 9 เพราะค่า r6 - ผลคือเขียน 9 ไบต์แทนที่จะเป็น 8 ไบต์ และทำให้ไบต์แรกของค่าที่ stack offset
-32ปนเปื้อน
- verifier ตัดสินว่า
- เมื่อจัดการ stack pointer ที่ปนเปื้อนได้ ก็สามารถรั่วไหล eBPF map pointer ได้
- อ่านค่า R2 และ R3 จาก user space แล้วตรวจสอบว่า R2 กลายเป็น
0xBACAหรือไม่ - หากเงื่อนไขนี้ตรง R3 จะเป็นค่ารั่วไหลของ map pointer
- การรั่วไหล eBPF map pointer นำไปสู่สถานะที่หลบเลี่ยง KASLR ได้แล้ว
- อ่านค่า R2 และ R3 จาก user space แล้วตรวจสอบว่า R2 กลายเป็น
- ด้วยกลยุทธ์เดียวกัน หากเขียนทับพื้นที่ 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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
บนแพลตฟอร์มที่ใช้ eBPF กันบ่อยที่สุด ตั้งแต่แรกแล้ว โค้ดที่ไม่มีสิทธิ์ไม่สามารถโหลดโปรแกรม eBPF ได้ ดังนั้นผลกระทบของบั๊กใน verifier จึงมักไม่รุนแรงมากนัก
บั๊กแบบนี้สุดท้ายคือช่องโหว่จาก root → ring0 ซึ่งไม่ได้หมายความว่าไม่สำคัญ แต่สำหรับงานฝั่งเซิร์ฟเวอร์โดยทั่วไปถือเป็นการแลกเปลี่ยนที่ยอมรับได้
โดยเฉพาะอย่างยิ่ง ประวัติการยกระดับสิทธิ์ในเครื่องของเคอร์เนลผ่าน eBPF ถือว่าค่อนข้างดีเมื่อเทียบกับเคอร์เนลทั้งหมด และคุณค่าหลักของ verifier ในสภาพแวดล้อม eBPF ปัจจุบันคือช่วยให้โปรแกรม eBPF ที่เขียนผิดพลาดทำเคอร์เนลล่มโดยไม่ตั้งใจได้ยากขึ้น
เรื่องนี้แทบใช้ไม่ได้เลยกับ โมดูลเคอร์เนลที่โหลดได้ ทั่วไป
ในกรณีนี้ บนแพลตฟอร์มส่วนใหญ่ไม่จำเป็นต้องมีสิทธิ์
และถ้ามี user namespace แบบไม่ต้องมีสิทธิ์ ก็สามารถทำให้ตัวเองเป็น “root” ได้ ดังนั้น root → ring0 ก็กลายเป็นปัญหาที่มีข้อจำกัดน้อยลง
นี่เป็นรูปแบบที่เห็นซ้ำ ๆ ใน PoC ของบั๊ก eBPF ที่ออกมาหลังจากดิสโทรต่าง ๆ เคยเปิดใช้สิ่งนี้แล้วส่วนใหญ่ก็ปิดกลับไป
เมื่อเครื่องมืออย่าง Cilium ใหญ่ขึ้น เส้นทางโจมตีที่เจาะเข้าไปยังสภาพแวดล้อมคอนเทนเนอร์ที่มี cap_bpf ก็ยิ่งเป็นจริงมากขึ้น
“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 ดูไม่ค่อยน่าเชื่อ
ตัวเลือกของเคอร์เนลไม่ใช่ eBPF หรือไม่มีอะไรเลย แต่เป็น eBPF หรือสิ่งอื่นที่คล้ายกัน
คุณอาจไม่ได้ใช้มันมากนัก แต่ก็มีคนที่ใช้มันตลอดทั้งวัน
เหมือนเคยได้ยินว่าวิศวกร FAANG รันโปรแกรมแบบนี้หลายสิบ หรืออาจถึงหลายร้อยตัว บนเซิร์ฟเวอร์ทุกเครื่องตลอดเวลา และยังไม่นับการใช้งานครั้งเดียวด้วย
FAANG ยังจ้างนักพัฒนาเคอร์เนลเฉพาะทางด้วย ดังนั้นก็เท่ากับพวกเขาเป็นคนให้ทุนกับความซับซ้อนที่ตัวเองใช้นี้
ผมเองก็เคยแก้ปัญหาด้วย 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'” จะตรงกว่า
บ้านเรามีสำนวนว่า “เม่นในกางเกง”
ต่อให้มีประโยชน์แค่ไหน สิ่งนี้ก็ดูไม่ได้ถูกเขียนขึ้นอย่าง ปลอดภัยและรอบคอบ นัก