July 26, 2022

Advanced Reentrancy

Всем привет, с вами @kyoukisu (кёкису). В этот раз мы рассмотрим детально атаку reentrancy на более сложном примере. Данная статья является вольным переводом+редактурой информации с Cross-Contract Reentrancy Attack.

Если вам что-то непонятно, тыкайте по встроенным ссылкам, они здесь не просто так.

Перед прочтением обязательно ознакомьтесь с моей первой статьей про reentrancy.

Исходники кода будут ссылками под картинками.

Еще один вид Reentrancy?

Ранее мы рассматривали reentrancy внутри одной функции и между несколькими, но такие очевидные уязвимости оставляют достаточно редко, ведь есть множество простых решений защиты от них (ReentrancyGuard от OpenZeppelin). Менее очевидная уязвимость возникает между разными смарт-контрактами, а именно, когда состояние из одного контракта используется в другом контракте, но это состояние не полностью обновляется перед вызовом.

Cross-Contract Reentrancy возможна, когда соблюдаются следующие условия:

  1. Есть места передачи потока управления.
  2. Состояния одного контракта используются в другом контракте.
  3. Неграмотное построение взаимодействия контрактов.

Пример Cross-Contract Reentrancy

Первый контракт

Рассмотрим простенький ERC20 контракт VaultToken (далее $VT). Обычный ровный токен, имеет свое хранилище бля обеспечения, почти стейбл. В нем используется интерфейс UniswapV2 роутера для того, чтобы свапать сторонние токены, на что-то более надежное, чтобы не стать холдером $LUNA и $UST. Инфу о UniswapV2 роутере можно глянуть в доках юнисвапа.

Uniswap - это децентрализованная криптовалютная биржа (DEX). Она работает на блокчейне Ethereum и полностью автоматизирована смартконтрактами. Подробнее про работу Uniswap'а.

без комментариев

Vault.sol

source

Некоторые особенности:

  • При создании контракта в конструкторе (18 строчка) мы указываем _router или же интерфейс UniswapV2, а также _baseToken сторонний токен, который будет вноситься пользователями и храниться в контракте, чаще всего это стейбл или эфир.
  • В коде можно заметить _burn, _mint, totalSupply; это стандартизированные функции ERC20.sol, от которого мы наследуемся.
  • Выдача токена $VT напрямую зависит от доли юзера в общих активах baseToken контракта. Расчеты относительно этой доли происходят в функциях shareToAmount и amountToShare (23 и 27 строчки соответственно).

Что происходит в контракте?

Юзеры вносят какие-то токены _srcToken, неважно какие, пока они сделаны по стандарту ERC20 и имеют ликвидность на Uniswap'e, чтоб контракт мог их сбыть по флору через функцию swapAndDeposit. Потом юзеры жмут deposit и минтят $VT, при вызове withdraw происходит все с точностью да наоборот - сжигают $VT и выводят baseToken, на свои скам токены юзер сам будет менять его.

На каждой функции контракта Vault, которая изменяет его стейт, висит модификатор nonReentrant из контракта ReentrancyGuard.sol, что обеспечивает защиту от повторного входа в данную функцию.

Модификатор - особая функция, которая оборачивается вокруг другой.

Если вспомнить прошлую статью, то nonReentrant по своей сути является мьютексом.

ReentrancyGuard.sol

source

Поэтому что бы ни пытался сделать злоумышленник с единственным контрактом Vault.sol, при рекурсивных вызовах он будет упираться в require функции _nonReentrantBefore. Однако это нас не остановит от reentrancy между контрактами.

наивное применение nonReentrant не спасет вас от reentrancy между несколькими контрактами

Второй контракт

Предположим, что у нас есть еще один контракт, некий ICOGov, позволяющий юзерам обменивать $VT на новый токен $GOV - какая-то донная P2E игрушка, проводит ICO, IGO на платформе токена $VT (можете представить здесь панкейк) и хочет бабок. Количество получаемых токенов определяется числом инвестированных $VT и курсом обмена tokenPrice.

ICOGov.sol

source

При создании контракта в конструктор передаются токен $GOV, токен $VT, _treasury адрес, куда будут скидываться все токены $VT при обмене, а также цену токена.

Функция buyToken как раз таки отвечает за обмен токенов, и, как мы видим, на ней тоже висит модификатор nonReentrant.

Находим уязвимость

Первая вещь, которую мы должны заметить в контракте Vault, это то, что есть передача потока управления контракту _srcToken в функции swapAndDeposit.

Вторая заключается в функции buyToken контракта ICOGov, т.к. она использует стейт Vault'а, обращаясь к нему за shareToAmount.

shareToAmount, в свою очередь, рассчитывает количество выдаваемых токенов. Исходя из этой формулы, становится ясным, что чем больше baseToken на аккаунте Vault и меньше totalSupply, тем больше мы получим токенов $GOV.

Вспомнив условия Cross-Contract Reentrancy и учтя два факта выше, мы можем написать свой scam контракт-токен для _srcToken и использовать его при вызове функции swapAndDeposit в местах передачи потока управления для того, чтобы как можно сильнее раздуть баланс baseToken на контракте Vault и оставить totalSupply неизменным на время атаки - минтим кучу $VT (надо быть китом) и меняем по старому курсу на $GOV (лутаем миллионы).

Вот схема того, что будет происходить во время атаки:

Непосредственно атака

Теперь посмотрим, как можно реализовать контракт-токен для атаки. Для этого мы также, как и в предыдущих контрактах должны унаследоваться от ERC20.sol.

EvilERC20.sol

source

При создании токена $EVIL мы передаем адреса на Vault.sol, ICOGov.sol и GovToken.sol, чтобы знать куда бить. Также в конструкторе мы минтим владельцу очень много токена $EVIL для того, чтобы он смог создать ликвидность на Uniswap перед атакой, чтоб Vault мог обменивать его на стейбл, не подозревая подвоха.

Заключение

Таким образом, злоумышленнику для атаки требуется:

  1. Задеплоить свой контракт токена с измененным approve
  2. Добавить ликвидность на пару $EVIL-baseToken на Uniswap'е
  3. Получить первоначальное количество токенов $VT на свой контракт
  4. Осуществить вызов swapAndDeposit

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

Чтобы такое не произошло с вашим контрактом:

  • Убедитесь, что все внутренние изменения состояния выполнены до передачи потока управления - шаблон Checks-Effects-Interactions.
  • Предотвращайте запуск неизвестного кода - добавьте whitelist адресов контрактов (токенов).
  • Используйте блокировку повторного входа, например, ReentrancyGuard от OpenZeppelin, но это не спасет вас от reentrancy между контрактами.

Атаку с обменом токена как в данном примере имеет смысл применять, когда токен только минтится, есть куда сбыть токен и есть весовая доля капитализации токена / активы для ее покупки.

Случаи реальных взломов:

Attack on ValueDefi (7 May 2021)

Attack on Rari Capital (8 May 2021)

Пишите свои замечания и пожелания в комментариях

Мой канал в телеграме - https://t.me/kyo_dev