2 คะแนน โดย GN⁺ 2025-01-10 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • การแปลงอักขระแบบ Best-Fit ของ Windows จะแทนที่สตริง UTF-16 ด้วยอักขระที่ดูคล้ายกันระหว่างแปลงเป็น ANSI code page และพฤติกรรมนี้กลายเป็นพื้นผิวโจมตี WorstFit ที่นำไปสู่ Path Traversal, Argument Injection และ RCE
  • ปัญหาเกิดจากโครงสร้างที่ ANSI API, C/C++ runtime, startup code ที่คอมไพเลอร์แทรกให้ และการที่นักพัฒนาใช้ API อักขระแบบไม่ใช่ wide ซ้อนทับกัน โดยเส้นทางอย่าง GetCommandLineA, GetEnvironmentVariableA, getenv, int main() ได้รับผลกระทบ
  • CVE-2024-4577 ทำให้ U+00AD ถูกแปลงเป็น - ใน code page ภาษาจีน/ญี่ปุ่น จึงเลี่ยงแพตช์ของ PHP-CGI ได้ ส่วน Filename Smuggling ทำให้ ¥, และ fullwidth slash ถูกแปลงเป็น / หรือ \ จนเกิดความสับสนของพาธ
  • Argument Splitting สามารถใช้อักขระอย่าง fullwidth double quote หรือสัญลักษณ์ Yen/Won เพื่อสร้างอักขระสำหรับ parsing command line และฉีดอาร์กิวเมนต์เข้าไปในเครื่องมือ CLI เช่น wget.exe, tar.exe, openssl.exe, java.exe ได้ โดยการ escape อาร์กิวเมนต์ทั่วไปของ PHP, Python, Node.js, Rust เพียงอย่างเดียวป้องกันได้ยาก
  • แนวทางบรรเทาคือเปิดตัวเลือก UTF-8 ของ Windows หรือให้นักพัฒนาใช้ Wide Character API และเส้นทางอักขระแบบ wide เช่น _wgetcwd, _wgetenv, wmain() และจนกว่า Microsoft จะเปิด UTF-8 เป็นค่าเริ่มต้นใน Windows ทุกเอดิชัน ปัญหาคล้ายกันอาจเกิดซ้ำได้

โครงสร้าง encoding ของ Windows และ Best-Fit

  • Windows ในยุคแรกใช้ ANSI code page และ code page จะแตกต่างกันตามกลุ่มภาษา เช่น 1252, 932, 936, 949, 950
    • ACP (ANSI Code Page) ใช้กับแอปพลิเคชันและการตั้งค่าระบบส่วนใหญ่ เช่น งานไฟล์และตัวแปรสภาพแวดล้อม
    • OEMCP (Original Equipment Manufacturer Code Page) ใช้หลัก ๆ กับการสื่อสารกับอุปกรณ์ เช่น การอ่าน/เขียนคอนโซล
    • chcp แสดง OEMCP ไม่ใช่ ACP จึงไม่ใช่วิธีตรวจสอบ ACP ซึ่งเป็นประเด็นหลักของงานวิจัยนี้
  • Windows เปลี่ยนไปใช้ Unicode ในช่วงกลางทศวรรษ 1990 และ API หลักในปัจจุบันใช้ wide character บนพื้นฐาน UTF-16
    • API หลักอย่างระบบไฟล์ ข้อมูลระบบ และการประมวลผลข้อความ ถูกเปลี่ยนไปเป็น wide character API
    • แม้จะมีฟังก์ชัน UTF-8 อยู่ แต่ในภาษาส่วนใหญ่ไม่ได้เปิดเป็นค่าเริ่มต้น และบทความอธิบายว่ายังอยู่ในขั้น beta
  • ด้วยเหตุผลด้านความเข้ากันได้ย้อนหลัง Windows API จึงมีทั้งเวอร์ชัน ANSI และ Unicode
    • ANSI API มี suffix A เช่น GetEnvironmentVariableA
    • Unicode API มี suffix W เช่น GetEnvironmentVariableW
    • เมื่อมีการเรียก ANSI API Windows จะแปลงสตริง UTF-16 ภายในเป็นสตริง ANSI ด้วย RtlUnicodeStringToAnsiString หรือ WideCharToMultiByte

Best-Fit กลายเป็น WorstFit ได้อย่างไร

  • Best-Fit คือพฤติกรรมที่แมปอักขระ UTF-16 ไปเป็นอักขระที่ดูคล้ายหรือให้ความรู้สึกคล้ายกัน เมื่อไม่สามารถแทนอักขระนั้นใน ANSI code page ปลายทางได้อย่างถูกต้อง
    • ตัวอย่างเช่น ใน Windows-1252 U+221E จะถูกแมปเป็น 8
    • เมื่อ √π⁷≤∞ ผ่าน ANSI API อาจกลายเป็น "vp7=8"
  • การแมปทำงานแตกต่างกันไปในแต่ละ code page
    • ¥ U+00A5 ถูกแมปเป็น \ ใน code page ญี่ปุ่น 932
    • ใน code page ยุโรปกลาง 1250 จะถูกแมปเป็น Y
    • ใน code page อื่นส่วนใหญ่จะไม่เปลี่ยนแปลง
  • การแปลงแบบเดียวกันเกิดขึ้นไม่เพียงกับการเรียก Windows API โดยตรง แต่ยังเกิดในฟังก์ชัน CRT และเส้นทางของฟังก์ชัน main ทั่วไปด้วย
    • ฟังก์ชัน CRT แบบ non-wide เช่น getenv จะถูกใช้การแปลง Best-Fit
    • กรณีรับอาร์กิวเมนต์และตัวแปรสภาพแวดล้อมในรูปแบบ int main(int argc, char* argv[], char* envp[]) ก็มีการแปลงเข้ามาเกี่ยวข้องเช่นกัน
    • เพราะ startup code ของ CRT ที่คอมไพเลอร์แทรกให้ทำงานร่วมกับการใช้ ANSI Windows API
  • ตรวจสอบการแมปได้จาก Best-fit Mapping Grepper และ ข้อมูลแมปดิบ WindowsBestFit ของ Unicode.org

