WEB3DEV

Cover image for Podem as ferramentas de hacking de contratos inteligentes hackear desafios de Segurança “CTF”?
Paulo Gio
Paulo Gio

Posted on

Podem as ferramentas de hacking de contratos inteligentes hackear desafios de Segurança “CTF”?

Em meu último artigo sobre ferramentas de hacking, destaquei que elas são muito boas para detectar falhas óbvias, como reentrância, overflows de número inteiros, retirada de ether desprotegida.

Mas essas falhas são muito fáceis! Todo desenvolvedor de Solidity é capaz de vê-las em um piscar de olhos. Agora vamos testar usando contratos inteligentes mais difíceis.

Regras

Usaremos as mesmas 4 ferramentas do episódio anterior:

  • Slither.
  • Mythix.
  • Mythril.
  • Plugin de análise estática do Remix.

Eles são bem fáceis de instalar.

Os contratos inteligentes dos seguintes desafios Capture the Flag - CTFs (Capture a Bandeira) serão usados para testar nossas ferramentas:

  • Capture the Ether (Capture o ETH)
  • DWVA DEFI
  • Ethernaut

Se a ferramenta encontrar exatamente a falha crítica, ganha 1 ponto.

Se a ferramenta encontrar algo suspeito, ganha 0,5 ponto.

Se a ferramenta não encontrar a falha crítica, ganha 0 ponto.

Agora, estamos prontos! Vamos lá!

ROUND 1 CTF: Falha matemática

Fonte: https://ethernaut.openzeppelin.com/level/0xC084FC117324D7C628dBC41F17CAcAaF4765f49e

(O código destacado é a falha)

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';

