WEB3DEV

Cover image for A Espada de Dois Gumes do abi.decode
Panegali
Panegali

Posted on

A Espada de Dois Gumes do abi.decode

O decodificador de ABI do Solidity é muito utilizado em protocolos de nível superior. Neste artigo, entenderemos como ele funciona nos bastidores e como pode ser aproveitado por agentes mal-intencionados para explorar protocolos.

Antes de nos aprofundarmos nos aspectos internos do abi.decode, é importante entender sua função na decodificação de mensagens de reversão personalizadas.

Motivos de reversão personalizados

Revertendo com um motivo personalizado

O Solidity lançou um artigo detalhando como reverter de forma eficiente um motivo personalizado. O artigo inclui um exemplo de bloco na linguagem Assembly que pode ser usado para retornar uma mensagem de erro personalizada:

let free_mem_ptr := mload(64)
mstore(free_mem_ptr, 0x08c379a000000000000000000000000000000000000000000000000000000000)
mstore(add(free_mem_ptr, 4), 32)
mstore(add(free_mem_ptr, 36), 12)
mstore(add(free_mem_ptr, 68), "Unauthorized")
revert(free_mem_ptr, 100)
Enter fullscreen mode Exit fullscreen mode
  1. Carregue o próximo endereço disponível do ponteiro de memória livre.
  2. Bytes 0-32: O seletor de “Error(string)” para os primeiros 32 bytes (0x08c379a0).
  3. Bytes 4–36: O deslocamento para o comprimento da string de erro (32).
  4. Bytes 36–68: Comprimento da string (12).
  5. Bytes 68–100: A própria string (“Não autorizado”).

Essencialmente, usando o YUL, qualquer contrato pode ser escrito para reverter com quaisquer dados arbitrários.

Analisando o motivo da reversão

É comum analisar o motivo de reversão na função do chamador usando a seguinte implementação. Essa implementação pode ser encontrada em protocolos populares como a UniswapV3:

(bool success, bytes memory result) = address(target).call(data);

if (!success) {
   // Próximas 5 linhas de https://ethereum.stackexchange.com/a/83577
   if (result.length < 68) revert();
   assembly {
       result := add(result, 0x04)
   }
   revert(abi.decode(result, (string)));
}
Enter fullscreen mode Exit fullscreen mode

No trecho de código acima, os dados retornados pela chamada de baixo nível são apontados em “results” (resultados) e o código adiciona 4 bytes ao deslocamento do ponteiro de resultados.

Esta etapa é necessária para alinhar os dados para que o abi.decode seja capaz de decodificar a string do motivo da reversão e ignorar o seletor de 4 bytes “0x08c379a0”, que é o seletor para “Error(string)”.

Brincando com ponteiros de memória

Embora isso nos ajude a decodificar o motivo da reversão corretamente, também torna os dados codificados mais longos, o que aproveitaremos para explorar.

Observe que o resultado da chamada é do tipo “bytes memory”.:

(bool success, bytes memory result) = address(target).call(data);
Enter fullscreen mode Exit fullscreen mode

Os primeiros 32 bytes da “bytes memory” apontam para o comprimento da própria carga útil.

Ao aplicar o deslocamento abaixo, os primeiros 32 bytes incluirão os bytes do deslocamento [32–36] que são 0x08c379a0. Nesse caso, result.length será muito alto (comprimento original + 0x08c379a0)

result := add(result, 0x04)
Enter fullscreen mode Exit fullscreen mode

“Results” antes de mudar:

[0] 0000000000000000000000000000000000000000000000000000000000000064
[1] 08c379a000000000000000000000000000000000000000000000000000000000
[2] 0000002000000000000000000000000000000000000000000000000000000000
[3] 0000000c556e617574686f72697a656400000000000000000000000000000000
[4] 0000000000000000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

“Results” após a mudança:

[0] 0000000000000000000000000000000000000000000000000000006408c379a0
[1] 0000000000000000000000000000000000000000000000000000000000000020
[2] 000000000000000000000000000000000000000000000000000000000000000c
[3] 556e617574686f72697a65640000000000000000000000000000000000000000
[4] 0000000000000000000000000000000000000000000000000000000000000000
Enter fullscreen mode Exit fullscreen mode

Após o alinhamento, os dados serão formatados corretamente e serão enviados para o abi.decode. O comprimento dos dados é 0x6408c379a0.

O que aprendemos até agora?

  1. Os contratos chamados podem ser revertidos com quaisquer dados de retorno arbitrários.
  2. Deslocar o seletor “Error(string)” aumenta o tamanho dos bytes para um valor grande.

O que pode dar errado?

Muitos protocolos incorporam retransmissores ou guardiões que executam transações e retornos de chamada e são pagos pelo custo de execução. Essas funções geralmente exigem que os callbacks chamados não sejam revertidos. Os protocolos implementam verificações de validação para garantir isso.

Um exemplo de como seria um protocolo vulnerável:

contract Protocol {
   event CallbackFailed(string reason);

   function executeSomethingForUser(address callback) external {
       try ICallback(callback).callbackFunc(0) {
       } catch (bytes memory reasonBytes) {
           // Próximas 5 linhas de https://ethereum.stackexchange.com/a/83577
           if (reasonBytes.length < 68) revert();
           assembly {
               reasonBytes := add(reasonBytes, 0x04)
           }
           emit CallbackFailed(abi.decode(reasonBytes, (string)));
       }
       // Seja pago pela execução
   }
}
Enter fullscreen mode Exit fullscreen mode

