Akıllı sözleşmeleri test etmek için Echidna nasıl kullanılır
Kurulum
Echidna, docker aracılığıyla veya önceden derlenmiş ikili dosya kullanılarak kurulabilir.
Docker aracılığıyla Echidna
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/training trailofbits/eth-security-toolboxSon komut, geçerli dizininize erişimi olan bir docker'da eth-security-toolbox'ı çalıştırır. Dosyaları ana makinenizden değiştirebilir ve docker'dan dosyalar üzerindeki araçları çalıştırabilirsiniz
Docker'ın içinde şunu çalıştırın:
solc-select 0.5.11cd /home/trainingİkili dosya
https://github.com/crytic/echidna/releases/tag/v1.4.0.0 (opens in a new tab)
Özellik tabanlı bulandırmaya giriş
Echidna, önceki blog yazılarımızda (1 (opens in a new tab), 2 (opens in a new tab), 3 (opens in a new tab)) açıkladığımız gibi özellik tabanlı bir bulandırıcıdır.
Bulandırma
Bulandırma (opens in a new tab), güvenlik topluluğunda iyi bilinen bir tekniktir. Programdaki hataları bulmak için aşağı yukarı rastgele girdiler oluşturmayı içerir. Geleneksel yazılımlar için bulandırıcıların (AFL (opens in a new tab) veya LibFuzzer (opens in a new tab) gibi) hata bulmak için etkili araçlar olduğu bilinmektedir.
Tamamen rastgele girdi oluşturmanın ötesinde, iyi girdiler üretmek için aşağıdakiler de dahil olmak üzere birçok teknik ve strateji vardır:
- Her yürütmeden geri bildirim alın ve bunu oluşturmaya rehberlik etmek için kullanın. Örneğin, yeni oluşturulan bir girdi yeni bir yolun keşfine yol açarsa, ona yakın yeni girdiler oluşturmak mantıklı olabilir.
- Yapısal bir kısıtlamaya göre girdi oluşturma. Örneğin, girdiniz bir sağlama toplamı içeren bir başlık içeriyorsa, bulandırıcının sağlama toplamını doğrulayan girdi oluşturmasına izin vermek mantıklı olacaktır.
- Yeni girdiler oluşturmak için bilinen girdileri kullanma: Büyük bir geçerli girdi veri kümesine erişiminiz varsa, bulandırıcınız sıfırdan oluşturmaya başlamak yerine bunlardan yeni girdiler üretebilir. Bunlara genellikle tohumlar denir.
Özellik tabanlı bulandırma
Echidna, özel bir bulandırıcı ailesine aittir: büyük ölçüde QuickCheck (opens in a new tab)'ten ilham alan özellik tabanlı bir bulandırma. Çökmeleri bulmaya çalışan klasik bulandırıcıların aksine Echidna, kullanıcı tanımlı değişmezleri kırmaya çalışacaktır.
Akıllı sözleşmelerde değişmezler, sözleşmenin ulaşabileceği ve aşağıdakileri içeren herhangi bir yanlış veya geçersiz durumu temsil edebilen Solidity fonksiyonlarıdır:
- Yanlış erişim denetimi: Saldırgan sözleşmenin sahibi oldu.
- Yanlış durum makinesi: Sözleşme duraklatılmışken jetonlar aktarılabilir.
- Yanlış aritmetik: kullanıcı bakiyesinde bir aşağı taşma yaratarak sınırsız ücretsiz jeton alabilir.
Echidna ile bir özelliği test etme
Echidna ile bir akıllı sözleşmenin nasıl test edileceğini göreceğiz. Hedef, aşağıdaki akıllı sözleşmedir: token.sol (opens in a new tab):
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}Tümünü gösterBu jetonun aşağıdaki özelliklere sahip olması gerektiği varsayımında bulunacağız:
- Herkes en fazla 1000 jetona sahip olabilir
- Jeton transfer edilemez (bu bir ERC20 jetonu değildir)
Bir özellik yazma
Echidna özellikleri Solidity fonksiyonlarıdır. Bir özellik şunları yapmalıdır:
- Argümanı olmamalıdır
- Başarılı olursa
truedöndürmelidir - Adı
echidnaile başlamalıdır
Echidna şunları yapar:
- Özelliği test etmek için otomatik olarak rastgele işlemler oluşturur.
- Bir özelliğin
falsedöndürmesine veya bir hata atmasına neden olan tüm işlemleri bildirir. - Bir özelliği çağırırken yan etkiyi atar (yani, özellik bir durum değişkenini değiştirirse, testten sonra atılır)
Aşağıdaki özellik, çağıranın 1000'den fazla jetona sahip olmadığını kontrol eder:
1function echidna_balance_under_1000() public view returns(bool){2 return balances[msg.sender] <= 1000;3}Sözleşmenizi özelliklerinden ayırmak için kalıtım kullanın:
1contract TestToken is Token{2 function echidna_balance_under_1000() public view returns(bool){3 return balances[msg.sender] <= 1000;4 }5 }token.sol (opens in a new tab), özelliği uygular ve jetondan miras alır.
Bir sözleşme başlatma
Echidna'nın argümansız bir kurucuya ihtiyacı vardır. Sözleşmenizin belirli bir başlatmaya ihtiyacı varsa, bunu kurucuda yapmanız gerekir.
Echidna'da bazı özel adresler vardır:
0x00a329c0648769A73afAc7F9381E08FB43dBEA72, kurucuyu çağırır.0x10000,0x20000ve0x00a329C0648769a73afAC7F9381e08fb43DBEA70, diğer fonksiyonları rastgele çağırır.
Mevcut örneğimizde herhangi bir özel başlatmaya ihtiyacımız yok, bu nedenle kurucumuz boş.
Echidna'yı Çalıştırma
Echidna şununla başlatılır:
echidna-test contract.solEğer contract.sol birden fazla sözleşme içeriyorsa, hedefi belirtebilirsiniz:
echidna-test contract.sol --contract MyContractÖzet: Bir özelliği test etme
Aşağıdakiler, Echidna'nın örneğimizde çalıştırılmasını özetlemektedir:
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 }echidna-test testtoken.sol --contract TestToken...echidna_balance_under_1000: failed!💥 Call sequence, shrinking (1205/5000): airdrop() backdoor()...Tümünü gösterEchidna, backdoor çağrılırsa özelliğin ihlal edildiğini buldu.
Bir bulandırma kampanyası sırasında çağrılacak fonksiyonları filtreleme
Bulandırmaya tabi tutulacak fonksiyonların nasıl filtreleneceğini göreceğiz. Hedef, aşağıdaki akıllı sözleşmedir:
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}Tümünü gösterBu küçük örnek, Echidna'yı bir durum değişkenini değiştirmek için belirli bir işlem dizisi bulmaya zorlar. Bu bir bulandırıcı için zordur (Manticore (opens in a new tab) gibi sembolik bir yürütme aracı kullanılması önerilir). Bunu doğrulamak için Echidna'yı çalıştırabiliriz:
echidna-test multi.sol...echidna_state4: passed! 🎉Seed: -3684648582249875403Fonksiyonları filtreleme
İki sıfırlama fonksiyonu (reset1 ve reset2) tüm durum değişkenlerini false olarak ayarlayacağından, Echidna bu sözleşmeyi test etmek için doğru diziyi bulmakta zorlanır.
Ancak, sıfırlama fonksiyonunu kara listeye almak veya yalnızca f, g,
h ve i fonksiyonlarını beyaz listeye almak için özel bir Echidna özelliği kullanabiliriz.
Fonksiyonları kara listeye almak için bu yapılandırma dosyasını kullanabiliriz:
1filterBlacklist: true2filterFunctions: ["reset1", "reset2"]Fonksiyonları filtrelemek için başka bir yaklaşım, beyaz listeye alınmış fonksiyonları listelemektir. Bunu yapmak için şu yapılandırma dosyasını kullanabiliriz:
1filterBlacklist: false2filterFunctions: ["f", "g", "h", "i"]filterBlacklistvarsayılan olaraktruedeğerindedir.- Filtreleme yalnızca isme göre (parametreler olmadan) gerçekleştirilecektir.
f()vef(uint256)fonksiyonlarınız varsa,"f"filtresi her iki fonksiyonla da eşleşecektir.
Echidna'yı Çalıştırma
blacklist.yaml yapılandırma dosyasıyla Echidna'yı çalıştırmak için:
echidna-test multi.sol --config blacklist.yaml...echidna_state4: failed!💥 Call sequence: f(12) g(8) h(42) i()Echidna, özelliği yanlışlayacak işlem dizisini neredeyse anında bulacaktır.
Özet: Fonksiyonları filtreleme
Echidna, bir bulandırma kampanyası sırasında çağrılacak fonksiyonları aşağıdakileri kullanarak kara listeye veya beyaz listeye alabilir:
1filterBlacklist: true2filterFunctions: ["f1", "f2", "f3"]echidna-test contract.sol --config config.yaml...Echidna, filterBlacklist boole değerine göre ya f1, f2 ve f3'ü kara listeye alarak ya da sadece bunları çağırarak bir bulandırma kampanyası başlatır.
Echidna ile Solidity'nin assert'ünü test etme
Bu kısa öğreticide, sözleşmelerde iddia kontrolünü test etmek için Echidna'nın nasıl kullanılacağını göstereceğiz. Diyelim ki elimizde şöyle bir sözleşme var:
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}Tümünü gösterBir iddia yazma
Farklarını döndürdükten sonra tmp'nin counter'dan küçük veya ona eşit olduğundan emin olmak istiyoruz. Bir
Echidna özelliği yazabiliriz, ancak tmp değerini bir yerde saklamamız gerekir. Bunun yerine, şöyle bir iddia kullanabiliriz:
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}Tümünü gösterEchidna'yı Çalıştırma
İddia hatası testini etkinleştirmek için bir Echidna yapılandırma dosyası (opens in a new tab) olan config.yaml dosyasını oluşturun:
1checkAsserts: trueBu sözleşmeyi Echidna'da çalıştırdığımızda beklenen sonuçları elde ederiz:
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: 1806480648350826486Tümünü gösterGördüğünüz gibi, Echidna inc fonksiyonunda bir iddia hatası bildiriyor. Fonksiyon başına birden fazla iddia eklemek mümkündür, ancak Echidna hangi iddianın başarısız olduğunu söyleyemez.
İddialar ne zaman ve nasıl kullanılır
İddialar, özellikle kontrol edilecek koşullar doğrudan bir f işleminin doğru kullanımıyla ilgiliyse, açık özelliklere alternatif olarak kullanılabilir. Bir koddan sonra iddia eklemek, kontrolün kod yürütüldükten hemen sonra gerçekleşmesini sağlar:
1function f(..) public {2 // karmaşık bir kod3 ...4 assert (condition);5 ...6}7Aksine, açık bir Echidna özelliği kullanmak işlemleri rastgele yürütecektir ve tam olarak ne zaman kontrol edileceğini zorlamanın kolay bir yolu yoktur. Yine de bu geçici çözümü uygulamak mümkündür:
1function echidna_assert_after_f() public returns (bool) {2 f(..);3 return(condition);4}Ancak, bazı sorunlar var:
finternalveyaexternalolarak bildirilirse başarısız olur.ffonksiyonunu çağırmak için hangi argümanların kullanılması gerektiği belirsizdir.fgeri dönerse, özellik başarısız olur.
Genel olarak, iddiaların nasıl kullanılacağı konusunda John Regehr'in tavsiyesine (opens in a new tab) uymanızı öneririz:
- İddia kontrolü sırasında herhangi bir yan etkiyi zorlamayın. Örneğin:
assert(ChangeStateAndReturn() == 1) - Açık ifadeleri iddia etmeyin. Örneğin,
varuintolarak bildirildiğindeassert(var >= 0).
Son olarak, lütfen assert yerine require kullanmayın, çünkü Echidna bunu tespit edemeyecektir (ancak sözleşme yine de geri dönecektir).
Özet: İddia Kontrolü
Aşağıdakiler, Echidna'nın örneğimizde çalıştırılmasını özetlemektedir:
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}Tümünü gösterechidna-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: 1806480648350826486Tümünü gösterEchidna, bu fonksiyon büyük argümanlarla birden çok kez çağrılırsa inc içindeki iddianın başarısız olabileceğini buldu.
Bir Echidna korpusu toplama ve değiştirme
Echidna ile bir işlem korpusunun nasıl toplanacağını ve kullanılacağını göreceğiz. Hedef, aşağıdaki akıllı sözleşmedir: magic.sol (opens in a new tab):
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}Tümünü gösterBu küçük örnek, Echidna'yı bir durum değişkenini değiştirmek için belirli değerleri bulmaya zorlar. Bu bir bulandırıcı için zordur (Manticore (opens in a new tab) gibi sembolik bir yürütme aracı kullanılması önerilir). Bunu doğrulamak için Echidna'yı çalıştırabiliriz:
echidna-test magic.sol...echidna_magic_values: passed! 🎉Seed: 2221503356319272685Ancak, bu bulandırma kampanyasını yürütürken korpus toplamak için yine de Echidna'yı kullanabiliriz.
Bir korpus toplama
Korpus toplamayı etkinleştirmek için bir korpus dizini oluşturun:
mkdir corpus-magicVe bir Echidna yapılandırma dosyası (opens in a new tab) olan config.yaml dosyasını oluşturun:
1coverage: true2corpusDir: "corpus-magic"Şimdi aracımızı çalıştırabilir ve toplanan korpusu kontrol edebiliriz:
echidna-test magic.sol --config config.yamlEchidna hala doğru sihirli değerleri bulamıyor, ancak topladığı korpusa bakabiliriz. Örneğin, bu dosyalardan biri şuydu:
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]Tümünü gösterAçıkçası, bu girdi özelliğimizdeki başarısızlığı tetiklemeyecektir. Ancak, bir sonraki adımda bunun için nasıl değiştirileceğini göreceğiz.
Bir korpusu besleme
Echidna'nın magic fonksiyonuyla başa çıkabilmesi için biraz yardıma ihtiyacı var. Bunun için uygun
parametreleri kullanmak üzere girdiyi kopyalayıp değiştireceğiz:
cp corpus/2712688662897926208.txt corpus/new.txtmagic(42,129,333,0) fonksiyonunu çağırmak için new.txt dosyasını değiştireceğiz. Şimdi, Echidna'yı yeniden çalıştırabiliriz:
echidna-test magic.sol --config config.yaml...echidna_magic_values: failed!💥 Call sequence: magic(42,129,333,0)Unique instructions: 142Unique codehashes: 1Seed: -7293830866560616537Tümünü gösterBu kez, özelliğin anında ihlal edildiğini buldu.
Yüksek gaz tüketimli işlemleri bulma
Echidna ile yüksek gaz tüketimli işlemlerin nasıl bulunacağını göreceğiz. Hedef, aşağıdaki akıllı sözleşmedir:
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}Tümünü gösterBurada expensive yüksek bir gaz tüketimine sahip olabilir.
Şu anda, Echidna'nın test etmek için her zaman bir özelliğe ihtiyacı vardır: burada echidna_test her zaman true döndürür.
Bunu doğrulamak için Echidna'yı çalıştırabiliriz:
1echidna-test gas.sol2...3echidna_test: passed! 🎉45Seed: 2320549945714142710Gaz Tüketimini Ölçme
Echidna ile gaz tüketimini etkinleştirmek için bir config.yaml yapılandırma dosyası oluşturun:
1estimateGas: trueBu örnekte, sonuçların daha kolay anlaşılmasını sağlamak için işlem dizisinin boyutunu da azaltacağız:
1seqLen: 22estimateGas: trueEchidna'yı Çalıştırma
Yapılandırma dosyası oluşturulduktan sonra, Echidna'yı şu şekilde çalıştırabiliriz:
echidna-test gas.sol --config config.yaml...echidna_test: passed! 🎉f used a maximum of 1333608 gas Call sequence: f(42,123,249) Gas price: 0x10d5733f0a Time delay: 0x495e5 Block delay: 0x88b2Unique instructions: 157Unique codehashes: 1Seed: -325611019680165325Tümünü göster- Gösterilen gaz, HEVM (opens in a new tab) tarafından sağlanan bir tahmindir.
Gaz Azaltan Çağrıları Filtreleme
Yukarıdaki bir bulandırma kampanyası sırasında çağrılacak fonksiyonları filtreleme öğreticisi, testlerinizden bazı fonksiyonları nasıl kaldıracağınızı gösterir.
Bu, doğru bir gaz tahmini elde etmek için kritik olabilir.
Aşağıdaki örneği inceleyin:
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}Tümünü gösterEchidna tüm fonksiyonları çağırabilirse, yüksek gaz maliyetli işlemleri kolayca bulamaz:
1echidna-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 gasTümünü gösterBunun nedeni, maliyetin addrs boyutuna bağlı olması ve rastgele çağrıların diziyi neredeyse boş bırakma eğiliminde olmasıdır.
Ancak, pop ve clear fonksiyonlarını kara listeye almak bize çok daha iyi sonuçlar verir:
1filterBlacklist: true2filterFunctions: ["pop", "clear"]1echidna-test pushpop.sol --config config.yaml2...3push used a maximum of 40839 gas4...5check used a maximum of 1484472 gasÖzet: Yüksek gaz tüketimli işlemleri bulma
Echidna, estimateGas yapılandırma seçeneğini kullanarak yüksek gaz tüketimli işlemleri bulabilir:
1estimateGas: trueechidna-test contract.sol --config config.yaml...Echidna, bulandırma kampanyası bittiğinde her fonksiyon için maksimum gaz tüketimine sahip bir dizi raporlayacaktır.
Sayfanın son güncellenmesi: 21 Ekim 2025