Slither – narzędzie do znajdowania błędów w inteligentnych kontraktach
Jak używać Slither
Celem tego samouczka jest pokazanie, jak używać Slither do automatycznego wyszukiwania błędów w inteligentnych kontraktach.
- Instalacja
- Użycie wiersza poleceń
- Wprowadzenie do analizy statycznej: krótkie wprowadzenie do analizy statycznej
- API: Opis API Pythona
Instalacja
Slither wymaga Pythona >= 3.6. Można go zainstalować za pomocą pip lub dockera.
Slither przez pip:
pip3 install --user slither-analyzer
Slither przez dockera:
docker pull trailofbits/eth-security-toolbox docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox
Ostatnie 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/
Uruchom skrypt
Aby uruchomić skrypt Pythona za pomocą Pythona 3:
python3 script.py
Wiersz poleceń
Skrypty wiersza poleceń a zdefiniowane przez użytkownika. Slither jest wyposażony w zestaw predefiniowanych detektorów, które znajdują wiele częstych błędów. Wywołanie Slither z wiersza poleceń uruchomi wszystkie detektory, nie jest potrzebna szczegółowa wiedza na temat analizy statycznej:
slither project_paths
Oprócz detektorów, Slither ma możliwości przeglądania kodu poprzez swoje narzędzia drukarki i .
Użyj crytic.io, aby uzyskać dostęp do prywatnych detektorów i integracji GitHub.
Analiza statyczna
Możliwości i projekt struktury analizy statycznej Slither zostały opisane w postach na blogu (1, 2) oraz w dokumencie akademickim.
Istnieją różne postacie analizy statycznej Najprawdopodobniej zdajesz sobie sprawę, że kompilatory takie jak clang i gcc zależą od tych technik badawczych, ale również stanowią one podstawę (Infer, CodeClimate, FindBugs i narzędzi opartych na formalnych metodach, takich jak Frama-C i Polyspace.
Nie dokonamy wyczerpującego przeglądu technik analizy statycznej. Zamiast tego skoncentrujemy się na tym, co jest potrzebne, aby zrozumieć, jak działa Slither tak, abyś mógł go skuteczniej używać, aby znaleźć błędy i zrozumieć kod.
Reprezentacja kodu
W przeciwieństwie do analizy dynamicznej, która rozważa pojedynczą ścieżkę wykonania, analiza statyczna rozważa wszystkie ścieżki naraz. W tym celu opiera się na innej reprezentacji kodu. Dwa najczęściej spotykane to abstrakcyjne drzewo składni (AST) i graf przepływu sterowania (CFG).
Abstrakcyjne drzewa składniowe (AST)
AST są używane za każdym razem, gdy kompilator analizuje kod. Jest to prawdopodobnie najbardziej podstawowa struktura, na podstawie której można przeprowadzić analizę statyczną.
Krótko mówiąc, AST jest ustrukturyzowanym drzewem, w którym zwyczajowo, każdy liść zawiera zmienną lub stałą, a węzły wewnętrzne są operandami lub operacjami przepływu sterowania. Rozważmy następujący kod:
1function safeAdd(uint a, uint b) pure internal returns(uint){2 if(a + b <= a){3 revert();4 }5 return a + b;6}7Kopiuj
Odpowiedni AST jest pokazany w:
Slither używa AST eksportowanego przez solc.
Choć prosty w budowie, AST jest strukturą zagnieżdżoną. Czasem jego przeanalizowanie nie jest proste. Na przykład, aby zidentyfikować operacje używane przez wyrażenie a + b <= a
, musisz najpierw przeanalizować <=
, a następnie +
. Wspólnym podejściem jest stosowanie tak zwanego wzoru odwiedzającego, który rekursywnie przechodzi przez drzewo. Slither zawiera ogólnego odwiedzającego w ExpressionVisitor
.
Następujący kod używa ExpressionVisitor
aby wykryć, czy wyrażenie zawiera dodatek:
1from slither.visitors.expression.expression import ExpressionVisitor2from slither.core.expressions.binary_operation import BinaryOperationType34class HasAddition(ExpressionVisitor):56 def result(self):7 return self._result89 def _post_binary_operation(self, expression):10 if expression.type == BinaryOperationType.ADDITION:11 self._result = True1213visitor = HasAddition(expression) # expression is the expression to be tested14print(f'The expression {expression} has a addition: {visitor.result()}')15Pokaż wszystkoKopiuj
Graf przepływu sterowania (CFG)
Drugą najbardziej powszechną reprezentacją kodu jest graf przepływu sterowania. Jak sugeruje jego nazwa, jest to przedstawienie oparte na wykresie, które ujawnia wszystkie ścieżki wykonania. Każdy węzeł zawiera jedną lub wiele instrukcji. Krawędzie na wykresie reprezentują operacje przepływu sterowania (if/then/else, loop itp.). CFG naszego poprzedniego przykładu to:
CFG jest reprezentacją, na której opiera się większość analiz.
Istnieje wiele innych reprezentacji kodów. Każda reprezentacja ma zalety i wady zgodnie z analizą, którą chcesz przeprowadzić.
Analiza
Najprostszym rodzajem analiz, które możesz wykonać za pomocą Slither, są analizy składni.
Analiza składni
Slither może nawigować przez różne elementy kodu i ich reprezentacje, aby znaleźć niespójności i wady za pomocą podejścia podobnego do dopasowania do wzorca.
Na przykład następujące detektory szukają problemów związanych z składnią:
Zastępowanie zmiennych stanu: porusza się iteracyjnie po wszystkich zmiennych stanu i sprawdza, czy któryś zastępuje zmienną z dziedziczonego kontraktu (state.py#L51-L62)
Nieprawidłowy interfejs ERC20: szukka nieprawidłowych sygnatur funkcji ERC20 (incorrect_erc20_interface.py#L34-L55)
Analiza semantyczna
W przeciwieństwie do analizy składni, analiza semantyczna sięga głębiej i analizuje „znaczenie” kodu. Rodzina ta obejmuje kilka szerokich rodzajów analiz. Prowadzą one do bardziej skutecznych i pożytecznych wyników, ale także są bardziej skomplikowane.
Analizy semantyczne są wykorzystywane do najbardziej zaawansowanego wykrywania podatności na zagrożenia.
Analiza zależności danych
Zmienna variable_a
jest zależna od danych variable_b
, jeśli istnieje ścieżka, dla której wartość variable_a
jest zależna od variable_b
.
W poniższym kodzie zmienna _a
jest zależna od variable_b
:
1// ...2variable_a = variable_b + 1;3Kopiuj
Slither posiada wbudowane funkcje zależności danych dzięki jego pośredniej reprezentacji (omówionej w dalszej części).
Przykład użycia zależności od danych można znaleźć w niebezpiecznym ścisłym detektorze równości. Tutaj Slither będzie szukał ścisłego porównania równości z niebezpieczną wartością (wronct_strict_equality. y#L86-L87), i poinformuje użytkownika, że powinien użyć >=
lub <=
zamiast ==
, aby uniemożliwić atakującemu przechwycenie kontraktu. Spośród innych detektor uzna za niebezpieczną wartość zwrotną wywołania do balanceOf(address)
(invalid _strict_equality. y#L63-L64) i użyje silnika zależności od danych, aby śledzić jego użycie.
Obliczenia stałoprzecinkowe
Jeśli Twoja analiza nawiguje przez CFG i porusza się wzdłuż krawędzi, prawdopodobnie zobaczysz już odwiedzone węzły. Na przykład, jeśli pętla jest przedstawiona w poniższy sposób:
1for(uint i; i < zakres; ++){2 variable_a += 13}4Kopiuj
Twoja analiza będzie musiała wiedzieć, kiedy się zatrzymać. Tutaj są dwie główne strategie: (1) powtórzyć na każdym węźle skończoną liczbę razy, (2) obliczyć tak zwany punkt stały. Punkt stały zasadniczo oznacza, że analiza tego węzła nie dostarcza żadnych istotnych informacji.
Przykład użytego puntu stałego można znaleźć w detektorach wielobieżności: Slither eksploruje węzły i szuka wywołań zewnętrznych, zapisuje i odczytuje w pamięci. Po osiągnięciu punktu stałego (reentrancy.py#L125-L131), zatrzymuje eksplorację i analizuje wyniki, aby sprawdzić, czy występuje wielobieżność, sprawdzając różne jej wzorce (reentrancy_benign. y, reentrancy_read_before_write.py, reentrancy_eth.py).
Analizy pisania z wykorzystaniem efektywnego obliczania punktów stałych wymagają dobrego zrozumienia sposobu, w jaki analiza propaguje jej informacje.
Reprezentacja pośrednia
Pośrednia reprezentacja (IR) to język mający być bardziej dostosowany do analizy statycznej niż oryginalny. Slither tłumaczy Solidity na własną IR: SlithIR.
Zrozumienie SlithIR nie jest konieczne, jeśli chcesz tylko zapisać podstawowe kontrole. Jeśli jednak planuje się napisać zaawansowane analizy semantyczne, będzie to pomocne. Drukarki SlithIR i SSA pomogą Ci zrozumieć, jak kod jest przetłumaczony.
Podstawowe informacje o API
Slither ma interfejs API, który pozwala odkrywać podstawowe atrybuty kontraktu i jego funkcje.
Aby załadować bazę kodu:
1from slither import Slither2slither = Slither('/path/to/project')34Kopiuj
Odkrywanie kontraktów i funkcji
Obiekt Slither
zawiera:
contracts (list(Contract)
: lista kontraktówcontracts_derived (list(Contract)
: lista kontraktów, które nie są dziedziczone przez inny kontrakt (podzbiór kontraktów)get_contract_from_name (str)
: zwraca kontrakt z jego nazwy
Obiekt Contract
ma:
name (str)
: nazwa kontraktufunctions (list(Function))
: lista funkcjimodifiers (list(Modifier))
: lista funkcjiall_functions_lated (list(Function/Modifier))
: lista wszystkich funkcji wewnętrznych osiągalnych przez kontraktinheritance (list(Contract))
: lista dziedziczonych kontraktówget_function_from_signature (str)
: zwraca funkcję z jej podpisuget_modifier_from_signature (str)
: zwraca modyfikator z jego podpisuget_state_variable_from_name (str)
: zwraca zmienną stanową z jej nazwy
Obiekt Function
lub Modifier
ma:
name (str)
: nazwa funkcjicontract (contract)
: kontrakt, w którym zadeklarowana jest funkcjanodes (list(Node))
: lista węzłów tworzących CFG funkcji/modyfikatoraentry_point (Node)
: punkt wejścia CFGvariables_read (list(Variable))
: lista odczytanych zmiennychvariables_written (list(Variable))
: lista zapisanych zmiennychstate_variables_read (list(StateVariable))
: lista odczytanych zmiennych stanu (podzbiór zmiennych`read)state_variables_written (list(StateVariable))
: lista zapisanych zmiennych stanu (podzbiór zmiennych`written)
Przykład: Drukuj podstawowe informacje
print_basic_information.py pokazuje, jak wydrukować podstawowe informacje o projekcie.