https://medium.com/@numencyberlabs/analysis-of-the-first-critical-0-day-vulnerability-of-aptos-move-vm-8c1fd6c2b98e
1. Kata pengantar
Bahasa pemrograman Move semakin populer akhir-akhir ini karena keunggulan kuat yang dimilikinya dibandingkan bahasa Soliditas Ethereum. Move digunakan di banyak proyek terkenal, seperti Aptos dan Sui. Baru-baru ini,Keamanan Numen Web3 produk deteksi kerentanan menemukan kerentanan keamanan tingkat kritis di Mesin Virtual (VM) rantai publik Aptos. Apa yang kami temukan adalah bahwa kerentanan dalam bahasa tersebut dapat menyebabkan node Aptos mogok dan menyebabkan penolakan layanan. Pada artikel ini, kami harap Anda memiliki pemahaman yang lebih baik tentang bahasa Move dan keamanannya melalui penjelasan tentang kerentanan ini. Sebagai pemimpin dalam penelitian keamanan bahasa Move, kami akan terus memberikan kontribusi berkelanjutan untuk keamanan ekologisnya.
2. Konsep Penting Bahasa Pindah
Modul dan Script
Move memiliki dua jenis program: Modul dan Skrip. Modul adalah pustaka yang mendefinisikan tipe struktural dan fungsi yang beroperasi pada tipe tersebut. Tipe struktur menentukan pola penyimpanan global Move, dan fungsi modul menentukan aturan untuk memperbarui penyimpanan. Modul itu sendiri juga disimpan di penyimpanan global. Skrip adalah titik masuk ke executable, mirip dengan fungsi utama dalam bahasa tradisional. Skrip biasanya memanggil fungsi modul yang diterbitkan untuk memperbarui penyimpanan global. Skrip adalah fragmen kode sementara yang tidak dipublikasikan di penyimpanan global. File sumber Pindah (atau unit kompilasi) dapat berisi banyak modul dan skrip. Namun, menerbitkan modul atau menjalankan skrip menggunakan operasi mesin virtual (VM) terpisah.
Bagi mereka yang terbiasa dengan sistem operasi, modul Move mirip dengan modul pustaka dinamis yang dimuat saat sistem yang dapat dieksekusi dijalankan, dan skripnya mirip dengan program utama. Pengguna dapat menulis skrip mereka sendiri untuk mengakses penyimpanan global, termasuk kode yang memanggil modul.
Penyimpanan Global
Tujuan dari program Move adalah membaca dan menulis ke penyimpanan global dalam bentuk pohon. Program tidak dapat mengakses sistem file, jaringan, atau data apa pun di luar pohon ini.
Dalam kode semu, penyimpanan global terlihat seperti ini:
Secara struktural, penyimpanan global adalah hutan yang terdiri dari pohon-pohon yang berakar pada alamat akun. Setiap alamat dapat menyimpan data sumber daya dan kode modul. Seperti yang ditunjukkan kode semu di atas, setiap alamat dapat menyimpan paling banyak satu nilai sumber daya dari jenis yang diberikan dan paling banyak satu modul dari nama yang diberikan.
MOVE Prinsip Mesin Virtual
mesin virtual movevm dan evm adalah sama, di mana ia perlu mengkompilasi kode sumber menjadi kode byte, dan kemudian dieksekusi di mesin virtual. Bagan berikut menunjukkan prosesnya.
1. bytecode dimuat melalui fungsi execution_script
2. Jalankan fungsi load_script, fungsi ini terutama digunakan untuk deserialize bytecode, dan verifikasi apakah bytecode itu legal, jika verifikasi gagal, itu akan kembali sebagai kegagalan
3. Setelah verifikasi berhasil, kode bytecode asli kemudian dieksekusi
4. Jalankan bytecode, akses atau ubah status penyimpanan global, termasuk sumber daya, modul
Catatan: Ada banyak fitur lain yang terkait dengan Move, tetapi kami tidak akan memperkenalkan semuanya di sini, dan kami akan terus menganalisis fitur bahasa move dari perspektif keamanan.
3. Deskripsi Kerentanan
Kerentanan ini terutama melibatkan modul verifikasi. Sebelum berbicara tentang kerentanan spesifik, fungsi modul verifikasi dan StackUsageVerifier::verify akan diperkenalkan.
Modul Verifikasi
Kita tahu bahwa sebelum eksekusi sebenarnya dari kode bytecode, akan ada verifikasi bytecode, dan verifikasi dapat dibagi lagi menjadi beberapa sub-proses masing-masing.
Mereka:
Pemeriksa Batas , terutama digunakan untuk memeriksa keamanan batas modul dan skrip. Ini termasuk memeriksa batas tanda tangan, konstanta, dll.
Pemeriksa Duplikasi , modul yang mengimplementasikan pemeriksa untuk memverifikasi apakah setiap vektor dalam CompiledModule berisi nilai yang berbeda
Pemeriksa Tanda Tangan , yang memeriksa apakah struktur bidang sudah benar saat tanda tangan digunakan untuk parameter fungsi, variabel lokal, dan anggota struktur
InstruksiKonsistensi , yang memverifikasi konsistensi instruksi
Konstanta digunakan untuk memverifikasi bahwa konstanta adalah tipe asli dan bahwa data konstanta diserialisasi dengan benar sesuai tipenya
Pemverifikasi CodeUnit , untuk memverifikasi kebenaran kode badan fungsi, masing-masing melalui stack_usage_verifier.rs dan abstract_interpreter.rs
script_signature , untuk memverifikasi bahwa skrip atau fungsi entri adalah tanda tangan yang valid
Kerentanan terjadi dalam proses verifikasi
CodeUnitVerifier::verify_script(config, skrip)? ;
fungsi. Anda dapat melihat bahwa ada banyak subproses verifikasi di sini.
Ini adalah checksum aman tumpukan, checksum aman tipe, checksum aman variabel lokal, dan checksum aman referensi. Kerentanan muncul dalam proses verifikasi keamanan tumpukan.
Verifikasi Keamanan Stack (StackUsageVerifier::verifikasi)
Modul ini digunakan untuk memverifikasi bahwa blok dasar dalam urutan instruksi bytecode dari suatu fungsi digunakan secara seimbang. Setiap blok dasar, kecuali yang diakhiri dengan opcode Ret (kembali ke pemanggil), harus memastikan bahwa ia meninggalkan blok dengan ketinggian tumpukan yang sama seperti di awal. Selain itu, untuk blok dasar apa pun, tinggi tumpukan tidak boleh lebih rendah dari tinggi tumpukan di awal blok.
Ulangi semua blok untuk memverifikasi bahwa kondisi di atas terpenuhi:
Loop berulang untuk memverifikasi keabsahan semua blok dasar.
Detail Kerentanan
Seperti yang diperkenalkan sebelumnya, karena movevm adalah mesin virtual tumpukan, saat memverifikasi keabsahan instruksi, jelas bahwa pertama, kita perlu memastikan bahwa kode byte instruksi benar, dan kedua, kita perlu memastikan bahwa memori tumpukan adalah legal setelah panggilan blok, yaitu, tumpukan diseimbangkan setelah operasi tumpukan.verifikasi_blokir
fungsi digunakan untuk mencapai tujuan kedua.
Seperti yang bisa kita lihat dariverifikasi_blokir
kode, itu akan mengulang semua instruksi di blok kode blok dan kemudian memverifikasi apakah efek blok instruksi pada tumpukan itu sah dengan menambah atau menguranginum_pops
,num_push
. Pertama, melaluistack_size_increment < num_pops
untuk menentukan apakah ruang stack legal. Jikanum_pops
lebih besar daristack_size_increment
, itu berarti jumlah bytecode yang muncul lebih besar dari ukuran tumpukan itu sendiri, dan kesalahan dikembalikan dan bytecode checksum gagal. Kemudian, melaluistack_size_increment -= num_pops; stack_size_increment += num_pushes;
, kedua instruksi ini mengubah dampak pada tinggi tumpukan setelah setiap instruksi dijalankan. Dan akhirnya, ketika loop berakhir,stack_size_increment
harus sama dengan 0, yaitu setelah menyimpan operasi di blok ini, tumpukan harus seimbang.
Tampaknya tidak ada yang salah di sini, tetapi karena dalam eksekusi 16 baris kode, tidak menentukan apakah ada integer overflow, mengakibatkan kerentanan integer overflow yang dapat dikontrol secara tidak langsung dengan membangun num_pushes, stack_size_increment yang besar . Jadi bagaimana kita membuat dorongan dalam jumlah besar?
Tampaknya tidak ada masalah, tetapi karena baris kode ke-16 dieksekusi di sini, tidak dinilai apakah ada integer overflow atau tidak. Akibatnya,stack_size_increment
dapat dikontrol secara tidak langsung dengan membangun sebuah oversizednum_push
, mengakibatkan kerentanan luapan bilangan bulat.
Di sini pertama-tama kita perlu memperkenalkan format file move bytecode.
Pindahkan Format File Kode Byte
Seperti file Windows PE, atau file ELF linux, pindahkan file bytecode yang diakhiri dengan .mv, dan file itu sendiri memiliki format tertentu.
Pertama magic, nilainya A11CEB0B, selanjutnya informasi versi, dan jumlah tabel, setelah itu header tabel, bisa banyak tabel. Jenis tabel adalah jenis tabel, total jenis 0x10 (seperti yang ditunjukkan di sisi kanan gambar), untuk lebih jelasnya Anda mungkin ingin melihat dokumentasi bahasa perpindahan, Berikutnya adalah offset tabel, dan panjang tabel meja. Setelah itu adalah isi tabelnya, dan terakhir adalah Data Spesifik, ada dua macam, untuk modul adalah Data Spesifik Modul, untuk tipe skrip adalah Data Spesifik Skrip.
Format File Berbahaya yang Dibangun
Di sini kita berinteraksi dengan Aptos dalam skrip, jadi kita membuat format file yang ditunjukkan di bawah ini untuk menyebabkan limpahan stack_size_increment:
Pertama, mari kita jelaskan format file bytecode ini:
+0x00–0x03: adalah kata ajaib 0xA11CEB0B
+0x04–0x7: adalah versi format file,versinya adalah 4
+0x8–0x8: adalah jumlah tabel, nilainya 1
+0x9–0x9: adalah jenis tabel, jenisnya adalah TANDA TANGAN
+0xa-0xa: adalah offset tabel, nilainya 0
+0xb-0xb: adalah panjang tabel,nilainya adalah 0x10
+0xc-0x18: adalah data Token TANDA TANGAN
Mulai dari 0x22, ini adalah bagian kode dari kode fungsi utama skrip.
Melalui alat move-disassembler, kita dapat melihat bahwa kode instruksi pembongkaran adalah sebagai berikut:
Diantaranya, kode yang sesuai dengan tiga instruksi 0, 1, dan 2 adalah data di kotak merah, kotak hijau, dan kotak kuning.
LdU64 tidak memiliki hubungan dengan kerentanan itu sendiri. Kami tidak akan membahas terlalu banyak detail di sini, tetapi Anda dapat memeriksa kodenya jika Anda tertarik. Di sini kami fokus untuk menjelaskan instruksi VecUnpack. Fungsi VecUnpack adalah untuk mendorong semua data ke tumpukan ketika objek vektor ditemui dalam kode.
Dalam file yang dibuat ini, kami membuat VecUnpack dua kali,Jumlah vektornya masing-masing adalah 3315214543476364830,18394158839224997406.
Ketika fungsiinstruksi_efek
dieksekusi, baris kedua kode di bawah ini sebenarnya dieksekusi:
Setelah mengeksekusiinstruksi_efek
fungsi, ia mengembalikan (1,3315214543476364830) untuk pertama kalinya. Saat ini, stack_size_increment adalah 0, num_pops adalah 1, dan num_pushes adalah 3315214543476364830. Pengembalian kedua adalah (1,18394158839224997406). Saat mengeksekusi lagistack_size_increment += num_pushes ;
stack_size_increment sudah 0x2e020210021e161d (3315214543476364829).
num_pushes adalah 0xff452e02021e161e (18394158839224997406), ketika keduanya ditambahkan, itu lebih besar dari nilai maksimum u64, menghasilkan pemotongan data, dan nilai stack_size_increment menjadi 0x12d473012043c2c3b, yang menyebabkan integer overflow, yang menyebabkan node Aptos macet, yang pada gilirannya menyebabkan node berhenti berjalan. Karena fitur keamanan dari bahasa karat, itu tidak akan menyebabkan dampak keamanan kode lebih lanjut seperti C/C++.
4. Dampak Kerentanan
Karena kerentanan ini terjadi pada modul eksekusi Pindahkan, untuk node pada rantai, jika kode bytecode dieksekusi, itu akan menyebabkan serangan DoS. Dalam kasus yang parah, jaringan Aptos dapat dihentikan sepenuhnya, yang akan menyebabkan kerusakan yang tak terhitung, dan berdampak serius pada stabilitas node.
5. Perbaikan Resmi
Saat kami menemukan kerentanan ini, kami melaporkannya ke tim resmi Aptos, dan mereka dengan cepat memperbaiki kerentanan tersebut. Anda dapat merujuk ke gambar di bawah ini untuk tangkapan layar perbaikan.
Tautan kode yang relevan ada di bawah ini: