Представляем четвертую часть цикла, посвященного типичным уязвимостям, атакам и проблемным местам, которые присущи смарт-контрактам на языке Solidity и платформе Ethereum в целом. Здесь мы поговорим о том, какие инструменты для анализа безопасности смарт-контрактов существуют, и почему они (не)нужны. В первой части мы обсудили front-running attack, различные алгоритмы генерации случайных чисел и отказоустойчивость сети с Proof-of-Authority консенсусом. Во второй говорили об Integer overflow, ABI encoding/decoding, Uninitialized storage pointer, Type Confusion и о том, как сделать бэкдор. В третьей части мы затронули несколько отличительных особенностей Solidity и разобрали кое-какие логические уязвимости, встречающиеся в контрактах. В этой части мы предлагаем вашему вниманию обзор существующих инструментов для анализа смарт-контрактов. Все инструменты мы поделили на несколько категорий, которые позволяют делать: Далее мы коснёмся каждой из них. Какие-то из инструментов работают только при наличии исходного кода смарт-контракта, другие — и без него. И сразу оговоримся, что речь пойдет только о тех инструментах, которые нам известны, есть в открытом доступе, и с которыми мы работали. Если у вас есть опыт работы с какими-либо другими тулзами данной тематики, то милости просим в комментарии :) На текущий момент существует всего одна (или единственная удобная) возможность отлаживать смарт-контракт с исходным кодом — Remix IDE. А если необходимо разобраться в байткоде, то вам может помочь Radare2 или MythrilTraceExplorer. Если вы разработчик, чаще всего будете использовать именно Remix для отладки (скорее всего, вы так и делаете). Очень удобно отслеживать состояние локальных переменных (стек), состояние memory, storage, потребляемый gas и многое другое. Если вас пугает содержимое вкладки "Storage completely loaded", советуем разобраться с устройством storage. Из минусов можно отметить разве что периодические баги в JavaScript VM и тот факт, что если вы используете ассемблерные вставки, то на построчную отладку вы вряд ли можете надеяться. Если не нравится Remix, но обожаете консольку, то попробуйте Radare2. При помощи него можно дизассемблировать и отлаживать контракты. Для отладки используются трассы (записанные случившиеся транзакции). Радар по rpc подключается к ноде (ganache-cli удобен) и стягивает данные, необходимые для повторения транзакции. В графическом режиме можно отрыть тайлы с содержимым стека и памяти: Можно ставить точки останова и писать в память, однако мы не нашли способа заглянуть в storage или модифицировать его.  У ребят из Positive Technologies есть примеры использования Radare для дизассемблирования и отладки контрактов. Так как радар не самый интуитивный инструмент, советуем заглянуть и ознакомиться с его командами. До нас уже было проведено сравнение трёх инструментов — Mythrill, Manticore и Oyente. По результатам этого анализа лидирует Mythrill, а хуже всех справляется Oyente. Во время тестов авторы "кормили" инструменты исходными кодами контрактов, так что результаты не отражают, насколько хорошо они справляются с сырым байткодом, но в основном совпадают с нашими впечатлениями.  Mythril обладает широкой функциональностью, в том числе, для анализа как исходного кода, так и байткода смарт-контрактов. Примечательно то, что он может также работать on-chain, то есть в режиме online подключиться к ноде, выкачать контракт и проанализировать его. Также он является дизассемблером и умеет строить неплохие графы (но будьте осторожны, он часто делает это некорректно, так что лучше попробуйте Ethersplay или IDA-EVM). Также Mythril может сгенерировать state для тулзы под названием MythrilTraceExplorer, которая позволит вам отлаживать байткод смарт-контракта с символьными переменными: С полным перечнем опций Mythil и MythrilTraceExplorer можно ознакомиться в документации, а найти секретный флаг, которого в ней нет, можно тут. Возьмём контракт eth_tx_order_dependence_minimal из тестов Consensys  и посмотрим, как Mythril справляется при наличии исходных кодов: а теперь без них: Как видим, сырой байткод Mythril проанализировать не удалось. Инструмент с собственным движком символьного выполнения для обнаружения уязвимостей в смарт-контрактах, написан на Python. Не так давно разработчики наконец-то добавили вывод информации о входных данных, которые приведут к срабатыванию проблемы. Инструмент выдаёт более-менее адекватные результаты только при анализе контрактов с исходными кодами. Если натравить его на байткод, то он покроет только небольшую его часть и потому, скорее всего, не найдёт баги. По аналогии с Mythril поставим опыт и над Oyente и посмотрим, как хорошо он ищет уязвимости с исходными кодами и без них: Теперь натравим на байткод этого контракта: Как видим, сильно уменьшилось покрытие (с 98 до 18 процентов) и инструмент не находит проблем с контрактом. Развивать Oyente непросто из-за (ну очень) плохого кода. Например, параметры из модуля в модуль передаются через глобальные переменные. Скорее всего потому, что над проектом работает не более полутора человек. На всякий случай стоит держать Oyente в своём ящичке с инструментами, но неизвестно, будет ли он развиваться и совершенствоваться.  Пожалуй, самый распиаренный декомпилятор. Его главной особенностью является то, что он не работает. Для демонстрации этой фичи давайте декомпилируем игрушечный контракт, который для чистоты эксперимента соберём относительно старой версией solc'а 0.4.12 от 1 июля 2017-го. Отметим, что в последний раз Porosity обновлялся в январе 2018-го, так что можно расчитывать, что инструмент справится с нашим контрактом. Оригинал: Неповторимый вывод декомпилятора: При попытке передать ещё и abi, чтобы упростить инструменту работу, Porosity упадёт. Мы не смогли найти применение Porosity, все попытки использования заканчивались либо получением бессмысленного вывода, либо падением инструмента. Инструмент для символьного выполнения программ. Некоторое время назад в него добавили поддержку EVM и сейчас развивают (а если появится постоянный разработчик, то дело пойдёт ещё быстрее). В нём много фишечек, но они не оттестированы и чаще всего не работают. Также у проекта довольно плохо с документацией, но мы надеемся, что в скором времени сообщество исправит этот недостаток. Перед тем, как мы рассмотрим примеры использования Manticore, стоит "на пальцах" объяснить, что такое символьное выполнение, и в этом нам поможет слайд из презентации "Symbolic Execution for finding bugs" авторства Michael Hicks: Символьное выполнение позволяет понять, какой вход нужно подать функции/программе, чтобы оказаться в заданной точке. Грубо говоря, во время символьного выполнения собираются все ограничения (условия) на пути исполнения в заданную точку, из этих ограничений составляется формула, решив которую, мы получим нужный нам ввод. Для демонстрации решим задачу "The Lock" конкурса EtherHack, проходившего на конференции Positive Hack Days 8. Участникам не был дан исходный код, а только адрес контракта. Для решения задания нужно было вызвать контракт с верным пин-кодом (его проверка реализована в fallback). Решение предполагало проведение обратной разработки для понимания алгоритма проверки пин-кода. Давайте автоматизируем решение этой задачи. Однако, нам всё же придётся взглянуть на дизассемблированный код контракта, чтобы выяснить, что произойдет (или не произойдёт), если участник отправит валидный пин-код. А именно: стоит подметить, что в программе всего одна инструкция sstore, и мы можем смело предположить, что она будет выполнена, если ключ от ларчика подобран верно.  К сожалению, честно решить задачу не дают баги в Manticore (мы отправили баг-репорты разработчикам, надеемся, в скором времени ошибки исправят), так что придётся выкручиваться — мы переписали контракт в исходный код на Solidity и заменили в нём код возведения в степень последовательными умножениями (из-за баги Мантикора на текущий момент не может эмулировать возведение в степень с символьной базой). Конечно, решать задание во время конкурса таким извращённым способом бессмысленно, но не забывайте, что мы просто демонстрируем инструмент! А наличие исходного кода не упрощает работу, ведь символьное выполнение работает не с ним, а с байткодом. Достаточно оправданий и выгораживаний, давайте взглянем на скрипт: Запустив его, получим такой вывод: Разберёмся с происходящим. Для начала взглянем на код контракта. Функция unlock принимает на вход четыре байта, в которых ожидает увидеть текстовое представление четырёхзначного десятичного числа. Подойдут только те пин-коды, сумма разрядов в четвёртой степени которых равняется самому пин-коду, исключая ноль и один. Действительно,    Теперь разберёмся со скриптом. Что происходит в первой части, легко понять: создаётся аккаунт пользователя, затем контракт. Запускается транзакция с символьными входными данными. Этим действием будет активировано символьное выполнение, которое займёт пару минут. По его завершению, мы проходимся по всем конечным состояниям, проверяя, было ли что-то записано в storage, и если было, выясняем, с какими входными данными совершалась транзакция. Мы вычленяем из них пин-код, четыре байта после сигнатуры вызываемой функции. Если бы мы решали задачу и не знали, сколько цифр в пин-коде ожидает контракт, то доставали бы не четыре, а все тридцать два байта (дефолтный размер ячейки памяти в EVM). Тогда на экране увидели бы подобные числа: Пользуясь результатами анализа Manticore, мы можем не только решать игрушечные задачки, но и искать игрушечные баги в игрушечных контрактах, просто задавая такие наивные вопросы вроде: "Что нужно отправить контракту, чтобы на нашем адресе оказались все эфирки, которые содержал контракт?" Посмотрим на пример с незащищённым кошельком с воркшопа EthCC 2018: Очевидно, разработчики забыли наградить changeOwner атрибутом onlyOwner. Из-за этого атакующий в две транзакции может заполучить средства. Как обычно, всё самое интересное — в конце скрипта. Перечисляя состояния, мы задаём вопрос: "В каком из них у атакующего появились монетки?" А Manticore на него в состоянии ответить. Ответы ищем в папке mcore_XXX в файле WalletHack_00000000.tx: Если Manticore не забросят, она вырастет в неплохой инструмент для гибкого анализа умных контрактов, но сейчас она ещё не готова к встрече с реальным миром и реальными задачами. Echidna — это экспериментальный инструмент, предназначеный для фаззинга смартконтрактов. Однако это не тот фаззинг, о котором вы могли подумать. Для того, чтобы Echidna могла фаззить функции смарт-контракта, вам необходимо специальным образом подготовить свой контракт. Другими словами, вы должны "вывернуть" функцию, которую хотите профазить. Echidna будет делать разные последовательности вызовов num_setter и stop_setter, а затем echidna_sensitiveFunc. Также заметьте, что Echidna ожидает (но явно вам не скажет), что ваша функция будет возвращать true, когда все идет хорошо. Таким образом, "неправильное" поведение вы можете пометить возвратом false, revert(), selfdestruct(0) и др. Также стоит отметить, что вы не можете задать некие входные данные, которые Echidna будет изменять, поэтому для сложных задач эффективность такого фаззинга оставляет желать лучшего. А вот то, что придется переписывать каждую функцию, которую нужно профаззить, вы наверняка заметили. Справедиливости ради стоит отметить, что если вы умеете писать на Haskell, вы все же можете задавать генераторы входных значений так, чтобы они давали определенный домен из множества возможных значений (например, вы знаете что num должен быть больше пяти).  В целом, фаззинг может дать свои плоды. Если уязвимость проявляется только после длинной цепочки действий, символьное исполнение потребует больших затрат CPU и памяти в отличие от фаззинга (который, впрочем, обнаружит эту уязвимость только с некоторой вероятностью). Подробнее о подходе, который используется в Echidna, можно почитать в тут. Весьма эксперементальный инструмент с закрытым исходным кодом, которому повсюду мерещатся reentrancy. Из приятного: можно просто скопипастить свой контракт в форму отправки или указать ссылку на гитхаб. И поскольку используется статический анализ кода, то ваш контракт даже не обязан компилироваться. Выглядит это так: В данном примере обнуление баланса происходит после передачи Ether, что и является сигнатурой для SmartCheck. Однако, использование функций send и transfer само по себе предотвращает reentrancy (мы разбирали это в третьей части), так что на скриншоте выше мы видим false positive. Поскольку на данный момент в открытом доступе нет работающих декомпиляторов, то для того, чтобы понять, какую логику реализует тот или иной контракт, придется пореверсить. В целом, надежда на то, что декомпилятор будет, есть. Первые попытки были предприняты в Porosity, а скорее всего решение работающее, но, к сожалению, закрытое есть у ребят из RET2. А пока давайте взглянем, что мы можем сделать, оставшись наедине с байткодом. Старый и самый удобный дизассемблер. Он не может похвастаться красивыми интерфейсами, зато у него есть фишечки, которых нет в других инструментах: во-первых, он сворачивает инструкции, вместо последовательности показывая одно, более читабельное выражение, и во-вторых, для каждого базового блока EVMdis показывает стек на входе в блок, в том числе символизированные переменные. Это значительно ускоряет процесс понимания логики кода, поскольку без этих данных состояние стека пришлось бы восстанавливать с начала функции, ведь EVM — стековая машина. Например: По 0x33 достаётся значение из calldata и кладётся на стек. Поскольку это значение неизвестно, оно символизируется. Далее, мы можем наблюдать его в стеке базового блока label36 среди других символьных и конкретных значений (чисел и адресов переходов). На наш взгляд, это самый удобный инструмент для обратной разработки контрактов. Плагин к Binary Ninja, добавляющий поддержку архитектуры EVM. Промежуточные представления (LLIL, и MLIL) ниндзи не будут работать, ведь EVM — это стековая машина, так что читать придётся голые ассемблерные инструкции. Из приятного: у плагина есть словарь с парами { хеш: прототип функции }, так что можно сразу определить многие известные функции (например, реализующие разные интерфейсы стандартов ERC): Также в плагине есть фича, показывающая состояние стека для каждый инструкции: Но она заменяет все символьные переменные словом Unknown, тогда как EVMdis вместо него пишет адрес, где эта символьная переменная появилась в стеке. Ethersplay не прижился в нашем арсенале, но, возможно, его разовьют до стадии, когда он превратится в полезный инструмент. Процессорный модуль для IDA Pro, позволяющий ей дизассемблировать контракты. Ещё в нём есть словарь с прототипами функций, как в Ethersplay. Однако, этого недостаточно для удобной обратной разработки контрактов, ведь состояние стека всей программы придётся прорабатывать руками. На наш взгляд, внимания этот инструмент не заслуживает, поскольку скорее всего развития не получит, а в текущем виде он бесполезен. В этом разделе хотелось бы поговорить о том, что реально имеет тенденцию сделать из вашего контракта защищенный контракт, а именно о тестирование. Как показывает практика, в смарт-контракте, который хотя бы на 95% покрыт тестами, вероятность серьезной уязвимости небольшая. В процессе написания тестов, скорее всего, будет дописана масса необходимых проверок, но не стоит обольщаться, еще останутся вещи, которые могут подсказать только опытные аудиторы безопасности или разработчик. Например, найти 0day уязвимости в предметной области, которую реализует контракт, или какие-то другие логические ошибки, либо же подсказать, как сделать определенные механизмы контракта (DApp) эффективнее. Удобнее всего проводить тестирование "не отходя от кассы" с помощью Truffle (c ним же и разрабатывается большинство смарт-контрактов). В папке test (появляется после truffle init) вы можете писать тесты и запускать их с помощью команды truffle test. Впрочем, вряд ли кто-то может рассказать лучше про тестирование с truffle, чем официальная документация. Также не забудьте померить покрытие тестами с помощью инструмента solidity-coverage. Если по каким-то причинам (а их может быть немало) вас не устраивает Truffle, можно писать тесты, используя web3 библиотеки (они есть под разные языки). Удобнее всего применять web3.js, поскольку написанный код тестов можно будет легко перенести в будущую DApp как часть логики. Единственное предостережение — используйте web3.js версии ^1.0.0. Будущее за ней, и с версией 0.14.0, которая будет у вас первой строкой в гугле, она не совместима. Рассмотрим пример: Прежде чем перейти к самим тестам, придется написать функцию, которая будет заливать свежий контракт в сеть, благо это нужно сделать только раз :) Под спойлером показан лишь один тест, поскольку тесты (на чем бы они ни писались) занимают обычно много места. С полным примером предлагаем самостоятельно ознакомиться в web3_utilz/Testing. Инструменты, позволяющие искать баги автоматически, могут выявить сейчас лишь мейнстримные уязвимости, которые и так у всех на слуху, но, скорее всего, будут полезны на CTF. Что касается инструментов из категории отладки и обратной разработки (reverse engineering), они определенно могут быть полезны в ситуации, когда исходного кода нет. Так, например, они могут помочь в процессе разбора эксплоита для очередной громкой уязвимости или при заимствовании ноу-хау у конкурентов (вспоминается пример с cryptoKittes). Ну и не забываем, что для работы с этими инструментами нужны люди с соотвествующими умениями и квалификациями по reverse engineering. Главными же друзьями разработчика, как всегда, остаются изучение чужих ошибок, тестирование и аудирование своих проектов. Большое спасибо, что докрутили ползунок до конца статьи, для вас старались p4lex и Igor1024. Pentester, Security researcher. @_p4lex