Представляем третью часть цикла, посвященного типичным уязвимостям, атакам и проблемным местам, присущим смарт-контрактам на языке Solidity, и платформе Ethereum в целом. Здесь поговорим о том, какими особенностями обладает Solidity и какими уязвимостями они могут обернуться в умелых руках. В первой части мы обсудили front-running attack, различные алгоритмы генерации случайных чисел и отказоустойчивость сети с Proof-of-Authority консенсусом. Во второй говорили об Integer overflow, ABI encoding/decoding, Uninitialized storage pointer, Type Confusion и о том, как сделать бэкдор. А в этой части мы обсудим несколько отличительных особенностей Solidity и посмотрим на логические уязвимости, которые могут встретиться в контрактах. Начнем с того, как смарт-контракты обмениваются друг с другом ценностями и пользовательскими адресами. В начале эфиры передавались посредством вызова другого контракта: Однако, при вызове контракта без указания сигнатуры будет вызвана его fallback-функция, в которой может быть произвольный код. Такая непривычная логика работы приводила к знаменитой reentrancy, с помощью которой была взломана TheDAO. После этого появилась функция send, которая тоже представляет собой просто синтаксический сахар, — под капотом у нее тот же call, только количество газа ограничено, поэтому reentrancy уже провернуть не получится. Однако, если что-то пойдет не так, и эфир отправить не получится, то send не будет прерывать поток исполнения. Такое поведение также может быть критично. Например, эфир не отправили, а состояние контракта уже поменяли. Кто-то останется без эфиров.  Поэтому появилась transfer, и она вызовет исключение, если что-то пойдет не так.  Но и она не является серебряной пулей. Представим, что у нас есть массив адресов, на которые надо разослать эфир, и если использовать transfer, то успех целой операции будет зависеть от каждого из получателей, — если кто-то один не примет эфир, тогда все изменения полностью откатятся. И последний момент с отправкой эфира — функция selfdestruct.  На самом деле, это функция для уничтожения контракта, но весь эфир, который остался на контракте, будет отправлен на тот адрес, который указан как аргумент. Причем этого никак нельзя избежать — эфир уйдет, даже если принимающий адрес — это контракт, и fallback-функция у него не payable (fallback попросту не вызывается). Эфир будет отправлен даже на еще не созданный контракт!  В Solidity, для разрешения множественного наследования, используется алгоритм C3 линеаризации (тот же, что и в Python, например). И для тех, кто имел удачу не наступать на грабли множественного наследования, итоговый граф, скорее всего, покажется неочевидным. Рассмотрим на примере: Продолжите граф вызова, начиная от Son.sonWannaHome(). Будет вызван Dad, а затем Mom. Итого, наследование выглядит следующим образом.
Son -> Dad -> Mom -> Grandfather Пример более-менее правдоподобного контракта с багом относительно множественного наследования был представлен на Underhanded Solidity Coding Contest. Смарт-контракты пишут люди, а люди часто ошибаются… в названии переменных, конструкторов; забывают ограничить доступ к каким-то функциям (как, например, в Parity Multisig) и др. Также разработчик должен внимательно следить за возможным наступлением состояния гонки, поскольку любая функция смарт-контракта может быть вызвана с любого адреса, в любое время. Он должен сам реализовать необходимые примитивы синхронизации и модификаторы доступа, для того чтобы смарт-контракт мог контролировать очередность вызова. Кроме того, есть вещи, которые не сможет найти ни один анализатор кода, — ошибки предметной области. Поэтому в данном разделе будут рассмотрены авторские уязвимости. В подавляющем большинстве контрактов, которым нужно работать с математикой, например, рассчитывать, сколько токенов получит пользователь за присланый эфир, применяется библиотека SafeMath. Однако, название может быть обманчивым — на самом деле, SafeMath заботится лишь о переполнениях. Предлагаем рассмотреть следующий кусочек контракта: Заметили что-нибудь подозрительное? Скорее всего нет, и это абсолютно нормально. Давайте разбираться. Обратите внимание на выражение sum.div(priceTokenWei).mul(1 ether) — с точки зрения логики тут все очень гладко: "Чтобы получить объем токенов, которые нужно начислить инвестору, нужно поделить сумму эфиров на выражение, отражающее цену токена в единицах эфира, а затем умножить на 1 ether, чтобы привести к нужным единицам".    Но есть нюанс. Каждый вызов библиотеки (а их тут два), будет получать два uint и возвращать также uint, а это, в свою очередь, означает, что дробная часть первой операции совершенно легитимно будет отброшена.  Таким образом, присылая не целое число эфиров в этот crowdsale-контракт, инвестор будет терять токены, а ICO может собрать больше, чем ожидалось :D Полный контракт можно найти в solidity_tricks. За таким длинным названием скрывается забавная уязвимость, обнаруженная при аудите контрактов PoA network. По правилам сети, в ней есть 12 или более валидаторов, которые могут проводить различные голосования, в том числе, на смену ключа (и, соответственно, адреса) валидатора. Для того, чтобы валидатор не смог сменить ключ и проголосовать дважды, смарт-контракт ведет историю всех ключей. И при валидации голоса проверяет, что среди проголосовавших нет его предка. Итак, каждый раз, когда ключ меняется, он помещается в mapping, где ссылается на предыдущий ключ. Поэтому при каждой новой смене у контракта есть возможность пройтись по истории ключей. Однако в такой конфигурации, без дополнительных проверок, валидатор может зациклить историю ключей и обрезать тем самым старые ключи: 1) Валидатор с ключом A регистирует голосование X, затем запрашивает смену ключа. После этого он имеет на руках ключ B. Если он прямо сейчас попробует проголосовать своим новым ключом, то потерпит неудачу, поскольку ключ А есть в истории B:
History(B): B => A => 0x 2) Поэтому валидатор запрашивает смену ключа снова, получает ключ C. Опять же, прямо сейчас трюк не пройдет по той же причине:
History(C): C => B => A => 0x 3) Тогда валидатор запрашивает смену ключа С на ключ В. После этого история ключей зацикливается между B и С, и не содержит А:
History(B): B => C => B => C => B => ... Теперь валидатор может использовать ключ B или С для того, чтобы проголосовать в голосовании Х второй раз. Фикс и оригинал отчета, а также другие уязвимости. Прямо сейчас у вас могут резонно возникнуть два вопроса: В любом случае, размер цикла будет не более 256 повторений из-за того, что переменная i определена как uint8. Реальная возможность эксплуатации данной уязвимости вызывает вопросы у автора, однако, она все же будет полезна тем, кто соберется хранить однонаправленный список в mapping после того, как узнает в чате ли на stackoverflow, что массивы — это дорого :) Следующая уязвимость относится, скорее, к незнанию/непониманию значений глобальных переменных. Предлагаем самостоятельно взглянуть на одну из возможных имплементаций схемы commit-reveal: Обратили внимание на то, что смарт-контракт возвращает потраченый газ при коммите очередной части seed (см. функцию put)? Само по себе желание вернуть потраченную коммисию не вписывается в парадигму платформы Ethereum, но это еще не самое плохое. Уязвимость здесь в том, что значение msg.gas контролируется отправителем и означает оставшийся газ. Таким образом атакующий, манипулируя газом транзации и его ценой, может вывести все средства из контракта.  В этой статье мы рассмотрели лишь несколько логических уязвимостей для того, чтобы сформировать у читателя интуицию относительно мест, где можно ошибиться при написании смарт-контрактов. На самом деле, таких логических (авторских) уязвимостей в контрактах больше всего. Связаны они, прежде всего, с бизнес-логикой или предметной областью. Также это говорит о том, что большинство уязвимостей в контрактах не может быть обнаружено автоматическими средствами, по-крайней мере, пока они не начнут позволять пользователю описывать критерии "неправильного поведения". Кстати, в следующей части мы рассмотрим, какие инструменты все же существуют, и на что они годятся в их текущем состоянии. P.S. Выражаю благодарность Raz0r за пример Generous refund :) Pentester, Security researcher. @_p4lex