paint-brush
Resolvendo a vulnerabilidade de estouro/subfluxo inteiro em contratos inteligentespor@dansierrasam79
1,873 leituras
1,873 leituras

Resolvendo a vulnerabilidade de estouro/subfluxo inteiro em contratos inteligentes

por Daniel Chakraborty9m2023/02/11
Read on Terminal Reader

Muito longo; Para ler

Os tipos de dados são algo que os programadores levam tempo para especificar ou não, dependendo da linguagem de programação em que codificam. Os tipos de dados são importantes para as principais operações aritméticas, mas têm um alcance limitado para qualquer cálculo. O exemplo mais popular de estouro de número inteiro no mundo real ocorre em veículos, onde o valor das milhas percorridas é redefinido para 000000.
featured image - Resolvendo a vulnerabilidade de estouro/subfluxo inteiro em contratos inteligentes
Daniel Chakraborty HackerNoon profile picture


Quase todos nós usamos o Planilhas Google ou o Microsoft Excel para inserir dados para algum cálculo. Digamos que você deseja inserir os nomes dos funcionários, seus números de telefone, cargos e o salário que recebem.


Em sua forma mais simples, é assim que um registro ou caso ficaria, no Planilhas ou no Excel:

Um registro ou caso no Excel ou no Planilhas Google

Como você pode ver, o nome e o cargo do funcionário consistem em texto, enquanto o número de telefone e o salário consistem em uma sequência de números.


Então, do ponto de vista semântico, nós, como humanos, entendemos o que esses campos significam no mundo real e podemos diferenciá-los.


Claramente, embora você não precise de um diploma em Ciência da Computação para saber a diferença, como um compilador ou interpretador processa esses dados?

Tipos de dados

É aqui que entram os tipos de dados, algo que os programadores demoram a especificar ou não, dependendo da linguagem de programação em que codificam.


Em outras palavras, os pontos de dados sob o nome do funcionário e o cargo são chamados de strings. Claro, o salário é claramente um inteiro, em virtude de não ter casas decimais. Simplificando, esses são tipos de dados que devem ser declarados como tal quando você codificar, para que apenas as operações corretas associadas a esse tipo de dados sejam executadas.


É assim que declaramos um tipo de dado inteiro no Solidity:

Dito isso, o campo Phone Number na planilha acima contém um ponto de dados que será usado como uma string única, mas essa discussão fica para outro dia. Por enquanto, nosso foco estará no tipo de dado primitivo com o qual todos realizamos aritmética básica.


Sim, estamos falando sobre o tipo de dado inteiro que, embora seja importante para as principais operações aritméticas, tem um alcance limitado para qualquer cálculo.

Por que ocorre estouro/subfluxo inteiro?

Provavelmente, o exemplo mais popular de estouro de número inteiro no mundo real ocorre em veículos. Também conhecido como hodômetro, esses dispositivos geralmente rastreiam quantos quilômetros um veículo percorreu.


Então, o que acontece quando o valor das milhas percorridas atinge o valor inteiro sem sinal de 999999 em um odômetro de seis dígitos?


Idealmente, uma vez que mais uma milha é adicionada, esse valor deve chegar a 1000000, certo? Mas isso não acontece porque está previsto um sétimo dígito.


Em vez disso, o valor das milhas percorridas é redefinido para 000000, conforme mostrado abaixo:

Estouro inteiro, em um odômetro


Por definição, como o sétimo dígito não está disponível, isso resulta em 'estouro', pois o valor exato não é representado.


Você entendeu a foto, certo?


Por outro lado, o oposto também pode ocorrer, mesmo que isso não seja tão comum. Ou seja, quando o valor registrado for menor que o menor valor disponível na faixa e que também é conhecido como 'underflow'.


Como todos sabemos, os computadores armazenam números inteiros na memória como seu equivalente binário. Agora, para simplificar, digamos que você esteja usando um registrador de 8 bits.


Então, se você quiser armazenar o inteiro sem sinal 511, isso seria dividido em:


= 2⁸*1 + 2⁷*1 + 2⁶*1 + 2⁵*1 + 2⁴*1 + 2³*1 + 2²*1 + 2¹*1 + 2⁰*1

= 256 + 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1

= 111111111


Onde cada bit é 1 e, como você pode ver, não é possível armazenar um valor maior.


Por outro lado, se você quiser armazenar o número 0 no registrador de 8 bits, ficaria assim:


