Инструмент статического анализа Slither

9a1dea12b7f1e5b045ba407263ed71f9.png

Всем привет! Я из команды по анализу уязвимостей распределенных систем Positive Technologies. Мы занимаемся исследованием безопасности в области блокчейн-технологий и хотим поделиться обзором фреймворка для статического анализа кода, написанного на Solidity, — Slither. Он разработан компанией Trail of Bits, релиз состоялся в 2018 году. Slither написан на Python 3.

Как это работает: инструмент запускает набор предустановленных детекторов уязвимостей, проверяет указанные смарт-контракты, выводит визуальную информацию о деталях контракта. Это позволяет разработчикам находить уязвимости, улучшать понимание кода и быстро создавать прототипы новых детекторов. Slither предоставляет информацию о коде смарт-контракта и обладает необходимой гибкостью для настройки и добавления пользовательского набора функций (например, для отображения артефактов типа графа наследования). Ссылка на github проекта.

Давайте рассмотрим пример использования Slither. Ниже представлен фрагмент кода, функция withdraw() уязвима к атакам типа cross-function reentrancy. Злоумышленник может создать атакующий смарт-контракт, который несколько раз произведет вызов этой функции до момента изменения баланса, тем самым получив больше средств, чем полагалось.

function withdraw() public {
uint balance = balances [msg.sender];
require (balance > 0, "”);

(bool sent, ) = msg.sender. call{value: balance}("");
require (sent, "Failed to send Ether");

balances [msg.sender] = 0;
}

Среди детекторов Slither есть те, которые определяют потенциальное наличие reentrancy. В этом случае сработает детектор reentrancy-eth. Помимо типа уязвимости в отчете отражаются уровень воздействия, вероятность возникновения и уязвимые строчки кода. Сам отчет можно просматривать в командной строке или в MD-файле.

44edde54be1cecc3b789c561a617f29a.png

Принципы работы

Для лучшего понимания работы Slither мы последовательно пройдем алгоритм анализа смарт-контракта от начала до конца.

С чего начинается работа любой программы? Конечно, с входных данных. На вход Slither получает абстрактное синтаксическое дерево, которое генерирует компилятор solc из исходного кода контракта.

926e5650f9e2bbe53e55c09927e83670.jpg

Абстрактное синтаксическое дерево (AST) — конечное ориентированное дерево, в листьях которого содержатся операнды, а внутренние вершины сопоставлены с операторами языка.

cb8ef283b34bc4207159f2ecdfc13f5b.jpg

По синтаксическому дереву восстанавливаются такие представления, как граф потока управления, граф наследования, который отражает связи между классами-родителями и классами-наследниками, и список выражений для перевода в язык представления.

Граф потока управления (CFG) — это множество всех возможных путей исполнения программы.

66bd6f99c7eadf4796be11f9c43c78db.jpg

Далее происходит преобразование всего исходного кода в промежуточную форму SlithIR.

SlithIR — это язык промежуточного представления кода на Solidity, созданный разработчиками Slither. При трансформации кода он опирается на граф потока управления и использует менее 40 инструкций. Понимание SlithIR не является необходимым, если вы хотите писать кастомные детекторы, однако оно пригодится, если вы планируете писать продвинутые семантические проверки.

SlithIR сохраняет две версии смарт-контракта — с SSA и без него.

SSA (static single assignment) — это еще одно промежуточное представление, в котором каждой переменной присваивается значение лишь единожды, все значения разделяются на версии. Таким образом, в представлении есть все возможные значения переменных.

Третий этап обработки кода состоит из трех видов анализа. Давайте рассмотрим подробнее каждый из них.

  1. Чтение и запись

Slither определяет, какие функции или узлы CFG читают или записывают переменные, и разбивает их все на два типа — переменные состояния и локальные переменные.

  1. Зависимость данных

Анализ того, как переменные зависят друг от друга. Некоторые переменные приобретают статус tainted, то есть переменная зависит от пользовательского ввода.

  1. Защищенные функции

Проверка модификаторов доступа функций и привилегированных операций.

После подключаются прочие инструменты, например:

  • инструмент обнаружения уязвимостей;

  • инструмент поиска конструкций, которые можно оптимизировать;

  • принтеры для отображения найденной информации;

  • другие сторонние приложения.

Детекторы

На момент написания статьи в Slither существует 92 детектора. Оценка риска происходит по двум параметрам — impact (воздействие) и confidence (вероятность).

Воздействие классифицируется пятью типами:

Вероятность может быть трех типов:

  • low (низкая);

  • medium (средняя);

  • high (высокая).

Здесь можно найти весь список детекторов и примеры к ним.

Синтаксические детекторы

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

Семантические детекторы

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

Зависимость данных

Считается, что variable_a зависит от variable_b, если существует путь, на котором значение variable_a зависит от variable_b.

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

variable_a = variable_b + 1

Slither поставляется со встроенной проверкой зависимостей данных (data dependency) благодаря своему промежуточному представлению.

Пример использования зависимости данных можно найти в dangerous strict equalities. Здесь Slither будет искать сравнение строгого равенства с опасным значением и сообщит пользователю, что следует использовать »>=» или »<=», а не »==», чтобы злоумышленник не смог сломать контракт.

Кастомные детекторы

Slither также поддерживает добавление собственных детекторов. Вот здесь можно найти инструкцию, как это сделать.

Использование

slither target --options

Target

  1. my-file.sol — путь до файла с исходным кодом;

  2. my-project — путь до папки с проектом Hardhat/Truffle/Foundry/Brownie/Dapp/Embark/Etherlime;

  3. 0x.. — адрес смарт-контракта в основной сети Ethereum;

  4. network:0x.. — адрес смарт-контракта в определенной сети, где network: mainnet, sepolia, rinkeby, goerli, arbi, testnet.arbi, poly, mumbai, avax, testnet.avax, ftm, bsc, testnet.bsc.

Важная особенность: тестирование по адресу не сработает, если смарт-контракт не верифицирован, так как код берется с API Etherscan.

Options

Давайте разберем, на мой взгляд, наиболее полезные функции Slither. Чтобы ознакомиться с полным списком, выполните команду: slither -h

Solc

Можно пробовать разные версии компилятора с помощью флага:

--solc-solcs-select solc1, solc2         # зависит от solc-select

--solc-solcs-bin solc1, solc2

Если нужно подтянуть зависимости к файлу и скомпилировать его через solc, то можно добавить remappings:

--solc--remaps my-remap

Выбор детекторов

По умолчанию Slither запускает все детекторы, но есть возможность как включить, так и исключить их из сценария тестирования:

slither file.sol --detect d1, d2

slither file.sol --exclude d1, d2

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

slither file.sol --exclude-low

Есть возможность убрать из отчета результаты анализа зависимостей:

slither file.sol --exclude-dependencies

Выбор принтеров

slither file.sol --print p1, p2

С помощью принтеров можно получить следующие артефакты:

  • call-graph — граф вызовов;

  • inheritance-graph — граф наследования;

  • cfg — граф потока управления;

  • evm — опкоды evm для каждой функции.

Чтобы получить полный список принтеров выполните команду: slither --list-printers

Сохранение отчетов

Чтобы сохранить отчет в формате MD, выполните команду:

slither my-file.sol --checklist >> my-report.md

Создание отчета в формате JSON: --json my-json

Кроме того, есть вариант создать ZIP-архив с JSON-файлом: --zip my-zip

Фильтрация путей

Когда тестируется целый проект, в нем могут находиться файлы, которые нам не нужно пропускать через анализатор (например, тесты, заглушки).

Чтобы исключить файлы из сценария тестирования, можно воспользоваться флагом --filter-paths my-path.

Конфигурационный файл

При желании большинство параметров можно описать в конфигурационном файле my-config.config.json.

Передать свой конфиг можно с помощью команды: --config-file my-config.config.json

{
    "detectors_to_run": "detector1,detector2",
    "printers_to_run": "printer1,printer2",
    "detectors_to_exclude": "detector1,detector2",
    "exclude_informational": false,
    "exclude_low": false,
    "exclude_medium": false,
    "exclude_high": false,
    "json": "",
    "disable_color": false,
    "filter_paths": "(mocks/|test/)", # regex
    "legacy_ast": false
}

Заключение

Итак, мы рассмотрели основные функции Slither. Он прекрасно подходит для быстрого тестирования большого объема кода и нахождения известных уязвимостей. Но не стоит забывать о том, что возможности статического анализа достаточно ограничены, поэтому существует вероятность, что какие-то баги не будут выявлены.

В Slither случаются и ложные срабатывания: уязвимость вроде бы и есть в отчете, но ее невозможно эксплуатировать ввиду каких-то дополнительных условий. Иногда возникают проблемы с преобразованием сложных конструкций (вроде пользовательских операторов) в промежуточное представление. В связи с этим необходимо помнить, что функциональность Slither обновляется и не стоит пренебрегать другими видами и инструментами тестирования.

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

© Habrahabr.ru