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

Як використовувати Slither для пошуку помилок у смартконтрактах

мова програмування
Смарт-контракти
захист
тестування
Для досвідчених користувачів
Trailofbits
9 червня 2020 р.
6 читається за хвилину

Як використовувати Slither

Мета цього посібника — показати, як використовувати Slither для автоматичного пошуку помилок у смарт-контрактах.

Встановлення

Slither вимагає Python >= 3.6. Його можна інсталювати за допомогою pip або за допомогою Docker.

Slither через pip:

pip3 install --user slither-analyzer

Slither через 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 постачається з набором попередньо визначених детекторів, які знаходять багато поширених помилок. Виклик Slither з командного рядка запустить усі детектори, для цього не потрібні глибокі знання статичного аналізу:

slither project_paths

Окрім детекторів, Slither має можливості перевірки коду за допомогою своїх принтерів (opens in a new tab) та інструментів (opens in a new tab).

Використовуйте crytic.io (opens in a new tab), щоб отримати доступ до приватних детекторів та інтеграції з GitHub.

Статичний аналіз

Можливості та дизайн фреймворку статичного аналізу Slither були описані в публікаціях у блозі (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)).

Тут ми не будемо вичерпно розглядати методи статичного аналізу та дослідників. Натомість ми зосередимося на тому, що необхідно для розуміння роботи Slither, щоб ви могли ефективніше використовувати його для пошуку помилок та розуміння коду.

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

На відміну від динамічного аналізу, який розглядає один шлях виконання, статичний аналіз розглядає всі шляхи одночасно. Для цього він покладається на інше представлення коду. Двома найпоширенішими є абстрактне синтаксичне дерево (AST) і граф потоку керування (CFG).

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

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

Якщо коротко, AST — це структуроване дерево, де зазвичай кожен лист містить змінну або константу, а внутрішні вузли є операндами або операціями потоку керування. Розгляньте такий код:

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

Відповідний AST показано в:

AST

Slither використовує AST, експортований solc.

Хоча AST легко побудувати, він є вкладеною структурою. Іноді це не найпростіша для аналізу структура. Наприклад, щоб визначити операції, які використовуються у виразі a + b <= a, ви повинні спочатку проаналізувати <= а потім +. Поширеним підходом є використання так званого шаблону «Відвідувач» (visitor pattern), який рекурсивно переміщується по дереву. Slither містить загальний відвідувач у ExpressionVisitor (opens in a new tab).

Наступний код використовує ExpressionVisitor, щоб визначити, чи містить вираз додавання:

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()}')
Показати все

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

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

CFG

CFG — це представлення, на основі якого будується більшість аналізів.

Існує багато інших представлень коду. Кожне представлення має переваги та недоліки залежно від аналізу, який ви хочете виконати.

Аналіз

Найпростіший тип аналізу, який можна виконати за допомогою Slither, — це синтаксичний аналіз.

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

Slither може переміщуватися різними компонентами коду та їхніми представленнями, щоб знаходити невідповідності й недоліки, використовуючи підхід, подібний до зіставлення зі зразком.

Наприклад, наведені нижче детектори шукають проблеми, пов'язані із синтаксисом:

Семантичний аналіз

На відміну від синтаксичного аналізу, семантичний аналіз заглиблюється і аналізує "значення" коду. Ця родина включає деякі широкі типи аналізів. Вони дають потужніші та корисніші результати, але їх також складніше писати.

Семантичний аналіз використовується для найсучасніших виявлень вразливостей.

Аналіз залежностей даних

Кажуть, що змінна variable_a залежить від даних змінної variable_b, якщо існує шлях, у якому на значення variable_a впливає variable_b.

У наведеному нижче коді variable_a залежить від variable_b:

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

Slither має вбудовані можливості залежності даних (opens in a new tab) завдяки своєму проміжному представленню (обговорюється в наступному розділі).

Приклад використання залежності даних можна знайти в детекторі небезпечної суворої рівності (opens in a new tab). Тут Slither шукатиме порівняння суворої рівності з небезпечним значенням (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 і слідує за ребрами, ви, ймовірно, побачите вже відвідані вузли. Наприклад, якщо цикл представлений, як показано нижче:

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

Ваш аналіз повинен знати, коли зупинитися. Тут є дві основні стратегії: (1) ітерувати кожен вузол скінченну кількість разів, (2) обчислити так звану нерухому точку (fixpoint). Нерухома точка по суті означає, що аналіз цього вузла більше не дає жодної значущої інформації.

Приклад використання нерухомої точки можна знайти в детекторах повторного входу: Slither досліджує вузли та шукає зовнішні виклики, запис у сховище та читання з нього. Щойно він досягає нерухомої точки (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) — це мова, яка має бути більш придатною для статичного аналізу, ніж вихідна. Slither перетворює Solidity у власне проміжне представлення: SlithIR (opens in a new tab).

Розуміння SlithIR не є необхідним, якщо ви хочете писати лише базові перевірки. Однак, це стане в пригоді, якщо ви плануєте писати розширений семантичний аналіз. Принтери SlithIR (opens in a new tab) та SSA (opens in a new tab) допоможуть вам зрозуміти, як перекладається код.

Основи API

Slither має API, який дозволяє досліджувати основні атрибути контракту та його функції.

Щоб завантажити кодову базу:

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

Дослідження контрактів і функцій

Об'єкт 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)

Останні оновлення сторінки: 3 лютого 2025 р.

Чи була ця інструкція корисною?