区块链安全—合约存储机制安全分析

一、前言

作为不太成熟的编程语言,Solidity函数由于其运行机制等问题目前能找到很多的安全问题。在之前的分析中,我们针对共识、合约等方向进行过概括性的研究,而最近区块链安全的研究热也激起了研究者对以太坊的深入了解。

最近的几次CTF比赛中,区块链的题目出现的频率也越来越高,也逐渐进入大家的视野中。今天,我们就针对部分区块链的CTF题目以及生产环境中的实例进行一些相关技术分析,并带领读者一步一步模拟这些漏洞的出现情况。并在以太坊平台上进行相关合约部署,方便研究者更进一步的研究。

在分析开始之前,我们先对智能合约有一个基础的概念了解。

智能合约就是运行在区块链网络上的程序,智能合约与合约执行的结果都会储存在区块链上。在区块链的背景下,智能合约不只是一个计算机程序:它自己就是一个参与者,对接收到的信息进行回应并在同时接受和存储相应的价值。除此之外,它也能同外部地址合约进行交互,向外发送信息和价值。智能合约与一般程序的差异主要体现在以下四个方面:

  • 整合资金流程度

智能合约通过以太坊自带的以太币可以非常容易的整合资金流系统。

  • 部署以及后续费用

一般程序部署在服务器上,程序部署成功后,除了需要花费一些维护费用外不需要其他的额外花费。智能合约在部署的时候需要一笔费用,这些费用将分给参与交易验证的人。而在合约部署成功后,合约会作为不可更改的区块链的一部分,分散地存储在全球各地的以太坊节点上。因此,智能合约部署后,并不需要定期提供维持费用,同时查询已写入区块链的静态数据时也不需要费用,只有在每次通过智能合约写入数据的时候才需要交易费用。

例如:


上述图片为查询owner的信息,而此次查询点击即可获得信息,并不需要支付交易费用。

然而对于根据智能合约写入信息来说,我们则需要进行手续费的提供。

image.png
  • 存储成本不同

一般的应用程序需要将数据存储到服务器上,需要数据时需要从服务器上读取。然而智能合约将数据存储在区块链上,存储数据所需的成本相对比较昂贵,需要根据存储数据的大小支付相当的费用情况。

  • 部署后无法更改

一般的程序可以通过版本升级的方式进行更改,而智能合约一旦部署到区块链上后,就无法更改这个智能合约。

二、关键威胁函数分析

根据知道,在Solidity合约的书写中,跨合约调用是经常出现危险的地方。而我们就要在这里对调用函数进行一些详细的分析。这里我们分别对call()以及delegatecall()函数进行实验分析,之后对某些函数存在的上下文问题进行深入的理论探讨。

在实验中,我部署了

pragma solidity ^0.4.23;
contract subFun {

    address public addr;

    function subTest() public returns (address a){
        addr = address(this);
        
 
    }
}
contract callAndDelegatecall {
    address public b;
    
    address public testaddress;
    constructor(address _address) public {
        testaddress = _address;
    }
    function withcall() public {
        testaddress.call(bytes4(keccak256("subTest()")));
    }
    function withdelegatecall() public {
        testaddress.delegatecall(bytes4(keccak256("subTest()")));
    }
}

并以此代码进行实验。

1 call()函数

image.png

开始的时候,我们传入地址信息并对此函数进行部署。

image.png

由下图,我们通过此函数部署了两个contract。

image.png

此时,我们查看两个合约对应的addr参数的值,我们知道初始时的值均为Ox0000000。

之后我们调用callAndDelegatecall合约中的withcall()函数,将addr的值更改后我们进行查看。

image.png

我们发现subFun合约中的地址被修改,而下面的地址仍然是0x00000。

这就可以很好地说明,调用call()时,上下文环境是被调用的合约的环境。

2 delegatecall()函数

二者执行代码的上下文环境的不同,当使用call调用其它合约的函数时,代码是在被调用的合约的环境里执行,对应的,使用delegatecall进行函数调用时代码则是在调用函数的合约的环境里执行。

对于delegatecall()函数来说,我们同样进行试验。

image.png

点击运行函数后,我们发现了不同的现象。

image.png

我们下面的地址有了改变。我们根据代码进行分析:

由于我调用了

testaddress.delegatecall(bytes4(keccak256("subTest()")));

而这个函数远程调用了子合约中的函数。而我们之严重被改变的地址是父合约的。所以意味着码则是在调用函数的合约的环境里执行。

所以进行总结,我们得出:

  • call: 最常用的调用方式,调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境(合约的 storage)。

  • delegatecall: 调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境。

三、实例分析

根据我们上述代码的实验分析,我们知道由于delegatecall函数是在调用者环境中执行代码的,所以我们可以大胆的进行设想:倘若有某个官方系统的合约代码中存在某个接口能够传入参数,并且拥有delegatecall函数的调用可能。那么我们是否可以通过此来进行合约调用?(因为它的上下文环境是在本机)而下面,我们就要针对这个相关的问题进行delegatecall函数的综合利用。并根据EVM的机制漏洞来实验相关不安全代码。

1 合约实例分析

pragma solidity ^0.4.23;
contract Subcontract {
    uint public start;
    uint public calculatedNumber;

    function setStart(uint _start) public {
        start = _start;
    }

    function setfun(uint n) public {
        calculatedNumber = test(n);
    }

    function test(uint n) internal returns (uint) {

        return start * n;

    }
}


contract Mastercontract {
    address public addr;
    uint public calculatedNumber = 1;
    uint public start = 1;
    uint public withdrawalCounter = 1;
    bytes4 constant fibSig = bytes4(keccak256("setfun(uint)"));

    constructor(address _fibonacciLibrary) public {
        fibonacciLibrary = _fibonacciLibrary;
    }

    function withdraw() public {
        withdrawalCounter += 1;
        require(addr.delegatecall(fibSig,withdrawalCounter),"something wrong");
        msg.sender.transfer(calculatedNumber * 1 ether);
    }


    }
}

分析上述合约,我们来看对应的函数。

首先例子中存在一个Subcontract()合约,这个为子合约。而自合约中存在test函数,而我们能够看出来test函数中返回的值为传入的n值与start的值的乘积。而在setfun()函数中,我们调用test()函数赋值给变量calculatedNumber

而我们再看主合约。对于以太币相关的东西,我们最应该关注的地方就是转账函数。而在withdraw函数中,我们存在msg.sender.transfer(calculatedNumber * 1 ether);函数。而在此函数中,合约会向调用者转账calculatedNumber * 1个以太币。所以倘若我们想增加转账数额,那么我们就需要提高calculatedNumber的值。

而在我们的合约中,我们发现转账参数只有1。所以转账的数额很少。

我们需要修改calculatedNumber的值,而我们并没有在主函数中发现修改其值的地方。然而,这个代码中却存在着很严重的问题。

虽然我们不能直接修改calculatedNumber参数的值,但是我们发现了代码中存在函数调用require(addr.delegatecall(fibSig,withdrawalCounter),"something wrong");。那我们能否在这个地方做手脚呢?

(此处是重点) 在子合约中我们定义了两个uint的变量start 与 calculatedNumber。而在Solidity存储机制中,他们两个被分别存储在slot[0]与slot[1]这两个位置。(代表以太坊虚拟机的两个空间)

类似的,在Mastercontract合约中,addr 与calculatedNumber也被存储在slot[0]与slot[1]这两个位置。而根据我们上面的测试内容,delegatecall保留了合约的上下文,运行环境其实为本合约。这意味着通过delegatecall的代码将对主调用合约的状态(如存储)产生作用。

也就是说,我使用delegatecall ()函数后由于是在主合约的上下文中,所以子合约将去寻找start,而在以太坊机制中,我们并不是通过名字来进行值的获取,而且根据位置了寻找。即库合约中的start的存储位置为slot[0],那么当使用delegatecall时,就是在主调用合约的slot[0]位置去找,但是在主调用合约中slot[0]位置的值为addr。也就是说,我们通过远程调用而改变了主函数中变量的值。

下面我们看具体的代码实验。

首先我们部署子合约:

image.png

