https://medium.com/numen-cyber-labs/the-story-of-a-high-vulnerability-in-move-reference-safety-verify-module-2340f3d8c642
0x0 kata pengantar
Beberapa waktu lalu, kami menemukan kritiskerentanan di Aptos Movevm. Setelah melakukan penelitian mendalam, kami menemukan kerentanan kritis lain yang juga merupakan luapan bilangan bulat, tetapi kali ini, sangat menarik.
Kita tahu bahwa bahasa Pindah memverifikasi unit kode sebelum mengeksekusi bytecode. Di unit kode verifikasi itu dibagi menjadi 4 langkah. Bug ini terjadi di reference_safety. Kami akan membahasnya lebih detail di bawah ini.
Modul ini mendefinisikan fungsi transfer untuk memverifikasi keamanan referensi dari badan prosedur. Pemeriksaan mencakup (namun tidak terbatas pada) memverifikasi bahwa tidak ada referensi yang menggantung, akses ke referensi yang dapat diubah aman, dan akses ke referensi penyimpanan global aman.
Inilah titik masuk verifikasi, ini akan memanggil fungsi_analisa.
Di analyze_function, fungsi akan diverifikasi dengan setiap blok dasar, jadi apa blok dasarnya?
Dalamkonstruksi penyusun , blok dasar adalah urutan kode garis lurus tanpa cabang kecuali untuk masuk dan tidak ada cabang keluar kecuali di pintu keluar.
Dalam bahasa Pindah, bagaimana kita mengidentifikasi blok dasar?
Dalam bahasa Pindah, blok dasar ditentukan dengan melintasi bytecode, menemukan semua instruksi cabang, dan instruksi loop. Anda dapat melihat kode inti di bawah ini:
Berikut adalah contoh blok kode dari bahasa Move. Kita bisa melihat ada 3 blok dasar. Cabang ditentukan oleh instruksi: BrTrue, Branch, Ret.
0x1 Keselamatan Referensi dalam Gerakan
Move mendukung dua jenis referensi:kekal — didefinisikan dengan & (mis. &T) danyg mungkin berubah — &mut (mis. &mut T). Anda dapat menggunakan referensi yang tidak dapat diubah (&) untuk membaca data dari struct dan menggunakan yang dapat diubah (&mut) untuk memodifikasinya. Dengan menggunakan jenis referensi yang tepat, Anda membantu menjaga keamanan dan Anda harus mengetahui apakah metode ini mengubah nilai atau hanya membaca.
Di bawah ini adalah contoh dari tutorial Pindah resmi:
Pada contoh di atas, kita dapat melihat mut_ref_t adalah referensi yang dapat diubah dari nilai t.
Jadi, modul keselamatan referensi Pindah mencoba untuk mengkonfirmasi apakah referensi itu valid atau tidak dengan mengambil fungsi sebagai unit, memindai blok dasar dalam fungsi, dan menilai apakah semua operasi referensi legal melalui verifikasi instruksi bytecode.
Gambar di bawah menunjukkan rutinitas yang memverifikasi keamanan referensi.
Status di sini adalah AbstractState yang berisi grafik pinjaman dan lokal yang keduanya digunakan untuk mengamankan referensi.
pinjam_grafik adalah grafik yang mewakili hubungan antara referensi lokal.
Dapat kita lihat dari gambar di atas, ada apra negara bagian yang menyertakan penduduk setempat dan meminjam grafik (L ,BG) dan kemudian mengeksekusi blok dasar akan menghasilkan akeadaan pos dengan (L’,BG’), dan kemudian akan menggabungkan status sebelum dan sesudah untuk memperbarui status blok dan menyebarkan postcondition dari blok ini ke blok penerus. Ini sepertiLautan Node pengoptimalan di turbofan V8.
Kode di bawah ini adalah loop utama yang sesuai dengan gambar di atas. Pertama, jalankan kode blok (Ini akan mengembalikan AnalysisError jika mengeksekusi instruksi tidak berhasil) dan kemudian coba gabungkanpra negara bagian Dankeadaan pos dengan menentukan apakahjoin_result diubah atau tidak. Jika diubah dan blok saat ini berisi titik tepi (yang berarti ada loop), itu akan melompat kembali ke awal loop, pada putaran berikutnya masih akan mengeksekusi blok ini sampaikeadaan pos adalah sama denganpra negara bagian atau dibatalkan oleh beberapa kesalahan.
Bagaimana kita menilai apakah joinResult diubah atau tidak diubah?
Melalui kode di atas, kita dapat mengetahui apakah hasil join berubah atau tidak dengan menilai apakah relasi local dan borrow mengalami perubahan atau tidak. Fungsi join_ di sini digunakan untuk memperbarui lokal dan meminjam status grafik.
Dengan kode join_ di bawah ini, baris 6 adalah untuk menginisialisasi Peta lokal baru, baris 9 digunakan untuk mengulangi semua indeks di lokal jika semua nilainya Tidak ada, sebelum dan sesudah mengeksekusi blok sehingga Anda tidak memasukkan ke yang baru peta penduduk setempat. Jika sebelum status memiliki nilai, status pasca adalah Tidak ada, kita perlu melepaskan id brow_graph, yang berarti menghilangkan hubungan peminjaman nilai. Kebalikannya juga sama. Secara khusus, ketika kedua nilai ada dan sama, masukkan ke dalam peta baru seperti baris 30–33 dan kemudian gabungkan grafik_pinjaman pada baris 38.
Dari atas kita dapat melihat self.iter_locals() adalah jumlah penduduk setempat. Dan perhatikan bahwa lokal ini tidak hanya menyertakan fungsi lokal asli tetapi juga parameter.
0x2 Kerentanan
Di sini kami telah melewati semua kode yang terkait dengan kerentanan, apakah Anda menemukannya?
Jika Anda tidak dapat menemukan kerentanannya, itu tidak masalah. Saya akan membahas proses pemicu kerentanan secara mendetail di bawah ini.
Pertama di kode tiup jika panjang parameter menambahkan panjang lokal lebih besar dari 256. Sepertinya tidak ada masalah kan?
Tapi fungsi ini akan mengembalikan Iterator dengan tipe item u8.
Jadi dalam fungsi join_() adalah function_view.parameters().len() dan function_view.locals().len() nilai gabungan lebih besar dari 256.
Dalam kode,untuk lokal di self.iter_locals() , variabel lokal bertipe u8. Setelah 256 iterasi, akan menyebabkan overflow. Setelah overflow, nilai lokal adalah 8.
Sebenarnya, Move memiliki rutin untuk memverifikasi nomor lokal, tetapi sayangnya hanya memverifikasi lokal di modul batas cek, tidak termasuk panjang parameternya.
Sepertinya para pengembang tahu di sini tentang perlunya memeriksa parameter + nilai lokal. Namun, kode tersebut memverifikasi jumlah penduduk lokal dalam modul batas cek , tidak termasuk panjang parameter.
0x3 Pindahkan Overflow ke DoS
Kami tahu ada loop utama untuk memindai blok kode dan setelah memanggil fungsi execution_block dan bergabung dengan negara. Jika kode pemindahan ada, sebuah loop akan melompat ke blok yang mulai dieksekusi lagi.
Jadi jika kita membuat blok kode loop dan mengeksploitasi overflow untuk mengubah keadaan blok, itu membuat peta lokal baru di objek AbstractState Berbeda dari sebelumnya, dan kemudian mengeksekusi blok lagi dengan fungsi execution_block function, kita tahu fungsi ini menganalisis bytecode kode dan memberikan akses lokal, jadi jika offset nilai referensi tidak ada di peta lokal AbstractState baru, itu akan mengarah ke DoS.
Setelah mengaudit kode saya menemukan bahwa dengan menggunakan opcode MoveLoc/CopyLoc/FreeRef, kita dapat mencapai tujuan ini.
Di sini mari kita lihat fungsi copy_loc yang merupakan kode yang dipanggil oleh fungsi execution_block di jalur file:
pindahkan/bahasa/pindahkan-bytecode-verifier/src/reference_safety/abstract_state.rs
Pada baris 287, kode mencoba untuk mendapatkan nilai lokal dengan LocalIndex sebagai parameter dan jika LocalIndex tidak ada, itu akan menyebabkan kepanikan, Pencitraan, ketika node mengeksekusi kode jahat ini, akan menyebabkan seluruh node crash.
0x4PoC
Inilah PoC, yang dapat Anda reproduksi di git commit:add615b64390ea36e377e2a575f8cb91c9466844
Ini adalah log kerusakan:
utas 'regression_tests::reference_analysis::PoC' panik di 'dipanggil `Option::unwrap()` pada `Tidak Ada` nilai’, bahasa/pindahkan-bytecode-verifier/src/reference_safety/abstract_state.rs:287:39
catatan: jalankan dengan `RUST_BACKTRACE=1` variabel lingkungan untuk menampilkan backtrace
Langkah-langkah pemicu DoS:
kita dapat melihat blok kode adalah cabang tanpa syarat, dan setiap kali mengeksekusi cabang instruksi terakhir (0), itu akan melompat kembali ke instruksi pertama sehingga akan memanggil fungsi execution_block dan join beberapa kali.
1. Pertama kali Di sini kita mengatur parameter ke SignatureIndex(0), lokal ke SignatureIndex(0) akan memimpin num_locals 132*2=264. Jadi setelah memanggil
Memimpin panjang lokal baru menjadi 264–256=8
2. Kedua kalinya ketika mengeksekusi fungsi execution_block dan mengeksekusi instruksi pertama copy_local(57), 57 adalah offset dari locals yang perlu push ke stack tetapi kali ini locals hanya dengan panjang 8, offset 57 tidak ada , jadi ini akan menyebabkan fungsi get(57).unwrap() tidak menghasilkan apa-apa dan panik.
Ringkasan 0x5
Ini adalah keseluruhan cerita tentang kerentanan ini. Dari situ, kita belajar bahwa:
Pertama, kerentanan ini menunjukkan bahwa tidak ada kode yang benar-benar aman. Bahasa Move melakukan verifikasi statis yang baik sebelum kode dieksekusi, tetapi seperti kerentanan ini, pemeriksaan batas sebelumnya dapat dilewati sepenuhnya melalui kerentanan luapan.
Kedua, audit kode sangat penting, karena programmer terkadang lalai. Sebagai pemimpin keamanan bahasa Move, kami akan terus menggali lebih dalam masalah keamanan Move.
Poin ketiga adalah untuk bahasa Move, kami menyarankan desainer bahasa menambahkan lebih banyak kode centang untuk mencegah situasi yang tidak terduga terjadi.
Saat ini, bahasa Pindah terutama melakukan serangkaian pemeriksaan keamanan pada tahap verifikasi, tetapi menurut saya ini tidak akan cukup. Setelah verifikasi dilewati, tidak ada terlalu banyak penguatan keamanan pada tahap runtime, yang akan memperdalam bahaya lebih lanjut, menyebabkan masalah yang lebih serius. Terakhir, kami telah menemukan kerentanan lain dalam bahasa Pindahkan, dan kami akan segera mengungkapkannya kepada Anda.