Ana içeriğe atla

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

Solidity
akıllı sözleşmeler
güvenlik
test etme
fuzzing
İleri
Trailofbits
10 Nisan 2020
12 dakikalık okuma

Kurulum

Echidna, Docker aracılığıyla veya önceden derlenmiş ikili dosya (binary) 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, eth-security-toolbox'ı mevcut dizininize erişimi olan bir Docker içinde çalıştırır. Dosyaları ana makinenizden değiştirebilir ve araçları Docker'daki dosyalar üzerinde çalıştırabilirsiniz

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

solc-select 0.5.11
cd /home/training

İkili Dosya (Binary)

https://github.com/crytic/echidna/releases/tag/v1.4.0.0 (opens in a new tab)

Özellik tabanlı fuzzing'e 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 özellik tabanlı bir fuzzer'dır.

Fuzzing

Fuzzing (opens in a new tab), güvenlik topluluğunda iyi bilinen bir tekniktir. Programdaki hataları bulmak için az çok rastgele girdiler üretmekten oluşur. Geleneksel yazılımlar için fuzzer'ların (AFL (opens in a new tab) veya LibFuzzer (opens in a new tab) gibi) hataları bulmada etkili araçlar olduğu bilinmektedir.

Girdilerin tamamen rastgele üretilmesinin ö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 almak ve bunu kullanarak üretimi yönlendirmek. Örneğin, yeni üretilen bir girdi yeni bir yolun keşfine yol açarsa, ona yakın yeni girdiler üretmek mantıklı olabilir.
  • Girdiyi yapısal bir kısıtlamaya uyarak üretmek. Örneğin, girdiniz bir sağlama toplamı (checksum) içeren bir başlık içeriyorsa, fuzzer'ın sağlama toplamını doğrulayan girdi üretmesine izin vermek mantıklı olacaktır.
  • Yeni girdiler üretmek için bilinen girdileri kullanmak: Geçerli girdilerden oluşan büyük bir veri kümesine erişiminiz varsa, fuzzer'ınız üretimine sıfırdan başlamak yerine bunlardan yeni girdiler üretebilir. Bunlara genellikle tohum (seed) denir.

Özellik tabanlı fuzzing

Echidna, belirli bir fuzzer ailesine aittir: QuickCheck (opens in a new tab)'ten büyük ölçüde ilham alan özellik tabanlı fuzzing. Çökmeleri bulmaya çalışacak klasik fuzzer'ların aksine Echidna, kullanıcı tanımlı değişmezleri (invariants) 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 işlevleridir, bunlara şunlar dahildir:

  • Yanlış erişim kontrolü: Saldırgan sözleşmenin sahibi oldu.
  • Yanlış durum makinesi: Sözleşme duraklatılmışken Token'lar transfer edilebilir.
  • Yanlış aritmetik: Kullanıcı bakiyesini sıfırın altına düşürebilir (underflow) ve sınırsız ücretsiz Token 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):

