区块链安全—详谈合约攻击(五)
字数 958 2025-08-22 12:22:15

智能合约安全:重入攻击与防御机制详解

一、重入攻击概述

重入攻击(Reentrancy Attack)是以太坊智能合约中最经典的安全问题之一,其危害性极大且开发者容易忽视。攻击原理是当合约调用外部合约时,外部合约可以通过回调函数再次进入原合约,利用状态更新不及时的漏洞进行恶意操作。

基本攻击模式

mapping (address => uint) private userBalances;

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    if (!(msg.sender.call.value(amountToWithdraw)())) { 
        throw; 
    }
    userBalances[msg.sender] = 0;
}

漏洞分析

  1. 合约先转账后更新余额状态
  2. 转账操作会触发接收者的fallback函数
  3. 攻击者在fallback函数中再次调用withdrawBalance()
  4. 由于余额尚未清零,攻击者可重复提取资金

修复方案

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    userBalances[msg.sender] = 0; // 先更新状态
    if (!(msg.sender.call.value(amountToWithdraw)())) { 
        throw; 
    }
}

关键原则:先完成所有内部状态变更,再执行外部调用

二、复杂场景下的重入攻击

1. 多函数重入

mapping (address => uint) private userBalances;

function transfer(address to, uint amount) {
    if (userBalances[msg.sender] >= amount) {
        userBalances[to] += amount;
        userBalances[msg.sender] -= amount;
    }
}

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    if (!(msg.sender.call.value(amountToWithdraw)())) { 
        throw; 
    }
    userBalances[msg.sender] = 0;
}

攻击路径

  1. 调用withdrawBalance()
  2. 在转账时触发攻击者的fallback函数
  3. fallback函数调用transfer()
  4. 由于余额未清零,攻击者可进行非法转账

2. 跨合约重入

mapping (address => uint) private rewardsForA;
mapping (address => bool) private claimedBonus;

function withdraw(address recipient) public {
    uint amountToWithdraw = userBalances[recipient];
    rewardsForA[recipient] = 0;
    if (!(recipient.call.value(amountToWithdraw)())) { 
        throw; 
    }
}

function getFirstWithdrawalBonus(address recipient) public {
    if (claimedBonus[recipient]) { throw; }
    rewardsForA[recipient] += 100;
    withdraw(recipient); // 外部调用点
    claimedBonus[recipient] = true;
}

攻击路径

  1. 调用getFirstWithdrawalBonus()
  2. 该函数调用withdraw()
  3. withdraw()中的转账触发攻击者合约
  4. 攻击者合约回调getFirstWithdrawalBonus()
  5. 由于claimedBonus未更新,可重复获取奖励

三、防御机制

1. 互斥锁模式

bool private lockBalances;

function withdraw(uint amount) public returns (bool) {
    if (!lockBalances && amount > 0 && balances[msg.sender] >= amount) {
        lockBalances = true;
        if (msg.sender.call(amount)) {
            balances[msg.sender] -= amount;
        }
        lockBalances = false;
        return true;
    }
    throw;
}

注意事项

  • 确保锁能被正确释放
  • 防止永久锁定(如攻击者获取锁后不释放)
  • 避免在锁定状态下进行外部调用

2. 检查-生效-交互模式

推荐的安全开发模式:

  1. 检查所有前置条件
  2. 生效:更新合约内部状态
  3. 交互:最后执行外部调用
function safeBid() payable {
    // 检查
    require(msg.value >= highestBid);
    
    // 生效
    if (highestBidder != address(0)) {
        pendingWithdrawals[highestBidder] += highestBid;
    }
    highestBidder = msg.sender;
    highestBid = msg.value;
    
    // 交互(分离到单独函数)
}

3. 提款分离模式

mapping(address => uint) private refunds;

function bid() payable external {
    require(msg.value >= highestBid);
    if (highestBidder != address(0)) {
        refunds[highestBidder] += highestBid; // 记录退款
    }
    highestBidder = msg.sender;
    highestBid = msg.value;
}

