РеентерабельностьРеентерабельность - это атака, которая может произойти, когда ошибка в функции контракта может позволить функциональному взаимодействию произойти несколько раз, в то время как это должно быть запрещено. При наличии злого умысла, это может быть использовано для отвода средств из смарт-контракта. На самом деле, реентерабельность была вектором атаки, который был использован для взлома DAO.
Одно-функциональная реентерабельностьОдно-функциональная реентерабельная атака происходит, когда уязвимая функция является той же самой функцией, которую злоумышленник пытается рекурсивно вызвать.
// INSECURE
function withdraw() external {
uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0;
}
Пример взят
ConsensysЗдесь мы можем видеть, что баланс изменяется только после перевода средств. Это может позволить хакеру вызывать функцию многократно до тех пор, пока баланс не отобразится равным 0, целиком истощив смарт-контракт.
Кросс-функциональная реентерабельностьКросс-функциональная реентерабельность - это более сложная версия того же процесса. Кросс-функциональная реентерабельность возникает, когда уязвимая функция используется совместно с функцией, которую может использовать злоумышленник.
// 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 может быть использована для вызова внешних функций, которые в свою очередь могут быть использованы для осуществления атак, связанных с реентерабельностью.
Еще один надежный превентивный метод - это
маркировка ненадежных функций.
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 с другими смарт-контрактами - например, вызовы внешних функций.
Эта структура эффективна от реентерабельности, потому что изменения в контракте предотвратят осуществление вредоносных взаимодействий злоумышленниками.
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.origintx.origin это глобальная переменная в Solidity, которая возвращает адрес, отправивший транзакцию. Важно, чтоб вы никогда не использовали
tx.origin для авторизации, так как другой контракт может использовать резервную функцию для вызова вашего контракта и получения авторизации, потому что уже использованный для авторизации адрес хранится в
tx.origin. Рассмотрим следующий пример:
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.
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 Эфира.