Bu Token'ın aşağıdaki özelliklere sahip olması gerektiğini varsayacağız:

  • Herkes en fazla 1000 Token'a sahip olabilir
  • Token transfer edilemez (bir ERC-20 Token'ı değildir)

Bir özellik yazın

Echidna özellikleri Solidity işlevleridir. Bir özellik şunları sağlamalıdır:

  • Argümanı olmamalıdır
  • Başarılı olursa true döndürmelidir
  • Adı echidna ile başlamalıdır

Echidna şunları yapacaktır:

  • Özelliği test etmek için otomatik olarak rastgele işlemler üretir.
  • Bir özelliğin false döndürmesine veya bir hata fırlatmasına yol açan tüm işlemleri raporlar.
  • Bir özelliği çağırırken yan etkileri göz ardı eder (yani, özellik bir durum değişkenini değiştirirse, testten sonra bu değişiklik geri alınır)

Aşağıdaki özellik, çağıranın 1000'den fazla Token'a sahip olmadığını kontrol eder:

function echidna_balance_under_1000() public view returns(bool){
      return balances[msg.sender] <= 1000;
}

Sözleşmenizi özelliklerinizden ayırmak için kalıtımı kullanın:

contract TestToken is Token{
      function echidna_balance_under_1000() public view returns(bool){
            return balances[msg.sender] <= 1000;
      }
  }

token.sol (opens in a new tab) özelliği uygular ve Token'dan miras alır.

Bir sözleşme başlatın

Echidna'nın argümansız bir kurucu fonksiyona ihtiyacı vardır. Sözleşmenizin belirli bir başlatmaya ihtiyacı varsa, bunu kurucu içinde yapmanız gerekir.

Echidna'da bazı özel adresler vardır:

  • Kurucuyu çağıran 0x00a329c0648769A73afAc7F9381E08FB43dBEA72.
  • Diğer işlevleri rastgele çağıran 0x10000, 0x20000 ve 0x00a329C0648769a73afAC7F9381e08fb43DBEA70.

Mevcut örneğimizde belirli bir başlatmaya ihtiyacımız yok, bu nedenle kurucumuz boştur.

Echidna'yı Çalıştırın

Echidna şu şekilde 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, örneğimizde Echidna'nın çalışmasını özetlemektedir:

contract TestToken is Token{
    constructor() public {}
        function echidna_balance_under_1000() public view returns(bool){
          return balances[msg.sender] <= 1000;
        }
  }

Echidna, backdoor çağrılırsa özelliğin ihlal edildiğini buldu.

Bir fuzzing kampanyası sırasında çağrılacak işlevleri filtreleme

Fuzzing işlemine tabi tutulacak işlevlerin nasıl filtreleneceğini göreceğiz. Hedef, aşağıdaki akıllı sözleşmedir:

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

İşlevleri filtreleme

Echidna, bu sözleşmeyi test etmek için doğru diziyi bulmakta zorlanıyor çünkü iki sıfırlama işlevi (reset1 ve reset2) tüm durum değişkenlerini false olarak ayarlayacaktır. Ancak, sıfırlama işlevini kara listeye almak veya yalnızca f, g, h ve i işlevlerini beyaz listeye almak için özel bir Echidna özelliği kullanabiliriz.

İşlevleri kara listeye almak için bu yapılandırma dosyasını kullanabiliriz:

filterBlacklist: true
filterFunctions: ["reset1", "reset2"]

İşlevleri filtrelemek için başka bir yaklaşım da beyaz listeye alınmış işlevleri listelemektir. Bunu yapmak için bu yapılandırma dosyasını kullanabiliriz:

filterBlacklist: false
filterFunctions: ["f", "g", "h", "i"]
  • filterBlacklist varsayılan olarak true değerindedir.
  • Filtreleme yalnızca isme göre (parametreler olmadan) gerçekleştirilecektir. Eğer f() ve f(uint256) işlevleriniz varsa, "f" filtresi her iki işlevle de eşleşecektir.

Echidna'yı Çalıştırın

Echidna'yı bir blacklist.yaml yapılandırma dosyasıyla ç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: İşlevleri filtreleme

Echidna, bir fuzzing kampanyası sırasında çağrılacak işlevleri şunları kullanarak kara listeye veya beyaz listeye alabilir:

filterBlacklist: true
filterFunctions: ["f1", "f2", "f3"]
echidna-test contract.sol --config config.yaml
...

Echidna, filterBlacklist boolean değerine göre f1, f2 ve f3 işlevlerini kara listeye alarak veya yalnızca bunları çağırarak bir fuzzing kampanyası başlatır.

Echidna ile Solidity'nin doğrulama (assert) işlevi nasıl test edilir

Bu kısa eğitimde, sözleşmelerdeki doğrulama (assertion) kontrollerini test etmek için Echidna'nın nasıl kullanılacağını göstereceğiz. Şunun gibi bir sözleşmemiz olduğunu varsayalım:

Bir doğrulama yazın

Farkını döndürdükten sonra tmp değerinin counter değerinden küçük veya ona eşit olduğundan emin olmak istiyoruz. Bir Echidna özelliği yazabilirdik, ancak tmp değerini bir yerde saklamamız gerekecekti. Bunun yerine, şunun gibi bir doğrulama kullanabiliriz:

Echidna'yı Çalıştırın

Doğrulama hatası testini etkinleştirmek için bir Echidna yapılandırma dosyası (opens in a new tab) olan config.yaml oluşturun:

checkAsserts: true

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

Gördüğünüz gibi Echidna, inc işlevinde bazı doğrulama hataları bildiriyor. İşlev başına birden fazla doğrulama eklemek mümkündür, ancak Echidna hangi doğrulamanın başarısız olduğunu söyleyemez.

Doğrulamalar ne zaman ve nasıl kullanılır

Doğrulamalar, özellikle kontrol edilecek koşullar bazı f işlemlerinin doğru kullanımıyla doğrudan ilişkiliyse, açık özelliklere alternatif olarak kullanılabilir. Bazı kodlardan sonra doğrulamalar eklemek, kontrolün kod yürütüldükten hemen sonra gerçekleşmesini zorunlu kılacaktır:

function f(..) public {
    // bazı karmaşık kodlar
    ...
    assert (condition);
    ...
}

Aksine, açık bir Echidna özelliği kullanmak işlemleri rastgele yürütecektir ve tam olarak ne zaman kontrol edileceğini zorunlu kılmanın kolay bir yolu yoktur. Yine de şu geçici çözümü yapmak mümkündür:

function echidna_assert_after_f() public returns (bool) {
    f(..);
    return(condition);
}

Ancak bazı sorunlar vardır:

  • f, internal veya external olarak bildirilirse başarısız olur.
  • f işlevini çağırmak için hangi argümanların kullanılması gerektiği belirsizdir.
  • f geri alınırsa (revert), özellik başarısız olur.

Genel olarak, doğrulamaların nasıl kullanılacağı konusunda John Regehr'in tavsiyesine (opens in a new tab) uymanızı öneririz:

  • Doğrulama kontrolü sırasında herhangi bir yan etkiyi zorlamayın. Örneğin: assert(ChangeStateAndReturn() == 1)
  • Bariz ifadeleri doğrulamayın. Örneğin, var değişkeninin uint olarak bildirildiği assert(var >= 0) durumu.

Son olarak, lütfen assert yerine require kullanmayın, çünkü Echidna bunu tespit edemeyecektir (ancak sözleşme yine de geri alınacaktır).

Özet: Doğrulama Kontrolü

Aşağıdakiler, örneğimizde Echidna'nın çalışmasını özetlemektedir:

Echidna, bu işlev büyük argümanlarla birden çok kez çağrılırsa inc içindeki doğrulamanın başarısız olabileceğini buldu.

Bir Echidna derlemini (corpus) toplama ve değiştirme

Echidna ile bir işlem derleminin (corpus) 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):

