Author

Topic: Атаки и Уязвимости Смарт-Контракта (Read 160 times)

jr. member
Activity: 46
Merit: 3
это все интересно но как мне кажется этот топик должен быть в сете или в технической части форума.
к альтернативным криптовалютам он не имеет отношения.

Возможно) но решение разместить топик в этой ветке - не совсем мое

The Encyclopedia of Smart Contract Attacks and Vulnerabilities

С разделом не определился,куда лучше такое опубликовать, надеюсь,xandry подскажет.
Там статья специфичная для смарт-контрактов на Ethereum, поэтому либо в тему по эфиру, либо отдельной темой в альтернативных криптовалютах можно оформить. В первом варианте, предполагаю, что читателей будет больше всё же.

Все с темой однозначно как 2*2, тема должна быть в кодерах.
Мерит бы дал, да где его взять.
sr. member
Activity: 826
Merit: 262
это все интересно но как мне кажется этот топик должен быть в сете или в технической части форума.
к альтернативным криптовалютам он не имеет отношения.

Возможно) но решение разместить топик в этой ветке - не совсем мое

The Encyclopedia of Smart Contract Attacks and Vulnerabilities

С разделом не определился,куда лучше такое опубликовать, надеюсь,xandry подскажет.
Там статья специфичная для смарт-контрактов на Ethereum, поэтому либо в тему по эфиру, либо отдельной темой в альтернативных криптовалютах можно оформить. В первом варианте, предполагаю, что читателей будет больше всё же.

принял и понял сорри за недопонимание.
в любом случае спасибо за интересную статью. на форуме очень редко такие качественные топики находишь. добавил тебе мерита так держать.
member
Activity: 63
Merit: 127
это все интересно но как мне кажется этот топик должен быть в сете или в технической части форума.
к альтернативным криптовалютам он не имеет отношения.

Возможно) но решение разместить топик в этой ветке - не совсем мое

The Encyclopedia of Smart Contract Attacks and Vulnerabilities

С разделом не определился,куда лучше такое опубликовать, надеюсь,xandry подскажет.
Там статья специфичная для смарт-контрактов на Ethereum, поэтому либо в тему по эфиру, либо отдельной темой в альтернативных криптовалютах можно оформить. В первом варианте, предполагаю, что читателей будет больше всё же.
sr. member
Activity: 826
Merit: 262
это все интересно но как мне кажется этот топик должен быть в сете или в технической части форума.
к альтернативным криптовалютам он не имеет отношения.
member
Activity: 63
Merit: 127
Наличие неиспользуемых переменных

Хотя это допустимо, рекомендуется избегать неиспользуемых переменных. Неиспользуемые переменные могут привести к нескольким проблемам:

  • Увеличение вычислений (ненужное потребление газа);
  • Индикация ошибок или искаженных структур данных;
  • Снижение читаемости кода.

Настоятельно рекомендуется удалить все неиспользуемые переменные из базы кода.

Неожидаемый баланс Эфира

Поскольку всегда можно отправить Эфир в контракт - смотрите “Принудительная отправка Эфира в контракт " - если контракт предполагает конкретный баланс, он уязвим для атаки.

Скажем, у нас есть контракт, который предотвращает выполнение всех функций, если в контракте хранится какой-либо Эфир. Если злоумышленник решит использовать это, принудительно отправив Эфир, они вызовут DoS-атаку, что сделает контракт непригодным для использования. По этой причине важно никогда не использовать строгие проверки равенства для баланса Эфира в контракте.

Незашифрованные секретные данные

Код смарт-контракта Ethereum всегда можно прочитать. Относитесь к этому как к данности. Даже если ваш код не верифицирован на Etherscan, злоумышленники все равно могут декомпилировать или даже просто проверить транзакции, чтобы проанализировать его.

Примером проблемы здесь могла бы быть игра в загадки, где пользователь должен угадать сохраненную частную переменную, чтобы выиграть эфир в контракте. Это, конечно, чрезвычайно тривиально для злоупотребления (до такой степени, что вы не должны пытаться это сделать, потому что это почти наверняка будет контракт-приманка, который окажется намного хитрее).

Еще одна распространенная проблема тут - это использование незашифрованных секретных данных вне цепочки, таких как ключи API с вызовами Oracle. Если ваш ключ API может быть определен, злоумышленники могут либо просто использовать его в своих целях, либо воспользоваться другими направлениями атаки, такими как использование всего доступного количества обращений к API и принуждение Oracle возвращать страницу ошибок, которая может или не может привести к проблемам, в зависимости от структуры контракта.

Выявление неисправного контракта

Некоторые контракты не хотят, чтобы другие контракты взаимодействовали с ними. Распространенный способ предотвратить это - проверить, есть ли в вызывающей учетной записи какой-либо хранящийся код. Однако учетные записи контрактов, инициирующие вызовы во время их конструирования, еще не показывают, что они хранят код, обходя обнаружение контракта.

