Vulnerabilidade de risco médio-alto no compilador Solidity: Análise de overflow de cabeçalho na ABIv2-Reencoding.
Visão geral
Este artigo fornece uma análise detalhada dos problemas de vulnerabilidade causados pelo compilador Solidity (versão 0.5.8<= <0.8.16) no processo de re-encoding ABIv2 devido à manipulação de erros de array (matriz) de tipos uint
e bytes32
de comprimento fixo, e propõe soluções relevantes e medidas de prevenção.
Detalhes da vulnerabilidade
O formato de codificação ABI (ABI (Application Binary Interface ou Interface Binária de Aplicativo) é um método de codificação padrão usado quando os usuários ou contratos chamam contratos para passar parâmetros durante as chamadas de função. Para obter detalhes, consulte a descrição oficial da codificação ABI do Solidity.
No processo de desenvolvimento de contratos, os dados que precisam ser obtidos a partir dos dados calldata
passados por usuários ou outros contratos podem ser encaminhados ou emitidos após serem obtidos. Devido ao fato de que todas as operações de opcode (código operacional) da máquina virtual evm são baseadas em memória, pilha e armazenamento
, o Solidity codificará os dados em calldata
de acordo com a nova ordem, de acordo com o formato ABI, quando envolver operações que exijam que os dados sejam codificados por ABI e armazenados em memória
.
O processo em si não apresenta grandes problemas lógicos, mas quando combinado com o mecanismo de limpeza do Solidity, devido às omissões no próprio código do compilador do Solidity, existem vulnerabilidades.
De acordo com as regras de codificação da ABI, depois de remover o seletor de função, os dados codificados pela ABI são divididos em duas partes: cabeça e cauda. Quando o formato dos dados é uma array uint
ou bytes32
de comprimento fixo, a ABI armazenará todos os dados desse tipo na parte da cabeça. E a implementação do mecanismo de limpeza na memória do Solidity consiste em definir a memória do próximo índice como vazia depois que a memória do índice atual for usada para evitar que os dados sujos afetem o uso da memória ao usar a memória para índices subsequentes. Além disso, quando o Solidity codifica um conjunto de dados de parâmetro usando a codificação ABI, ele é codificado da esquerda para a direita!!!
Para facilitar a exploração do princípio da vulnerabilidade mais tarde, considere o código do contrato da seguinte forma:
contract Eocene {
event VerifyABI( bytes[],uint[2]);
function verifyABI(bytes[] calldata a,uint[2] calldata b) public {
emit VerifyABI(a, b); // a,b serão armazenados na cadeia após a codificação
}
}
A finalidade da função verifyABI no contrato Eocene
é simplesmente emitir o comprimento variável bytes[] a
e o comprimento fixo uint[2] b
nos parâmetros da função.
Observe que o evento VerifyABI( bytes[],uint[2]);
também acionará a codificação ABI. Aqui, os parâmetros a,b
serão codificados no formato ABI antes de serem armazenados na cadeia.
Compilamos o código do contrato usando a versão v0.8.14 do Solidity, implantamos por meio do remix e passamos verifyABI(['0xaaaaaa','0xbbbbbb'],[0x11111,0x22222])
.
Primeiro, vamos dar uma olhada no formato de codificação correto para verifyABI(['0xaaaaaa','0xbbbbbb'],[0x11111,0x22222])
:
0x52cd1a9c // bytes4(sha3("verify(btyes[],uint[2])"))
0000000000000000000000000000000000000000000000000000000000000060 // index of a
0000000000000000000000000000000000000000000000000000000000011111 // b[0]
0000000000000000000000000000000000000000000000000000000000022222 // b[1]
0000000000000000000000000000000000000000000000000000000000000002 // length of a
0000000000000000000000000000000000000000000000000000000000000040 // index of a[0]
0000000000000000000000000000000000000000000000000000000000000080 // index of a[1]
0000000000000000000000000000000000000000000000000000000000000003 // length of a[0]
aaaaaa0000000000000000000000000000000000000000000000000000000000 // a[0]
0000000000000000000000000000000000000000000000000000000000000003 // length of a[1]
bbbbbb0000000000000000000000000000000000000000000000000000000000 // a[1]
Se o compilador Solidity estiver funcionando corretamente, quando os parâmetros a,b
forem registrados na cadeia pelo evento, o formato dos dados deverá ser o mesmo que enviamos. Vamos tentar chamar o contrato e verificar o registro na cadeia. Se quiser compará-lo você mesmo, pode dar uma olhada nesta transação.
Após uma chamada bem-sucedida, o evento VerifyABI()
é registrado da seguinte forma:
0000000000000000000000000000000000000000000000000000000000000060 // índice de a
0000000000000000000000000000000000000000000000000000000000011111 // b[0]
0000000000000000000000000000000000000000000000000000000000022222 // b[1]
0000000000000000000000000000000000000000000000000000000000000000 // comprimento de a ??por que se torna 0??
0000000000000000000000000000000000000000000000000000000000000040 // índice de a[0]
0000000000000000000000000000000000000000000000000000000000000080 // índice de a[1]
0000000000000000000000000000000000000000000000000000000000000003 // comprimento de a[0]
aaaaaa0000000000000000000000000000000000000000000000000000000000 // a[0]
0000000000000000000000000000000000000000000000000000000000000003 // comprimento de a[1]
bbbbbb0000000000000000000000000000000000000000000000000000000000 // a[1]
Surpreendentemente, após b[1]
, o valor que armazena o comprimento do parâmetro a
foi excluído incorretamente!!!
Por que isso aconteceu?
Como mencionamos anteriormente, quando o Solidity encontra uma série de parâmetros que exigem codificação ABI, a ordem em que os parâmetros são gerados é da esquerda para a direita. A lógica de codificação específica para a,b
é a seguinte:
1 .O Solidity codifica primeiro a
. De acordo com as regras de codificação, o índice de a
é colocado na cabeça, e o comprimento e o valor específico de a são armazenados na cauda.
2 .Processe os dados b
. Como o tipo de dados de b
está no formato uint[2]
, o valor específico dos dados é armazenado na parte da cabeça. Entretanto, devido ao próprio mecanismo de limpeza do Solidity, após armazenar b[1]
na memória, ele define o valor do endereço de memória imediatamente seguinte (usado para armazenar o comprimento do elemento a) como 0.
3 .A operação de codificação ABI termina e os dados codificados incorretamente são armazenados na cadeia, resultando na vulnerabilidade SOL-2022-6.
No nível do código-fonte, a lógica de erro específica também é muito óbvia. Quando o Solidity precisa obter dados de array bytes32 ou uint de comprimento fixo de calldata (dados de chamada) para a memória, ele sempre define o valor do índice de memória imediatamente após os dados como 0 depois que os dados são copiados. E como a codificação ABI tem partes de cabeça e cauda, e a ordem de codificação também é da esquerda para a direita, isso leva à existência de vulnerabilidades.
O código compilado do Solidity para a vulnerabilidade específica é o seguinte.
Quando o local de armazenamento dos dados de origem é Calldata
e o tipo de dados de origem é ByteArray, String ou o tipo de base do array original é uint ou bytes32
, ele entra em ABIFunctions::abiEncodingFunctionCalldataArrayWithoutCleanup()
.
Após entrar na função, ele primeiro verifica se os dados de origem são uma array de comprimento fixo por meio de fromArrayType.isDynamicallySized()
.
Somente as arrays de comprimento fixo atendem às condições de acionamento da vulnerabilidade.
O resultado do julgamento isByteArrayOrString()
é passado para YulUtilFunctions::copyToMemoryFunction()
, que determina se deve ser feita a limpeza na posição do índice após a operação calldatacopy
de acordo com o resultado do julgamento.
Com a combinação das restrições acima, somente arrays de uint
ou bytes32
com formatos fixos em calldata podem acionar vulnerabilidades quando copiadas para a memória. Ou seja, o motivo das restrições que provocam vulnerabilidades.
Devido ao fato de que a ABI sempre codifica os parâmetros da esquerda para a direita, considerando as condições de exploração da vulnerabilidade, devemos entender que deve haver um tipo de dado de comprimento dinâmico armazenado na parte final do formato de codificação da ABI antes dos arrays uint
e bytes32
de comprimento fixo, e os arrays uint
ou bytes32
de comprimento fixo devem estar localizados na última posição dos parâmetros a serem codificados.
O motivo é óbvio.
Se os dados de comprimento fixo não estiverem localizados na última posição dos parâmetros a serem codificados, a definição de 0 na próxima posição de memória não terá efeito, pois o próximo parâmetro de codificação substituirá essa posição.
Se não houver dados que precisem ser armazenados na parte final antes dos dados de comprimento fixo, não importa que a próxima posição de memória seja definida como 0, pois essa posição não é usada pela codificação ABI.
Além disso, deve-se observar que todas as operações ABI implícitas ou explícitas e todas as tuplas (um grupo de dados) que estejam em conformidade com o formato serão afetadas por essa vulnerabilidade. As operações específicas envolvidas são as seguintes:
event
error
abi.encode*
returns // retorno de uma função
struct // estrutura definida pelo usuário
all external call
Solução
Quando houver operações afetadas pela vulnerabilidade no código do contrato, certifique-se de que o último parâmetro não seja uma array uint
ou bytes32
de comprimento fixo.
Use um compilador Solidity que não seja afetado pela vulnerabilidade.
Procure ajuda de uma equipe de segurança profissional para realizar auditorias de segurança profissional dos contratos.
Sobre nós
Na Eocene Research, fornecemos os informações de intenções e segurança por trás de tudo o que você sabe ou não sabe sobre blockchain e capacitamos cada indivíduo e organização a responder a perguntas complexas com as quais nem sequer sonhávamos naquela época.
Saiba mais: Website | Medium | Twitter
Este artigo foi escrito por Eocene I Security e traduzido para o português por Rafael Ojeda
Você encontra o artigo original aqui
Top comments (0)