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 docker
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.11cd /home/trufflecon/Manticore przez pip
pip3 install --user manticoreZalecany jest solc w wersji 0.5.11.
Uruchamianie skryptu
Aby uruchomić skrypt Pythona za pomocą Pythona 3:
python3 script.pyWprowadzenie do dynamicznej egzekucji symbolicznej
Dynamiczna egzekucja symboliczna w pigułce
Dynamiczna egzekucja symboliczna (DSE) to technika analizy programu, która bada przestrzeń stanów z wysokim stopniem świadomości semantycznej. Technika ta opiera się na odkrywaniu „ścieżek programu” reprezentowanych jako formuły matematyczne zwane path predicates. Koncepcyjnie, technika ta operuje na predykatach ścieżek w dwóch krokach:
- 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.
Podejście to nie generuje fałszywych alarmów, w tym sensie, że wszystkie zidentyfikowane stany programu mogą zostać wywołane podczas konkretnej egzekucji. Na przykład, jeśli analiza znajdzie przepełnienie liczby całkowitej, jest ono gwarantowanie 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 // Występuje błąd5 }67}Ponieważ f() zawiera dwie ścieżki, DSE skonstruuje dwa różne predykaty ścieżek:
- Ścieżka 1:
a == 65 - Ścieżka 2:
Not (a == 65)
Każdy predykat ścieżki jest formułą matematyczną, którą można przekazać do tak zwanego solvera SMTopens in a new tab, który spróbuje rozwiązać równanie. Dla Ścieżki 1 solver stwierdzi, że ścieżka może być zbadana przy a = 65. Dla Ścieżki 2 solver może podać dla a dowolną wartość inną niż 65, na przykład a = 0.
Weryfikacja właściwości
Manticore pozwala na pełną kontrolę nad wykonaniem każdej ścieżki. W rezultacie pozwala na dodawanie dowolnych ograniczeń do prawie wszystkiego. Taka kontrola pozwala na tworzenie właściwości w kontrakcie.
Rozważmy następujący przykład:
1function unsafe_add(uint a, uint b) returns(uint c){2 c = a + b; // brak zabezpieczenia przed przepełnieniem3 return c;4}W tej funkcji istnieje tylko jedna ścieżka do zbadania:
- Ścieżka 1:
c = a + b
Używając Manticore, można sprawdzić przepełnienie i dodać ograniczenia do predykatu ścieżki:
c = a + b AND (c < a OR c < b)
Jeśli możliwe jest znalezienie takich wartości a i b, dla których powyższy predykat ścieżki jest spełnialny, oznacza to, że znaleziono przepełnienie. Na przykład solver może wygenerować dane wejściowe a = 10, b = MAXUINT256.
Rozważmy poprawioną wersję:
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ązana formuła ze sprawdzaniem przepełnienia wyglądałaby następująco:
c = a + b AND (c >= a) AND (c=>b) AND (c < a OR c < b)
Ta formuła nie może zostać rozwiązana; innymi słowy jest to dowód na to, że w safe_add, c zawsze będzie rosło.
Dlatego DSE jest potężnym narzędziem, które może weryfikować dowolne ograniczenia w Twoim kodzie.
Uruchamianie w Manticore
W tej sekcji pokazano, jak eksplorować inteligentny kontrakt przy użyciu API Manticore. Celem jest następujący inteligentny kontrakt example.solopens 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}Pokaż wszystkoUruchomienie samodzielnej eksploracji
Możesz uruchomić Manticore bezpośrednio na inteligentnym kontrakcie za pomocą następującego polecenia (project może być plikiem Solidity lub katalogiem projektu):
$ manticore projectOtrzymasz dane wyjściowe przypadków testowych, takie jak te (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ą nowych transakcji symbolicznych, dopóki nie zbada nowych ścieżek w kontrakcie. Manticore nie uruchamia nowych transakcji po nieudanej transakcji (np. po operacji revert).
Manticore zapisze informacje w katalogu mcore_*. W tym katalogu znajdziesz między innymi:
global.summary: pokrycie kodu i ostrzeżenia kompilatoratest_XXXXX.summary: pokrycie kodu, ostatnia instrukcja, salda kont dla każdego przypadku testowegotest_XXXXX.tx: szczegółowa lista transakcji dla każdego przypadku testowego
Manticore znajduje tutaj 7 przypadków testowych, które odpowiadają (kolejność nazw plików może się zmienić):
| Transakcja 0 | Transakcja 1 | Transakcja 2 | Wynik | |
|---|---|---|---|---|
| test_00000000.tx | Utworzenie kontraktu | f(!=65) | f(!=65) | STOP |
| test_00000001.tx | Utworzenie kontraktu | funkcja zastępcza | REVERT | |
| test_00000002.tx | Utworzenie kontraktu | RETURN | ||
| test_00000003.tx | Utworzenie kontraktu | f(65) | REVERT | |
| test_00000004.tx | Utworzenie kontraktu | f(!=65) | STOP | |
| test_00000005.tx | Utworzenie kontraktu | f(!=65) | f(65) | REVERT |
| test_00000006.tx | Utworzenie kontraktu | f(!=65) | funkcja zastępcza | REVERT |
Podsumowanie eksploracji: f(!=65) oznacza wywołanie f z dowolną wartością inną niż 65.
Jak można zauważyć, Manticore generuje unikalny przypadek testowy dla każdej pomyślnej lub anulowanej (reverted) transakcji.
Użyj flagi --quick-mode, jeśli chcesz szybkiej eksploracji kodu (wyłącza ona wykrywacze błędów, obliczanie gazu, ...)
Manipulowanie inteligentnym kontraktem poprzez API
Ta sekcja opisuje szczegółowo, jak manipulować inteligentnym kontraktem za pośrednictwem API Manticore dla Pythona. Możesz utworzyć nowy plik z rozszerzeniem Pythona *.py i napisać niezbędny kod, dodając polecenia API (których podstawy zostaną opisane poniżej) do tego pliku, a następnie uruchomić go za pomocą polecenia $ 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ą należy zrobić, jest zainicjowanie nowego blockchaina za pomocą następujących poleceń:
1from manticore.ethereum import ManticoreEVM23m = ManticoreEVM()Konto niebędące kontraktem jest tworzone za pomocą m.create_accountopens in a new tab:
1user_account = m.create_account(balance=1000)Kontrakt Solidity można wdrożyć za pomocą m.solidity_create_contractopens 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# Zainicjuj kontrakt12contract_account = m.solidity_create_contract(source_code, owner=user_account)Pokaż wszystkoPodsumowanie
- Można tworzyć konta użytkowników i konta kontraktów za pomocą m.create_accountopens in a new tab i m.solidity_create_contractopens in a new tab.
Wykonywanie transakcji
Manticore obsługuje dwa typy transakcji:
- Surowa transakcja: wszystkie funkcje są badane
- Nazwana transakcja: tylko jedna funkcja jest badana
Surowa transakcja
Surowa transakcja jest wykonywana za pomocą m.transactionopens in a new tab:
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_valueopens in a new tab tworzy wartość symboliczną.
- m.make_symbolic_buffer(size)opens in a new tab 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śli dane są symboliczne, Manticore zbada wszystkie funkcje kontraktu podczas wykonywania transakcji. Pomocne będzie zapoznanie się z wyjaśnieniem funkcji fallback w artykule Hands on the Ethernaut CTFopens in a new tab, aby zrozumieć, jak działa wybór funkcji.
Nazwana transakcja
Funkcje mogą być wykonywane za pomocą 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 value transakcji nie jest określone, domyślnie wynosi 0.
Podsumowanie
- Argumenty transakcji mogą być konkretne lub symboliczne
- Surowa transakcja zbada wszystkie funkcje
- Funkcje mogą być wywoływane po nazwie
Przestrzeń robocza
m.workspace to katalog używany jako katalog wyjściowy dla wszystkich wygenerowanych plików:
1print("Results are in {}".format(m.workspace))Zakończenie eksploracji
Aby zatrzymać eksplorację, użyj m.finalize()opens in a new tab. Po wywołaniu tej metody nie należy wysyłać żadnych dalszych transakcji, a Manticore generuje przypadki testowe dla każdej zbadanej ścieżki.
Podsumowanie: Uruchamianie w 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() # zatrzymaj eksploracjęPokaż wszystkoCały powyższy kod można znaleźć w example_run.pyopens in a new tab
Uzyskiwanie ścieżek powodujących błąd
Teraz wygenerujemy określone dane wejściowe dla ścieżek zgłaszających wyjątek w f(). Celem nadal jest następujący inteligentny kontrakt example.solopens 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}Korzystanie z informacji o stanie
Każda wykonana ścieżka ma swój stan blockchaina. Stan jest gotowy albo zakończony (killed), co oznacza, że osiąga instrukcję THROW lub REVERT:
- m.ready_statesopens in a new tab: lista stanów, które są gotowe (nie wykonały REVERT/INVALID)
- m.killed_statesopens in a new tab: lista stanów, które są zakończone (killed)
- m.all_statesopens in a new tab: wszystkie stany
1for state in m.all_states:2 # zrób coś ze stanemMoż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ę to tablica, którą można przekonwertować na wartość za pomocą 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(state, name)opens in a new tab, aby wygenerować przypadek testowy:
1m.generate_testcase(state, 'BugFound')Podsumowanie
- Można iterować po stanach za pomocą
m.all_states state.platform.get_balance(account.address)zwraca saldo kontastate.platform.transactionszwraca listę transakcjitransaction.return_datato zwrócone danem.generate_testcase(state, name)generuje dane wejściowe dla stanu
Podsumowanie: uzyskiwanie ścieżki powodującej błąd
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## Sprawdź, czy wykonanie kończy się instrukcją REVERT lub INVALID1516for state in m.terminated_states:17 last_tx = state.platform.transactions[-1]18 if last_tx.result in ['REVERT', 'INVALID']:19 print('Throw found {}'.format(m.workspace))20 m.generate_testcase(state, 'ThrowFound')Pokaż wszystkoCały powyższy kod można znaleźć w example_run.pyopens in a new tab
Uwaga: mogliśmy wygenerować znacznie prostszy skrypt, ponieważ wszystkie stany zwrócone przez terminated_states mają w wyniku REVERT lub INVALID: ten przykład miał jedynie na celu zademonstrowanie, jak manipulować API.
Dodawanie ograniczeń
Zobaczymy, jak ograniczyć eksplorację. Przyjmiemy założenie, że dokumentacja funkcji f() stwierdza, że funkcja nigdy nie jest wywoływana z a == 65, więc każdy błąd przy a == 65 nie jest prawdziwym błędem. Celem nadal jest następujący inteligentny kontrakt example.solopens 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}Operatory
Moduł Operatorsopens in a new tab ułatwia manipulowanie ograniczeniami, między innymi udostępnia:
- Operators.AND,
- Operators.OR,
- Operators.UGT (większe niż bez znaku),
- Operators.UGE (większe lub równe niż bez znaku),
- Operators.ULT (mniejsze niż bez znaku),
- Operators.ULE (mniejsze lub równe niż bez znaku).
Aby zaimportować moduł, użyj następującego polecenia:
1from manticore.core.smtlib import OperatorsOperators.CONCAT służy do łączenia tablicy z wartością. Na przykład return_data transakcji musi zostać zmienione na wartość, aby można było je porównać z inną wartością:
1last_return = Operators.CONCAT(256, *last_return)Ograniczenia
Można używać ograniczeń globalnie lub dla określonego stanu.
Ograniczenie globalne
Użyj m.constrain(constraint), aby dodać ograniczenie globalne.
Na przykład, można 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)opens in a new tab, aby dodać ograniczenie do określonego stanu. Można go użyć do ograniczenia stanu po jego eksploracji, aby sprawdzić na nim pewną właściwość.
Sprawdzanie ograniczenia
Użyj solver.check(state.constraints), aby dowiedzieć się, czy ograniczenie jest nadal możliwe do spełnienia.
Na przykład, poniższy kod ograniczy symbolic_value do wartości innej niż 65 i sprawdzi, czy stan jest nadal możliwy do spełnienia:
1state.constrain(symbolic_var != 65)2if solver.check(state.constraints):3 # stan jest możliwy do spełnieniaPodsumowanie: dodawanie 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## Sprawdź, czy wykonanie kończy się instrukcją REVERT lub INVALID2021for state in m.terminated_states:22 last_tx = state.platform.transactions[-1]23 if last_tx.result in ['REVERT', 'INVALID']:24 # nie bierzemy pod uwagę ścieżki, w której a == 6525 condition = symbolic_var != 6526 if m.generate_testcase(state, name="BugFound", only_if=condition):27 print(f'Bug found, results are in {m.workspace}')28 no_bug_found = False2930if no_bug_found:31 print(f'No bug found')Pokaż wszystkoCały powyższy kod można znaleźć w example_run.pyopens in a new tab
Strona ostatnio zaktualizowana: 14 lutego 2026