function withdrawRefund() external {
    uint refund = refunds[msg.sender];
    refunds[msg.sender] = 0;
    msg.sender.transfer(refund); // 单独提款
}

四、其他安全注意事项

1. 循环操作风险

// 危险做法
function refundAll() public {
    for(uint x; x < refundAddresses.length; x++) {
        if(!refundAddresses[x].send(refunds[refundAddresses[x]])) {
            throw; // 单个失败会阻塞全部
        }
    }
}

// 安全做法
uint256 nextPayeeIndex;
function payOut() {
    uint256 i = nextPayeeIndex;
    while (i < payees.length && msg.gas > 200000) {
        payees[i].addr.send(payees[i].value);
        i++;
    }
    nextPayeeIndex = i;
}

最佳实践

  • 限制单次操作处理的条目数
  • 跟踪处理进度以便继续
  • 检查剩余gas量

2. 失败处理策略

避免使用可能被阻塞的外部调用作为关键逻辑:

// 危险
if (!currentLeader.send(highestBid)) { throw; }

// 安全
pendingWithdrawals[currentLeader] += highestBid;

五、总结与最佳实践

  1. 状态变更优先:在调用外部合约前完成所有状态变更
  2. 使用提款模式:让用户主动提取资金而非自动发送
  3. 限制循环操作:分批处理大数据集,避免gas耗尽
  4. 互斥锁谨慎使用:确保锁能被正确释放
  5. 遵循检查-生效-交互模式:结构化编写合约函数
  6. 全面测试:特别测试重入场景和边界条件

通过遵循这些原则和模式,开发者可以显著降低智能合约遭受重入攻击的风险,提高合约的安全性。

智能合约安全:重入攻击与防御机制详解 一、重入攻击概述 重入攻击(Reentrancy Attack)是以太坊智能合约中最经典的安全问题之一,其危害性极大且开发者容易忽视。攻击原理是当合约调用外部合约时,外部合约可以通过回调函数再次进入原合约,利用状态更新不及时的漏洞进行恶意操作。 基本攻击模式 漏洞分析 : 合约先转账后更新余额状态 转账操作会触发接收者的fallback函数 攻击者在fallback函数中再次调用withdrawBalance() 由于余额尚未清零,攻击者可重复提取资金 修复方案 关键原则: 先完成所有内部状态变更,再执行外部调用 。 二、复杂场景下的重入攻击 1. 多函数重入 攻击路径 : 调用withdrawBalance() 在转账时触发攻击者的fallback函数 fallback函数调用transfer() 由于余额未清零,攻击者可进行非法转账 2. 跨合约重入 攻击路径 : 调用getFirstWithdrawalBonus() 该函数调用withdraw() withdraw()中的转账触发攻击者合约 攻击者合约回调getFirstWithdrawalBonus() 由于claimedBonus未更新,可重复获取奖励 三、防御机制 1. 互斥锁模式 注意事项 : 确保锁能被正确释放 防止永久锁定(如攻击者获取锁后不释放) 避免在锁定状态下进行外部调用 2. 检查-生效-交互模式 推荐的安全开发模式: 检查 所有前置条件 生效 :更新合约内部状态 交互 :最后执行外部调用 3. 提款分离模式 四、其他安全注意事项 1. 循环操作风险 最佳实践 : 限制单次操作处理的条目数 跟踪处理进度以便继续 检查剩余gas量 2. 失败处理策略 避免使用可能被阻塞的外部调用作为关键逻辑: 五、总结与最佳实践 状态变更优先 :在调用外部合约前完成所有状态变更 使用提款模式 :让用户主动提取资金而非自动发送 限制循环操作 :分批处理大数据集,避免gas耗尽 互斥锁谨慎使用 :确保锁能被正确释放 遵循检查-生效-交互模式 :结构化编写合约函数 全面测试 :特别测试重入场景和边界条件 通过遵循这些原则和模式,开发者可以显著降低智能合约遭受重入攻击的风险,提高合约的安全性。