23 de julho de 2023
Os hackers de contratos inteligentes se tornaram uma ameaça notória para a ascensão das finanças descentralizadas. Só em 2022, houve cerca de 2,7 bilhões de dólares em fundos perdidos devido à exploração das brechas nos contratos inteligentes. Isso representa um aumento de aproximadamente 1250% em relação a 2020!
Essas estatísticas mostram o quão importante é a segurança e a auditoria adequadas do seu código. A auditoria é o processo de revisar o seu código para encontrar possíveis bugs, ambiguidades ou falhas graves em seus contratos. Isso geralmente é feito por empresas de auditoria, embora a auditoria individual também tenha se tornado um novo nicho. No entanto, aqui está algo que você deve saber:
A auditoria não significa que seu código seja 100% “não hackeável”.
A auditoria apenas dá ao seu contrato uma chance melhor de sobreviver no mundo real.
Neste artigo, vamos analisar alguns dos ataques mais conhecidos. No entanto, existem muitos ataques com maior gravidade e que ocorrem com frequência (abordaremos esses em breve).
Isenção de responsabilidade: esses ataques não são organizados em nenhuma ordem de gravidade.
Os Ataques👨🏻💻
Ataque de reentrância
Bem, obviamente, temos que começar com um dos maiores e piores que existem. O infame “DAO Hack” foi realizado usando este mesmo ataque.
Em junho de 2016, uma DAO chamada “The DAO” foi hackeada e o invasor drenou 3,6 milhões de ETH, o que equivalia a cerca de 70 milhões de dólares na época.
O atacante explorou uma falha no código. Ele enviou uma pequena quantia para o contrato e depois chamou a função de retirada recursivamente. Isso fez com que o contrato enviasse ether a ele continuamente a partir do seu saldo. Esta é a premissa básica do Ataque de Reentrância.
Vitalik Buterin, o fundador da Ethereum sugeriu um soft fork em resposta a esse ataque. No entanto, isso levou à divisão da blockchain Ethereum em Ethereum (ETH) e Ethereum Classic (ETC).
A solução?
- Para proteção manual, os desenvolvedores podem usar um conceito semelhante aos Mutex Locks, comumente usados em sistemas operacionais para evitar deadlocks.
- No entanto, o OpenZeppelin tem uma função útil para enfrentar esse ataque e facilitar sua vida. Você pode encontrar mais sobre o mesmo aqui.
Honestamente, este ataque merece seu próprio show. Portanto, examinaremos todos os detalhes técnicos e o funcionamento interno desse ataque em uma postagem posterior.
Ataque de Dependência de Tempo
Isso é mais uma manipulação do que um hack (mais ou menos). Esse tipo de ataque acontecerá se o seu contrato usar block.timestamp
em algum cálculo crucial. O block.timestamp
é uma variável global fornecida pelo Solidity que retorna o tempo atual no formato Unix. Essa variável pode ser acessada em todo o seu código.
uint256 constant private salt = block.timestamp;
function random(uint Max) constant private returns (uint256 result){
uint256 x = salt * 100/Max;
uint256 y = salt * block.number/(salt % 5) ;
uint256 seed = block.number/3 + (salt % 300) + Last_Payout + y;
uint256 h = uint256(block.blockhash(seed));
return uint256((h / x)) % Max + 1; //número aleatório entre 1 e o máximo
}
Mas aqui está o problema: validadores e mineradores têm controle sobre essa variável até certo ponto. Eles podem manipular o valor da variável para produzir resultados favoráveis. Podem alterar totalmente o resultado produzido para garantir o lucro máximo. Assim, se o block.timestamp
for usado de forma a determinar o fluxo do programa ou resultar em qualquer transação, pode muito facilmente ser usado para produzir resultados imprevistos.
Isso pode parecer um ataque pequeno e sem importância, mas pode causar estragos se usado corretamente.
A solução?
De acordo com o Ethereum Yellow Paper, cada marca temporal (timestamp) deve ser maior que a marca temporal do seu pai. Assim, uma boa regra a seguir ao usar marca temporal é:
Se a escala do seu evento dependente do tempo puder variar em 15 segundos e manter a integridade, é seguro usar um
block.timestamp
.
As duas implementações do protocolo Ethereum popularmente conhecidas, Geth e Parity, rejeitam quaisquer blocos com uma marca temporal superior a 15 segundos no futuro.
Ou, se você precisar usar block.timestamp
em uma parte crucial da sua lógica, considere usar dados de Oráculos Descentralizados (como Chainlink).
Ataque de Dependência de Ordem de Transação
O ataque TOD (também chamado de ataque de front-running) geralmente é realizado em exchanges descentralizadas (DEX). O hack da DEX Bancor em 2020, que resultou na perda de 460.000 dólares em ether, é um exemplo de como isso pode ser perigoso.
As transações não são adicionadas imediatamente a uma blockchain. Elas são coletadas em blocos e adicionadas apenas ao livro-razão como parte desses blocos. Quando um novo bloco está sendo criado, o criador do bloco extrai de um pool de transações não verificadas. A ordem na qual as transações são adicionadas aos blocos geralmente é determinada com base nas taxas de transação. Quanto mais altas as taxas, mais rápido sua transação será confirmada.
O atacante utiliza esse sistema para alterar a ordem das transações a seu favor. Eles enviam sua própria transação com taxas mais altas para garantir que ela seja confirmada antes das transações originalmente previstas.
Este ataque é altamente eficaz em um ambiente de troca (Exchange).
O ataque da Bancor: o ataque envolveu a colocação de um grande pedido de compra de um determinado token na exchange Bancor e, em seguida, a utilização de uma segunda transação para retardar intencionalmente o processamento da primeira transação, incluindo um preço de gás muito alto.
Isso fez com que o pedido fosse executado a um preço acima do mercado, permitindo que os hackers lucrassem com a diferença de preço. Os hackers então usaram uma terceira transação para cancelar o pedido de compra original, permitindo-lhes repetir o ataque várias vezes.
Ataque de phishing Tx.Origin
Aqui está outro exemplo de um ataque que faz uso de uma variável global fornecida pelo Solidity. Primeiro, vamos ver como tx.origin
é diferente de msg.sender
:
tx.origin
é usado para retornar o pai chamador absoluto (só pode ser EOA).
msg.sender
é usado para retornar o pai imediato (pode ser EOA/CA).
Agora, digamos que existe um contrato como este:
contract myContract{
address public owner;
constructor(){
owner=msg.sender;
}
function withdraw(address payable recipient) public{
require(tx.origin==owner,"only owner can call");
recipient.transfer(address(this).balance);
}
}
O atacante criará seu próprio contrato, que terá as seguintes funcionalidades:
contract AttackContract {
myContract myContractRef;
address payable attacker;
constructor (myContract _myContractRef, address payable attackerAddress) {
myContractRef = _myContractRef;
attacker = _attackerAddress;
}
receive() external payable {
_myContractRef.withdrawAll(attacker);
}
}
O próximo passo do atacante é enviar um link de phishing para o proprietário do contrato. Quando o proprietário clicar no link e executar a transação, ele chamará a função withdraw
do myContract
. É aqui que a mágica acontece:
Neste caso, tx.origin
será o verdadeiro proprietário do contrato (uma vez que ele/ela chamou a função). Mas a variável recipient
será definida para o endereço do atacante. Isso significa que todo o saldo do contrato será enviado ao atacante.
A solução?
Bem, a única maneira de contornar esse ataque é reduzir o uso de tx.origin
. Como esse ataque usa a arquitetura básica do Solidity, fica difícil criar um gateway para bloqueá-lo. Um bom princípio a ser seguido é:
Use msg.sender sempre que possível. Use tx.origin somente se for absolutamente necessário.
Overflow/Underflow
Os ataques de overflow ou underflow são bastante simples de executar se certas verificações não forem implementadas. Isso pode facilmente causar a quebra de todo o sistema, resultando em resultados inesperados.
Aqui, mais uma vez, o atacante tira vantagem da arquitetura do Solidity. Os tipos de dados de tamanho fixo para números inteiros são especificados pela EVM, o que significa que só podem representar um determinado intervalo de números. O overflow/underflow acontece quando uint
é incrementado/decrementado para além de seu limite. O valor máximo para um uint
é 2 ^ 256 – 1. Se um número inteiro for incrementado para além desse número, acontecerá um overflow e o valor voltará a 0. Da mesma forma, se o valor de uint
cair abaixo de 0, acontecerá um underflow e retornará para o valor máximo, que é 1.157920892E77 — 1
.
//Exemplo de Overflow
function transfer(address _to, uint256 _amount) {
require(balanceOf[msg.sender] >= _amount);
balanceOf[msg.sender] -= _amount;
balanceOf[_to] += _amount;
}
Para overflow: o atacante pode enviar uma grande quantidade de ether para o contrato que excede o valor máximo de um uint. Se o valor da variável amount ultrapassar o limite de 2²⁵⁶, ele vai retornar e tornar o valor igual a 0. Isso pode levar a um resultado inesperado, como o saldo total se tornar negativo e permitir que o atacante roube fundos do contrato.
//Exemplo de Underflow
function withdraw(uint _amount) {
require(balances[msg.sender] – _amount > 0);
msg.sender.transfer(_amount);
balances[msg.sender] -= _amount;
}
Para underflow: os underflows são mais fáceis de alcançar do que seu inverso. Aqui, não há verificação de underflow de número inteiro, o atacante pode retirar grandes quantidades de tokens. Como resultado, o atacante pode retirar mais tokens do que possui e pode até obter um saldo máximo.
A solução?
A maneira mais fácil de proteger seu código de overflow/underflow é usar uma biblioteca fornecida pelo OpenZeppelin chamada Safe Math. Saiba mais sobre o mesmo aqui.
Ataque DOS
DOS ou Denial of Service (Ataque de Negação de Serviço) é um ataque bastante comum no mundo da segurança cibernética. Ele é usado para tornar os servidores completamente inúteis, monopolizando todos os seus recursos.
Da mesma forma, os ataques DOS em contratos inteligentes funcionam criando inúmeras transações redundantes (mas válidas) que bloqueiam o sucesso de transações autênticas.
Vejamos um exemplo de um contrato de Leilão como tal:
contract Auction {
address prevBidder;
uint256 highestBid;
function bid() public payable {
require(msg.value > highestBid, "Need to be higher than highest bid");
// Reembolsa o licitante anterior com lance mais alto, se falhar, então reverte
require(payable(prevBidder).send(highestBid), "Failed to send Ether");
prevBidder= msg.sender;
highestBid = msg.value;
}
}
E o contrato do atacante:
import "./Auction.sol";
contract Attacker{
Auction auction;
constructor(Auction _auctionaddr){
auction = Auction(_auctionaddr);
}
function attack (){
auction.bid{value: msg.value}();
}
}
Uma vez que o atacante chama attack()
e coloca uma quantia maior que o prevBidder
, ele nunca pode ser removido dessa posição. Isso ocorre porque o contrato de ataque não possui uma função receive() necessária para receber qualquer ether de entrada ou, neste caso, o reembolso. Assim, o fluxo do ataque será o seguinte:
- O User1 faz um lance de 5 ether e se torna o licitante com lance mais alto.
- O atacante chama
attack()
com um valor de 7 ether e se torna o novo lance mais alto. 5 ether é devolvido ao User1. - O User2 faz um lance de 10 ether. No entanto, a transação falha quando 7 ether está sendo reembolsado ao atacante (já que nenhuma função receive() está presente).
- Assim, nenhum outro licitante pode ganhar, não importa quão alto seja seu lance. O atacante torna-se o licitante com lance mais alto permanente.
Este é um exemplo muito simplificado que mostra como um ataque DOS pode ser perigoso.
Existem 3 tipos de ataques DOS em contratos inteligentes:
- Reversão inesperada;
- Bloqueio de limite de gás;
- Block Stuffing.
A solução?
Não existe uma solução padrão para impedir o DOS, pois é um ataque muito lógico. Os desenvolvedores devem ter muito cuidado ao criar seus contratos, especialmente em torno de funções receive()
e transações em loops. Um contrato bem auditado lhe dará uma imagem melhor da probabilidade de ser atingido por um ataque DOS.
É isso para este artigo. Espero que agora você tenha uma melhor compreensão de alguns dos ataques possíveis ao seu contrato. Você pode esperar muito mais postagens sobre a segurança de contratos inteligentes daqui em diante!
Para mais postagens informativas, siga-me no Twitter.
Obrigado por ler! 🎉
Esse artigo foi escrito por Varun Doshi e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Top comments (0)