教程区块链区块链技术ch1212.3 典型漏洞与攻击手法

本页目录

智能合约一旦部署就不可改变,漏洞意味着直接的经济损失。截至 2024 年,DeFi 协议因智能合约漏洞损失超过 $50 亿。这一节拆解四大经典攻击的机理与防御。


12.3.1 重入攻击(Reentrancy):TheDAO 的 3.6 亿美元遗产

攻击原理

solidity
// 有漏洞的版本
contract VulnerableVault {
    mapping(address => uint256) public balances;

    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "no balance");
        (bool ok, ) = msg.sender.call{value: amount}("");  // 外部调用 -> 攻击者回调
        require(ok, "transfer failed");
        balances[msg.sender] = 0;  // 状态更新在转账之后!
    }
}

// 攻击合约
contract Attacker {
    VulnerableVault target;
    function attack() external {
        target.withdraw(); // 触发第一次,目标会在 fallback 前调用这里
    }
    fallback() external payable {
        if (address(target).balance > 0) {
            target.withdraw(); // 再次调用,balance 还没被扣减!
        }
    }
}

数学建模

攻击者可以在每次重入中重复提取余额 BB,直到合约被榨干:

Total Drained=B×n\text{Total Drained} = B \times n

其中 nn 是重入次数,受限于:

  1. 调用栈深度(EVM 限制 1024)
  2. 合约余额
  3. 每次调用的 gas 可用量

防御方案

方法实现局限
Checks-Effects-Interactions先更新状态,再外部调用需要审查所有代码路径
重入锁(mutex)bool locked + require(!locked)增加单线程阻塞,Gas 略增
Pull 而非 Push用户 pull 资金,而非合约 push用户体验稍差
typescript
/**
 * 重入攻击的数学模型:状态更新的不同顺序导致的差异
 */
function simulateReentrancy(
  initialBalance: bigint,
  hackerDeposits: bigint,
  safe: boolean,  // 如果 true,状态先更新
): { victimDrained: bigint; hackerWins: bigint } {
  let contractBalance = initialBalance + hackerDeposits;
  let hackerBalance = 0n; // 攻击者在目标合约中"看起来"的余额
  
  // 攻击者先存入
  // 然后触发 withdraw
  
  if (safe) {
    // CEI 模式:先扣减余额,再发钱
    const claim = hackerDeposits;
    hackerBalance = 0n;  // 扣减!
    contractBalance -= claim;
    // 外部调用...
    return { victimDrained: 0n, hackerWins: claim };
  } else {
    // 攻击路径:先外部调用,再扣减
    let drained = 0n;
    let iterations = 0;
    while (contractBalance > 0 && iterations < 10) {
      const amount = hackerDeposits; // 扣减前余额仍为 hackerDeposits
      if (contractBalance >= amount) {
        drained += amount;
        contractBalance -= amount;
        iterations++;
      } else { break; }
      // 注意:在目标中,只有在最后才设置 hackerBalance = 0
    }
    return { victimDrained: drained - hackerDeposits, hackerWins: drained };
  }
}

// 模拟:攻击者存 1 ETH,合约有 10 ETH
const result = simulateReentrancy(10n, 1n, false);
console.log(`重入攻击: 攻击者投入 1 ETH,最终到手 ${result.hackerWins} ETH`);
console.log(`受害者损失: ${result.victimDrained} ETH`);
// 如果未加防护,攻击者可能榨干全部合约余额

12.3.2 闪电贷攻击:无限资本的瞬间

核心机制

闪电贷允许用户无需任何抵押,在单个交易中借出任意金额,只要在同一个区块的调用结束时偿还 + 手续费。这意味着攻击者拥有瞬时无限资本

攻击者资本=(subject to flash loan quantum)\text{攻击者资本} = \infty \quad \text{(subject to flash loan quantum)}

价格操纵闪电贷

典型链条:

  1. 从 Aave/AAVE 闪电贷 10,000 ETH
  2. 在 DEX(如 Uniswap V2)用 10,000 ETH 买 Token X(推高价格)
  3. 用少量 ETH 作为抵押在借贷协议(如 Compound)以虚高价格借出其他资产
  4. 在 Uniswap 卖出 Token X(恢复价格)
  5. 偿还闪电贷
sequenceDiagram
    participant A as 攻击者
    participant L as 闪电贷池
    participant D1 as Uniswap
    participant M as 价格预言机依赖的合约
    
    A->>L: 借 10K ETH (0 抵押)
    A->>D1: 10K ETH → 买 TokenX<br/>(价格被推高 100x)
    A->>M: 用少量 TokenX 抵押借出 500 ETH (按虚假价格)
    A->>D1: 卖 TokenX (价格恢复)
    A->>L: 归还 10K + 手续费
    
    Note over M: 价格预言机未使用 TWAP<br/>被瞬间操纵
    Note over A: 净赚: 500 - fee (0.09%)<br/>→ 约 499.1 ETH

