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

Як використовувати Ехідну для тестування смарт-контрактів

Solidity
смарт-контракти
безпека
тестування
фазинг
Просунутий рівень
Трейлофбітс
10 квітня 2020 р.
12 хвилин на читання

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

Ехідну можна встановити за допомогою Docker або використовуючи попередньо скомпільований бінарний файл.

Ехідна через Docker

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/training trailofbits/eth-security-toolbox

Остання команда запускає eth-security-toolbox у Docker, який має доступ до вашого поточного каталогу. Ви можете змінювати файли на своєму хості та запускати інструменти для файлів із Docker

Усередині Docker виконайте:

solc-select 0.5.11
cd /home/training

Бінарний файл

https://github.com/crytic/echidna/releases/tag/v1.4.0.0 (opens in a new tab)

Вступ до фазингу на основі властивостей

Ехідна — це фазер на основі властивостей, який ми описували в наших попередніх публікаціях у блозі (1 (opens in a new tab), 2 (opens in a new tab), 3 (opens in a new tab)).

Фазинг

Фазинг (opens in a new tab) — це добре відомий метод у спільноті з безпеки. Він полягає у генеруванні більш-менш випадкових вхідних даних для пошуку помилок у програмі. Фазери для традиційного програмного забезпечення (такі як AFL (opens in a new tab) або LibFuzzer (opens in a new tab)) відомі як ефективні інструменти для пошуку помилок.

Крім суто випадкового генерування вхідних даних, існує багато методів і стратегій для створення якісних вхідних даних, зокрема:

  • Отримання зворотного зв'язку від кожного виконання та спрямування генерації за його допомогою. Наприклад, якщо щойно згенеровані вхідні дані призводять до виявлення нового шляху, має сенс генерувати нові вхідні дані, близькі до них.
  • Генерування вхідних даних з урахуванням структурних обмежень. Наприклад, якщо ваші вхідні дані містять заголовок із контрольною сумою, буде доцільно дозволити фазеру генерувати вхідні дані, які проходять перевірку контрольної суми.
  • Використання відомих вхідних даних для генерування нових: якщо ви маєте доступ до великого набору дійсних вхідних даних, ваш фазер може генерувати нові вхідні дані на їхній основі, а не починати генерацію з нуля. Їх зазвичай називають сідами (seeds).

Фазинг на основі властивостей

Ехідна належить до специфічної родини фазерів: фазинг на основі властивостей, значною мірою натхненний QuickCheck (opens in a new tab). На відміну від класичних фазерів, які намагаються знайти збої, Ехідна намагатиметься порушити визначені користувачем інваріанти.

У смарт-контрактах інваріантами є функції Solidity, які можуть представляти будь-який неправильний або недійсний стан, якого може досягти контракт, зокрема:

  • Неправильний контроль доступу: зловмисник став власником контракту.
  • Неправильна машина станів: токени можна переказувати, поки контракт призупинено.
  • Неправильна арифметика: користувач може викликати антипереповнення (underflow) свого балансу та отримати необмежену кількість безкоштовних токенів.

Тестування властивості за допомогою Ехідни

Ми розглянемо, як протестувати смарт-контракт за допомогою Ехідни. Ціллю є наступний смарт-контракт token.sol (opens in a new tab):

Ми зробимо припущення, що цей токен повинен мати такі властивості:

  • Будь-хто може мати щонайбільше 1000 токенів
  • Токен не можна переказувати (це не токен ERC-20)

Написання властивості

Властивості Ехідни — це функції Solidity. Властивість повинна:

  • Не мати аргументів
  • Повертати true, якщо вона успішна
  • Мати ім'я, що починається з echidna

Ехідна буде:

  • Автоматично генерувати довільні транзакції для тестування властивості.
  • Повідомляти про будь-які транзакції, що призводять до повернення властивістю false або видачі помилки.
  • Відкидати побічні ефекти під час виклику властивості (тобто, якщо властивість змінює змінну стану, вона відкидається після тесту)

Наступна властивість перевіряє, що той, хто викликає, має не більше 1000 токенів:

function echidna_balance_under_1000() public view returns(bool){
      return balances[msg.sender] <= 1000;
}

Використовуйте успадкування, щоб відокремити ваш контракт від ваших властивостей:

contract TestToken is Token{
      function echidna_balance_under_1000() public view returns(bool){
            return balances[msg.sender] <= 1000;
      }
  }

token.sol (opens in a new tab) реалізує властивість і успадковує від токена.

Ініціалізація контракту

Ехідні потрібен конструктор без аргументів. Якщо ваш контракт потребує специфічної ініціалізації, вам потрібно зробити це в конструкторі.

В Ехідні є деякі специфічні адреси:

  • 0x00a329c0648769A73afAc7F9381E08FB43dBEA72, яка викликає конструктор.
  • 0x10000, 0x20000 та 0x00a329C0648769a73afAC7F9381e08fb43DBEA70, які випадковим чином викликають інші функції.

У нашому поточному прикладі нам не потрібна жодна особлива ініціалізація, тому наш конструктор порожній.

Запуск Ехідни

Ехідна запускається за допомогою:

echidna-test contract.sol

Якщо contract.sol містить кілька контрактів, ви можете вказати ціль:

echidna-test contract.sol --contract MyContract

Підсумок: Тестування властивості

Нижче наведено підсумок запуску Ехідни на нашому прикладі:

contract TestToken is Token{
    constructor() public {}
        function echidna_balance_under_1000() public view returns(bool){
          return balances[msg.sender] <= 1000;
        }
  }

Ехідна виявила, що властивість порушується, якщо викликається backdoor.

Фільтрування функцій для виклику під час кампанії фазингу

Ми розглянемо, як фільтрувати функції, які підлягають фазингу. Ціллю є наступний смарт-контракт:

Цей невеликий приклад змушує Ехідну знайти певну послідовність транзакцій для зміни змінної стану. Це складно для фазера (рекомендується використовувати інструмент символьного виконання, такий як Мантікора (opens in a new tab)). Ми можемо запустити Ехідну, щоб перевірити це:

echidna-test multi.sol
...
echidna_state4: passed! 🎉
Seed: -3684648582249875403

Фільтрування функцій

Ехідні важко знайти правильну послідовність для тестування цього контракту, оскільки дві функції скидання (reset1 та reset2) встановлять усі змінні стану на false. Однак ми можемо використати спеціальну функцію Ехідни, щоб додати функцію скидання до чорного списку або додати до білого списку лише функції f, g, h та i.

Щоб додати функції до чорного списку, ми можемо використати цей файл конфігурації:

filterBlacklist: true
filterFunctions: ["reset1", "reset2"]

Інший підхід до фільтрування функцій полягає в переліку функцій із білого списку. Для цього ми можемо використати цей файл конфігурації:

filterBlacklist: false
filterFunctions: ["f", "g", "h", "i"]
  • filterBlacklist за замовчуванням має значення true.
  • Фільтрування виконуватиметься лише за іменем (без параметрів). Якщо у вас є f() та f(uint256), фільтр "f" відповідатиме обом функціям.

Запуск Ехідни

Щоб запустити Ехідну з файлом конфігурації blacklist.yaml:

echidna-test multi.sol --config blacklist.yaml
...
echidna_state4: failed!💥
  Call sequence:
    f(12)
    g(8)
    h(42)
    i()

Ехідна майже миттєво знайде послідовність транзакцій для фальсифікації властивості.

Підсумок: Фільтрування функцій

Ехідна може додавати функції до чорного або білого списку для виклику під час кампанії фазингу за допомогою:

filterBlacklist: true
filterFunctions: ["f1", "f2", "f3"]
echidna-test contract.sol --config config.yaml
...

Ехідна розпочинає кампанію фазингу, або додаючи до чорного списку f1, f2 та f3, або викликаючи лише їх, залежно від значення логічної змінної filterBlacklist.

Як тестувати assert у Solidity за допомогою Ехідни

У цьому короткому посібнику ми покажемо, як використовувати Ехідну для тестування перевірки асертів у контрактах. Припустімо, у нас є такий контракт:

Написання асерту

Ми хочемо переконатися, що tmp менше або дорівнює counter після повернення їхньої різниці. Ми могли б написати властивість Ехідни, але нам потрібно було б десь зберігати значення tmp. Натомість ми можемо використати такий асерт:

Запуск Ехідни

Щоб увімкнути тестування збоїв асертів, створіть файл конфігурації Ехідни (opens in a new tab) config.yaml:

checkAsserts: true

Коли ми запускаємо цей контракт в Ехідні, ми отримуємо очікувані результати:

Як бачите, Ехідна повідомляє про збій асерту у функції inc. Додавання більше одного асерту на функцію можливе, але Ехідна не зможе визначити, який саме асерт дав збій.

Коли та як використовувати асерти

Асерти можна використовувати як альтернативу явним властивостям, особливо якщо умови для перевірки безпосередньо пов'язані з правильним використанням певної операції f. Додавання асертів після певного коду гарантує, що перевірка відбудеться відразу після його виконання:

function f(..) public {
    // деякий складний код
    ...
    assert (condition);
    ...
}