contract Dex is Ownable {
 using SafeMath for uint;
 address public token1;
 address public token2;
 constructor() public {}

 function setTokens(address _token1, address _token2) public onlyOwner {
   token1 = _token1;
   token2 = _token2;
 }

 function addLiquidity(address token_address, uint amount) public onlyOwner {
   IERC20(token_address).transferFrom(msg.sender, address(this), amount);
 }

 function swap(address from, address to, uint amount) public {
   require((from == token1 && to == token2) || (from == token2 && to == token1), "Tokens invalidos");
   require(IERC20(from).balanceOf(msg.sender) >= amount, "Sem saldo suficiente para swap");
   uint swapAmount = getSwapPrice(from, to, amount);
   IERC20(from).transferFrom(msg.sender, address(this), amount);
   IERC20(to).approve(address(this), swapAmount);
   IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
 }

 function getSwapPrice(address from, address to, uint amount) public view returns(uint){
//Falha Abaixo
   return((amount *IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
//Falha Acima
 }

 function approve(address spender, uint amount) public {
   SwappableToken(token1).approve(msg.sender, spender, amount);
   SwappableToken(token2).approve(msg.sender, spender, amount);
 }

 function balanceOf(address token, address account) public view returns (uint){
   return IERC20(token).balanceOf(account);
 }
}

contract SwappableToken is ERC20 {
 address private _dex;
 constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply) public ERC20(name, symbol) {
       _mint(msg.sender, initialSupply);
       _dex = dexInstance;
 }

 function approve(address owner, address spender, uint256 amount) public returns(bool){
   require(owner != _dex, "InvalidApprover");
   super._approve(owner, spender, amount);
 }
}
Enter fullscreen mode Exit fullscreen mode

O jogador tem 10 dos tokens A e B, o contrato (DEX) tem 100 de A e B.

O objetivo é drenar a DEX do token A ou do token B.

A falha é que este contrato é bem simples. Na linha de código destacada, há uma “vulnerabilidade matemática” que permitirá ao jogador trocar o token de forma a obter mais tokens do que antes.

Nossas ferramentas podem detectar as falhas?

Resultados

  • Descoberta Slither (+0): encontrou uma transferência não checada, esta não é a falha…
  • Descoberta Mythril (+0): encontrou um underflow em “aumentar permissão”, mas esta não é a principal falha.
  • Descoberta Mythix (+0.25): encontrou um overflow, swap(), mas não é a falha principal.
  • Descoberta Remix (+0): Nada encontrado (além de uma reentrância falsa positiva..)

ROUND 2 CTF: Preveja o hash do bloco

Fonte: https://capturetheether.com/challenges/lotteries/predict-the-block-hash/

pragma solidity ^0.4.21;

contract PredictTheBlockHashChallenge {
   address guesser;
   bytes32 guess;
   uint256 settlementBlockNumber;

   function PredictTheBlockHashChallenge() public payable {
       require(msg.value == 1 ether);
   }

   function isComplete() public view returns (bool) {
       return address(this).balance == 0;
   }

   function lockInGuess(bytes32 hash) public payable {
       require(guesser == 0);
       require(msg.value == 1 ether);

       guesser = msg.sender;
       guess = hash;
       settlementBlockNumber = block.number + 1;
   }

   function settle() public {
       require(msg.sender == guesser);
       require(block.number > settlementBlockNumber);
//Falha Abaixo
       bytes32 answer = block.blockhash(settlementBlockNumber);
//Falha Acima
       guesser = 0;
       if (guess == answer) {
           msg.sender.transfer(2 ether);
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

Este desafio é um pouco mais difícil de explicar, mas basicamente:

  1. Eu chamo a função lockInGuess e forneço um valor byte32 e 1 ETH

    O valor byte32 é gravado no armazenamento.

  2. Eu chamo a função settle().

    block.blockhash() é calculado e comparado ao nosso valor fornecido anteriormente.

    Se for falso, tenho que tentar novamente no início chamando lockInGuess().

Mas…

block.blockhash retorna o hash do bloco apenas do bloco atual, e é superior a no máximo 256, caso contrário, se pedir para recuperar um hash de bloco mais antigo (mais antigo que o bloco 256, que equivale a cerca de 50 minutos), ele retornará 0x000..000.

Portanto, a resposta é fornecer 0x000.000, aguardar 257 blocos e chamar a função liquid().

  • Descoberta Slither (+0,25): PredictTheBlockHashChallenge.settle() (2.sol#25–35) usa uma igualdade estrita perigosa:

    guess == answer (OK, mas não ajuda a resolver o desafio)

  • Descoberta Mythril (+0.25): Uso potencial de “block.number” como fonte de aleatoriedade. (OK, mas não ajuda a resolver o desafio)

  • Descoberta Mythix (+0,25): Uma decisão de fluxo de controle é feita com base em uma variável previsível. (igual aos 2 primeiros)

  • Descoberta Remix (+0,75): Uso de “blockhash”: “blockhash(uint blockNumber)” é usado para acessar os últimos 256 hashes de bloco. EXCELENTE!!! O Remix quase encontrou a falha!

ROUND 3 CTF: Variável Privada

Fonte: https://ethernaut.openzeppelin.com/level/0xf94b476063B6379A3c8b6C836efB8B3e10eDe188

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Vault {
 bool public locked;
 bytes32 private password; // falha

 constructor(bytes32 _password) public {
   locked = true;
   password = _password; // falha
 }

 function unlock(bytes32 _password) public {
   if (password == _password) { // password == _password - falha
     locked = false;
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

O objetivo deste desafio é ler a variável-chave no armazenamento, que é privada. Felizmente, para nós a “vulnerabilidade” é muito simples, uma variável privada na blockchain não significa que você não possa lê-la.

Você simplesmente não pode acessá-la dentro de outro contrato inteligente, mas pode usar web3.getStorageAt(address,slot) para obter o valor privado e passar no desafio.

Nesse caso, precisamos definir locked como falso para passar no desafio.

  • Descobertas Slither (+0): Nada encontrado.
  • Descobertas Mythril (+0): Nada encontrado.
  • Descobertas Mythix (+0): Nada encontrado.
  • Descobertas Remix (+0): Nada encontrado.

Este foi mais difícil para nossas ferramentas porque elas não conseguem entender o propósito do código, por isso são muito limitadas.

ROUND 4 CTF: Flash Loan (Empréstimo Relâmpago)

Fonte: https://github.com/tinchoabbate/damn-vulnerable-defi/blob/v2.2.0/contracts/truster/TrusterLenderPool.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
* @title TrusterLenderPool
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/

contract TrusterLenderPool is ReentrancyGuard {
   using Address for address;
   IERC20 public immutable damnValuableToken;
   constructor (address tokenAddress) {
       damnValuableToken = IERC20(tokenAddress);
   }    
   function flashLoan(uint256 borrowAmount,address borrower,address target,bytes calldata data) external nonReentrant {
       uint256 balanceBefore = damnValuableToken.balanceOf(address(this));
      require(balanceBefore >= borrowAmount, "Tokens insuficientes no pool");
       damnValuableToken.transfer(borrower, borrowAmount);
       target.functionCall(data); // falha
       uint256 balanceAfter = damnValuableToken.balanceOf(address(this));
      require(balanceAfter >= balanceBefore, "Emprestimo relampago nao foi pago");
   }
}
Enter fullscreen mode Exit fullscreen mode

O contrato inteligente é um pool de empréstimos com 1 milhão de tokens, mas não temos nada, e o objetivo é drenar o pool.

Vemos que o pool pode chamar qualquer contrato (com dados) que fornecemos (oficialmente para fazer o empréstimo relâmpago). Portanto, neste caso, podemos aprovar o jogador (nós!) no token com o contexto do contrato.

Depois disso, podemos drenar os fundos do contrato inteligente. Este passo é bem fácil e é tipo um “presente”.

  • Descoberta Slither (+0): Igual ao remix.
  • Descoberta Mythril (+0,5): possível chamada para contrato inteligente arbitrário (nada mal…).
  • Descoberta Mythix (+ 0): não é possível executar o contrato inteligente rsrs. (depois de 45 min de tentativa decidi desistir).
  • Descoberta Remix (+0): Uso de “call”: deve ser evitado sempre que possível. Pode levar a um comportamento inesperado se o valor de retorno não for tratado corretamente. NÃO. Isso não conta como uma descoberta.

Resumo dos resultados

Vamos resumir o resultado:

Slither (Total: +0,25/4)

Mythix (Total: +0,75/4)

Mythril (Total: +0,5/4)

Remix (Total: +0,75/4)

O vencedor é:

Conclusão

Então, você deve confiar em uma ferramenta simples para proteger seu contrato inteligente?

Claro que não, pois esses contratos inteligentes são bem simples e até as melhores ferramentas têm dificuldades. E se eles auditassem 1.500 linhas de código derivadas de 4 contratos inteligentes?

Eles podem, no máximo, ajudá-lo a remover falhas óbvias que você pode ter perdido por causa de desatenção mas, meramente, só um pouco mais.

Portanto, a melhor coisa a fazer é uma revisão profunda à mão :)

Artigo original escrito por TrustChain. Traduzido por Paulinho Giovannini.

Top comments (0)