10分钟智能合约:进阶实战-9.3 高危漏洞审查

10 阅读5分钟

欢迎订阅专栏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.originrequire(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 调用外部地址 → 高危 ❌
逻辑合约是否包含 selfdestructdelegatecall 触发 → 调用方销毁 ❌
存储布局是否一致代理与逻辑合约的前 N 个变量必须一致
初始化函数是否使用 initializerOpenZeppelin 的 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{} 块中的运算;类型转换(uint256uint8
拒绝服务(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[提交项目方确认与修复]

八、审查人员实战建议

  1. 先跑工具,但别信工具:Slither 等工具能覆盖 70% 的常见漏洞,但对业务逻辑漏洞几乎无效。
  2. 重点审计“资金流出”函数withdrawtransfersendswapliquidate —— 这些是攻击者的主要目标。
  3. 绘制权限地图:列出每个敏感函数谁能调用、什么时候能调用、能产生什么影响。
  4. 模拟极端输入amount = 0type(uint256).maxlength = 0length = huge
  5. 关注“看起来太方便的”函数killsetImplementationonlyOwner 且无时间锁、无多签。

高危漏洞审查不是机械地跑工具或读代码,而是有策略地假设代码有漏洞,然后系统性地验证这一假设。熟练掌握上述审查方法和工具链,可以大幅提升审计效率和漏洞发现率。