Засоренная опора на блокчейн

Многие контракты рассчитаны на вызовы, происходящие в течение определенного периода времени, но Ethereum может быть заспамлен очень большим количеством Gwei-транзакций за скромное количество времени относительно дешево.

Например, Fomo3D (игра с обратным отсчетом, в которой последний инвестор выигрывает джекпот, но каждая инвестиция добавляет время к обратному отсчету) был выигран пользователем, который полностью засорил блокчейн на небольшой период времени, запрещая другим инвестировать, пока таймер не закончится, и он выиграл (смотрите “DoS-атака с ограничением лимита газа”).

В настоящее время существует много контрактов выполняющих роль крупье азартных игр, которые полагаются на предыдущие блок-хеши для обеспечения Генерации Случайного Числа (ГСЧ). Это не худший источник ГСЧ по большей части, и они даже учитывают обрезку хэшей, которая происходит после 256 блоков. Но в этот момент многие из них просто обнуляют ставку. Это позволит кому-то делать ставки для большого количества одинаково функционирующих контрактов на определенный результат на победу для них всех, затем проверять результат крупье, пока он все еще находится на рассмотрении, и, если результат неблагоприятен, просто забивать блокчейн, пока не произойдет обрезка, и тогда они смогут вернуть свои ставки.

Нестрогое следование стандартам

В разработке смарт-контрактов важно следовать стандартам. Стандарты устанавливаются для предотвращения уязвимостей, и их игнорирование может привести к неожиданным последствиям.

Возьмем, к примеру, оригинальный токен BNB от Binance. Он продавался на рынке как токен ERC20, но позже было указано, что он на самом деле не соответствует стандартам ERC-20 по нескольким причинам:

  • Он не позволяет отправку на 0x0;
  • Он блокирует передачу значения 0;
  • Он не возвращает true или false при успехе или «завале» программы.

Основная причина для беспокойства по поводу этой неправильной реализации заключается в том, что если он используется со смарт-контрактом, который ожидает токен ERC-20, он будет вести себя неожидаемым образом. Он даже может быть заблокирован в контракте навсегда.

Хотя стандарты не всегда совершенны и могут со временем устаревать, они способствуют большей безопасности смарт-контрактов.

.     .     .

Вывод

Как вы можете видеть, существует множество способов, с помощью которых ваши смарт-контракты могут быть злонамеренно использованы. Очень важно, чтобы вы полностью понимали каждый вектор атаки и все уязвимостьи перед созданием контракта.

Особая благодарность RobertMCForster за значительный вклад.

.     .     .

Дополнительная информация

https://github.com/ethereum/wiki/wiki/Safety
https://swcregistry.io/
https://eprint.iacr.org/2016/1007.pdf
https://www.dasp.co/
https://consensys.github.io/smart-contract-best-practices/
https://github.com/sigp/solidity-security-blog
https://solidity.readthedocs.io/en/latest/bugs.html

#Ethereum #SmartContract #Security #Blockchain #Programming
member
Activity: 63
Merit: 127
Дефолтная видимость переменной состояния

Обычно разработчики явно объявляют видимость функции, но не так часто объявляют видимость переменной. Основные переменные могут иметь один из трех идентификаторов видимости: public, internal или private. К счастью, видимость по умолчанию для переменных является внутренней (internal), а не общей (public), но даже если вы собираетесь объявить переменную как внутреннюю, важно быть однозначным, чтобы не было неверных предположений о том, кто может получить доступ к переменной.

Неинициализированный указатель памяти

Данные хранятся в Виртуальной Машине Ethereum в storage, memory или calldata. Важно, чтобы они были правильно интерпретированы и корректно инициализированы. Неправильная инициализация указателей хранилища данных или просто их неинициализация может привести к возникновению уязвимостей контрактов.

Начиная с Solidity 0.5.0, неинициализированные указатели хранилища больше не являются проблемой, так как контракты с неинициализированными указателями хранилища больше не будут компилироваться. При этом по-прежнему важно понимать, какие указатели хранилища вы должны использовать в определенных ситуациях.

Утверждение нарушения

В Solidity 0.4.10 были созданы следующие функции: assert(), require() и revert(). Здесь мы обсудим функцию утверждения (assert) и как ее использовать.

Официально сказано, что функция assert() предназначена для утверждения инвариантов; неофициально сказано, что assert() является чрезмерно напористым телохранителем, который защищает ваш контракт, но крадет ваш газ в процессе этого. Правильно функционирующие контракты никогда не должны достигать «заваленного» значения assert. Если вы достигли «заваленного» значения assert, вы либо неправильно использовали функцию assert(), либо в вашем контракте есть ошибка, которая переводит его в недопустимое состояние.

Если условие, проверенное в assert(), на самом деле не является инвариантом, предлагается заменить его оператором require().