กรณี WorstFit แรก: PHP-CGI CVE-2024-4577

  • CVE-2024-4577 เป็นกรณีโจมตี WorstFit ที่ทำให้เซิร์ฟเวอร์ PHP-CGI ซึ่งตั้งค่าเป็น code page ภาษาจีน/ญี่ปุ่นถูกเจาะได้ด้วยคำขอเพียง ?%ADs
    • code page ที่ได้รับผลกระทบคือ 932 (ญี่ปุ่น), 936 (จีนตัวย่อ), 950 (จีนตัวเต็ม)
    • อักขระที่เป็นภัยคือ ­ U+00AD
  • ช่องโหว่ PHP-CGI ปี 2012 เป็น argument injection ที่เกิดจาก Apache ประมวลผล query string เป็นอาร์กิวเมนต์แรกของโปรแกรม CGI โดยอัตโนมัติ
    • หากเติม ?-s จะทำให้รั่วไหลซอร์สโค้ดของหน้าเว็บและทำ RCE ได้
    • แพตช์ของ PHP ใช้วิธีหยุด parsing อาร์กิวเมนต์เมื่อ query string เริ่มต้นด้วย dash
  • เพราะ Best-Fit ทำให้ soft hyphen U+00AD ถูกแปลงเป็น - ใน code page ภาษาจีน/ญี่ปุ่น จึงเลี่ยงแพตช์เดิมได้
    • ?%ADs อาจทำงานเหมือน -s ในมุมมองของ PHP-CGI
    • กรณีนี้ทำให้ทีมวิจัยได้พบคำว่า Best-Fit เป็นครั้งแรก

Filename Smuggling: ปัญหาที่อักขระพาธถูกแปลง

  • Filename Smuggling คือการโจมตีที่อักขระ Unicode ในชื่อไฟล์ถูกแปลงเป็น / หรือ \ บนเส้นทาง ANSI API จนอาจสร้าง path traversal ได้
    • API ที่เกี่ยวข้องได้แก่ GetCurrentDirectoryA, getcwd, FindFirstFileA, findfirst*, GetFullPathNameA เป็นต้น
    • code page ที่ได้รับผลกระทบคือ 874, 125x, 932 (JP), 949 (KR)
    • อักขระที่เป็นภัยคือ U+FF0F, U+FF3C, ¥ U+00A5 (JP), U+20A9 (KR)
  • d8.exe ซึ่งเป็น Developer Shell ของ Chrome V8 ใช้ GetCurrentDirectoryA() ในการดึงไดเรกทอรีทำงานปัจจุบันในการทำงานภายใน
    • หากสามารถสร้างไดเรกทอรีทำงานที่มีอักขระ Unicode อันตรายได้ เมื่อเข้าถึงผ่าน ANSI API จะถูกแปลงเป็น payload สำหรับ path traversal
    • ตัวอย่างเช่นสามารถเข้าถึง C:/windows/win.ini โดยไม่ตั้งใจได้
  • การใช้งาน Windows ของ Dir.getwd() ใน mruby พึ่งพาฟังก์ชัน ANSI CRT _getcwd()
    • ค่าที่ส่งคืนอาจถูกปนเปื้อน และนำไปสู่ Path Traversal ได้

Cuckoo Sandbox: จาก Path Traversal ไปจนถึง RCE

  • การเข้าถึงระบบไฟล์ Windows ของ Python เคยสามารถใช้ wide API หรือ ANSI API ได้ ขึ้นอยู่กับว่าสตริงเป็น wide หรือ narrow
    • หลังจาก PEP 529 การเข้ารหัสระบบไฟล์ Windows ถูกทำให้เป็นมาตรฐานเป็น UTF-8
    • Python 2 และ Python 3 ก่อนเวอร์ชัน 3.6 ยังคงอยู่ในสถานะที่เสี่ยงต่อการโจมตี WorstFit
  • Cuckoo Sandbox เป็นแพลตฟอร์มวิเคราะห์มัลแวร์แบบอัตโนมัติ และเวอร์ชันทางการล่าสุดยังพึ่งพา Python 2.7
    • Cuckoo ประกอบด้วย Cuckoo Host และ VM Cluster
    • ตัวอย่างที่อัปโหลดจะถูกเรียกใช้งานแบบแยก isolated ใน VM และซิงก์แพ็กเก็ตเครือข่าย ไฟล์ที่ถูก drop และ log ด้วยกลไกของตัวเอง
  • หากมัลแวร์สร้างไฟล์ drop ที่มีชื่อไฟล์เป็น Unicode อาจเกิด Path Traversal ในการประมวลผล path ของ Python บน Cuckoo Host ได้
    • ตัวอย่าง PoC สร้าง path AAAA\u00a5..\u00a5..\u00a5..\u00a5..\u00a5..\u00a5conf\u00a5cuckoo.conf
    • หลังการวิเคราะห์จบลง เมื่อผู้ใช้กดปุ่มดาวน์โหลดในเว็บอินเทอร์เฟซ จะ trigger การทำงานกับไฟล์ของ Python
    • Cuckoo Host อาจประมวลผล path ที่ผ่านการแปลงและมี ../ อยู่ ทำให้ส่งข้อมูลอ่อนไหวให้ผู้โจมตีได้
  • ผู้โจมตีสามารถดาวน์โหลด cuckoo.conf และรวบรวมข้อมูลอ่อนไหวที่จำเป็นต่อการคำนวณ Flask PIN เพื่อทำ RCE บน Sandbox Host ได้
    • วิดีโอสาธิตมีให้ดูที่ Video 11

