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

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

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

Окрім детекторів, Слізер має можливості перевірки коду за допомогою своїх принтерів (відкривається в новій вкладці) та інструментів (відкривається в новій вкладці).

Використовуйте crytic.io (відкривається в новій вкладці), щоб отримати доступ до приватних детекторів та інтеграції з GitHub.

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

Можливості та архітектура фреймворку статичного аналізу Слізер були описані в публікаціях у блозі (1 (відкривається в новій вкладці), 2 (відкривається в новій вкладці)) та науковій статті (відкривається в новій вкладці).

Статичний аналіз існує в різних варіаціях. Ви, мабуть, розумієте, що компілятори, такі як clang (відкривається в новій вкладці) та gcc (відкривається в новій вкладці), залежать від цих методів дослідження, але вони також лежать в основі Infer (відкривається в новій вкладці), CodeClimate (відкривається в новій вкладці), FindBugs (відкривається в новій вкладці) та інструментів, заснованих на формальних методах, таких як Frama-C (відкривається в новій вкладці) і Polyspace (відкривається в новій вкладці).

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

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

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

Наступний код використовує 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;

Слізер постачається з вбудованими можливостями залежності даних (відкривається в новій вкладці) завдяки своєму проміжному представленню (обговорюється в наступному розділі).

Приклад використання залежності даних можна знайти в детекторі небезпечної строгої рівності (відкривається в новій вкладці). Тут Слізер шукатиме порівняння на строгу рівність із небезпечним значенням (incorrect_strict_equality.py#L86-L87 (відкривається в новій вкладці)) і повідомить користувача, що слід використовувати >= або <= замість ==, щоб запобігти потраплянню контракту в пастку зловмисника. Крім іншого, детектор вважатиме небезпечним значення, що повертається викликом balanceOf(address) (incorrect_strict_equality.py#L63-L64 (відкривається в новій вкладці)), і використовуватиме механізм залежності даних для відстеження його використання.

Обчислення фіксованої точки

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

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

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

Приклад використання фіксованої точки можна знайти в детекторах повторного входу: Слізер досліджує вузли та шукає зовнішні виклики, запис і читання зі сховища. Щойно він досягає фіксованої точки (reentrancy.py#L125-L131 (відкривається в новій вкладці)), він зупиняє дослідження та аналізує результати, щоб побачити, чи присутній повторний вхід, за допомогою різних патернів повторного входу (reentrancy_benign.py (відкривається в новій вкладці), reentrancy_read_before_write.py (відкривається в новій вкладці), reentrancy_eth.py (відкривається в новій вкладці)).

Написання аналізів з використанням ефективного обчислення фіксованої точки вимагає хорошого розуміння того, як аналіз поширює свою інформацію.

Проміжне представлення

Проміжне представлення (IR) — це мова, яка має бути більш піддатливою до статичного аналізу, ніж оригінальна. Слізер перекладає Solidity на власне IR: SlithIR (відкривається в новій вкладці).

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

Основи 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)