Использование устаревших функций

Время идет и функции Solidity устаревают и заменяются функциями получше. Важно не использовать устаревшие функции, так как это может привести к неожиданным эффектам и ошибкам компиляции.

Вот список устаревших функций и альтернатив. Многие альтернативы являются просто аналогами с другим именем и не нарушают текущего поведения, если используются в качестве замены.


Устаревшие функции и альтернативы

Delegatecall к ненадежному вызываемому

Delegatecall - это отдельный вариант вызова сообщения. Он почти идентичен обычному вызову сообщения, за исключением того, что целевой адрес выполняется в контексте вызывающего контракта и msg.sender и msg.value остается прежним. По существу, delegatecall делегирует другим контрактам возможность изменить «stotage» вызывающего контракта.

Поскольку delegatecall дает так много контроля над контрактом, очень важно использовать его только с доверенными контрактами, например, вашими собственными. Если целевой адрес задан пользовательским вводом, убедитесь, что это контракт, которому можно доверять.

Гибкость подписи

Часто люди полагают, что использование криптографической системы подписи в смарт-контрактах само по себе предполагает, что подписи уникальны; однако это не так. Подписи в Ethereum могут быть изменены без закрытого ключа и оставаться действительными. Например, криптография с эллиптическим ключом состоит из трех переменных — v, r и s — и если эти значения изменены правильным образом, вы можете получить действующую подпись с недействующим закрытым ключом.

Чтобы избежать проблемы гибкости подписи, никогда не используйте подпись в хэше подписанного сообщения для проверки, были ли ранее подписанные сообщения обработаны контрактом, потому что злоумышленники могут найти вашу подпись и воссоздать ее.

Неверное имя конструктора

До версии Solidity 0.4.22 единственным способом определения конструктора было создание функции с именем контракта. В некоторых случаях это вызвать проблему. Например, если смарт-контракт повторно используется с другим именем, но функция конструктора не изменяется, он просто становится обычной вызываемой функцией.

В современных версиях Solidity вы можете определить конструктор с помощью ключевого слова constructor, устраняя эту уязвимость. Таким образом, решение этой проблемы заключается просто в использовании современных версий компилятора Solidity.

Затенение переменных состояния

В Solidity есть возможность использовать одну переменную дважды, но это может привести к нежелательным последствиям. Сложность может возникнуть особенно при работе с составными контрактами. Возьмем следующий пример:

Code:
contract SuperContract {
  uint a = 1;
}

contract SubContract is SuperContract {
  uint a = 2;
}
Пример затенения переменных состояния

Здесь мы видим, что SubContract наследует от SuperContract, и переменная «a» определяется дважды с разными значениями. Теперь, скажем, мы используем «a» для выполнения некоторой функции в SubContract. Функциональность, унаследованная от SuperContract, больше не будет работать, так как значение a было изменено.

Чтобы избежать этой уязвимости, важно проверить всю систему смарт-контрактов на наличие двусмысленностей. Также важно проверить наличие предупреждений компилятора, поскольку они могут отмечать эти двусмысленности, пока они находятся в смарт-контракте.

Слабые источники случайных событий от атрибутов цепи

В Ethereum существуют определенные приложения, которые полагаются на генерацию случайных чисел для беспристрастности. Тем не менее, генерация случайных чисел очень сложна в Ethereum, и есть несколько подводных камней, которые стоит рассмотреть.

Использование атрибутов цепи, таких как block.timestamp, blockhash и block.difficulty может показаться хорошей идеей, поскольку они часто производят псевдослучайные значения. Проблема, однако, заключается в способности майнера изменять эти значения. Например, в игорном приложении с многомиллионным джекпотом есть достаточный стимул для майнера генерировать много альтернативных блоков, просто выбирая блок, который приведет майнера к джекпоту. Конечно, это связано с существенными затратами на контроль блокчейна, но если ставки достаточно высоки, это, безусловно, можно сделать.

Чтобы избежать манипуляций майнера при генерации случайных чисел, есть несколько решений:

  • Придерживаться схемы, такой как RANDAO или DAO, где случайное число генерируется всеми участниками DAO;
  • Внешние источники через оракулы - например, Oraclize;
  • Использовать хэши блоков Биткоина, поскольку сеть более децентрализована, а блоки дороже добывать.

Отсутствие защиты от атак повторного воспроизведения

Иногда в смарт-контрактах необходимо выполнить проверку подписи, чтобы улучшить удобство использования и стоимость газа. Однако, при осуществлении проверки подписи необходимо принимать во внимание следующее.

Для защиты от атак, связанных с повторным воспроизведением подписей, контракт должен разрешать обработку только новых хэшей. Это предотвращает многократное воспроизведение подписи другого пользователя злоумышленниками.

