Kerentanan dalam Kontrak Cerdas Etherium. Contoh kode

Dengan posting ini, saya memulai serangkaian artikel tentang keamanan kontrak pintar Ethereum. Saya pikir topik ini sangat relevan, karena jumlah pengembang tumbuh seperti longsoran salju, dan tidak ada yang menyelamatkan dari "rake". Sampai jumpa - terjemahan ...

1. Memindai Kontrak Ethereum Langsung untuk Kesalahan Tidak Terkirim-Kirim


Asli - Memindai Kontrak Langsung Ethereum untuk "Tidak Terkirim-Kirim ..."


Penulis: Zikai Alex Wen dan Andrew Miller

Pemrograman kontrak pintar Ethereum dikenal sebagai rawan kesalahan [1] . Baru-baru ini, kami melihat beberapa
kontrak pintar kelas atas seperti King of the Ether dan The DAO-1.0 mengandung kerentanan yang disebabkan oleh kesalahan pemrograman.

Sejak Maret 2015, programmer kontrak yang cerdas telah diperingatkan akan bahaya pemrograman tertentu yang mungkin timbul ketika kontrak saling mengirim pesan [6] .

Beberapa panduan pemrograman merekomendasikan cara untuk menghindari kesalahan umum (dalam kertas putih Ethereum [3] dan dalam panduan independen dari UMD [2] ). Meskipun bahaya-bahaya ini cukup dapat dipahami untuk menghindarinya, konsekuensi dari kesalahan seperti itu mengerikan: uang dapat diblokir, hilang atau dicuri.

Seberapa umum kesalahan yang dihasilkan dari bahaya ini? Apakah ada kontrak blockchain Ethereum yang lebih rentan tetapi hidup? Dalam artikel ini, kami menjawab pertanyaan ini dengan menganalisis kontrak pada blockchain langsung Ethereum menggunakan alat analisis baru yang kami kembangkan.


Apa kesalahan kirim-tidak dicentang?


Untuk mengirim kontrak airtime ke alamat lain, cara termudah adalah dengan menggunakan kata kunci kirim . Ini bertindak sebagai metode yang didefinisikan untuk setiap objek. Misalnya, cuplikan kode berikut dapat ditemukan dalam kontrak pintar yang menerapkan permainan papan.


