CVE-2023-40547 – ช่องโหว่ที่เกิดจาก shim เชื่อถือ HTTP header อย่างไม่ถูกต้อง
(github.com/rhboot)- commit
0226b56ของ rhboot/shim แก้ไข CVE-2023-40547 ซึ่งเกิดจากการเชื่อถือ ค่าขนาดใน HTTP header ตามที่ระบุไว้ในกระบวนการรับไฟล์ - หาก header ที่ถูกดัดแปลงระบุขนาดเล็กกว่าข้อมูลที่รับมาจริง shim อาจจัดสรรพื้นที่เล็กกว่า buffer ที่จำเป็น
- โค้ดเดิมใช้ค่า header สำหรับการจัดสรรหน่วยความจำ แต่ใช้ metadata ของโปรโตคอล สำหรับการคัดลอกข้อมูล ซึ่งอาจนำไปสู่ out-of-bounds write
- แพตช์เพิ่มการตรวจสอบ
*buf_size < rx_message.BodyLengthในreceive_http_response()ของhttpboot.cและเมื่อไม่ผ่านจะจัดการเป็นEFI_BAD_BUFFER_SIZEพร้อมข้อความInvalid Content-Length - ขอบเขตการเปลี่ยนแปลงคือ เพิ่ม 7 บรรทัด·ลบ 1 บรรทัด ในไฟล์
httpboot.cเพียงไฟล์เดียว และแก้คำสะกดผิดContent-Lenghtเป็นContent-Lengthด้วย
ลำดับการเกิดช่องโหว่
- CVE-2023-40547 เป็นปัญหาที่เกิดขึ้นเมื่อ shim ดึงไฟล์ผ่าน HTTP หรือโปรโตคอลที่เกี่ยวข้อง
- ในกระบวนการจัดสรร buffer เพื่อเก็บข้อมูลที่รับเข้ามา มีการใช้ ค่าขนาดใน HTTP header
- HTTP header สามารถถูกดัดแปลงได้ และอาจระบุขนาดที่เล็กกว่าข้อมูลที่รับมาจริง
- ลำดับการทำงานเดิมใช้ค่า header สำหรับการจัดสรร buffer และเมื่อคัดลอกข้อมูลจาก rx buffer จะอ้างอิงตาม metadata ของโปรโตคอล
- ความแตกต่างนี้ทำให้สามารถคัดลอกข้อมูลที่ใหญ่กว่า buffer ที่จัดสรรไว้ได้ และส่งผลให้เกิด out-of-bounds write
เนื้อหาของแพตช์
- แพตช์เพิ่มการตรวจสอบเชิงป้องกันใน
receive_http_response(EFI_HTTP_PROTOCOL *http, VOID **buffer, UINT64 *buf_size)ของhttpboot.c - กรณี
*buf_size == 0จะแก้คำสะกดผิดในข้อความข้อผิดพลาดเดิม แล้วไปที่goto errorFailed to get Content-Lenght→Failed to get Content-Length
- การตรวจสอบใหม่จะตรวจเงื่อนไข
*buf_size < rx_message.BodyLength- หากเงื่อนไขเป็นจริง จะตั้งค่า
efi_status = EFI_BAD_BUFFER_SIZE - แสดงข้อผิดพลาด
Invalid Content-Length - จากนั้นไปที่
goto error
- หากเงื่อนไขเป็นจริง จะตั้งค่า
ขอบเขตการเปลี่ยนแปลง
- ไฟล์ที่เปลี่ยนแปลงมีเพียง
httpboot.c1 ไฟล์ - ปริมาณการเปลี่ยนแปลงคือ เพิ่ม 7 บรรทัด, ลบ 1 บรรทัด
- สาระสำคัญคือ logic ที่ตรวจสอบว่า
rx_message.BodyLengthซึ่งเป็นความยาวของ body ที่รับเข้ามา มากกว่า*buf_sizeซึ่งเป็นขนาดที่จัดสรรไว้หรือไม่
บันทึกที่เกี่ยวข้อง
- commit นี้ถูกระบุว่าเป็นการเปลี่ยนแปลงเพื่อแก้ไข CVE-2023-40547
- ข้อความ commit ระบุชัดเจนว่าปัญหาเกิดจากการเชื่อถือ HTTP header อย่างไม่ถูกต้อง
- ผู้รายงานช่องโหว่ถูกบันทึกว่าเป็น Bill Demirkapi จาก Microsoft Security Response Center
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
shim เป็น EFI bootloader ที่มักใช้กันใน Linux distributions ที่ต้องการเปิดใช้ Secure Boot
ในมุมของดิสโทร แทนที่จะให้ผู้ใช้ลงทะเบียนคีย์ด้วยตนเอง ก็อยากให้เปิด Secure Boot ได้ง่ายด้วยคีย์ลายเซ็นของ Microsoft ที่มีมาให้เป็นค่าเริ่มต้น
แต่โดยทั่วไป Microsoft จะไม่เซ็นชื่อให้ bootloader แบบ GPL อย่าง GRUB ดังนั้นจึงมีการสร้าง shim ที่สามารถเซ็นด้วยคีย์ของ Microsoft ได้ขึ้นมา และ shim จะตรวจสอบลายเซ็นของสิ่งที่จะบูตด้วยคีย์แยกต่างหากชื่อ Machine Owner Key หรือ MOK
ตอนระบุ EFI binary ที่จะให้ shim บูต สามารถส่งเป็น HTTP URL ได้ และหาก HTTP server เป็นอันตราย ก็อาจทำให้เกิดการเขียนออกนอกขอบเขตได้
อย่างไรก็ตาม โดยปกติมักใช้เพื่อบูต local second-stage bootloader อย่าง GRUB จึงดูมีโอกาสน้อยที่จะเป็นปัญหาในระบบติดตั้งส่วนใหญ่
Secure Boot ถูกออกแบบมาตั้งแต่แรกให้สามารถเพิกถอนแม้แต่ไบนารีที่ถูกเซ็นชื่อแล้วผ่าน รายการ DBX ได้ และเมื่อใส่รายการนี้ลงใน UEFI ก็จะปฏิเสธไบนารีนั้นแม้จะมีลายเซ็นที่ถูกต้องก็ตาม
หากเพิ่มลายเซ็นของไบนารี shim รุ่นเก่าที่มีบั๊กนี้เข้าไปในรายการ แต่ละคนก็อัปเดตรายการบนอุปกรณ์ของตนได้ อาจกระจายผ่าน capsule update อย่าง LVFS ได้เช่นกัน และถ้าจัดการ Secure Boot keys และ variables ด้วยตนเอง ก็สามารถดาวน์โหลดรายการจาก https://uefi.org/revocationlistfile มาลงทะเบียนได้
ถ้าเป็นแบบนั้นจริงก็คงไม่ได้ระดับ Critical
บั๊กนี้สามารถถูกใช้โจมตีได้เมื่อมัลแวร์ที่มีสิทธิ์ระดับสูงในเครื่องเขียนทับ EFI partition จากฝั่ง local, เมื่อมีการโจมตีแบบ man-in-the-middle จากเครือข่ายใกล้เคียงที่เปิด PXE boot, และเมื่อใช้ HTTP boot ก็ถูกใช้โจมตีแบบ remote man-in-the-middle ได้
ผู้โจมตีจากระยะไกลที่ไม่มีสิทธิ์แต่ยืนอยู่ในตำแหน่ง man-in-the-middle และเครื่องเป้าหมายใช้ HTTP boot สามารถใช้ประโยชน์ได้โดยไม่ต้องเข้าถึงเครื่องโดยตรง
ผู้โจมตีจากระยะไกลที่ได้สิทธิ์และสามารถรันโค้ดบนเครื่องเป้าหมายได้ สามารถ bypass Secure Boot ได้แม้เหยื่อจะไม่ได้ใช้ HTTP boot ตราบใดที่ firmware รองรับ HTTP
ตัวอย่างเช่น เปลี่ยนตัวแปรลำดับการบูตให้ชี้ไปยัง server ที่ผู้โจมตีควบคุม หรือเขียนทับ bootloader ใน EFI partition ด้วยภาพ shim และ GRUB2 ปกติ แล้วทำให้
grub.cfgchainload shim ตัวใหม่ผ่าน HTTPเพราะไวยากรณ์อุปกรณ์ของ GRUB2 สามารถระบุอุปกรณ์ที่รองรับได้รวมถึง HTTP
นอกจากนี้ หากผู้โจมตีใกล้เคียงที่ไม่มีสิทธิ์อยู่ในตำแหน่ง man-in-the-middle และเครื่องเป้าหมายใช้ PXE boot ก็สามารถเชื่อมลำดับแบบ shim ของ PXE → GRUB2 ของ PXE → shim บน HTTP เพื่อใช้โจมตีได้
ที่มา: https://techcommunity.microsoft.com/t5/hardware-dev-center/u...
ถ้าเงื่อนไข anti-Tivoization ของ GPLv3 อาจบังคับให้ต้องให้ Secure Boot signing key งั้นก็ไม่ใช่ว่าควรต้องให้ MOK signing key เมื่อมีการร้องขอด้วยหรือ
ถ้าอย่างนั้นใคร ๆ ก็จะได้คีย์ที่สามารถเซ็นโค้ดใดก็ได้ให้ถูกบูตทางอ้อมผ่าน Secure Boot ซึ่งไม่แน่ใจว่าต่างอย่างมีนัยสำคัญจากการที่ Microsoft ออก signing key ให้โครงการ GPLv3 อย่าง GRUB โดยตรงหรือไม่
บนเครื่อง BIOS รุ่นเก่าเคยตั้งให้ WBM chainload GRUB ไว้ แต่บนเครื่อง UEFI ยังไม่เคยลอง จึงไม่แน่ใจว่ามีจุดไหนที่ทำไม่ได้หรือไม่
อาจมีคนสงสัยว่า “ทำไมถึงบูตจากเซิร์ฟเวอร์ที่ไม่น่าเชื่อถือหรือถูกเจาะ?”, “ถ้าเซิร์ฟเวอร์ถูกเจาะอยู่แล้ว ส่งไบนารีอันตรายมาเลยก็ได้ แบบนี้ไม่ไร้ความหมายหรือ?” คำตอบสั้น ๆ คือไบนารีที่ shim จะบูตเป็นตัวสุดท้ายต้อง ถูกเซ็นด้วย MOK
ดังนั้นไม่ว่าจะบูตภายในเครือข่ายที่ถูกเจาะ, บูตผ่าน HTTP, หรือบูตจากเซิร์ฟเวอร์ที่ถูกเจาะ ก็ควรคงหลักประกันด้านความปลอดภัยแบบเดียวกันไว้ ไม่เกี่ยวว่าจะใช้ HTTPS หรือไม่
Secure Boot ไม่ได้ป้องกันการ downgrade อยู่แล้ว ดังนั้นการที่เซิร์ฟเวอร์ที่ถูกเจาะสามารถถูกใช้ทำ downgrade attack ได้จึงเป็นคนละประเด็นกับช่องโหว่นี้
การป้องกัน downgrade attack ก็ต้องไปทำแยกด้วยวิธีที่แข็งแรงกว่านี้อยู่ดี
แต่ก็ยังไม่แน่ใจว่าทำไม shim ถึงต้องรองรับ HTTP boot ได้โดยตรง
น่าจะให้ local EFI binary ตัวที่สองซึ่งเซ็นด้วย MOK จัดการแทนก็ได้ แต่คงคิดว่าเป็นฟีเจอร์ที่ทำได้ค่อนข้างง่าย
ไม่เข้าใจว่าทำไมโค้ดนี้ถึงจัดการความยาวของเนื้อหาด้วยเกณฑ์สองแบบ
ตาม RFC ใน HTTP/1.1 ค่า Content-Length คือข้อมูลที่เชื่อถือได้สำหรับความยาวของเนื้อความในคำขอ/คำตอบ HTTP
ข้อมูลที่อยู่บนสายเกินกว่าความยาวนั้น ตามนิยามแล้วถือเป็นส่วนหนึ่งของข้อความอื่น
ในทางกลับกัน ถ้า Content-Length มากกว่า
rx_message.BodyLengthก็แปลว่ายังรับข้อความมาไม่ครบ จึงควรรอต่อหรือคืนข้อผิดพลาดหมดเวลาไม่ว่าแบบไหน ถ้าไม่มีการรับประกันว่า
rx_message.BodyLengthเท่ากับ Content-Length ก็แปลว่าค่านั้นไม่ถูกต้องถ้าอยากจัดการแบบผ่อนปรนกว่านี้ ก็ไม่มีเหตุผลต้องดูเฮดเดอร์ Content-Length เลย และใช้
rx_message.BodyLengthเป็นขนาดบัฟเฟอร์ ตีความข้อมูลทั้งหมดบนสายว่าเป็นข้อความที่รับมาได้เลยโค้ดปัจจุบันซับซ้อนเกินจำเป็น และบั๊กแบบนี้ก็เลยหลุดเข้ามา
เมื่อดูโค้ดรอบข้าง https://github.com/rhboot/shim/blob/0226b56513b2b8bd5fd281bc... จะเห็นว่าภายในลูปมีการรับข้อมูลเป็นชิ้น ๆ และทุกครั้งจะตรวจว่าข้อมูลใหม่ทำให้เกินความจุบัฟเฟอร์ที่กำหนดโดย Content-Length หรือไม่
แต่ก่อนหน้านี้ไม่ได้ตรวจสำหรับ การอ่านครั้งแรก ที่อยู่นอกลูป และนั่นคือบั๊ก
อย่างไรก็ตาม ไม่เห็นโค้ดที่ตรวจยืนยันขั้นสุดท้ายว่าขนาดที่ดาวน์โหลดมาเท่ากับ
*buf_sizeหรือก็คือ Content-Lengthถ้าเงื่อนไขนี้ไม่เป็นจริง ก็อาจเป็นสัญญาณว่าการเชื่อมต่อถูกปิดเร็วเกินไป
นี่เป็นบั๊กชัดเจนและก็ดีแล้วที่แก้ไข แต่ก็อดสงสัยไม่ได้ว่าใครจะบูตเครื่องของตัวเองจากโฮสต์ที่ไม่น่าเชื่อถือ
ถ้าผู้โจมตีควบคุมบริการ HTTP ได้มากพอจะส่งเฮดเดอร์อันตรายมาได้ การหลีกเลี่ยง overflow นี้ก็น่าจะเป็นปัญหาที่เล็กที่สุดแล้ว เพราะใบรับรองก็น่าจะถูกเจาะด้วย และยังสามารถใส่มัลแวร์ลงใน payload ที่ถูกต้องตามสเปกแล้วส่งมาได้อีก
มันเป็นบั๊กก็จริง แต่ไม่แน่ใจว่าเป็นระดับ Critical หรือเปล่า
พวกเขาเชื่ออย่างจริงจังในยุทธศาสตร์ความปลอดภัยที่กำหนดว่าทุกอย่างที่อาจถูกเจาะได้ต้องไม่สามารถทำงานได้ถ้าไม่ได้ลงนามด้วย Secure Boot
ถ้ามีของที่ถูกลงนามแต่มีช่องโหว่อยู่แม้เพียงชิ้นเดียว ก็อาจใช้มันเพื่อปลดล็อกดิสก์เข้ารหัสด้วย Secure Boot+TPM ของทุกคนได้
ยากจะเข้าใจว่าทำไมแนวทางนี้ถึงถูกมองว่าเป็นโมเดลความปลอดภัยที่ใช้ได้ และช่องโหว่แบบนี้ก็มีอยู่เต็มไปหมดแล้ว
แถมยังมองข้ามช้างตัวโตในห้องอย่าง Windows ไปโดยสิ้นเชิง
ตัวอย่างของวิธีคิดแบบนี้: https://lkml.org/lkml/2018/4/3/767
แม้จะมีความกังวลจาก Linus แต่หลายดิสโทรก็เปิด integrity mode จริงเมื่อบูตด้วย Secure Boot
น่าจะเป็นเพราะนโยบายของ Microsoft และสถานการณ์ที่ดิสโทรซึ่งต้องการลายเซ็น UEFI ของ Microsoft ถูกบังคับให้ทำตามขั้นตอนที่อธิบายไว้ในเธรดนั้น
ผลก็คือเมื่อเปิด Secure Boot ฟีเจอร์ของดิสโทรมักถูกจำกัด เช่น ใช้โหมดจำศีลไม่ได้
เวลามีเรื่องสำคัญเกิดขึ้นควรใช้ HTTP ที่มี S
การบูตอุปกรณ์ก็เข้าข่ายนี้ และ เฮดเดอร์ HTTPS ก็ถูกเข้ารหัสอยู่แล้วเสมอ
ถึงอย่างนั้นก็เป็นบั๊กที่หาพบได้ดี
เฮดเดอร์ที่ไม่ถูกต้องส่งมาได้ทั้งสองแบบ
เพราะการเข้ารหัสต้องอาศัยเวลาและวันที่ที่ถูกต้อง
RTC อาจใช้ได้ แต่ก็ไม่รู้ว่าจัดการเขตเวลาได้ดีหรือเปล่า และอย่างไรก็ยังอาจตั้งเวลาคลาดเคลื่อนได้
ดูเหมือนจะยังเข้าใจปัญหาไม่ถูกต้อง
Content-lengthไม่ใช่ความยาวของเนื้อความจริง แต่เป็น ความยาวหลัง Content-encodingใน build ของ shim เหล่านี้มี httpboot รวมมาด้วยหรือ?
เท่าที่รู้ shim มีไว้แค่รัน EFI binary อื่นจากดิสก์ และไม่แน่ใจว่าเคยเห็นฟังก์ชันบูตผ่านเครือข่ายของ shim ถูกใช้งานจริงหรือเปล่า
อาจเป็นเพราะผมไม่รู้เอง แต่คิดว่า HTTP client ส่วนใหญ่จะอ่านแค่ตาม Content-Length ที่กำหนด และถ้าอ่านได้ไบต์น้อยกว่าค่า Content-Length ก็จะถือว่าเป็นข้อผิดพลาด
เท่าที่ดู สเปก UEFI เหมือนจะไม่ได้ระบุพฤติกรรมอย่างชัดเจนเมื่อเฮดเดอร์ Content-Length ไม่ตรงกับความยาวของ response body
เพราะงั้นก็มีความเป็นไปได้มากที่บาง implementation จะสร้างคำขอ
connection:closeแล้วไม่ตรวจ Content-Length เลยช่องโหว่นี้ถูกรายงานโดย MSRC และคำอธิบาย CVE ก็ไม่ได้กล่าวถึงการนำไปใช้โจมตีจริง
อาจมีการเปิดเผยภายหลัง หรืออาจเป็นเพียงปัญหาในทางทฤษฎี
ช่วยอธิบายได้ไหมว่าทำไมการอ่านน้อยกว่าความยาวเนื้อความจริงถึงอันตราย?
ผมกลับคาดว่ากรณีตรงข้ามน่าจะอันตรายกว่า
แต่กลับเอาขนาดมาจากเฮดเดอร์ HTTP ที่ผู้โจมตีควบคุมได้ และผู้โจมตีก็กำหนดขนาดให้เล็กกว่าข้อมูลที่ส่งมาจริงได้
ในกรณีนี้ โค้ดใช้ค่าในเฮดเดอร์ตอนจัดสรร แต่ตอนคัดลอกจากบัฟเฟอร์รับกลับใช้ขนาดจาก metadata ของโปรโตคอล จึงเกิด การเขียนนอกขอบเขต