Чтобы обеспечить дополнительную безопасность при проверке подписи, следуйте этим рекомендациям:

  • Сохраняйте каждый хэш сообщения, обработанный контрактом — а затем проверяйте хэши сообщений на существующие перед выполнением функции;
  • Включайте адрес контракта в хэш, чтобы убедиться, что сообщение используется только в одном контракте;
  • Никогда не создавайте хэш сообщения, включая подпись. Смотрите “Гибкость подписи”.

Нарушение требований

Метод require() предназначен для проверки условий, таких как входные данные или переменные состояния контракта, или для проверки возвращаемых значений из внешних вызовов контракта. Для проверки внешних вызовов входы могут быть предоставлены «вызывающими» или возвращены «вызываемыми». В случае, если нарушение ввода произошло возвращаемым значением «вызываемым», скорее всего, одна из двух вещей пошла не так:

  • В контракте есть ошибка, которая обеспечивала ввод;
  • Условие требования слишком строгое.

Чтобы решить эту проблему, сначала подумайте, является ли условие требования слишком строгим. При необходимости ослабьте его требования, чтобы разрешить другой допустимый внешний ввод. Если проблема не в условии требования, то в контракте, предоставляющем внешний ввод, должна быть ошибка. Убедитесь, что этот контракт не предоставляет недопустимые входные данные.

Запись в произвольно выбранное место хранения

Только авторизованные адреса должны иметь доступ для записи в конфиденциальные места хранения. Если во всем контракте нет надлежащих проверок авторизации, злоумышленник может перезаписать конфиденциальные данные. Однако даже при наличии проверок авторизации для записи в конфиденциальные данные злоумышленник все равно может перезаписать конфиденциальные данные с помощью неконфиденциальных данных. Это может дать злоумышленнику доступ к перезаписи важных переменных, таких как владелец контракта.

Чтобы предотвратить это, мы хотим не только защитить конфиденциальные хранилища данных с помощью требований авторизации, но также хотим гарантировать, что записи одной структуры данных не могут случайно перезаписать записи другой структуры данных.

Неправильный порядок наследования

В Solidity существует возможность наследования из нескольких источников, которая, если будет понята неправильно, может привести к двусмысленности. Эта двусмысленность известна как «Diamond problem»: если два базовых контракта имеют одну и ту же функцию, какой из них должен быть приоритетным? К счастью, Solidity справляется с этой проблемой - до тех пор, пока разработчик понимает решение.

Решение, которое Solidity предлагает для «Diamond problem», обеспечивается с помощью обратной С3-линеаризации. Это означает, что наследование будет линеаризовываться справа налево и порядок будет иметь значение. Предлагается начать с более общих контрактов и закончить более конкретными контрактами, чтобы избежать проблем.

Произвольный скачек с переменной типа «function»

Типы «функция» поддерживаются в Solidity. Это означает, что переменной типа function может быть задана функция с совпадающей подписью. Функция может быть вызвана из переменной так же, как и любая другая функция. Пользователи не должны иметь возможность изменять переменную типа function, но в некоторых случаях это возможно.

Если смарт-контракт использует определенные инструкции по сборке, например mstore, злоумышленник может задать в переменную типа function любую другую функцию. Это может дать злоумышленнику возможность нарушить функциональность контракта - и, возможно, даже истощить средства на контракте.

Поскольку встроенная сборка - это способ доступа к EVM (виртуальной машине Ethereum) на низком уровне, она обходит многие важные функции безопасности. Поэтому важно использовать сборку только в том случае, если это необходимо и правильно интерпретировано.
member
Activity: 63
Merit: 127
Реентерабельность

Реентерабельность - это атака, которая может произойти, когда ошибка в функции контракта может позволить функциональному взаимодействию произойти несколько раз, в то время как это должно быть запрещено. При наличии злого умысла, это может быть использовано для отвода средств из смарт-контракта. На самом деле, реентерабельность была вектором атаки, который был использован для взлома DAO.

Одно-функциональная реентерабельность

Одно-функциональная реентерабельная атака происходит, когда уязвимая функция является той же самой функцией, которую злоумышленник пытается рекурсивно вызвать.

Code:
// INSECURE
function withdraw() external {
    uint256 amount = balances[msg.sender];
    require(msg.sender.call.value(amount)());
    balances[msg.sender] = 0;
}
Пример взят Consensys

Здесь мы можем видеть, что баланс изменяется только после перевода средств. Это может позволить хакеру вызывать функцию многократно до тех пор, пока баланс не отобразится равным 0, целиком истощив смарт-контракт.

Кросс-функциональная реентерабельность

Кросс-функциональная реентерабельность - это более сложная версия того же процесса. Кросс-функциональная реентерабельность возникает, когда уязвимая функция используется совместно с функцией, которую может использовать злоумышленник.

Code:
// INSECURE
function transfer(address to, uint amount) external {
  if (balances[msg.sender] >= amount) {
    balances[to] += amount;
    balances[msg.sender] -= amount;
  }
}

