Ir al contenido principal

Cómo usar Slither para encontrar errores en contratos inteligentes

soliditycontratos inteligentesseguridadpruebasanálisis estático
Recursos avanzados
Trailofbits
Desarrollar contratos seguros(opens in a new tab)
9 de junio de 2020
8 minuto leído minute read

Cómo usar Slither

El objetivo de este tutorial es mostrar cómo usar Slither para encontrar errores de manera automática en los contratos inteligentes.

  • Instalación
  • Uso de línea de comandos
  • Introducción al análisis estático: breve introducción al análisis estático
  • API: descripción de la API de Python

Instalación

Slither usa Python >= 3.6. Puede ser instalado a traves de pip o usando docker.

Slither a través de pip:

pip3 install --user slither-analyzer

Slither a través de docker:

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

El comando de arriba ejecuta eth-security-toolbox en un docker que tiene acceso a su directorio actual. Puede cambiar los archivos desde su host y correr las herramientas dentro de los archivos desde el docker.

Dentro del docker, ejecute:

solc-select 0.5.11
cd /home/trufflecon/

Ejecución de un script

Para ejecutar un script de python con python 3:

python3 script.py

Línea de comandos

Línea de comandos vs. scripts definidos por el usuario. Slither viene con un conjunto de detectores predefinidos que encuentran muchos errores comunes. Al llamar a Slither desde la línea de comandos, se ejecutarán todos los detectores sin necesidad de tener conocimientos detallados de análisis estático:

slither project_paths

Además de los detectores, Slither tiene capacidades de revisión de código por medio de impresoras(opens in a new tab) y herramientas(opens in a new tab).

Use crytic.io(opens in a new tab) para acceder a los detectores privados y a la integración con GitHub.

Análisis estático

Las capacidades y el diseño del framework de análisis estático de Slither han sido descritos en entradas de blog (1(opens in a new tab), 2(opens in a new tab)) y en un documento académico(opens in a new tab).

El análisis estático existe en distintas formas. Lo más probable es que se dé cuenta de que compiladores como clang(opens in a new tab) y gcc(opens in a new tab) dependen de estas técnicas de investigación, pero también sustenta (Infer(opens in a new tab), CodeClimate(opens in a new tab), FindBugs(opens in a new tab) y herramientas basadas en métodos formales como Frama-C(opens in a new tab) y Polyspace(opens in a new tab).

No vamos a repasar aquí en detalle las técnicas de análisis estático y el investigador. En cambio, nos centraremos en lo necesario para entender cómo funciona Slither y así poder utilizarlo de forma más eficaz para encontrar errores y entender el código.

  • Representación del código
  • Análisis de código
  • Representación intermedia

Representación del código

A diferencia del análisis dinámico, que se basa en una única ruta de ejecución, el análisis estático se basa en todas las rutas a la vez. Para ello, se basa en una representación de código diferente. Los dos más comunes son el árbol de sintaxis abstracta (AST) y el gráfico de flujo de control (CFG).

Árboles de sintaxis abstracta (AST)

Los AST se utilizan cada vez que el compilador analiza el código. Es probablemente la estructura más básica sobre la que se puede realizar un análisis estático.

En pocas palabras, un AST es un árbol estructurado en el que, normalmente, cada hoja contiene una variable o una constante, y los nodos internos son operadores u operaciones de flujo de control. Considere el siguiente código:

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

El AST correspondiente se muestra en:

AST

Slither utiliza el AST exportado por solc.

Si bien es sencillo de construir, el AST es una estructura anidada. A veces, esto no es lo más sencillo de analizar. Por ejemplo, para identificar las operaciones usadas por la expresión a + b <= a, primero debe analizarla <= y luego +. Un enfoque común es utilizar el llamado patrón de visitantes, que navega por el árbol recursivamente. Slither contiene un visitante genérico en ExpressionVisitor(opens in a new tab).

El siguiente código utiliza ExpressionVisitor para detectar si la expresión contiene una adición:

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()}')
Mostrar todo
Copiar

Gráfico de flujo de control (CFG)

La segunda representación de código más común es el gráfico de flujo de control (CFG). Como su nombre indica, es una representación basada en un gráfico que expone todas las rutas de ejecución. Cada nodo contiene una o varias instrucciones. Los bordes en el gráfico representan las operaciones de flujo de control (if/then/else, loop, etc). El CFG de nuestro ejemplo anterior es:

CFG

El CFG es la representación sobre la que se construye la mayoría de los análisis.

Existen muchas otras representaciones de código. Cada representación tiene ventajas y desventajas según el análisis que se quiera realizar.

Análisis

El tipo de análisis más sencillo que se puede realizar con Slither es el análisis sintáctico.

Análisis de sintaxis

Slither puede explorar los diferentes componentes del código y su representación para encontrar inconsistencias y defectos usando un enfoque del tipo coincidencia de patrones.

