欢迎订阅专栏:10分钟智能合约:进阶实战
中危漏洞审查:从“可忽略”到“组合利用”的关键防线
中危漏洞在审计报告中常被低估,但它们是攻击链中最容易被串联利用的“短板”。单个中危漏洞可能只能造成有限影响,但两个或三个中危漏洞组合起来,往往可以构成一条完整的攻击路径,造成严重后果。
本文聚焦于中危漏洞的分类审查方法、实战识别技巧及组合风险评估策略。
一、中危漏洞的定位与分级标准
审计行业对中危漏洞的通用定义:在特定条件下可能导致资金损失或功能异常,但利用门槛较高(需要特定权限、特定市场状态、或与其他漏洞配合)。
分级参考标准:
| 等级 | 判定依据 | 典型场景 |
|---|---|---|
| 中危(Medium) | 满足部分条件:需要较高信任假设/需要特定外部状态/影响范围有限/存在缓解措施可降低风险 | 特定用户可被 DoS、条件性授权过高、价格精度损失累积 |
| 中高危(Medium-High) | 介于中危和高危之间,可能在高危漏洞配合下升级为严重风险 | 特定情形下的重入(需特定代币合约配合)、治理提案无时间锁但需多签 |
| 中低危(Medium-Low) | 接近低危,但影响经济模型或用户体验 | Gas 优化不当导致用户成本过高、事件缺失影响监控效率 |
审查核心原则:不单独评估漏洞等级,而是评估所有中危漏洞叠加后的“组合杀伤力”。
二、中危漏洞核心审查清单
类别 A:访问控制相关(中等风险)
| 漏洞模式 | 危害 | 审查要点 |
|---|---|---|
| 初始化函数可被抢先调用(但已有防护) | 低-中 | 检查是否使用了 initializer 修饰符,且调用者是否限于部署者 |
| onlyOwner 函数无时间锁 | 中 | 确认敏感操作(升级、暂停、参数调整)是否通过 Timelock 执行 |
| 权限角色定义过宽 | 中 | 检查 DEFAULT_ADMIN_ROLE 是否被赋予过多权限 |
| 临时权限未及时撤销 | 中 | 审计合约中是否存在“已弃用但仍有效的授权” |
实战排查命令:
# 1. 搜索所有 onlyOwner 函数
grep -rn "onlyOwner" ./contracts/ --include="*.sol"
# 2. 检查是否有时间锁修饰符
grep -rn "Timelock\|timelock\|timeLock" ./contracts/ --include="*.sol"
# 3. 列出所有具有管理权限的函数
slither . --print function-summary | grep -i "onlyOwner\|onlyRole\|admin"
类别 B:业务逻辑与状态完整性
| 漏洞模式 | 危害 | 审查要点 |
|---|---|---|
| 舍入误差累积 | 中(长期) | 检查所有涉及比例分配(如分红、奖励、手续费)的计算,是否将余数留给协议或用户 |
| 循环中外部调用未隔离失败 | 中 | 检查 for 循环中的 call/transfer 是否使用 try/catch 捕获异常 |
| 重入风险(仅限特定代币) | 中 | 某些 ERC-777 或自定义钩子代币可能触发重入;检查 transfer/transferFrom 是否调用 tokensReceived |
| 数组长度无上限 | 中 | 检查 push 操作是否可被无限调用,导致 gas limit 耗尽 |
| 函数执行 Gas 成本不可预测 | 中 | 循环操作是否依赖于外部输入长度,且未设置上限 |
实战排查命令:
# 1. 搜索所有循环中的外部调用
grep -rn "for.*call\|for.*transfer\|for.*send" ./contracts/ --include="*.sol"
# 2. 搜索所有 .push 操作
grep -rn ".push(" ./contracts/ --include="*.sol"
# 3. 搜索所有除法操作(可能导致舍入误差)
grep -rn "/" ./contracts/ --include="*.sol" | grep -v "//" | grep -v "*/"
类别 C:代币标准与兼容性
| 漏洞模式 | 危害 | 审查要点 |
|---|---|---|
| 对非标准 ERC-20 的假设 | 中 | USDT/BUSD 等代币的 transfer 返回值不标准(返回 void 而非 bool) |
| 对可重入代币的处理不当 | 中 | ERC-777/ERC-1155 的 tokensReceived 回调可能造成重入 |
对 transferFrom 的返回值未检查 | 中 | 某些代币实现返回 false 但不 revert,需用 SafeERC20 |
| 代币转账 fee-on-transfer 未考虑 | 中 | 转账过程中实际接收金额可能小于 amount,需用 balanceOf 前后差计算 |
对 decimals 精度假设错误 | 中 | 假设所有代币都是 18 位,但实际可能是 6/8/其他值 |
典型缺陷示例:
// ❌ 危险:未处理非标准 ERC-20 返回值
IERC20(token).transferFrom(msg.sender, address(this), amount);
// 某些代币(USDT)返回 void,此处不会 revert,但金额可能未转成功
// ✅ 安全:使用 SafeERC20
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
实战排查命令:
# 1. 搜索所有 .transfer 调用
grep -rn ".transfer(" ./contracts/ --include="*.sol" | grep -v "address(this).balance\|msg.sender"
# 2. 搜索所有 .transferFrom 调用
grep -rn ".transferFrom(" ./contracts/ --include="*.sol"
# 3. 检查是否使用 SafeERC20
grep -rn "SafeERC20" ./contracts/ --include="*.sol"
类别 D:预言机与时序相关(中危变种)
| 漏洞模式 | 危害 | 审查要点 |
|---|---|---|
| TWAP 窗口过短 | 中 | observation 时间窗口 < 5 分钟 → 易受闪电贷短暂操纵 |
| 预言机更新频率不足 | 中 | 价格更新间隔过长,可能导致套利窗口 |
使用 block.timestamp 进行结算 | 中 | 与时间相关的结算(如奖励计算)可能被矿工微调 ±15 秒影响 |
使用 block.number 进行时间计算 | 中 | 区块时间波动可能造成预期偏差 |
实战排查命令:
# 1. 搜索所有 block.timestamp 的使用
grep -rn "block.timestamp\|now" ./contracts/ --include="*.sol"
# 2. 检查 TWAP 窗口参数
grep -rn "TWAP\|twap\|observation" ./contracts/ --include="*.sol"
三、中危漏洞的“组合升级”路径
这是中危漏洞审查中最关键、最容易被忽视的部分。以下模型用于系统化评估中危漏洞的聚合风险:
模型:攻击链叠加分析
flowchart LR
A[中危漏洞 A<br>例如: 授权范围过宽] --> D[高危攻击路径]
B[中危漏洞 B<br>例如: 舍入误差累积] --> D
C[中危漏洞 C<br>例如: 代币兼容性问题] --> D
D --> E[严重资金损失]
示例:三条中危路径如何组合成致命攻击链
场景:借贷协议存在以下中危问题:
- 授权范围过宽(中危):
approve允许无限额度,且无撤销机制。 - 舍入误差累积(中危):利息计算使用向下取整,且未处理余数,长期造成协议损失。
- 预言机更新频率低(中危):价格每 30 分钟更新一次,存在套利窗口。
组合攻击路径:
- 攻击者利用预言机更新窗口,在低流动性 DEX 上操纵价格(需额外闪电贷,但协议未限制)。
- 利用授权范围过宽,使恶意合约代表其他用户授权(需配合钓鱼,但用户未设限额)。
- 利用舍入误差累积,反复调用
borrow/repay提取被截留的微小金额(每次可能只提取 0.0001 ETH,但可自动化大量执行)。
结果:原本三个各自评分为中危的漏洞,叠加后形成完整攻击路径,导致资金损失。
审查策略:不仅检查单个漏洞,更要绘制“漏洞地图”,识别哪些中危漏洞可能被串联。
四、中危漏洞的复现与验证
中危漏洞往往需要特定的条件才能触发,因此验证它们的存在是审计中的一项关键挑战。以下是优先序建议与操作参考:
优先级划分(按潜在影响与组合风险排序):
- 高优先级:涉及资金流出、可被攻击者控制输入参数的漏洞。
- 中优先级:影响核心业务逻辑、可能被矿工或特定角色利用的漏洞。
- 低优先级:仅影响代码规范或 Gas 优化的中危项。
对于高、中优先级的漏洞,建议编写 Foundry PoC 进行验证。
实战示例:验证授权范围过宽的攻击路径
// 测试授权范围过宽是否能被利用
function test_OverApproval() public {
// 1. 部署协议和攻击者合约
// 2. 模拟用户授权无限额度
token.approve(protocol, type(uint256).max);
// 3. 攻击者合约调用 transferFrom 转移资金
vm.prank(attacker);
token.transferFrom(user, attacker, userBalance);
// 4. 验证攻击成功
assertEq(token.balanceOf(attacker), userBalance);
}
验证要点:
- 是否可在分叉环境中重现。
- 攻击成本是否低于潜在收益。
- 是否存在任何前提条件(如特殊权限、特定市场状态)。
对于成本较高或条件较苛刻的攻击路径,记录其理论可行性而非强行复现。
五、中危漏洞的缓解与处置建议
| 缓解策略 | 适用场景 | 实施成本 |
|---|---|---|
| 添加参数范围限制 | 防止数组过长或输入值过大 | 低 |
| 增加时间锁 | 敏感操作延迟执行,给予用户反应时间 | 中 |
| 使用 SafeERC20 | 处理非标准 ERC-20 代币 | 低 |
| 采用拉取模式(Pull over Push) | 避免循环推送导致的 DoS | 中(需架构调整) |
| 引入多签/多角色验证 | 降低单一管理员权限风险 | 高 |
设置授权上限(如 increaseAllowance 而非无限批准) | 降低授权泄露风险 | 低 |
修复优先级判定矩阵:
| 组合风险 | 与高危漏洞距离 | 建议处置优先级 |
|---|---|---|
| 高组合风险 | 可与其他中危/高危漏洞串联 | 高:应在审计修复期内修复 |
| 中组合风险 | 需要特定条件(如特定市场状态) | 中:建议修复或制定缓解措施 |
| 低组合风险 | 单独存在,无串联可能 | 低:可长期优化,不影响上线 |
六、实战案例:多个中危漏洞的聚合利用
案例背景:某收益聚合器协议的审计中发现三个中危问题:
- 奖励计算舍入误差(中危):
reward = amount * rate / 1e18,余数被截留。 - 策略提款无滑点保护(中危):提款时允许攻击者在交易间隔内操纵价格。
onlyOwner函数无时间锁(中危):管理员可立即修改关键参数。
组合利用路径:
- 管理员(攻击者控制的恶意多签)无时间锁地修改策略参数,将资金池导向恶意策略。
- 利用无滑点保护,攻击者在策略切换后立即进行三明治攻击,提取比预期更多的收益。
- 舍入误差累积被攻击者通过大量小额提款反复提取。
最终影响:虽然每个漏洞单独危害有限,但叠加后造成约 15% 的协议资金流失。该案例被标记为综合严重风险,要求项目方一次性修复所有相关漏洞。
审计启示:即使是中危漏洞,如果存在多条串联路径,也应升级其综合风险评级。
七、报告模板参考
中危漏洞的审计报告应包含以下章节:
### 漏洞名称:[简明命名]
- **严重等级**:中危(Medium)
- **影响范围**:[合约名 + 函数名]
- **代码位置**:`path/to/contract.sol#L100-L120`
**漏洞描述**:
[简洁描述漏洞的本质和触发条件]
**影响分析**:
[阐述攻击者可以做什么,需要什么条件,可能造成什么后果]
**组合风险评估**:
[是否存在与其他漏洞串联的可能?如果是,说明组合路径]
**修复建议**:
[提供具体的代码修改示例]
**复现步骤**:
[可选:附上 Foundry 测试用例或攻击脚本]
**状态**:[待修复 / 已修复 / 风险接受]
八、审查总结与经验法则
| 法则 | 说明 |
|---|---|
| 不孤立评估漏洞等级 | 多个中危漏洞叠加可能造成严重损失 |
| 关注组合路径 | 审查时应绘制漏洞地图,识别攻击链 |
| 优先修复可升级路径 | 优先修复那些可能与其他漏洞串联的问题 |
| 使用标准库 | OpenZeppelin 的 SafeERC20、ReentrancyGuard、TimelockController 等可缓解多数中危问题 |
| 持续监控 | 部分中危漏洞(如舍入误差累积)只在长期运行后显现,上线后需持续监测 |
核心原则:中危漏洞审查的目标不是“找问题”,而是“识别问题组合的潜在危害”。一个攻击者不会只利用一个漏洞,他会利用所有可利用的条件。审计人员应以攻击者的视角,将所有中危漏洞视为攻击链中的“拼图”,而非独立的碎片。