Как использовать Слизер для поиска ошибок в смарт-контрактах
Как использовать Слизер
Цель этого руководства — показать, как использовать Слизер для автоматического поиска ошибок в смарт-контрактах.
- Установка
- Использование командной строки
- Введение в статический анализ: Краткое введение в статический анализ
- API: Описание API Python
Установка
Слизер требует 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, экспортируемое solc.
Несмотря на простоту построения, AST представляет собой вложенную структуру. Иногда это не самый простой вариант для анализа. Например, чтобы определить операции, используемые выражением a + b <= a, вы должны сначала проанализировать <=, а затем +. Распространенным подходом является использование так называемого паттерна «Посетитель» (visitor), который рекурсивно перемещается по дереву. Слизер содержит общий паттерн посетителя в ExpressionVisitor (opens in a new tab).
Следующий код использует ExpressionVisitor для определения того, содержит ли выражение сложение:
from slither.visitors.expression.expression import ExpressionVisitor
from slither.core.expressions.binary_operation import BinaryOperationType
class HasAddition(ExpressionVisitor):
def result(self):
return self._result
def _post_binary_operation(self, expression):
if expression.type == BinaryOperationType.ADDITION:
self._result = True
visitor = HasAddition(expression) # expression — это выражение для проверки
print(f'The expression {expression} has a addition: {visitor.result()}')
Граф потока управления (CFG)
Вторым по распространенности представлением кода является граф потока управления (CFG). Как следует из названия, это представление на основе графа, которое раскрывает все пути выполнения. Каждый узел содержит одну или несколько инструкций. Ребра в графе представляют операции управления потоком (if/then/else, циклы и т. д.). CFG нашего предыдущего примера выглядит так:
CFG — это представление, на основе которого строится большинство анализов.
Существует множество других представлений кода. Каждое представление имеет свои преимущества и недостатки в зависимости от анализа, который вы хотите выполнить.
Анализ
Самый простой тип анализа, который вы можете выполнить с помощью Слизер, — это синтаксический анализ.
Синтаксический анализ
Слизер может перемещаться по различным компонентам кода и их представлению, чтобы находить несоответствия и недостатки, используя подход, похожий на сопоставление с шаблоном.
Например, следующие детекторы ищут проблемы, связанные с синтаксисом:
-
Перекрытие переменных состояния (State variable shadowing) (opens in a new tab): перебирает все переменные состояния и проверяет, не перекрывает ли какая-либо из них переменную из унаследованного контракта (state.py#L51-L62 (opens in a new tab))
-
Некорректный интерфейс ERC-20 (opens in a new tab): ищет некорректные подписи функций ERC-20 (incorrect_erc20_interface.py#L34-L55 (opens in a new tab))
Семантический анализ
В отличие от синтаксического анализа, семантический анализ идет глубже и анализирует «смысл» кода. Это семейство включает в себя несколько широких типов анализов. Они приводят к более мощным и полезным результатам, но их также сложнее писать.
Семантический анализ используется для наиболее продвинутого обнаружения уязвимостей.
Анализ зависимости данных
Переменная 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): точка входа CFGvariables_read (list(Variable)): список прочитанных переменныхvariables_written (list(Variable)): список записанных переменныхstate_variables_read (list(StateVariable)): список прочитанных переменных состояния (подмножество variables`read)state_variables_written (list(StateVariable)): список записанных переменных состояния (подмножество variables`written)

