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 (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):
contract Token{
mapping(address => uint) public balances;
function airdrop() public{
balances[msg.sender] = 1000;
}
function consume() public{
require(balances[msg.sender]>0);
balances[msg.sender] -= 1;
}
function backdoor() public{
balances[msg.sender] += 1;
}
}
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
truedöndürmelidir - Adı
echidnaile başlamalıdır
Echidna şunları yapacaktır:
- Özelliği test etmek için otomatik olarak rastgele işlemler üretir.
- Bir özelliğin
falsedö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,0x20000ve0x00a329C0648769a73afAC7F9381e08fb43DBEA70.
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-test testtoken.sol --contract TestToken
...
echidna_balance_under_1000: failed!💥
Call sequence, shrinking (1205/5000):
airdrop()
backdoor()
...
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:
contract C {
bool state1 = false;
bool state2 = false;
bool state3 = false;
bool state4 = false;
function f(uint x) public {
require(x == 12);
state1 = true;
}
function g(uint x) public {
require(state1);
require(x == 8);
state2 = true;
}
function h(uint x) public {
require(state2);
require(x == 42);
state3 = true;
}
function i() public {
require(state3);
state4 = true;
}
function reset1() public {
state1 = false;
state2 = false;
state3 = false;
return;
}
function reset2() public {
state1 = false;
state2 = false;
state3 = false;
return;
}
function echidna_state4() public returns (bool) {
return (!state4);
}
}
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"]
filterBlacklistvarsayılan olaraktruedeğerindedir.- Filtreleme yalnızca isme göre (parametreler olmadan) gerçekleştirilecektir. Eğer
f()vef(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:
contract Incrementor {
uint private counter = 2**200;
function inc(uint val) public returns (uint){
uint tmp = counter;
counter += val;
// tmp <= counter
return (counter - tmp);
}
}
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:
contract Incrementor {
uint private counter = 2**200;
function inc(uint val) public returns (uint){
uint tmp = counter;
counter += val;
assert (tmp <= counter);
return (counter - tmp);
}
}
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:
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
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,internalveyaexternalolarak bildirilirse başarısız olur.fişlevini çağırmak için hangi argümanların kullanılması gerektiği belirsizdir.fgeri 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,
vardeğişkenininuintolarak bildirildiğiassert(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:
contract Incrementor {
uint private counter = 2**200;
function inc(uint val) public returns (uint){
uint tmp = counter;
counter += val;
assert (tmp <= counter);
return (counter - tmp);
}
}
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
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):
contract C {
bool value_found = false;
function magic(uint magic_1, uint magic_2, uint magic_3, uint magic_4) public {
require(magic_1 == 42);
require(magic_2 == 129);
require(magic_3 == magic_4+333);
value_found = true;
return;
}
function echidna_magic_values() public returns (bool) {
return !value_found;
}
}
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:
[
{
"_gas'": "0xffffffff",
"_delay": ["0x13647", "0xccf6"],
"_src": "00a329c0648769a73afac7f9381e08fb43dbea70",
"_dst": "00a329c0648769a73afac7f9381e08fb43dbea72",
"_value": "0x0",
"_call": {
"tag": "SolCall",
"contents": [
"magic",
[
{
"contents": [
256,
"93723985220345906694500679277863898678726808528711107336895287282192244575836"
],
"tag": "AbiUInt"
},
{
"contents": [256, "334"],
"tag": "AbiUInt"
},
{
"contents": [
256,
"68093943901352437066264791224433559271778087297543421781073458233697135179558"
],
"tag": "AbiUInt"
},
{
"tag": "AbiUInt",
"contents": [256, "332"]
}
]
]
},
"_gasprice'": "0xa904461f1"
}
]
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:
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
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:
contract C {
uint state;
function expensive(uint8 times) internal {
for(uint8 i=0; i < times; i++)
state = state + i;
}
function f(uint x, uint y, uint8 times) public {
if (x == 42 && y == 123)
expensive(times);
else
state = 0;
}
function echidna_test() public returns (bool) {
return true;
}
}
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:
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
- Gösterilen Gaz, HEVM (opens in a new tab) tarafından sağlanan bir tahmindir.
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:
contract C {
address [] addrs;
function push(address a) public {
addrs.push(a);
}
function pop() public {
addrs.pop();
}
function clear() public{
addrs.length = 0;
}
function check() public{
for(uint256 i = 0; i < addrs.length; i++)
for(uint256 j = i+1; j < addrs.length; j++)
if (addrs[i] == addrs[j])
addrs[j] = address(0x0);
}
function echidna_test() public returns (bool) {
return true;
}
}
Eğer Echidna tüm işlevleri çağırabilirse, yüksek Gaz maliyeti olan işlemleri kolayca bulamayacaktır:
echidna-test pushpop.sol --config config.yaml
...
pop used a maximum of 10746 gas
...
check used a maximum of 23730 gas
...
clear used a maximum of 35916 gas
...
push used a maximum of 40839 gas
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