防御

方案原理
TWAP / Chainlink时间加权平均价格,对单个区块的操纵不敏感
跨区块读取锁定同一交易者不能在 N 个区块内连续做大额交易
价值边界检查借贷/清算时,要求多个独立价格源对齐

12.3.3 抢跑攻击(MEV):暗池中的拍卖

交易内存池(Mempool)的透明性

在交易被打包进区块之前,它暂存于内存池,对全网可见(默认)。攻击者可以:

  1. Front-running:复制你的交易,付更高 gas,抢在你之前执行
  2. Sandwich attack:在你大额买之前先买,你买之后立刻卖,赚差价
  3. Back-running:在你交易后立刻利用其结果(如清算)

提取价值公式

对于 AMM 上的 DEX 交易:

MEV=f(x)f(x+ϵ)price impact\text{MEV} = |f(x) - f(x + \epsilon)| \cdot \text{price impact}

其中 ϵ\epsilon 是被抢跑者注入的量。

防御

方案效果代价
隐私内存池(Flashbots)交易不广播到全网,直接发送给矿工/验证者信任矿工不抢跑
时间锁延迟执行,让价格变动冷却延迟体验
批量随机化将大单拆为随机小单轻微额外滑点
提交-揭示(Commit-Reveal)先提交哈希,后揭示内容两步交互,体验差
typescript
/**
 * 抢跑攻击的数学模型:AMM 上的三明治攻击
 * 恒定乘积做市商: x * y = k
 */
class ConstantProductAMM {
  private x: bigint;  // 资产 X 储备
  private y: bigint;  // 资产 Y 储备
  private fee = 30n;  // 0.3% = 30 basis points
  private feeDen = 10000n;

  constructor(reserveX: bigint, reserveY: bigint) {
    this.x = reserveX; this.y = reserveY;
  }

  // 给定 xIn,计算 yOut(含手续费)
  getAmountOut(xIn: bigint): bigint {
    const xInFee = (xIn * this.fee) / this.feeDen; // 0.3% 手续费给 LP
    const xInNet = xIn - xInFee;
    const yOut = (this.y * xInNet) / (this.x + xInNet);
    return yOut;
  }

  swap(xIn: bigint): bigint {
    const yOut = this.getAmountOut(xIn);
    this.x += xIn;
    this.y -= yOut;
    return yOut;
  }
}

// 三明治攻击模拟
function simulateSandwich(
  amm: ConstantProductAMM,
  victimAmount: bigint,
  attackerAmount: bigint,
): { attackerProfit: bigint; victimSlippage: number } {
  // 攻击前状态
  const x0 = 10000n; const y0 = 10000n; // 1:1 价格
  const amm = new ConstantProductAMM(x0, y0);
  
  // 正常情况(无攻击)
  const normalOut = amm.getAmountOut(victimAmount);
  
  // 重置 + 攻击
  const amm2 = new ConstantProductAMM(x0, y0);
  const attackerIn = attackerAmount;
  const attackerOut1 = amm2.swap(attackerIn);  // 攻击者先买 → 推高 Y 的价格
  
  const victimOut = amm2.swap(victimAmount);   // 受害者以更高价格买
  
  const attackerOut2 = amm2.swap(attackerOut1); // 攻击者回卖,赚差价
  
  const attackerProfit = attackerOut2 - attackerIn;
  const slippageLoss = Number(normalOut - victimOut) / Number(normalOut);
  
  return { attackerProfit, victimSlippage: slippageLoss };
}

const result = simulateSandwich(
  new ConstantProductAMM(10000n, 10000n),
  100n,   // 受害者买 100 X
  50n,    // 攻击者用 50 X 做三明治
);
console.log(`攻击者利润: ${result.attackerProfit} Y(手续费后的净赚)`);
console.log(`受害者额外滑点损失: ${(result.victimSlippage * 100).toFixed(2)}%`);

12.3.4 整数溢出(Solidity 0.7.x 之前)

solidity
// Solidity 0.7.x 之前
uint8 a = 255;
uint8 b = 1;
uint8 c = a + b; // c = 0! (溢出绕回)

// 0.8.0 之后自动 revert,但 assembly 中仍然需要手动检查

12.3.5 知识地图

mindmap
  root((智能合约漏洞))
    重入攻击
      CEI 模式防御
      ReentrancyGuard
      拉取优于推送
    闪电贷
      无限瞬时资本
      价格预言机操纵
      TWAP 防御
    抢跑攻击
      MEV 提取
      三明治攻击
      Flashbots / 提交-揭示
    整数溢出
      0.8+ 自动检查
      SafeMath 库
    权限控制
       Ownable
      角色制 AccessControl

> ← 上一节:12.2 事件 | 前往 → 12.4 审计与工具 |*

评论

0

评论加载中…

发表评论

0/2000