Argument Splitting: Best-Fit ที่เปลี่ยนการ parse บรรทัดคำสั่ง

  • Argument Splitting คือการโจมตีที่สตริงบรรทัดคำสั่งถูกเปลี่ยนจนทำให้อาร์กิวเมนต์ถูกแยกออก ในเส้นทางของเอาต์พุต GetCommandLineA หรือ non-Unicode int main()
    • API และเส้นทางที่เกี่ยวข้องคือ GetCommandLineA, int main()
    • code page ที่ได้รับผลกระทบคือ 874, 125x, 932(JP), 949(KR)
    • อักขระที่เป็นภัยคือ U+FF02, U+FF3C, ¥ U+00A5(JP), U+20A9(KR)
  • ตัวอย่างโค้ด PHP ใช้ escapeshellarg() ครอบ URL อย่างปลอดภัยแล้วเรียก wget.exe -q แต่ด้วยอินพุต " --use-askpass=calc " สามารถทำให้ calc.exe รันได้
    • อินพุตเดียวกันนี้ แม้เปลี่ยนเป็น Node.js, Rust, Python ก็ไม่สามารถป้องกันได้
    • ใช้ได้แม้ในตัวอย่าง subprocess.run(["wget", "-q", ...]) ของ Python เวอร์ชันล่าสุด
  • Windows ส่งบรรทัดคำสั่งทั้งหมดให้โปรเซสใหม่เป็นสตริงเดียว และไฟล์ executable จะ parse เองโดยตรง
    • ไม่ใช่โครงสร้างที่ส่งอาร์เรย์ของอาร์กิวเมนต์เสมอเหมือนระบบตระกูล UNIX
    • API CreateProcess รับพารามิเตอร์ lpCommandLine โดยตรง
  • อักขระสำคัญในการ parse บรรทัดคำสั่งทั่วไปคือ whitespace และ tab, double quote, backslash
    • whitespace และ tab จะแยกอาร์กิวเมนต์เมื่อไม่ได้อยู่ใน quote mode
    • " ใช้สลับ quote mode
    • \ ใช้ escape double quote และ backslash ในลำดับบางแบบ
  • standard library ของภาษาส่วนใหญ่จะ escape อาร์กิวเมนต์ผู้ใช้ให้ตรงตามกฎนี้ แต่การ escape สิ้นสุดก่อนการแปลง Best-Fit
    • PHP escapeshellarg เปลี่ยน double quote เป็น whitespace ครอบอาร์กิวเมนต์ด้วย quote และจัดการ backslash
    • Python subprocess ใช้ list2cmdline เพื่อ escape ตามกฎการ parse บรรทัดคำสั่งของ Microsoft CRT
    • หลังจากนั้น หากในการแปลง ANSI U+FF02 ถูกเปลี่ยนเป็น " U+0022 โครงสร้างบรรทัดคำสั่งเดิมก็จะเปลี่ยนไป
  • โปรแกรมที่ใช้เพียง int main() ก็อาจมีช่องโหว่ได้
    • compiler สร้าง mainCRTStartup ไว้ใน binary และฟังก์ชันเริ่มต้นนี้เชื่อมกับ CRT library
    • หากภายใน CRT ใช้ ANSI API เพื่อดึงบรรทัดคำสั่งมา parse การแปลง Best-Fit จะเข้ามาเกี่ยวข้อง
    • ด้วยพฤติกรรมนี้ จึงยากที่จะป้องกันการโจมตีได้สมบูรณ์ด้วย standard library ของภาษาโปรแกรมบางภาษาเพียงอย่างเดียว

กรณีจริงของ Argument Splitting

  • ElFinder เป็นตัวจัดการไฟล์บนเว็บแบบโอเพนซอร์สที่ใช้แบ็กเอนด์ PHP และรองรับ Windows server รวมถึงการสร้าง/แตกไฟล์บีบอัดเป็นค่าเริ่มต้น
    • การจัดการ archive ถูก implement ด้วยการรัน shell command และอาร์กิวเมนต์ถูก escape ด้วย escapeshellarg
    • การจัดการรูปแบบ tar ใช้ tar.exe ที่มาพร้อม Windows
    • สามารถฉีดอาร์กิวเมนต์ --use-compress-program ด้วยชื่อไฟล์ tar เช่น aaa" "--use-compress-program=calc" "bbb.tar เพื่อรันคำสั่งตามอำเภอใจได้
    • เดโมอิงตาม Windows server ที่ตั้งค่าเป็นภาษาอังกฤษและ Code Page 1252 และสรุปว่าควรใช้ได้กับ code page 125x และ Code Page 874 ด้วย
    • วิดีโอสาธิตมีให้ดูที่ Video 12
  • กรณี plink.exe ที่ถูกปรับแก้ซึ่งใช้ใน TortoiseGit สามารถ trigger การรันโค้ดได้เมื่อใส่ URI อันตรายในอินพุต clone
    • ดูรายละเอียดได้ใน curated list
    • วิดีโอสาธิตคือ Video 13
  • RStudio รองรับการควบคุมเวอร์ชัน SVN และหากมีโปรเจกต์ SVN อยู่ในโฟลเดอร์ที่สร้างขึ้นอย่างประสงค์ร้าย ก็สามารถรันเครื่องคิดเลขได้ด้วยการคลิกเพียงครั้งเดียว
    • ดูรายละเอียดได้ใน curated list
    • วิดีโอสาธิตคือ Video 14
  • กรณี Microsoft Excel คือ CVE-2024-49026 ซึ่งผสาน Argument Splitting กับฟีเจอร์ “Open-With” ของ Windows
    • Windows เก็บ handler table แยกตามนามสกุลไฟล์ และสามารถตรวจสอบได้ด้วย ftype และ assoc
    • ชื่อไฟล์จะกลายเป็นส่วนหนึ่งของอาร์กิวเมนต์ของโปรแกรม handler จึงสามารถใช้ชื่อไฟล์เพื่อโจมตีได้
    • ทำให้เกิด argument injection ต่อ Excel.exe ด้วยชื่อไฟล์ที่เปลี่ยน dot, slash, backslash, double quote เป็นรูปแบบ fullwidth
    • ตัว Excel เองไม่มีอาร์กิวเมนต์ที่เหมาะต่อการ exploit เพิ่มเติม จึงใช้ NTLM Relay และ RBCD/ADCS ร่วมกันเพื่อทำ RCE
    • วิดีโอสาธิตมีให้ดูที่ Video 15

