Xz/liblzma: Giải thích lớp làm rối ở giai đoạn Bash
(gynvael.coldwind.pl)- Backdoor xz/liblzma không chỉ là vấn đề của payload nhị phân cuối cùng; giai đoạn Bash được chạy trong lúc build cũng bị che giấu bằng nhiều lớp trích xuất và giải mã, khiến việc phân tích rất khó khăn
- Các phiên bản bị ảnh hưởng là xz/liblzma 5.6.0 và 5.6.1; script bị làm rối và payload nhị phân được chứa trong hai tệp trông giống như tệp kiểm thử
- Stage 0 bắt đầu từ
m4/build-to-host.m4, khôi phục một luồng xz trông như đã hỏng, rồi dùngxz -dđể lấy ra và chạy script Stage 1 - Stage 1 cắt dữ liệu từ
good-large_compressed.lzma, thay thế rồi chạy **script Bash Stage 2`; trong 5.6.1 có thêm đoạn mã kiểm tra việc đang chạy trên Linux tới 5 lần - Stage 2 dùng file carving, mật mã thay thế và giải mã biến thể RC4 dựa trên AWK để chèn tệp
.ovào quá trình build và liên kết; trong 5.6.1 còn có cấu trúc mở rộng để có thể chạy thêm script trong tương lai
Phạm vi giai đoạn Bash của backdoor xz/liblzma
- Andres Freund đã thông báo trên mailing list oss-security về việc phát hiện backdoor trong xz/liblzma, và backdoor này ảnh hưởng đến OpenSSH server
- Đối tượng phân tích không phải bản thân backdoor nhị phân, mà là các giai đoạn khởi đầu dựa trên Bash được chạy trước đó và cách chúng bị làm rối
- Luồng Bash kéo dài từ Stage 0 đến Stage 2, và cũng lộ ra một phần Stage 3 tiềm năng có vẻ chưa được hoàn thiện đầy đủ
- Các giai đoạn bị làm rối/mã hóa và backdoor nhị phân phía sau được giấu trong hai tệp kiểm thử
tests/files/bad-3-corrupt_lzma2.xztests/files/good-large_compressed.lzma
- Các phiên bản bị ảnh hưởng là 5.6.0 và 5.6.1, với một số khác biệt nhỏ giữa hai bản
Stage 0: Khôi phục luồng xz trông như tệp kiểm thử bị hỏng
- Điểm bắt đầu là đoạn mã trong
m4/build-to-host.m4, có vẻ được thực thi ở đâu đó trong quá trình build - Pipeline cốt lõi nối
sed,tr, vàxz -dđể trích xuất script Stage 1 - Sau khi đọc các byte của
tests/files/bad-3-corrupt_lzma2.xz, nó thay thế một số byte bằngtr "\t \-_" " \t_\-"- tab
0x09đổi thành khoảng trắng0x20 - khoảng trắng
0x20đổi thành tab0x09 - dấu gạch ngang
0x2dđổi thành dấu gạch dưới0x5f - dấu gạch dưới
0x5fđổi thành dấu gạch ngang0x2d
- tab
- Việc thay thế này có vai trò biến
bad-3-corrupt_lzma2.xztrở lại thành một luồng xz hợp lệ - Cuối cùng nó giải nén luồng đã khôi phục bằng
xz, bỏ qua lỗi và lấy được script Stage 1 để chạy ngay lập tức- Luồng trông như bị cắt cụt, nhưng điều đó không thành vấn đề vì phần đầu ra có ý nghĩa đã được ghi xong
Stage 1: Lấy Stage 2 từ good-large_compressed.lzma
- Stage 1 là một tệp Bash ngắn bắt đầu bằng
####Hello####, được Andres Freund nêu trong email - Khác biệt đầu tiên giữa hai phiên bản là các byte ngẫu nhiên trong chú thích ở dòng thứ hai
- 5.6.0:
86 F9 5A F7 2E 68 6A BC - 5.6.1:
E5 55 89 B7 24 04 D8 17
- 5.6.0:
- Trong 5.6.1 có thêm kiểm tra Linux dạng
[ ! $(uname) = "Linux" ] && exit 0, và cùng một kiểm tra này được lặp lại 5 lần - Việc trích xuất Stage 2 bắt đầu từ dòng dài
export i=..., giá trị này sau đó được gọi bằngeval $inhư một loại hàm - Luồng trích xuất là một pipeline liên tục nối stdout và stdin
- Trước tiên giải nén
good-large_compressed.lzmabằngxz -dc eval $ilặp đi lặp lại lời gọiheadtheo mẫu bỏ qua 1024 byte rồi xuất 2048 byte- Ở cuối tệp, 5.6.0 xuất 724 byte còn 5.6.1 xuất 939 byte
- Trước tiên giải nén
- Chuỗi
headnày loại bỏ các vùng rỗng, entropy thấp và chỉ nối lại các vùng có dữ liệu thực sự - Sau đó
tail -c +31233loại bỏ phần đầu- Trong phần đầu này có giấu backdoor nhị phân sẽ được trích xuất ở bước sau
- Hiệu ứng thực tế là bỏ 31264 byte đầu trong 5.6.0 và 31232 byte đầu trong 5.6.1
- Tiếp theo
trđược dùng như một mật mã thay thế đơn giản, với khóa ánh xạ khác nhau giữa 5.6.0 và 5.6.1- 5.6.0:
tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" - 5.6.1:
tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377"
- 5.6.0:
- Dữ liệu sau thay thế tiếp tục được giải nén bằng
xz -F raw --lzma1 -dc, và Stage 2 thu được sẽ được chạy ngay
Stage 2: Chỉnh sửa quá trình build và cơ chế mở rộng
- Stage 2 là tệp
infected.txtmà Andres Freund đính kèm trong email gốc, một script Bash nơi việc chỉnh sửa quá trình biên dịch thực sự diễn ra - Xét về góc độ làm rối, Stage 2 có ba mảnh đáng chú ý, trong đó hai mảnh chỉ xuất hiện ở 5.6.1
- Hai mảnh trong 5.6.1 gần như giống hệt nhau và tìm trong
tests/files/các tệp có chữ ký cụ thể- Fragment 1 tìm
~!:_ Wvà|_!{ - - Fragment 3 tìm
jV!.^%và%.R.1Z
- Fragment 1 tìm
- Nếu tìm thấy chữ ký, nó sẽ trích xuất tên tệp và offset, rồi cắt dải byte từ sau chữ ký thứ nhất đến trước chữ ký thứ hai
- Dữ liệu được cắt ra sau đó được giải mã thay thế bằng khóa
trdùng trong 5.6.0, tiếp tục giải nén bằngxz -F raw --lzma2 -dc, rồi chạy bằngeval - Trong các TAR archive 5.6.0 và 5.6.1 đã được khảo sát không có tệp nào mang các chữ ký đó
- Cấu trúc này trông như một hệ thống mở rộng/vá cho phép chạy script mới trong tương lai chỉ bằng cách thêm tệp kiểm thử mới, không cần sửa lại các tệp kiểm thử đang mang payload
Stage 2: Trích xuất tệp .o của backdoor nhị phân
- Một phần cốt lõi khác của Stage 2 là đoạn mã trích xuất tệp
.ođể chèn vào quá trình biên dịch và liên kết - Kết quả mục tiêu của việc trích xuất là
liblzma_la-crc64-fast.o, chính là backdoor nhị phân cuối cùng - Quá trình trích xuất, giống các bước trước, kết nối nhiều công cụ chuẩn trong một pipeline
- Giải nén
good-large_compressed.lzmabằngxz -dc - Dùng lại chuỗi
headcủaeval $itừng được dùng ở Stage 1 - Dùng
sed "s/\(.\)/\1\n/g"để thêm xuống dòng sau mỗi byte, biến dữ liệu thành dạng một dòng cho mỗi byte
- Giải nén
- Script AWK tiếp theo thực hiện giải mã kiểu RC4 trên luồng đầu vào
- Mảng khóa được tạo bằng
((i * 7) + 5) % 256 - 5.6.0 bỏ qua 4096 byte đầu của luồng PRNG đầu ra
- 5.6.1 bỏ qua 8192 byte
- RC4 gốc dùng XOR, nhưng đoạn mã này dùng phép cộng dạng
(v + k) % 256
- Mảng khóa được tạo bằng
- Dữ liệu đã giải mã sau đó được giải nén bằng
xz -dc --single-stream - Cuối cùng
headcắt lấy đúng dải byte cần thiết và lưu thànhliblzma_la-crc64-fast.o- Payload đã nén nhưng có biến đổi một phần có kích thước 88792 byte ở 5.6.0 và 88664 byte ở 5.6.1
- Dải được trích xuất cuối cùng có quy mô khoảng 86KB
Bản chất của lớp làm rối chỉ dùng công cụ chuẩn
- Toàn bộ giai đoạn Bash được tạo nên chỉ bằng các công cụ dòng lệnh chuẩn
sedtrxzheadtailawkgrepcutexpr
- Payload được giấu trong các tệp nhị phân trông như tệp kiểm thử hợp lệ, rồi chỉ lấy ra các vùng cần thiết bằng file carving
- Lớp làm rối kết hợp mật mã thay thế đơn giản với giải mã biến thể RC4 dựa trên AWK
- Việc thực thi diễn ra qua tổng cộng 3 giai đoạn, và trong 5.6.1 có thêm cấu trúc cho phép sau này chạy script bằng cách bổ sung một tệp kiểm thử riêng
- Toàn bộ cấu trúc cho thấy đã có rất nhiều nỗ lực để khiến nó trông vô hại và được che giấu tốt; nếu đây là một trường hợp bị phát hiện tình cờ thì cũng đặt ra câu hỏi còn bao nhiêu trường hợp tương tự chưa bị phát hiện
1 bình luận
Các ý kiến trên Hacker News
Nhờ phần giải thích được đơn giản hóa và so sánh với ảnh nhiễu, tôi đã hình dung được mọi người muốn nói gì khi gọi nó là tinh vi
Tôi cũng thấy trên reddit có nói rằng cách “sandboxing” đã bị phá hỏng chỉ bởi một dấu chấm; dấu chấm nằm ở mép trái của dòng ngay sau
#includehttps://git.tukaani.org/?p=xz.git;a=commitdiff;h=328c52da8a2...
https://old.reddit.com/r/linux/comments/1brhlur/xz_utils_bac...
+,+.,+, và một dấu chấm đó rất khó nhận raChỉ riêng lỗi đơn giản khiến test luôn fail hoặc luôn pass đã đủ đau đầu rồi; có thể thấy vì sao nó dễ trở thành mục tiêu cho hành vi ác ý
a) Thực tế hầu như không ai build gói này bằng cmake
b) Nếu build bằng cmake với
-DENABLE_SANDBOX=landlockthì build đơn giản là thất bại: https://i.imgur.com/7xbeWFx.pngDấu chấm đó không vô hiệu hóa sandboxing, mà chỉ khiến không thể build bằng cmake. Nếu thực sự có ai đó build bằng cmake, họ hẳn đã thấy lỗi và nhận ra có gì đó sai, nên việc coi đây là một nỗ lực ác ý nhằm hạ thấp bảo mật là không hợp lý
“Nếu chuyện này được phát hiện một cách tình cờ, thì còn bao nhiêu thứ chưa bị phát hiện nữa?” mới là câu hỏi quan trọng nhất
Không thể nào Andres Freund lại tình cờ tìm ra đúng một dự án mã nguồn mở phổ biến duy nhất bị cài backdoor. Dù ngoài thực tế đã có khoảng một tá thứ như vậy thì cũng không lạ
Kẻ tấn công tiếp theo sẽ ít bất cẩn hơn nhiều trong việc để lại dấu vết như thời gian chạy tăng lên
Điều gây khó chịu ở đây là unit test đã mở ra đường tấn công. Nếu không có test, việc che giấu như vậy sẽ khó hơn rất nhiều
bad-3-corrupt_lzma2.xzcó ba stream; stream thứ nhất và thứ ba là các xz stream hợp lệ. README ghi rằng stream ở giữa có stream header, block header, index và stream footer đúng, chỉ dữ liệu LZMA2 bị hỏng, và khi dùng--single-streamthì nó phải được giải nénCác chuỗi
####Hello####và####World####giúp tạo ra một kết quả bình thường có vẻ hợp lý khi thực sự làm theo hướng dẫn trong README$ cat tests/files/bad-3-corrupt_lzma2.xz | xz -d --single-stream####Hello####Những chuỗi này là comment của shell nên không cản trở việc thực thi payload
Cuối cùng, về sau chúng đóng vai trò như dấu mốc để regex có thể tìm ra file mà không cần tham chiếu trực tiếp tên file hay dùng đúng chuỗi Hello/World
$ gl_am_configmake=\grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null``$ echo $gl_am_configmake./tests/files/bad-3-corrupt_lzma2.xzViệc kiểm tra này cần được áp dụng theo tính bắc cầu, để khi một bản phân phối Linux định cập nhật lên phiên bản liblzma mới, build cũng phải fail hoặc cảnh báo vì có dependency nhị phân mới
Tôi không biết thông lệ này phổ biến đến đâu trong các bản build của bản phân phối Linux. Nếu phổ biến thì việc dọn dẹp sẽ đòi hỏi khối lượng công việc khổng lồ, và ngay từ đầu cũng chưa chắc có khả thi không. Với bazel thì có vẻ làm được, còn các hệ thống build khác thì tôi không rõ
Không biết đã có ai tìm thử trên GitHub các mánh head | tail tương tự chưa. Khó mà tin rằng trò này được phát minh mới chỉ để phục vụ vụ này
Kiểu như một file
.shkhổng lồ hiển thị giấy phép và nhận đồng ý, rồicatchính nó qua pipelinehead/tailđểcpiotrích xuất các tài nguyên thực sựKhông có câu trả lời nào tốt hơn, nhưng bản thân cái đống bash khó hiểu này chẳng phải đã có mùi rồi sao?
Cũng như trong các lĩnh vực khác của thế giới phát triển phần mềm, lẽ nào không thể viết bớt mờ mịt hơn để thấy rõ hơn chuyện gì đang xảy ra sao?
Tôi biết maintainer có thể đưa mã độc vào mà không bị rà soát nghiêm ngặt như contributor bên ngoài, nhưng hẳn phải có cách tốt hơn một đống code “súc tích”, thực tế trông như kiểu obfuscation ngoài ý muốn
Vấn đề cốt lõi là vào thập niên 80–90 có vô số hệ thống họ Unix, mỗi hệ thống có lỗi và thiếu tính năng riêng, còn tác giả phần mềm thì muốn giảm tối đa phụ thuộc khi build
Vì vậy nhiều cộng đồng đã chuẩn hóa việc tự động hóa build bằng shell script chạy được ở mọi nơi. Nhưng viết shell script rất khổ, nên người ta bắt đầu dùng các công cụ như bộ tiền xử lý macro M4 để sinh shell script
Kết quả là nhiều dự án có cả đống shell script khổng lồ và mờ mịt, phòng trường hợp ai đó muốn chạy mã trên AIX hoặc một Unix cổ lỗ bị hỏng nào đó
Muốn loại bỏ bụi rậm shell khó xuyên thủng này thì phải giảm mạnh số nền tảng được hỗ trợ, chuẩn hóa vào các công cụ build sạch hơn, và xây dựng nhiều hạ tầng lõi hơn bằng những ngôn ngữ không cần shell cho các bản build portable
Nhưng đó là một công việc khổng lồ, và phần lớn thư viện C lõi được một hai tình nguyện viên không lương duy trì. Việc ngừng hỗ trợ “Obscurnix-1997” nhìn chung cũng là quyết định khá gây tranh cãi
Vì vậy một phần lớn hạ tầng lõi vẫn bị bao quanh bởi đầm lầy shell script sinh tự động không rõ là gì
Không ít công cụ được build theo cách này
Điều trông đáng ngờ là các lần gọi tr lặp đi lặp lại. Thấy kiểu đó thì tôi nghĩ ai đó đang cố tỏ ra khôn lanh, và ở đây “khôn lanh” mang nghĩa tiêu cực. Nếu tôi là maintainer, khi thấy đoạn mã như vậy được đưa vào, tôi đã yêu cầu giải thích nó làm gì. Vì hầu như luôn có cách tốt hơn để tránh kiểu chaining như thế
Vấn đề thật sự là lúc mã này được đưa vào không có maintainer khác để xem xét. Một thành phần quan trọng trong stack phụ thuộc vào một người, và trong trường hợp này người đó có ác ý
execthứ gì đó không? Tôi tự hỏi nếu bash có kiểu “chế độ an toàn” thì saoTuy vậy tôi không hình dung được làm sao chạy bash trong cái “chế độ an toàn” giả định đó cho script configure của xv, nên xin rút lại
https://github.com/tukaani-project/.github/issues/2
Toàn bộ hệ sinh thái C, bao gồm cả công cụ build và các tiện ích Unix cũ, là một mớ hỗn độn bảo mật chỉ chờ bị khai thác, và rồi sẽ bị khai thác
Chỉ cần nhìn việc một dấu chấm có thể phá hỏng mọi thứ dễ đến mức nào là đủ. Đã đến lúc mọi người phải nhận ra rằng không thể đặt cược an ninh của thế giới vào C nữa
Mong là hãy dùng Ada hoặc Rust với toolchain hiện đại
Tôi không hiểu nổi làm sao thứ này qua được code review rồi được merge. Nếu tôi không bỏ sót gì thì trông cẩu thả đến mức vô lý
Hơn nữa, nó được obfuscate rất nặng bên trong một file binary được đánh dấu là file test, tức các file test nén xz “good”/“bad”. Nếu không biết phải tìm gì thì không có cách nào nhận ra
Dùng bản phân phối LTS có thể bảo vệ được ở mức nào đó. Slackware có vẻ dùng lzma, tức
tar.xz, cho package, nhưng bản ổn định cuối cùng ngoại trừ-currentkhông gặp vấn đề nàyNếu muốn tiến thêm một bước về phía phần mềm tự do, Hyperbola GNU cũng không gặp vấn đề này
Ngoài ra Slackware
-currentcũng không linksshdvới xz, và cũng không dùng systemd