1 คะแนน โดย GN⁺ 2024-04-05 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • -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) และ nonnull
    • char 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))
  • งานในการรันตัววิเคราะห์กับเคอร์เนลเพื่อค้นหาช่องโหว่และแก้ false positive ของตัววิเคราะห์ยังคงดำเนินต่อไป

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

 
GN⁺ 2024-04-05
ความคิดเห็นจาก Hacker News
  • สำหรับผม fanalyzer เป็นหนึ่งในฟีเจอร์เด็ดของ GCC เมื่อเทียบกับ Clang มันอธิบายข้อผิดพลาดให้ ทำให้การเขียนโปรแกรม C ง่ายขึ้นมาก และข้อความ error ก็เริ่มให้ความรู้สึกคล้าย Rust ในแง่ที่เป็นมิตรกับนักพัฒนา

    • ผมรู้ว่า Rust ได้รับความสนใจมาก โดยเฉพาะใน HN เพราะ memory safety และ abstraction ที่ดี แต่ก็สงสัยว่าความนิยมของ Rust มีสัดส่วนมากแค่ไหนที่มาจาก ข้อความ error
      เหตุผลใหญ่ที่สุดที่คนเลิกเรียนเทคโนโลยีมักเป็น error ที่ชวนหงุดหงิดหรือไม่ชัดเจน อาจจะออกนอกประเด็นไปนิด แต่หมายความว่าผมชอบ C และคิดว่ามันจะดียิ่งขึ้นถ้ามีข้อความ error ระดับเดียวกับ Rust
    • Clang ก็มีเครื่องมือคล้ายกันชื่อ Clang Static Analyzer: https://clang-analyzer.llvm.org/
    • ผมเจอประสบการณ์ตรงกันข้ามเลย Clang มักแสดงข้อความ error ที่ดีกว่า GCC มาก และการ implement warning หรือ error บางอย่างก็จับเคสได้มากกว่า ส่วน clang-tidy ก็ทำ static analysis ได้ดีกว่ามาก
    • นึกถึงหนึ่งในเหตุผลที่ผมเคยเกลียด C++ มาก แทนที่จะเป็น error: missing semicolon กลับมีข้อความ error เกี่ยวกับ template instantiation ไหลออกมาเกิน 1,000 บรรทัด
    • เรื่องนี้ฟังดูน่าประหลาดใจทีเดียว อยากรู้ว่า analyzer ของ GCC จับอะไรเพิ่มได้ที่ Clang Static Analyzer ยังไม่รายงาน
      ผมลองใช้ 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

    • อ้างอิงเพิ่มเติม ผม implement SARIF output ใน GCC 13 แล้ว และสามารถดูผ่านปลั๊กอินใน VS Code ได้ เป็นต้น แต่จะไม่มี ASCII art รวมอยู่ด้วย
      ดูตัวอย่าง 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 ก็ตาม
    • ใช่ แต่เมื่อมีการระบุขนาด buffer มาแล้ว การเขียนทับปลาย buffer ของผู้เรียกก็ไม่สมเหตุสมผลเช่นกัน ดังนั้นจะส่งค่าที่ยาวกว่า 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 ระดับภาษา ไม่ถูกทำลาย ถ้ากรณีแบบนั้นพัง แปลว่ามีปัญหาใหญ่กว่านั้นมากอยู่แล้ว แม้จะเป็นกรณีที่หายาก แต่ก็อาจโต้แย้งได้ว่าทางเลือกที่ในนามปลอดภัยกว่าอาจมีประสิทธิภาพต่ำลงเล็กน้อยโดยไม่ได้ประโยชน์เพิ่ม
    • ตราบใดที่ใช้ให้เทียบเท่าทางตรรกะกับโครงสร้าง structured programming ที่ไม่มีใน C การใช้ goto บางแบบก็ยังเป็น idiom ใน C อยู่ ต้องระวังแน่นอน แต่ยังไงมันก็คือ C จึงเป็นไปได้
      แต่ผมไม่ชอบ longjmp เลยจริง ๆ
    • การใช้ goto แบบเรียบง่ายไม่มีปัญหา ในทางกลับกัน ตระกูล strxcpy นั้นเละเทะโดยสิ้นเชิง และไม่ควรใช้ไม่ว่าด้วยเหตุผลใดก็ตาม การที่มันถูกใช้ใน kernel เป็นเรื่องน่ากลัว
      ฟังก์ชันเหล่านั้นกับความพยายามทั้งหมดที่ “จะแก้” มันแต่ล้มเหลว ควรถูกยิงทิ้งจากวงโคจรไปเลย
    • goto ถ้าใช้ด้วยความระมัดระวังก็โอเค strcpy กับ strcat ก็ “โอเค” ในความหมายที่ว่าคุณรู้ว่าโค้ดถูกต้อง และถ้ามันผิดก็จะเกิดปัญหาใหญ่ น่าเสียดายที่คำอธิบายนี้ใช้ได้กับ C ส่วนใหญ่
    • ผมไม่คิดว่า goto เป็นสิ่งต้องห้ามเท่า strcat, strcpy goto ใช้ได้ ส่วน strcat และ strcpy ที่ใช้ใน scope เดียวกันโดยไม่มี malloc ขนาดพอดี ดูใกล้เคียงกับ code smell มากกว่า
  • ดีมาก ดีใจที่มี รายงานละเอียด แนบมาครบถ้วนเพื่ออธิบายว่าอะไรผิดพลาด