Przejdź do głównej treści

Jak używać narzędzia Slither do znajdowania błędów w inteligentnych kontraktach

Solidity
inteligentne kontrakty
bezpieczeństwo
testowanie
Zaawansowany
Trailofbits
9 czerwca 2020
7 minut czytania

Jak używać narzędzia Slither

Celem tego samouczka jest pokazanie, jak używać narzędzia Slither do automatycznego znajdowania błędów w inteligentnych kontraktach.

Instalacja

Slither wymaga języka Python w wersji >= 3.6. Można go zainstalować za pomocą pip lub używając narzędzia Docker.

Instalacja Slither przez pip:

pip3 install --user slither-analyzer

Instalacja Slither przez Docker:

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox

Ostatnie polecenie uruchamia eth-security-toolbox w kontenerze Docker, który ma dostęp do Twojego bieżącego katalogu. Możesz zmieniać pliki na swoim hoście i uruchamiać narzędzia na plikach z poziomu Dockera.

Wewnątrz Dockera uruchom:

solc-select 0.5.11
cd /home/trufflecon/

Uruchamianie skryptu

Aby uruchomić skrypt w języku Python za pomocą Python 3:

python3 script.py

Wiersz poleceń

Wiersz poleceń a skrypty zdefiniowane przez użytkownika. Slither jest dostarczany z zestawem predefiniowanych detektorów, które znajdują wiele typowych błędów. Wywołanie narzędzia Slither z wiersza poleceń uruchomi wszystkie detektory, bez konieczności posiadania szczegółowej wiedzy na temat analizy statycznej:

slither project_paths

Oprócz detektorów, Slither posiada możliwości przeglądu kodu dzięki swoim modułom wypisywania (printers) (opens in a new tab) i narzędziom (opens in a new tab).

Użyj crytic.io (opens in a new tab), aby uzyskać dostęp do prywatnych detektorów i integracji z GitHub.

Analiza statyczna

Możliwości i projekt frameworka do analizy statycznej Slither zostały opisane we wpisach na blogu (1 (opens in a new tab), 2 (opens in a new tab)) oraz w artykule naukowym (opens in a new tab).

Analiza statyczna występuje w różnych odmianach. Prawdopodobnie zdajesz sobie sprawę, że kompilatory takie jak clang (opens in a new tab) i gcc (opens in a new tab) opierają się na tych technikach badawczych, ale stanowią one również podstawę dla narzędzi takich jak Infer (opens in a new tab), CodeClimate (opens in a new tab), FindBugs (opens in a new tab) oraz narzędzi opartych na metodach formalnych, takich jak Frama-C (opens in a new tab) i Polyspace (opens in a new tab).

Nie będziemy tutaj wyczerpująco omawiać technik analizy statycznej ani badań. Zamiast tego skupimy się na tym, co jest potrzebne do zrozumienia, jak działa Slither, abyś mógł skuteczniej używać go do znajdowania błędów i rozumienia kodu.

Reprezentacja kodu

W przeciwieństwie do analizy dynamicznej, która wnioskuje o pojedynczej ścieżce wykonania, analiza statyczna wnioskuje o wszystkich ścieżkach jednocześnie. Aby to zrobić, opiera się na innej reprezentacji kodu. Dwie najpopularniejsze to drzewo składni abstrakcyjnej (AST - Abstract Syntax Tree) i graf przepływu sterowania (CFG - Control Flow Graph).

Drzewa składni abstrakcyjnej (AST)

AST są używane za każdym razem, gdy kompilator parsuje kod. Jest to prawdopodobnie najbardziej podstawowa struktura, na której można przeprowadzić analizę statyczną.

W skrócie, AST to ustrukturyzowane drzewo, w którym zazwyczaj każdy liść zawiera zmienną lub stałą, a węzły wewnętrzne to operandy lub operacje przepływu sterowania. Rozważmy następujący kod:

function safeAdd(uint a, uint b) pure internal returns(uint){
    if(a + b <= a){
        revert();
    }
    return a + b;
}

Odpowiadające mu AST pokazano poniżej:

AST

Slither używa AST wyeksportowanego przez solc.

Choć proste w budowie, AST jest strukturą zagnieżdżoną. Czasami nie jest to najprostsze do analizy. Na przykład, aby zidentyfikować operacje użyte w wyrażeniu a + b <= a, musisz najpierw przeanalizować <=, a następnie +. Powszechnym podejściem jest użycie tak zwanego wzorca odwiedzającego (visitor pattern), który rekurencyjnie porusza się po drzewie. Slither zawiera ogólnego odwiedzającego w ExpressionVisitor (opens in a new tab).

Poniższy kod używa ExpressionVisitor do wykrycia, czy wyrażenie zawiera dodawanie:

Graf przepływu sterowania (CFG)

Drugą najpopularniejszą reprezentacją kodu jest graf przepływu sterowania (CFG). Jak sama nazwa wskazuje, jest to reprezentacja oparta na grafie, która ujawnia wszystkie ścieżki wykonania. Każdy węzeł zawiera jedną lub wiele instrukcji. Krawędzie w grafie reprezentują operacje przepływu sterowania (if/then/else, pętle itp.). CFG naszego poprzedniego przykładu to:

CFG

CFG to reprezentacja, na której opiera się większość analiz.

