Перейти к основному контенту

Как использовать Слизер для поиска ошибок в смарт-контрактах

Solidity
смарт-контракты
безопасность
тестирование
Продвинутый уровень
Trailofbits
9 июня 2020 г.
7 минут на чтение

Как использовать Слизер

Цель этого руководства — показать, как использовать Слизер для автоматического поиска ошибок в смарт-контрактах.

Установка

Слизер требует Python версии 3.6 или выше. Его можно установить через pip или с помощью Docker.

Установка Слизер через pip:

pip3 install --user slither-analyzer

Установка Слизер через Docker:

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

Последняя команда запускает eth-security-toolbox в Docker, который имеет доступ к вашей текущей директории. Вы можете изменять файлы на вашем хосте и запускать инструменты для этих файлов из Docker.

Внутри Docker выполните:

solc-select 0.5.11
cd /home/trufflecon/

Запуск скрипта

Чтобы запустить скрипт Python с помощью Python 3:

python3 script.py

Командная строка

Командная строка в сравнении с пользовательскими скриптами. Слизер поставляется с набором предустановленных детекторов, которые находят множество распространенных ошибок. Вызов Слизер из командной строки запустит все детекторы, для этого не требуется глубоких знаний статического анализа:

slither project_paths

Помимо детекторов, Слизер имеет возможности для проверки кода с помощью своих принтеров (printers) (opens in a new tab) и инструментов (opens in a new tab).

Используйте crytic.io (opens in a new tab), чтобы получить доступ к приватным детекторам и интеграции с GitHub.

Статический анализ

Возможности и архитектура фреймворка статического анализа Слизер были описаны в статьях блога (1 (opens in a new tab), 2 (opens in a new tab)) и научной работе (opens in a new tab).

Статический анализ существует в различных вариантах. Вы, скорее всего, понимаете, что компиляторы, такие как clang (opens in a new tab) и gcc (opens in a new tab), зависят от этих методов исследования, но они также лежат в основе Infer (opens in a new tab), CodeClimate (opens in a new tab), FindBugs (opens in a new tab) и инструментов, основанных на формальных методах, таких как Frama-C (opens in a new tab) и Polyspace (opens in a new tab).

Здесь мы не будем исчерпывающе рассматривать методы статического анализа и исследования. Вместо этого мы сосредоточимся на том, что необходимо для понимания работы Слизер, чтобы вы могли более эффективно использовать его для поиска ошибок и понимания кода.

Представление кода

В отличие от динамического анализа, который рассматривает один путь выполнения, статический анализ рассматривает все пути одновременно. Для этого он опирается на другое представление кода. Двумя наиболее распространенными являются абстрактное синтаксическое дерево (AST) и граф потока управления (CFG).

Абстрактные синтаксические деревья (AST)

AST используются каждый раз, когда компилятор анализирует код. Вероятно, это самая базовая структура, на основе которой может выполняться статический анализ.

Вкратце, AST — это структурированное дерево, где обычно каждый лист содержит переменную или константу, а внутренние узлы являются операндами или операциями управления потоком. Рассмотрим следующий код:

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

Соответствующее AST показано на рисунке:

AST

Слизер использует AST, экспортируемое solc.

Несмотря на простоту построения, AST представляет собой вложенную структуру. Иногда это не самый простой вариант для анализа. Например, чтобы определить операции, используемые выражением a + b <= a, вы должны сначала проанализировать <=, а затем +. Распространенным подходом является использование так называемого паттерна «Посетитель» (visitor), который рекурсивно перемещается по дереву. Слизер содержит общий паттерн посетителя в ExpressionVisitor (opens in a new tab).

Следующий код использует ExpressionVisitor для определения того, содержит ли выражение сложение:

Граф потока управления (CFG)

Вторым по распространенности представлением кода является граф потока управления (CFG). Как следует из названия, это представление на основе графа, которое раскрывает все пути выполнения. Каждый узел содержит одну или несколько инструкций. Ребра в графе представляют операции управления потоком (if/then/else, циклы и т. д.). CFG нашего предыдущего примера выглядит так:

CFG

CFG — это представление, на основе которого строится большинство анализов.

Существует множество других представлений кода. Каждое представление имеет свои преимущества и недостатки в зависимости от анализа, который вы хотите выполнить.

Анализ

Самый простой тип анализа, который вы можете выполнить с помощью Слизер, — это синтаксический анализ.

Синтаксический анализ

Слизер может перемещаться по различным компонентам кода и их представлению, чтобы находить несоответствия и недостатки, используя подход, похожий на сопоставление с шаблоном.

Например, следующие детекторы ищут проблемы, связанные с синтаксисом:

Семантический анализ

В отличие от синтаксического анализа, семантический анализ идет глубже и анализирует «смысл» кода. Это семейство включает в себя несколько широких типов анализов. Они приводят к более мощным и полезным результатам, но их также сложнее писать.

Семантический анализ используется для наиболее продвинутого обнаружения уязвимостей.

Анализ зависимости данных

Переменная variable_a считается зависимой от данных variable_b, если существует путь, на котором значение variable_a зависит от variable_b.

В следующем коде variable_a зависит от variable_b:

// ...
variable_a = variable_b + 1;

Слизер поставляется со встроенными возможностями зависимости данных (opens in a new tab) благодаря своему промежуточному представлению (обсуждается в следующем разделе).

Пример использования зависимости данных можно найти в детекторе опасного строгого равенства (opens in a new tab). Здесь Слизер будет искать сравнение на строгое равенство с опасным значением (incorrect_strict_equality.py#L86-L87 (opens in a new tab)) и сообщит пользователю, что следует использовать >= или <= вместо ==, чтобы не дать злоумышленнику заблокировать контракт. Помимо прочего, детектор будет считать опасным возвращаемое значение вызова balanceOf(address) (incorrect_strict_equality.py#L63-L64 (opens in a new tab)) и будет использовать механизм зависимости данных для отслеживания его использования.

Вычисление фиксированной точки

Если ваш анализ перемещается по CFG и следует по ребрам, вы, вероятно, увидите уже посещенные узлы. Например, если цикл представлен так, как показано ниже:

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

Вашему анализу нужно будет знать, когда остановиться. Здесь есть две основные стратегии: (1) итерация по каждому узлу конечное число раз, (2) вычисление так называемой фиксированной точки (fixpoint). Фиксированная точка в основном означает, что анализ этого узла не дает никакой значимой информации.

Пример использования фиксированной точки можно найти в детекторах повторного входа: Слизер исследует узлы и ищет внешние вызовы, запись и чтение в хранилище. Как только он достигает фиксированной точки (reentrancy.py#L125-L131 (opens in a new tab)), он останавливает исследование и анализирует результаты, чтобы увидеть, присутствует ли повторный вход, с помощью различных шаблонов повторного входа (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)).

Написание анализов с использованием эффективного вычисления фиксированной точки требует хорошего понимания того, как анализ распространяет свою информацию.

Промежуточное представление

Промежуточное представление (IR) — это язык, который должен быть более податливым для статического анализа, чем исходный. Слизер переводит Solidity в свое собственное IR: SlithIR (opens in a new tab).

Понимание SlithIR не обязательно, если вы хотите писать только базовые проверки. Однако это пригодится, если вы планируете писать продвинутые семантические анализы. Принтеры SlithIR (opens in a new tab) и SSA (opens in a new tab) помогут вам понять, как переводится код.

Основы API

Слизер имеет API, который позволяет вам исследовать базовые атрибуты контракта и его функций.

Чтобы загрузить кодовую базу:

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

Исследование контрактов и функций

Объект Slither имеет:

  • contracts (list(Contract): список контрактов
  • contracts_derived (list(Contract): список контрактов, которые не унаследованы другим контрактом (подмножество контрактов)
  • get_contract_from_name (str): возвращает контракт по его имени

Объект Contract имеет:

  • name (str): имя контракта
  • functions (list(Function)): список функций
  • modifiers (list(Modifier)): список функций
  • all_functions_called (list(Function/Modifier)): список всех внутренних функций, доступных контракту
  • inheritance (list(Contract)): список унаследованных контрактов
  • get_function_from_signature (str): возвращает функцию по ее подписи
  • get_modifier_from_signature (str): возвращает модификатор по его подписи
  • get_state_variable_from_name (str): возвращает переменную состояния по ее имени

Объект Function или Modifier имеет:

  • name (str): имя функции
  • contract (contract): контракт, в котором объявлена функция
  • nodes (list(Node)): список узлов, составляющих CFG функции/модификатора
  • entry_point (Node): точка входа CFG
  • variables_read (list(Variable)): список прочитанных переменных
  • variables_written (list(Variable)): список записанных переменных
  • state_variables_read (list(StateVariable)): список прочитанных переменных состояния (подмножество variables`read)
  • state_variables_written (list(StateVariable)): список записанных переменных состояния (подмножество variables`written)