• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Обзор исправлений двойной инициализации xToken

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
Оригинальная статья
Переведено специяльно для xss.pro
Камнями кидать в Jolah Milovski

TLDR: мы обнаружили 3 уязвимости в смарт-контрактах xToken.​


  1. Кража нераспределенных токенов вознаграждения (двойная инициализация)
  2. Кража средств xToken-Terminal (инициализация повторного входа)
  3. Временно заморозить невостребованный доход пользователей (что не будет объяснено в этой статье).

Их мы и отправили в программу bug bounty xToken на Immunefi . Первая ошибка, которая позволила бы злоумышленнику украсть нераспределенные токены, получила критический рейтинг серьезности и была исправлена командой xToken.

Анализ уязвимостей

xToken Terminal — это инфраструктура добычи ликвидности для Uniswap V3, которая позволяет развертывать поощрительные пулы.

Рассмотрим следующие контракты:

  • CLR.sol— Это собственно контракт пула ликвидности, в котором хранятся средства.
  • LMTerminal.sol— Это менеджер всей системы. Он взимает комиссию за обмен и создает стимулирующие пулы CLR.

Ошибка 1: двойная инициализация

Чтобы сделать пул CLR обновляемым, для доступа к нему используется прозрачный прокси-сервер. CLRProxy, который делегирует вызовы пулу CLR. При просмотре прокси мы обнаружили кое-что очень интересное:

Код:
contract CLRProxy is TransparentUpgradeableProxy {
    ICLRDeployer clrDeployer;
    ...
}

Прокси имеет переменную состояния, расположенную в слоте 0.
Поскольку прокси делегирует вызовы пулу CLR, они разделяют пространство памяти, а это означает, что слот 0 пула CLR столкнется с переменной clrDeployer. Но что находится в слоте 0 пула CLR? И как, черт возьми, контракт работает без ошибок?
Исследуя контракт CLR дальше, мы обнаружили, что слот 0 контракта CLR происходит от первого контракта, который он наследует, а это контракт Initializable.

Код:
abstract contract Initializable {
    bool private _initialized;

    bool private _initializing;
    
    ...
}

Это означает, что переменная clrDeployer сталкивается с _initialized и _initializing! Но каковы будут последствия этого столкновения? Контракт Initializeable работает с использованием модификатора initializer, который при действии на функции делает их вызываемыми только один раз.

Код:
abstract contract Initializable {
    ...
    
    modifier initializer() {
        require(_initializing || _isConstructor() || !_initialized, "Initializable: contract is already initialized");

        bool isTopLevelCall = !_initializing;
        if (isTopLevelCall) {
            _initializing = true;
            _initialized = true;
        }

        _;

        if (isTopLevelCall) {
            _initializing = false;
        }
    }
    
    ...
}

Поскольку _initializing и _initialized будут TRUE с очень высокой вероятностью (они будут практически случайными, а boolean - это всего один байт, и поэтому с вероятностью 99,6% они будут true). Таким образом, оператор require пройдет, а isTopLevelCall будет false. Следовательно, модификатор всегда будет проходить и вообще не изменит состояние контракта. Итак, мы можем вызывать функцию initialize столько раз, сколько захотим! Путь от вызова initialize до кражи всех средств в CLR довольно прост. Она вызывает функцию safeIncreaseAllowance на аргументах, предоставленных злоумышленником. Мы можем просто увеличить наше разрешение, чтобы иметь возможность украсть все токены в пуле.

Ошибка 2: Инициализация реентерабельности (вне области применения)

Мы исследовали контракт LMTerminal и охуели встревожились:

Код:
contract LMTerminal is Initializable, OwnableUpgradeable, PausableUpgradeable {
  // ...
 
  function deployIncentivizedPool(...) external payable {
    // ..
 
    // Deploy CLR
    ICLR clrPool = ICLR(clrDeployer.deployCLRPool(proxyAdmin));
    
    // ...
    
    // Initialize CLR
    clrPool.initialize(...);

    // Approve tokens to clr pool
    IERC20(pool.token0).safeApprove(address(clrPool), type(uint256).max);
    IERC20(pool.token1).safeApprove(address(clrPool), type(uint256).max);
    
     // Transfer initial mint tokens to Terminal
    IERC20(pool.token0).safeTransferFrom(
        msg.sender,
        address(this),
        pool.amount0
    );
    IERC20(pool.token1).safeTransferFrom(
        msg.sender,
        address(this),
        pool.amount1
    );

    // ...
    
    // Create Uniswap V3 Position, seed with initial liquidity
    clrPool.mintInitial(pool.amount0, pool.amount1, msg.sender);
    
    // ...
  }
 
  // ...
}

Он разрешает пулу CLR использовать все свои средства! Может быть, мы можем заставить пул CLR воровать токены у LMTerminal?
Чтобы дать нам больше контроля над пулом CLR, мы можем развернуть наполовину злонамеренный пул с одним настоящим токеном (который мы хотим украсть) и одним фальшивым токеном, который мы контролируем. Но что мы можем сделать с этим контролем? Снова взглянув на модификатор инициализатора, мы замечаем нечто странное:

Код:
abstract contract Initializable {
    ...
    
    modifier initializer() {
        require(_initializing || _isConstructor() || !_initialized, "Initializable: contract is already initialized");

        bool isTopLevelCall = !_initializing;
        if (isTopLevelCall) {
            _initializing = true;
            _initialized = true;
        }

        _;

        if (isTopLevelCall) {
            _initializing = false;
        }
    }
    
    ...
}

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

1659078787749.png


Теперь мы можем вызывать initialize с любым аргументом, который нам нужен.

Итак, давайте вспомним, что мы имеем:

SafeApproves передает все свои средства в пул CLR.
Мы управляем аргументами инициализации пула CLR.

Итак, чтобы украсть средства, нам нужно перевести средства терминала в пул CLR, а затем перевести их из пула CLR к нам.
Вторая часть проста. Функция initialize вызывает функцию safeIncreaseAllowance на аргументах, предоставленных злоумышленником.
Первая часть немного сложнее. Причина, по которой терминал safeApproves использует все свои средства, заключается в том, что mintInitial должен использовать некоторые из своих средств. Чтобы использовать это, мы можем поменять вредоносный токен на настоящий токен. И тогда мы сможем получить больше токенов, чем то количество, которое мы отправили.

1659078869308.png


Пошагово атака выглядит следующим образом:

Атакующий вызывает deployIncentivizedPool для создания нового пула CLR.

→ Новый пул будет содержать два токена: настоящий токен, который мы хотим украсть, и поддельный токен.
→ Мы даем много поддельных токенов, но мало настоящих. a.k.a pool.amount0<pool.amount1 (настоящий токен равен 0, а поддельный - 1).

2. Наш фальшивый токен вызывает функцию initialize во второй раз. Аргументы CLR контролируются нами, и мы будем использовать их для переключения токенов и увеличения нашего запаса.
3. Вызывается функция mintInital, которая передаст в пул CLR много настоящих токенов (но несколько фальшивых).
4. Поскольку CLR-пул выдал нам разрешение, мы можем перевести все настоящие токены к нам.

Как это было:
  • 8 мая 2022 г., 17:42 — отправил уязвимости в Immunefi.
  • 8 мая 2022 г., 17:54 — Immunefi расширил уязвимость до xToken.
  • 8 мая 2022 г., 20:15 — xToken развернул исправление (всего через ~ 2 часа).
  • 13 мая 2022 г., 19:27 — Заплатили за критическую уязвимость
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх