Оригинальная статья
Переведено специяльно для xss.pro
Камнями кидать в Jolah Milovski
Их мы и отправили в программу bug bounty xToken на Immunefi . Первая ошибка, которая позволила бы злоумышленнику украсть нераспределенные токены, получила критический рейтинг серьезности и была исправлена командой xToken.
Рассмотрим следующие контракты:
Прокси имеет переменную состояния, расположенную в слоте 0.
Поскольку прокси делегирует вызовы пулу CLR, они разделяют пространство памяти, а это означает, что слот 0 пула CLR столкнется с переменной clrDeployer. Но что находится в слоте 0 пула CLR? И как, черт возьми, контракт работает без ошибок?
Исследуя контракт CLR дальше, мы обнаружили, что слот 0 контракта CLR происходит от первого контракта, который он наследует, а это контракт Initializable.
Это означает, что переменная clrDeployer сталкивается с _initialized и _initializing! Но каковы будут последствия этого столкновения? Контракт Initializeable работает с использованием модификатора initializer, который при действии на функции делает их вызываемыми только один раз.
Поскольку _initializing и _initialized будут TRUE с очень высокой вероятностью (они будут практически случайными, а boolean - это всего один байт, и поэтому с вероятностью 99,6% они будут true). Таким образом, оператор require пройдет, а isTopLevelCall будет false. Следовательно, модификатор всегда будет проходить и вообще не изменит состояние контракта. Итак, мы можем вызывать функцию initialize столько раз, сколько захотим! Путь от вызова initialize до кражи всех средств в CLR довольно прост. Она вызывает функцию safeIncreaseAllowance на аргументах, предоставленных злоумышленником. Мы можем просто увеличить наше разрешение, чтобы иметь возможность украсть все токены в пуле.
Ошибка 2: Инициализация реентерабельности (вне области применения)
Мы исследовали контракт LMTerminal иохуели встревожились:
Он разрешает пулу CLR использовать все свои средства! Может быть, мы можем заставить пул CLR воровать токены у LMTerminal?
Чтобы дать нам больше контроля над пулом CLR, мы можем развернуть наполовину злонамеренный пул с одним настоящим токеном (который мы хотим украсть) и одним фальшивым токеном, который мы контролируем. Но что мы можем сделать с этим контролем? Снова взглянув на модификатор инициализатора, мы замечаем нечто странное:
Он допускает реентерабельность! Внутри initialize он вызывает функции нашего вредоносного токена, и таким образом мы можем вызвать initialize снова из initialize, с аргументами, контролируемыми злоумышленником.
Теперь мы можем вызывать initialize с любым аргументом, который нам нужен.
Итак, давайте вспомним, что мы имеем:
SafeApproves передает все свои средства в пул CLR.
Мы управляем аргументами инициализации пула CLR.
Итак, чтобы украсть средства, нам нужно перевести средства терминала в пул CLR, а затем перевести их из пула CLR к нам.
Вторая часть проста. Функция initialize вызывает функцию safeIncreaseAllowance на аргументах, предоставленных злоумышленником.
Первая часть немного сложнее. Причина, по которой терминал safeApproves использует все свои средства, заключается в том, что mintInitial должен использовать некоторые из своих средств. Чтобы использовать это, мы можем поменять вредоносный токен на настоящий токен. И тогда мы сможем получить больше токенов, чем то количество, которое мы отправили.
Пошагово атака выглядит следующим образом:
Атакующий вызывает deployIncentivizedPool для создания нового пула CLR.
→ Новый пул будет содержать два токена: настоящий токен, который мы хотим украсть, и поддельный токен.
→ Мы даем много поддельных токенов, но мало настоящих. a.k.a pool.amount0<pool.amount1 (настоящий токен равен 0, а поддельный - 1).
2. Наш фальшивый токен вызывает функцию initialize во второй раз. Аргументы CLR контролируются нами, и мы будем использовать их для переключения токенов и увеличения нашего запаса.
3. Вызывается функция mintInital, которая передаст в пул CLR много настоящих токенов (но несколько фальшивых).
4. Поскольку CLR-пул выдал нам разрешение, мы можем перевести все настоящие токены к нам.
Как это было:
Переведено специяльно для xss.pro
Камнями кидать в Jolah Milovski
TLDR: мы обнаружили 3 уязвимости в смарт-контрактах xToken.
- Кража нераспределенных токенов вознаграждения (двойная инициализация)
- Кража средств xToken-Terminal (инициализация повторного входа)
- Временно заморозить невостребованный доход пользователей (что не будет объяснено в этой статье).
Их мы и отправили в программу 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, с аргументами, контролируемыми злоумышленником.
Теперь мы можем вызывать initialize с любым аргументом, который нам нужен.
Итак, давайте вспомним, что мы имеем:
SafeApproves передает все свои средства в пул CLR.
Мы управляем аргументами инициализации пула CLR.
Итак, чтобы украсть средства, нам нужно перевести средства терминала в пул CLR, а затем перевести их из пула CLR к нам.
Вторая часть проста. Функция initialize вызывает функцию safeIncreaseAllowance на аргументах, предоставленных злоумышленником.
Первая часть немного сложнее. Причина, по которой терминал safeApproves использует все свои средства, заключается в том, что mintInitial должен использовать некоторые из своих средств. Чтобы использовать это, мы можем поменять вредоносный токен на настоящий токен. И тогда мы сможем получить больше токенов, чем то количество, которое мы отправили.
Пошагово атака выглядит следующим образом:
Атакующий вызывает 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 — Заплатили за критическую уязвимость