Как использовать Manticore для поиска ошибок в смарт-контрактах
Цель этого руководства — показать, как использовать Manticore для автоматического поиска ошибок в смарт-контрактах.
Установка
Для Manticore требуется Python версии 3.6 или выше. Его можно установить через pip или с помощью Docker.
Manticore через Docker
docker pull trailofbits/eth-security-toolboxdocker run -it -v "$PWD":/home/training trailofbits/eth-security-toolboxПоследняя команда запускает eth-security-toolbox в контейнере Docker, который имеет доступ к вашему текущему каталогу. Вы можете изменять файлы со своего хоста и запускать инструменты для работы с этими файлами из контейнера Docker_
Внутри контейнера Docker выполните:
solc-select 0.5.11cd /home/trufflecon/Manticore через pip
pip3 install --user manticoreРекомендуется использовать solc 0.5.11.
Запуск скрипта
Чтобы запустить скрипт Python с помощью Python 3:
python3 script.pyВведение в динамическое символическое выполнение
Динамическое символическое выполнение в двух словах
Динамическое символическое выполнение (DSE) — это метод анализа программ, который исследует пространство состояний с высокой степенью семантической осведомленности. Этот метод основан на обнаружении «путей программы», представленных в виде математических формул, называемых предикатами путей. Концептуально этот метод работает с предикатами путей в два этапа:
- Они создаются с помощью ограничений на входные данные программы.
- Они используются для генерации входных данных программы, которые вызовут выполнение соответствующих путей.
Этот подход не дает ложноположительных результатов в том смысле, что все выявленные состояния программы могут быть вызваны во время конкретного выполнения. Например, если анализ находит целочисленное переполнение, оно гарантированно будет воспроизводимым.
Пример предиката пути
Чтобы понять, как работает DSE, рассмотрим следующий пример:
1function f(uint a){23 if (a == 65) {4 // Присутствует ошибка5 }67}Поскольку f() содержит два пути, DSE создаст два разных предиката пути:
- Путь 1:
a == 65 - Путь 2:
Not (a == 65)
Каждый предикат пути является математической формулой, которую можно передать в так называемый решатель SMT (opens in a new tab), который попытается решить уравнение. Для Пути 1 решатель скажет, что путь можно исследовать при a = 65. Для Пути 2 решатель может присвоить a любое значение, кроме 65, например, a = 0.
Проверка свойств
Manticore предоставляет полный контроль над выполнением каждого пути. В результате это позволяет добавлять произвольные ограничения практически ко всему. Этот контроль позволяет создавать свойства для контракта.
Рассмотрим следующий пример:
1function unsafe_add(uint a, uint b) returns(uint c){2 c = a + b; // нет защиты от переполнения3 return c;4}Здесь в функции есть только один путь для исследования:
- Путь 1:
c = a + b
Используя Manticore, вы можете проверить наличие переполнения и добавить ограничения к предикату пути:
c = a + b AND (c < a OR c < b)
Если возможно найти такие значения a и b, для которых приведенный выше предикат пути выполним, это означает, что вы обнаружили переполнение. Например, решатель может сгенерировать входные данные a = 10, b = MAXUINT256.
Если рассмотреть исправленную версию:
1function safe_add(uint a, uint b) returns(uint c){2 c = a + b;3 require(c>=a);4 require(c>=b);5 return c;6}Связанная формула с проверкой на переполнение будет выглядеть так:
c = a + b AND (c >= a) AND (c=>b) AND (c < a OR c < b)
Эту формулу невозможно решить; другими словами, это доказательство того, что в safe_add значение c всегда будет увеличиваться.
Таким образом, DSE является мощным инструментом, который может проверять произвольные ограничения в вашем коде.
Запуск в Manticore
Мы рассмотрим, как исследовать смарт-контракт с помощью Manticore API. Целью является следующий смарт-контракт example.sol (opens in a new tab):
1pragma solidity >=0.4.24 <0.6.0;23contract Simple {4 function f(uint a) payable public{5 if (a == 65) {6 revert();7 }8 }9}Показать всеЗапуск автономного исследования
Вы можете запустить Manticore непосредственно на смарт-контракте с помощью следующей команды (project может быть файлом Solidity или каталогом проекта):
$ manticore projectВы получите вывод тестовых случаев, подобный этому (порядок может измениться):
1...2... m.c.manticore:INFO: Сгенерирован тестовый пример № 0 - STOP3... m.c.manticore:INFO: Сгенерирован тестовый пример № 1 - REVERT4... m.c.manticore:INFO: Сгенерирован тестовый пример № 2 - RETURN5... m.c.manticore:INFO: Сгенерирован тестовый пример № 3 - REVERT6... m.c.manticore:INFO: Сгенерирован тестовый пример № 4 - STOP7... m.c.manticore:INFO: Сгенерирован тестовый пример № 5 - REVERT8... m.c.manticore:INFO: Сгенерирован тестовый пример № 6 - REVERT9... m.c.manticore:INFO: Результаты в /home/ethsec/workshops/Automated Smart Contracts Audit - TruffleCon 2018/manticore/examples/mcore_t6vi6ij310...Показать всеБез дополнительной информации Manticore будет исследовать контракт с новыми символическими транзакциями до тех пор, пока не перестанет находить новые пути в контракте. Manticore не запускает новые транзакции после неудачной (например, после revert).
Manticore выведет информацию в каталог mcore_*. Помимо прочего, в этом каталоге вы найдете:
global.summary: покрытие и предупреждения компилятораtest_XXXXX.summary: покрытие, последняя инструкция, балансы аккаунтов для каждого тестового примераtest_XXXXX.tx: подробный список транзакций для каждого тестового примера
Здесь Manticore нашел 7 тестовых примеров, которые соответствуют следующему (порядок файлов может меняться):
| Транзакция 0 | Транзакция 1 | Транзакция 2 | Результат | |
|---|---|---|---|---|
| test_00000000.tx | Создание контракта | f(!=65) | f(!=65) | Приостановка выполнения |
| test_00000001.tx | Создание контракта | Резервная функция | REVERT | |
| test_00000002.tx | Создание контракта | RETURN | ||
| test_00000003.tx | Создание контракта | f(65) | REVERT | |
| test_00000004.tx | Создание контракта | f(!=65) | Приостановка выполнения | |
| test_00000005.tx | Создание контракта | f(!=65) | f(65) | REVERT |
| test_00000006.tx | Создание контракта | f(!=65) | Резервная функция | REVERT |
Сводка исследования: f(!=65) означает, что f вызывается с любым значением, отличным от 65.
Как вы можете заметить, Manticore создает уникальный тестовый пример для каждой успешной или отмененной транзакции.
Используйте флаг --quick-mode для быстрого исследования кода (он отключает детекторы ошибок, расчет газа и т. д.).
Манипулирование смарт-контрактом через API
В этом разделе подробно описывается, как управлять смарт-контрактом через Manticore Python API. Вы можете создать новый файл с расширением *.py и написать необходимый код, добавив в этот файл команды API (основы которых будут описаны ниже), а затем запустить его с помощью команды $ python3 *.py. Также вы можете выполнять приведенные ниже команды непосредственно в консоли python, для запуска консоли используйте команду $ python3.
Создание аккаунтов
Первое, что вам нужно сделать, — это инициировать новый блокчейн с помощью следующих команд:
1from manticore.ethereum import ManticoreEVM23m = ManticoreEVM()Пользовательский аккаунт создается с помощью m.create_account (opens in a new tab):
1user_account = m.create_account(balance=1000)Контракт Solidity можно развернуть с помощью m.solidity_create_contract (opens in a new tab):
1source_code = '''2pragma solidity >=0.4.24 <0.6.0;3contract Simple {4 function f(uint a) payable public{5 if (a == 65) {6 revert();7 }8 }9}10'''11# Инициировать контракт12contract_account = m.solidity_create_contract(source_code, owner=user_account)Показать всеСводка
- Вы можете создавать аккаунты пользователей и контрактов с помощью m.create_account (opens in a new tab) и m.solidity_create_contract (opens in a new tab).
Выполнение транзакций
Manticore поддерживает два типа транзакций:
- Необработанная транзакция: исследуются все функции
- Именованная транзакция: исследуется только одна функция
Необработанная транзакция
Необработанная транзакция выполняется с помощью m.transaction (opens in a new tab):
1m.transaction(caller=user_account,2 address=contract_account,3 data=data,4 value=value)Вызывающий, адрес, данные или значение транзакции могут быть конкретными или символическими:
- m.make_symbolic_value (opens in a new tab) создает символическое значение.
- m.make_symbolic_buffer(size) (opens in a new tab) создает символический массив байтов.
Например:
1symbolic_value = m.make_symbolic_value()2symbolic_data = m.make_symbolic_buffer(320)3m.transaction(caller=user_account,4 address=contract_address,5 data=symbolic_data,6 value=symbolic_value)Если данные являются символическими, Manticore будет исследовать все функции контракта во время выполнения транзакции. Будет полезно ознакомиться с объяснением резервной функции в статье Hands on the Ethernaut CTF (opens in a new tab), чтобы понять, как работает выбор функции.
Именованная транзакция
Функции могут выполняться по их имени.
Чтобы выполнить f(uint var) с символическим значением с аккаунта user_account и с 0 эфира, используйте:
1symbolic_var = m.make_symbolic_value()2contract_account.f(symbolic_var, caller=user_account, value=0)Если value транзакции не указано, по умолчанию оно равно 0.
Итог
- Аргументы транзакции могут быть конкретными или символическими
- Необработанная транзакция будет исследовать все функции
- Функция может быть вызвана по имени
Рабочее пространство
m.workspace — это каталог, используемый в качестве выходного каталога для всех сгенерированных файлов:
1print("Результаты находятся в {}".format(m.workspace))Завершение исследования
Чтобы остановить исследование, используйте m.finalize() (opens in a new tab). После вызова этого метода не следует отправлять никаких других транзакций, и Manticore сгенерирует тестовые примеры для каждого исследованного пути.
Итог: запуск в Manticore
Объединив все предыдущие шаги, мы получим:
1from manticore.ethereum import ManticoreEVM23m = ManticoreEVM()45with open('example.sol') as f:6 source_code = f.read()78user_account = m.create_account(balance=1000)9contract_account = m.solidity_create_contract(source_code, owner=user_account)1011symbolic_var = m.make_symbolic_value()12contract_account.f(symbolic_var)1314print("Результаты в {}".format(m.workspace))15m.finalize() # остановить исследованиеПоказать всеВесь приведенный выше код вы можете найти в файле example_run.py (opens in a new tab)
Получение путей, вызывающих исключения
Теперь мы сгенерируем конкретные входные данные для путей, вызывающих исключение в f(). Целью является следующий смарт-контракт example.sol (opens in a new tab):
1pragma solidity >=0.4.24 <0.6.0;2contract Simple {3 function f(uint a) payable public{4 if (a == 65) {5 revert();6 }7 }8}Использование информации о состоянии
Каждый выполненный путь имеет свое состояние блокчейна. Состояние может быть либо готовым (ready), либо завершенным (killed), что означает, что оно достигло инструкции THROW или REVERT:
[m.ready_states](https://manticore.readthedocs.io/en/latest/states.html#accessing): список готовых состояний (они не выполнили REVERT/INVALID)[m.killed_states](https://manticore.readthedocs.io/en/latest/states.html#accessings): список завершенных состояний[m.all_states](https://manticore.readthedocs.io/en/latest/states.html#accessings): все состояния
1for state in m.all_states:2 # сделать что-то с состояниемВы можете получить доступ к информации о состоянии. Например:
state.platform.get_balance(account.address): баланс аккаунтаstate.platform.transactions: список транзакцийstate.platform.transactions[-1].return_data: данные, возвращенные последней транзакцией
Данные, возвращенные последней транзакцией, представляют собой массив, который можно преобразовать в значение с помощью ABI.deserialize, например:
1data = state.platform.transactions[0].return_data2data = ABI.deserialize("uint", data)Как сгенерировать тестовый пример
Используйте m.generate_testcase(state, name) (opens in a new tab) для генерации тестового примера:
1m.generate_testcase(state, 'BugFound')Итог
- Вы можете перебирать состояния с помощью m.all_states
state.platform.get_balance(account.address)возвращает баланс аккаунтаstate.platform.transactionsвозвращает список транзакцийtransaction.return_data— это возвращаемые данныеm.generate_testcase(state, name)генерирует входные данные для состояния
Итог: получение пути, вызывающего исключение
1from manticore.ethereum import ManticoreEVM23m = ManticoreEVM()45with open('example.sol') as f:6 source_code = f.read()78user_account = m.create_account(balance=1000)9contract_account = m.solidity_create_contract(source_code, owner=user_account)1011symbolic_var = m.make_symbolic_value()12contract_account.f(symbolic_var)1314## Проверить, завершается ли выполнение с REVERT или INVALID1516for state in m.terminated_states:17 last_tx = state.platform.transactions[-1]18 if last_tx.result in ['REVERT', 'INVALID']:19 print('Найдено исключение {}'.format(m.workspace))20 m.generate_testcase(state, 'ThrowFound')Показать всеВесь приведенный выше код вы можете найти в файле example_run.py (opens in a new tab)
Обратите внимание, что мы могли бы создать гораздо более простой скрипт, поскольку все состояния, возвращаемые terminated_state, имеют в своем результате REVERT или INVALID: этот пример был предназначен только для демонстрации того, как управлять API.
Добавление ограничений
Мы рассмотрим, как ограничивать исследование. Мы сделаем допущение, что в документации f() указано, что функция никогда не вызывается с a == 65, поэтому любая ошибка при a == 65 не является настоящей ошибкой. Целью является следующий смарт-контракт example.sol (opens in a new tab):
1pragma solidity >=0.4.24 <0.6.0;2contract Simple {3 function f(uint a) payable public{4 if (a == 65) {5 revert();6 }7 }8}Операторы
Модуль Operators (opens in a new tab) облегчает работу с ограничениями, среди прочего он предоставляет:
- Operators.AND,
- Operators.OR,
- Operators.UGT (больше беззнакового),
- Operators.UGE (больше или равно беззнакового),
- Operators.ULT (меньше беззнакового),
- Operators.ULE (меньше или равно беззнакового).
Для импорта модуля используйте следующее:
1from manticore.core.smtlib import OperatorsOperators.CONCAT используется для конкатенации массива со значением. Например, return_data транзакции необходимо преобразовать в значение для сравнения с другим значением:
1last_return = Operators.CONCAT(256, *last_return)Ограничения
Вы можете использовать ограничения глобально или для определенного состояния.
Глобальное ограничение
Используйте m.constrain(constraint) для добавления глобального ограничения.
Например, вы можете вызвать контракт с символического адреса и ограничить этот адрес определенными значениями:
1symbolic_address = m.make_symbolic_value()2m.constraint(Operators.OR(symbolic == 0x41, symbolic_address == 0x42))3m.transaction(caller=user_account,4 address=contract_account,5 data=m.make_symbolic_buffer(320),6 value=0)Ограничение состояния
Используйте state.constrain(constraint) (opens in a new tab) для добавления ограничения к определенному состоянию. Его можно использовать для ограничения состояния после его исследования, чтобы проверить какое-либо свойство на нем.
Проверка ограничения
Используйте solver.check(state.constraints), чтобы узнать, является ли ограничение все еще выполнимым.
Например, следующий код наложит ограничение на symbolic_value, чтобы оно было отличным от 65, и проверит, остается ли состояние выполнимым:
1state.constrain(symbolic_var != 65)2if solver.check(state.constraints):3 # состояние выполнимоИтог: добавление ограничений
Добавляя ограничение к предыдущему коду, мы получим:
1from manticore.ethereum import ManticoreEVM2from manticore.core.smtlib.solver import Z3Solver34solver = Z3Solver.instance()56m = ManticoreEVM()78with open("example.sol") as f:9 source_code = f.read()1011user_account = m.create_account(balance=1000)12contract_account = m.solidity_create_contract(source_code, owner=user_account)1314symbolic_var = m.make_symbolic_value()15contract_account.f(symbolic_var)1617no_bug_found = True1819## Проверить, завершается ли выполнение с REVERT или INVALID2021for state in m.terminated_states:22 last_tx = state.platform.transactions[-1]23 if last_tx.result in ['REVERT', 'INVALID']:24 # мы не рассматриваем путь, где a == 6525 condition = symbolic_var != 6526 if m.generate_testcase(state, name="BugFound", only_if=condition):27 print(f'Найдена ошибка, результаты в {m.workspace}')28 no_bug_found = False2930if no_bug_found:31 print(f'Ошибок не найдено')Показать всеВесь приведенный выше код вы можете найти в файле example_run.py (opens in a new tab)
Последнее обновление страницы: 26 апреля 2024 г.