function withdraw() external {
  uint256 amount = balances[msg.sender];
  require(msg.sender.call.value(amount)());
  balances[msg.sender] = 0;
}
Пример взят Consensys

В этом примере хакер может использовать этот контракт через вызов transfer() в резервной функции для перевода потраченных средств до тех пор, пока баланс не отразится 0 в функции withdraw().

Предотвращение реентерабельности

При переводе средств в смарт-контракте используйте send или transfer вместо call. Проблема с использованием call в том, что в отличие от других функций, эта не имеет предела газа в 2300. Это означает, что call может быть использована для вызова внешних функций, которые в свою очередь могут быть использованы для осуществления атак, связанных с реентерабельностью.

Еще один надежный превентивный метод - это маркировка ненадежных функций.

Code:
function untrustedWithdraw() public {
  uint256 amount = balances[msg.sender];
  require(msg.sender.call.value(amount)());
  balances[msg.sender] = 0;
}
Пример взят Consensys

Кроме того, для обеспечения оптимальной безопасности используйте шаблон checks-effects-interactions. Это простое правило для упорядочивания функций смарт-контракта.

Функция должна начинаться с checks - например, операторов require и assert.

Затем должны быть выполнены effects контракта - например, модификация состояния.

Наконец, мы можем осуществить interactions с другими смарт-контрактами - например, вызовы внешних функций.

Эта структура эффективна от реентерабельности, потому что изменения в контракте предотвратят осуществление вредоносных взаимодействий злоумышленниками.

Code:
function withdraw() external {
  uint256 amount = balances[msg.sender];
  balances[msg.sender] = 0;
  require(msg.sender.call.value(amount)());
}
Пример взят Consensys

Поскольку баланс устанавливается равным 0 до выполнения каких-либо взаимодействий,  то после первой транзакции, если контракт вызывается рекурсивно, отправлять нечего.

.     .     .

Факторы уязвимости

В этом разделе мы рассмотрим известные уязвимости смарт-контрактов и способы их устранения. Почти все перечисленные здесь уязвимости можно найти в Классификации Слабых Мест Смарт-Контрактов.

Переполнение и опустошение целочисленных типов

В solidity типы integer имеют максимальные значения. Например:

uint8 => 255

uint16 => 65535

uint24 => 16777215

uint256 => (2^256) - 1

Ошибки переполнения и опустошения могут возникать при превышении максимального значения (переполнение) или при переходе ниже минимального значения (опустошение). Когда вы превышаете максимальное значение, вы возвращаетесь к нулю, а когда вы опускаетесь ниже минимального значения, это возвращает вас к максимальному значению.

Так как меньшие целочисленные типы - такие как uint8, uint16 и т. д. - имеют меньшие максимальные значения, легко вызвать их переполнение; таким образом, используйте их с большей осторожностью.

Вероятно, лучшим доступным решением для ошибок переполнения и опустошения является использование библиотеки OpenZeppelin SafeMath при выполнении математических операций.

Зависимость от метки времени

Метка времени блока, доступ к которому осуществляется now или block.timestamp, может быть обработана майнером. При использовании метки времени для выполнения функции контракта следует учитывать три соображения.

Манипуляция меткой времени

Если метка времени была использована для генерации случайного события, майнер может опубликовать метку времени в течение 15 секунд после проверки блока, что дает им возможность задать метке времени значение, которое увеличит их шансы на получение выгоды от функции.

Например, приложение лотереи может использовать метку времени блока для выбора случайного участника конкурса из группы. Майнер может войти в лотерею, а затем изменить метку времени на значение, которое даст им лучшие шансы на победу в лотерее.

Таким образом, временные метки не должны использоваться для создания случайных событий.

Правило 15 секунд

Справочная спецификация Ethereum, «Yellow Paper», не указывает предела, как сильно блоки могут меняться по времени, - просто его временная метка должна быть больше, чем временная метка его родителя. При этом популярные реализации протокола отклоняют блоки с метками времени, которые заходят более чем на 15 секунд в будущее, поэтому, пока ваше зависящее от времени событие может безопасно изменяться в течение 15 секунд, использовать метку времени блока - безопасно.

Не используйте block.number в качестве метки времени

Вы можете оценить разницу во времени между событиями, используя block.number и среднее время блока. Но временные значения блоков могут меняться и нарушать функциональные возможности, поэтому лучше избегать их использования.

Авторизация через tx.origin

tx.origin это глобальная переменная в Solidity, которая возвращает адрес, отправивший транзакцию. Важно, чтоб вы никогда не использовали tx.origin для авторизации, так как другой контракт может использовать резервную функцию для вызова вашего контракта и получения авторизации, потому что уже использованный для авторизации адрес хранится в tx.origin. Рассмотрим следующий пример:

Code:
pragma solidity >=0.5.0 <0.7.0;

// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
    address owner;

    constructor() public {
        owner = msg.sender;
    }

    function transferTo(address payable dest, uint amount) public {
        require(tx.origin == owner);
        dest.transfer(amount);
    }
}
Пример взят Solidity docs

Здесь мы видим, что контракт TxUserWallet инициирует функцию transferTo() с tx.origin.

Code:
pragma solidity >=0.5.0 <0.7.0;

interface TxUserWallet {
    function transferTo(address payable dest, uint amount) external;
}

contract TxAttackWallet {
    address payable owner;

    constructor() public {
        owner = msg.sender;
    }

    function() external {
        TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
    }
}
Пример взят Solidity docs

Теперь, если бы кто-то обманом заставил вас послать Эфир на адрес контракта TxAttackWallet, они смогли бы украсть ваши средства, проверив tx.origin на содержание адреса, с которого была отправлена транзакция.

Для предотвращения атаки такого типа, используйте msg.sender для авторизации.

Плавающая директива pragma

Считается лучшей практикой выбрать одну версию компилятора и придерживаться ее. При использовании плавающей директивы pragma контракты могут быть случайно развернуты с использованием устаревшей или проблемной версии компилятора - что может привести к ошибкам, ставящим под угрозу безопасность смарт-контракта. Для проектов с открытым исходным кодом директива pragma также указывает разработчикам, какую версию следует использовать при развертывании контракта. Выбранная версия компилятора должна быть тщательно протестирована и рассмотрена на наличие известных ошибок.

Исключение, в котором допустимо использовать плавающую директиву pragma, относится к библиотекам и программным пакетам. В противном случае разработчикам потребуется вручную обновить директиву pragma для локальной компиляции.

Область видимости функции по умолчанию

Область видимости функции может быть задана как общая, частная, внутренняя или внешняя. Важно решить, какая видимость лучше всего подходит для функции смарт-контракта.

Многие атаки смарт-контрактов вызваны тем, что разработчик забывает или отказывается использовать модификатор видимости. Затем функция по умолчанию становится общей, что может привести к нежелательным изменениям.

Устаревшая версия компилятора

Разработчики часто находят ошибки и уязвимости в существующем программном обеспечении и делают исправления. По этой причине важно использовать самую последнюю из возможных версий компилятора. Смотрите ошибки прошлых версий компилятора здесь.

Не проверено возвращаемое значение вызова

Если возвращаемое значение низкоуровневого вызова не проверено, выполнение может возобновиться, даже если вызов функции выбрасывает ошибку. Это может привести к нежелательному поведению и нарушить логику программы. Неудачный вызов может быть даже инициирован злоумышленником, который сможет в дальнейшем использовать приложение в своих целях.

В Solidity вы можете использовать как низкоуровневые вызовы, такие как address.call(), address.callcode(), address.delegatecall() и adress.send(), так и вызовы контракта, например, ExternalContract.doSomething(). Низкоуровневые вызовы никогда не будут выбрасывать исключение - вместо этого они вернут значение false, если они встретят исключение, тогда как вызовы контракта выбросят исключение автоматически.

В случае использования низкоуровневых вызовов, убедитесь, что проверили возвращаемое значение, чтобы обработать возможность неудачного вызова.

Незащищенный вывод Эфира

Без надлежащего контроля доступа злоумышленники могут быть в состоянии изъять часть или весь эфир из контракта. Это может быть вызвано неправильным именем функции, которая должна быть конструктором, что дает любому доступ к повторной инициализации контракта. Чтобы избежать этой уязвимости, разрешайте вывод средств только авторизованным пользователям или так, как задумали, и называйте свой конструктор соответственно.

Незащищенная инструкция саморазрушения

В контрактах, содержащих метод selfdestruct, при отсутствии или недостаточном контроле доступа злоумышленники могут инициировать саморазрушение контракта. Важно учитывать, является ли функция саморазрушения абсолютно необходимой. Если это необходимо, рассмотрите возможность использования мульти-авторизации для предотвращения атаки.

Такой тип атаки был использован при атаке на Parity. Анонимный пользователь обнаружил и воспользовался уязвимостью в смарт-контракте” библиотека", сделав себя владельцем контракта. Затем злоумышленник приступил к самоуничтожению контракта. Это привело к тому, что средства были заблокированы в 587 уникальных кошельках, содержащих в общей сложности 513 774,16 Эфира.
member
Activity: 63
Merit: 127
Перевод статьи:
https://medium.com/better-programming/the-encyclopedia-of-smart-contract-attacks-vulnerabilities-dfc1129fdaac

Энциклопедия Смарт-Контракта:
Атаки и Уязвимости


Глубокое погружение в обеспечение безопасности смарт-контрактов



