Przejdź do głównej zawartości

Slither – narzędzie do znajdowania błędów w inteligentnych kontraktach

solidityinteligentne kontraktyochronatestinganaliza statyczna
Zaawansowane
Trailofbits
Tworzenie bezpiecznych kontraktów(opens in a new tab)
9 czerwca 2020
6 minuta czytania minute read

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 drukarki(opens in a new tab) i narzędzia(opens in a new tab).

Użyj crytic.io(opens in a new tab), 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(opens in a new tab), 2(opens in a new tab)) oraz w dokumencie akademickim(opens in a new tab).

Istnieją różne postacie analizy statycznej Najprawdopodobniej zdajesz sobie sprawę, że kompilatory takie jak clang(opens in a new tab) i gcc(opens in a new tab) zależą od tych technik badawczych, ale również stanowią one podstawę (Infer(opens in a new tab), CodeClimate(opens in a new tab), FindBugs(opens in a new tab) i narzędzi opartych na formalnych metodach, takich jak Frama-C(opens in a new tab) i Polyspace(opens in a new tab).

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
  • Analiza kodu
  • Reprezentacja pośrednia

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}
Kopiuj

Odpowiedni AST jest pokazany w:

AST

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(opens in a new tab).

Następujący kod używa ExpressionVisitor aby wykryć, czy wyrażenie zawiera dodatek:

1from slither.visitors.expression.expression import ExpressionVisitor
2from slither.core.expressions.binary_operation import BinaryOperationType
3
4class HasAddition(ExpressionVisitor):
5
6 def result(self):
7 return self._result
8
9 def _post_binary_operation(self, expression):
10 if expression.type == BinaryOperationType.ADDITION:
11 self._result = True
12
13visitor = HasAddition(expression) # expression is the expression to be tested
14print(f'The expression {expression} has a addition: {visitor.result()}')
Pokaż wszystko
Kopiuj

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

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ą:

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;
Kopiuj

Slither posiada wbudowane funkcje zależności danych(opens in a new tab) 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(opens in a new tab). Tutaj Slither będzie szukał ścisłego porównania równości z niebezpieczną wartością (wronct_strict_equality. y#L86-L87(opens in a new tab)), 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(opens in a new tab)) 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 += 1
3}
Kopiuj

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(opens in a new tab)), zatrzymuje eksplorację i analizuje wyniki, aby sprawdzić, czy występuje wielobieżność, sprawdzając różne jej wzorce (reentrancy_benign. y(opens in a new tab), reentrancy_read_before_write.py(opens in a new tab), reentrancy_eth.py(opens in a new tab)).

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(opens in a new tab).

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(opens in a new tab) i SSA(opens in a new tab) 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 Slither
2slither = Slither('/path/to/project')
3
Kopiuj

Odkrywanie kontraktów i funkcji

Obiekt Slither zawiera:

  • contracts (list(Contract): lista kontraktów
  • contracts_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 kontraktu
  • functions (list(Function)): lista funkcji
  • modifiers (list(Modifier)): lista funkcji
  • all_functions_lated (list(Function/Modifier)): lista wszystkich funkcji wewnętrznych osiągalnych przez kontrakt
  • inheritance (list(Contract)): lista dziedziczonych kontraktów
  • get_function_from_signature (str): zwraca funkcję z jej podpisu
  • get_modifier_from_signature (str): zwraca modyfikator z jego podpisu
  • get_state_variable_from_name (str): zwraca zmienną stanową z jej nazwy

Obiekt Function lub Modifier ma:

  • name (str): nazwa funkcji
  • contract (contract): kontrakt, w którym zadeklarowana jest funkcja
  • nodes (list(Node)): lista węzłów tworzących CFG funkcji/modyfikatora
  • entry_point (Node): punkt wejścia CFG
  • variables_read (list(Variable)): lista odczytanych zmiennych
  • variables_written (list(Variable)): lista zapisanych zmiennych
  • state_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)

Ostatnia edycja: @nhsz(opens in a new tab), 15 sierpnia 2023

Czy ten samouczek był pomocny?