Índice
- Introdução
- Implementação de contrato vulnerável
- Ataque de negação de serviço
- Ataque de Reentrância
- Conclusões
- Referências
Introdução
Neste artigo, simulamos 2 ataques aos contratos inteligentes da blockchain Ethereum, ataques de negação de serviço e reentrância. Começamos com um histórico sobre a rede Principal e de Teste da Ethereum, contratos inteligentes e carteiras digitais e a linguagem de programação Solidity. Em seguida, continuamos implementando e implantando contratos inteligentes vulneráveis na Ropsten, uma rede de teste Ethereum. Por fim, exploramos as vulnerabilidades exatamente como aconteceram em contratos reais na história da Ethereum. Explicamos o fluxo completo dos ataques, incluindo as próprias vulnerabilidades, por que elas acontecem, como explorá-las e, finalmente, como mitigar e evitar esses riscos.
Termos gerais:
- Rede Ethereum[1] — Uma blockchain (como Bitcoin) que permite armazenamento e transferência de valor independente da confiança de terceiros e descentralizada de ponta a ponta. Além disso, ela suporta execução de código e persistência de dados usando a Máquina Virtual Ethereum Turing-completa (EVM).
- Rede Principal — A rede principal da Ethereum (valor real).
- Ropsten — A rede de teste da Ethereum.
- Transação — uma ação desencadeada por uma carteira digital para transferir valor ou executar o código de um contrato inteligente.
- Ether — A moeda nativa usada na rede Ethereum.
- Gás — Uma unidade de medida que determina a taxa a ser paga por um remetente de transação.
Contas:
Um participante na rede Ethereum com um endereço hexadecimal de 20 bytes de comprimento. Existem 2 tipos de contas na Ethereum.
1. EOA (Carteira Digital) — Um par de chaves públicas e privadas geradas usando o Algoritmo de Assinatura Digital de Curva Elíptica (ECDSA) [3]. Este par representa uma carteira na rede Ethereum. É chamada de conta de propriedade externa, pois é de propriedade de um usuário humano usando uma chave privada.
- Chave Privada (256 bits) — Deve ser mantida em segredo e representa a propriedade de um usuário sobre uma carteira e os fundos nela contidos. Esta chave é usada para assinar transações enviadas por esta carteira.
- Chave pública — é usada para derivar o endereço público da carteira que é exposta à rede Ethereum, para interações como recebimento de fundos, envio de fundos, controle de acesso e outras interações on-chain diferentes.
2. Contrato Inteligente — Uma conta que também possui um código executável e um estado aberto publicamente para leitura e uso por qualquer usuário da Ethereum. O código é escrito em bytecodes como assembly ou bytecodes Java, e é executado usando a EVM. Ao contrário da EOA, teoricamente os contratos inteligentes não são de propriedade de ninguém (quase sempre eles são), e seus endereços não são derivados de um par de chaves público-privadas.
- Vulnerabilidades — Como em todo código escrito por humanos, pode haver erros e falhas intencionais e não intencionais no programa. Como o código da Ethereum é público e imutável após a implantação, no curto prazo aumenta a capacidade de exploração dos contratos inteligentes. No entanto, melhora a qualidade geral do código do ecossistema a longo prazo, pois é auditado massivamente.
Contas na Ethereum: EOA e contrato inteligente
Desenvolvimento:
- Solidity [4] — Uma linguagem de programação como Java que é usada para programar contratos inteligentes Ethereum. O código é compilado antes da implantação dos bytecodes EVM usando compiladores '.solc'.
- Remix IDE [5] — Um IDE simples baseado em navegador para Solidity e Ethereum.
- Etherscan.io [6] — Um explorador de blocos que permite explorar os blocos, transações, contas e qualquer outra informação da rede Ethereum usando uma interface web e uma API.
- MetaMask [7] — Uma extensão para navegador que fornece recursos de carteira digital para usuários da Ethereum, como criação, gerenciamento e backup de chaves, envio de transações, visualização de informações da carteira digital, interação com o nó Ethereum, etc.
- DeFi — Finanças Descentralizadas, que é um nome agregador para todos os aplicativos financeiros implantados na Ethereum e outras blockchains inteligentes usando contratos inteligentes.
2. Implementação de Contrato Vulnerável
Implementamos um contrato inteligente com 2 funções vulneráveis, um para DoS e o segundo para Reentrância. O código do contrato foi projetado para permitir que um empregador envie salários para seus funcionários usando a rede Ethereum. A implementação é o mais simples possível, por isso é mais fácil focar e enfatizar as partes importantes dela. Existem 2 tipos de entidades: o proprietário
do contrato (empregador) e os colaboradores
da sua empresa. Existem mais 2 funções que são simples e autoexplicativas.
Todos os contratos inteligentes são desenvolvidos no Remix usando a linguagem Solidity e compilados para bytecodes EVM usando o compilador solc. Além disso, os contratos inteligentes são implantados na rede Ropsten usando a carteira digital MetaMask, e o código-fonte dos contratos é verificado no explorador de blocos Etherscan.io (caso contrário, apenas os bytecodes binários são visíveis). Para simplificar, todas as transações e implantações são iniciadas usando a mesma carteira digital EOA: https://ropsten.etherscan.io/address/0x8020cc47e35cd4e291385c16fb587e270bda39e9
Usando este link, você pode explorar todas as transações e contratos envolvidos neste artigo.
2.1. Funcionalidade Desejável
Depois de desenvolver, compilar, implantar, financiar com 5 Ethers e verificar o código do contrato inteligente chamado Company, podemos explorá-lo no etherscan.io no endereço: https://ropsten.etherscan.io/address/0xef801ac273c1e42556d16a948f3926eed97481df#code
Começamos executando a lógica para a qual o contrato foi desenvolvido. Primeiro, o proprietário utiliza registerEmployee(address employee)
para registrar 2 EOAs como funcionários. Em seguida, enviamos a esses funcionários seus salários de 0,1 Ether usando sendSalaries()
. Todas as transações são bem-sucedidas conforme o esperado e tudo está ótimo até agora.
transação sendSalaries()
: https://ropsten.etherscan.io/tx/0x5e87b6cefeb73f71eb23eb004362d70320117498f8001d1d59d0d90c9bc834b6
pragma solidity ^0.8.6;
contract Company {
address public owner;
address[] public employees;
mapping(address => bool) public isEmployee;
mapping(address => bool) public hasWithdrawnSalary;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Somente o proprietário pode enviar salários aos funcionários.");
_;
}
function registerEmployee(address employee) external onlyOwner {
employees.push(employee);
isEmployee[employee] = true;
hasWithdrawedSalary[employee] = false;
}
/* === funções vulneráveis === */
// 1. Vulnerabilidade DoS
function sendSalaries() external onlyOwner {
for (uint256 i=0;i<employees.length;i++) {
payable(employees[i]).transfer(0.1 ether);
}
}
// 2. Vulnerabilidade de reentrância
function withdrawSalary() external {
require(isEmployee[msg.sender], "Somente os funcionários podem retirar o salário.");
require(!hasWithdrawnSalary[msg.sender], "O funcionário já retirou o salário.");
payable(msg.sender).call{value: 0.1 ether}("");
hasWithdrawnSalary[msg.sender] = true;
}
}
3. Ataque de negação de serviço
3.1. Contextualização
Como o código de um contrato inteligente na Ethereum é aberto publicamente e imutável, fica mais fácil para os invasores explorar a vulnerabilidade e mais difícil (se não impossível) para os desenvolvedores corrigirem os problemas. Negação de serviço [8] é um nome para ataques que tornam os recursos atacados incapazes de fornecer seus serviços. Como o código é imutável, pode-se usar o estado de um contrato para explorar uma vulnerabilidade DoS e atacar o contrato. Ataques semelhantes ao que apresentamos neste artigo foram executados em contratos reais.
// 1. Vulnerabilidade DoS
function sendSalaries() external onlyOwner {
for (uint256 i=0;i<employees.length;i++) {
payable(employees[i]).transfer(0.1 ether);
}
}
Observando mais de perto a função sendSalaries()
, podemos ver que ela interage sobre os funcionários registrados e envia a eles 0,1 Ether usando transfer(0.1 ether)
. Para uma conta de destino sem código como EOA, transfer()
simplesmente transferirá o valor. Para uma conta de contrato inteligente, ele executará a funçãofallback()
ou a funçãoreceive()
em versões mais recentes do compilador [9]. Estas funções são opcionais e possuem uma implementação padrão que simplesmente recebe os fundos recebidos.
No entanto, essas funções podem ser anuladas, permitindo que o código do contrato inteligente decida o que fazer com os fundos recebidos. Isso pode ser usado por contratos mal intencionados para interromper a funcionalidade do contrato do remetente.
3.2. Exploração
Implantamos o contrato mal intencionado AttackerDos e o registramos como um contrato de um funcionário: https://ropsten.etherscan.io/address/0x4080c2acd8939e6b8db4f3dcc977aba22dd6a682#code
pragma solidity ^0.8.6;
contract AttackerDos {
receive() external payable {
revert();
}
}
Agora, quando o dono do contrato da Empresa envia a transação sendSalaries()
, ele envia os salários para todos os 3 funcionários. 2 funcionários são os EOAs anteriores e o 3º é nosso contrato mal intencionado. No entanto, quando o contrato da empresa tenta transferir(0.1 ether
) para nosso contrato mal intencionado, AttackerDos, ele chamará sua função receive
().
Nossa implementação receive
() apenas reverte a execução e se recusa a receber os fundos recebidos. Isso resulta na função transfer()
lançando uma exceção de execução e revertendo toda a transação, o que significa cancelar a transação. Isso resulta em todos os 3 funcionários não recebendo seus salários apenas por causa de um funcionário mal-intencionado. Dado que o código é imutável e não há funcionalidade de remoção para funcionários, afunção
sendSalaries()
sempre será revertida. Portanto, cria um contrato de Negação de Serviço no contrato da Empresa, tornando-o inútil e bloqueando os fundos no contrato para sempre.
Transação sendSalaries()
fracassada: https://ropsten.etherscan.io/tx/0x6677bd867765a30f2ae8bb1c6263e33b89154066da74e0ee3818112b0369435a
3.3. Mitigação
send() — uma opção para mitigar o risco de ataque DoS é mudar
transfer()
porsend()
. Ao contrário detransfer()
,send()
não lança uma exceção, mas retorna verdadeiro ou falso como uma indicação de sucesso. No entanto, cria um novo risco de falha na transferência dos fundos para o usuário devido a um erro inesperado, resultando em fundos bloqueados para sempre no contrato. Assim, de fato, muitos dos usos desend()
são realmente usados comorequire(send()
). Isso se comporta como transferência em termos de lançamento de exceção. Outra questão é quetransfer()
esend()
consomem uma quantidade constante de 2.300 de gás. Isso é problemático, pois os custos de gás das instruções tendem a mudar e pode afetar a eficiência da execução do código ou até mesmo falha devido à exceção ' sem gás'.call{value: amount}('') — Esta é a solução preferida para transferir Ether [10] [11].
call()
é usada para chamar qualquer função de um contrato e não apenas os fallbacks. Por um lado, desta forma a quantidade de gás utilizada não é constante, ao contrário desend()
. Por outro lado, ele abre um novo vetor de ataque chamado 'Reentrância'.Padrão de Retirada — sugere uma implementação lenta em vez de apressada. A vulnerabilidade de um usuário mal-intencionado capaz de afetar todos os outros usuários envolvidos em uma transação pode ser resolvida usando o padrão de retirada. Em vez de enviar Ether para todos os funcionários, o contrato armazenará quanto cada funcionário é elegível para receber. Então, todo usuário que quiser receber seu salário, precisará inicializar uma transação por conta própria e sacar apenas seu salário. Dessa forma, a responsabilidade de receber o salário é do funcionário, e caso ele decida ser mal-intencionado e reverter a transação, o único usuário afetado será ele mesmo. Todos os outros funcionários podem sacar seus salários de forma independente. Implementamos esse padrão em
withdrawSalary()
.
Ataque de Reentrância
4.1. Descrição
Há riscos em contratos que chamam externamente para funções de outros contratos, pois eles não podem assumir o fluxo de controle. Reentrância [12] é quando um contrato chamado chama de volta para o contrato de chamada, antes que a primeira invocação da função de chamada seja concluída. Por exemplo, o contrato A função a()
chama o contrato B função b()
, eb()
chama de volta para a(
) antes que a primeira invocação seja concluída.
Contratos mal intencionados podem usar essa técnica para alterar o fluxo de controle de forma indesejável causando impactos negativos como roubo de tokens. Esta vulnerabilidade ocorre na solução da seção anterior para DoS usando o padrão call()
e de retirada:
// 2. Vulnerabilidade de reentrância
function withdrawSalary() external {
require(isEmployee[msg.sender], "Somente os funcionários podem retirar o salário.");
require(!hasWithdrawnSalary[msg.sender], "O funcionário já retirou o salário.");
payable(msg.sender).call{value: 0.1 ether}("");
hasWithdrawnSalary[msg.sender] = true;
}
Em um fluxo regular simples, um funcionário inicia uma transação withdrawSalary()
para receber seu salário. Primeiro verificamos se o endereço inicial é de fato um funcionário e se ele ainda não retirou seu salário. Então, transferimos o salário para ele e marcamos como pago, para que ele não possa sacar várias vezes.
4.2. Exploração
Implantamos o contrato mal intencionado AttackerReentrancy e o registramos como um contrato de um funcionário: https://ropsten.etherscan.io/address/0x88c3c78e47840826363a933ea74048920ebc6146#code
pragma solidity ^0.8.6;
contract AttackerReentrancy {
address public vulnerableContract = 0xef801Ac273c1E42556D16a948f3926eED97481df;
uint256 numOfReentrances = 0;
receive() external payable {
if (numOfReentrances <= 5) {
numOfReentrances += 1;
vulnerableContract.call(abi.encodeWithSignature("withdrawSalary()"));
}
}
}
Fluxo de ataque:
Enviamos uma pequena fração de ETH para o contrato
AttackerReentrancy
para chamar sua funçãoreceive()
.A função
receive()
do contrato mal intencionado chamawithdrawSalary()
do contrato da Empresa vítima.-
A função
withdrawSalary()
executa 2 verificações de segurança, tem êxito e transfere o 0,1 ETH:-
isEmployee[msg.sender]
- O endereço de chamada deve ser um funcionário. -
!hasWithdrawnSalary[msg.sender] -
O empregado ainda não retirou o seu salário. -
payable(msg.sender).call{value: 0.1 ether}('') -
Transferir o salário para o funcionário.
-
No entanto, antes de marcar o funcionário como tendo retirado seu salário usando
hasWithdrawnSalary[msg.sender] = true
, a função chamadareceive()
do contrato mal intencionado chama de volta parawithdrawSalary()
. Assim, resulta na execução da etapa 2 novamente.As etapas 2 a 4 são executadas novamente, retirando cada vez mais ETH. Este ciclo vicioso é executado 5 vezes de acordo com nosso código, retirando 0,5 ETH no total. Isso pode ser implementado para retirar todos os fundos do contrato da vítima (se fornecermos gás suficiente para a execução da transação).
Finalmente, após a última iteração,
hasWithdrawnSalary[msg.sender] = true
é executado.
Transação reentrante withdrawSalary()
: https://ropsten.etherscan.io/tx/0x1f8e47f3bcf734a3c2a551d9ba405acd3417b05f49cf325519837ac3453e1bb5
4.3. Mitigação
Sem escritas após a chamada — Se observarmos mais de perto a vulnerabilidade, podemos observar que o contrato mal intencionado conseguiu retirar seu salário várias vezes. Isso ocorre porque o contrato da vítima marca um funcionário como
hasWithdrawnSalary[msg.sender] = true
depois de fazer a chamada externa para o contrato mal intencionado. Dessa forma, é possível reinserir a função antes que o estado seja atualizado. Gravar todas as alterações de estado antes da chamada de função externamente para outros contratos reduzirá esse risco. Assim, na segunda iteração, o sinalizadorhasWithdrawnSalary[msg.sender]
já serátrue
, e a execução será revertida.Guarda de reentrância — As funções reentrantes expõem um alto risco aos contratos inteligentes, pois essa vulnerabilidade pode ser mais sofisticada. Por exemplo, a reentrância de cruzamento entre funções [13] envolve várias invocações de funções para explorar a reentrância. Assim, a melhor maneira de mitigar a reentrância é bloqueando-a usando um bloqueio como o modificador
nonreentrant
[14] do OpenZeppelin.
5. Conclusões
Neste artigo, abordamos duas vulnerabilidades de contratos inteligentes da Ethereum. A primeira é a Negação de Serviço e a segunda é a Reentrância. Desenvolvemos e implantamos contratos simples de vítimas e invasores no Ropsten. Por fim, exploramos as vulnerabilidades e discutimos diferentes opções para mitigar esses riscos.
6. Referências
- https://ethereum.org/en/
- https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf
- https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm)
- https://docs.soliditylang.org/en/latest/index.html
- https://remix.ethereum.org/
- https://etherscan.io/
- https://metamask.io/
- https://swcregistry.io/docs/SWC-113
- https://blog.soliditylang.org/2020/03/26/fallback-receive-split/
- https://ethereum.stackexchange.com/questions/19341/address-send-vs-address-transfer-best-practice-usage/38642
- https://ethereum.stackexchange.com/questions/78124/is-transfer-still-safe-after-the-istanbul-update
- https://swcregistry.io/docs/SWC-107
- https://consensys.github.io/smart-contract-best-practices/known_attacks/#cross-function-reentrancy 14.https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard](https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard)
Lista de endereços
- EOA do implantador: https://ropsten.etherscan.io/address/0x8020cc47e35cd4e291385c16fb587e270bda39e9
- Contrato Vulnerável da Empresa: https://ropsten.etherscan.io/address/0xef801ac273c1e42556d16a948f3926eed97481df#code
- Funcionário 1 EOA: https://ropsten.etherscan.io/address/0xc1387017d4ae2cf3cc7da19f977fa74d85df0cdd
- Funcionário 2 EOA: https://ropsten.etherscan.io/address/0x7dab537acb832738f020192a9cdb2b531fa1c599
- Contrato de ataque DoS: https://ropsten.etherscan.io/address/0x4080c2acd8939e6b8db4f3dcc977aba22dd6a682#code
- Contrato de ataque de reentrância: https://ropsten.etherscan.io/address/0x88c3c78e47840826363a933ea74048920ebc6146#code
Este artigo foi escrito por Mark Yosef, a versão original pode ser encontrada aqui. Traduzido e adaptado por Marcelo Panegali.
Top comments (0)