Jak korzystać z Manticore, aby znaleźć błędy w inteligentnych kontraktach
Celem tego samouczka jest pokazanie, jak używać Manticore do automatycznego wyszukiwania błędów w inteligentnych kontraktach.
Instalacja
Manticore wymaga >= Pythona 3.6. Można go zainstalować za pomocą pip lub dockera.
Manticore przez dockera
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/training trailofbits/eth-security-toolboxOstatnie polecenie uruchamia eth-security-toolbox w dockerze, który ma dostęp do bieżącego katalogu. Możesz zmienić pliki z hosta i uruchomić narzędzia na plikach z dockera
Wewnątrz dockera uruchom:
solc-select 0.5.11 cd /home/trufflecon/Manticore przez pip
pip3 install --user manticorezaleca się solc 0.5.11.
Uruchom skrypt
Aby uruchomić skrypt Pythona za pomocą Pythona 3:
python3 script.pyWprowadzenie do dynamicznego wykonania symbolicznego
Dynamiczne wykonanie symboliczne w skrócie
Dynamiczne wykonanie symboliczne (DSE) to technika analizy programu, która bada przestrzeń stanów z wysokim stopniem świadomości semantycznej. Ta technika opiera się na odkryciu „ścieżek programu”, przedstawianych jako wzory matematyczne o nazwie path predicates. W ujęciu koncepcyjnym technika ta działa na ścieżce dwueatpowo:
- Są one konstruowane przy użyciu ograniczeń na wejściu programu.
- Są one używane do generowania danych wejściowych programu, które spowodują wykonanie powiązanych ścieżek.
Takie podejście nie daje fałszywych alarmów w tym sensie, że wszystkie zidentyfikowane stany programu mogą zostać wyzwolone podczas konkretnego wykonania. Na przykład, jeżeli w wyniku analizy stwierdza się przepełnienie liczby całkowitej, gwarantuje się, że jest ono odtwarzalne.
Przykład predykatu ścieżki
Aby zrozumieć, jak działa DSE, rozważmy następujący przykład:
1function f(uint a){23 if (a == 65) {4 // A bug is present5 }67}Jako że f() zawiera dwie ścieżki, DSE będzie konstruować dwa różne predykaty ścieżki:
- Ścieżka 1:
a == 65 - Ścieżka 2:
Not (== 65)
Każdy predykat ścieżki jest wzorem matematycznym, który można przypisać tak zwanemu SMT solver, który spróbuje rozwiązać to równanie. W przypadku Path 1 solver powie, że ścieżka może zostać zbadana za pomocą a = 65. Dla Path 2 solver może dać a dowolną wartość inną niż 65, na przykład a = 0.
Weryfikacja właściwości
Manticore pozwala na pełną kontrolę nad każdym wykonaniem każdej ścieżki. W rezultacie pozwala dodawać dowolne ograniczenia do prawie wszystkiego. Kontrola ta pozwala na tworzenie właściwości na kontrakcie.
Rozważ następujący przykład:
1function unsafe_add(uint a, uint b) returns(uint c){2 c = a + b; // no overflow protection3 return c;4}Tutaj jest tylko jedna ścieżka do zbadania w funkcji:
- Ścieżka 1:
c = a + b
Używając Manticore, możesz sprawdzić przepełnienie i dodać ograniczenia do predykatu ścieżki:
c = a + b AND (c < a OR c < b)
Jeśli można znaleźć ocenę a i b, dla której powyższy predykat ścieżki jest wykonalny, oznacza to, że znaleziono przepełnienie. Na przykład solver może wygenerować wejście a = 10 , b = MAXUINT256.
Jeśli rozważasz wersję stałą:
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}Powiązany wzór z kontrolą przepełnienia to:
c = a + b AND (c >= a) AND (c=>b) AND (c < a OR c < b)
Ten wzór nie może być rozwiązany; innymi słowy jest to dowód, że w safe_add c zawsze będzie wzrastać.
DSE jest więc potężnym narzędziem, które może weryfikować dowolne ograniczenia Twojego kodu.
Uruchamianie pod Manticore
Zobaczymy, jak zbadać inteligentny kontrakt z API Manticore. Celem jest następujący inteligentny kontrakt example.sol:
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}Pokaż wszystkoUruchom samodzielną eksplorację
Możesz uruchomić Manticore bezpośrednio na inteligentnym kontrakcie za pomocą następującego polecenia (projekt może być plikiem Solidity lub katalogiem projektu):
$ manticore projectOtrzymasz wyniki takich przypadków testowych (kolejność może się zmienić):
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...Pokaż wszystkoBez dodatkowych informacji Manticore będzie badać kontrakt za pomocą nowej transakcji symbolicznej, dopóki nie zbada nowych ścieżek w kontrakcie. Manticore nie uruchamia nowych transakcji po niepowodzeniu (np. po wycofaniu).
Manticore umieści te informacje w katalogu mcore_*. W tym katalogu znajdziesz między innymi:
global.summary: ostrzeżenia o zakresie i kompilatorzetest_XXXXX.summary: zakres, ostatnia instrukcja, salda konta na przypadek testowytest_XXXXX.tx: szczegółowa lista transakcji na przypadek testowy
W tym miejscu Manticore znajduje 7 przypadków testowych, które odpowiadają (nazwa pliku może się zmienić):
| Transakcja 0 | Transakcja 1 | Transakcja 2 | Wynik | |
|---|---|---|---|---|
| test_00000000.tx | Tworzenie kontraktu | f(!=65) | f(!=65) | STOP |
| test_00000001.tx | Tworzenie kontraktu | funkcja zastępcza | REVERT | |
| test_00000002.tx | Tworzenie kontraktu | RETURN | ||
| test_00000003.tx | Tworzenie kontraktu | f(65) | REVERT | |
| test_00000004.tx | Tworzenie kontraktu | f(!=65) | STOP | |
| test_00000005.tx | Tworzenie kontraktu | f(!=65) | f(65) | REVERT |
| test_00000006.tx | Tworzenie kontraktów | f(!=65) | funkcja zastępcza | REVERT |
Podsumowanie eksploracji f(!=65) oznacza f o dowolnej wartości innej niż 65.
Jak możesz zauważyć, Manticore generuje unikalny przypadek testowy dla każdej udanej lub odwróconej transakcji.
Użyj flagi --quick-mode, jeśli chcesz szybko eksplorować kod (wyłącza wykrywanie błędów, obliczanie gazu, ...)
Obsługa inteligentnego kontraktu poprzez API
W tej sekcji opisano szczegóły, jak obsługiwać inteligentny kontrakt za pośrednictwem API Pythona Manticore. Możesz utworzyć nowy plik z rozszerzeniem python *.py i napisać potrzebny kod, dodając polecenia API (podstawy, które zostaną opisane poniżej) do tego pliku, a następnie uruchom go poleceniem $ python3 *.py. Możesz również wykonać poniższe polecenia bezpośrednio w konsoli Pythona, aby uruchomić konsolę, użyj polecenia $ python3.
Tworzenie kont
Pierwszą rzeczą, którą powinieneś zrobić, jest uruchomienie nowego blockchaina za pomocą następujących poleceń:
1from manticore.ethereum import ManticoreEVM23m = ManticoreEVM()Konto bez kontraktu jest tworzone przy użyciu m.create_account:
1user_account = m.create_account(balance=1000)Kontrakt Solidity można wdrożyć za pomocą m.solidity_create_contract:
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# Initiate the contract12contract_account = m.solidity_create_contract(source_code, owner=user_account)Pokaż wszystkoPodsumowanie
- Konta użytkowników i konitraktów można tworzyć za pomocą m.create_account i m.solidity_create_contract.
Wykonywanie transakcji
Manticore obsługuje dwa rodzaje transakcji:
- Transakcja surowa: wszystkie funkcje są analizowane
- Nazwana transakcja: analizowana jest tylko jedna funkcja
Transakcja surowa
Surowa transakcja jest wykonywana przy użyciu m.transaction:
1m.transaction(caller=user_account,2 address=contract_account,3 data=data,4 value=value)Wywołujący, adres, dane lub wartość transakcji mogą być konkretne lub symboliczne:
- m.make_symbolic_value tworzy wartość symboliczną.
- m.make_symbolic_buffer(size) tworzy symboliczną tablicę bajtów.
Na przykład:
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)Jeżeli dane są symboliczne, Manticore zbada wszystkie funkcje kontraktu podczas realizacji transakcji. Pomocne będzie zapoznanie się z wyjaśnieniem funkcji fallback w artykule wyjaśniającym, jak działa wybór funkcji: Hands on the Ethernaut CTF.
Nazwana transakcja
Funkcje mogą być wykonywane za pośrednictwem ich nazwy. Aby wykonać f(uint var) z wartością symboliczną, z user_account i 0 etherów, użyj:
1symbolic_var = m.make_symbolic_value()2contract_account.f(symbolic_var, caller=user_account, value=0)Jeśli wartość value transakcji nie jest określona, jest to domyślnie 0.
Podsumowanie
- Argumenty transakcji mogą być konkretne lub symboliczne
- Surowa transakcja analizuje wszystkie funkcje
- Funkcja może być wywołana przez jej nazwę
Obszar roboczy
m.workspace to katalog używany jako katalog wyjściowy dla wszystkich wygenerowanych plików:
1print("Results are in {}".format(m.workspace))Kończenie eksploracji
Aby zatrzymać eksplorację, użyj m.finalize(). Kolejne transakcje nie powinny być wysyłane po wywołaniu tej metody i wygenerowaniu przypadków testowych dla każdej zbadanej ścieżki.
Podsumowanie: uruchamianie pod Manticore
Łącząc wszystkie poprzednie kroki, otrzymujemy:
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("Results are in {}".format(m.workspace))15m.finalize() # stop the explorationPokaż wszystkoCały powyższy kod można znaleźć w example_run.py
Pobieranie ścieżek zgłaszania
Teraz wygenerujemy konkretne dane wejściowe dla ścieżek zgłaszających wyjątek w f(). Celem jest nadal następujący inteligentny kontrakt example.sol:
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}Używanie informacji o stanie
Każda wykonana ścieżka ma swój stan blockchain. Stan jest gotowy lub został zabity, co oznacza, że osiąga instrukcję THROW lub REVERT:
- m.ready_states: lista stanów, które są gotowe (nie wykonały REVERT/INVALID)
- m.killed_states: lista stanów, które są gotowe (nie wykonały REVERT/INVALID)
- m.all_states: wszystkie stany
1for state in m.all_states:2 # do something with stateMożesz uzyskać dostęp do informacji o stanie. Na przykład:
state.platform.get_balance(account.address): saldo kontastate.platform.transactions: lista transakcjistate.platform.transactions[-1].return_data: dane zwrócone przez ostatnią transakcję
Dane zwrócone przez ostatnią transakcję są tablicą, która może zostać przekonwertowana na wartość z ABI.deserialize, na przykład:
1data = state.platform.transactions[0].return_data2data = ABI.deserialize("uint", data)Jak wygenerować przypadek testowy
Użyj m.generate_testcase(stan, nazwa) aby wygenerować testcase:
1m.generate_testcase(state, 'BugFound')Podsumowanie
- Możesz iterować ponad stanem za pomocą stanów m.all_states
state.platform.get_balance(account.address)zwraca saldo kontaState.platform.transactionszwraca listę transakcjitransaction.return_datato dane zwróconem.generate_testcase(stan, nazwa)generuje dane wejściowe dla stanu
Podsumowanie: uzyskanie ścieżki zgłaszania
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## Check if an execution ends with a REVERT or INVALID15for state in m.terminated_states:16 last_tx = state.platform.transactions[-1]17 if last_tx.result in ['REVERT', 'INVALID']:18 print('Throw found {}'.format(m.workspace))19 m.generate_testcase(state, 'ThrowFound')Pokaż wszystkoCały powyższy kod można znaleźć w example_run.py
Zauważ, że mogliśmy wygenerować znacznie prostszy skrypt, ponieważ wszystkie stany zwrócone przez terminated_state mają w wyniku REVERT lub INVALID: ten przykład miał jedynie zademonstrować, jak obsługiwać API.
Dodawanie ograniczeń
Zobaczymy, jak ograniczyć eksplorację. Przyjmiemy założenie, że dokumentacja f() stwierdza, że funkcja nigdy nie jest wywoływana z a == 65, więc każdy błąd z a == 65 nie jest prawdziwy błąd. Celem jest nadal następujący inteligentny kontrakt example.sol:
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}Operatory
Moduł Operatorzy ułatwia manipulowanie ograniczeniami między innymi:
- Operators.AND,
- Operators.OR,
- Operators.UGT (bez znaku większe niż),
- Operators.UGE (bez znaku większe lub równe),
- Operators.ULT (bez znaku mniejsze niż),
- Operators.ULE (bez znaku mniejsze lub równe).
Aby zaimportować moduł, użyj następujących elementów:
1from manticore.core.smtlib import OperatorsOperators.CONCAT jest używany do dołączania tablicy do wartości. Na przykład należy zmienić wartość return_data transakcji na wartość sprawdzaną względem innej wartości:
1last_return = Operators.CONCAT(256, *last_return)Ograniczenia
Możesz używać ograniczeń globalnie lub dla określonego stanu.
Globalne ograniczenie
Użyj m.constrain (constraint) aby dodać globalne ograniczenie. Na przykład możesz wywołać kontrakt z adresu symbolicznego i ograniczyć ten adres do określonych wartości:
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)Ograniczenie stanu
Użyj state.constrain(constraint), aby dodać ograniczenie do określonego stanu Może być używany do ograniczania stanu po jego eksploracji, aby sprawdzić na nim jakąś właściwość.
Sprawdzanie ograniczenia
Użyj solver.check(state.constraints), aby dowiedzieć się, czy ograniczenie jest nadal możliwe. Na przykład poniższe ograniczy symbolic_value do wartości różnej od 65 i sprawdzi, czy stan jest nadal możliwy:
1state.constrain(symbolic_var != 65)2if solver.check(state.constraints):3 # state is feasiblePodsumowanie: dodwanie ograniczeń
Dodając ograniczenie do poprzedniego kodu, otrzymujemy:
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## Check if an execution ends with a REVERT or INVALID20for state in m.terminated_states:21 last_tx = state.platform.transactions[-1]22 if last_tx.result in ['REVERT', 'INVALID']:23 # we do not consider the path were a == 6524 condition = symbolic_var != 6525 if m.generate_testcase(state, name="BugFound", only_if=condition):26 print(f'Bug found, results are in {m.workspace}')27 no_bug_found = False2829if no_bug_found:30 print(f'No bug found')Pokaż wszystkoCały powyższy kod można znaleźć w example_run.py