Bu küçük örnek, Echidna'yı bir durum değişkenini değiştirmek için belirli değerler bulmaya zorlar. Bu bir fuzzer 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 fuzzing kampanyasını yürütürken derlem toplamak için yine de Echidna'yı kullanabiliriz.

Bir derlem toplama

Derlem toplamayı etkinleştirmek için bir derlem dizini oluşturun:

mkdir corpus-magic

Ve bir Echidna yapılandırma dosyası (opens in a new tab) olan config.yaml:

coverage: true
corpusDir: "corpus-magic"

Şimdi aracımızı çalıştırabilir ve toplanan derlemi kontrol edebiliriz:

echidna-test magic.sol --config config.yaml

Echidna hala doğru sihirli değerleri bulamıyor, ancak topladığı derleme göz atabiliriz. Örneğin, bu dosyalardan biri şuydu:

Açıkçası, bu girdi özelliğimizdeki hatayı tetiklemeyecektir. Ancak bir sonraki adımda, bunu tetiklemek için nasıl değiştireceğimizi göreceğiz.

Bir derlemi tohumlama (seeding)

Echidna'nın magic işleviyle 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) işlevini çağırmak için new.txt dosyasını değiştireceğiz. Şimdi Echidna'yı yeniden çalıştırabiliriz:

Bu kez, özelliğin anında ihlal edildiğini buldu.

Yüksek Gaz tüketimi olan işlemleri bulma

Echidna ile yüksek Gaz tüketimi olan işlemlerin nasıl bulunacağını göreceğiz. Hedef, aşağıdaki akıllı sözleşmedir:

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 döndürür. Bunu doğrulamak için Echidna'yı çalıştırabiliriz:

echidna-test gas.sol
...
echidna_test: passed! 🎉

Seed: 2320549945714142710

Gaz Tüketimini Ölçme

Echidna ile Gaz tüketimini etkinleştirmek için bir config.yaml yapılandırma dosyası oluşturun:

estimateGas: true

Bu örnekte, sonuçların anlaşılmasını kolaylaştırmak için işlem dizisinin boyutunu da küçülteceğiz:

seqLen: 2
estimateGas: true

Echidna'yı Çalıştırın

Yapılandırma dosyasını oluşturduktan sonra Echidna'yı şu şekilde çalıştırabiliriz:

Gaz Azaltan Çağrıları Filtreleme

Yukarıdaki bir fuzzing kampanyası sırasında çağrılacak işlevleri filtreleme eğitimi, bazı işlevlerin testinizden nasıl çıkarılacağını gösterir.
Bu, doğru bir Gaz tahmini elde etmek için kritik olabilir. Aşağıdaki örneği göz önünde bulundurun:

Eğer Echidna tüm işlevleri çağırabilirse, yüksek Gaz maliyeti olan işlemleri kolayca bulamayacaktır:

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 işlevlerini kara listeye almak bize çok daha iyi sonuçlar verir:

filterBlacklist: true
filterFunctions: ["pop", "clear"]
echidna-test pushpop.sol --config config.yaml
...
push used a maximum of 40839 gas
...
check 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:

estimateGas: true
echidna-test contract.sol --config config.yaml
...

Echidna, fuzzing kampanyası bittiğinde her işlev için maksimum Gaz tüketimine sahip bir dizi raporlayacaktır.

Sayfanın son güncellenme tarihi: 3 Mart 2026