欢迎订阅专栏:10分钟智能合约:进阶实战
高危漏洞审查:智能合约审计的核心战场
高危漏洞审查是智能合约审计中最关键的环节,目标是识别、验证和报告可能导致资金损失、合约失控或系统崩溃的致命缺陷。这部分工作占据了审计总时间的 60%~70%,也是区分专业审计师与初学者扫描器的分水岭。
一、高危漏洞的优先级分类
为便于系统性审查,将高危漏洞按“杀伤力”和“出现频率”分为三个梯队:
| 梯队 | 漏洞类型 | 杀伤力 | 出现频率 |
|---|---|---|---|
| 第一梯队 | 重入攻击、访问控制绕过 | ⭐⭐⭐⭐⭐ | 中 |
| 第二梯队 | 预言机操纵、未检查返回值、delegatecall滥用 | ⭐⭐⭐⭐ | 高 |
| 第三梯队 | 算术溢出/下溢、拒绝服务、时间戳操纵、随机数预测 | ⭐⭐⭐ | 低(现代版本) |
第一梯队漏洞往往能在单笔交易中造成 100% 资金损失,是审查的重中之重。
二、第一梯队:重入攻击
重入攻击的本质是状态更新滞后于外部调用。审查时应重点检查所有涉及 ETH 或代币转账的函数。
审查检查清单
function withdraw(uint256 amount) external {
// 1️⃣ 检查:是否先更新状态?
// 2️⃣ 检查:外部调用是否在状态更新之前?
// 3️⃣ 检查:是否使用了重入锁(nonReentrant)?
require(balances[msg.sender] >= amount);
// ❌ 危险:先调用后更新
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount; // 状态更新滞后
// ✅ 安全:先更新后调用
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
跨函数重入审查要点
多个函数共享同一状态变量时,需检查所有函数的状态更新顺序。例如:
// ❌ 危险:stateA 在多个函数中被修改,但更新顺序不一致
function deposit() external {
stateA += 1; // 先更新
someExternalCall();
}
function withdraw() external {
someExternalCall(); // 先调用
stateA -= 1; // 后更新 —— 重入入口
}
实战工具命令
# Slither 检测重入漏洞
slither . --detect reentrancy-eth,reentrancy-no-eth
# Foundry 模糊测试验证
forge test --match-test testReentrancy -vvvv
三、第一梯队:访问控制绕过
访问控制缺陷是“最容易被忽视的严重漏洞”,因为工具很难发现业务逻辑层面的权限错配。
审查检查清单
| 检查点 | 示例 |
|---|---|
| 初始化函数是否保护 | function initialize() public { owner = msg.sender; } ❌ |
| 构造函数是否拼写错误 | contract Token { function Token() public { ... } } ✅ |
| 代理模式初始化是否调用 | 逻辑合约的 initialize 需在代理中调用并加锁 |
权限检查是否使用 tx.origin | require(tx.origin == owner) ❌(钓鱼风险) |
delegatecall 是否影响权限状态 | 调用方存储槽与逻辑合约不匹配 |
代理模式存储冲突审查
// ❌ 危险:代理和逻辑合约的存储布局不一致
// 代理合约
contract Proxy {
address public implementation; // slot 0
address public admin; // slot 1
}
// 逻辑合约
contract Logic {
address public admin; // slot 0 ← 覆盖了 implementation!
uint256 public value; // slot 1 ← 覆盖了 admin!
}
实战工具命令
# 检查未受保护的函数
slither . --detect unprotected-upgrade
# 列出所有 external/public 函数及其可见性
slither . --print function-summary
四、第二梯队:预言机操纵
预言机操纵是 DeFi 审计的高频高危区,需重点审查价格数据来源。
审查检查清单
| 检查点 | 说明 |
|---|---|
| 价格来源是否为单一流动性池 | 单池瞬时储备 → 易操纵 ❌ |
| 是否使用 TWAP 或多源验证 | 时间加权均价 / Chainlink 聚合 → 较安全 ✅ |
| 价格更新频率是否允许抢先交易 | 更新间隔内可被操纵 |
| 是否存在价格偏差熔断 | 单次变动 > 阈值 → 拒绝或延迟 |
典型漏洞模式
// ❌ 危险:直接从 Uniswap V2 读取储备计算价格
function getPrice() external view returns (uint256) {
(uint112 reserve0, uint112 reserve1, ) = pair.getReserves();
return reserve1 * 1e18 / reserve0; // 可被闪电贷瞬间扭曲
}
// ✅ 较安全:使用 Chainlink Price Feed
function getPrice() external view returns (uint256) {
(, int price, , , ) = priceFeed.latestRoundData();
require(price > 0, "Invalid price");
return uint256(price);
}
实战工具命令
# 搜索价格相关函数
grep -r "getReserves\|latestRoundData\|TWAP" ./contracts/
# 使用 Foundry 分叉测试闪电贷攻击
forge test --fork-url $MAINNET_RPC --match-test testPriceManipulation -vvvv
五、第二梯队:delegatecall 滥用
delegatecall 是 Solidity 中最容易被误用的指令,危害包括存储碰撞、权限覆盖、自毁。
审查检查清单
| 检查点 | 说明 |
|---|---|
| 是否可调用未受信任的地址 | delegatecall 调用外部地址 → 高危 ❌ |
逻辑合约是否包含 selfdestruct | 被 delegatecall 触发 → 调用方销毁 ❌ |
| 存储布局是否一致 | 代理与逻辑合约的前 N 个变量必须一致 |
初始化函数是否使用 initializer | OpenZeppelin 的 initializer 修饰符 ✅ |
典型漏洞模式
// ❌ 危险:fallback 无条件转发所有调用
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success);
}
// ✅ 安全:限制调用函数白名单
fallback() external payable {
bytes4 sig = bytes4(msg.data);
require(sig == UPGRADE_SIG || sig == PING_SIG, "Unauthorized");
(bool success, ) = implementation.delegatecall(msg.data);
require(success);
}
六、第三梯队:算术溢出、DoS、时间戳操纵
这些漏洞在 Solidity 0.8+ 中部分被编译器自动防御,但仍需关注:
| 类型 | 审查要点 |
|---|---|
| 算术溢出 | 检查 unchecked{} 块中的运算;类型转换(uint256 → uint8) |
| 拒绝服务(DoS) | 是否有循环推送(for 遍历大数组);是否依赖不可靠的外部调用 |
| 时间戳操纵 | 是否将 block.timestamp 用于随机数或关键条件 |
七、高危漏洞审查流程速查
flowchart TD
A[获取合约源码] --> B[自动化工具扫描<br>Slither/Aderyn/Mythril]
B --> C[筛选高危/严重报告项]
C --> D[人工逐项确认<br>跟踪数据流/权限映射]
D --> E[是否可被利用?]
E -->|是| F[编写 PoC 攻击合约]
E -->|否| G[标记为误报/低危]
F --> H[在分叉环境测试验证]
H --> I[撰写漏洞报告<br>含代码位置/攻击路径/修复建议]
I --> J[提交项目方确认与修复]
八、审查人员实战建议
- 先跑工具,但别信工具:Slither 等工具能覆盖 70% 的常见漏洞,但对业务逻辑漏洞几乎无效。
- 重点审计“资金流出”函数:
withdraw、transfer、send、swap、liquidate—— 这些是攻击者的主要目标。 - 绘制权限地图:列出每个敏感函数谁能调用、什么时候能调用、能产生什么影响。
- 模拟极端输入:
amount = 0、type(uint256).max、length = 0、length = huge。 - 关注“看起来太方便的”函数:
kill、setImplementation、onlyOwner且无时间锁、无多签。
高危漏洞审查不是机械地跑工具或读代码,而是有策略地假设代码有漏洞,然后系统性地验证这一假设。熟练掌握上述审查方法和工具链,可以大幅提升审计效率和漏洞发现率。