Recentemente, tenho revisado meus conhecimentos em Solidity para consolidar os detalhes e também para escrever um "WTF Solidity Simplificado" para iniciantes (os programadores experientes podem procurar outros tutoriais). Estarei postando de 1 a 3 lições por semana.
Twitter: @0xAA_Science | @WTFAcademy_
Comunidade: Discord | Grupo no WeChat | Site Oficial wtf.academy
Todo o código e tutoriais são de código aberto no GitHub: github.com/AmazingAng/WTF-Solidity
Nesta lição, vamos discutir o ataque de phishing tx.origin em contratos inteligentes e métodos para prevenir esse tipo de ataque.
Quando eu estava no ensino fundamental, gostava muito de jogar videogame, mas os desenvolvedores, para evitar o vício de menores de idade, implementaram uma política em que apenas jogadores com mais de dezoito anos, verificados por meio de um número de identidade, poderiam jogar sem restrições. Como contornar essa política? Eu usei o número de identidade dos meus pais para verificar minha idade e consegui burlar o sistema. Esse caso se assemelha ao ataque de phishing com tx.origin.
No Solidity, o uso de tx.origin
permite obter o endereço que iniciou a transação e é semelhante ao msg.sender
. A diferença é que, se o usuário A chamar o contrato B, que por sua vez chama o contrato C, o msg.sender
em C será o contrato B e o tx.origin
será o usuário A. Para entender mais sobre o mecanismo de call
, você pode ler a S22. Chamada de Função.
Dessa forma, se um contrato bancário usa tx.origin
para autenticar a identidade, um hacker poderá implantar um contrato malicioso e induzir o proprietário do contrato a chamá-lo. Mesmo que o msg.sender
seja o endereço do contrato malicioso, o tx.origin
será o endereço do proprietário do contrato bancário, o que permitirá a realização de transferências.
Vamos analisar um contrato bancário simples, com uma variável de estado owner
para registrar o proprietário do contrato. Ele possui um construtor e uma função pública:
- Construtor: atribui o valor de
msg.sender
à variávelowner
durante a criação do contrato. transfer()
: esta função recebe os parâmetros_to
e_amount
e verifica setx.origin == owner
antes de transferir a quantidade_amount
de ETH para o endereço_to
. Observação: essa função é vulnerável a ataques de phishing!
contract Bank {
address public owner; // Armazena o proprietário do contrato
// Atribui o valor de msg.sender à variável owner
constructor() payable {
owner = msg.sender;
}
function transfer(address payable _to, uint _amount) public {
// Verifica a origem da mensagem !!! Existe um risco de phishing
require(tx.origin == owner, "Not owner");
// Transfere ETH
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
}
Agora, vamos ver o contrato malicioso. Ele possui uma função attack()
simples, que é usada para realizar o ataque de phishing, transferindo todo o saldo do contrato bancário para o endereço do hacker. O contrato possui duas variáveis de estado: hacker
e bank
, que armazenam o endereço do hacker e do contrato bancário a ser atacado, respectivamente.
Ele contém duas funções:
- Construtor: inicializa o endereço do contrato bancário.
attack()
: essa função é usada para induzir o proprietário do contrato bancário a chamar o contrato malicioso, que por sua vez chama a funçãotransfer()
do contrato bancário, verificando setx.origin == owner
e transferindo todo o saldo para o endereço do hacker.
contract Attack {
address payable public hacker;
Bank bank;
constructor(Bank _bank) {
bank = Bank(_bank);
hacker = payable(msg.sender);
}
function attack() public {
bank.transfer(hacker, address(bank).balance);
}
}
1. Defina o value
como 10 ETH, implante o contrato Bank
e o endereço do proprietário owner
será definido como o endereço do contrato implantado.
2. Mude para outra carteira, que será usada como a carteira do hacker, informe o endereço do contrato bancário a ser atacado e implante o contrato Attack
, onde o endereço do hacker será definido como o endereço do contrato implantado.
3. Volte para o endereço owner
e chame a função attack()
do contrato Attack
, induzindo a transferência de todo o saldo do contrato Bank
para o endereço do hacker.
Atualmente, existem duas maneiras de prevenir o ataque de phishing com tx.origin
:
msg.sender
obtém o endereço do remetente direto da chamada atual ao contrato. Verificando o valor de msg.sender
, você pode evitar chamadas maliciosas de contratos externos.
function transfer(address payable _to, uint256 _amount) public {
require(msg.sender == owner, "Not owner");
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
Se você precisa usar tx.origin
, verifique se tx.origin
é igual a msg.sender
para evitar chamadas maliciosas de contratos externos. Porém, essa abordagem rejeitará qualquer chamada de função feita por outros contratos.
function transfer(address payable _to, uint _amount) public {
require(tx.origin == owner, "Not owner");
require(tx.origin == msg.sender, "can't call by external contract");
(bool sent, ) = _to.call{value: _amount}("");
require(sent, "Failed to send Ether");
}
Nesta lição, abordamos o ataque de phishing com tx.origin
em contratos inteligentes e métodos para prevenir esse tipo de ataque: usando msg.sender
em vez de tx.origin
ou verificando se tx.origin == msg.sender
. A primeira abordagem é a mais recomendada, pois a segunda rejeitará chamadas de função feitas por outros contratos.