区块链与智能合约安全
区块链与智能合约安全
本文适合
已经了解基础公钥密码和 Web/API 交互,准备处理 Solidity 合约、交易 calldata、链上状态和 CTF 链上题的学习者。学完你能:根据合约源码、部署地址、交易记录和题目目标,判断是重入、权限、整数、随机数、签名或业务状态机问题,并写出最小复现交易。
一句话判断
区块链 CTF 题不是“链很复杂”,而是合约状态可公开、交易可复现、代码不可随便改;解题要找到能让合约状态进入目标条件的最小交易序列。
题目中常见信号
说明:合约漏洞题
说明:状态达成题
说明:重入风险
说明:权限或随机数误用
说明:签名绑定错误
说明:链上取证/状态分析
核心概念
智能合约题的证据在链上:部署代码、交易输入、事件日志、storage、余额和调用结果都可以复现。CTF 中常见漏洞包括重入、权限控制错误、整数边界、随机数可预测、签名重放、delegatecall/storage 布局错位和业务状态机跳步。
分析时先明确目标函数,再倒推需要修改的状态变量,而不是从漏洞列表里盲猜。
最小分析流程
- 读题目目标:要拿 owner、取余额、通过
isSolved(),还是提交 flag。 - 编译/阅读合约,标出状态变量、权限检查和外部调用。
- 用本地链或测试脚本复现部署和初始状态。
- 构造最小交易:一次调用、两步授权、重入合约或签名消息。
- 读取 storage、事件和返回值验证状态是否变化。
- 把交易序列写进 WP,而不是只贴最终 tx hash。
最小验证示例
// 重入题最小观察点
function withdraw() public {
uint256 amount = balances[msg.sender];
require(amount > 0);
(bool ok,) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] = 0;
}如果外部转账发生在状态清零之前,攻击合约的 receive() 可以再次调用 withdraw()。验证时要记录:第一次余额、回调次数、目标合约余额变化和最终 isSolved()。
常见利用 / 解题路线
路线总览:
- 重入:外部调用前未更新状态。
- 权限错误:
tx.origin、未初始化 owner、错误 modifier。 - 随机数误用:
block.timestamp、blockhash、公开 seed。 - 签名重放:签名未绑定 chainId、合约地址、nonce 或用途。
- storage 读取:private 变量、mapping slot、代理合约布局错位。
- delegatecall:库地址可控或 storage slot 被覆盖。
常见失败原因
排查动作:检查编译器版本、初始余额、chainId、gas 和区块时间
排查动作:目标使用 transfer/send 限制 gas,或已有 reentrancy guard
排查动作:检查 EIP-191/EIP-712 前缀、nonce、合约地址和 v/r/s 编码
排查动作:重新计算 slot,mapping 要 keccak256(key . slot)
排查动作:区块号、timestamp 和调用时机必须与链上交易一致
迷你案例
题目目标是让 isSolved() 返回 true,条件是合约余额为 0。源码中 withdraw() 先 call 转账,再把余额置 0。部署攻击合约,先 deposit(),再调用 attack(),在 receive() 里重复 withdraw(),直到目标余额被取空。最后调用 isSolved() 返回 true。
WP 要包含攻击合约代码、调用顺序、余额变化和 solved 验证,而不是只写“重入攻击”。