Приложения на Ethereum распоряжаются финансовыми величинами, что придает вопросу их безопасности исключительную важность. Будучи только формирующейся, экспериментальной технологией, смарт-контракты в своей истории безусловно подвергались ряду атак.
Чтобы предотвратить дальнейшие атаки, я составил список почти всех известных атак и уязвимостей. Хотя этот список включает известные атаки, новые эксплойты все еще регулярно обнаруживаются, и для вас (как для инженера) это должно быть только началом исследования безопасности смарт-контрактов.

Этот список можно найти на GitHub

.     .     .

Атаки

В этом разделе мы рассмотрим известные виды атак, которые могут быть использованы для нападения на слабые места смарт-контрактов.

Фронтраннинг, aka Зависимость от запроса на транзакцию

Университет Конкордии определяет фронтраннинг как  «совокупность действий, благодаря которым объект получает выгоду от предварительного доступа к привилегированной рыночной информации о предстоящих транзакциях и сделках». Знание будущих событий на рынке может привести к использованию этой информации в корыстных целях.

Например, зная, что произойдет очень крупная покупка определенного токена, злоумышленник может купить этот токен заранее и продать его с прибылью, когда крупный заказ на покупку увеличит цену актива.

Фронтраннинг-атаки уже долгое время являются проблемой на финансовых рынках, и из-за прозрачной природы блокчейна эта проблема возникает и на криптовалютных рынках.

Поскольку решение этой проблемы зависит от базы конкретного контракта, от нее может быть трудно защититься. Возможные решения могут быть найдены в использовании пакетного режима для транзакций и схемы предварительной фиксации (т. е. пользователю предоставляется возможность отправлять некоторые данные с задержкой).

DoS-атака с ограничением лимита газа

В блокчейне Ethereum у блоков есть лимит газа. Одно из преимуществ ограничения лимита газа заключается в том, что оно не позволяет злоумышленникам создавать бесконечный цикл транзакций, но если использование газа транзакцией превышает это ограничение, то транзакция не выполняется. Это может привести к Dos-атаке несколькими способами.

Неограниченные операции

Ситуация, в которой может возникнуть проблема с ограничением по лимиту газа, заключается в отправке средств на массив адресов. Даже без какого-либо злого умысла что-то легко может пойти не так. Просто слишком большой массив пользователей, которым нужно заплатить, может максимально увеличить лимит газа и не дать транзакции завершиться успешно.

Эта ситуация также может привести к атаке. Скажем, злоумышленник решает создать значительное количество адресов, при этом каждому адресу выплачивается небольшая сумма средств из смарт-контракта. Если все сделано успешно, транзакция может быть заблокирована на неопределенный срок, что может даже препятствовать выполнению дальнейших транзакций.

Эффективным решением этой проблемы могло бы быть использование системы pull-payment поверх текущей push-payment. Для этого разделите каждый платеж на отдельную транзакцию и попросите получателя совершить вызов функции.

Если по какой-то причине вам действительно нужно перебрать массив неопределенной длины, тогда, по крайней мере, ожидайте, что он может занять несколько блоков, и разрешите его выполнение в нескольких транзакциях - как в этом примере:

Code:
struct Payee {
    address addr;
    uint256 value;
}

Payee[] payees;
uint256 nextPayeeIndex;

function payOut() {
    uint256 i = nextPayeeIndex;
    while (i < payees.length && msg.gas > 200000) {
      payees[i].addr.send(payees[i].value);
      i++;
    }
    nextPayeeIndex = i;
}
Пример взят Consensys

Заполнение блока

В некоторых ситуациях ваш контракт может быть атакован с ограничением лимита газа, даже если вы не перебираете массив неопределенной длины. Злоумышленник может заполнить несколько блоков, прежде чем транзакция будет обработана, используя повышение цены на газ.

Эта атака совершается путем осуществления нескольких транзакций по очень высокой цене на газ. Если цена на газ достаточно высока и транзакции расходуют достаточно газа, они могут заполнить блоки целиком и не дать обработаться другим транзакциям.

Транзакции Ethereum требуют, чтобы отправитель оплачивал газ, чтобы лишать стимула спам-атаки, но в некоторых ситуациях может быть оправданным проведение такой атаки. Например, атака с заполнением блока использовалась на игровом Dapp, Fomo3D. В приложении был таймер обратного отсчета, и пользователь мог выиграть джек-пот, став последним, кто приобрел ключ - кроме того, каждый раз, когда пользователь покупал ключ, таймер продлевался. Злоумышленник купил ключ, а затем заполнил следующие 13 блоков подряд, поэтому смог выиграть джекпот.

Чтобы избежать таких атак, важно тщательно продумать, безопасно ли включать основанные на времени действия в ваше приложение.

DoS-атака с (неожидаемым) возвратом

DoS-атаки (отказ в обслуживании) могут возникать в функциях, когда вы пытаетесь отправить средства пользователю и функциональность зависит от того, был ли перевод успешным.

