การปรับปรุงด้านการวิเคราะห์แบบสถิตใน GCC 14
(developers.redhat.com)-fanalyzerของ GCC 14 มุ่งเน้นการติดตามหลายเส้นทางการทำงานของโค้ด C ด้วย symbolic execution เพื่อค้นหาข้อบกพร่องที่จับได้ยากก่อนรันจริงตั้งแต่ขั้นตอนคอมไพล์- การปรับปรุงครั้งนี้แบ่งเป็น การตรวจจับลูปไม่สิ้นสุด, การแสดงภาพบัฟเฟอร์โอเวอร์โฟลว์, การติดตามสตริง C และการเปิดใช้ taint analysis เป็นค่าเริ่มต้น โดยเน้นทำให้ผลการวินิจฉัยเข้าใจง่ายขึ้น
-Wanalyzer-out-of-boundsไม่ได้มีแค่คำเตือนแบบธรรมดาอีกต่อไป แต่แสดง แผนภาพข้อความ เพื่อให้เห็นความสัมพันธ์ระหว่างบัฟเฟอร์กับตำแหน่งที่เขียนข้อมูล และยังระบุได้ด้วยว่าโอเวอร์โฟลว์เกิดขึ้นกลางอักขระใดในสตริง UTF-8- แอททริบิวต์ใหม่
null_terminated_string_arg(PARAM_IDX)ใช้บอกทั้งตัววิเคราะห์และผู้อ่านโค้ดว่า API นั้นคาดหวังสตริงที่ลงท้ายด้วยนัล ทำให้มองเห็นเส้นทางการส่งผ่าน บัฟเฟอร์ที่ไม่ได้ลงท้ายด้วยนัล ได้ชัดเจนขึ้น - taint analysis จะเปิดใช้งานโดยอัตโนมัติเมื่อเลือก
-fanalyzerทำให้ตรวจสอบได้ง่ายขึ้นว่า ค่าที่ผู้โจมตีควบคุมสามารถถูกนำไปใช้เป็นขนาด ดัชนี หรือออฟเซ็ตโดยไม่มีการตรวจขอบเขตบนได้อย่างไร
ขอบเขตปัญหาที่ -fanalyzer จัดการ
-fanalyzerคือพาสสำหรับการวิเคราะห์แบบสถิตของ GCC ที่ใช้ symbolic execution กับหลายเส้นทางการทำงานของซอร์สโค้ด C เพื่อพยายามค้นหาข้อบกพร่องตั้งแต่ขั้นตอนคอมไพล์- การปรับปรุงใน GCC 14 ถูกรวบรวมไว้ก่อนการออกรุ่นอย่างเป็นทางการในเดือนเมษายน 2024 และ ณ เวลาที่เขียนบทความนั้น คาดว่า GCC 14.1 จะออกในช่วงเดือนเมษายน 2024
- GCC 14.0 รุ่นพรีรีลีสถูกใช้งานอยู่แล้วใน Fedora 40 Beta
- สามารถลองตัวอย่างต่างๆ พร้อมออปชันคอมไพเลอร์ใหม่ได้ที่ Compiler Explorer
การตรวจจับลูปไม่สิ้นสุดแบบง่าย
- GCC 14 เพิ่มคำเตือนใหม่
-Wanalyzer-infinite-loop - โค้ดตัวอย่างเป็นกรณีที่ในลูป
forซ้อนกัน เงื่อนไขของลูปที่สองคือj < nแต่ส่วนเพิ่มค่ากลับยังเป็นi++- เป็นความผิดพลาดจากการคัดลอกลูป
forแรกมาแล้วลืมเปลี่ยนiในส่วนเพิ่มค่าให้เป็นj - ตัววิเคราะห์จะติดตามกิ่ง true ของ
j < nต่อไปบนเส้นทางนี้ และเตือนว่าลูปวนซ้ำไม่สิ้นสุด
- เป็นความผิดพลาดจากการคัดลอกลูป
- ปัจจุบัน เอาต์พุตการวินิจฉัยจะเข้าใจลำดับเหตุการณ์ได้ง่ายที่สุดเมื่ออ่านตามหมายเลขเหตุการณ์ตั้งแต่
(1)ถึง(5) - ใน GCC 15 ยังมีความหวังว่าจะเพิ่มความอ่านง่ายด้วยวิธีคล้าย ASCII art ที่ช่วยเน้นเส้นทางการไหลของการควบคุม
- สามารถลองโค้ดนี้ได้จาก ตัวอย่างบน Compiler Explorer
แสดงภาพบัฟเฟอร์โอเวอร์โฟลว์ด้วยข้อความ
- ใน GCC 13 ตัววิเคราะห์เริ่มรองรับการตรวจสอบขอบเขตผ่าน
-Wanalyzer-out-of-bounds - ใน GCC 14 สามารถแสดงความสัมพันธ์เชิงพื้นที่ของบัฟเฟอร์โอเวอร์โฟลว์ที่คาดการณ์ได้ด้วย แผนภาพแบบข้อความ
- ในตัวอย่างที่ทำ
strcpy(buf, "hello")ลงในchar buf[10]แล้วตามด้วยstrcat(buf, " world!")จะตรวจพบบัฟเฟอร์โอเวอร์โฟลว์บนสแตก- ข้อความแบบเดิมจะแสดงว่า
bufมีความจุ 10 ไบต์ และมีการเขียนเกินขอบเขตตั้งแต่ byte 10 ถึง byte 12 - แผนภาพใน GCC 14 จะแสดงทั้งบัฟเฟอร์ปลายทางที่
strcpyเติมไว้ และไบต์NULปิดท้ายเดิมที่เป็นจุดเริ่มของstrcat
- ข้อความแบบเดิมจะแสดงว่า
- ในตัวอย่างสตริงที่ไม่ใช่ ASCII ก็สามารถระบุตำแหน่งโอเวอร์โฟลว์ตามการแทนค่าแบบ UTF-8 ได้เช่นกัน
- ใช้โค้ดตัวอย่างที่คัดลอก
"サツキ"ลงในchar buf[11]แล้วต่อท้ายด้วย"メイ" - แผนภาพแสดงว่าโอเวอร์โฟลว์เกิดขึ้นกลางอักขระ
メหรือก็คือ U+30E1
- ใช้โค้ดตัวอย่างที่คัดลอก
- ดูโค้ดที่เกี่ยวข้องได้จาก ตัวอย่างสตริง ASCII และ ตัวอย่างสตริงที่ไม่ใช่ ASCII
ปรับปรุงการติดตามการทำงานของสตริง C
- ตัววิเคราะห์ของ GCC 14 ปรับปรุงการติดตามการทำงานของสตริง C ให้ดีขึ้น โดยจำลอง API ที่สแกนบัฟเฟอร์เพื่อหาไบต์ปิดท้ายแบบนัล
- หากมีเส้นทางที่พอยน์เตอร์ชี้ไปยัง บัฟเฟอร์ที่ไม่ได้ลงท้ายด้วยนัล แล้วถูกส่งเข้า API ดังกล่าว ก็จะมีคำเตือน
- แอททริบิวต์ฟังก์ชันใหม่
null_terminated_string_arg(PARAM_IDX)ใช้บอกทั้งตัววิเคราะห์และผู้อ่านโค้ดว่า พารามิเตอร์ที่กำหนดต้องเป็นสตริงที่ลงท้ายด้วยนัล - ในตัวอย่าง
example_fn(const char *p)มีการใส่แอททริบิวต์null_terminated_string_arg(1)และnonnullchar str[3] = "abc";ไม่มีพื้นที่สำหรับไบต์นัลปิดท้าย- เมื่อเรียก
example_fn(str)ตัววิเคราะห์จะเตือนเรื่อง การอ่านเกินบัฟเฟอร์บนสแตก จำนวน 1 ไบต์หลังจบstr - ในข้อความวินิจฉัยยังมี note ระบุด้วยว่าอาร์กิวเมนต์ลำดับที่ 1 ของ
example_fnต้องเป็นพอยน์เตอร์ไปยังสตริงที่ลงท้ายด้วยนัล
- สามารถลองตัวอย่างนี้ได้ใน Compiler Explorer
เปิดใช้ taint analysis เป็นค่าเริ่มต้น
- taint analysis ของตัววิเคราะห์ใช้ติดตามอินพุตที่ผู้โจมตีควบคุม จุดที่มีการทำความสะอาดค่า และจุดที่ถูกใช้งานโดยไม่ผ่านการทำความสะอาด
- ใน GCC รุ่นก่อนหน้า ฟีเจอร์นี้ไม่ได้เปิดใช้เป็นค่าเริ่มต้น เนื่องจากมีทั้งบั๊กและ false positive จำนวนมาก และถูกซ่อนไว้หลังอาร์กิวเมนต์บรรทัดคำสั่งแยกต่างหาก
- ใน GCC 14 หลังจากแก้บั๊กหลายรายการแล้ว เมื่อเลือก
-fanalyzerระบบจะเปิดใช้ taint analysis โดยอัตโนมัติ - การเปลี่ยนแปลงนี้ยังเปิดใช้คำเตือนแบบอิง taint ต่อไปนี้อีก 6 รายการด้วย
กรณีศึกษาการวิเคราะห์เคอร์เนลจาก CVE-2011-2210
- ใช้ตัวอย่างที่ตัดมาจาก CVE-2011-2210 ของเคอร์เนล Linux เป็นกรณีศึกษาสำหรับ taint analysis
- มีการเพิ่ม
__attribute__((tainted_args))เข้าไปในแมโคร__SYSCALL_DEFINExเพื่อบอกตัววิเคราะห์ว่าอาร์กิวเมนต์ของosf_getsysinfoเป็นค่าที่มาจากข้ามขอบเขตความเชื่อถือ และควรถูกปฏิบัติเป็น ค่าปนเปื้อน - ตัววิเคราะห์ของ GCC 14 จะเตือนว่าใน
copy_to_user(buffer, hwrpb, nbytes)มีการใช้ค่าnbytesที่ผู้โจมตีควบคุมเป็นขนาดโดยไม่มีการตรวจขอบเขตบน- ข้อความวินิจฉัยแสดงว่า
nbytesผ่านการตรวจเพียงขอบเขตล่างในif (nbytes < sizeof(*hwrpb)) - พารามิเตอร์ตัวที่สามของ
copy_to_userถูกระบุว่าเป็นพารามิเตอร์ขนาดด้วยแอททริบิวต์access(write_only, 1, 3)
- ข้อความวินิจฉัยแสดงว่า
- ปัญหาอยู่ที่เขียนเงื่อนไขการทำความสะอาดไว้เป็น
if (nbytes < sizeof(*hwrpb))- เงื่อนไขที่ตั้งใจจริงคือรูปแบบ
if (nbytes > sizeof(*hwrpb)) - ในเวอร์ชันที่แก้เงื่อนไขแล้ว ตัววิเคราะห์จะไม่เตือน
- เงื่อนไขที่ตั้งใจจริงคือรูปแบบ
- งานในการรันตัววิเคราะห์กับเคอร์เนลเพื่อค้นหาช่องโหว่และแก้ false positive ของตัววิเคราะห์ยังคงดำเนินต่อไป
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
สำหรับผม fanalyzer เป็นหนึ่งในฟีเจอร์เด็ดของ GCC เมื่อเทียบกับ Clang มันอธิบายข้อผิดพลาดให้ ทำให้การเขียนโปรแกรม C ง่ายขึ้นมาก และข้อความ error ก็เริ่มให้ความรู้สึกคล้าย Rust ในแง่ที่เป็นมิตรกับนักพัฒนา
เหตุผลใหญ่ที่สุดที่คนเลิกเรียนเทคโนโลยีมักเป็น error ที่ชวนหงุดหงิดหรือไม่ชัดเจน อาจจะออกนอกประเด็นไปนิด แต่หมายความว่าผมชอบ C และคิดว่ามันจะดียิ่งขึ้นถ้ามีข้อความ error ระดับเดียวกับ Rust
error: missing semicolonกลับมีข้อความ error เกี่ยวกับ template instantiation ไหลออกมาเกิน 1,000 บรรทัดผมลองใช้ analyzer ของ GCC อยู่สองสามครั้ง แต่หา frontend ดี ๆ ที่ทำให้อ่าน output ได้ง่ายไม่เจอ ฝั่ง Clang มีตัวเลือกหลายอย่าง เช่น HTML output ที่ค่อนข้างดี, CodeChecker, การผสานกับ Xcode เลยสงสัยว่าฝั่ง GCC อ่าน output กันอย่างไร แถม GCC ยังดูเหมือนให้ false positive มากกว่า Clang เยอะด้วย
มีความคิดเห็นอีก 36 รายการในเธรดอื่น: https://news.ycombinator.com/item?id=39918278
“GCC 14 Boasts Nice ASCII Art for Visualizing Buffer Overflows (phoronix.com)” เป็นโพสต์เมื่อ 2 ชั่วโมงก่อน
เมื่อไม่กี่เดือนก่อนผมทำ ยูทิลิตี Linux เล็ก ๆ ตัวหนึ่ง เป็น drop-in shim ที่แทนที่ executable ใดก็ได้ เมื่อถูกเรียก มันจะแกล้งทำตัวเป็นโปรแกรมเดิม แล้ว fork ตัวต้นฉบับและเชื่อมต่อกับ stdout/stderr
output ของ error จะถูกส่งไปยังผู้ช่วย GPT แบบปรับแต่งเฉพาะที่รู้บริบทของโปรแกรมนั้น จากนั้นผู้ช่วยจะแปลง error เดิมให้อยู่ในรูปแบบที่มนุษย์อ่านง่าย แล้วส่งออกทาง stderr ของ shim ผมใช้มันเพราะเบื่อกับการดู compiler dump แบบซ้อนกันของ concept/template ใน GCC/Clang แต่ถ้าต้องการก็ใช้กับโปรแกรมอะไรก็ได้ มันทำงานได้ดี แต่ผมป่วยหนักเลยไม่ได้ทำต่อ และคิดว่าน่าจะเป็นโปรเจกต์ที่ดีถ้ามีใครกลับมาทำให้จริงจังและทำให้ใช้ได้ทั่วไป
อยากให้มีรูปแบบ output ที่ดีกว่านี้สำหรับผลการวิเคราะห์ วิธีปัจจุบันเป็นนรกสำหรับ screen reader
ดูตัวอย่าง output ได้ที่นี่: https://godbolt.org/z/aan6Kfxds
เป็นการเพิ่มตัวเลือก command line
-fdiagnostics-format=sarif-stderrให้กับตัวอย่างแรกในบทความ ผมเคยทดลอง output ไดอะแกรมเป็น SVG ด้วย แต่ยังขัดเกลาไม่พอที่จะใส่ใน GCC 14โค้ดเดิมเป็นลักษณะที่ถ้า
nbytes < sizeof(*hwrpb)จะคืนค่า-1และถ้าcopy_to_user(buffer, hwrpb, nbytes)ล้มเหลวจะคืนค่า-2การแก้ไขที่ถูกนำไปใช้คือการตรวจnbytes > sizeof(*hwrpb)แต่ผมมองว่าการแก้ที่ถูกต้องควรเป็นcopy_to_user(buffer, hwrpb, sizeof(*hwrpb))การคัดลอกจาก pointer
hwrpbออกไปด้วยขนาดที่ไม่ใช่sizeof(*hwrpb)นั้นไม่สมเหตุสมผลnbytes = 4มา และsizeof(hwrpb)เท่ากับ 16 ไบต์ ก็อาจ คัดลอกเกินไป 12 ไบต์ จาก buffer ของผู้เรียก และอ่านหน่วยความจำที่ไม่ได้เป็นเจ้าของได้ ผมคิดว่าควรหลีกเลี่ยงทางแก้ที่ดีกว่าคือคัดลอกเฉพาะจำนวนไบต์ขั้นต่ำที่ทั้งผู้เรียกและผู้ถูกเรียกรองรับ เช่น
nbytes = MIN(nbytes, sizeof(hwrpb));ภายใต้สมมติฐานว่าข้อมูลเวอร์ชันในhwrpb->sizeถูกปฏิบัติตาม ก็จะรับประกัน backward/forward compatibility ได้ แม้บางส่วนของ struct จะยังไม่ได้ initialize ก็ตามnbytesไปไม่ได้เหมือนกันยอดเยี่ยมจริง ๆ ดูเหมือนงานที่ใส่ลงไปจะมหาศาลมาก ระดับความยากดูพอ ๆ กับการนำ fat pointer/array view เข้าสู่ standard library และมาตรฐาน C
-Wstringop-overflowมี false positive เยอะเกินไป จนเป็น warning ตัวแรกที่ผมปิดเสมอ เลยสงสัยว่า variant แบบ analyzer จะดีกว่านี้หรือไม่เจ๋งมาก ช่วงนี้ผมไม่ได้ทำ C development มากนัก เลยสงสัยว่า
strcpyกับstrcatยังถูกใช้บ่อยแค่ไหน เท่าที่ดูครั้งล่าสุด มันใกล้เคียงกับสิ่งต้องห้ามพอ ๆ กับgotoแน่นอนว่าผมรู้ว่าในการพัฒนา kernel มักนิยมใช้
gotoอยู่บ่อย ๆ เลยสงสัยว่า การวิเคราะห์สตริง C จะช่วยได้จริงมากแค่ไหนgotoนั้นถูกต้องและสวยงามอย่างชัดเจน ถ้าพยายามหลีกเลี่ยงแบบไม่มีเงื่อนไข อาจทำให้โค้ดน่าเกลียด พันกัน และดูแลรักษายาก แม้จะไม่พบบ่อย แต่ก็มีการใช้งานที่สมเหตุสมผลฟังก์ชันอย่าง
strcpyไม่ค่อยถูกแนะนำ แต่ก็มีสถานการณ์ที่รับประกันได้ว่าถูกต้อง ตราบใดที่ invariant ที่แข็งแรงกว่า เช่น invariant ระดับภาษา ไม่ถูกทำลาย ถ้ากรณีแบบนั้นพัง แปลว่ามีปัญหาใหญ่กว่านั้นมากอยู่แล้ว แม้จะเป็นกรณีที่หายาก แต่ก็อาจโต้แย้งได้ว่าทางเลือกที่ในนามปลอดภัยกว่าอาจมีประสิทธิภาพต่ำลงเล็กน้อยโดยไม่ได้ประโยชน์เพิ่มgotoบางแบบก็ยังเป็น idiom ใน C อยู่ ต้องระวังแน่นอน แต่ยังไงมันก็คือ C จึงเป็นไปได้แต่ผมไม่ชอบ
longjmpเลยจริง ๆgotoแบบเรียบง่ายไม่มีปัญหา ในทางกลับกัน ตระกูล strxcpy นั้นเละเทะโดยสิ้นเชิง และไม่ควรใช้ไม่ว่าด้วยเหตุผลใดก็ตาม การที่มันถูกใช้ใน kernel เป็นเรื่องน่ากลัวฟังก์ชันเหล่านั้นกับความพยายามทั้งหมดที่ “จะแก้” มันแต่ล้มเหลว ควรถูกยิงทิ้งจากวงโคจรไปเลย
gotoถ้าใช้ด้วยความระมัดระวังก็โอเคstrcpyกับstrcatก็ “โอเค” ในความหมายที่ว่าคุณรู้ว่าโค้ดถูกต้อง และถ้ามันผิดก็จะเกิดปัญหาใหญ่ น่าเสียดายที่คำอธิบายนี้ใช้ได้กับ C ส่วนใหญ่gotoเป็นสิ่งต้องห้ามเท่าstrcat,strcpygotoใช้ได้ ส่วนstrcatและstrcpyที่ใช้ใน scope เดียวกันโดยไม่มีmallocขนาดพอดี ดูใกล้เคียงกับ code smell มากกว่าดีมาก ดีใจที่มี รายงานละเอียด แนบมาครบถ้วนเพื่ออธิบายว่าอะไรผิดพลาด