Cara menggunakan Echidna untuk menguji kontrak pintar
Instalasi
Echidna dapat diinstal melalui docker atau dengan menggunakan binari yang telah dikompilasi sebelumnya.
Echidna melalui docker
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox
Perintah terakhirnya menjalankan kotak peralatan keamanan eth di dalam docker yang memiliki akses ke direktori Anda saat ini. Anda bisa mengubah file dari host Anda, dan menjalankan peralatannya pada file dari docker
Dalam docker, jalankan :
solc-select 0.5.11cd /home/training
Binari
https://github.com/crytic/echidna/releases/tag/v1.4.0.0(opens in a new tab)
Pengantar fuzzing berbasis properti
Echidna merupakan sebuah fuzzer berbasis properti, yang kami deskripsikan dalam postingan blog kami sebelumnya (1(opens in a new tab), 2(opens in a new tab), 3(opens in a new tab)).
Fuzzing
Fuzzing(opens in a new tab) adalah sebuah teknik yang terkenal dalam komunitas keamanan. Teknik ini berfungsi untuk membuat lebih banyak atau lebih sedikit input secara acak untuk menemukan bug dalam program. Fuzzer bagi perangkat lunak tradisional (seperti AFL(opens in a new tab) atau LibFuzzer(opens in a new tab)) dikenal sebagai peralatan efisien untuk menemukan bug.
Di luar dari pembentukan input acak secara murni, ada banyak teknik dan strategi untuk membuat input yang baik, termasuk:
- Memperoleh umpan balik dari setiap eksekusi dan generasi pemandu yang menggunakannya. Misalnya, jika sebuah input yang baru saja dibuat menghasilkan penemuan jalur baru, ini dapat menjelaskan cara membuat input baru yang dekat dengannya.
- Menghasilkan input yang sesuai dengan batasan struktural. Misalnya, jika input Anda berisi header dengan checksum, akan masuk akal untuk mengizinkan fuzzer membuat input yang memvalidasi checksum.
- Menggunakan input yang diketahui untuk membuat input baru: jika Anda memiliki akses ke kumpulan data besar input yang valid, fuzzer Anda dapat membuat input yang baru darinya, daripada memulai generasinya dari awal. Ini biasa disebut seed.
Fuzzing berbasis properti
Echidna termasuk pada keluarga fuzzer khusus: fuzzing berbasis properti yang sangat diinspirasikan oleh QuickCheck(opens in a new tab). Berlawanan dengan fuzzer klasik yang akan mencoba menemukan kesalahan, Echidna akan mencoba memecah invarian yang ditentukan oleh pengguna.
Dalam kontrak pintar, invarian adalah fungsi Solidity, yang dapat mewakili state yang salah atau tidak valid yang dapat dicapai oleh kontrak, yang mencakup:
- Kontrol akses yang salah: penyerang menjadi pemilik kontrak.
- Mesin state yang salah: token dapat ditransfer sementara kontrak dijeda.
- Aritmatika yang salah: pengguna dapat melakukan underflow terhadap saldonya dan mendapatkan token gratis yang tak terbatas.
Menguji properti dengan Echidna
Kita akan melihat cara menguji kontrak pintar dengan Echidna. Targetnya adalah kontrak pintar token.sol
(opens in a new tab) berikut ini:
1contract Token{2 mapping(address => uint) public balances;3 function airdrop() public{4 balances[msg.sender] = 1000;5 }6 function consume() public{7 require(balances[msg.sender]>0);8 balances[msg.sender] -= 1;9 }10 function backdoor() public{11 balances[msg.sender] += 1;12 }13}Tampilkan semuaSalin
Kita akan membuat asumsi bahwa token ini harus memiliki properti berikut ini:
- Siapa pun dapat memiliki maksimum 1000 token
- Token tidak dapat ditransfer (ini bukan token ERC20)
Tulis sebuah properti
Properti Echidna merupakan fungsi Solidity. Sebuah properti harus:
- Tidak memiliki argumen
- Mengembalikan
true
jika berhasil - Memiliki nama yang dimulai dengan
echidna
Echidna akan:
- Secara otomatis membuat transaksi arbitrari untuk menguji properti.
- Melaporkan transaksi mana pun yang menyebabkan properti mengembalikan
false
atau yang menghasilkan kesalahan. - Membuang efek samping saat memanggil properti (mis. jika properti mengubah variabel state, ini akan dibuang setelah tes selesai)
Properti berikut memeriksa bahwa pemanggil tidak memiliki lebih dari 1000 token:
1function echidna_balance_under_1000() public view returns(bool){2 return balances[msg.sender] <= 1000;3}Salin
Gunakan warisan untuk memisahkan kontrak Anda dari properti Anda:
1contract TestToken is Token{2 function echidna_balance_under_1000() public view returns(bool){3 return balances[msg.sender] <= 1000;4 }5 }Salin
token.sol
(opens in a new tab) mengimplementasikan properti dan mewariskannya dari token.
Mulai sebuah kontrak
Echidna memerlukan sebuah pembangun tanpa argumen. Jika kontrak Anda memerlukan inisialisasi khusus, Anda perlu melakukannya di pembangun.
Ada beberapa alamat khusus di Echidna:
0x00a329c0648769A73afAc7F9381E08FB43dBEA72
yang memanggil pembangun.0x10000
,0x20000
, dan0x00a329C0648769a73afAC7F9381e08fb43DBEA70
yang secara acak memanggil fungsi lainnya.
Kita tidak memiliki inisialisasi khusus mana pun dalam contoh kita saat ini, akibatnya pembangun kita kosong.
Jalankan Echidna
Echidna diluncurkan dengan:
$ echidna-test contract.sol
Jika contract.sol berisi berbagai kontrak, Anda dapat menentukan target:
$ echidna-test contract.sol --contract MyContract
Ringkasan: Menguji sebuah properti
Berikut ini merangkum operasi echidna dalam contoh kita:
1contract TestToken is Token{2 constructor() public {}3 function echidna_balance_under_1000() public view returns(bool){4 return balances[msg.sender] <= 1000;5 }6 }Salin
$ echidna-test testtoken.sol --contract TestToken...echidna_balance_under_1000: failed!💥Call sequence, shrinking (1205/5000):airdrop()backdoor()...Tampilkan semua
Echidna menemukan bahwa properti dilanggar jika backdoor
dipanggil.
Menyaring fungsi untuk memanggil saat kampanye fuzzing berlangsung
Kita akan melihat cara menyaring fungsi untuk di-fuzz. Targetnya adalah kontrak pintar berikut ini:
1contract C {2 bool state1 = false;3 bool state2 = false;4 bool state3 = false;5 bool state4 = false;67 function f(uint x) public {8 require(x == 12);9 state1 = true;10 }1112 function g(uint x) public {13 require(state1);14 require(x == 8);15 state2 = true;16 }1718 function h(uint x) public {19 require(state2);20 require(x == 42);21 state3 = true;22 }2324 function i() public {25 require(state3);26 state4 = true;27 }2829 function reset1() public {30 state1 = false;31 state2 = false;32 state3 = false;33 return;34 }3536 function reset2() public {37 state1 = false;38 state2 = false;39 state3 = false;40 return;41 }4243 function echidna_state4() public returns (bool) {44 return (!state4);45 }46}Tampilkan semuaSalin
Contoh kecil ini memaksa Echidna menemukan urutan transaksi spesifik untuk mengubah variabel state. Ini sulit bagi sebuah fuzzer (direkomendasikan untuk menggunakan peralatan eksekusi simbolis seperti Manticore(opens in a new tab)). Kita dapat menjalankan Echidna untuk memverifikasi ini:
$ echidna-test multi.sol...echidna_state4: passed! 🎉Seed: -3684648582249875403
Menyaring fungsi
Echidna memiliki masalah dalam menemukan urutan yang benar untuk menguji kontrak ini karena dua fungsi pengaturan ulang (reset1
dan reset2
) akan menetapkan semua variabel state ke false
. Namun, kita dapat menggunakan fitur spesial Echidna untuk membuat daftar hitam fungsi pengaturan ulang atau membuat daftar putih hanya untuk fungsi f
, g
, h
dan i
.
Untuk membuat daftar hitam fungsi, kita dapat menggunakan file konfigurasi ini:
1filterBlacklist: true2filterFunctions: ["reset1", "reset2"]
Pendekatan lainnya untuk menyaring fungsi adalah mencantumkan fungsi yang dimasukkan ke dalam daftar putih. Untuk itu, kita dapat menggunakan file konfigurasi ini:
1filterBlacklist: false2filterFunctions: ["f", "g", "h", "i"]
filterBlacklist
bersifattrue
secara default.- Penyaringan akan dilakukan berdasarkan nama saja (tanpa parameter). Jika Anda memiliki
f()
danf(uint256)
,"f"
penyaring akan memasangkan kedua fungsi tersebut.
Jalankan Echidna
Untuk menjalankan Echidna dengan file konfigutasi blacklist.yaml
:
$ echidna-test multi.sol --config blacklist.yaml...echidna_state4: failed!💥Call sequence:f(12)g(8)h(42)i()
Echidna akan segera menemukan urutan transaksi untuk memalsukan properti.
Ringkasan: Fungsi penyaringan
Echidna dapat membuat daftar hitam atau daftar putih fungsi yang dipanggil pada saat kampanye fuzzing dengan menggunakan:
1filterBlacklist: true2filterFunctions: ["f1", "f2", "f3"]
$ echidna-test contract.sol --config config.yaml...
Echidna memulai kampanye fuzzing dengan membuat daftar hitam f1
, f2
dan f3
atau hanya memanggil ini, berdasarkan nilai dari boolean filterBlacklist
.
Bagaimana cara menguji assert Solidity dengan Echidna
Dalam tutorial pendek ini, kami akan menunjukkan bagaimana cara menggunakan Echidna untuk menguji pemeriksaan fungsi penegasan dalam kontrak. Anggaplah kita memiliki sebuah kontrak seperti ini:
1contract Incrementor {2 uint private counter = 2**200;34 function inc(uint val) public returns (uint){5 uint tmp = counter;6 counter += val;7 // tmp <= counter8 return (counter - tmp);9 }10}Tampilkan semuaSalin
Tulis sebuah penegasan
Kita ingin memastikan bahwa tmp
kurang dari atau sama dengan counter
setelah mengembalikan perbedaannya. Kita dapat menulis sebuah properti Echidna, tetapi kita akan perlu menyimpan nilai tmp
di suatu tempat. Sebagai gantinya, kita dapat menggunakan sebuah penegasan seperti yang ini:
1contract Incrementor {2 uint private counter = 2**200;34 function inc(uint val) public returns (uint){5 uint tmp = counter;6 counter += val;7 assert (tmp <= counter);8 return (counter - tmp);9 }10}Tampilkan semuaSalin
Jalankan Echidna
To enable the assertion failure testing, create an Echidna configuration file(opens in a new tab) config.yaml
:
1checkAsserts: true
Ketika kita menjalankan kontrak di Echidna, kita mendapatkan hasil yang diharapkan:
$ echidna-test assert.sol --config config.yamlMenganalisis kontrak: assert.sol:Incrementorassertion in inc: failed!💥Urutan pemanggilan, menyusut (2596/5000):inc(21711016731996786641919559689128982722488122124807605757398297001483711807488)inc(7237005577332262213973186563042994240829374041602535252466099000494570602496)inc(86844066927987146567678238756515930889952488499230423029593188005934847229952)Seed: 1806480648350826486Tampilkan semua
Seperti yang Anda dapat lihat, Echidna melaporkan beberapa kegagalan penegasan dalam fungsi inc
. Dimungkinkan menambahkan lebih dari satu penegasan per fungsi, tetapi Echidna tidak dapat mengetahui penegasan mana yang gagal.
Kapan dan bagaimana cara menggunakan penegasan
Penegasan dapat digunakan sebagai alternatif untuk properti eksplisit, secara khusus jika kondisi untuk memeriksa terkait langsung dengan penggunaan beberapa operasi f
yang benar. Adding assertions after some code will enforce that the check will happen immediately after it was executed:
1function f(..) public {2 // some complex code3 ...4 assert (condition);5 ...6}7Salin
Sebaliknya, menggunakan properti eksplisit echidna akan secara acak mengeksekusi transaksi dan tidak ada cara mudah untuk menerapkannya secara persis ketika akan diperiksa. Namun, masih mungkin untuk menyelesaikan ini:
1function echidna_assert_after_f() public returns (bool) {2 f(..);3 return(condition);4}Salin
Namun, ada beberapa masalah:
- Gagal jika
f
dideklarasikan sebagaiinternal
atauexternal
. - Tidak jelas argumen mana yang harus digunakan untuk memanggil
f
. - Jika
f
melakukan pembalikan, properti akan gagal.
Secara umum, kami menyarankan mengikuti rekomendasi John Regehr(opens in a new tab) tentang bagaimana cara menggunakan penegasan:
- Jangan memaksakan efek samping apa pun pada saat pemeriksaan penegasan dilakukan. Misalnya:
assert(ChangeStateAndReturn() == 1)
- Jangan menegaskan pernyataan yang jelas. Misalnya
assert(var >= 0)
di manavar
dideklarasikan sebagaiuint
.
Akhirnya, harap tidak menggunakan require
ketimbang assert
, karena Echidna tidak akan dapat mendeteksinya (tetapi kontraknya bagaimana pun akan melakukan pembalikan).
Ringkasan: Pemeriksaan Penegasan
Berikut ini merangkum operasi echidna dalam contoh kita:
1contract Incrementor {2 uint private counter = 2**200;34 function inc(uint val) public returns (uint){5 uint tmp = counter;6 counter += val;7 assert (tmp <= counter);8 return (counter - tmp);9 }10}Tampilkan semuaSalin
$ echidna-test assert.sol --config config.yamlAnalyzing contract: assert.sol:Incrementorassertion in inc: failed!💥Call sequence, shrinking (2596/5000):inc(21711016731996786641919559689128982722488122124807605757398297001483711807488)inc(7237005577332262213973186563042994240829374041602535252466099000494570602496)inc(86844066927987146567678238756515930889952488499230423029593188005934847229952)Seed: 1806480648350826486Tampilkan semua
Echidna menemukan bahwa penegasan dalam inc
dapat gagal jika fungsi ini dipanggil beberapa kali dengan argumen yang besar.
Mengumpulkan dan memodifikasi sebuah korpus Echidna
Kita akan melihat bagaimana cara mengumpulkan dan menggunakan sebuah korpus transaksi dengan Echidna. Targetnya adalah kontrak pintar magic.sol
(opens in a new tab) berikut ini:
1contract C {2 bool value_found = false;3 function magic(uint magic_1, uint magic_2, uint magic_3, uint magic_4) public {4 require(magic_1 == 42);5 require(magic_2 == 129);6 require(magic_3 == magic_4+333);7 value_found = true;8 return;9 }1011 function echidna_magic_values() public returns (bool) {12 return !value_found;13 }1415}Tampilkan semuaSalin
Contoh kecil ini memaksa Echidna menemukan nilai tertentu untuk mengubah sebuah variabel state. Ini sulit bagi sebuah fuzzer (direkomendasikan untuk menggunakan peralatan eksekusi simbolis seperti Manticore(opens in a new tab)). Kita dapat menjalankan Echidna untuk memverifikasi ini:
$ echidna-test magic.sol...echidna_magic_values: passed! 🎉Seed: 2221503356319272685
Namun, kita masih dapat menggunakan Echidna untuk mengumpulkan korpus ketika menjalankan kampanye fuzzing ini.
Mengumpulkan sebuah korpus
Untuk memungkinkan pengumpulan korpus, buat sebuah direktori korpus:
$ mkdir corpus-magic
Dan file konfigurasi Echidna(opens in a new tab) config.yaml
:
1coverage: true2corpusDir: "corpus-magic"
Sekarang kita dapat menjalankan peralatan kita dan memeriksa korpus yang dikumpulkan:
$ echidna-test magic.sol --config config.yaml
Echidna masih tidak dapat menemukan nilai magic yang benar, tetapi kita dapat melihat korpus yang dikumpulkannya. Sebagai contoh, salah satu dari file ini adalah:
1[2 {3 "_gas'": "0xffffffff",4 "_delay": ["0x13647", "0xccf6"],5 "_src": "00a329c0648769a73afac7f9381e08fb43dbea70",6 "_dst": "00a329c0648769a73afac7f9381e08fb43dbea72",7 "_value": "0x0",8 "_call": {9 "tag": "SolCall",10 "contents": [11 "magic",12 [13 {14 "contents": [15 256,16 "93723985220345906694500679277863898678726808528711107336895287282192244575836"17 ],18 "tag": "AbiUInt"19 },20 {21 "contents": [256, "334"],22 "tag": "AbiUInt"23 },24 {25 "contents": [26 256,27 "68093943901352437066264791224433559271778087297543421781073458233697135179558"28 ],29 "tag": "AbiUInt"30 },31 {32 "tag": "AbiUInt",33 "contents": [256, "332"]34 }35 ]36 ]37 },38 "_gasprice'": "0xa904461f1"39 }40]Tampilkan semuaSalin
Jelas, input ini tidak akan memicu kegagalan dalam properti kita. Namun, dalam tahap berikutnya, kita akan melihat bagaimana cara untuk memodifikasinya untuk tujuan itu.
Membuat seed dari sebuah korpus
Echidna membutuhkan beberapa bantuan untuk menangani fungsi magic
. Kita akan menyalin dan memodifikasi input agar parameter yang sesuai digunakan untuknya:
$ cp corpus/2712688662897926208.txt corpus/new.txt
Kita akan memodifikasi new.txt
untuk memanggil magic(42,129,333,0)
. Sekarang, kita dapat menjalankan Echidna kembali:
$ echidna-test magic.sol --config config.yaml...echidna_magic_values: failed!💥Call sequence:magic(42,129,333,0)Unique instructions: 142Unique codehashes: 1Seed: -7293830866560616537Tampilkan semua
This time, it found that the property is violated immediately.
Menemukan transaksi dengan pemakaiaan gas yang tinggi
We will see how to find the transactions with high gas consumption with Echidna. Targetnya adalah kontrak pintar berikut ini:
1contract C {2 uint state;34 function expensive(uint8 times) internal {5 for(uint8 i=0; i < times; i++)6 state = state + i;7 }89 function f(uint x, uint y, uint8 times) public {10 if (x == 42 && y == 123)11 expensive(times);12 else13 state = 0;14 }1516 function echidna_test() public returns (bool) {17 return true;18 }1920}Tampilkan semuaSalin
Di sini expensive
dapat memiliki pemakaian gas yang besar.
Saat ini, Echidna selalu memerlukan sebuah properti untuk melakukan pengujian: di sini echidna_test
selalu mengembalikan true
. Kita dapat menjalankan Echidna untuk memverifikasi ini:
1$ echidna-test gas.sol2...3echidna_test: passed! 🎉45Seed: 2320549945714142710
Mengukur Pemakaian Gas
To enable the gas consumption with Echidna, create a configuration file config.yaml
:
1estimateGas: true
Dalam contoh ini, kita juga akan mengurangi ukuran urutan transaksi untuk membuat hasilnya lebih mudah dimengerti:
1seqLen: 22estimateGas: true
Run Echidna
Setelah kita memiliki file konfigurasi yang telah dibuat, kita dapat menjalankan Echidna seperti ini:
$ echidna-test gas.sol --config config.yaml...echidna_test: passed! 🎉f used a maximum of 1333608 gasCall sequence:f(42,123,249) Gas price: 0x10d5733f0a Time delay: 0x495e5 Block delay: 0x88b2Unique instructions: 157Unique codehashes: 1Seed: -325611019680165325Tampilkan semua
- Gas yang diperlihatkan adalah sebuah perkiraan yang disediakan oleh HEVM(opens in a new tab).
Menyaring Pemanggilan Pengurangan Gas
Tutorial tentang menyaring fungsi untuk melakukan pemanggilan pada saat kampanye fuzzing di atas menunjukkan cara menghilangkan beberapa fungsi dari pengujian Anda.
Langkah ini dapat menjadi sangat penting untuk mendapatkan sebuah perkiraan gas yang akurat. Perhatikan contoh berikut:
1contract C {2 address [] addrs;3 function push(address a) public {4 addrs.push(a);5 }6 function pop() public {7 addrs.pop();8 }9 function clear() public{10 addrs.length = 0;11 }12 function check() public{13 for(uint256 i = 0; i < addrs.length; i++)14 for(uint256 j = i+1; j < addrs.length; j++)15 if (addrs[i] == addrs[j])16 addrs[j] = address(0x0);17 }18 function echidna_test() public returns (bool) {19 return true;20 }21}Tampilkan semuaSalin
Jika Echidna dapat memanggil semua fungsi, fungsi ini tidak akan dengan mudah menemukan transaksi dengan harga has yang tinggi:
1$ echidna-test pushpop.sol --config config.yaml2...3pop used a maximum of 10746 gas4...5check used a maximum of 23730 gas6...7clear used a maximum of 35916 gas8...9push used a maximum of 40839 gasTampilkan semua
Itu karena harganya bergantung pada ukuran addrs
dan pemanggilan acak cenderung membuat array hampir kosong. Namun, memasukkan pop
dan clear
ke dalam daftar hitam memberikan kita hasil yang lebih baik:
1filterBlacklist: true2filterFunctions: ["pop", "clear"]
1$ echidna-test pushpop.sol --config config.yaml2...3push used a maximum of 40839 gas4...5check used a maximum of 1484472 gas
Ringkasan: Menemukan transaksi dengan pemakaian gas yang tinggi
Echidna dapat menemukan transaksi dengan pemakaian gas yang tinggi dengan menggunakan opsi konfigurasi estimateGas
:
1estimateGas: true
$ echidna-test contract.sol --config config.yaml...
Echidna akan melaporkan urutan dengan pemakaian gas maksimum untuk setiap fungsi, setelah kampanye fuzzing selesai.
Terakhir diedit: @pettinarip(opens in a new tab), 4 Desember 2023