关于 smart-contract-vulnerabilities
面向审计 Solidity 与 DeFi 系统人员的智能合约安全进阶技能,整理重入、访问控制、MEV、签名重放和 CREATE2 等攻击模式,并提供缓解建议与分析工具。
主要功能
- 01重入与算术缺陷模式
- 02访问控制和 delegatecall 风险
- 03随机数、MEV 与闪电贷攻击
- 04签名重放和 CREATE2 案例
- 05缓解模式与审计工具
适用场景
- 01检查合约代码中的重入和访问控制缺陷
- 02评估代理合约的存储布局与选择器冲突
- 03测试交易排序与签名重放风险
- 04规划静态分析、模糊测试或形式化检查
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill smart-contract-vulnerabilities安装后在 Claude Code 中输入/smart-contract-vulnerabilities确认已加载。
附带文件SOLIDITY_VULN_PATTERNS.md
详细介绍
技能:智能合约漏洞 — 专家攻击手册
AI 加载说明:智能合约审计专家技术。涵盖重入(单函数、跨函数、跨合约、只读)、整数溢出、访问控制、delegatecall、随机数操纵、闪电贷、签名重放、抢跑/MEV 和 CREATE2 漏洞利用。基础模型会漏掉微妙的跨合约重入和代理模式中的存储布局冲突。
0. 相关路由
- 当漏洞属于 DeFi 协议攻击的一部分(闪电贷、预言机操纵、治理攻击)时,使用 defi-attack-patterns
- 当目标是反序列化区块链数据的链下基础设施时,使用 deserialization-insecure
高级参考
在需要以下内容时,也加载 SOLIDITY_VULN_PATTERNS.md:
- 每类漏洞的易受攻击与修复后代码模式对照
- 引入漏洞的 Gas 优化陷阱
- 包含插槽计算的代理模式存储冲突示例
1. 重入
最具代表性的智能合约漏洞。外部调用会转移执行控制权;如果在调用前没有更新状态,被调用方就能重新进入。
1.1 经典重入(单函数)
Victim.withdraw()
├── checks balance[msg.sender] > 0 ✓
├── msg.sender.call{value: balance}("") ← external call
│ └── Attacker.receive()
│ └── Victim.withdraw() ← re-enters before state update
│ ├── checks balance[msg.sender] ← still > 0!
│ └── sends ETH again
└── balance[msg.sender] = 0 ← too late
1.2 跨函数重入
两个函数共享状态;攻击者在回调期间重新进入另一个函数:
| 步骤 | 执行过程 | 状态 |
|---|---|---|
| 1 | 调用 withdraw() → 外部调用 | 余额仍为正 |
| 2 | 攻击者的回退函数调用 transfer(attacker2) | 重置前使用了余额 |
| 3 | transfer 读取过期余额 → 转移资金 | attacker2 收到代币 |
| 4 | 原始 withdraw 完成,将余额清零 | 损害已造成 |
1.3 跨合约重入
合约 A 调用合约 B,而合约 B 回调合约 A(或读取合约 A 过期状态的合约 C)。在多个合约共享状态的 DeFi 协议中尤其危险。
1.4 只读重入
重新进入的函数是供第三方合约用于价格计算的 view 函数。受害合约的状态不会被修改,但过期的中间状态会误导读取方。
真实案例:Curve 池的 remove_liquidity() 回调期间读取 get_virtual_price() → 价格被抬高 → 在依赖该池的借贷协议中获利。
缓解措施
| 模式 | 防护级别 |
|---|---|
| 检查-效果-交互(CEI) | 核心防护;在外部调用前更新状态 |
ReentrancyGuard(OpenZeppelin) | 互斥锁;防止同一交易内重入 |
| 提取式支付模式 | 消除状态变更函数中的外部调用 |
| 对所有公共函数使用 CEI + 防重入锁 | 纵深防御,抵御跨函数重入 |
2. 整数溢出 / 下溢
Solidity 0.8 之前
算术运算会静默回绕:uint8(255) + 1 == 0,uint8(0) - 1 == 255。
| 攻击方式 | 示例 |
|---|---|
| 余额下溢 | 当 amount > balance 时执行 balances[attacker] -= amount → 得到巨大余额 |
| 供应量溢出 | totalSupply + mintAmount 回绕 → 绕过上限检查 |
| 绕过时间锁 | lockTime[msg.sender] + extend 回绕到过去 → 提前解锁 |
Solidity 0.8 之后
默认的检查算术会在溢出时回滚。但 unchecked{} 代码块会重新引入风险:
unchecked {
// "gas optimization" — but if i can be influenced by user input, overflow returns
for (uint i = start; i < end; i++) { ... }
}
SafeMath 绕过场景
- 类型转换:在 SafeMath 检查前从
uint256转为uint128,导致截断 - Assembly 代码块:
mstore/add可绕过 Solidity 级别的检查 - 除法前的中间乘法溢出:
(a * b) / c中的a * b溢出
3. 访问控制
tx.origin 与 msg.sender
| 属性 | msg.sender | tx.origin |
|---|---|---|
| 值 | 直接调用方 | 发起该交易的 EOA |
| 用于身份验证是否安全 | 是 | 否 — 钓鱼合约可以继承 tx.origin |
攻击方式:诱骗所有者调用攻击者合约 → 攻击者合约使用所有者的 tx.origin 调用受害合约。
常见模式
| 问题 | 影响 |
|---|---|
关键函数缺少 onlyOwner | 任何人都可以调用管理函数 |
selfdestruct 未受保护 | 任何人都可以销毁合约并强制发送 ETH |
delegatecall 未受保护 | 攻击者可在受害者的上下文中执行任意代码 |
| 默认可见性(0.6.0 之前) | 函数默认可见性为 public |
| 缺少零地址检查 | 所有权被转移到 address(0) |
4. 随机数操纵
链上随机数来源可被矿工/验证者预测:
| 来源 | 可预测性 |
|---|---|
block.timestamp | 矿工有约 15 秒的操纵窗口 |
blockhash(block.number - 1) | 执行时所有人都已知 |
blockhash(block.number) | 始终返回 0(当前区块哈希未知) |
block.difficulty / block.prevrandao | 合并后:已知的信标链值 |
绕过承诺-揭示机制:如果揭示阶段未强制执行超时或保证金,攻击者可以选择不揭示不利结果(选择性中止攻击)。
5. DELEGATECALL 漏洞
delegatecall 在调用者的存储上下文中执行被调用者的代码。存储槽布局必须完全一致。
存储布局冲突
Proxy (storage): Implementation (code):
slot 0: owner slot 0: someVariable
slot 1: implementation slot 1: anotherVariable
实现合约写入 someVariable(槽 0)→ 覆盖代理合约的 owner。攻击者调用会写入槽 0 的实现函数 → 成为代理合约的所有者。
函数选择器冲突
4 字节函数选择器可能发生冲突。如果代理合约的 admin() 选择器与实现合约的 transfer() 冲突,在代理合约上调用 admin() 就会执行 transfer() 逻辑。
工具:使用 cast selectors <bytecode>(Foundry)枚举选择器。
6. 抢先交易/MEV
交易排序操纵
Victim submits DEX swap tx (visible in mempool)
├── Front-runner: buy token before victim (raise price)
├── Victim tx executes at worse price
└── Back-runner: sell token after victim (profit from spread)
= Sandwich attack
防护模式
| 防御措施 | 机制 |
|---|---|
| 承诺-揭示机制 | 在揭示前隐藏交易意图 |
| Flashbots / 私有内存池 | 直接向区块构建者提交 tx |
| 滑点保护 | 设置 minAmountOut 以限制 MEV 提取 |
| 时间锁 | 延迟执行以降低可预测性 |
7. 签名重放
缺少 Nonce
重复使用有效签名以多次重复执行操作(例如转账)。
跨链重放
同一合约以相同地址部署在多条链上 → 签名在所有链上都有效。签名消息中必须包含 block.chainid。
EIP-712 实现错误
| 错误 | 后果 |
|---|---|
DOMAIN_SEPARATOR 中缺少 chainId | 跨链重放 |
| 部署时缓存域分隔符 | 硬分叉更改 chainId 后失效 |
| 结构哈希中缺少 nonce | 签名重放 |
无效签名时 ecrecover 返回 address(0) | 通过 == address(0) 所有者检查 |
8. SELF-DESTRUCT 与强制发送 ETH
selfdestruct(recipient) 会将合约的全部 ETH 强制发送给 recipient——绕过 receive() 和 fallback(),且无法拒绝。
会破坏依赖 address(this).balance 实现逻辑的合约(例如 require(balance == expected))。
EIP-6780(Dencun)之后:selfdestruct 只会发送 ETH;仅当它与合约创建发生在同一笔 tx 中时,才会删除代码/存储。
9. CREATE2 与确定性地址利用
CREATE2 地址 = keccak256(0xff ++ deployer ++ salt ++ keccak256(initCode))。
| 攻击 | 方法 |
|---|---|
| 预充值利用 | 预测地址 → 在部署前发送代币/ETH → selfdestruct → 在同一地址重新部署不同代码 |
| 预授权利用 | 向预测地址授予代币授权 → 部署恶意合约 → 提取已授权代币 |
| 变形合约 | CREATE2 → selfdestruct → 使用相同 salt、不同 initCode 执行 CREATE2(EIP-6780 之前) |
10. 闪电贷攻击模式
Single transaction:
├── Borrow large amount (no collateral)
├── Manipulate state (price oracle, governance, etc.)
├── Extract profit from manipulated state
├── Repay loan + fee
└── Keep profit
关键点:整个序列必须原子性地成功,否则整笔 tx 都会回滚。
11. 短地址攻击
EVM 会用零填充 ABI 编码的调用数据中缺失的字节。如果使用 19 字节地址调用 transfer(address, uint256),uint256 金额会左移 8 位 → 乘以 256。
缓解措施:验证调用数据长度;现代 Solidity 编译器会添加检查。
12. 工具
| 工具 | 用途 | 使用方法 |
|---|---|---|
| Slither | 静态分析、漏洞检测 | 在项目根目录运行 slither . |
| Mythril | 符号执行、路径探索 | myth analyze contract.sol |
| Echidna | 基于属性的模糊测试 | 定义不变量,通过模糊测试查找违规情况 |
| Foundry (Forge) | 测试框架、模糊测试、Gas 分析 | forge test --fuzz-runs 10000 |
| Hardhat | 开发、测试、部署 | npx hardhat test |
| Certora | 形式化验证 | 编写规范,证明或证伪属性 |
| 4naly3er | 自动化 Gas 优化与漏洞报告 | CI 集成 |
13. 决策树
Auditing a smart contract?
├── Is it a proxy pattern?
│ ├── Yes → Check storage layout collision (Section 5)
│ │ ├── Compare slot assignments between proxy and implementation
│ │ ├── Check for function selector collision
│ │ └── Verify initializer cannot be called twice
│ └── No → Continue
├── Does it make external calls?
│ ├── Yes → Check reentrancy (Section 1)
│ │ ├── State updated before call? → CEI pattern OK
│ │ ├── ReentrancyGuard present? → Check all entry points
│ │ ├── Cross-function state sharing? → Cross-function reentrancy risk
│ │ └── View functions read during callback? → Read-only reentrancy
│ └── No → Continue
├── Does it handle tokens/ETH?
│ ├── Yes → Check integer overflow (Section 2)
│ │ ├── Solidity < 0.8? → All arithmetic suspect
│ │ ├── unchecked{} blocks? → Verify no user-influenced values
│ │ └── Casting between uint sizes? → Truncation risk
│ └── Also check self-destruct force-send (Section 8)
├── Does it use signatures?
│ ├── Yes → Check replay (Section 7)
│ │ ├── Nonce included? → Verify incremented
│ │ ├── ChainId included? → Cross-chain safe
│ │ └── ecrecover result checked for address(0)? → OK
│ └── No → Continue
├── Does it use on-chain randomness?
│ ├── Yes → Predictable (Section 4)
│ │ └── Recommend Chainlink VRF or commit-reveal with bond
│ └── No → Continue
├── Does it interact with DeFi protocols?
│ ├── Yes → Load [defi-attack-patterns](../defi-attack-patterns/SKILL.md)
│ │ ├── Flash loan vectors
│ │ ├── Oracle manipulation
│ │ └── MEV exposure
│ └── No → Continue
├── Does it use CREATE2?
│ ├── Yes → Check deterministic address exploitation (Section 9)
│ └── No → Continue
└── Run automated tools (Section 12)
├── Slither for static analysis
├── Mythril for symbolic execution
└── Echidna for fuzzing invariants