Istnieje wiele innych reprezentacji kodu. Każda z nich ma swoje zalety i wady w zależności od analizy, którą chcesz przeprowadzić.

Analiza

Najprostszym rodzajem analiz, jakie można przeprowadzić za pomocą narzędzia Slither, są analizy składniowe.

Analiza składniowa

Slither może poruszać się po różnych komponentach kodu i ich reprezentacji, aby znaleźć niespójności i wady, używając podejścia podobnego do dopasowywania wzorców.

Na przykład poniższe detektory szukają problemów związanych ze składnią:

Analiza semantyczna

W przeciwieństwie do analizy składniowej, analiza semantyczna sięga głębiej i analizuje „znaczenie” kodu. Ta rodzina obejmuje kilka szerokich typów analiz. Prowadzą one do potężniejszych i bardziej użytecznych wyników, ale są również bardziej złożone w pisaniu.

Analizy semantyczne są używane do najbardziej zaawansowanego wykrywania podatności.

Analiza zależności danych

Mówi się, że zmienna variable_a jest zależna od danych ze zmiennej variable_b, jeśli istnieje ścieżka, dla której na wartość variable_a wpływa variable_b.

W poniższym kodzie variable_a jest zależna od variable_b:

// ...
variable_a = variable_b + 1;

Slither posiada wbudowane możliwości analizy zależności danych (opens in a new tab), dzięki swojej reprezentacji pośredniej (omówionej w dalszej części).

Przykład użycia zależności danych można znaleźć w detektorze niebezpiecznej ścisłej równości (opens in a new tab). W tym przypadku Slither będzie szukał porównania ścisłej równości z niebezpieczną wartością (incorrect_strict_equality.py#L86-L87 (opens in a new tab)) i poinformuje użytkownika, że powinien użyć >= lub <= zamiast ==, aby zapobiec uwięzieniu kontraktu przez atakującego. Między innymi detektor uzna za niebezpieczną wartość zwracaną przez wywołanie balanceOf(address) (incorrect_strict_equality.py#L63-L64 (opens in a new tab)) i użyje silnika zależności danych do śledzenia jej użycia.

Obliczanie punktu stałego

Jeśli Twoja analiza porusza się po CFG i podąża za krawędziami, prawdopodobnie napotkasz już odwiedzone węzły. Na przykład, jeśli pętla jest przedstawiona jak poniżej:

for(uint i; i < range; ++){
    variable_a += 1
}

Twoja analiza będzie musiała wiedzieć, kiedy się zatrzymać. Istnieją tutaj dwie główne strategie: (1) iteracja po każdym węźle skończoną liczbę razy, (2) obliczenie tak zwanego punktu stałego (fixpoint). Punkt stały w zasadzie oznacza, że analiza tego węzła nie dostarcza już żadnych istotnych informacji.

Przykład użycia punktu stałego można znaleźć w detektorach reentrancji: Slither bada węzły i szuka wywołań zewnętrznych, zapisów i odczytów z pamięci (storage). Gdy osiągnie punkt stały (reentrancy.py#L125-L131 (opens in a new tab)), zatrzymuje eksplorację i analizuje wyniki, aby sprawdzić, czy występuje reentrancja, poprzez różne wzorce reentrancji (reentrancy_benign.py (opens in a new tab), reentrancy_read_before_write.py (opens in a new tab), reentrancy_eth.py (opens in a new tab)).

Pisanie analiz wykorzystujących wydajne obliczanie punktu stałego wymaga dobrego zrozumienia, w jaki sposób analiza propaguje swoje informacje.

Reprezentacja pośrednia

Reprezentacja pośrednia (IR - Intermediate Representation) to język, który ma być bardziej podatny na analizę statyczną niż oryginał. Slither tłumaczy język Solidity na własne IR: SlithIR (opens in a new tab).

Zrozumienie SlithIR nie jest konieczne, jeśli chcesz pisać tylko podstawowe testy. Przyda się jednak, jeśli planujesz pisać zaawansowane analizy semantyczne. Moduły wypisywania SlithIR (opens in a new tab) i SSA (opens in a new tab) pomogą Ci zrozumieć, jak tłumaczony jest kod.

Podstawy API

Slither posiada API, które pozwala na eksplorację podstawowych atrybutów kontraktu i jego funkcji.

Aby załadować bazę kodu:

from slither import Slither
slither = Slither('/path/to/project')

Eksploracja kontraktów i funkcji

Obiekt Slither posiada:

  • 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 na podstawie jego nazwy

Obiekt Contract posiada:

  • name (str): Nazwa kontraktu
  • functions (list(Function)): Lista funkcji
  • modifiers (list(Modifier)): Lista funkcji
  • all_functions_called (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ę (Function) na podstawie jej sygnatury
  • get_modifier_from_signature (str): Zwraca modyfikator (Modifier) na podstawie jego sygnatury
  • get_state_variable_from_name (str): Zwraca zmienną stanu (StateVariable) na podstawie jej nazwy

Obiekt Function lub Modifier posiada:

  • 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 odczytywanych zmiennych
  • variables_written (list(Variable)): Lista zapisywanych zmiennych
  • state_variables_read (list(StateVariable)): Lista odczytywanych zmiennych stanu (podzbiór variables`read)
  • state_variables_written (list(StateVariable)): Lista zapisywanych zmiennych stanu (podzbiór variables`written)