ความสับสนของ Environment Variable

  • Environment Variable Confusion เกิดขึ้นเมื่อ GetEnvironmentVariableA, GetEnvironmentStringsA, char *getenv() ส่งคืน environment variable ในเวอร์ชันที่ผ่านการแปลงแบบ Best-Fit
    • ไม่ได้ระบุ code page ที่ได้รับผลกระทบและอักขระที่เป็นภัยคุกคามอย่างเฉพาะเจาะจง
    • ในกรณีของ Apache HTTPd เกี่ยวข้องกับ 0x00-0xFF
  • เพื่อให้การโจมตีนี้เกิดขึ้นได้ environment variable ต้องถูกควบคุมโดยผู้ใช้ได้
    • รวมถึงกรณีที่โปรเซสแม่ส่งข้อมูลไปยังโปรเซสลูกที่สร้างขึ้น
    • ใน CGI ข้อมูลจำนวนมากของคำขอ HTTP เช่น query string และ HTTP header จะถูกส่งต่อเป็น environment variable
  • ตัวอย่างการหลบเลี่ยง WAF กล่าวถึงสถานการณ์ที่ CGI script ทำงานเหมือน routing service
    • การตั้งค่า Apache มี rule ที่ปฏิเสธ REQUEST_URI ที่มี /admin เพื่อป้องกันการเข้าถึงระยะไกลไปยัง /cgi.pl/admin
    • เนื่องจากพฤติกรรม WorstFit ของ Windows Perl หากเปลี่ยนบางส่วนของ admin ให้เป็น Best-Fit equivalent ก็สามารถหลบเลี่ยงได้
    • ใน Code Page 1250 อักขระ à U+00E0 จะถูกแปลงเป็น a ระหว่างการแปลง ANSI
    • คำขอ /cgi.pl/%E0dmin จะถูกมองเป็น path อื่นใน rule ฝั่งเซิร์ฟเวอร์ แต่เมื่อ Perl CGI script อ่าน PATH_INFO ผ่าน ANSI API จะถูกประมวลผลเป็น /admin
  • ใน PHP-CGI บน Windows พบ file existence oracle และ LFI ที่อาจเกิดขึ้นได้ในบางการตั้งค่า
    • สาเหตุคือวิธีจัดการ PATH_INFO และ environment variable อื่น ๆ ที่เกี่ยวข้องกับ path
    • คำขอ /index.php/foo/bar จะถูกส่งต่อเป็น environment variable เช่น REDIRECT_URL, REQUEST_URI, PATH_INFO, PATH_TRANSLATED ตามมุมมองของ Apache
    • ด้วยข้อมูลเพียงเท่านี้ ยากที่จะแยกขอบเขตระหว่างชื่อไฟล์ PHP กับ PATH_INFO เพิ่มเติมได้อย่างชัดเจน และ php-cgi.exe จะเป็นผู้ตีความสิ่งนี้
  • หากใช้ ¥ ใน Japanese code page การตีความ path ของเว็บเซิร์ฟเวอร์กับ PHP-CGI จะแตกต่างกัน
    • เว็บเซิร์ฟเวอร์จะประมวลผล /..¥..¥windows/win.ini/foo ทั้งหมดเป็น PATH_INFO เพิ่มเติม
    • PHP-CGI ได้รับค่าที่ถูกแปลงมา เช่น REQUEST_URI=/index.php/..\..\windows/win.ini/foo และเกิดความสับสนในขั้นตอนแยกไฟล์ PHP จริงออกจาก PATH_INFO
    • ใน Apache สามารถทำ file existence oracle ได้จากความแตกต่างของ response ระหว่างไฟล์ที่ไม่มีอยู่กับไฟล์ที่มีอยู่
    • ใน IIS หากตั้งค่า directive doc_root ไว้ สามารถทำ LFI เพื่อ include และอ่าน C:\Windows\win.ini ผ่าน path อย่าง /index.php/..¥..¥..¥windows/win.ini/ ได้
    • หากไฟล์ที่ถูก include สามารถรันได้หรือมีโค้ดที่ผู้ใช้ควบคุมได้ อาจนำไปสู่ RCE ได้ แต่ scenario ดังกล่าวถูกจัดว่าใกล้เคียงกับ bug ที่พบได้ยากในแอปพลิเคชันจริง

