k2gutierrez/POC_Bank_Attack
GitHub: k2gutierrez/POC_Bank_Attack
一个基于 Solidity 和 Foundry 的 Web3 智能合约重入攻击概念验证项目,包含漏洞利用模拟与安全修复方案的完整对比演示。
Stars: 0 | Forks: 0
🏦 PoC Bank Attack (Reentrancy)
使用 Foundry 进行智能合约漏洞与安全状态管理的实战演示。
## 📖 关于本项目
**PoC Bank Attack (Reentrancy)** 是一个使用 **Solidity** 构建、并通过 **Foundry** 框架进行测试的 Web3 智能合约安全项目。其核心在于,该项目为去中心化金融中最臭名昭著的漏洞之一——重入攻击,提供了一个高度逼真的概念验证。
该仓库包含一个存在漏洞的银行合约、一个旨在利用该漏洞的攻击者合约,以及一个经过安全重构的银行合约。这种架构完美地说明了恶意攻击者是如何在合约更新其内部状态之前,通过递归调用提款函数来抽干协议资金的。
本项目展示了对 EVM 执行上下文、fallback 函数、恶意向量分析以及使用 **Checks-Effects-Interactions** 模式进行防御性编程的深刻理解。对于希望编写坚不可摧代码的 Web3 安全研究人员、智能合约审计员和 DeFi 开发者来说,这是一个理想的展示案例。
**核心技术亮点:**
* **Solidity `0.8.24`:** 证明了即使是现代的 Solidity 编译器,也需要进行严谨的架构设计以防止重入攻击。
* **安全缓解措施:** 实现了状态优先的余额更新机制,以中断递归执行流程。
* **Foundry 框架:** 包含了高速测试、执行模拟、控制台日志记录,以及通过 Arbitrum RPC(或本地 Anvil 环境)进行的状态断言。
## ⚙️ 工作原理
该项目分为三个主要组件:
1. **`SimpleBank`:** 一个允许用户存取 ETH 的存在漏洞的合约。漏洞在于,在状态映射中将用户余额清零*之前*,就将 ETH 转移给了用户。
2. **`Attacker`:** 一个恶意合约,它会存入少量 ETH,发起提款请求,并利用 `receive()` fallback 函数,在银行更新攻击者余额之前,递归调用银行的 `withdraw()` 函数。
3. **`FixedBank`:** 银行的安全版本,它会将用户余额缓存起来,在映射中将其清零(即*效应*),然后才转移 ETH(即*交互*)。
### 架构图

[SimpleBank.sol](./src/SimpleBank.sol) - 存在漏洞的银行合约
[FixedBank.sol](./src/FixedBank.sol) - 安全的银行合约
[Attacker.sol](./src/Attacker.sol) - 恶意利用合约
## 💻 技术文档
应用程序的主要交互点处理了对存在漏洞的 `withdraw` 函数的利用,以及修复后合约中的架构修正。
### 漏洞 (`SimpleBank.sol`)
请注意,对 `msg.sender` 的外部 `call` 是如何在 `userBalance[msg.sender]` 被设置为 `0` *之前*发生的。这就为重入攻击留下了可乘之机。
```
function withdraw() public {
require(userBalance[msg.sender] >= 1 ether, "User has not enough balance");
require(address(this).balance > 0, "Ban is rekt");
// VULNERABILITY: External interaction before state update
(bool success, ) = msg.sender.call{value: userBalance[msg.sender]}("");
require(success, "fail");
userBalance[msg.sender] = 0;
}
```
### 利用过程 (Attacker.sol)
攻击者发起攻击,但真正的破坏发生在 `receive()` 函数内部。每次 SimpleBank 发送 ETH 时,都会触发该 receive 函数,并在银行仍认为攻击者有余额时,立即再次调用 `withdraw()`。
```
function attack() external payable {
simpleBank.deposit{value: msg.value}();
simpleBank.withdraw(); // Initiates the first withdrawal
(bool success, ) = msg.sender.call{value: address(this).balance}("");
require(success, "Transfer Failed");
}
// Triggered automatically when receiving ETH from the bank
receive() external payable {
if (address(simpleBank).balance >= 1 ether) {
simpleBank.withdraw(); // Recursive call!
}
}
```
### 修复方案 (FixedBank.sol)
通过将余额缓存到 memory 中,并在与外部地址交互之前将 storage 状态更新为 0,递归循环被打破了。如果攻击者尝试重入,`require` 检查将会失败。
```
function withdraw() public {
require(userBalance[msg.sender] >= 1 ether, "User has not enough balance");
require(address(this).balance > 0, "Ban is rekt");
// MITIGATION: Checks-Effects-Interactions pattern
uint256 balance = userBalance[msg.sender];
userBalance[msg.sender] = 0; // State is updated first
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "fail");
}
```
🚀 执行示例
以下是测试套件如何模拟对 `SimpleBank` 合约进行攻击的分步示例。
步骤 1:设置与注资
部署 `SimpleBank` 合约。一个合法用户(USER1)向银行存入 50 ETH 以提供流动性。攻击者获得了 10 ETH 的资金。
步骤 2:攻击者初始化
攻击者部署 `Attacker` 合约,并将其指向存在漏洞的 `SimpleBank` 地址。
步骤 3:实施攻击
攻击者使用 10 ETH 调用 `attack()`。攻击者合约将这 10 ETH 存入 `SimpleBank`,并立即调用 `withdraw()`。
步骤 4:重入循环
`SimpleBank` 验证了 10 ETH 的余额,并向攻击者合约发送 10 ETH。攻击者合约的 `receive()` 函数接收了该 ETH,并注意到银行仍有资金,于是立即再次调用 `withdraw()`。由于 `SimpleBank` 尚未执行到将攻击者余额设置为 0 的代码行,它通过了检查并再次发送了 10 ETH。
步骤 5:后果
这个循环会一直持续,直到银行彻底耗尽 USER1 的 50 ETH。测试断言证明,尽管 USER1 的内部状态仍然声称他们拥有 50 ETH,但银行实际的 ETH 余额为 0,而攻击者总共卷走了 60 ETH(其最初的 10 ETH + 盗窃的 50 ETH)。
步骤 6:修复后的银行模拟
当针对 `FixedBank` 尝试完全相同的流程时,交易会安全地回滚。`FixedBank` 在发送资金之前将余额更新为 0,这意味着 `receive()` 循环在第二次执行时会立即无法通过 `userBalance[msg.sender] >= 1 ether` 的检查。
⬆️ 安装
确保您的机器上已安装 Foundry。使用以下命令安装所需的项目依赖:
```
forge install foundry-rs/forge-std
```
🧪 测试
```
forge test -vvvv)
```
📊 覆盖率
```
forge coverage
```
标签:Foundry, Solidity, Web3, 区块链安全, 智能合约