Уязвимости в смарт-контрактах: Часть 2
Оригинал — https://github.com/kadenzipfel/smart-contract-vulnerabilities
Канал — https://t.me/jetix37eth
- Неправильное имя конструктора
- Переопределение переменных
- Слабые источники случайности
- Отсутствие защиты от атак с повторным воспроизведением подписей
- Нарушение требований
- Запись в произвольное место хранения
- Неправильный порядок наследования
- Произвольный переход функциональной переменной
- Наличие неиспользуемых переменных
- Неожиданный баланс эфира
- Незашифрованные секреты
- Обнаружение неисправного контракта
- Очищенная зависимость от блокчейна
- Несоответствие стандартам
- Незащищенный Callback
- Определение типа адреса на основе размера кода
- Зависимость от очереди транзакций
- DoS с ограничением расхода газа в блоке
- DoS с (неожиданным) возвратом
Неправильное имя конструктора
До Solidity 0.4.22 единственным способом определить конструктор было создание функции с именем контракта. В некоторых случаях это было проблематично. Например, если смарт-контракт повторно используется с другим именем, но функция конструктора также не изменяется, он просто становится обычной вызываемой функцией.
Теперь, с современными версиями Solidity, вы можете определить конструктор с помощью ключевого слова constructor, устраняя эту уязвимость. Таким образом, решение этой проблемы заключается просто в использовании современных версий компилятора Solidity.
Переопределение переменных
Можно дважды использовать одну и ту же переменную в Solidity, но это может привести к непреднамеренным побочным эффектам. Это особенно сложно при работе с несколькими контрактами. Например:
Здесь мы можем видеть, что SubContract наследует SuperContract, а переменная a определяется дважды с разными значениями. Теперь предположим, что мы используем a для выполнения некоторой функции в SubContract, функциональность, унаследованная от SuperContract, больше не будет работать, поскольку значение a было изменено.
Чтобы избежать этой уязвимости, важно, чтобы мы проверили всю систему смарт-контрактов на наличие двусмысленностей. Также важно проверить наличие предупреждений компилятора, поскольку они могут отмечать эти двусмысленности, пока они есть в смарт-контракте.
Слабые источники случайности
В Ethereum существуют определенные приложения, которые полагаются на генерацию случайных чисел для обеспечения справедливости. Однако генерация случайных чисел в Ethereum очень сложна, и есть несколько подводных камней, которые стоит рассмотреть.
Использование атрибутов цепочки, таких как: block.timestamp, block.hash и block.difficulty, может показаться хорошей идеей, поскольку они часто выдают псевдослучайные значения. Проблема, однако, заключается в способности майнера изменять эти значения.
Например, в приложении для азартных игр с джекпотом в несколько миллионов долларов у майнера есть достаточный стимул генерировать множество альтернативных блоков, выбирая только тот блок, который принесет майнеру джекпот. Конечно, подобный контроль над блокчейном сопряжен со значительными затратами, но если ставки достаточно высоки, это, безусловно, можно сделать.
Чтобы избежать манипуляций майнера при генерации случайных чисел, существует несколько решений:
Отсутствие защиты от атак с повторным воспроизведением подписей
Иногда в смарт-контрактах необходимо выполнить проверку подписи, чтобы улучшить удобство использования и снизить стоимость газа. Однако при внедрении проверки подписи необходимо проверять хеши. Для защиты от атак с повторным использованием сигнатур контракт должен разрешать обработку только новых хэшей. Это предотвращает многократную замену подписи другого пользователя злоумышленниками.
Чтобы быть в большей безопасности при проверке подписи, следуйте этим рекомендациям:
Нарушение требований
Метод require() предназначен для проверки условий, таких как входные данные или переменные состояния контракта, или для проверки возвращаемых значений из внешних вызовов контракта.
Для проверки внешних вызовов входные данные могут предоставляться вызывающими абонентами или они могут быть возвращены вызываемым. В случае, если возвращаемое значение вызываемого абонента нарушило ввод, скорее всего, одна из двух вещей пошла не так:
- Существует ошибка в контракте, который предоставил входные данные.
- Условие требования слишком строгое.
Чтобы решить эту проблему, сначала подумайте, не является ли условие требования слишком строгим. При необходимости ослабьте его, чтобы разрешить любой допустимый внешний ввод.
Если проблема не в обязательном условии, то в контракте, предоставляющем внешний ввод, должна быть ошибка. Убедитесь, что этот контракт не предоставляет недопустимые входные данные.
Запись в произвольное место хранения
Только авторизованные адреса должны иметь доступ для записи в важные хранилища. Если на протяжении всего контракта не проводится надлежащая проверка авторизации, злоумышленник может перезаписать важные данные.
Однако, даже если существуют проверки авторизации для записи важных данных данных, злоумышленник все равно может перезаписать их с помощью нечувствительных данных. Это может дать злоумышленнику доступ к перезаписи важных переменных, таких как владелец контракта.
Чтобы предотвратить это, надо не только защитить хранилища конфиденциальных данных с помощью требований авторизации, но и гарантировать, что записи в одну структуру данных не могут непреднамеренно перезаписывать записи в другой структуре данных.
Неправильный порядок наследования
В Solidity возможно наследование из нескольких источников, что при неправильном понимании может привести к двусмысленности. Эта двусмысленность известна как проблема бриллианта, в которой, если два базовых контракта выполняют одну и ту же функцию, какому из них следует отдать приоритет? К счастью, Solidity изящно справляется с этой проблемой, то есть до тех пор, пока разработчик понимает решение.
Решение, которое Solidity обеспечивает для проблемы алмаза, заключается в использовании обратной C3-линеаризации. Это означает, что он будет линеаризовать наследование справа налево, поэтому порядок наследования имеет значение. Предлагается начинать с более общих контрактов и заканчивать более конкретными контрактами, чтобы избежать проблем.
Произвольный переход функциональной переменной
Функциональные типы поддерживаются в Solidity. Это означает, что переменная типа function может быть назначена функции с соответствующей сигнатурой. Затем функция может быть вызвана из переменной точно так же, как и любая другая функция. Пользователи не должны иметь возможности изменять функциональную переменную, но в некоторых случаях это возможно.
Если смарт-контракт использует определенные инструкции по сборке, например, mstore, злоумышленник может указать функциональную переменную на любую другую функцию. Это может дать злоумышленнику возможность нарушить функциональность контракта и, возможно, даже истощить средства контракта.
Наличие неиспользуемых переменных
Хоть это и разрешено, лучше всего избегать неиспользуемых переменных. Неиспользуемые переменные могут привести к нескольким различным проблемам:
- Увеличение объема вычислений (ненужное потребление газа).
- Указание на ошибки или искаженные структуры данных
- Сниженная читаемость кода
Настоятельно рекомендуется удалить все неиспользуемые переменные из кода.
Неожиданный баланс эфира
Если контракт для работы предполагает определенный баланс, он уязвим для атаки.
Допустим, у нас есть контракт, который предотвращает выполнение всех функций, если в контракте хранится какой-либо эфир. Если злоумышленник решит воспользоваться путем принудительной отправки эфира, он вызовет DoS, что сделает контракт непригодным для использования.
По этой причине важно никогда не использовать строгие проверки равенства для баланса эфира в контракте.
Незашифрованные секреты
Код смарт-контракта Ethereum всегда можно прочитать. Даже если ваш код не проверен на Etherscan, злоумышленники все равно могут декомпилировать или даже просто проверять транзакции к нему и из него, чтобы проанализировать его.
Одним из примеров проблемы здесь может быть "игра в угадайку", в которой пользователь должен угадать сохраненную закрытую переменную, чтобы выиграть эфир в контракте. Это, конечно, чрезвычайно тривиально для использования (до такой степени, что вам не стоит пробовать это, потому что это почти наверняка контракт honeypot, который намного сложнее).
Другой распространенной проблемой здесь является использование незашифрованных секретов вне цепочки, таких как ключи API и вызовы оракулов. Если ваш ключ API может быть определен, злоумышленники могут либо просто использовать его для себя, либо воспользоваться другими преимуществами, такими как исчерпание разрешенных вами вызовов API и принудительное возвращение оракулом страницы с ошибкой, которая может привести или не привести к проблемам в зависимости от структуры контракта.
Обнаружение неисправного контракта
Некоторые контракты не хотят, чтобы другие контракты взаимодействовали с ними (очень распространено в контрактах азартных игр, использующих низкокачественный рандом).
Распространенный способ предотвратить это - проверить, хранится ли в вызывающей учетной записи какой-либо код. Однако учетные записи контрактов, инициирующие вызовы во время их создания, еще не будут показывать, что они хранят код, эффективно обходя обнаружение контракта.
Очищенная зависимость от блокчейна
Многие контракты полагаются на вызовы, происходящие в течение определенного периода времени, но Ethereum можно заспамить транзакциями с очень высоким Gwei в течение приличного периода времени относительно дешево.
Например, FOMO3D (игра с обратным отсчетом, в которой последний инвестор выигрывает джекпот, но каждая инвестиция добавляет время к обратному отсчету) выиграл пользователь, который полностью заблокировал блокчейн на небольшой промежуток времени, запретив другим инвестировать, пока не истечет таймер и он не выиграет.
В настоящее время существует множество контрактов на азартные игры с участием "крупье", которые полагаются на хэши прошлых блоков для обеспечения рандома. По большей части это не самый плохой источник рандома, и они даже учитывают сокращение хэшей, которое происходит после 256 блоков, но в этот момент многие из них просто обнуляют ставку.
Это позволило бы кому-то делать ставки на многие из этих аналогично функционирующих контрактов с определенным результатом в качестве победителя по всем из них, проверять подачу крупье, пока она еще находится на рассмотрении, и, если она неблагоприятна, просто блокировать блокчейн до тех пор, пока не произойдет обрезка, и вы сможете вернуть свои ставки.
Несоответствие стандартам
С точки зрения разработки смарт-контрактов важно следовать стандартам. Стандарты установлены для предотвращения уязвимостей, и игнорирование их может привести к неожиданным последствиям.
Возьмем, к примеру, оригинальный токен BNB от Binance. Он продавался как токен ERC20, но позже было указано, что на самом деле он не соответствует стандарту ERC20 по нескольким причинам:
- Он блокирует отправку на 0x0
- Он блокирует пустые транзакции
- Он не возвращает true или false после транзакции
Основная причина для беспокойства по поводу этой неправильной реализации заключается в том, что если он используется со смарт-контрактом, который ожидает токен ERC-20, она будет вести себя неожиданным образом. Он может даже застрять в контракте навсегда.
Хотя стандарты не всегда идеальны и могут когда-нибудь устареть, они способствуют созданию наиболее безопасных смарт-контрактов.
Незащищенный Callback
При написании или взаимодействии с callback-функциями в Solidity важно убедиться, что они не могут быть использованы для выполнения неожиданных эффектов.
Давайте посмотрим на функцию ERC721._safeMint от OpenZeppelin:
Функция называется _safeMint, потому что она предотвращает непреднамеренный минт токенов к контракту, сначала проверяя, реализован ли в этом контракте ERC721Receiver, т.е. помечая себя как добровольного получателя NFT.
Все это кажется прекрасным, но поскольку _checkOnERC721Received является callback-функцией, контракт получателя может определять любую произвольную логику для выполнения, включая повторный вход в исходную функцию mint, тем самым обходя ограничения, определенные в коде контракта. Например, в этой функции:
Поскольку у нас есть доступ к незащищенному callback, мы можем повторно войти в mint после того, как будет отчеканен только один токен, и отчеканить дополнительные MAX_PER_USER - 1 токенов следующим образом:
Чтобы устранить уязвимость, мы можем использовать функцию ERC721._mint или защиту от повторного входа.
Определение типа адреса на основе размера кода
Распространенным методом определения того, является ли отправитель контрактом или кошельком, была проверка размера кода отправителя. Эта проверка утверждает, что если отправитель имеет размер кода > 0, то это должен быть контракт, а если нет, то это должен быть кошелек. Например:
Однако недавно было обнаружено, что это утверждение неверно и может быть игнорировано. По этой причине важно, чтобы логика вашего смарт-контракта не утверждала, что адрес отправителя является кошельком просто потому, что размер кода равен 0.
Зависимость от очереди транзакций
Транзакции в Ethereum сгруппированы в блоки, которые обрабатываются с полурегулярным интервалом ~15 секунд. Прежде чем транзакции будут размещены в блоках, они передаются в мемпул, где создатели блоков затем могут приступить к их размещению в соответствии с экономически оптимальным вариантом. Что здесь важно понимать, так это то, что мемпул является общедоступным, и, следовательно, любой может видеть транзакции до их выполнения, что дает ему возможность запускать их заранее, размещая свою собственную транзакцию, выполняющую то же самое или аналогичное действие с более высокой ценой на газ.
Фронтраннинг стал настолько распространенным в результате того, что все более распространенными становятся боты-фронтраннеры, которые работают, наблюдая за мемпулом в поисках прибыльных, воспроизводимых транзакций, которые они могут заменить для собственной выгоды.
Одним из решений зависимости от порядка транзакций является использование схемы commit-reveal в случае передачи информации в блокчейне. Это работает за счет того, что отправитель отправляет хэш информации, сохраняя ее в цепочке вместе с адресом пользователя, чтобы позже они могли раскрыть ответ вместе с солью, чтобы доказать, что они действительно были правильными. Другое решение - просто использовать частный мемпул.
DoS с ограничением расхода газа в блоке
Одним из основных преимуществ лимита газа в блоке является то, что оно не позволяет злоумышленникам создавать бесконечный цикл транзакций. Если использование газа в транзакции превысит этот предел, транзакция завершится неудачей. Однако наряду с этим преимуществом возникает побочный эффект, который важно понимать.
Неограниченные операции
Примером, в котором лимит газа в блоке может быть проблемой, является выполнение логики в неограниченном цикле. Даже без какого-либо злого умысла это может легко пойти не так. Например, наличие слишком большого числа пользователей для отправки средств может превысить лимит газа и помешать успешной транзакции, потенциально навсегда заблокировав средства.
Эта ситуация также может привести к атаке. Допустим, плохой человек решает создать значительное количество адресов, при этом каждому адресу выплачивается небольшая сумма средств из смарт-контракта. Если все сделано эффективно, транзакция может быть заблокирована на неопределенный срок, возможно, даже предотвратив проведение дальнейших транзакций.
Эффективным решением этой проблемы было бы использование pull-платежной системы поверх вышеупомянутой push-платежной системы. Чтобы сделать это, выделите каждый платеж в отдельную транзакцию и попросите получателя вызвать функцию.
Если по какой-то причине вам действительно нужно перебирать массив неопределенной длины, по крайней мере, ожидайте, что он потенциально займет несколько блоков, и разрешите выполнять его в нескольких транзакциях - как показано в этом примере:
Набивание блока
В некоторых ситуациях ваш контракт может быть атакован с помощью лимита газа в блоке, даже если вы не выполняете цикл по массиву неопределенной длины. Злоумышленник может заполнить несколько блоков, прежде чем транзакция сможет быть обработана, используя достаточно высокую цену на газ.
Эта атака осуществляется путем проведения нескольких транзакций по очень высокой цене на газ. Если цена на газ достаточно высока, а транзакции потребляют много газа, они могут заполнить целые блоки и помешать обработке других транзакций.
Транзакции Ethereum требуют, чтобы отправитель платил газ, чтобы предотвратить спам-атаки, но в некоторых ситуациях может быть достаточно стимулов для проведения такой атаки. Например, атака с набиванием блоков была использована в приложении для азартных игр Fomo3D.
В приложении был таймер обратного отсчета, и пользователи могли выиграть джекпот, купив ключ последними, за исключением того, что каждый раз, когда пользователь покупал ключ, таймер продлевался. Злоумышленник купил ключ, а затем набил следующие 13 блоков подряд, чтобы выиграть джекпот.
Чтобы предотвратить возникновение таких атак, важно тщательно продумать, безопасно ли включать действия, основанные на времени, в ваше приложение.
DoS с (неожиданным) возвратом
DoS-атаки (отказ в обслуживании) могут возникать в функциях, когда вы пытаетесь отправить средства пользователю, и функциональность зависит от того, что перевод средств будет успешным.
Это может быть проблематично в случае, если средства отправляются на смарт-контракт, созданный злоумышленником, поскольку они могут просто создать fallback-функцию, которая отменяет все платежи.
Как вы можете видеть в этом примере, если злоумышленник делает ставки по смарт-контракту с fallback-функцией, возвращающей все платежи, они никогда не могут быть возвращены, и, следовательно, никто никогда не сможет сделать более высокую ставку.
Это также может быть проблематично без присутствия злоумышленника. Например, вы можете захотеть оплатить массив пользователей путем итерации по массиву, и, конечно, вы хотели бы убедиться, что каждому пользователю должным образом оплачено. Проблема здесь в том, что если один платеж не выполняется, функция отменяется и никому не выплачивается.
Эффективным решением этой проблемы было бы использование pull-платежной системы поверх вышеупомянутой push-платежной системы. Чтобы сделать это, выделите каждый платеж в отдельную транзакцию и попросите получателя вызвать функцию.