ความยากลำบากในกระบวนการเปิดเผยและแก้ไข

  • ทีมวิจัยรายงานปัญหาหลายรายการในภาษาโปรแกรม, โครงการโอเพนซอร์ส และโปรแกรม CLI ที่มากับ Windows ไปยัง upstream maintainer แต่ละราย
    • ประเด็นที่ถกเถียงกันมากที่สุดเกิดขึ้นใน Argument Splitting
    • vendor บางรายมองว่าการส่ง user input เข้า command line เองเป็นช่องโหว่
  • ความไม่ชัดเจนเรื่องผู้รับผิดชอบก็เป็นปัญหาเช่นกัน
    • โค้ดที่เป็นปัญหาครอบคลุมทั้ง mainCRTStartup() ที่ถูกแทรกอัตโนมัติระหว่างการ compile และการเรียก ANSI API ภายใน MSVCRT/UCRT
    • แยกได้ยากว่าเป็นปัญหาที่ developer ไม่ใช้ wmain() หรือเป็นปัญหาที่ CRT แบ่ง command line ผิดและส่ง argument ที่ผิดไปยัง main()
    • บางโครงการให้เฉพาะ source code ส่วน Windows prebuilt executable นั้นเผยแพร่โดยอาสาสมัครบุคคลที่สามบนอินเทอร์เน็ต
  • การแก้ไขไม่ใช่แค่เปลี่ยน main() เป็นเวอร์ชัน wide-character เท่านั้น
    • เมื่อ function signature เปลี่ยน ต้องเขียนนิยามตัวแปรและตรรกะการ parse argument ใหม่จากฐาน char * เป็น wchar_t *
    • กระบวนการนี้ยุ่งยากและเกิดข้อผิดพลาดได้ง่าย
  • Curl ตอบว่าเป็นฟีเจอร์ของ Windows และไม่มีแผนแก้ไข ส่วน Curl ที่ Microsoft port มาได้แก้ entry เป็น wmain() แล้ว ทำให้ curl.exe ที่มากับ Windows ไม่ได้รับผลกระทบ
    • binary official build ของ Curl ได้รับผลกระทบจากการโจมตี Argument Splitting
    • รายงานฉบับเต็มเผยแพร่ที่ HackerOne
  • OpenSSL สามารถประมวลผล argument ในรูปแบบ wide character ได้ด้วย environment variable OPENSSL_WIN32_UTF8
    • เดิมมีจุดประสงค์เพื่อแก้ปัญหาการแสดงผล UTF-8 ใน UI แต่ก็ช่วยบรรเทาการโจมตี Argument Splitting ด้วย
    • ในการใช้งาน OpenSSL ทั่วไป developer มักไม่รู้ว่าต้องตั้งค่า environment variable นี้ และสามารถเกิดการรันโค้ดตามอำเภอใจผ่าน argument -engine ได้
  • distribution ทางการของ Perl ไม่ได้ให้ Windows prebuilt executable และมักใช้ installer จาก third-party เช่น Strawberry Perl และ ActiveState Perl
    • ทั้งสอง distribution ได้รับผลกระทบจากการโจมตี Argument Splitting
    • หลังหารือกับ Perl maintainer ได้ข้อสรุปว่า “ใกล้เคียงกับ bug ของ Microsoft มากกว่า bug ของ Perl” และปัจจุบันยังไม่ได้รับการแก้ไข
  • มีรายงานถึง Microsoft ผ่าน MSRC 3 รายการ และทั้งหมดถูกปฏิเสธในตอนแรกเพราะไม่ถึงเกณฑ์ความรุนแรง
    • หลังเปิดเคสซ้ำหลายครั้ง มีเพียงกรณี Excel ที่ได้รับการยอมรับหลังลองครั้งที่สาม
    • กรณีอื่น ๆ ยังไม่ได้รับการแก้ไขจนถึงปัจจุบัน
    • MSRC ตอบว่าเป็นการพึ่งพาช่องโหว่ที่แอปพลิเคชันแยกต่างหากนำ input ที่ไม่น่าเชื่อถือใส่ใน command line แล้วรัน และ technique ที่ทำให้การโจมตีนี้เป็นไปได้เองไม่เข้าเงื่อนไขของช่องโหว่
  • ได้ขอความช่วยเหลือจาก CERT/CC ด้วย และไม่กี่เดือนต่อมา Microsoft ได้เพิ่ม คำเตือนด้านความปลอดภัย ในเอกสาร GetCommandLineA
    • คำเตือนถูกเพิ่มเฉพาะใน GetCommandLineA เท่านั้น และยังมี ANSI API อื่น ๆ ที่ต้องระวังเหลืออยู่

เป้าหมายที่ได้รับรายงานว่ามีผลกระทบและสถานะ

  • รายการที่ได้รับการยืนยันและรายงานระหว่างกระบวนการเปิดเผยมีดังนี้
    • 2024/05/07: PHP php-cgi.exeCVE-2024-4577
    • 2024/06/13: Curl Official BuildWon’t Fix
    • 2024/06/13: Apache Subversion svn.exeCVE-2024-45720
    • 2024/06/16: Microsoft Tar tar.exe — Won’t Fix
    • 2024/06/19: Microsoft Excel excel.exeCVE-2024-49026
    • 2024/06/19: Microsoft PhoneBook rasphone.exe — Won’t Fix
    • 2024/06/19: Oracle Java java.exe — Pending Fix
    • 2024/06/19: Perl perl.exe — Won’t Fix
    • 2024/07/15: Perforce p4.exeCVE-2024-8067
    • 2024/08/05: PostgreSQL psql.exe — Won’t Fix
    • 2024/08/08: Putty plink.exe — Fixed
    • 2024/08/19: OpenSSL openssl.exe — Other
    • 2024/08/19: wkhtmltopdf wkhtmltopdf.exe — EOL
    • 2024/08/19: GNU Wget — No Reply

