
EF/CF — Экстремально быстрый фаззинг смарт-контрактов
EF/CF — это новый подход к фаззингу смарт-контрактов: вместо создания нового фаззера с нуля, он переиспользует существующую инфраструктуру фаззинга кода на C/C++ для смарт-контрактов. В настоящее время основным поддерживаемым фаззером является AFL++, хотя также есть некоторая базовая поддержка libfuzzer и honggfuzz.
Зачем использовать существующую инфраструктуру фаззинга?
С какими проблемами мы сталкиваемся на этом пути?
./src/ethmutator/./src/evm2cpp/Этот репозиторий — основная точка входа для проекта EF/CF. Он содержит весь соответствующий код в виде подпроектов в ./src/, а также несколько вспомогательных скриптов для установки, скриптов для запуска фаззинг-кампаний и различные наборы данных для тестирования фаззера (и сравнения с другими инструментами).
./data/ — содержит наборы данных, использованные при оценке./scripts — содержит скрипты для запуска экспериментов, установки и т.д../docker — Dockerfile для работы в контейнере
./docker/tools/ содержит docker-файлы для инструментов, с которыми мы сравнивали EF/CF. Мы постарались зафиксировать в docker-файлах версии, которые использовали в нашей оценке в статье../EXPERIMENTS.md — содержит руководство по воспроизведению экспериментов из нашей статьи./examples — содержит примеры результатов, созданных EF/CFМы описываем архитектуру и реализацию EF/CF, а также обобщаем результаты нашей оценки в нашей статье: препринт arxiv.org
При ссылке на EF/CF в академических работах, пожалуйста, используйте следующую bibtex-запись для цитирования:```bibtex @InProceedings{efcf2023, author = "Michael Rodler and David Paaßen and Wenting Li and Lukas Bernhard and Thorsten Holz and Ghassan Karame and Lucas Davi", title = "EF/CF: High Performance Smart Contract Fuzzing for Exploit Generation", booktitle = "{IEEE} European Symposium on Security and Privacy ({EuroS&P})", publisher = "{IEEE}", year = "2023", }
## Быстрый старт
Рекомендуемый способ — запустить EF/CF как интерактивный docker-контейнер.
1. Войдите в контейнер с оболочкой ```
docker run --rm -it ghcr.io/uni-due-syssec/efcf-framework
или соберите контейнер из клонированного репозитория ``` make gitmodules # to fetch the git submodules make container-enter
1. Скомпилируйте, а затем фаззите контракт solidity до тех пор, пока не будет
обнаружен первый сбой/баг: ```
efcfuzz --until-crash --out ./baby_bank_results/ --source ./data/examples/baby_bank.sol
Нет git? если вы используете tarball/docker release, игнорируйте это.
Выполните git submodule update --init, чтобы получить последние коммиты подмодулей в уже клонированных репозиториях.
Убедитесь, что вы также выполните это в ./src/eEVM.```
git submodule update --init; cd src/eEVM/; git submodule update --init; cd ../../
*Предупреждение:* Выполнение `git clone --recursive $repo` или передача аргумента `--recursive` команде `git sumbodule (update|init)` приведёт к рекурсивному переходу git в подмодули репозитория AFL++, которые не нужны для этого проекта. Поэтому для экономии места лучше избегать рекурсивной проверки подмодулей.
### Контейнер
Мы предоставляем следующие удобные make-цели для рабочих процессов на основе контейнеров:```sh
make container-build # build default efcf container
make container-enter # enter default efcf container in current working dir
Если вы хотите обеспечить чистую сборку, вы можете использовать следующую команду```sh make container-build CLEAN_CHECKOUT=1
В качестве альтернативы контейнер можно собрать с помощью следующей команды docker:```sh
docker build \
-f docker/ubuntu.Dockerfile \
-t efcf:latest \
.
Обратите внимание, что существует также Dockerfile на основе Archlinux и Fedora. Они должны работать, но не так хорошо протестированы.
Для ручного распространения Docker-образа (например, если вы включаете некоторые локальные изменения), используйте:``` make container-release docker load -i ./efcf*.tar
Рекомендуем следующие параметры Docker для запуска:
* `--security-opt seccomp=unconfined` — лучшая производительность фаззинга
* `--net=host` — для удобного доступа к локальному узлу Ethereum
* `--tmpfs "/tmp/efcf/":exec,size=6g` — поместить временные файлы EF/CF на ramdisk, если возможно (меньше износ диска)
* `--privileged` — для запуска `afl-system-config` или `efcfuzz --configure-system`
* `-v` — для сохранения выходных данных EF/CF
### VM / Bare-Metal
Для рабочих процессов на основе VM или bare-metal:```sh
make system-install # install efcf to current system (requires root or sudo rights)
Обратите внимание, что многие скрипты в любом случае работают с относительной структурой каталогов, так что в основном это устанавливает зависимости и некоторые инструменты, которые полезно иметь в вашем PATH. Мы тестировали запуск EF/CF на следующих дистрибутивах Linux:
(Дистрибутив не так важен: мы тестировали LLVM 13 и 14, причём 14 предпочтительнее. LLVM 11 или 12, возможно, всё ещё работают, но, как всегда, чем новее, тем лучше. Важно, чтобы был LLVM, совместимый с нашим форком AFL++.)
Мы не тестировали EF/CF на Mac OS нативно. Вероятно, что-то не заработает (например, afl-clang-lto на Mac OS, похоже, не работает). Лучший вариант — использовать docker.```sh
make gitmodules
docker pull ubuntu:jammy --platform linux/amd64
docker build -t efcf:latest -f docker/ubuntu.Dockerfile --platform linux/amd64 .
docker run --tmpfs "/tmp/efcf/":exec,size=8g --platform linux/amd64 --rm -it -v $(pwd):$(pwd) -w $(pwd) efcf:latest
Мы тестировали с использованием docker desktop v4.21.1, и базовое использование EF/CF работает. Однако учтите следующее:
* Если при сборке вы видите segfault: попробуйте увеличить лимит памяти виртуальной машины, которую docker использует в Mac OS.
* Попробуйте включить ускорение с помощью rosetta в docker — возможно, так будет немного быстрее.
### Настройка окружения для разработки
Инструменты, как правило, устанавливать не нужно. Установите необходимые
зависимости, как указано в скрипте `system-install.sh` или в Dockerfile.
Для удобства у нас есть несколько скриптов для обновления вашего `PATH`:```sh
# POSIX-like shells (i.e., bash, ...)
source ./scripts/env.sh
# for the fish shell
source ./scripts/env.fish
Некоторые скрипты требуют ключ API для получения метаданных (например, ABI) из
сервиса Etherscan. Если у вас есть ключ API, необходимо задать
переменную окружения ETHERSCAN_API_KEY, чтобы передать его скриптам. При
работе на основе docker вы можете либо запустить docker-контейнер с
флагом --env, либо поместить ваш ключ API в файл .etherscan_api_key, который
встроит ключ API в docker-контейнер.
Для удобства мы используем обёрточный скрипт, который берёт на себя все детали
за вас при запуске EF/CF-фаззера: efcfuzz
Вы можете задать множество параметров командной строки для настройки поведения
фаззера в отношении процесса сборки и фаззинга. Ознакомьтесь с efcfuzz --help для
получения списка параметров.
Примеры
Скомпилируйте исходный код Solidity в нативный код EF/CF и начните фаззинг на 5 минут (то есть 300 секунд).```bash efcfuzz --timeout 300 --source ./data/examples/baby_bank.sol
Альтернативно, запустите с сокращённым выводом фаззинга (`--quiet` подавляет
вывод базового фаззера, а `--print-progress` выведет краткую сводку
о ходе фаззинга), и запустите фаззер на 4 ядрах.```bash
efcfuzz --quiet --print-progress --cores 4 --timeout 300 --source ./data/examples/baby_bank.sol
Используйте уже скомпилированный байткод и скомпилируйте байткод в нативный код EF/CF и начните фаззинг.```bash
pushd ./data/examples/; make baby_bank.combined.json; popd efcfuzz --timeout 300 --bin-runtime ./data/examples/baby_bank.combined.json
pushd ./data/examples/; make baby_bank; popd
efcfuzz --timeout 300
--bin-runtime ./data/examples/baby_bank.bin-runtime
--bin-deploy ./data/examples/baby_bank.bin
--abi ./data/examples/baby_bank.abi
Обёртка может экспортировать состояние контракта из узла go-ethereum/erigon и начать фаззинг оттуда.```bash
$ efcfuzz --timeout 300 --live-state 0xfffF8D17CB019E0825c478c666B251A7099df3FD
Кроме того, вы можете передать --include-address-deps=y, чтобы рекурсивно искать адреса других аккаунтов в хранилище экспортируемого контракта и также включать их в экспорт состояния. Однако это не включает другие контракты, хранящиеся в типах Solidity mapping. Чтобы действительно экспортировать всё состояние рекурсивно, передайте также флаг --include-mapping-deps=y.
Но будьте осторожны: такой рекурсивный поиск может привести к долгой компиляции и низкой производительности фаззинга. Особенно часто используемые контракты могут иметь много внутреннего состояния, и использование их экспортированного состояния может замедлить фаззинг. Проверьте, может ли фаззер достигать более 1k выполнений/сек. Если нет, лучше попробуйте создать искусственное и меньшее состояние. Попробуйте запустить локальный узел go-ethereum в режиме --dev и развернуть там свои контракты. Затем экспортируйте живое состояние из него.
Обёртка кэширует сборки, поэтому второй запуск фаззинга должен запускаться намного быстрее, поскольку начальное время компиляции больше не требуется. Если вы хотите только собрать и поместить результат в кэш, вы можете передать аргумент --build-only.
Пример: фаззинг со свойствами
EF/CF также поддерживает фаззинг на основе свойств, используя те же определения свойств, что и фаззер echidna. Свойства (или инварианты) выражаются в виде функций Solidity, которые действуют как оракул ошибок для фаззера. Например, вы можете добавить такую функцию Solidity:```solidity function test_property_balance() public view returns (bool) { return total_balance < 1000; }
Что представляет собой свойство, согласно которому total_balance всегда должен быть ниже
1000. Затем EF/CF сообщит об ошибке, если ему удастся нарушить это свойство с помощью
некоторой последовательности транзакций, т.е. оракул возвращает `false`.
Чтобы сообщить EF/CF, что это свойство, необходимо указать список сигнатур
функций в файле, который будет воспринят EF/CF как список свойств
для проверки во время фаззинга.
Самый простой способ — получить соответствующие сигнатуры с помощью флага `--hashes`
компилятора Solidity, например,```
solc --hashes ./path/to/your.sol | grep test_property > property_list
Теперь вы можете запустить фаззер с помощью:``` efcfuzz --source ./path/to/your.sol --properties ./property_list -C
Вы также можете добавить `--disable-detectors`, чтобы отключить встроенные оракулы ошибок на основе ether.
Вы можете попробовать следующий пример для фаззинга на основе свойств:```
efcfuzz \
--properties ./data/examples/harvey_baz_properties.signatures
--disable-detectors \
--until-crash --timeout 120 \
--source ./data/examples/harvey_baz.sol \
Пример: Фаззинг событий
EF/CF поддерживает фаззинг для поиска нарушений утверждений (assertions), которые выражаются через
события. Кроме того, мы поддерживаем использование произвольных пользовательских событий в качестве
оракула ошибок. По умолчанию EF/CF определит ошибку, если целевой контракт залогировал одно
из следующих событий: AssertionFailed(), AssertionFailed(uint256),
AssertionFailed(string) и Panic(uint256).```
efcfuzz --event-assertions
--timeout 120 --until-crash
--source ./data/properties-assertions-tests/verifyfunwithnumbers.sol
Вы также можете указать дополнительные пользовательские темы/хеши событий для отслеживания в
файле с помощью `--event-assertions-list ./path/to/eventslist.txt`. Как и в случае со
списком свойств выше, формат можно получить, используя `solc --hashes`, и
скопировав хеши и имена событий в файл списка событий.
По умолчанию EF/CF игнорирует события, которые не были выпущены целевым
контрактом. Если вы хотите изменить это, используйте `--event-assertions-target-only=n`.
(Примечание: вы можете использовать `--assertions`, чтобы включить проверку как событий, так и
твердотельных утверждений)
**Пример: Фаззинг для проверки утверждений Solidity ^0.8**
В настоящее время мы не поддерживаем фаззинг произвольных утверждений в коде Solidity
для версий Solidity ниже 0.8. Ранее утверждения Solidity просто вызывали
опкод `invalid`, что приводило к довольно жёсткому откату (revert). Версия Solidity
0.8 изменила поведение: вместо использования опкода `invalid` для
отката транзакций теперь используется механизм `revert`, и ошибки передаются
обратно вызывающему коду. Мы можем использовать такой тип распространения ошибок как
оракул ошибок в EF/CF. В настоящее время EF/CF поддерживает проверку типа ошибки Solidity
`Panic(uint256)`. [Дополнительная информация об ошибках
Solidity.](https://docs.soliditylang.org/en/v0.8.0/control-structures.html?highlight=assert#panic-via-assert-and-error-via-require)```
efcfuzz --sol-assertions \
--timeout 120 --until-crash \
--source ./data/assertions-tests/overflow.sol
(Примечание: вы можете использовать --assertions для включения проверки как событий, так и
утверждений Solidity)
Системные требования и конфигурация
Мы рекомендуем выделять от 4 до 16 ядер и примерно 1 ГБ памяти на ядро. Вы
можете использовать флаг --configure-system для настройки системы для
высокоскоростного фаззинга или настроить её самостоятельно. В docker-контейнерах
также необходимо настроить хост для достижения наилучшей производительности. Если хост
некритичен, вы можете запустить контейнер с флагом --privileged и использовать
/usr/local/bin/afl-system-config для настройки системы для высокоскоростного
фаззинга (обратите внимание, что это фактически запускает контейнер от имени root).```
docker run --rm -it --privileged efcf afl-system-config
docker run --rm -it
--security-opt seccomp=unconfined
--tmpfs "/tmp/efcf/":exec,size=6g
efcf
## Запуск эксперимента по фаззингу
Чтобы запустить эксперимент на наборе данных `data/tests/`, можно использовать
следующую команду для сборки контрактов и их фаззинг-обвязки (harness), а затем
запустить фаззер с различными настройками, многократными повторами и т.д.
Поскольку это займёт довольно много времени, мы можем запустить эти эксперименты
параллельно. Мы разделяем эксперименты по фаззингу на этап сборки и этап
фаззинга. Этапы сборки собирают все смарт-контракты последовательно (сама сборка,
тем не менее, использует несколько ядер). Затем мы запускаем 8 экземпляров
фаззера в фоновом режиме, которые возьмут артефакты сборки с этапа сборки и
начнут запуски фаззинга. Makefile автоматически попытается запустить всё в
соответствующем контейнере, если доступен либо `docker`, либо `podman`.```bash
make build-tests
make fuzz-tests CONTAINER_BACKGROUND=1 FUZZER_INSTANCES=8
Мы отключаем seccomp и сетевую песочницу при запуске контейнеров в фоновом режиме. Отключение seccomp-песочницы повышает производительность фаззинга. Использование сетевого режима хоста позволяет EF/CF обращаться к узлам Ethereum в локальной сети без дополнительной настройки.
Мы использовали скрипт ./scripts/run-tools-on-dataset.py для запуска других инструментов
внутри docker-контейнеров на этих наборах данных, например, с помощью этих команд для
набора данных multi:```bash
python3 ./scripts/run-tools-on-dataset.py ./data/multi/
cd ./results/tools-multi/
python3 ../../scripts/get-tools-on-dataset-stats.py
head stats.csv
Вам нужно адаптировать скрипт для настройки инструментов и количества запусков.
### Настройка фаззинг-эксперимента
Здесь мы используем эксперимент `tests` в качестве примера. Просто замените строку
`tests` на имя эксперимента в следующих шагах:
1. Соберите свой набор данных в `./data/`, например, набор данных `./data/tests` с тестовыми
контрактами. Для контрактов Solidity у нас есть универсальный `Makefile` для сборки
контрактов: `sol.Makefile`. Вы можете использовать его повторно, если хотите, см.
`./data/tests/Makefile` в качестве примера.
2. Создайте скрипт для создания артефактов сборки, включая все
необходимые шаги предварительной обработки/сбора данных. Например, для набора данных `tests` мы
имеем скрипт `./scripts/build-tests.sh`. Артефакты сборки должны
храниться в `./builds/tests/${contract}.build.tar.xz`.
3. Создайте скрипт для запуска фаззинг-кампании, например, для набора данных `tests`
создайте скрипт с именем `./scripts/fuzz-tests.sh`. Обычно можно использовать
общую функцию фаззинг-кампании из `./scripts/common.sh`. Посмотрите на
`fuzz-tests.sh` как на шаблон.
4. Результаты `fuzz-tests.sh` будут сохранены в `./results/run-fuzz-tests/`.
5. Для обобщения результатов мы предоставляем `./scripts/summarize.py` для скриптов-лаунчеров на bash
и `./scripts/summarize_l.py` для скриптов-лаунчеров на python
(инструмент `efcfuzz`).
Возможно, вам потребуется адаптировать эти скрипты в зависимости от вашего шага 3.
### Существующие фаззинг-эксперименты
#### Бенчмарки
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/multi">`./data/multi`</a> содержит бенчмарк масштабируемости,
который мы использовали для оценки того, насколько хорошо инструмент анализа масштабируется на более длинные
последовательности транзакций. Он состоит из трёх типов контрактов:
* `multi_gen_*.sol` - автоматически синтезированные контракты, которые выполняют
набор проверок `require(input <= MAGIC)` и затем устанавливают внутреннюю переменную состояния. Если
все переменные состояния установлены, то `selfdestruct` (или оракул echidna)
может быть инициирован.
* `multi_man_complex_*.sol` - созданные вручную варианты, которые работают аналогично
контрактам типа `multi_gen`, но содержат немного более хитрые
ограничения (например, не только равенство и неравенство с магическим
значением)
* `justlen_*.sol` - они взяты из [примера echidna-parade](https://github.com/crytic/echidna-parade/blob/main/examples/justlen.sol)
* `multi_simple_*.sol` - проверки работоспособности, которые подтверждают, что фаззер/инструмент может
в теории найти ошибки, требующие 9 или 10 транзакций. Здесь
анализатору нужно лишь вызвать 10 функций в правильном порядке без
аргументов. Это довольно просто для большинства инструментов анализа.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/throughput">`./data/throughput`</a> содержит контракты, которые мы
использовали для оценки пропускной способности. Это выборка контрактов с
разным размером. Обратите внимание, что мы исправили все уязвимости в этих контрактах,
чтобы найденные уязвимости не влияли на измерения пропускной способности.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/cov-max-testset">`./data/cov-max-testset`</a> содержит
контракты, которые мы использовали для сравнения фаззеров на основе покрытия кода.
#### Обнаружение ошибок
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/ethbmc-vuln">`./data/ethbmc-vuln`</a> список контрактов, которые
EthBMC определил как уязвимые.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/ethbmc-timeouts">`./data/ethbmc-timeouts`</a> список
контрактов, где EthBMC остановил анализ из-за тайм-аута.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/reentrancy">`./data/reentrancy`</a> набор контрактов,
уязвимых к атакам повторного входа.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/sailfish-dao-tp">`./data/sailfish-dao-tp`</a> набор контрактов,
которые, как было подтверждено, содержат ошибку повторного входа в рамках
[исследования sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/ground-truth).
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/sailfish-dao">`./data/sailfish-dao`</a> список всех контрактов,
где
[sailfish](https://github.com/ucsb-seclab/sailfish/tree/master/data/bugs)
обнаружил ошибку повторного входа.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/sereum">`./data/sereum`</a> список контрактов,
уязвимых к атакам повторного входа, согласно [Sereum](https://github.com/uni-due-syssec/sereum-results).
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/smartbugs-curated-accesscontrol">`./data/smartbugs-curated-accesscontrol`</a>
контракты из курируемого набора smartbugs, классифицированные как ошибки «управления доступом»
([репозиторий smartbugs](https://github.com/smartbugs/smartbugs/tree/master/dataset/access_control))
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/smartbugs-curated-reentrancy">`./data/smartbugs-curated-reentrancy`</a>
контракты из курируемого набора smartbugs, классифицированные как ошибки «повторного входа»
([репозиторий smartbugs](https://github.com/smartbugs/smartbugs/tree/master/dataset/reentrancy))
#### Тесты
Следующие наборы данных содержат базовые синтетические тестовые контракты для проверки возможностей
фаззера:
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/tests">`./data/tests`</a> базовые тесты, собранные из различных
источников, которые проверяют базовые возможности фаззера. Все используют selfdestruct
оракул.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/tests-not-vuln">`./data/tests-not-vuln`</a> то же, что и тесты, но не должны
определяться как уязвимые.
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/properties-tests">`./data/properties-tests`</a> тесты для
фаззинга на основе свойств
* <a href="https://github.com/uni-due-syssec/efcf-framework/blob/main/data/assertions-tests">`./data/assertions-tests`</a> тесты для
фаззинга на утверждения.
## Фаззинг подробнее
Мы используем скрипты-обёртки для запуска непосредственно фаззера (в нашем случае AFL++).
Это делается автоматически при использовании лаунчера `efcfuzz`.```bash
$ cd data/tests
$ make SimpleDAO.evm2cpp
$ cd ../../src/eEVM/
$ env AFL_BENCH_UNTIL_CRASH=1 ./fuzz/launch-aflfuzz.sh SimpleDAO
Если у вас установлены tmux и tmuxp, то для разработки и проверки интерактивная версия скрипта может быть полезной:```bash $ ./fuzz/interactive-aflfuzz.sh -b SimpleDAO
Затем он скомпилируется и будет фаззить в течение довольно длительного времени. После этого вы можете
выполнить `cd ./fuzz/out/SimpleDAO*`, чтобы просмотреть результаты фаззинга. Наши скрипты-обёртки
выполняют дополнительную работу помимо запуска программы `afl-fuzz`, то есть в основном
постобработку результатов. Кроме того, будут созданы несколько удобных
скриптов для анализа полученных тестовых случаев.
* `./a.sh` — выводит человекочитаемое представление тестового случая, обёртка вокруг
`efuzzcaseanalyzer`.
* `./r.sh` — запускает тестовый случай с теми же настройками, что и при запуске фаззера.
* `./m.sh` — минимизирует тестовый случай с теми же настройками, что и при запуске
фаззера.
* `./c.sh` — анализирует «цепочку» тестовых случаев, которые привели к данному тестовому
случаю. Полезен для анализа/оптимизации фаззера. Вы можете быстро увидеть, какой
тестовый случай был получен какой цепочкой мутаций над какими записями очереди.
Требует `fzf`.
Существуют также некоторые другие удобные отчёты, например:
* `./bugs` и `./bugtypes`, которые обобщают все обнаруженные баги.
* `./crashes_min`, который содержит минимизированные краши всех экземпляров
`afl-fuzz`.
**Просмотр покрытия кода базовых блоков EVM**```bash
$ cat coverage-percent-all.evmcov
70.73170731707317
Скрипт fuzz/evm-bb-coverage.sh вычисляет покрытие базовых блоков
для заданного выходного каталога AFL. Харнес может по желанию выгружать трассу базовых
блоков, которые затем сравниваются со списком базовых блоков, который выводится
evm2cpp (т.е. файлами .bb_list в eEVM/contracts/).
По умолчанию мы также вычисляем покрытие, которое наши стандартные универсальные сиды (см.
eEVM/fuzz/generic_seeds) дают:```bash
$ cat coverage-percent-seeds.evmcov
10.5890
Список покрытых базовых блоков хранится в файле `all.evmcov`.
**Просмотр сводки сгенерированных тестовых примеров**
`efuzzcaseanalyzer` можно использовать для просмотра/обобщения сгенерированных тестовых примеров,
например,```
$ efuzzcaseanalyzer -a ./contract.abi -s ./crashes_min/
Transactions Sequences:
--------------------------------------------------------------
TX [🪙]
deposit()[🪙];
withdraw(uint256)[↕️ ↩️ ];
withdraw(uint256)[];
--------------------------------------------------------------
Number of fuzzcases: 1
Average number of TXs: 3
Number of unique TX sequences: 1
Number of unique TX sequences (consecutive deduplicated): 1
Сводки обычно хранятся в файлах crashes_tx_summary и
queue_tx_summary, но последний может быть довольно подробным.
Анализ одного падающего тест-кейса``` $ ./a.sh default/crashes/id:000000,...
$ efuzzcaseanalyzer -a ./contract.abi default/crashes/id:000000,... Block header: number: 0 difficulty: 0 gas_limit: 0 timestamp: 0 initial_ether: 0
TX with tx_sender: 54 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(80), } TX with tx_sender: 238 (selector); call_value: 0x246ddf979; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 153 (selector); call_value: 0x3860e6373; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=1 func: withdraw(uint256) input: { Uint(37000000000000000000), } returns: return val: 1; allows reenter: 2; data: 0x0000000000000000000000000000000000000000000000000000000000000001 TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(37000000000000000000), }
И чтобы получить фактический результат фаззинг-цели, вы можете выполнить:```
$ ./r.sh default/crashes/id:000000,sig:06,src:000000+000010,time:1584,EM-________SAO_______AD
# roughly equivalent to running
$ env EVM_DEBUG_PRINT=1 ./build/fuzz_multitx default/crashes/id:000000,sig:06,src:000000+000010,time:1584,EM-________SAO_______AD
[...]
account 0xc4b803ea8bc30894cc4672a9159ca000d377d9a3 has balance 0x100000000000000000000000000000001bc16d67562e80000( > 0x1000000000000000000000000000000000000000000000000)
Aborted (core dumped)
Это даёт вам много подробного вывода, включая некоторые части трассировок выполнения контрактов и результат проверки баланса, которую выполняет harness.
Минимизация падающих входных данных
Падающие входные данные часто содержат несвязанные транзакции из-за рандомизированного подхода к тестированию. Это можно смягчить, выполнив минимизацию падающего входа (т.е. сокращая вход до тех пор, пока он всё ещё вызывает падение). Если вы хотите минимизировать не падающие входные данные, то можете использовать флаг -M для включения минимизации по покрытию в качестве критерия минимизации.
Следующая команда уменьшит тестовый пример и перезапишет файл:``` $ efuzzcaseminimizer -oa ./contract.abi ./build/fuzz_multitx ./default/crashes/id:000000,sig:06,src:000000+000010,time:1584,EM-________SAO_______AD
[..]
=== Before minimizing: === Block header: number: 0 difficulty: 0 gas_limit: 0 timestamp: 0 initial_ether: 0
TX with tx_sender: 54 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(80), } TX with tx_sender: 238 (selector); call_value: 0x246ddf979; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 153 (selector); call_value: 0x3860e6373; length: 4; block+=1; #returns=0 func: deposit() input: { } TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=1 func: withdraw(uint256) input: { Uint(37000000000000000000), } returns: return val: 1; allows reenter: 2; data: 0x0000000000000000000000000000000000000000000000000000000000000001 TX with tx_sender: 166 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(37000000000000000000), }
=== After minimizing: === Block header: number: 0 difficulty: 0 gas_limit: 0 timestamp: 0 initial_ether: 15133991795
TX with tx_sender: 4 (selector); call_value: 0x246ddf979; length: 4; block+=0; #returns=0 func: deposit() input: { } TX with tx_sender: 4 (selector); call_value: 0x0; length: 36; block+=1; #returns=1 func: withdraw(uint256) input: { Uint(37000000000000000000), } returns: return val: 1; allows reenter: 2; data: 0x TX with tx_sender: 0 (selector); call_value: 0x0; length: 36; block+=1; #returns=0 func: withdraw(uint256) input: { Uint(37000000000000000000), }
## Чтение формата тест-кейса
Формат тест-кейса ориентирован на фаззинг и не так прост для чтения. Следует учитывать несколько тонкостей.
* Формат тест-кейса рассматривается как «очередь» транзакций, которые могут быть выполнены. Как только возникает любая проблема, обработка тест-кейса останавливается. Это включает:
* Когда транзакция откатывается (revert).
* Любую ошибку, обнаруженную обвязочным кодом.
* Любой баг, который срабатывает и обнаруживается.
Как следствие, распечатанный тест-кейс не обязательно соответствует тому, что выполняется — в конце могут быть транзакции, которые не выполняются. Проверьте подробный вывод и используйте минимизатор, чтобы избавиться от них!
* Аналогично, может быть слишком много `returns` или ложных флагов `reenter`. Используйте минимизатор тест-кейсов, чтобы избавиться от них.
* Повторный вход в контракт происходит только тогда, когда в списке есть другая транзакция после той, которая должна выполнить повторный вход (т.е. в очереди есть следующая запись).
* Даже если флаг `reenter` установлен в какое-либо значение, это не обязательно означает, что в контракт будет выполнен повторный вход; это лишь означает, что обвязочный код попытается сделать это, если возможно. Например, если контракт не выполняет вызов, флаг `reenter` игнорируется, поскольку возможности для повторного входа нет. Обычно минимизатор удаляет все ложные флаги `reenter`.
В целом многие из этих проблем исчезают при использовании минимизатора тест-кейсов, так что всегда полезно применять его перед анализом сгенерированных тест-кейсов.
## Известные ложные срабатывания
Мы наблюдали несколько типов ложных срабатываний, которые, судя по всему, регулярно возникают при фаззинге контрактов с помощью EF/CF.
* Контракты, которые по замыслу выплачивают Ether. Оракул ошибок прироста Ether (Ether-gains) в EF/CF помечает такие контракты как уязвимые, хотя они работают как задумано:
* Азартные контракты: многие азартные контракты используют ту или иную форму случайности, что само по себе уже является плохой практикой в Ethereum. Однако некоторые азартные контракты реализованы так, что вынуждают угадывать, например, последние две цифры следующего blockhash или что-то подобное. Это может быть реализовано через схему обязательств (commitment scheme): первая транзакция фиксирует обязательство пользователя перед определённым значением, а вторая запускает угадывание и выплату в случае выигрыша. Такие контракты обычно не эксплуатируемы в реальном блокчейне. Однако в симулированном блокчейне EF/CF фаззер может скорректировать обязательство после того, как значение во второй транзакции уже наблюдается. Этот факт важен для EF/CF для достижения лучшего покрытия кода. Однако он также позволяет EF/CF легко находить последовательность транзакций, которая позволяет фаззеру детерминированно выигрывать в азартном контракте.
* Контракты, выплачивающие проценты: существует множество небольших контрактов, которые позволяют инвестировать Ether, а затем выплачивают определённый процент в качестве процентов каждые `N` блоков. Симулированный атакующий EF/CF способен подождать `N` блоков и затем получить процентную выплату, что снова обнаруживается оракулом ошибок Ether-gains.
* Airdrop'ы: некоторые токен-контракты поддерживают airdrop'ы, то есть просто раздают токены всем, кто их запросит, пока не будет достигнут определённый лимит. Например, airdrop'ы часто доступны только в течение короткого периода времени. Если такой контракт развёрнут в EF/CF, высока вероятность, что временной лимит установлен так, что airdrop'ы всё ещё активны. Тогда EF/CF фиксирует прирост Ether (Ether-gains), если полученные через airdrop токены можно снова продать.
* Немедленное сообщение об управляемом `DELEGATECALL`: сейчас мы сообщаем об управляемом delegatecall сразу после его вызова. Однако существует множество контрактов с функциями, которые намеренно позволяют вызывающему выполнить delegatecall на произвольный адрес. Но эти функции безусловно откатывают (revert) транзакцию сразу после delegatecall. Это предотвращает сохранение любых изменений состояния или переводов ether. Обычно такие функции содержат в названии слова вроде «simulate», поэтому их легко обнаружить.
* Это можно было бы исправить в EF/CF, отложив сообщение до конца выполнения. Однако это значительно усложняет оракул ошибок.
* Сейчас планов по исправлению нет.
* Вызываемый инициализатор: Мы заметили, что при фаззинге контрактов, экспортированных из блокчейна, EF/CF иногда может вызывать функции-инициализаторы, даже если контракт уже был инициализирован. Обычно это должно вызывать revert, но в среде EVM от EF/CF этого не происходит. Повторный вызов инициализатора часто приводит к тривиальному приросту Ether, поскольку, например, инициализатор устанавливает переменную *owner* или что-то подобное.
* Мы пока не уверены в первопричине этой проблемы. Однако её обычно легко обнаружить, поскольку функция-инициализатор обычно называется `initializer`, `init` или подобным образом.
## Частые ловушки
Мы постарались сделать всё возможное, чтобы этот инструмент был хоть как-то пригоден к использованию, но это всё ещё исследовательский прототип. Будьте готовы к тому, что что-то может ломаться. Вот некоторые распространённые проблемы, которые мы наблюдали:
* *В: У меня возникает странная ошибка компиляции из-за макроса `TOKENPASTE`.*
О: Это часто происходит, когда `efcfuzz` угадывает неправильное имя контракта (т.е. угадывает абстрактный контракт). Попробуйте передать `--name YourContract`, чтобы указать целевой контракт.