Jak používat Manticore k vyhledávání chyb v chytrých kontraktech
Cílem tohoto tutoriálu je ukázat, jak používat Manticore k automatickému vyhledávání chyb v chytrých kontraktech.
Instalace
Manticore vyžaduje python >= 3.6. Lze jej nainstalovat pomocí pip nebo s použitím dockeru.
Manticore přes docker
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/training trailofbits/eth-security-toolboxPoslední 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 z vašeho hostitele a spouštět nástroje na souborech z dockeru
Uvnitř dockeru spusťte:
solc-select 0.5.11cd /home/trufflecon/Manticore přes pip
pip3 install --user manticoreDoporučuje se solc 0.5.11.
Spuštění skriptu
Pro spuštění pythonového skriptu v pythonu 3:
python3 script.pyÚvod do dynamického symbolického provádění
Dynamické symbolické provádění v kostce
Dynamické symbolické provádění (DSE) je technika analýzy programů, která prozkoumává stavový prostor s vysokou mírou sémantického povědomí. Tato technika je založena na objevování „cest programu“, které jsou reprezentovány jako matematické vzorce zvané path predicates. Koncepčně tato technika pracuje s predikáty cest ve dvou krocích:
- Jsou vytvořeny pomocí omezení na vstupu programu.
- Používají se ke generování vstupů programu, které způsobí provedení příslušných cest.
Tento přístup neprodukuje žádné falešně pozitivní výsledky v tom smyslu, že všechny identifikované stavy programu mohou být spuštěny během konkrétního provádění. Pokud například analýza najde celočíselné přetečení, je zaručeno, že bude reprodukovatelné.
Příklad predikátu cesty
Pro lepší představu o tom, jak DSE funguje, zvažte následující příklad:
1function f(uint a){23 if (a == 65) {4 // Je přítomna chyba5 }67}Protože f() obsahuje dvě cesty, DSE vytvoří dva různé predikáty cest:
- Cesta 1:
a == 65 - Cesta 2:
Not (a == 65)
Každý predikát cesty je matematický vzorec, který lze předat takzvanému řešiči SMT (opens in a new tab), který se pokusí rovnici vyřešit. Pro Cestu 1 řešič řekne, že cestu lze prozkoumat s a = 65. Pro Cestu 2 může řešič dát a jakoukoli jinou hodnotu než 65, například a = 0.
Ověřování vlastností
Manticore umožňuje plnou kontrolu nad veškerým prováděním každé cesty. V důsledku toho vám umožňuje přidat libovolná omezení téměř k čemukoli. Tato kontrola umožňuje vytváření vlastností kontraktu.
Zvažte následující příklad:
1function unsafe_add(uint a, uint b) returns(uint c){2 c = a + b; // žádná ochrana proti přetečení3 return c;4}Zde je ve funkci k prozkoumání pouze jedna cesta:
- Cesta 1:
c = a + b
Pomocí Manticore můžete zkontrolovat přetečení a přidat omezení k predikátu cesty:
c = a + b AND (c < a OR c < b)
Pokud je možné najít ohodnocení a a b, pro které je výše uvedený predikát cesty proveditelný, znamená to, že jste našli přetečení. Řešič může například vygenerovat vstup a = 10, b = MAXUINT256.
Pokud zvážíte opravenou verzi:
1function safe_add(uint a, uint b) returns(uint c){2 c = a + b;3 require(c>=a);4 require(c>=b);5 return c;6}Přidružený vzorec s kontrolou přetečení by byl:
c = a + b AND (c >= a) AND (c=>b) AND (c < a OR c < b)
Tento vzorec nelze vyřešit; jinými slovy, je to důkaz, že v safe_add se c vždy zvýší.
DSE je proto mocný nástroj, který dokáže ověřit libovolná omezení ve vašem kódu.
Spuštění pod Manticore
Podíváme se, jak prozkoumat chytrý kontrakt s Manticore API. Cílem je následující chytrý kontrakt example.sol (opens in a new tab):
1pragma solidity >=0.4.24 <0.6.0;23contract Simple {4 function f(uint a) payable public{5 if (a == 65) {6 revert();7 }8 }9}Zobrazit všeSpustit samostatné prozkoumávání
Manticore můžete spustit přímo na chytrém kontraktu pomocí následujícího příkazu (project může být soubor Solidity nebo adresář projektu):
$ manticore projectZískáte výstup testovacích případů, jako je tento (pořadí se může změnit):
1...2... m.c.manticore:INFO: Generated testcase No. 0 - STOP3... m.c.manticore:INFO: Generated testcase No. 1 - REVERT4... m.c.manticore:INFO: Generated testcase No. 2 - RETURN5... m.c.manticore:INFO: Generated testcase No. 3 - REVERT6... m.c.manticore:INFO: Generated testcase No. 4 - STOP7... m.c.manticore:INFO: Generated testcase No. 5 - REVERT8... m.c.manticore:INFO: Generated testcase No. 6 - REVERT9... m.c.manticore:INFO: Results in /home/ethsec/workshops/Automated Smart Contracts Audit - TruffleCon 2018/manticore/examples/mcore_t6vi6ij310...Zobrazit všeBez dalších informací bude Manticore prozkoumávat kontrakt s novými symbolickými transakcemi, dokud neprozkoumá nové cesty v kontraktu. Manticore nespouští nové transakce po neúspěšné (např. po revertu).
Manticore vypíše informace do adresáře mcore_*. V tomto adresáři mimo jiné najdete:
global.summary: pokrytí a varování kompilátorutest_XXXXX.summary: pokrytí, poslední instrukce, zůstatky na účtech pro každý testovací případtest_XXXXX.tx: podrobný seznam transakcí pro každý testovací případ
Zde Manticore našel 7 testovacích případů, které odpovídají (pořadí souborů se může změnit):
| Transakce 0 | Transakce 1 | Transakce 2 | Výsledek | |
|---|---|---|---|---|
| test_00000000.tx | Vytvoření kontraktu | f(!=65) | f(!=65) | STOP |
| test_00000001.tx | Vytvoření kontraktu | záložní funkce | REVERT | |
| test_00000002.tx | Vytvoření kontraktu | RETURN | ||
| test_00000003.tx | Vytvoření kontraktu | f(65) | REVERT | |
| test_00000004.tx | Vytvoření kontraktu | f(!=65) | STOP | |
| test_00000005.tx | Vytvoření kontraktu | f(!=65) | f(65) | REVERT |
| test_00000006.tx | Vytvoření kontraktu | f(!=65) | záložní funkce | REVERT |
Shrnutí prozkoumávání f(!=65) označuje volání f s jakoukoliv hodnotou jinou než 65.
Jak si můžete všimnout, Manticore generuje jedinečný testovací případ pro každou úspěšnou nebo vrácenou transakci.
Použijte příznak --quick-mode, pokud chcete rychlé prozkoumání kódu (vypíná detektory chyb, výpočet gasu atd.)
Manipulace s chytrým kontraktem přes API
Tato část popisuje podrobnosti o tom, jak manipulovat s chytrým kontraktem prostřednictvím Python API Manticore. Můžete vytvořit nový soubor s příponou pythonu *.py a napsat potřebný kód přidáním příkazů API (jejichž základy budou popsány níže) do tohoto souboru a poté jej spustit příkazem $ python3 *.py. Níže uvedené příkazy můžete také provést přímo v konzoli pythonu. Konzoli spustíte příkazem $ python3.
Vytváření účtů
První věc, kterou byste měli udělat, je inicializovat nový blockchain pomocí následujících příkazů:
1from manticore.ethereum import ManticoreEVM23m = ManticoreEVM()Účet, který není kontraktem, se vytváří pomocí m.create_account (opens in a new tab):
1user_account = m.create_account(balance=1000)Kontrakt v Solidity lze nasadit pomocí m.solidity_create_contract (opens in a new tab):
1source_code = '''2pragma solidity >=0.4.24 <0.6.0;3contract Simple {4 function f(uint a) payable public{5 if (a == 65) {6 revert();7 }8 }9}10'''11# Inicializace kontraktu12contract_account = m.solidity_create_contract(source_code, owner=user_account)Zobrazit všeShrnutí
- Uživatelské účty a účty kontraktů můžete vytvořit pomocí m.create_account (opens in a new tab) a m.solidity_create_contract (opens in a new tab).
Provádění transakcí
Manticore podporuje dva typy transakcí:
- Nezpracovaná transakce: prozkoumány jsou všechny funkce
- Pojmenovaná transakce: je prozkoumána pouze jedna funkce
Nezpracovaná transakce
Nezpracovaná transakce se provádí pomocí m.transaction (opens in a new tab):
1m.transaction(caller=user_account,2 address=contract_account,3 data=data,4 value=value)Volající, adresa, data nebo hodnota transakce mohou být buď konkrétní, nebo symbolické:
- m.make_symbolic_value (opens in a new tab) vytvoří symbolickou hodnotu.
- m.make_symbolic_buffer(size) (opens in a new tab) vytvoří symbolické bajtové pole.
Například:
1symbolic_value = m.make_symbolic_value()2symbolic_data = m.make_symbolic_buffer(320)3m.transaction(caller=user_account,4 address=contract_address,5 data=symbolic_data,6 value=symbolic_value)Pokud jsou data symbolická, Manticore prozkoumá všechny funkce kontraktu během provádění transakce. Pro pochopení toho, jak funguje výběr funkcí, bude užitečné podívat se na vysvětlení záložní funkce v článku Hands on the Ethernaut CTF (opens in a new tab).
Pojmenovaná transakce
Funkce lze spouštět prostřednictvím jejich názvu.
Pro spuštění f(uint var) se symbolickou hodnotou z user_account a s 0 ethery použijte:
1symbolic_var = m.make_symbolic_value()2contract_account.f(symbolic_var, caller=user_account, value=0)Pokud není hodnota transakce specifikována, je ve výchozím nastavení 0.
Shrnutí
- Argumenty transakce mohou být konkrétní nebo symbolické
- Nezpracovaná transakce prozkoumá všechny funkce
- Funkci lze volat podle jejího jména
Pracovní prostor
m.workspace je adresář používaný jako výstupní adresář pro všechny generované soubory:
1print("Výsledky jsou v {}".format(m.workspace))Ukončení prozkoumávání
Pro zastavení prozkoumávání použijte m.finalize() (opens in a new tab). Po zavolání této metody by se neměly odesílat žádné další transakce a Manticore vygeneruje testovací případy pro každou prozkoumanou cestu.
Shrnutí: Spuštění pod Manticore
Když spojíme všechny předchozí kroky, získáme:
1from manticore.ethereum import ManticoreEVM23m = ManticoreEVM()45with open('example.sol') as f:6 source_code = f.read()78user_account = m.create_account(balance=1000)9contract_account = m.solidity_create_contract(source_code, owner=user_account)1011symbolic_var = m.make_symbolic_value()12contract_account.f(symbolic_var)1314print("Výsledky jsou v {}".format(m.workspace))15m.finalize() # zastavení prozkoumáváníZobrazit všeVeškerý výše uvedený kód najdete v example_run.py (opens in a new tab)
Získání cest s výjimkami
Nyní vygenerujeme specifické vstupy pro cesty, které vyvolávají výjimku v f(). Cílem je stále následující chytrý kontrakt example.sol (opens in a new tab):
1pragma solidity >=0.4.24 <0.6.0;2contract Simple {3 function f(uint a) payable public{4 if (a == 65) {5 revert();6 }7 }8}Použití informací o stavu
Každá provedená cesta má svůj stav blockchainu. Stav je buď připraven, nebo je ukončen, což znamená, že dosáhne instrukce THROW nebo REVERT:
- m.ready_states (opens in a new tab): seznam stavů, které jsou připraveny (neprovedly REVERT/INVALID)
- m.killed_states (opens in a new tab): seznam stavů, které jsou ukončeny
- m.all_states (opens in a new tab): všechny stavy
1for state in m.all_states:2 # udělat něco se stavemMůžete přistupovat k informacím o stavu. Například:
state.platform.get_balance(account.address): zůstatek účtustate.platform.transactions: seznam transakcístate.platform.transactions[-1].return_data: data vrácená poslední transakcí
Data vrácená poslední transakcí jsou pole, které lze převést na hodnotu pomocí ABI.deserialize, například:
1data = state.platform.transactions[0].return_data2data = ABI.deserialize("uint", data)Jak generovat testovací případ
Pro generování testovacího případu použijte m.generate_testcase(state, name) (opens in a new tab):
1m.generate_testcase(state, 'BugFound')Shrnutí
- Stav můžete iterovat pomocí m.all_states
state.platform.get_balance(account.address)vrací zůstatek účtustate.platform.transactionsvrací seznam transakcítransaction.return_datajsou vrácená datam.generate_testcase(state, name)generuje vstupy pro daný stav
Shrnutí: Získání cesty s výjimkou
1from manticore.ethereum import ManticoreEVM23m = ManticoreEVM()45with open('example.sol') as f:6 source_code = f.read()78user_account = m.create_account(balance=1000)9contract_account = m.solidity_create_contract(source_code, owner=user_account)1011symbolic_var = m.make_symbolic_value()12contract_account.f(symbolic_var)1314## Zkontrolujte, zda provádění končí s REVERT nebo INVALID1516for state in m.terminated_states:17 last_tx = state.platform.transactions[-1]18 if last_tx.result in ['REVERT', 'INVALID']:19 print('Nalezena výjimka {}'.format(m.workspace))20 m.generate_testcase(state, 'ThrowFound')Zobrazit všeVeškerý výše uvedený kód naleznete v example_run.py (opens in a new tab)
Poznámka: mohli jsme vygenerovat mnohem jednodušší skript, protože všechny stavy vrácené terminated_state mají ve svém výsledku REVERT nebo INVALID: tento příklad měl pouze demonstrovat, jak manipulovat s API.
Přidávání omezení
Podíváme se, jak omezit prozkoumávání. Budeme vycházet z předpokladu, že
dokumentace funkce f() uvádí, že funkce se nikdy nevolá s a == 65, takže jakákoli chyba s a == 65 není skutečnou chybou. Cílem je stále následující chytrý kontrakt example.sol (opens in a new tab):
1pragma solidity >=0.4.24 <0.6.0;2contract Simple {3 function f(uint a) payable public{4 if (a == 65) {5 revert();6 }7 }8}Operátory
Modul Operators (opens in a new tab) usnadňuje manipulaci s omezeními, mimo jiné poskytuje:
- Operators.AND,
- Operators.OR,
- Operators.UGT (větší než bez znaménka),
- Operators.UGE (větší nebo rovno bez znaménka),
- Operators.ULT (menší než bez znaménka),
- Operators.ULE (menší nebo rovno bez znaménka).
Pro import modulu použijte následující příkaz:
1from manticore.core.smtlib import OperatorsOperators.CONCAT se používá ke zřetězení pole na hodnotu. Například return_data transakce musí být změněna na hodnotu, která se má porovnat s jinou hodnotou:
1last_return = Operators.CONCAT(256, *last_return)Omezení
Omezení můžete použít globálně nebo pro určitý stav.
Globální omezení
Použijte m.constrain(constraint) pro přidání globálního omezení.
Můžete například zavolat kontrakt ze symbolické adresy a omezit tuto adresu na konkrétní hodnoty:
1symbolic_address = m.make_symbolic_value()2m.constraint(Operators.OR(symbolic == 0x41, symbolic_address == 0x42))3m.transaction(caller=user_account,4 address=contract_account,5 data=m.make_symbolic_buffer(320),6 value=0)Omezení stavu
Použijte state.constrain(constraint) (opens in a new tab) k přidání omezení ke konkrétnímu stavu. Lze jej použít k omezení stavu po jeho prozkoumání a ke kontrole nějaké jeho vlastnosti.
Kontrola omezení
Použijte solver.check(state.constraints), abyste zjistili, zda je omezení stále proveditelné.
Následující příklad například omezí symbolic_value tak, aby se lišila od 65, a zkontroluje, zda je stav stále proveditelný:
1state.constrain(symbolic_var != 65)2if solver.check(state.constraints):3 # stav je proveditelnýShrnutí: Přidání omezení
Přidáním omezení k předchozímu kódu získáme:
1from manticore.ethereum import ManticoreEVM2from manticore.core.smtlib.solver import Z3Solver34solver = Z3Solver.instance()56m = ManticoreEVM()78with open("example.sol") as f:9 source_code = f.read()1011user_account = m.create_account(balance=1000)12contract_account = m.solidity_create_contract(source_code, owner=user_account)1314symbolic_var = m.make_symbolic_value()15contract_account.f(symbolic_var)1617no_bug_found = True1819## Zkontrolujte, zda provádění končí s REVERT nebo INVALID2021for state in m.terminated_states:22 last_tx = state.platform.transactions[-1]23 if last_tx.result in ['REVERT', 'INVALID']:24 # nebereme v úvahu cestu, kde a == 6525 condition = symbolic_var != 6526 if m.generate_testcase(state, name="BugFound", only_if=condition):27 print(f'Byla nalezena chyba, výsledky jsou v {m.workspace}')28 no_bug_found = False2930if no_bug_found:31 print(f'Nebyla nalezena žádná chyba')Zobrazit všeVeškerý výše uvedený kód naleznete v example_run.py (opens in a new tab)
Stránka naposledy aktualizována: 26. dubna 2024