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-toolbox
Son komut, geçerli dizininize erişimi olan bir docker'da eth-security-toolbox'ı çalıştırır. Dosyaları ana makinenizden değiştirebilir ve dosyalar üzerindeki araçları docker'dan çalıştırabilirsiniz
Docker'ın içinde şunu çalıştırın:
solc-select 0.5.11cd /home/training
İkili
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 tanımladığımız bir özellik tabanlı bulandırıcıdır (1(opens in a new tab), 2(opens in a new tab), 3(opens in a new tab)).
Bulandırma
Bulandırma (opens in a new tab) (Fuzzing), güvenlik topluluğunda iyi bilinen bir tekniktir. Programdaki hataları bulmak için hemen hemen rastgele girdiler oluşturmayı içerir. Geleneksel yazılım için bulandırıcılar (AFL(opens in a new tab) veya LibFuzzer(opens in a new tab) gibi) hata tespiti için verimli araçlar olarak bilinirler.
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çıyorsa, ona yakın yeni girdiler oluşturmak mantıklı olabilir.
- Yapısal bir kısıtlamaya göre girdi oluşturma. Örneğin, girdiniz sağlama toplamı olan 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: Eğer büyük bir geçerli girdi veri setine erişiminiz varsa, bulandırıcınız sıfırdan üretime başlamak yerine onlardan yeni girdiler üretebilir. Bunlara genellikle tohum denir.
Özellik temelli bulandırma
Echidna spesifik bir bulandırıcı ailesine mensuptur: özellik temelli bulandırma çoğunlukla QuickCheck(opens in a new tab)'ten ilham almıştır. Çökmeleri bulmaya çalışan klasik bulandırıcını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 herhangi bir yanlış veya geçersiz durumu temsil edebilen Solidity fonksiyonlarıdır:
- Hatalı erişim denetimi: Saldırgan sözleşmenin sahibi oldu.
- Hatalı durum makinesi: Sözleşme duraklatılmışken token'lar aktarılabilir.
- Hatalı aritmetik: Kullanıcı bakiyesini yetersiz gösterip sınırsız ücretsiz token alabilir.
Echidna ile bir özelliği test etme
Echidna ile akıllı bir sözleşmenin nasıl test edileceğini göreceğiz. Hedef, aşağıdaki akıllı sözleşme token.sol
'dür(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österKopyala
Bu token'ın aşağıdaki özelliklere sahip olması gerektiği varsayımını yapacağız:
- Herkes en fazla 1000 token'a sahip olabilir
- Token transfer edilemez (bir ERC20 token'ı değildir)
Bir özellik yazın
Echidna özellikleri, Solidity fonksiyonlarıdır. Bir özellikte şunlar bulunmalı:
- Argümanı olmamalı
- Başarılıysa
true
döndürmeli - Adı
echidna
ile başlıyor olmalı
Echidna şunları yapacaktır:
- Özelliği test etmek için otomatik olarak rastgele işlemler oluşturacak.
- Bir özelliğin
false
döndürmesine veya bir hata vermesine neden olan tüm işlemleri bildirecek. - Bir özelliği çağırırken yan etkiyi atacak (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 token'a sahip olup olmadığını kontrol eder:
1function echidna_balance_under_1000() public view returns(bool){2 return balances[msg.sender] <= 1000;3}Kopyala
Sözleşmenizi özelliklerinizden 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 }Kopyala
token.sol
(opens in a new tab) özelliği uygular ve token'dan kalıtım gerçekleştirir.
Bir sözleşme başlatın
Echidna, argümanı olmayan bir yapıcıya ihtiyaç duyar. Sözleşmenizin özel bir başlatmaya ihtiyacı varsa, bunu yapıcıda yapmanız gerekir.
Echidna'da bazı özel adresler vardır:
0x00a329c0648769A73afAc7F9381E08FB43dBEA72
yapıcıyı çağırır.0x10000
,0x20000
, ve0x00a329C0648769a73afAC7F9381e08fb43DBEA70
diğer fonksiyonları rastgele çağırır.
Mevcut örneğimizde herhangi bir özel başlatmaya ihtiyacımız yok, bu yüzden yapıcımız boş.
Echidna'yı çalıştırın
Echidna şöyle başlatılır:
echidna-test contract.sol
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ğıdaki, örneğimizde echidna'nın çalışmasını özetler:
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 }Kopyala
echidna-test testtoken.sol --contract TestToken...echidna_balance_under_1000: failed!💥Call sequence, shrinking (1205/5000):airdrop()backdoor()...Tümünü göster
Echidna, backdoor
çağrılırsa özelliğin ihlal edildiğini tespit etti.
Bir bulandırma işlemi sırasında çağrılacak filtreleme işlevleri
Bulandırılacak 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österKopyala
Bu küçük örnek, Echidna'yı bir durum değişkenini değiştirmek için belirli bir işlem dizisini 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: -3684648582249875403
Fonksiyonları filtreleme
Echidna, iki sıfırlama fonksiyonu (reset1
ve reset2
) tüm durum değişkenlerini false
olarak ayarlayacağından, bu sözleşmeyi test etmek için doğru sırayı bulmakta zorlanıyor. 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ğini kullanabiliriz.
İşlevleri 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ınan fonksiyonları listelemektir. Bunu yapmak için şu yapılandırma dosyasını kullanabiliriz:
1filterBlacklist: false2filterFunctions: ["f", "g", "h", "i"]
filterBlacklist
varsayılan olaraktrue
hâldedir.- Filtreleme sadece ada göre yapılacaktır (parametreler olmadan). Eğer
f()
vef(uint256)
varsa,"f"
filtresi iki fonksiyon ile de eşleşecektir.
Echidna'yı çalıştırın
Echidna'yı bir blacklist.yaml
yapılandırma dosyası ile ç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 tahrif edecek işlemlerin sırasını neredeyse anında bulacaktır.
Özet: Fonksiyonları filtreleme
Echidna, aşağıdakileri kullanarak bulanıklaştırma çalışması sırasında çağrılacak fonksiyonları kara veya beyaz listeye alabilir:
1filterBlacklist: true2filterFunctions: ["f1", "f2", "f3"]
echidna-test contract.sol --config config.yaml...
Echidna, f1
, f2
ve f3
'ü kara listeye alarak veya filterBlacklist
boolean değerine göre yalnızca bunları çağırarak bir bulanıklaştırma çalışması başlatır.
Solidity'nin teyidi Echidna ile nasıl test edilir
Bu kısa öğreticide, sözleşmelerde teyit kontrolünü test etmek için Echidna'nın nasıl kullanılacağını göstereceğiz. Diyelim ki şuna benzer bir sözleşmemiz 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österKopyala
Bir teyit yazın
Farkını döndürdükten sonra tmp
öğesinin counter
değerinden küçük veya eşit olduğundan emin olmak istiyoruz. Bir Echidna özelliği yazabiliriz, ancak tmp
değerini bir yerde saklamamız gerekecek. Onun yerine, bunun gibi bir teyit kullanabilirdik:
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österKopyala
Echidna'yı çalıştırın
Teyit hatası testini etkinleştirmek için bir Echidna yapılandırma dosyası(opens in a new tab) config.yaml
oluşturun:
1checkAsserts: true
Bu sözleşmeyi Echidna'da çalıştırdığımızda, beklenen sonuçları elde ediyoruz:
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öster
Gördüğünüz gibi, Echidna inc
fonksiyonunda bazı onaylama hataları bildiriyor. Fonksiyon başına birden fazla teyit eklemek mümkündür, ancak Echidna hangi iddianın başarısız olduğunu söyleyemez.
Teyitler nerede ve nasıl kullanılır
Teyitler, özellikle kontrol edilecek koşullar bazı f
işlemlerinin doğru kullanımıyla doğrudan ilgiliyse, açık özelliklere alternatif olarak kullanılabilir. Bazı kodlardan sonra teyitler eklemek, kontrolün yürütüldükten hemen sonra yapılmasını zorunlu kılar:
1function f(..) public {2 // some complex code3 ...4 assert (condition);5 ...6}7Kopyala
Aksine, 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. Bu geçici çözümü yapmak hâlâ mümkündür:
1function echidna_assert_after_f() public returns (bool) {2 f(..);3 return(condition);4}Kopyala
Ancak, bazı sorunlar vardır:
f
internal
veyaexternal
olarak duyurulursa başarısız olur.f
'u çağırmak için hangi bağımsız değişkenlerin kullanılması gerektiği açık değil.f
geri dönerse, özellik başarısız olur.
Genel olarak, teyitlerin nasıl kullanılacağına ilişkin John Regehr'in tavsiyesini(opens in a new tab) izlemenizi öneririz:
- Teyit kontrolü sırasında herhangi bir yan etkiyi zorlamayın. Örnek olarak:
assert(ChangeStateAndReturn() == 1)
- Açık ifadeleri teyit etmeyin. Örnek olarak
var
'ınuint
olarak duyurulduğu yerdeassert(var >= 0)
olması gibi.
Son olarak, Echidna bunu algılamayacağı (ancak sözleşme yine de geri dönecek) için lütfen assert
yerine require
kullanmayın.
Özet: Teyit Kontrolü
Aşağıdakiler, örneğimizde echidna'nın çalışmasını özetler:
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österKopyala
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öster
Echidna, bu fonksiyon büyük argümanlarla birden çok kez çağrılırsa inc
içindeki teyidin başarısız olabileceğini buldu.
Bir Echidna korpusunu toplama ve değiştirme
Echidna ile bir işlem korpusunun nasıl toplanıp kullanılacağını göreceğiz. Hedef, aşağıdaki akıllı sözleşme magic.sol
'dur(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österKopyala
Bu 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: 2221503356319272685
Ancak, bu bulandırma çalışmasını yürütürken korpus toplamak için hâlâ Echidna'yı kullanabiliriz.
Bir korpus toplama
Korpus toplamayı etkinleştirmek için bir korpus dizini oluşturun:
mkdir corpus-magic
Bir Echidna konfigürasyon dosyası(opens in a new tab) config.yaml
da oluşturun:
1coverage: true2corpusDir: "corpus-magic"
Şimdi aracımızı çalıştırabilir ve toplanan korpusu kontrol edebiliriz:
echidna-test magic.sol --config config.yaml
Echidna hâlâ 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österKopyala
Bu girdinin özelliğimizdeki başarısızlığı tetiklemeyeceği açıktır. Ancak bir sonraki adımda bunun için nasıl yapılandırılabileceğini göreceğiz.
Bir korpus tohumlama
Echidna'nın magic
fonksiyonuyla başa çıkabilmesi için biraz yardıma ihtiyacı var. Bunun için uygun parametreleri kullanmak için girdiyi kopyalayıp değiştireceğiz:
cp corpus/2712688662897926208.txt corpus/new.txt
magic(42,129,333,0)
çağırması için new.txt
'yi düzenleyeceğ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öster
Bu kez, özelliğin ihlal edildiğini hemen tespit etti.
Yüksek gaz tüketimi olan işlemleri bulma
Echidna ile yüksek gaz tüketimi olan işlemleri nasıl bulacağımızı 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österKopyala
Burada expensive
büyük 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
değerini döndürür. Bunu doğrulamak için Echidna'yı çalıştırabiliriz:
1echidna-test gas.sol2...3echidna_test: passed! 🎉45Seed: 2320549945714142710
Gaz Tüketimini Hesaplama
Echidna ile gaz tüketimini etkinleştirmek için bir config.yaml
yapılandırma dosyası oluşturun:
1estimateGas: true
Bu örnekte, sonuçların anlaşılmasını kolaylaştırmak için işlem sırasının boyutunu da azaltacağız:
1seqLen: 22estimateGas: true
Echidna'yı çalıştırın
Yapılandırma dosyasını oluşturduktan sonra Echidna'yı şu şekilde çalıştırabiliriz:
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: -325611019680165325Tümünü göster
- Gösterilen gaz HEVM(opens in a new tab) tarafından sağlanan bir tahmindir.
Gaz Düşürücü Çağrıları Filtreleme
Yukarıdaki bir bulandırma çalışması sırasında çağrılacak fonksiyonları filtreleme hakkındaki öğretici, bazı fonksiyonların testinizden nasıl kaldırılacağını gösterir.
Bu, doğru bir gaz tahmini elde etmek için kritik öneme sahiptir. Aşağıdaki örneği göz önünde bulundurun:
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österKopyala
Echidna tüm fonksiyonları çağırabilirse yüksek gaz maliyeti olan 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öster
Bunun nedeni, maliyetin addrs
boyutuna bağlı olması ve rastgele aramaların diziyi neredeyse boş bırakma eğiliminde olmasıdır. Ancak pop
ve clear
'ı 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üketimi olan işlemleri bulma
Echidna, estimateGas
yapılandırma seçeneğini kullanarak yüksek gaz tüketimi olan işlemleri bulabilir:
1estimateGas: true
echidna-test contract.sol --config config.yaml...
Echidna, bulandırma çalışması sona erdiğinde her fonksiyon için maksimum gaz tüketimine sahip bir dizi raporlayacaktır.
Son düzenleme: @pettinarip(opens in a new tab), 4 Aralık 2023