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

Як використовувати Слізер для пошуку помилок у смартконтрактах

solidity
смартконтракти
безпека
тестування
Просунутий рівень
Трейлофбітс
9 червня 2020 р.
6 хвилин на читання

Як використовувати Слізер

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

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

Слізер потребує 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

Окрім детекторів, Слізер має можливості перевірки коду за допомогою своїх принтерів (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)