/*** Listing 1 ***/ if (gameHasEnded && !( prizePaidOut ) ) { winner.send(1000); //    prizePaidOut = True; } 

Masalahnya di sini adalah bahwa metode pengiriman mungkin gagal. Jika tidak berhasil, maka pemenang tidak akan menerima uang, namun variabel hadiahPaidOut akan disetel ke True.

Ada dua kasus berbeda di mana fungsi winner.send () dapat gagal. Kami akan menganalisis perbedaan di antara mereka nanti. Kasus pertama adalah bahwa alamat pemenang adalah kontrak (bukan akun pengguna), dan kode untuk kontrak ini mengeluarkan pengecualian (misalnya, jika menggunakan terlalu banyak "gas"). Jika demikian, maka mungkin dalam kasus ini ini adalah "kesalahan pemenang". Kasus kedua kurang jelas. Mesin virtual Ethereum memiliki sumber daya terbatas yang disebut " callstack " (kedalaman tumpukan panggilan), dan sumber daya ini dapat digunakan oleh kode kontrak lain yang sebelumnya dieksekusi dalam transaksi. Jika callstack telah digunakan pada saat perintah send dieksekusi, perintah akan gagal, terlepas dari bagaimana pemenang ditentukan. Hadiah pemenang akan dihancurkan bukan karena kesalahannya sendiri!



Bagaimana kesalahan ini bisa dihindari?

Dokumentasi Ethereum berisi peringatan singkat tentang bahaya potensial ini [3] : "Ada beberapa bahaya saat menggunakan pengiriman - transmisi gagal jika kedalaman tumpukan panggilan 1024 (ini selalu dapat disebabkan oleh pemanggil), dan juga gagal jika penerima "gas" berakhir. Oleh karena itu, untuk memastikan siaran yang aman, selalu periksa nilai balik pengiriman atau lebih baik: gunakan templat di mana penerima menarik uang. "

Dua kalimat. Yang pertama adalah untuk memeriksa nilai balik dari pengiriman untuk melihat apakah itu selesai dengan sukses. Jika ini bukan masalahnya, maka lontarkan pengecualian untuk mengembalikan negara.


  /*** Listing 2 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (winner.send(1000)) prizePaidOut = True; else throw; } 

Ini adalah perbaikan yang memadai untuk contoh saat ini, tetapi tidak selalu keputusan yang tepat. Misalkan kita memodifikasi contoh kita sehingga saat pertandingan usai, pemenang dan pecundang mengembalikan kekayaan mereka. Aplikasi nyata dari solusi "formal" adalah sebagai berikut:


 /*** Listing 3 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (winner.send(1000) && loser.send(10)) prizePaidOut = True; else throw; } 

Namun, ini adalah kesalahan karena memperkenalkan kerentanan tambahan. Meskipun kode ini melindungi pemenang dari serangan callstack , kode ini juga membuat pemenang dan pecundang rentan terhadap satu sama lain. Dalam hal ini, kami ingin mencegah serangan callstack , tetapi melanjutkan eksekusi jika perintah kirim gagal karena beberapa alasan.

Oleh karena itu, bahkan praktik terbaik terbaik (direkomendasikan dalam Panduan Programmer Ethereum dan Serpent kami, meskipun berlaku sama untuk Soliditas), adalah memeriksa sumber daya callstack . Kita dapat mendefinisikan callStackIsEmpty makro () , yang akan mengembalikan kesalahan jika dan hanya jika callstack kosong.


 /*** Listing 4 ***/ if (gameHasEnded && !( prizePaidOut ) ) { if (callStackIsEmpty()) throw; winner.send(1000) loser.send(10) prizePaidOut = True; } 

Bahkan lebih baik, rekomendasi dari dokumentasi Ethereum - "Gunakan templat di mana penerima mengambil uang" agak samar, tetapi ada penjelasannya. Sarannya adalah untuk mengatur ulang kode Anda sehingga efek dari kegagalan pengiriman terisolasi dan hanya mempengaruhi satu penerima pada satu waktu. Berikut ini adalah contoh dari pendekatan ini. Namun, tip ini juga anti-pola. Dia menerima tanggung jawab untuk memeriksa callstack kepada penerima sendiri, yang memungkinkan untuk jatuh ke dalam perangkap yang sama.


 /*** Listing 5 ***/ if (gameHasEnded && !( prizePaidOut ) ) { accounts[winner] += 1000 accounts[loser] += 10 prizePaidOut = True; } ... function withdraw(amount) { if (accounts[msg.sender] >= amount) { msg.sender.send(amount); accounts[msg.sender] -= amount; } } 

Banyak kontrak pintar yang sangat maju rentan. Lotre Raja Udara dari Tahta adalah kasus paling terkenal dari kesalahan ini [4] . Kesalahan ini tidak diketahui sampai jumlah 200 eter (bernilai lebih dari 2.000 dolar AS pada harga saat ini) tidak bisa mendapatkan pemenang lotre yang sah. Kode yang sesuai di King of the Ether mirip dengan kode pada Listing 2. Untungnya, dalam hal ini, pengembang kontrak dapat menggunakan fungsi yang tidak terkait dalam kontrak sebagai "penggantian manual" untuk melepaskan dana yang macet. Administrator yang kurang cermat bisa menggunakan fungsi yang sama untuk mencuri siaran!


Lanjutan Memindai Kontrak Ethereum Langsung untuk Kesalahan Tidak Terkirim-Kirim. Bagian 2

Source: https://habr.com/ru/post/id430602/


All Articles