Ana içeriğe geç

Akıllı sözleşmeleri test etmek için Echidna nasıl kullanılır

solidityakıllı kontratlargüvenliktest etmekbulandırma
Gelişmiş
Trailofbits
Güvenli sözleşmeler oluşturmak(opens in a new tab)
10 Nisan 2020
11 dakikalık okuma minute read

Kurulum

Echidna, docker aracılığıyla veya önceden derlenmiş ikili dosya kullanılarak kurulabilir.

Docker aracılığıyla Echidna

docker pull trailofbits/eth-security-toolbox
docker 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.11
cd /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öster
Kopyala

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, ve 0x00a329C0648769a73afAC7F9381e08fb43DBEA70 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;
6
7 function f(uint x) public {
8 require(x == 12);
9 state1 = true;
10 }
11
12 function g(uint x) public {
13 require(state1);
14 require(x == 8);
15 state2 = true;
16 }
17
18 function h(uint x) public {
19 require(state2);
20 require(x == 42);
21 state3 = true;
22 }
23
24 function i() public {
25 require(state3);
26 state4 = true;
27 }
28
29 function reset1() public {
30 state1 = false;
31 state2 = false;
32 state3 = false;
33 return;
34 }
35
36 function reset2() public {
37 state1 = false;
38 state2 = false;
39 state3 = false;
40 return;
41 }
42
43 function echidna_state4() public returns (bool) {
44 return (!state4);
45 }
46}
Tümünü göster
Kopyala

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: true
2filterFunctions: ["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: false
2filterFunctions: ["f", "g", "h", "i"]
  • filterBlacklist varsayılan olarak true hâldedir.
  • Filtreleme sadece ada göre yapılacaktır (parametreler olmadan). Eğer f() ve f(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: true
2filterFunctions: ["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;
3
4 function inc(uint val) public returns (uint){
5 uint tmp = counter;
6 counter += val;
7 // tmp <= counter
8 return (counter - tmp);
9 }
10}
Tümünü göster
Kopyala

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;
3
4 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öster
Kopyala

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.yaml
Analyzing contract: assert.sol:Incrementor
assertion in inc: failed!💥
Call sequence, shrinking (2596/5000):
inc(21711016731996786641919559689128982722488122124807605757398297001483711807488)
inc(7237005577332262213973186563042994240829374041602535252466099000494570602496)
inc(86844066927987146567678238756515930889952488499230423029593188005934847229952)
Seed: 1806480648350826486
Tü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 code
3 ...
4 assert (condition);
5 ...
6}
7
Kopyala

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 veya external 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'ın uint olarak duyurulduğu yerde assert(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;
3
4 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öster
Kopyala
echidna-test assert.sol --config config.yaml
Analyzing contract: assert.sol:Incrementor
assertion in inc: failed!💥
Call sequence, shrinking (2596/5000):
inc(21711016731996786641919559689128982722488122124807605757398297001483711807488)
inc(7237005577332262213973186563042994240829374041602535252466099000494570602496)
inc(86844066927987146567678238756515930889952488499230423029593188005934847229952)
Seed: 1806480648350826486
Tü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 }
10
11 function echidna_magic_values() public returns (bool) {
12 return !value_found;
13 }
14
15}
Tümünü göster
Kopyala

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: true
2corpusDir: "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öster
Kopyala

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: 142
Unique codehashes: 1
Seed: -7293830866560616537
Tü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;
3
4 function expensive(uint8 times) internal {
5 for(uint8 i=0; i < times; i++)
6 state = state + i;
7 }
8
9 function f(uint x, uint y, uint8 times) public {
10 if (x == 42 && y == 123)
11 expensive(times);
12 else
13 state = 0;
14 }
15
16 function echidna_test() public returns (bool) {
17 return true;
18 }
19
20}
Tümünü göster
Kopyala

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.sol
2...
3echidna_test: passed! 🎉
4
5Seed: 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: 2
2estimateGas: 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 gas
Call sequence:
f(42,123,249) Gas price: 0x10d5733f0a Time delay: 0x495e5 Block delay: 0x88b2
Unique instructions: 157
Unique codehashes: 1
Seed: -325611019680165325
Tümünü göster

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öster
Kopyala

Echidna tüm fonksiyonları çağırabilirse yüksek gaz maliyeti olan işlemleri kolayca bulamaz:

1echidna-test pushpop.sol --config config.yaml
2...
3pop used a maximum of 10746 gas
4...
5check used a maximum of 23730 gas
6...
7clear used a maximum of 35916 gas
8...
9push used a maximum of 40839 gas
Tü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: true
2filterFunctions: ["pop", "clear"]
1echidna-test pushpop.sol --config config.yaml
2...
3push used a maximum of 40839 gas
4...
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: @kaanmetu(opens in a new tab), 23 Kasım 2023

Bu rehber yararlı oldu mu?