Jerrytriple8/sol-reentrancy-scanner
GitHub: Jerrytriple8/sol-reentrancy-scanner
一款基于纯 Python 正则表达的 Solidity 智能合约重入漏洞静态分析工具,支持 CI/CD 集成与多级严重度报告。
Stars: 0 | Forks: 0
# sol-reentrancy-scanner
用于检测 Solidity 智能合约中重入漏洞的静态分析工具。没有外部 Solidity 解析器依赖 —— 纯 Python 正则表达式分析。
## 什么是 Reentrancy?
重入是最具破坏性的智能合约漏洞之一。在 [2016 DAO 攻击](https://blog.openzeppelin.com/15-lines-of-code-that-could-have-prevented-thedao-hack)中,攻击者利用重入漏洞窃取了约 360 万个 ETH(当时价值约 6000 万美元)。
这种模式通常发生在合约执行以下操作时:
1. 发起一次 **外部调用**(发送 ETH 或调用其他合约)
2. 被调用的合约 **回调** 原始函数
3. 状态尚未更新,因此攻击者可以 **重复执行** 存在漏洞的代码
```
Attacker VulnerableContract
│ │
│──── withdraw() ─────────────>│
│ │─── .call{value} ──> Attacker
│ │ ↑
│──── withdraw() ─────────────>│ (callback!)
│ │ balance not zeroed!
│ │─── .call{value} ──> Attacker
│ │
│<─── ETH ─────────────────────│ (double withdrawal)
```
## 工具检测内容
### 1. 单函数重入 (HIGH)
在**同一函数**中,外部调用之后紧跟状态更改。
```
function withdraw() { // ← VULNERABLE
uint256 amount = balances[msg.sender];
(bool ok,) = msg.sender.call{value: amount}(""); // ← External call
balances[msg.sender] = 0; // ← State change AFTER call
}
```
### 2. 跨函数重入 (MEDIUM)
**函数 A** 中的外部调用可以通过共享状态的**函数 B** 被利用。
```
function withdraw() external { // ← Makes external call
uint256 amount = balances[msg.sender];
(bool ok,) = msg.sender.call{value: amount}("");
balances[msg.sender] = 0;
}
function transfer(address to, ...) { // ← Shares 'balances' state
balances[msg.sender] -= amount; // ← Attacker can call this during callback
balances[to] += amount;
}
```
### 3. 只读重入 (LOW)
```
function withdraw() external { // ← External call
(bool ok,) = msg.sender.call{value: balances[msg.sender]}("");
balances[msg.sender] = 0;
}
function getBalance() external view { // ← Reads state during callback
return balances[msg.sender]; // ← May return stale data
}
```
### 4. 跨合约重入 (MEDIUM)
向合约 X 发起外部调用,随后进行状态更改。合约 X 可能会进行回调。
```
// ContractA
function swap() external { // ← Calls ContractB
uint256 tokenBal = tokenB.balanceOf(address(this));
tokenB.transfer(msg.sender, tokenBal); // ← External call
tokenB.balance -= tokenBal; // ← State change after
}
```
## 安装
```
git clone https://github.com/Jerrytriple8/sol-reentrancy-scanner.git
cd sol-reentrancy-scanner
pip install -r requirements.txt
```
唯一依赖:用于格式化输出的 `rich>=13`。解析部分是纯 Python 实现。
## 用法
### 扫描单个文件
```
python cli.py examples/VulnerableVault.sol
```
### 扫描目录
```
python cli.py ./contracts/
```
### JSON 输出(用于 CI 流水线)
```
python cli.py ./contracts/ --json
```
### 按严重程度过滤
```
python cli.py ./contracts/ --severity HIGH
python cli.py ./contracts/ --severity MEDIUM # shows HIGH + MEDIUM
python cli.py ./contracts/ --severity LOW # shows HIGH + MEDIUM + LOW
```
### CI 集成
```
# Exit code: 0 = 未发现 HIGH/MEDIUM,1 = 发现 HIGH/MEDIUM
python cli.py ./contracts/ --json --severity MEDIUM > report.json
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
echo "Reentrancy vulnerabilities found!"
cat report.json
exit 1
fi
```
### 静默模式(抑制进度消息)
```
python cli.py ./contracts/ -q --json
```
## 示例输出
### Rich 表格(默认)
```
Scanning 3 file(s)...
Reentrancy Scanner Results — 2 HIGH, 1 MEDIUM, 1 LOW
┌──────────┬──────────────────┬────────────────────┬──────────────────┬──────────┬────────────────┬──────────────────────────────┐
│ Severity │ Pattern │ Contract │ Function │ Call Line│ State Var │ File │
├──────────┼──────────────────┼────────────────────┼──────────────────┼──────────┼────────────────┼──────────────────────────────┤
│ HIGH │ Single-Function │ VulnerableVault │ withdraw │ 22 │ balances │ VulnerableVault.sol │
│ HIGH │ Single-Function │ CrossFunctionVault │ withdraw │ 20 │ balances │ CrossFunctionVault.sol │
│ MEDIUM │ Cross-Function │ CrossFunctionVault │ withdraw │ 20 │ balances │ CrossFunctionVault.sol │
│ LOW │ Read-Only │ VulnerableVault │ withdraw │ 22 │ balances │ VulnerableVault.sol │
└──────────┴──────────────────┴────────────────────┴──────────────────┴──────────┴────────────────┴──────────────────────────────┘
Detailed Findings:
1. [HIGH] single reentrancy in VulnerableVault.withdraw
File: examples/VulnerableVault.sol
External call: line 22 — (bool success, ) = msg.sender.call{value: balance}("");
State variable at risk: balances
State variable 'balances' is written at line 26 after external call at line 22 in function 'withdraw'.
Fix: Apply checks-effects-interactions pattern: perform all state changes BEFORE external calls.
```
### JSON 输出
```
{
"total_findings": 3,
"summary": {
"HIGH": 2,
"MEDIUM": 1
},
"findings": [
{
"severity": "HIGH",
"pattern_type": "single",
"contract": "VulnerableVault",
"function": "withdraw",
"function_line": 17,
"call_line": 22,
"call_text": "(bool success, ) = msg.sender.call{value: balance}(\"\")",
"state_variable": "balances",
"file": "/path/to/VulnerableVault.sol",
"description": "State variable 'balances' is written at line 26 after external call at line 22 in function 'withdraw'.",
"suggestion": "Apply checks-effects-interactions pattern: perform all state changes BEFORE external calls."
}
]
}
```
## 运行测试
```
python tests/test_scanner.py
```
## CI/CD 集成
### GitHub Actions
```
name: Reentrancy Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install -r requirements.txt
- name: Scan contracts
run: python cli.py ./contracts/ --json --severity MEDIUM > findings.json
- name: Upload findings
if: always()
uses: actions/upload-artifact@v4
with:
name: reentrancy-findings
path: findings.json
```
如果检测到 HIGH 或 MEDIUM 严重程度的发现,工具将以退出代码 1 退出,这使其非常适用于 CI。
## 识别出的安全模式
扫描器会识别并跳过以下安全模式:
| 模式 | 检测行为 |
|---------|-----------|
| `nonReentrant` 修饰符 | 跳过该函数 |
| ReentrancyGuard 导入 | 将合约标记为受保护 |
| `.transfer()` 调用 | 标记为 INFO(2300 gas 限制)|
| `.send()` 调用 | 标记为 INFO(2300 gas 限制)|
| 遵循 CEI 模式 | 不生成发现 |
## 局限性
- **基于正则表达式的解析**:不是完整的 Solidity AST 解析器。可能会遗漏复杂/嵌套语法中的边缘情况。
- **误报**:状态变量是通过启发式方法检测的;局部变量可能会被错误分类为状态变量。
- **无数据流分析**:无法跟踪值在表达式中的流动方式。
- **无跨文件解析**:Import 解析较为浅层 —— 仅检查 ReentrancyGuard 的 import 语句。
- **修饰符检测**:仅检测 `nonReentrant`;自定义的重入防护可能无法被识别。
- **未解析 Assembly**:不对内联 Assembly(`assembly { ... }`)进行分析。
- **可升级合约**:代理模式可能会导致误报/漏报。
请将此工具作为初步筛选手段,结合更深入的分析(Slither、Mythril、形式化验证)一起使用。
### 添加新检测器
1. 在 `detectors.py` 中创建一个包含 `detect(contracts)` 方法的类
2. 返回一个 `Finding` 对象列表
3. 将该检测器添加到 `run_all_detectors()` 中
4. 添加一个包含示例 `.sol` 文件的测试用例
## License
MIT
标签:逆向工具