Навпаки, використання явної властивості Ехідни призведе до випадкового виконання транзакцій, і немає простого способу точно визначити, коли вона буде перевірена. Усе ще можливо застосувати такий обхідний шлях:

function echidna_assert_after_f() public returns (bool) {
    f(..);
    return(condition);
}

Однак є деякі проблеми:

  • Це не спрацює, якщо f оголошено як internal або external.
  • Незрозуміло, які аргументи слід використовувати для виклику f.
  • Якщо f скасовується, властивість дасть збій.

Загалом, ми рекомендуємо дотримуватися рекомендацій Джона Регера (opens in a new tab) щодо використання асертів:

  • Не викликайте жодних побічних ефектів під час перевірки асерту. Наприклад: assert(ChangeStateAndReturn() == 1)
  • Не використовуйте асерти для очевидних тверджень. Наприклад, assert(var >= 0), де var оголошено як uint.

Нарешті, будь ласка, не використовуйте require замість assert, оскільки Ехідна не зможе це виявити (але контракт усе одно буде скасовано).

Підсумок: Перевірка асертів

Нижче наведено підсумок запуску Ехідни на нашому прикладі:

Ехідна виявила, що асерт у inc може дати збій, якщо цю функцію викликати кілька разів із великими аргументами.

Збір та модифікація корпусу Ехідни

Ми розглянемо, як збирати та використовувати корпус транзакцій за допомогою Ехідни. Ціллю є наступний смарт-контракт magic.sol (opens in a new tab):

Цей невеликий приклад змушує Ехідну знайти певні значення для зміни змінної стану. Це складно для фазера (рекомендується використовувати інструмент символьного виконання, такий як Мантікора (opens in a new tab)). Ми можемо запустити Ехідну, щоб перевірити це:

echidna-test magic.sol
...

echidna_magic_values: passed! 🎉

Seed: 2221503356319272685

Однак ми все ще можемо використовувати Ехідну для збору корпусу під час запуску цієї кампанії фазингу.

Збір корпусу

Щоб увімкнути збір корпусу, створіть каталог корпусу:

mkdir corpus-magic

Та файл конфігурації Ехідни (opens in a new tab) config.yaml:

coverage: true
corpusDir: "corpus-magic"

Тепер ми можемо запустити наш інструмент і перевірити зібраний корпус:

echidna-test magic.sol --config config.yaml

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

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

Сідінг корпусу

Ехідні потрібна допомога, щоб впоратися з функцією magic. Ми збираємося скопіювати та змінити вхідні дані, щоб використовувати для неї відповідні параметри:

cp corpus/2712688662897926208.txt corpus/new.txt

Ми змінимо new.txt для виклику magic(42,129,333,0). Тепер ми можемо перезапустити Ехідну:

Цього разу вона виявила, що властивість порушується негайно.

Пошук транзакцій із високим споживанням газу

Ми розглянемо, як знайти транзакції з високим споживанням газу за допомогою Ехідни. Ціллю є наступний смарт-контракт:

Тут expensive може мати велике споживання газу.

Наразі Ехідні завжди потрібна властивість для тестування: тут echidna_test завжди повертає true. Ми можемо запустити Ехідну, щоб перевірити це:

echidna-test gas.sol
...
echidna_test: пройдено! 🎉

Сід: 2320549945714142710

Вимірювання споживання газу

Щоб увімкнути вимірювання споживання газу за допомогою Ехідни, створіть файл конфігурації config.yaml:

estimateGas: true

У цьому прикладі ми також зменшимо розмір послідовності транзакцій, щоб результати було легше зрозуміти:

seqLen: 2
estimateGas: true

Запуск Ехідни

Після створення файлу конфігурації ми можемо запустити Ехідну таким чином:

Відфільтровування викликів, що зменшують газ

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

Якщо Ехідна може викликати всі функції, їй буде нелегко знайти транзакції з високою вартістю газу:

Це тому, що вартість залежить від розміру addrs, а випадкові виклики, як правило, залишають масив майже порожнім. Однак додавання pop та clear до чорного списку дає нам набагато кращі результати:

filterBlacklist: true
filterFunctions: ["pop", "clear"]
echidna-test pushpop.sol --config config.yaml
...
push використав максимум 40839 газу
...
check використав максимум 1484472 газу

Підсумок: Пошук транзакцій із високим споживанням газу

Ехідна може знаходити транзакції з високим споживанням газу за допомогою параметра конфігурації estimateGas:

estimateGas: true
echidna-test contract.sol --config config.yaml
...

Ехідна повідомить про послідовність із максимальним споживанням газу для кожної функції після завершення кампанії фазингу.