Это может быть проблемой в том случае, если средства отправляются на умный контракт, созданный злоумышленником, поскольку они могут просто создать резервную функцию, отменяющую все платежи.

Например:

Code:
// INSECURE
contract Auction {
    address currentLeader;
    uint highestBid;

    function bid() payable {
        require(msg.value > highestBid);

        require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert

        currentLeader = msg.sender;
        highestBid = msg.value;
    }
}
Пример взят Consensys

Как вы можете видеть в этом примере, если злоумышленник делает заявки из смарт-контракта с резервной функцией, возвращающей все платежи, они никогда не смогут быть профинансированы, и, таким образом, никто никогда не сможет сделать более высокую заявку.

Эта проблема может появиться и без злоумышленника. Например, вы, возможно, хотите заплатить массиву пользователей перебором по этому массиву, и, конечно же, вы хотите убедиться, что каждому пользователю наверняка оплачено. Проблема здесь в том, что если один платеж не удается, функция возвращается, и не один из платежей не проходит.

Code:
address[] private refundAddresses;
mapping (address => uint) public refunds;

// bad
function refundAll() public {
    for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
        require(refundAddresses[x].send(refunds[refundAddresses[x]])) // doubly bad, now a single failure on send will hold up all funds
    }
}
Пример взят Consensys

Эффективным решением этой проблемы было бы использование системы pull-payment поверх существующей системы push-payment. Для этого выделите каждый платеж в отдельную транзакцию и попросите получателя вызвать функцию.

Code:
contract auction {
    address highestBidder;
    uint highestBid;
    mapping(address => uint) refunds;

    function bid() payable external {
        require(msg.value >= highestBid);

        if (highestBidder != address(0)) {
            refunds[highestBidder] += highestBid; // record the refund that this user can claim
        }

        highestBidder = msg.sender;
        highestBid = msg.value;
    }

    function withdrawRefund() external {
        uint refund = refunds[msg.sender];
        refunds[msg.sender] = 0;
        (bool success, ) = msg.sender.call.value(refund)("");
        require(success);
    }
}
Пример взят Consensys

Принудительная отправка Эфира в контракт

Иногда пользователи не хотят, чтобы у них была возможность отправлять Эфир на смарт-контракт. Несмотря на это обстоятельство, существует возможность обойти резервную функцию контракта и отправить Эфир.

Code:
contract Vulnerable {
    function () payable {
        revert();
    }

    function somethingBad() {
        require(this.balance > 0);
        // Do something bad
    }
}
Пример взят Consensys

Хотя это выглядит, будто любая транзакция к Зараженному контракту будет возвращена, на самом деле существует пара возможностей принудительно отправить Эфир.

Во-первых можно вызвать selfdestruct метод для контракта, у которого адресом бенефициара задан адрес Зараженного контракта. Это сработает, потому что selfdestruct не станет обращаться к резервной функции.

Другой вариант заключается в предварительном вычислении адреса контракта и отправке эфира на адрес до того, как контракт будет даже развернут. Удивительно, но это возможно.

Грифинг недостаточным количеством газа

Грифинг - это тип атаки, часто встречающийся в видеоиграх, при котором вредитель играет в игру нежелательным образом так, чтобы раздражать других игроков, также известен как троллинг. Этот тип атаки также используется для предотвращения выполнения транзакций по назначению.

Эта атака может быть выполнена на контрактах, которые принимают данные и используют их в подзапросе на другой контракт. Этот метод часто используется в кошельках с мультиподписью, а также с ретрансляторами транзакций. Если подвызов завершается неудачно, либо вся транзакция отменяется, либо выполнение продолжается.

Давайте рассмотрим в качестве примера простой контракт с ретранслятором. Как показано ниже, контракт с ретранслятором позволяет кому-либо создавать и подписывать транзакцию без необходимости ее выполнения. Часто это используется, когда пользователь не может заплатить за газ, связанный с транзакцией.

Code:
contract Relayer {
    mapping (bytes => bool) executed;

    function relay(bytes _data) public {
        // replay protection; do not call the same transaction twice
        require(executed[_data] == 0, "Duplicate call");
        executed[_data] = true;
        innerContract.call(bytes4(keccak256("execute(bytes)")), _data);
    }
}
Пример взят Consensys

Выполняющий транзакцию пользователь, forwarder, может подвергать транзакции цензуре, используя количество газа, достаточное только для выполнения транзакции, но недостаточное для успешного выполнения подзапроса.

Есть два способа предотвратить это. Первым решением было бы разрешить только доверенным пользователям ретранслировать транзакции. Другим решением является требование, чтобы forwarder обеспечивал достаточное количество газа, как показано ниже.

Code:
// contract called by Relayer
contract Executor {
    function execute(bytes _data, uint _gasLimit) {
        require(gasleft() >= _gasLimit);
        ...
    }
}
Пример взят Consensys
Jump to: