智能合约攻击分析入门

智能合约攻击分析的目标是基于链上证据还原状态变化,定位漏洞根因,并验证修复措施。复现工作只应在本地分叉、测试网或已获得明确授权的环境中进行。

分析流程

1
2
3
4
5
6
7
8
9
10
flowchart TD
A[确认事件与交易哈希] --> B[收集合约源码和链上状态]
B --> C[还原调用树与资产流]
C --> D[定位首个异常状态变化]
D --> E[提出漏洞假设]
E --> F[构建最小复现]
F --> G{复现是否符合链上结果}
G -- 否 --> C
G -- 是 --> H[评估影响与受影响版本]
H --> I[提出修复并编写回归测试]

信息收集

至少保存受害合约地址、攻击交易哈希、区块高度、相关代币、代理实现地址和事件时间。源码应与链上字节码核对,代理合约还要确认攻击发生时使用的具体实现版本。

交易浏览器适合初步观察,调试器和节点追踪接口适合检查内部调用、事件、存储差异与 Gas 消耗。分析报告中的每项结论都应能回溯到交易、代码或状态证据。

调用链与资产流

先画出调用关系,再记录每一步的资产变化。闪电贷通常只是放大资金规模,不一定是根因。根因往往是价格计算、授权边界、状态更新顺序或会计不变量被破坏。

1
2
3
4
5
6
7
8
9
10
sequenceDiagram
participant A as 攻击合约
participant L as 闪电贷池
participant V as 受害合约
participant M as 外部市场
A->>L: 借入临时资金
A->>M: 改变可被引用的市场状态
A->>V: 调用存在缺陷的函数
V-->>A: 错误转出资产
A->>L: 归还本金与费用

常见漏洞分类

  • 重入:外部调用发生在关键状态更新之前,攻击者能够重复进入同一路径。
  • 权限错误:管理函数缺少访问控制,或授权判断使用了错误主体。
  • 价格操纵:协议直接依赖可在单笔交易内显著改变的现货价格。
  • 会计错误:份额、资产、债务或手续费之间的换算不满足预期不变量。
  • 签名缺陷:缺少 nonce、截止时间、链 ID 或合约域隔离,导致重放。
  • 代理问题:初始化、升级授权或存储布局存在错误。
  • 拒绝服务:循环、外部依赖或异常处理使关键功能长期不可用。

最小重入示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerableVault {
mapping(address => uint256) public balanceOf;

function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}

function withdraw(uint256 amount) external {
require(balanceOf[msg.sender] >= amount, "insufficient balance");
// 错误顺序:先执行不可信外部调用,再更新余额。
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
balanceOf[msg.sender] -= amount;
}
}

对应修复采用检查、状态更新、外部交互的顺序,并可增加重入锁作为第二道防线。

1
2
3
4
5
6
function withdraw(uint256 amount) external nonReentrant {
require(balanceOf[msg.sender] >= amount, "insufficient balance");
balanceOf[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}

Foundry 分叉复现框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";

contract IncidentTest is Test {
uint256 constant INCIDENT_BLOCK = 20_000_000;

function setUp() public {
vm.createSelectFork(vm.envString("RPC_URL"), INCIDENT_BLOCK - 1);
}

function testIncidentInvariant() public {
uint256 assetsBefore = address(target).balance;

vm.startPrank(attacker);
reproduceAuthorizedSequence();
vm.stopPrank();

uint256 assetsAfter = address(target).balance;
assertLt(assetsAfter, assetsBefore);
}
}

复现测试应固定区块,记录依赖地址,并断言关键余额与存储变化。不要只断言攻击者最终获利,因为转账路径可能改变,而不变量破坏才是稳定证据。

报告结构

  1. 事件摘要:时间、网络、协议、损失和受影响合约。
  2. 前置条件:攻击者需要的资金、权限与市场状态。
  3. 攻击步骤:按交易内执行顺序描述调用与资产变化。
  4. 根因:指出具体代码与被破坏的不变量。
  5. 影响范围:判断其他部署、版本和用户是否受到影响。
  6. 修复建议:给出代码修改、参数处置和运营措施。
  7. 回归验证:使用单元测试、模糊测试或不变量测试证明修复有效。

分析纪律

事实、推断和未知项应明确区分。反编译结果不能自动等同于原始源码,事件日志也不能单独证明全部状态变化。对涉及真实资金的事件,不应公开仍可直接复用且尚未修复的攻击细节。