มาตรการบรรเทาและพื้นผิวการโจมตีที่ยังเหลืออยู่

  • การโจมตี WorstFit เป็นปัญหาในระดับระบบปฏิบัติการ ดังนั้นจนกว่า Microsoft จะเปิดใช้ UTF-8 เป็นค่าเริ่มต้นใน Windows ทุกเอดิชัน ปัญหาลักษณะเดียวกันอาจยังกลับมาเกิดซ้ำได้
  • สิ่งที่ผู้ใช้ทำได้คือ ตรวจสอบและเปิดใช้ ตัวเลือก UTF-8 ของ Windows
    • ฟีเจอร์นี้ยังแสดงว่าอยู่ในขั้น beta และยังไม่แน่ชัดว่าจะมีผลข้างเคียงหรือไม่
  • นักพัฒนาควรใช้ Wide Character API ให้มากที่สุดเท่าที่เป็นไปได้
    • CRT ก็มีเวอร์ชัน wide character เช่น _wgetcwd, _wgetenv ให้ใช้งานเช่นกัน
    • หากยังใช้เส้นทางแบบ non-wide ต่อไป การทำงานภายในอาจเรียก ANSI API และอาจเปิดให้ถูกโจมตีแบบ WorstFit ได้
  • เนื่องจากความเข้ากันได้ย้อนหลังของ Windows อาจยังมีจุดอื่นที่ซ่อนการใช้ ANSI API อยู่
    • ตัวอย่างเช่น การ query Windows Registry อย่าง RegQueryValueA อาจได้รับผลกระทบ แต่จำเป็นต้องค้นหาสถานการณ์ที่เป็นช่องโหว่
    • ทีมวิจัยยังสังเกตเห็นพฤติกรรม Best-Fit ใน Active Directory ด้วย

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

 
GN⁺ 2025-01-10
ความคิดเห็นจาก Hacker News
  • นี่เป็นปัญหาที่ค่อนข้างยุ่งยาก การแมปโค้ดแบบ “best fit” ของ Microsoft เป็นตัวแมปที่เปลี่ยน Unicode ช่วงกว้างให้เป็น ASCII ซึ่งเปิดเผยต่อสาธารณะ แต่โดยพื้นฐานแล้วเป็นการแมปแบบ “ตามความรู้สึก” และฝังอยู่ทั่วทั้งระบบ
    ตัวแมปนี้ถูกลิงก์เป็นค่าเริ่มต้นในที่จำนวนมาก และจากมุมมองของ Microsoft เรื่องความเข้ากันได้ย้อนหลัง ก็ดูเหมือนว่าจะต้องรวมไว้ต่อไป การโจมตีส่วนใหญ่มักเกิดจาก code point แปลก ๆ ที่ถูกแมป “ตามความรู้สึก” ไปเป็นสแลช ไฮเฟน หรือเครื่องหมายอัญประกาศ ภายในภาษาโปรแกรมสมัยใหม่จะถูกตรวจสอบเป็น Unicode ที่ถูกต้อง แต่เมื่อส่งต่อไปยังคำสั่งเชลล์หรือ Win32 API หลังจากส่งมอบการควบคุมไปแล้ว มันจะถูก ย่อแปลงในอีกแบบหนึ่ง อย่างที่ผู้ดูแล curl พูดไว้ ตรงนี้ “curl เป็นเหยื่อ” แต่คำถามคือผู้ร้ายคือใคร ถ้าเซิร์ฟเวอร์บดขยี้อินพุตผู้ใช้ไม่เหมือนกันระหว่างตอนตรวจสอบกับตอนส่งให้ไลบรารีระบบ สุดท้ายก็จะเกิดปัญหา ตัวเลือกให้ปิดการแปลงแบบ best fit ฝั่ง Win32 อาจเป็นทางแก้ได้ แต่ผมไม่ใช่ผู้เชี่ยวชาญ Windows จึงเป็นแค่การคาดเดา ถึงทำแบบนั้น ก็ยังต้องมีปฏิสัมพันธ์กับ API ทางการหรือซอฟต์แวร์ที่ยังไม่ได้ปิดอยู่ดี

    • วิธี opt-out คือใช้ Unicode Windows API กล่าวคือใช้ฟังก์ชันที่ลงท้ายด้วย "w" ไม่ใช่ "a" วิธีนี้ถ้าใส่คำนำหน้า "\\?\" หรือกำหนด manifest ให้ถูกต้อง ก็จะแก้ปัญหา path ยาวเกิน 260 ตัวอักษรไปพร้อมกันด้วย ทำได้มาตั้งแต่ Windows XP และถูกแนะนำมาโดยตลอด
      ผมไม่ค่อยเข้าใจว่าทำไม API ที่ไม่ใช่ Unicode ยังถูกใช้กันแพร่หลายขนาดนี้ นึกภาพไม่ออกว่าเป็นเพราะอยากรองรับ Windows 98 หรือ Windows 2000
    • Windows มี ไฟล์ manifest มาตั้งแต่ Windows XP เพื่อปิดพฤติกรรม legacy ถ้าจำไม่ผิด หากไม่มี manifest แม้แต่ GetWindowsVersion ก็จะไม่คืนค่าเวอร์ชันปัจจุบัน การเพิ่ม opt-out เข้าไปในนั้น แล้วสักวันทำให้เป็นค่าเริ่มต้นของ Visual Studio ดูไม่น่าจะยากนัก
      อีกอย่างที่ต้องมีคือ linting แบบหนึ่ง โดยปกติแล้วแอปพลิเคชันสมัยใหม่ไม่มีเหตุผลต้องเรียกฟังก์ชัน ANSI WinAPI อาจมีแนวทางตั้งค่า locale เป็น UTF-8 แล้วใช้เฉพาะฟังก์ชัน 8 บิต แต่ไม่รู้ว่าทำงานได้ดีแค่ไหน เท่าที่รู้ยังมีการตั้งค่าและ header บางอย่างที่ทำให้ argv, printf, std::cout ทำงานเป็น UTF-8 และใช้เฉพาะฟังก์ชันแปลง UTF-8/UTF-16 สำหรับ WinAPI โดยไม่ผ่านการแปลงประหลาด ๆ Microsoft ควรจัดทำเอกสารขั้นตอนแบบนี้ไว้ในที่เดียว
    • ไม่ว่าจะเป็นช่องโหว่ความปลอดภัยหรือไม่ ถ้า curl จัดการ อาร์กิวเมนต์ Unicode บน Windows ได้ไม่ถูกต้อง นั่นก็เป็นบั๊กของ curl ด้วย
    • วิธีแมป code point เป็นตัวอักษรแบบหลวม ๆ เป็นสิ่งที่ทำให้รู้สึกขัดใจเสมอใน Unicode
  • เรื่องนี้คาดเดาได้ในระดับหนึ่ง แต่ถึงผมจะเคยทำงานพัฒนา Windows และแฮ็ก Wine API อยู่ราว 10 ปีในช่วงที่ความสับสนระหว่าง W/A เกิดขึ้น มันก็ยังเป็นเรื่องใหม่สำหรับผม
    Windows เหมือนเกมไพ่ Munchkin ตรงที่เมื่อฟีเจอร์หลายอย่างบังเอิญเข้ามาประกอบกัน ก็อาจรวมตัวเป็น exploit ที่สุ่มและทรงพลังอย่างไม่น่าเชื่อได้ เป็นเรื่องน่ายินดีที่กำลังเปลี่ยนซับซิสเต็ม ANSI ให้เป็น UTF-8 และในทางทฤษฎีก็น่าจะบรรเทาปัญหาแบบนี้ได้มาก สงสัยเหมือนกันว่าทีม Rust จะต้องแก้ไข อีกครั้ง ใน API สำหรับสร้างโปรเซสหรือไม่

    • โดยพื้นฐานแล้วไลบรารีมาตรฐานของ Rust แทบไม่ใช้ ANSI API บทความไม่ได้แสดงการโจมตีที่ใช้ได้กับ Rust และถ้ามีการโจมตีแบบนั้นก็ควรรายงานอย่างยิ่ง
      แน่นอนว่า Rust ไม่สามารถควบคุมสิ่งที่เกิดขึ้นข้ามขอบเขตโปรเซสได้ ถ้าแอปพลิเคชันที่ Rust รันใช้ ANSI API ปัญหาก็จะเกิดฝั่งนั้น แต่นั่นเป็นความรับผิดชอบของแอปพลิเคชันนั้น
  • “การค่อย ๆ เลิกใช้ ANSI และแนะนำให้ใช้ Wide Character API” ถ้าจำไม่ผิด เป็นจุดยืนทางการของ Microsoft มาตั้งแต่ NT 3.5
    น่าเสียดายที่อุปสรรคใหญ่ข้อหนึ่งคือวิธีที่ไลบรารี runtime C/C++ ของ Microsoft อย่าง msvcrt.dll ถูก implement ฟังก์ชัน wide ที่ไม่ใช่มาตรฐานอย่าง _wfopen(), _wgetenv() จะใช้ฟังก์ชัน W ของ Win API ภายใน แต่ฟังก์ชัน narrow มาตรฐานอย่าง fopen(), getenv() กลับใช้ฟังก์ชัน A โดยตรง แทนที่จะแปลงไปใช้เวอร์ชัน wide และฟังก์ชัน A มักไม่รายงานความล้มเหลวในการแปลง Unicode แต่กลบด้วยวิธี best-fit คนที่พอร์ตซอฟต์แวร์ที่เขียนด้วย C มายัง Windows มักไม่อยากเปลี่ยนการใช้ฟังก์ชันมาตรฐานทั้งหมดไปเป็นฟังก์ชันของ Microsoft ที่พกพาไม่ได้ จากจุดนั้นก็แทบเท่ากับต้องเขียนใหม่ทั้งระบบ

    • ความประทับใจที่ได้จากการอ่านเอกสาร Microsoft ในช่วง 2 ปีหลังกลับตรงกันข้าม คือให้ตั้ง activeCodePage เป็น UTF-8 ใน manifest ของแอปพลิเคชัน แล้วใช้เฉพาะฟังก์ชัน “ANSI”
    • ในโค้ดที่ต้องพกพาได้ เมื่อเป็น build สำหรับ Windows จะ #define ฟังก์ชันมาตรฐานอย่าง main และ fopen ให้เป็นฟังก์ชันคู่ wide
      แบบนี้จะใช้ char* และ string literal แบบไม่ตกแต่งตรง ๆ ไม่ได้ จึงกำหนดชนิด tchar ซึ่งเป็น char บน Linux และเป็น wchar_t บน Windows รวมถึงมาโคร _T() สำหรับ string literal โดยรวมแล้วมันทำงานได้ดีโดยไม่ต้องคิดมาก
    • สิ่งที่น่ารำคาญจริง ๆ ในทุกวันนี้คือเมื่อค้นหา Win32 API บน Google มักจะได้ตัวแปร -A ขึ้นมาก่อนเสมอ ไม่ใช่ตัวแปร -W ไม่รู้ว่าใน robots.txt มีอะไรแปลก ๆ หรือเปล่า แต่ API ที่แนะนำให้ใช้ตัวแปร -W สำหรับโค้ดใหม่ กลับคืนค่า API legacy เป็นค่าเริ่มต้นนี่มันแปลก
    • runtime C/C++ ของ Microsoft อย่าง msvcrt.dll ถูกแทนที่ด้วย Universal C Runtime (UCRT)[1] แล้ว และ UCRT ก็สอดคล้องกับ C99
    • Windows ควรมี API ที่ปฏิบัติกับ path name เป็น ลำดับไบต์ ไปเลย โดยไม่ต้องจัดการ encoding แบบโง่ ๆ แบบนี้ ตอนนำ UNC path เข้ามาก็น่าจะทำแบบนั้นได้
  • มีสองวิธีในการบังคับให้ code page “Ansi” เป็น UTF-8 จริง ๆ ในแอปพลิเคชันที่เขียนเองหรือ EXE ที่แพตช์แล้ว
    วิธีหนึ่งคือใช้ไฟล์ manifest ซึ่งทำงานได้ตั้งแต่บาง build ของ Windows 10 เป็นต้นมา สามารถใช้กับ EXE ใด ๆ หลัง build ได้ด้วย จึงยัดการรองรับ UTF-8 เข้าไปในโปรแกรมได้ เหมาะเป็นพิเศษกับโปรแกรมโหมดคอนโซล อีกวิธีคือใช้แฮ็กแบบที่เครื่องมือแนว “App Locale” ใช้ วิธีหนึ่งเกี่ยวข้องกับการเรียกฟังก์ชันที่ไม่ได้มีเอกสารของ NTDLL ไม่แน่ใจว่าต้องใช้ฟังก์ชันใดกันแน่ แต่ RtlInitNlsTables และ RtlResetRtlTranslations อาจเกี่ยวข้อง

  • ไม่แน่ใจว่า Microsoft จะเปิดใช้ UTF-8 เป็นค่าเริ่มต้นใน Windows ทุกเอดิชันหรือไม่ เพราะมีแอปพลิเคชันเก่าจำนวนมากที่สมมติว่าใช้ code page เฉพาะ หรือ 1 ไบต์ต่ออักขระ ซึ่งอาจพังได้
    ที่ละเอียดอ่อนกว่านั้นคือมีแอปพลิเคชันที่นำบัฟเฟอร์เดิมกลับมาใช้ซ้ำ โดยสมมติว่าเมื่อแปลงจาก wide character เป็น ANSI แล้วจำนวนไบต์จะไม่เพิ่มขึ้น ใน UTF-8 ไม่เป็นเช่นนั้น และใน code page เดิมส่วนใหญ่โดยทั่วไปสมมตินี้เคยถูกต้อง จึงอาจทำให้เกิดช่องโหว่ใหม่ได้ น่าจะพังน้อยกว่ามากถ้าเอา Best-Fit logic ออกจาก Win32 xxxA API แล้วแทนที่อักขระที่แมปไม่ได้ด้วยอักขระอย่าง x ที่ไม่มีความหมายเมตาร่วมกัน

    • ตัวอย่างของแอปพลิเคชันแบบนั้นคือ Adobe After Effects[0] อย่างน้อยเมื่อก่อนก็เป็นแบบนั้น และตอนนี้ไม่ได้ใช้ Windows แล้ว
      [0] https://tambre.ee/blog/adobe_after_effects_windows_utf-8/
    • ถ้ายังไม่มี ก็น่าจะนำเวอร์ชันของ OS API มาใช้ เพื่อให้แอปใหม่/แอปที่อัปเดตซึ่ง target API เวอร์ชันใหม่หรือ SDK ใหม่ สมมติว่าเป็น UTF-8 โดยค่าเริ่มต้นได้ ส่วนที่ต่ำกว่า API เวอร์ชันหนึ่งก็ emulation เป็นโหมด legacy ไป Windows มีแนวคิดเรื่อง shim ที่เลียนแบบพฤติกรรมของ Windows หลายเวอร์ชันอยู่แล้ว
    • แม้แต่ Windows ก่อนยุค UTF-8 ก็มีปัญหาอยู่แล้วว่าเมื่อเปลี่ยน code page เริ่มต้น แอปก็ทำงานแปลก ๆ ดังนั้นการให้ตัวเลือก UTF-8 แก่ผู้ใช้จึงสมเหตุสมผล
      เมื่อดูปัญหาที่ Best-Fit mapping ก่อขึ้น การทำให้เป็นค่าเริ่มต้นก็สมเหตุสมผลเช่นกัน แต่ Microsoft ควรช่วยให้ผู้ใช้หาวิธีรันโค้ดเก่าได้ง่าย วิธีที่สมเหตุสมผลน้อยกว่าคือเอา mapping ทั้งหมดที่ไปยังอักขระ ASCII “พิเศษ” ออกจาก Best-Fit mapping แต่ก็ไม่ช่วยแอปที่ลิงก์ CRT แบบ static และไม่ได้แก้ช่องโหว่ด้วย จึงไม่ใช่ทางออกที่ดี บางครั้งช่องโหว่ด้านความปลอดภัยก็เป็นแรงผลักดันให้เกิดการ ทำลายความเข้ากันได้ย้อนหลัง
  • Microsoft รู้เรื่องปัญหานี้มาอย่างน้อย 1 ปีแล้ว เพราะได้ออกกฎวิเคราะห์โค้ดพิเศษชื่อ CA2101[1] ซึ่งระบุชัดว่าไม่แนะนำให้ใช้ best-fit mapping
    คำอธิบายกฎพูดถึงช่องโหว่ด้านความปลอดภัย แต่จงใจทำให้รายละเอียดคลุมเครือ
    [1] https://learn.microsoft.com/en-us/dotnet/fundamentals/code-a...

  • ไม่จำเป็นต้องเปลี่ยนทุกอย่างจาก char * เป็น wchar * แค่แปลง wide character ที่ได้รับมาเป็น UTF-8 หรือถ้าต้องการรองรับลำดับที่ไม่ถูกต้องอย่าง surrogate ที่ไม่มีคู่ด้วย ก็แปลงเป็นอะไรทำนอง WTF-8 ของ Rust แล้วใช้ char ต่อไปได้
    แน่นอนว่าต้องระวังไม่ให้สตริง ANSI หรือ OEMCP ปนกับสตริง UTF-8 แต่ถ้าใช้แค่ UTF-8 ก็ง่าย แนวทางนี้เป็นวิธีที่เว็บไซต์คลาสสิก https://utf8everywhere.org/ แนะนำ

  • บนคอมพิวเตอร์ Windows ส่วนตัว ผมเปิดโหมด UTF-8 ไว้มาหลายปีแล้ว เลยบังเอิญหลีกเลี่ยงบั๊กนี้ได้ เป็นการตั้งค่าที่อยู่ท้ายบทความ
    ผมเปิดไว้เพราะเกมต่างประเทศเก่า ๆ แสดงตัวอักษรเพี้ยน และแม้จะมีป้ายว่า “Beta” แต่ก็ไม่รู้สึกว่ามีบั๊กหรือผลข้างเคียง

    • น่าสนใจ แต่ในกรณีของผม ช่องติ๊กนั้นแทบไม่ได้ทำอะไรนอกจากทำให้แอปสุ่ม ๆ จำนวนมาก แครช ดูเหมือนว่าการทำงานได้ดีหรือไม่นั้นขึ้นอยู่กับว่า code page เริ่มต้นของผู้ใช้เป็นอะไรตอนที่ปิดอยู่
    • เพิ่งเปิดตัวเลือก “Beta: Use Unicode UTF-8 for worldwide language support” ไป น่าจะน่าสนใจว่าจะมีแอปพังมากแค่ไหน
  • ผมสงสัยว่า checkbox เบต้านั้นเหมือนกับการตั้ง ActiveCodePage เป็น UTF-8 ใน manifest หรือไม่ แต่ดูจากเอกสาร[0] แล้วระบุชัดว่า GDI ไม่ทำตาม code page รายโปรเซส แต่ทำตาม code page ส่วนกลางเพียงค่าเดียว ที่ checkbox ตั้งไว้เท่านั้น
    น่าเสียดายอยู่บ้างที่ไม่สามารถ opt-in ให้ *A API ในแอปของตัวเองเป็น UTF-8 ได้อย่างสมบูรณ์ ถึงอย่างนั้นสำหรับปัญหาที่บทความเน้น ผมก็ยังมองว่ามันเป็น workaround หรือมาตรการ defense-in-depth ที่ใช้ได้
    [0] https://learn.microsoft.com/en-us/windows/apps/design/global...

  • ให้ตายเถอะ ผมรู้ว่า Windows API มีการแปลงแบบ best-fit ประเภทนั้นให้ใช้ แต่ไม่รู้ว่ามันเป็นพฤติกรรมเริ่มต้นของฟังก์ชัน ANSI หลายตัวใน code page เริ่มต้นของผมคือ 949[1]
    มาถึงจุดนี้ก็ควรแบนไปเลยเหมือน gets [1] ผมรู้ว่ามี UTF-8 code page 65001 อยู่ มันเคยอยู่ในระดับที่แทบใช้จริงไม่ได้มานาน และตอนนี้ก็ยังมีปัญหาความเข้ากันได้อยู่