Preskočiť na hlavný obsah

How to use Slither to find smart contract bugs

soliditysmart contractssecuritytestingstatic analysis
Advanced
Trailofbits
Building secure contracts(opens in a new tab)
9. júna 2020
7 minute read minute read

How to use Slither

The aim of this tutorial is to show how to use Slither to automatically find bugs in smart contracts.

Installation

Slither requires Python >= 3.6. It can be installed through pip or using docker.

Slither through pip:

pip3 install --user slither-analyzer

Slither through docker:

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

The last command runs eth-security-toolbox in a docker that has access to your current directory. You can change the files from your host, and run the tools on the files from the docker

Inside docker, run:

solc-select 0.5.11
cd /home/trufflecon/

Running a script

To run a python script with python 3:

python3 script.py

Command line

Command line versus user-defined scripts. Slither comes with a set of predefined detectors that find many common bugs. Calling Slither from the command line will run all the detectors, no detailed knowledge of static analysis needed:

slither project_paths

In addition to detectors, Slither has code review capabilities through its printers(opens in a new tab) and tools(opens in a new tab).

Use crytic.io(opens in a new tab) to get access to private detectors and GitHub integration.

Static analysis

The capabilities and design of the Slither static analysis framework has been described in blog posts (1(opens in a new tab), 2(opens in a new tab)) and an academic paper(opens in a new tab).

Static analysis exists in different flavors. You most likely realize that compilers like clang(opens in a new tab) and gcc(opens in a new tab) depend on these research techniques, but it also underpins (Infer(opens in a new tab), CodeClimate(opens in a new tab), FindBugs(opens in a new tab) and tools based on formal methods like Frama-C(opens in a new tab) and Polyspace(opens in a new tab).

We won't be exhaustively reviewing static analysis techniques and researcher here. Instead, we'll focus on what is needed to understand how Slither works so you can more effectively use it to find bugs and understand code.

Code representation

In contrast to a dynamic analysis, which reasons about a single execution path, static analysis reasons about all the paths at once. To do so, it relies on a different code representation. The two most common ones are the abstract syntax tree (AST) and the control flow graph (CFG).

Abstract Syntax Trees (AST)

AST are used every time the compiler parses code. It is probably the most basic structure upon which static analysis can be performed.

In a nutshell, an AST is a structured tree where, usually, each leaf contains a variable or a constant and internal nodes are operands or control flow operations. Consider the following code:

1function safeAdd(uint a, uint b) pure internal returns(uint){
2 if(a + b <= a){
3 revert();
4 }
5 return a + b;
6}
Kopírovať

The corresponding AST is shown in:

AST

Slither uses the AST exported by solc.

While simple to build, the AST is a nested structure. At times, this is not the most straightforward to analyze. For example, to identify the operations used by the expression a + b <= a, you must first analyze <= and then +. A common approach is to use the so-called visitor pattern, which navigates through the tree recursively. Slither contains a generic visitor in ExpressionVisitor(opens in a new tab).

The following code uses ExpressionVisitor to detect if the expression contains an addition:

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()}')
Zobraziť všetko
Kopírovať

Control Flow Graph (CFG)

The second most common code representation is the control flow graph (CFG). As its name suggests, it is a graph-based representation which exposes all the execution paths. Each node contains one or multiple instructions. Edges in the graph represent the control flow operations (if/then/else, loop, etc). The CFG of our previous example is:

CFG

The CFG is the representation on top of which most of the analyses are built.

Many other code representations exist. Each representation has advantages and drawbacks according to the analysis you want to perform.

Analysis

The simplest type of analyses you can perform with Slither are syntactic analyses.

Syntax analysis

Slither can navigate through the different components of the code and their representation to find inconsistencies and flaws using a pattern matching-like approach.

For example the following detectors look for syntax-related issues:

Semantic analysis

In contrast to syntax analysis, a semantic analysis will go deeper and analyze the “meaning” of the code. This family includes some broad types of analyses. They lead to more powerful and useful results, but are also more complex to write.

Semantic analyses are used for the most advanced vulnerability detections.

Data dependency analysis

A variable variable_a is said to be data-dependent of variable_b if there is a path for which the value of variable_a is influenced by variable_b.

In the following code, variable_a is dependent of variable_b:

1// ...
2variable_a = variable_b + 1;
Kopírovať

Slither comes with built-in data dependency(opens in a new tab) capabilities, thanks to its intermediate representation (discussed in a later section).

An example of data dependency usage can be found in the dangerous strict equality detector(opens in a new tab). Here Slither will look for strict equality comparison to a dangerous value (incorrect_strict_equality.py#L86-L87(opens in a new tab)), and will inform the user that it should use >= or <= rather than ==, to prevent an attacker to trap the contract. Among other, the detector will consider as dangerous the return value of a call to balanceOf(address) (incorrect_strict_equality.py#L63-L64(opens in a new tab)), and will use the data dependency engine to track its usage.

Fixed-point computation

If your analysis navigates through the CFG and follows the edges, you are likely to see already visited nodes. For example, if a loop is presented as shown below:

1for(uint i; i < range; ++){
2 variable_a += 1
3}
Kopírovať

Your analysis will need to know when to stop. There are two main strategies here: (1) iterate on each node a finite number of times, (2) compute a so-called fixpoint. A fixpoint basically means that analyzing this node does not provide any meaningful information.

An example of fixpoint used can be found in the reentrancy detectors: Slither explores the nodes, and look for externals calls, write and read to storage. Once it has reached a fixpoint (reentrancy.py#L125-L131(opens in a new tab)), it stops the exploration, and analyze the results to see if a reentrancy is present, through different reentrancy patterns (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)).

Writing analyses using efficient fixed point computation requires a good understanding of how the analysis propagates its information.

Intermediate representation

An intermediate representation (IR) is a language meant to be more amenable to static analysis than the original one. Slither translates Solidity to its own IR: SlithIR(opens in a new tab).

Understanding SlithIR is not necessary if you only want to write basic checks. However, it will come in handy if you plan to write advanced semantic analyses. The SlithIR(opens in a new tab) and SSA(opens in a new tab) printers will help you to understand how the code is translated.

API Basics

Slither has an API that lets you explore basic attributes of the contract and its functions.

To load a codebase:

1from slither import Slither
2slither = Slither('/path/to/project')
3
Kopírovať

Exploring contracts and functions

A Slither object has:

  • contracts (list(Contract): list of contracts
  • contracts_derived (list(Contract): list of contracts that are not inherited by another contract (subset of contracts)
  • get_contract_from_name (str): Return a contract from its name

A Contract object has:

  • name (str): Name of the contract
  • functions (list(Function)): List of functions
  • modifiers (list(Modifier)): List of functions
  • all_functions_called (list(Function/Modifier)): List of all the internal functions reachable by the contract
  • inheritance (list(Contract)): List of inherited contracts
  • get_function_from_signature (str): Return a Function from its signature
  • get_modifier_from_signature (str): Return a Modifier from its signature
  • get_state_variable_from_name (str): Return a StateVariable from its name

A Function or a Modifier object has:

  • name (str): Name of the function
  • contract (contract): the contract where the function is declared
  • nodes (list(Node)): List of the nodes composing the CFG of the function/modifier
  • entry_point (Node): Entry point of the CFG
  • variables_read (list(Variable)): List of variables read
  • variables_written (list(Variable)): List of variables written
  • state_variables_read (list(StateVariable)): List of state variables read (subset of variables`read)
  • state_variables_written (list(StateVariable)): List of state variables written (subset of variables`written)

Naposledy editované: @nhsz(opens in a new tab), 15. augusta 2023

Bol tento návod užitočný?