1 poin oleh GN⁺ 2024-03-31 | 1 komentar | Bagikan ke WhatsApp
  • Backdoor xz/liblzma bukan hanya soal payload biner akhir; tahap Bash yang dijalankan saat build disembunyikan lewat berlapis-lapis proses ekstraksi dan dekripsi, sehingga sulit dianalisis
  • Versi yang terdampak adalah xz/liblzma 5.6.0 dan 5.6.1, dan skrip yang diobfuskasi serta payload biner berada di dua berkas yang tampak seperti berkas pengujian
  • Stage 0 dimulai dari m4/build-to-host.m4, memulihkan stream xz yang tampak rusak, lalu mengekstrak dan menjalankan skrip Stage 1 dengan xz -d
  • Stage 1 memotong dan mengganti data dari good-large_compressed.lzma, lalu menjalankan **skrip Bash Stage 2`; pada 5.6.1 ditambahkan kode yang memeriksa apakah berjalan di Linux sebanyak 5 kali
  • Stage 2 menggunakan file carving, cipher substitusi, dan dekripsi varian RC4 berbasis AWK untuk menyisipkan berkas .o ke proses build dan linking; 5.6.1 juga memuat struktur ekstensi yang dapat menjalankan skrip tambahan di masa mendatang

Cakupan tahap Bash pada backdoor xz/liblzma

  • Andres Freund mengumumkan penemuan backdoor xz/liblzma di milis oss-security, dan backdoor ini berdampak pada OpenSSH server
  • Fokus analisis bukan backdoor biner itu sendiri, melainkan tahap awal berbasis Bash yang berjalan sebelumnya serta cara obfuscasinya
  • Alur Bash berlanjut dari Stage 0 hingga Stage 2, dan sebagian juga memperlihatkan potensi Stage 3 yang tampaknya belum diimplementasikan sepenuhnya
  • Tahap yang diobfuskasi/dienkripsi dan backdoor biner berikutnya disembunyikan di dalam dua berkas pengujian
    • tests/files/bad-3-corrupt_lzma2.xz
    • tests/files/good-large_compressed.lzma
  • Versi yang terdampak adalah 5.6.0 dan 5.6.1, dengan sedikit perbedaan di antara keduanya

Stage 0: memulihkan stream xz yang tampak seperti berkas pengujian rusak

  • Titik awalnya adalah kode di m4/build-to-host.m4, dan tampaknya dijalankan di suatu titik dalam proses build
  • Pipeline intinya menghubungkan sed, tr, dan xz -d untuk mengekstrak skrip Stage 1
  • Setelah membaca byte dari tests/files/bad-3-corrupt_lzma2.xz, sebagian byte diganti dengan tr "\t \-_" " \t_\-"
    • Tab 0x09 diubah menjadi spasi 0x20
    • Spasi 0x20 diubah menjadi tab 0x09
    • Tanda hubung 0x2d diubah menjadi underscore 0x5f
    • Underscore 0x5f diubah menjadi tanda hubung 0x2d
  • Substitusi ini berfungsi mengubah bad-3-corrupt_lzma2.xz kembali menjadi stream xz yang valid
  • Pada akhirnya, stream yang telah dipulihkan didekompresi dengan xz, dan skrip Stage 1 didapatkan serta langsung dijalankan sambil mengabaikan error
    • Stream tampak terpotong, tetapi itu tidak menjadi masalah karena output yang bermakna sudah ditulis

Stage 1: mengeluarkan Stage 2 dari good-large_compressed.lzma

  • Stage 1 adalah berkas Bash pendek yang dimulai dengan ####Hello#### dalam email Andres Freund
  • Perbedaan pertama antara kedua versi adalah byte acak pada komentar baris kedua
    • 5.6.0: 86 F9 5A F7 2E 68 6A BC
    • 5.6.1: E5 55 89 B7 24 04 D8 17
  • Pada 5.6.1 ditambahkan pemeriksaan Linux berbentuk [ ! $(uname) = "Linux" ] && exit 0, dan pemeriksaan yang sama diulang 5 kali
  • Ekstraksi Stage 2 dimulai dari baris panjang export i=..., dan nilai ini kemudian digunakan seperti semacam fungsi yang dipanggil dengan eval $i
  • Alur ekstraksinya berupa struktur pipeline yang terus menyambungkan standard output dan standard input
    • Pertama, good-large_compressed.lzma didekompresi dengan xz -dc
    • eval $i berulang kali memanggil head dengan pola membuang 1024 byte dan mengeluarkan 2048 byte
    • Di akhir berkas, 5.6.0 mengeluarkan 724 byte, sedangkan 5.6.1 mengeluarkan 939 byte
  • Rangkaian head ini menghapus area kosong berentropi rendah dan hanya menyambungkan area yang berisi data nyata
  • Setelah itu, bagian awal dibuang dengan tail -c +31233
    • Di bagian awal ini tersembunyi backdoor biner yang diekstrak pada tahap berikutnya
    • Efeknya, pada 5.6.0 sebanyak 31264 byte pertama dibuang, sedangkan pada 5.6.1 sebanyak 31232 byte pertama dibuang
  • Berikutnya tr digunakan seperti cipher substitusi sederhana, dengan kunci pemetaan yang berbeda antara 5.6.0 dan 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"
  • Data yang telah disubstitusi didekompresi lagi dengan xz -F raw --lzma1 -dc, lalu Stage 2 hasilnya langsung dijalankan

Stage 2: modifikasi proses build dan mekanisme ekstensi

  • Stage 2 adalah berkas infected.txt yang dilampirkan Andres Freund pada email aslinya, berupa skrip Bash tempat modifikasi proses kompilasi sesungguhnya terjadi
  • Dari sudut pandang obfuskasi, Stage 2 memiliki tiga potongan menarik, dua di antaranya hanya muncul pada 5.6.1
  • Dua potongan pada 5.6.1 hampir identik satu sama lain dan mencari berkas dengan signature tertentu di tests/files/
    • Fragment 1 mencari ~!:_ W dan |_!{ -
    • Fragment 3 mencari jV!.^% dan %.R.1Z
  • Jika signature ditemukan, nama berkas dan offset diekstrak, lalu rentang byte dari setelah signature pertama hingga sebelum signature kedua dipotong
  • Data yang dipotong didekripsi dengan substitusi menggunakan kunci tr yang dipakai pada 5.6.0, kemudian didekompresi dengan xz -F raw --lzma2 -dc dan dijalankan dengan eval
  • Dalam arsip TAR 5.6.0 dan 5.6.1 yang diperiksa, tidak ada berkas dengan signature tersebut
  • Struktur ini tampak seperti sistem ekstensi/patch yang memungkinkan skrip dijalankan di masa mendatang hanya dengan menambahkan berkas pengujian baru, tanpa perlu kembali mengubah berkas pengujian pembawa payload yang sudah ada

Stage 2: ekstraksi berkas .o backdoor biner

  • Inti lain dari Stage 2 adalah kode yang mengekstrak berkas .o dan menyisipkannya ke proses kompilasi/linking
  • Artefak hasil ekstraksinya adalah liblzma_la-crc64-fast.o, dan inilah backdoor biner final
  • Proses ekstraksi, seperti tahap sebelumnya, menghubungkan berbagai tool standar dalam pipeline
    • good-large_compressed.lzma didekompresi dengan xz -dc
    • Rangkaian head dari eval $i yang digunakan pada Stage 1 dipakai lagi
    • sed "s/\(.\)/\1\n/g" menambahkan newline setelah setiap byte, sehingga menjadi format satu baris per byte
  • Skrip AWK berikutnya melakukan dekripsi mirip RC4 terhadap stream input
    • Array kunci dibuat dengan ((i * 7) + 5) % 256
    • 5.6.0 melewati 4096 byte pertama dari stream PRNG output
    • 5.6.1 melewati 8192 byte pertama
    • RC4 asli menggunakan XOR, tetapi kode ini memakai penjumlahan berbentuk (v + k) % 256
  • Data yang telah didekripsi didekompresi dengan xz -dc --single-stream
  • Setelah itu, rentang byte yang diperlukan dipotong dengan head dan disimpan sebagai liblzma_la-crc64-fast.o
    • Ukuran payload yang terkompresi tetapi sebagian dimodifikasi adalah 88792 byte pada 5.6.0 dan 88664 byte pada 5.6.1
    • Rentang ekstraksi final berukuran sekitar 86KB

Karakter obfuskasi yang dibuat hanya dengan tool standar

  • Seluruh tahap Bash tersusun hanya dari tool command-line standar
    • sed
    • tr
    • xz
    • head
    • tail
    • awk
    • grep
    • cut
    • expr
  • Payload disembunyikan dalam berkas biner yang tampak seperti berkas pengujian normal, lalu hanya area yang diperlukan dikeluarkan lewat file carving
  • Obfuskasinya memakai gabungan cipher substitusi sederhana dan dekripsi varian RC4 berbasis AWK
  • Eksekusi berlangsung dalam total 3 tahap, dan pada 5.6.1 terdapat struktur yang memungkinkan skrip dijalankan nanti dengan menambahkan berkas pengujian terpisah
  • Struktur keseluruhan ini dibuat dengan banyak upaya agar tampak tidak berbahaya dan tersembunyi dengan baik; jika ini adalah kasus yang ditemukan secara kebetulan, ia meninggalkan pertanyaan berapa banyak kasus serupa yang belum ditemukan

1 komentar

 
GN⁺ 2024-03-31
Komentar Hacker News
  • Berkat penjelasan yang disederhanakan dan perbandingan gambar noise, jadi lebih kebayang apa yang orang maksud dengan kecanggihan
    Saya juga melihat di reddit bahwa metode “sandboxing” dirusak hanya oleh satu titik, dan ada titik di paling kiri baris tepat setelah #include
    https://git.tukaani.org/?p=xz.git;a=commitdiff;h=328c52da8a2...
    https://old.reddit.com/r/linux/comments/1brhlur/xz_utils_bac...

    • Benar-benar kejam. Di diff itu cuma terlihat seperti +, +., +, dan satu titik itu nyaris tak terlihat
    • Saya sangat tidak suka penggunaan conditional pada waktu kompilasi/build seperti ini. Sulit menguji apakah sesuatu aktif saat seharusnya aktif dan mati saat seharusnya mati, terlebih lagi jika itu ada di dalam sistem build tanpa framework unit test
      Bahkan jika hanya karena kesalahan sederhana sampai pengujian selalu gagal atau selalu berhasil pun sudah merepotkan, jadi mudah terlihat kenapa ini jadi sasaran yang bagus untuk perilaku jahat
    • Ini kemungkinan besar bukan tindakan sengaja, melainkan kesalahan sederhana
      a) praktis tidak ada yang membangun paket ini dengan cmake
      b) jika dibangun dengan cmake dan -DENABLE_SANDBOX=landlock, build-nya langsung gagal: https://i.imgur.com/7xbeWFx.png
      Titik itu bukan menonaktifkan sandboxing, melainkan hanya membuatnya tidak bisa dibangun dengan cmake. Jika benar ada orang yang mencoba membangunnya dengan cmake, mereka akan melihat error dan sadar ada yang salah, jadi tidak masuk akal jika ini dianggap upaya jahat untuk menurunkan keamanan
  • Jika ini ditemukan secara kebetulan, maka pertanyaan terpenting adalah “berapa banyak yang masih belum ditemukan”
    Sulit dipercaya bahwa Andres Freund secara kebetulan menemukan satu-satunya proyek open source populer yang telah disusupi backdoor. Tidak akan mengejutkan kalau sudah ada selusin hal seperti ini di alam liar

    • Bukan sekadar ditemukan secara kebetulan, melainkan lebih seperti dia merasakannya. Itu berbeda
      Penyerang berikutnya kemungkinan akan jauh lebih hati-hati agar tidak meninggalkan jejak seperti peningkatan waktu eksekusi
    • Bisa jadi, tapi dalam kenyataannya mungkin tidak banyak kasus separah itu. Kalau memang banyak, rasanya kita akan lebih sering menemui situasi seperti ini
  • Pikiran yang mengganggu di sini adalah bahwa unit test justru membuka jalur serangan. Tanpa test, akan jauh lebih sulit menyembunyikan ini

    • Penyerang bahkan menutupi jejak payload awal dengan paragraf README yang tampak tidak berbahaya. Semacam “tidak ada yang perlu dilihat!”
      bad-3-corrupt_lzma2.xz memiliki tiga stream, dan stream pertama serta ketiga adalah stream xz yang valid. Stream di tengah memiliki stream header, block header, index, dan stream footer yang benar, hanya data LZMA2-nya yang rusak, dan tertulis bahwa jika memakai --single-stream maka itu seharusnya bisa didekompresi
      String ####Hello#### dan ####World#### membuat hasil normal yang masuk akal muncul ketika instruksi README benar-benar diikuti
      $ cat tests/files/bad-3-corrupt_lzma2.xz | xz -d --single-stream
      ####Hello####
      String-string ini adalah komentar shell sehingga tidak mengganggu eksekusi payload
      Terakhir, di kemudian hari ini berfungsi sebagai penanda agar regex bisa menemukan file tersebut tanpa langsung merujuk ke nama file atau memakai string Hello/World yang sebenarnya
      $ gl_am_configmake=\grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null``
      $ echo $gl_am_configmake
      ./tests/files/bad-3-corrupt_lzma2.xz
    • Untuk proyek yang penting bagi keamanan, tampaknya masuk akal untuk menyiapkan infrastruktur build agar memunculkan error, atau setidaknya peringatan, ketika file biner disertakan dalam build
      Pemeriksaan ini harus berlaku secara transitif, sehingga ketika distribusi Linux mencoba memperbarui ke versi liblzma baru, build juga harus gagal atau memunculkan peringatan karena adanya dependensi biner baru
      Saya tidak tahu seberapa umum praktik seperti ini dalam build distribusi Linux. Kalau ternyata umum, membersihkannya akan jadi pekerjaan besar, dan belum jelas apakah itu benar-benar memungkinkan. Dengan bazel tampaknya bisa, tapi saya kurang tahu soal sistem build lainnya
  • Saya penasaran apakah ada yang sudah mencari trik head | tail serupa di GitHub. Rasanya sulit percaya ini diciptakan khusus untuk kasus ini

    • Saya cukup sering melihat cara seperti ini pada installer Unix dari vendor software komersial
      File .sh besar menampilkan lisensi dan meminta persetujuan, lalu melakukan cat pada dirinya sendiri, melewatkannya melalui pipe head/tail, dan mengekstrak aset sebenarnya dengan cpio
    • Cerdik, tapi bukan sesuatu yang sepenuhnya baru, dan justru cukup dekat dengan skenario penggunaan yang memang dimaksudkan untuk alat-alat seperti ini
    • Ini kesempatan untuk menulis makalah
  • Tidak ada jawaban yang lebih baik, tetapi rasanya gumpalan bash yang sulit dipahami ini sendiri sudah merupakan tanda yang mencurigakan
    Sama seperti di area lain dalam dunia pengembangan, tidakkah ini bisa ditulis dengan cara yang kurang buram agar lebih jelas terlihat apa yang sebenarnya terjadi?
    Saya paham bahwa maintainer bisa menyisipkan kode berbahaya tanpa tinjauan seketat kontributor eksternal, tetapi seharusnya ada cara yang lebih baik daripada gumpalan kode yang “ringkas”, yang pada praktiknya terlihat seperti obfuscation yang tidak disengaja

    • Ini tanda yang sangat lama
      Masalah intinya adalah pada era 80–90-an ada banyak sekali sistem turunan Unix, masing-masing dengan bug dan fitur yang hilang, dan para penulis perangkat lunak ingin meminimalkan dependensi build
      Karena itu, banyak komunitas menstandarkan otomatisasi build dengan skrip shell yang bisa berjalan di mana saja. Tetapi skrip shell menyakitkan untuk ditulis, sehingga orang akhirnya menghasilkan skrip shell memakai alat seperti preprosesor makro M4
      Hasilnya, banyak proyek memiliki gumpalan besar skrip shell yang buram, untuk berjaga-jaga jika suatu hari ada orang yang ingin menjalankan kodenya di AIX atau Unix kuno yang rusak
      Untuk menyingkirkan rimba shell yang sulit ditembus ini, kita harus sangat mengurangi jumlah platform yang didukung, menstandarkan alat build yang lebih bersih, dan membangun lebih banyak infrastruktur inti dalam bahasa yang tidak memerlukan shell untuk build portabel
      Tetapi ini pekerjaan besar, dan cukup banyak library C inti dikelola oleh satu atau dua sukarelawan tanpa bayaran. Menghentikan dukungan untuk “Obscurnix-1997” juga biasanya menjadi keputusan yang cukup kontroversial
      Jadi cukup banyak infrastruktur inti masih dikelilingi rawa skrip shell hasil mesin yang tidak jelas asal-usulnya
    • Shell itu bukan ditulis manusia, melainkan kode hasil generasi. Karena autoconf dipakai luas, konfigurasi shell hasil generasi menumpuk seperti gunung, dan autoconf menghasilkan ribuan baris skrip shell portabel yang sulit dibaca dari skrip preprosesor makro M4
      Tidak sedikit alat yang dibangun dengan cara seperti ini
    • Fakta bahwa bash terlihat sulit dipahami sekilas menurut saya bukan tanda bahaya. Skrip bash yang ditulis sangat padat memang kadang terlihat seperti itu. Apakah memang perlu ditulis sepadat itu adalah perdebatan terpisah
      Yang terlihat mencurigakan adalah pemanggilan tr yang berulang. Kalau melihat itu, saya menganggap ada orang yang sedang berusaha terlalu “cerdik”, dan “cerdik” di sini bermakna negatif. Jika saya maintainer, saya akan meminta penjelasan tentang apa yang dilakukan kode seperti itu saat diajukan. Karena hampir selalu ada solusi yang lebih baik daripada chaining semacam itu
      Masalah sebenarnya adalah saat kode ini masuk, tidak ada maintainer lain yang meninjaunya. Komponen penting dalam stack bergantung pada satu orang, dan dalam kasus ini orang itu memang berniat jahat
    • Bukan “tidak sengaja”; poin utamanya adalah bahwa ini sengaja di-obfuscate
    • Mungkin saya salah memahami risiko backdoor XV, tetapi adakah cara menjalankan bash agar tidak bisa exec sesuatu? Rasanya akan menarik kalau bash punya semacam “mode aman”
      Tapi untuk skrip configure xv, saya sama sekali tidak bisa membayangkan bagaimana bash bisa dijalankan dalam “mode aman” hipotetis seperti itu, jadi lupakan saja
  • https://github.com/tukaani-project/.github/issues/2

    • Cukup lucu. Ini bukan hanya backdoor yang sengaja dibuat dan mengerikan, tetapi juga pelanggaran GPL karena backdoor tersebut merupakan karya turunan, namun source-nya tidak disertakan dan tidak didistribusikan dalam “bentuk yang disukai untuk dimodifikasi”
  • Seluruh ekosistem C, termasuk alat build dan utilitas Unix lama, adalah kekacauan keamanan yang tinggal menunggu untuk dieksploitasi, dan pada akhirnya memang akan dieksploitasi
    Lihat saja betapa mudahnya satu titik merusak segalanya. Orang-orang sekarang perlu sadar bahwa keamanan dunia tidak bisa lagi ditumpukan pada C
    Saya berharap orang memakai Ada atau Rust dengan toolchain modern

    • Menambahkan satu titik ke Rust juga tidak merusaknya?
  • Saya benar-benar tidak paham bagaimana ini bisa lolos code review dan di-merge. Kalau saya tidak melewatkan sesuatu, ini tampak sangat ceroboh sampai tidak masuk akal

    • Pelaku jahat itu adalah co-maintainer repositori, untuk sementara waktu bahkan lebih aktif daripada maintainer asli, dan memiliki hak commit penuh. Ini langsung di-commit ke master tanpa PR maupun review
      Selain itu, ini sangat di-obfuscate di dalam file biner yang ditandai sebagai file uji, yaitu file uji kompresi xz “good”/“bad”. Kalau tidak tahu apa yang harus dicari, tidak ada cara untuk menyadarinya
    • Pesan commit file uji itu mengklaim dibuat dengan generator angka acak. Orang yang membuat tarball rilis menaruh baris terakhir di lokasi yang tepat, tetapi itu tidak pernah di-check-in ke repositori
    • Paket dengan satu maintainer aktif tidak memiliki code review
  • Memakai distro LTS bisa memberi perlindungan sampai tingkat tertentu. Slackware tampaknya memakai lzma, yaitu tar.xz, untuk paket, tetapi rilis stabil terakhir selain -current tidak terdampak masalah ini
    Jika ingin melangkah sedikit lebih jauh ke sisi perangkat lunak bebas, Hyperbola GNU juga tidak terdampak masalah ini
    Selain itu, Slackware -current juga tidak menautkan sshd ke xz, dan juga tidak memakai systemd