之后我们传入子合约地址并部署master合约,之后得到

image.png

之后传递aaa的值为55555:

image.png

再次点击aaa后,我们查看更新后的值:

image.png

发现我们的合约fibonacciLibrary1的值被更改了。

我们将代码放于此:

pragma solidity ^0.4.23;
contract Subcontract {
    
    uint public calculatedNumber;
    // uint public start = 99;

    // function setStart(uint _start) public {
    //     start = _start;
    // }

    function setfun(uint n) public {
        calculatedNumber =  n;
    }

}


contract Mastercontract {
   
    // uint public withdrawalCounter = 20;
    address public fibonacciLibrary1;
    address public fibonacciLibrary;

    
    bytes4 constant fibSig = bytes4(keccak256("setfun(uint256)"));

    constructor(address _fibonacciLibrary) public {
        fibonacciLibrary = _fibonacciLibrary;
        
    }
    function aaa(uint Counter) public {
       
        fibonacciLibrary.delegatecall(fibSig,Counter);
    }

}

我们发现我们并没有能够更改fibonacciLibrary1参数的入口,但是它确实被更改了。也就意味着我们使用delegatecall函数成功了。

2 合约CTF题目分析

下面,我们看一道改编后的ctf题目。

在测试环境中,我们需要用到三个合约地址:

image.png

这三个合约地址分别部署子合约、父合约以及攻击合约。

而下面我们看一下题目。

pragma solidity ^0.4.23;
import "github.com/Arachnid/solidity-stringutils/strings.sol";

contract Ttest {

  address public addr1;
  address public addr2;
  address public owner; 
  using strings for *;


  bytes4 constant setTimeSignature = bytes4(keccak256("set(uint256)"));

  

  constructor(address _a, address _b) public {
    addr1 = _a; 
    addr2 = _b; 
    owner = msg.sender;
  }


  function First(uint _timeStamp) public {
    addr1.delegatecall(setTimeSignature, _timeStamp);
  }


  function Second(uint _timeStamp) public {
    addr2.delegatecall(setTimeSignature, _timeStamp);
  }

  function attack(string name) public returns(string){
    require (owner == msg.sender);
    string memory c = "Congratulations  attacker !!";
    
    return c.toSlice().concat(name.toSlice());
  }
}


contract Library {


  uint first;  

  function set(uint _time) public {
    first = _time;
  }
}

主合约中共有三个函数:First Second attack。而前两个函数用于调用子合约中的set函数。我们在attack()函数中看到,在内部需要require,即在执行此函数的过程中需要将我们的owner身份验证为调用者。(也就是说我攻击者需要将owner改成自己的地址才能攻击)

所以我们根据上面提及的内容,进行分析。我们知道在First Second函数中存在delegatecall (),而我们知道这个函数是在运行函数方的上下文中进行的。所以我们根据上文提及的存储漏洞来进行合约攻击。

首先部署好合约:

image.png

这里分别使用第一个地址与第二个地址部署。

之后我们使用第三个地址部署攻击合约:

image.png

此时我们能够看到目前addr1与addr2变量对应的地址为子合约那个部分的地址。也就是说我现在调用函数会执行自合约部分的set函数。

image.png

之后我使用存储漏洞修改掉地址一。此时我们将attack部署在地址三上。然后传入attack合约地址于First函数中。

image.png

运行后查看得到:

image.png

此时,我们addr1的地址已经变成了部署attack的地方,也就是说此时倘若我运行First()函数,那么我们就会调用attack合约中的set()函数。而我们具体看一下set函数的内容:

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

我们任意的传入参数,之后就会将owner更改为合约所有者——即attacker的地址。

image.png

此时我们调用了First函数,之后我们再看owner的变化。

image.png

它从0x14.......变成了0x4B......。也就是说它变成了我们攻击者的owner地址。

image.png

此时我们就可以调用Ttest合约中的attack()函数(因为已经绕过了owner)。得到:

image.png

至此,我们的攻击成功。

四、参考链接

本稿为原创稿件,转载请标明出处。谢谢。

本文首发于先知社区,地址为:https://xz.aliyun.com/t/3606
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342