Embora a decodificação do motivo da reversão pareça funcionar como esperado, há uma vulnerabilidade que pode ser explorada. Especificamente, se um invasor puder fazer com que a função do abi.decode no bloco de captura seja revertida ou consuma uma quantidade significativa de gás, isso pode levar a uma vulnerabilidade em que o usuário controla quando a execução é bem-sucedida ou esgota os fundos do chamador.

Para quebrar o código acima, vamos olhar sob o capô do abi.decode

Explorando o abi.decode

O decodificador de ABI do Solidity é difícil de entender a partir do nível do código-fonte. É mais fácil desmontar o bytecode para entender exatamente como ele funciona.

Existem algumas repetições de código de operação (opcode) que podemos ignorar e usar o rattle nos ajudará a realizar a análise estática binária de maneira legível.

Vamos desmontar o seguinte contrato para entender como funciona o abi.decode:

pragma solidity 0.8.17;

contract test {
   function testDecode() external {
       bytes memory result = abi.encode("aaaa");
       abi.decode(result, (string));
   }
}
Enter fullscreen mode Exit fullscreen mode

Lembre-se, nosso objetivo é identificar como fazer o abi.decode:

  1. Reverter.
  2. Consumir uma grande quantidade de gás.

Abaixo está uma captura de tela dos códigos de operação gerados onde o abi.decode é chamado:

Produzido por rattle

As três setas vermelhas apontam para três caminhos de reversão diferentes e a última seta verde aponta para um bloco onde pode ocorrer grande consumo de gás.

Reversões

De acordo com a montagem, os três caminhos de reversão são revertidos quando:

  1. O deslocamento para a carga útil é inferior a 32 bytes.
  2. O deslocamento para a carga útil é maior que 0xffffffffffffffff, que é o fim da pilha de memória da evm (uint64).
  3. O deslocamento para a carga útil é maior que o comprimento dos dados codificados + 0x1f.

Um ator mal-intencionado pode armazenar qualquer valor que atenda a um dos três caminhos de reversão nos dados de retorno para fazer a reversão do abi.decode.

Exemplo de alavancagem nº 2:

let free_mem_ptr := mload(64)
mstore(free_mem_ptr, 0x08c379a000000000000000000000000000000000000000000000000000000000)
mstore(add(free_mem_ptr, 4), 0xfffffffffffffffff)
mstore(add(free_mem_ptr, 36), 12)
mstore(add(free_mem_ptr, 68), "Unauthorized")
revert(free_mem_ptr, 100)
Enter fullscreen mode Exit fullscreen mode

Bomba de gás

Agora que vimos como reverter o abi.decode, vamos ver como um ator mal-intencionado pode explorar o abi.decode para drenar fundos dos retransmissores ou guardiões do protocolo criando uma bomba de gás.

Como aprendemos anteriormente, deslocar os dados de retorno em 4 bytes cria um comprimento grande para os dados codificados.

Portanto, um invasor pode criar o deslocamento da carga útil para ficar um pouco abaixo do tamanho dos dados codificados para desperdiçar gás, mas não reverter o abi.decode.

O abi.decode tentará fazer MLOAD da memória em um deslocamento muito grande, o que acionará uma expansão de memória que consome muito gás.

Você pode ler mais sobre expansões de memória aqui: https://www.evm.codes/about#memoryexpansion

O código de operação MLOAD é carregado a partir do deslocamento para obter o comprimento da carga útil

Ao fornecer um valor de deslocamento logo abaixo do comprimento dos dados codificados, a quantidade de gás que será consumida no MLOAD é maior do que o limite de gás do bloco. Um exemplo de valor de deslocamento é “0x6408c37900”

Um ator mal-intencionado pode colocar a seguinte carga útil para criar uma bomba de gás que maximizará todo o gás fornecido para a transação.

let free_mem_ptr := mload(64)
mstore(free_mem_ptr, 0x08c379a000000000000000000000000000000000000000000000000000000000)
mstore(add(free_mem_ptr, 4), 0x6408c37900) // logo abaixo do len
mstore(add(free_mem_ptr, 36), 12)
mstore(add(free_mem_ptr, 68), "Não autorizado")
revert(free_mem_ptr, 100)
Enter fullscreen mode Exit fullscreen mode

Alternativamente, se o ator mal-intencionado estiver tentando drenar o gás da transação e NÃO causar uma reversão “sem gás”, pode calcular um deslocamento dinâmico com base no gás restante (usando a função gasleft()) para desperdiçar quase todo o gás sem causar uma reversão.

Prevenção

É importante validar os dados de retorno de uma chamada de função que reverteu

  • Valide o seletor “0x08c379a0” nos primeiros 4 bytes de dados.
  • Valide se o deslocamento para a carga é limitado a um número pequeno, como 200.
  • Valide se o comprimento da string é menor que o tamanho dos dados codificados do deslocamento.

Resumo

Entender como as funções integradas, como o decodificador e o codificador ABI funcionam sob o capô, é essencial para manter o ecossistema Web3 seguro.

Qualquer dúvida, sinta-se à vontade para entrar em contato comigo no twitter: https://twitter.com/0xdeadbeef____


Artigo escrito por Xdeadbeefx. Traduzido por Marcelo Panegali.

Latest comments (0)