June 25, 2022

Reentrancy attack для чайникoff

Несколько слов о том, что это за атака, как оно работает, к чему может привести и пара примеров из жизни.

Концепт

Давайте представим, что мы наткнулись на этот прекрасный псевдокод.

Разберем что тут происходит.

  1. Если переменная timesCalled > 0, то функция завершается ошибкой
  2. Вызываем какую-то функцию из стороннего контракта
  3. увеличиваем переменную timesCalled на 1

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

Так ли это? Зависит от того, что происходит в externalContractFunction.

Для наглядности того, что мы получим в итоге, в badFunction заменим вызов externalContractFunction на код самой badFunction

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

В итоге в начале мы имеем набор одинаковых проверок каунтера timesCalled и все они пройдут успешно, т.к. все увеличения каунтера происходят в самом конце. Как итог, в случае если externalContractFunction будет вызвана 2 раза - значение timesCalled после выполнения всего кода будет 2, если 6 раз - то 6 и т.п.

Был ли способ этого избежать?

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

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

Ближе к реальности

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

В функции mintPublic можно заметить знакомый паттерн.

  • проверка каунтера
  • вызов функции
  • увеличения каунтера

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

А не так с _safeMint то, что слово safe здесь обозначает только то, что её невозможно использовать для минта токена на контракт, с которого будет невозможно этот токен вывести.

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

Таким образом, атака на подобный контракт выглядит так.

  • Деплоим контракт с кастомной функцией onERC721Received
  • С нашего атакующего контракта начинаем минт уязвимого контракта
  • Уязвимый контракт во время выполнения _safeMint вызывает на атакующем контракте onERC721Received, эта функция в свою очередь снова вызывает mintPublic на уязвимом контракте
кастомная функция onERC721Received, вызывающая publicMint на уязвимом контракте 6 раз в цикле.

Примеры из жизни

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

Ниже несколько примеров из реальных контрактов недавно созданных коллекций. Для удобства самостоятельного разбора:

- проверки выделены синим

- вызов внешней функции красным

- необходимые изменения в контракте зеленым

SPOODEEMOONWTF

https://etherscan.io/address/0x973e7cf876932fa820139287244b66e21b9030bc#code - SPOODEEMOONWTF

При минте SPOODEEMOONWTF с контракта, можно не только обойти ограничение в количестве токенов на кошелек, но и глобальное ограничение количества токенов.

LACOSTE

https://etherscan.io/address/0xcd041f40d497038e2da65988b7d7e2c0d9244619#code - UNDW3Collection

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

GossamerSeed

https://etherscan.io/address/0xc7c962e44316e0c052448a0fdd1da15ea24fa9a9#code - GossamerSeed

Здесь так же можно обойти ограничение на количество минт мест на кошелек.