Přejít na hlavní obsah

Jak používat Echidnu k testování chytrých kontraktů

Solidity
chytré kontrakty
bezpečnost
testování
fuzzing
Pokročilý
Trailofbits
10. dubna 2020
12 minut čtení
Upravit stránku (opens in a new tab)

Instalace

Echidna může být nainstalována pomocí Dockeru nebo pomocí předkompilované binárky.

Echidna přes Docker

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox

Poslední příkaz spustí eth-security-toolbox v Dockeru, který má přístup k vašemu aktuálnímu adresáři. Můžete měnit soubory na svém hostitelském počítači a spouštět nástroje na souborech z Dockeru.

Uvnitř Dockeru spusťte:

solc-select 0.5.11
cd /home/training

Binárka

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

Úvod do fuzzingu založeného na vlastnostech

Echidna je fuzzer založený na vlastnostech, který jsme popsali v našich předchozích příspěvcích na blogu (1 (opens in a new tab), 2 (opens in a new tab), 3 (opens in a new tab)).

Fuzzing

Fuzzing (opens in a new tab) je v bezpečnostní komunitě dobře známá technika. Spočívá v generování více či méně náhodných vstupů za účelem nalezení chyb v programu. Fuzzery pro tradiční software (jako je AFL (opens in a new tab) nebo LibFuzzer (opens in a new tab)) jsou známé jako efektivní nástroje pro hledání chyb.

Kromě čistě náhodného generování vstupů existuje mnoho technik a strategií pro generování dobrých vstupů, včetně:

  • Získávání zpětné vazby z každého spuštění a její využití k usměrnění generování. Pokud například nově vygenerovaný vstup vede k objevování nové cesty, může mít smysl generovat nové vstupy, které jsou mu blízké.
  • Generování vstupu s ohledem na strukturální omezení. Pokud například váš vstup obsahuje hlavičku s kontrolním součtem, bude dávat smysl nechat fuzzer generovat vstupy, které tento kontrolní součet validují.
  • Použití známých vstupů ke generování nových vstupů: pokud máte přístup k velké datové sadě platných vstupů, váš fuzzer z nich může generovat nové vstupy, místo aby začínal s generováním od nuly. Tyto vstupy se obvykle nazývají seedy (semínka).

Fuzzing založený na vlastnostech

Echidna patří do specifické rodiny fuzzerů: fuzzing založený na vlastnostech, který je silně inspirován nástrojem QuickCheck (opens in a new tab). Na rozdíl od klasických fuzzerů, které se snaží najít pády aplikace, se Echidna bude snažit porušit uživatelem definované invarianty.

V chytrých kontraktech jsou invarianty funkce v Solidity, které mohou představovat jakýkoli nesprávný nebo neplatný stav, do kterého se kontrakt může dostat, včetně:

  • Nesprávné řízení přístupu: útočník se stal vlastníkem kontraktu.
  • Nesprávný stavový automat: tokeny lze převádět, i když je kontrakt pozastaven.
  • Nesprávná aritmetika: uživatel může podtéct (underflow) svůj zůstatek a získat neomezené množství tokenů zdarma.

Testování vlastnosti pomocí Echidny

Ukážeme si, jak otestovat chytrý kontrakt pomocí Echidny. Cílem je následující chytrý kontrakt token.sol (opens in a new tab):

Budeme předpokládat, že tento token musí mít následující vlastnosti:

  • Kdokoli může mít maximálně 1000 tokenů
  • Token nelze převádět (nejde o ERC-20 token)

Napsání vlastnosti

Vlastnosti v Echidně jsou funkce v Solidity. Vlastnost musí:

  • Být bez argumentů
  • Vracet true, pokud je úspěšná
  • Mít název začínající na echidna

Echidna provede následující:

  • Automaticky vygeneruje libovolné transakce k otestování vlastnosti.
  • Nahlásí jakékoli transakce, které vedou k tomu, že vlastnost vrátí false nebo vyhodí chybu.
  • Zahodí vedlejší efekty při volání vlastnosti (tj. pokud vlastnost změní stavovou proměnnou, je tato změna po testu zvrácena)

Následující vlastnost kontroluje, že volající nemá více než 1000 tokenů:

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

Použijte dědičnost k oddělení vašeho kontraktu od vašich vlastností:

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) implementuje vlastnost a dědí z tokenu.

Inicializace kontraktu

Echidna potřebuje konstruktor bez argumentů. Pokud váš kontrakt vyžaduje specifickou inicializaci, musíte ji provést v konstruktoru.

V Echidně existují určité specifické adresy:

  • 0x00a329c0648769A73afAc7F9381E08FB43dBEA72, která volá konstruktor.
  • 0x10000, 0x20000 a 0x00a329C0648769a73afAC7F9381e08fb43DBEA70, které náhodně volají ostatní funkce.

V našem aktuálním příkladu nepotřebujeme žádnou zvláštní inicializaci, a proto je náš konstruktor prázdný.

Spuštění Echidny

Echidna se spouští pomocí:

echidna-test contract.sol

Pokud contract.sol obsahuje více kontraktů, můžete specifikovat cíl:

echidna-test contract.sol --contract MyContract

Shrnutí: Testování vlastnosti

Následující text shrnuje běh Echidny na našem příkladu:

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

Echidna zjistila, že vlastnost je porušena, pokud je zavolána funkce backdoor.

Filtrování funkcí volaných během fuzzingové kampaně

Ukážeme si, jak filtrovat funkce, které se mají fuzzovat. Cílem je následující chytrý kontrakt:

Tento malý příklad nutí Echidnu najít určitou sekvenci transakcí ke změně stavové proměnné. To je pro fuzzer obtížné (doporučuje se použít nástroj pro symbolické provádění, jako je Manticore (opens in a new tab)). Můžeme spustit Echidnu, abychom si to ověřili:

echidna-test multi.sol
...
echidna_state4: passed! 🎉
Seed: -3684648582249875403

Filtrování funkcí

Echidna má potíže s nalezením správné sekvence k otestování tohoto kontraktu, protože dvě resetovací funkce (reset1 a reset2) nastaví všechny stavové proměnné na false. Můžeme však použít speciální funkci Echidny k tomu, abychom buď přidali resetovací funkci na blacklist, nebo přidali na whitelist pouze funkce f, g, h a i.

Pro přidání funkcí na blacklist můžeme použít tento konfigurační soubor:

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

Dalším přístupem k filtrování funkcí je vypsat funkce na whitelistu. K tomu můžeme použít tento konfigurační soubor:

filterBlacklist: false
filterFunctions: ["f", "g", "h", "i"]
  • filterBlacklist je ve výchozím nastavení true.
  • Filtrování se bude provádět pouze podle názvu (bez parametrů). Pokud máte f() a f(uint256), filtr "f" bude odpovídat oběma funkcím.

Spuštění Echidny

Pro spuštění Echidny s konfiguračním souborem blacklist.yaml:

echidna-test multi.sol --config blacklist.yaml
...
echidna_state4: failed!💥
  Call sequence:
    f(12)
    g(8)
    h(42)
    i()

Echidna najde sekvenci transakcí k falzifikaci vlastnosti téměř okamžitě.

Shrnutí: Filtrování funkcí

Echidna může během fuzzingové kampaně přidávat funkce na blacklist nebo whitelist pomocí:

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

Echidna zahájí fuzzingovou kampaň buď s blacklistem funkcí f1, f2 a f3, nebo bude volat pouze tyto funkce, v závislosti na hodnotě booleovské proměnné filterBlacklist.

Jak testovat asert v Solidity pomocí Echidny

V tomto krátkém tutoriálu si ukážeme, jak používat Echidnu k testování kontroly asertů v kontraktech. Předpokládejme, že máme kontrakt jako je tento:

Napsání asertu

Chceme se ujistit, že tmp je menší nebo rovno counter po vrácení jejich rozdílu. Mohli bychom napsat vlastnost pro Echidnu, ale museli bychom někam uložit hodnotu tmp. Místo toho můžeme použít asert jako je tento:

Spuštění Echidny

Chcete-li povolit testování selhání asertů, vytvořte konfigurační soubor Echidny (opens in a new tab) config.yaml:

checkAsserts: true

Když spustíme tento kontrakt v Echidně, získáme očekávané výsledky:

Jak vidíte, Echidna hlásí selhání asertu ve funkci inc. Přidání více než jednoho asertu na funkci je možné, ale Echidna nedokáže říct, který asert selhal.

Kdy a jak používat aserty

Aserty lze použít jako alternativy k explicitním vlastnostem, zejména pokud podmínky, které se mají kontrolovat, přímo souvisejí se správným použitím nějaké operace f. Přidání asertů za nějaký kód vynutí, že kontrola proběhne bezprostředně po jeho provedení:

function f(..) public {
    // nějaký složitý kód
    ...
    assert (condition);
    ...
}

Naopak použití explicitní vlastnosti v Echidně bude náhodně provádět transakce a neexistuje snadný způsob, jak přesně vynutit, kdy bude zkontrolována. Stále je však možné použít toto náhradní řešení:

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

Existují zde však určité problémy:

  • Selže, pokud je f deklarována jako internal nebo external.
  • Není jasné, jaké argumenty by měly být použity k volání f.
  • Pokud se f zvrátí, vlastnost selže.

Obecně doporučujeme dodržovat doporučení Johna Regehra (opens in a new tab) ohledně používání asertů:

  • Během kontroly asertu nevynucujte žádné vedlejší efekty. Například: assert(ChangeStateAndReturn() == 1)
  • Neasertujte zřejmá tvrzení. Například assert(var >= 0), kde je var deklarováno jako uint.

Nakonec prosím nepoužívejte require místo assert, protože Echidna to nebude schopna detekovat (ale kontrakt se stejně zvrátí).

Shrnutí: Kontrola asertů

Následující text shrnuje běh Echidny na našem příkladu:

Echidna zjistila, že asert ve funkci inc může selhat, pokud je tato funkce volána vícekrát s velkými argumenty.

Shromažďování a úprava korpusu Echidny

Ukážeme si, jak shromažďovat a používat korpus transakcí pomocí Echidny. Cílem je následující chytrý kontrakt magic.sol (opens in a new tab):

Tento malý příklad nutí Echidnu najít určité hodnoty ke změně stavové proměnné. To je pro fuzzer obtížné (doporučuje se použít nástroj pro symbolické provádění, jako je Manticore (opens in a new tab)). Můžeme spustit Echidnu, abychom si to ověřili:

echidna-test magic.sol
...

echidna_magic_values: passed! 🎉

Seed: 2221503356319272685

Stále však můžeme použít Echidnu ke shromažďování korpusu při spuštění této fuzzingové kampaně.

Shromažďování korpusu

Chcete-li povolit shromažďování korpusu, vytvořte adresář pro korpus:

mkdir corpus-magic

A konfigurační soubor Echidny (opens in a new tab) config.yaml:

coverage: true
corpusDir: "corpus-magic"

Nyní můžeme spustit náš nástroj a zkontrolovat shromážděný korpus:

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

Echidna stále nemůže najít správné magické hodnoty, ale můžeme se podívat na korpus, který shromáždila. Například jedním z těchto souborů byl:

Je zřejmé, že tento vstup nespustí selhání v naší vlastnosti. V dalším kroku si však ukážeme, jak jej pro tento účel upravit.

Seedování korpusu

Echidna potřebuje trochu pomoci, aby se vypořádala s funkcí magic. Zkopírujeme a upravíme vstup tak, abychom pro něj použili vhodné parametry:

cp corpus/2712688662897926208.txt corpus/new.txt

Upravíme new.txt tak, aby volal magic(42,129,333,0). Nyní můžeme Echidnu spustit znovu:

Tentokrát okamžitě zjistila, že vlastnost je porušena.

Hledání transakcí s vysokou spotřebou gasu

Ukážeme si, jak pomocí Echidny najít transakce s vysokou spotřebou gasu. Cílem je následující chytrý kontrakt:

Zde může mít expensive velkou spotřebu gasu.

V současné době Echidna vždy potřebuje vlastnost k testování: zde echidna_test vždy vrací true. Můžeme spustit Echidnu, abychom si to ověřili:

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

Seed: 2320549945714142710

Měření spotřeby gasu

Chcete-li v Echidně povolit měření spotřeby gasu, vytvořte konfigurační soubor config.yaml:

estimateGas: true

V tomto příkladu také zmenšíme velikost sekvence transakcí, aby byly výsledky snáze srozumitelné:

seqLen: 2
estimateGas: true

Spuštění Echidny

Jakmile máme vytvořený konfigurační soubor, můžeme Echidnu spustit takto:

Odfiltrování volání snižujících gas

Výše uvedený tutoriál o filtrování funkcí volaných během fuzzingové kampaně ukazuje, jak z testování odstranit některé funkce.
To může být kritické pro získání přesného odhadu gasu. Zvažte následující příklad:

Pokud Echidna může volat všechny funkce, nenajde snadno transakce s vysokými náklady na gas:

Je to proto, že náklady závisí na velikosti addrs a náhodná volání mají tendenci ponechat pole téměř prázdné. Přidání pop a clear na blacklist nám však dává mnohem lepší výsledky:

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

Shrnutí: Hledání transakcí s vysokou spotřebou gasu

Echidna dokáže najít transakce s vysokou spotřebou gasu pomocí konfigurační volby estimateGas:

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

Jakmile fuzzingová kampaň skončí, Echidna nahlásí sekvenci s maximální spotřebou gasu pro každou funkci.

Poslední aktualizace stránky: 3. března 2026