= 2⁸*0 + 2⁷*0 + 2⁶*0 + 2⁵*0 + 2⁴*0 + 2³*0 + 2²*0 + 2¹*0 + 2⁰*0

= 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0

= 000000000


Onde cada bit é 0, o que deve dizer que você não pode armazenar um valor menor.


Em outras palavras, o intervalo de inteiros permitido para tal registrador de 8 bits é de 0 a 511. Então, é possível armazenar o inteiro 512 ou -1 em tal registrador?


Claro que não. Como resultado, você armazenará um valor semelhante ao valor redefinido de milhas percorridas no exemplo do odômetro, mas como valores binários.


Claramente, você precisaria de registradores com mais alguns bits para acomodar tal número confortavelmente. Ou então, arriscar mais uma vez a situação de transbordamento.


No caso de inteiros com sinal, também armazenamos inteiros negativos. Assim, quando tentamos armazenar um número menor que o intervalo aceito, ou menor que zero, conforme mostrado acima, ocorre o underflow.


Mais uma vez, uma vez que o objetivo de qualquer cálculo é obter resultados determinísticos, isso pode, na melhor das hipóteses, ser irritante, mas, na pior das hipóteses, causar a perda de milhões. Particularmente, quando esses erros de estouro ou estouro de número inteiro ocorrem em contratos inteligentes.

Por que a vulnerabilidade de estouro/subfluxo inteiro pode ser tão prejudicial?

Embora o estouro e o estouro de números inteiros existam há décadas, sua existência como um bug em um contrato inteligente aumentou as apostas. Quando os invasores fazem uso de tais erros, eles podem drenar o contrato inteligente de grandes quantidades de tokens.


Provavelmente a primeira vez que esse tipo de bug ocorreu foi com o bloco 74638, que criou bilhões de Bitcoin para três endereços. Levaria horas para resolver esse erro por meio de um soft fork e que descartasse o bloco, tornando a transação inválida.


Por um lado, as transações com valor superior a 21 milhões de bitcoins foram rejeitadas. Isso não foi diferente para as transações de estouro, como aquela que enviou tanto dinheiro para as três contas mencionadas.


No entanto, os contratos inteligentes da Ethereum também experimentaram estouro e estouro de números inteiros, com o BeautyChain sendo um exemplo proeminente também.


Nesse caso, o contrato inteligente continha uma linha de código defeituosa:


Aquele único código que causou estouro de inteiro


Como resultado, os atacantes foram teoricamente capazes de receber uma quantidade ilimitada de tokens BEC, que teoricamente poderiam chegar a um valor de (2²⁵⁶)-1.


A função batchTransfer vulnerável


Agora, vamos ver outro exemplo de um contrato inteligente no qual ocorre estouro/estouro de número inteiro.

Quebrando a vulnerabilidade de estouro/subfluxo inteiro em um contrato inteligente

À primeira vista, existem dois contratos que interagem neste exemplo, e que demonstram o que acontece no caso de um estouro inteiro.


Como você pode ver abaixo, o contrato TimeLock, permite que você deposite e levante fundos, mas com uma diferença: você só pode realizar o último somente após um período de tempo. Nesse caso, você só pode retirar seus fundos em uma semana.


O contrato TimeLock


No entanto, uma vez que você chama a função de ataque no contrato de ataque, o bloqueio de tempo no local não é mais eficaz e é por isso que o invasor pode retirar o valor do saldo imediatamente.

Em outras palavras, devido a causar um estouro de número inteiro com a instrução type(uint).max+1-timeLock.locktime(address(this)), o bloqueio de tempo é eliminado.


Usando Integer Overflow, o bloqueio de tempo para o depósito é eliminado imediatamente


Por exemplo, depois de implantar os dois contratos inteligentes usando o código acima, você pode testar se o timelock é válido invocando as funções de depósito e retirada no contrato TimeLock, conforme mostrado abaixo:


Saldo, após depositar 2 ETH


Como você pode ver, selecionando uma quantidade de 2 Ether, obtemos o saldo do contrato inteligente de 2 Ether mostrado acima:


Depositando 2 ETH


Especificamente, o endereço específico que contém o saldo de 2 Ether pode ser verificado adicionando o endereço no campo da função de saldos e clicando no botão saldos:


Qual endereço contém 2 ETH?


