Ana içeriğe geç

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

solidity
akıllı kontratlar
güvenlik
test etmek
bulandırma
Gelişmiş
Trailofbits
10 Nisan 2020
12 dakikalık okuma

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 docker'dan dosyalar üzerindeki araçları çalıştırabilirsiniz

Docker'ın içinde şunu çalıştırın:

solc-select 0.5.11
cd /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öster

Bu 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 true döndürmelidir
  • Adı echidna ile başlamalıdır

Echidna şunları yapar:

  • Özelliği test etmek için otomatik olarak rastgele işlemler oluşturur.
  • Bir özelliğin false dö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, 0x20000 ve 0x00a329C0648769a73afAC7F9381e08fb43DBEA70, 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.sol

Eğ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öster

Echidna, 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;
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

Bu 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: -3684648582249875403

Fonksiyonları 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: true
2filterFunctions: ["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: false
2filterFunctions: ["f", "g", "h", "i"]
  • filterBlacklist varsayılan olarak true değerindedir.
  • Filtreleme yalnızca isme göre (parametreler olmadan) gerçekleştirilecektir. f() ve f(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: true
2filterFunctions: ["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;
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

Bir 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;
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

Echidna'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: true

Bu sözleşmeyi Echidna'da çalıştırdığımızda beklenen sonuçları elde ederiz:

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 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 kod
3 ...
4 assert (condition);
5 ...
6}
7

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. 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:

  • f internal veya external olarak bildirilirse başarısız olur.
  • f fonksiyonunu çağırmak için hangi argümanların kullanılması gerektiği belirsizdir.
  • f geri 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, var uint olarak bildirildiğinde assert(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;
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
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 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 }
10
11 function echidna_magic_values() public returns (bool) {
12 return !value_found;
13 }
14
15}
Tümünü göster

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 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-magic

Ve bir Echidna yapılandırma dosyası (opens in a new tab) olan config.yaml dosyasını 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 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öster

Açı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.txt

magic(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: 142
Unique codehashes: 1
Seed: -7293830866560616537
Tümünü göster

Bu 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;
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

Burada 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.sol
2...
3echidna_test: passed! 🎉
4
5Seed: 2320549945714142710

Gaz Tüketimini Ölçme

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 daha kolay anlaşılmasını sağlamak için işlem dizisinin boyutunu da azaltacağız:

1seqLen: 2
2estimateGas: true

Echidna'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: 0x88b2
Unique instructions: 157
Unique codehashes: 1
Seed: -325611019680165325
Tümünü göster

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

Echidna tüm fonksiyonları çağırabilirse, yüksek gaz maliyetli 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 ç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: 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üketimli işlemleri bulma

Echidna, estimateGas yapılandırma seçeneğini kullanarak yüksek gaz tüketimli işlemleri bulabilir:

1estimateGas: true
echidna-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

Bu rehber yararlı oldu mu?