Por ejemplo, los siguientes detectores buscan problemas relacionados con la sintaxis:

Análisis semántico

En contraste con el análisis de sintaxis, un análisis semántico profundizará y analizará el “significado” del código. Esta familia incluye algunos amplios tipos de análisis. Conducen a resultados más potentes y útiles, pero también son más complejos de escribir.

Los análisis semánticos se usan para las detecciones de vulnerabilidad más avanzadas.

Análisis de dependencias de datos

Se dice que una variable variable_a tiene una dependencia de datos de variable_b si hay una ruta para la cual el valor de variable_a está influenciado por variable_b.

En el siguiente código, variable_a depende de variable_b:

1// ...
2variable_a = variable_b + 1;
Copiar

Slither viene con capacidades de dependencia de datos(opens in a new tab), gracias a su representación intermedia (se verá en una sección posterior).

Se puede encontrar un ejemplo de uso de dependencia de datos en detector de equidad estricta peligrosa(opens in a new tab). Aquí Slither buscará una comparación estricta de equidad con un valor peligroso (incorrec_strict_equality. y#L86-L87(opens in a new tab)) e informará al usuario que debe usar >= o <= en lugar de == para impedir que un atacante atrape el contrato. Entre otros, el detector considerará peligroso el valor de retorno de una llamada a balanceOf(address) (incorrect_strict_equality.py#L63-L64(opens in a new tab)) y usará el motor de dependencias de datos para rastrear su uso.

Cómputo de punto fijo

Si su análisis explora el CFG y sigue las aristas, o bordes, es probable que vea nodos ya visitados. Por ejemplo, si un bucle se presenta como se muestra a continuación:

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

Su análisis necesitará saber cuándo detenerse. Hay dos estrategias principales aquí: (1) iterar en cada nodo un número finito de veces, (2) calcular un punto de fijación. Un punto fijo, o fixpoint, básicamente significa que el análisis de este nodo no proporciona ninguna información significativa.

Un ejemplo de punto fijo puede estar en los detectores de reentrada: Slither explora los nodos y busca llamadas externas, escribir y leer para almacenar. Una vez que haya alcanzado un punto fijo (reentrancy.py#L125-L131(opens in a new tab)), detiene la exploración y analiza los resultados para ver si una reentrada está presente a través de diferentes patrones de reentrada (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)).

Escribir análisis utilizando un cálculo de punto fijo eficiente requiere una buena comprensión de cómo el análisis propaga su información.

Representación intermedia

Una representación intermedia (IR) es un lenguaje que pretende ser más susceptible al análisis estático que el original. Slither traduce Solidity a su propio IR: SlithIR(opens in a new tab).

Entender SlithIR no es necesario si solo desea escribir comprobaciones básicas. Sin embargo, será útil si tiene pensado escribir análisis semánticos avanzados. Los resultados de SlithIR(opens in a new tab) y SSA(opens in a new tab) le ayudarán a entender cómo se traduce el código.

Aspectos básicos de la API

Slither tiene una API que le permite explorar los atributos básicos del contrato y sus funciones.

Para cargar una base de código:

1from slither import Slither
2slither = Slither('/path/to/project')
3
Copiar

Exploración de contratos y funciones

Un objeto de Slither tiene:

  • contracts (list(Contract): lista de contratos
  • contracts_derived (list(Contract): lista de contratos que no son heredados por otro contrato (subconjunto de contratos)
  • get_contract_from_name (str): retorna un contrato a partir de su nombre

Un objeto Contract tiene:

  • name (str): nombre del contrato
  • functions (list(Function)): lista de funciones
  • modifiers (list(Modifier)): lista de funciones
  • all_functions_called (list(Function/Modifier)): lista de todas las funciones internas accesibles para el contrato
  • inheritance (list(Contract)): lista de los contratos heredados
  • get_function_from_signature (str): retorna una función desde su firma
  • get_modifier_from_signature (str): retorna un modificador desde su firma
  • get_state_variable_from_name (str): retorna una StateVariable desde su nombre

Una Function o un objeto Modifier tiene:

  • name (str): nombre de la función
  • contract (contract): el contrato donde se declara la función
  • nodes (list(Node)): lista de los nodos que componen el CFG de la función/modificador
  • entry_point (Node): punto de entrada del CFG
  • variables_read (list(Variable)): lista de variables a leer
  • variables_written (list(Variable)): lista de variables escritas
  • state_variables_read (list(StateVariable)): lista de variables de estado a leer (subconjunto de variables a leer)
  • state_variables_written (list(StateVariable)): lista de variables de estado escritas (subconjunto de variables escritas)

Última edición: @nhsz(opens in a new tab), 15 de agosto de 2023

¿Le ha resultado útil este tutorial?