No entanto, conforme mencionado acima, você ainda não pode sacar esses fundos, devido ao bloqueio de tempo em vigor. Ao olhar para o console depois de pressionar a função de retirada, você encontrará um erro indicado pelo símbolo 'x' vermelho. Como você pode ver abaixo, o motivo desse erro é fornecido pelo contrato é “Lock time not expired”:


Erro de tempo de bloqueio não expirado


Agora, vamos ver o contrato de ataque implantado, conforme mostrado abaixo:



Agora, para invocar a função de ataque, você precisa depositar um valor de 1 Ether ou mais. Portanto, neste caso, selecionamos 2 Ether, conforme mostrado abaixo:


Deposite 2 ETH primeiro!


Depois disso, clique em 'atacar'. Você descobrirá que os 2 Ether que você depositou serão retirados imediatamente e adicionados ao contrato de ataque, conforme evidenciado pelo saldo de 2 Ether abaixo:


2 ETH transferidos para contrato inteligente de ataque


Claramente, isso não deveria acontecer devido ao fato de que o longo bloqueio de tempo deve entrar em vigor assim que você fizer o depósito. É claro que, como sabemos, a instrução type(uint).max+1-timeLock.locktime(address(this)) reduz o tempo de bloqueio usando a função gainLockTime. É exatamente por isso que podemos retirar o saldo do Ether imediatamente.


O que nos leva à pergunta óbvia: existem maneiras de corrigir a vulnerabilidade de estouro e estouro de número inteiro?

2 maneiras de contornar a vulnerabilidade de estouro/subfluxo inteiro

Ao reconhecer que a vulnerabilidade de estouro/subfluxo inteiro pode ser devastadora, algumas correções para esse bug foram implementadas. Vejamos essas duas correções e como elas contornam esse erro:


Método 1: Use a biblioteca SafeMath da OpenZeppelin

O Open Zeppelin, como organização, oferece muito quando se trata de tecnologia e serviços de segurança cibernética, com a biblioteca SafeMath fazendo parte de seu repositório de desenvolvimento de contrato inteligente. Este repositório contém contratos que podem ser importados para o seu código de contrato inteligente, sendo a biblioteca SafeMath um deles.


Vamos ver como uma das funções dentro de SafeMath.sol verifica o estouro de número inteiro:


função tryAdd SafeMath


Agora, uma vez que o cálculo de a+b tenha ocorrido, uma verificação para ver se c<a ocorre. Obviamente, isso só seria verdade no caso de um estouro de número inteiro.


Com a versão do compilador do Solidity chegando a 0.8.0 e superior, as verificações de estouro e estouro de inteiros agora estão incorporadas. Portanto, ainda é possível usar esta biblioteca para verificar essa vulnerabilidade, tanto ao usar a linguagem quanto esta biblioteca. Obviamente, se o seu contrato inteligente exigir uma versão do compilador inferior a 0.8.+, você deverá usar esta biblioteca para evitar estouro ou estouro.


Método 2: Use a versão 0.8.0 do compilador

Agora, como mencionado anteriormente, se para o seu contrato inteligente você estiver usando uma versão do compilador 0.8.0 e superior, esta versão possui um verificador integrado para tal vulnerabilidade.


Na verdade, apenas para verificar se funciona com o contrato inteligente acima, ao alterar a versão do compilador para “^0.8.0” e reimplantá-lo, o seguinte erro 'reverter' é recebido:


Reverter erro que impede estouro de inteiro


Obviamente, nenhum depósito de 2 Ether é realizado, devido à verificação do estouro do valor do bloqueio de tempo. Como resultado, nenhum saque é possível devido ao fato de nenhum fundo ter sido depositado em primeiro lugar.


A transferência de 2 ETH para o contrato de Ataque foi impedida


Sem dúvida, a chamada da função Attack.attack() não funcionou aqui, então está tudo bem!

Resumindo a vulnerabilidade de estouro/subfluxo

Se há algo que você deve aprender com esta longa postagem no blog, é que ignorar essa vulnerabilidade, como no ataque BEC, pode custar caro. Como você também pode ver, se não for verificado, é fácil ocorrer erros não maliciosos. Ou tão simples quanto os hackers explorarem essa vulnerabilidade.


Falando nisso, e usando nossa compreensão de como o ataque BEC ocorreu, reconhecer essa vulnerabilidade pode ajudar bastante na prevenção de ataques ao escrever seus contratos inteligentes, graças às correções oferecidas. Mesmo que existam várias outras vulnerabilidades de contratos inteligentes que estão à espreita para te enganar.