区块链安全—整数溢出分析

一、前言

最近在对区块链漏洞进行一些代码研究,在实例复现以及竞赛题目的研究中,我发现整数溢出是现在区块链比较火的漏洞之一。但是整数漏洞的技术难度并不大,容易理解。但是它常常出现在Solidity代码中,容易被攻击者利用。

本篇文章中,我们就针对整数溢出漏洞进行原理上的分析,并对部分实例以及竞赛题目进行线上实验,并提出一些预防措施。

二、漏洞介绍

在介绍整数溢出漏洞前,我们需要先简单介绍一下Solidity中的部分语法知识。

在solidity中,我们知道在变量类型中有int/uint(变长的有符号与无符号整形)类型。这类变量支持以8递增,支持从uint8到uint256,以及int8到int256。而需要我们注意的时,uint与int默认表示uint256与int256 。

我们知道,无符号整形是计算机编程中一种数值类型,其只能表示非负数(0以及正数)。然而有符号整形(int)可以表示任何规定范围内的整数。

有符号整数能够表示负数的代价是能够存储正数的范围缩小,因为其约一半的数值范围需要表示负数。如:uint8的存储范围为0255,然而int8的范围为-127127.

如果用二进制表示的话:

  • uint8 :0b00000000 ~ 0b1111111 每一位都存储相关内容,其范围为0~255 。

  • int8 :0b1111111 ~ 0b0111111 最左边一位表示符号,1表示为负数,0表示为正,范围为 -127~127 。

而整数溢出的概念是什么呢?我们来看一个简单的例子:

pragma solidity ^0.4.10;

contract Test{
​
  // 整数上溢
  //如果uint8 类型的变量达到了它的最大值(255),如果在加上一个大于0的值便会变成0
  function test() returns(uint8){
    uint8 a = 255;
    uint8 b = 1;
​
    return a+b;// return 0
  }
​
  //整数下溢
  //如果uint8 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成255(uin8 类型的最大值)
  function test_1() returns(uint8){
    uint8 a = 0;
    uint8 b = 1;
​
    return a-b;// return 255
  }
}

在这个例子中,我们知道uint8的范围为——0~255,所以我们的测试代码中定义变量使用了uint8 。

test()中,我们赋值了a,b两个变量,令a+b。

image.png

按照我们的理解,a+b = 255+1 =256才对。现在我们进行测试。

我们得到的并不是256,而是0 。

类似的,我们对test1()函数进行测试。0 -1 应该为-1,而uint并不包括负数的部分。所以结果应该是什么样子的呢?

我们发现,得到的值为255 。

这就是我们所提及的上下溢。假设我们有一个 uint8, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制 11111111 (或者说十进制的 2^8 - 1 = 255) 。下溢(underflow)也类似,如果你从一个等于 0 的 uint8 减去 1, 它将变成 255 (因为 uint 是无符号的,其不能等于负数)。

而上面的例子介绍了原理。下面我们将逐步向读者介绍溢出漏洞是如何在生产环境中进行恶意攻击的。

三、真实例子

1 模拟场景例子

下面我们看一个真实环境中的真实场景问题。

pragma solidity ^0.4.10;


contract TimeLock {
    
    mapping(address => uint256) public balances;
    mapping(address => uint256) public lockTime;
    
    function deposit() public payable {
        balances[msg.sender] += msg.value;
        lockTime[msg.sender] = now + 1 weeks;
    }
    
    function increaseLockTime(uint _secondsToIncrease) public {
        lockTime[msg.sender] += _secondsToIncrease;
    }
    
    function withdraw() public {
        require(balances[msg.sender] > 0);
        require(now > lockTime[msg.sender]);
        balances[msg.sender] = 0;
        msg.sender.transfer(balances[msg.sender]);
    }
    
    function getTime() public constant returns(uint256) {
        return lockTime[msg.sender];
    }
}

上述合约描述了一个银行存定期的合约。

我们在合约中针对每个用户设置了一个mapping用于存储定期时间。而键为address类型,值为uint256类型。

之后我们拥有三个函数--deposit ()为存钱函数,并且存储时间至少为一个礼拜;increaseLockTime ()为增加存钱时间的函数,用户可以自行增加存款时间;withdraw ()为取钱函数,当用户拥有余额并且现在的时间>存款时间后变可以将所有的钱提取出来。

此时,我们可以对漏洞进行分析。倘若用户存钱后想要将钱提取取出来可行吗?根据我们对合约的设置来讲,我们并不希望用户可以提取将钱取出。但是我们来看下面的函数:

function increaseLockTime(uint _secondsToIncrease) public {
        lockTime[msg.sender] += _secondsToIncrease;
    }

这个函数中我们可以自行传入变量,并更新lockTime[]的值。而我们知道其值的类型为uint256,即我们可以使用整数溢出漏洞,传入2^256- userLockTime。以进行溢出使变量的值变成0 。下面我们进行测试:

首先我们对合约进行部署:

image.png

之后我们存入部分钱:

image.png

我们可以查看存入的钱的数量以及时间(一周)。

下一步我们看看能不能提出钱:

发现失败了emmmm。

所以我们现在想办法,传入2^256- userLockTime即:115792089237316195423570985008687907853269984665640564039457584007913129639936 -1546593080115792089237316195423570985008687907853269984665640564039457584007911583046856

image.png

传入数据,之后我们查看剩余时间:

之后我们就可以把钱取出来了。

2 SMT合约的安全研究

SmartMesh Token是基于Ethereum的合约代币,简称SMT。Ethereum是一个开源的、公共的分布式计算平台,SmartMesh代币合约SmartMeshTokenContract基于ERC20Token标准。漏洞发生在转账操作中,攻击者可以在无实际支出的情况下获得大额转账。

合约源码地址为:https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code

我们在这里放上关键函数:

function transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt,
        uint8 _v,bytes32 _r, bytes32 _s) public transferAllowed(_from) returns (bool){
​
        if(balances[_from] < _feeSmt + _value) revert();
​
        uint256 nonce = nonces[_from];
        bytes32 h = keccak256(_from,_to,_value,_feeSmt,nonce);
        if(_from != ecrecover(h,_v,_r,_s)) revert();
​
        if(balances[_to] + _value < balances[_to]
            || balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
        balances[_to] += _value;
        Transfer(_from, _to, _value);
​
        balances[msg.sender] += _feeSmt;
        Transfer(_from, msg.sender, _feeSmt);
​
        balances[_from] -= _value + _feeSmt;
        nonces[_from] = nonce + 1;
        return true;
    }

这里我们简单的介绍一下这个函数的内容。

这里我们要介绍一下代理的概念。这个转账函数需要一个中间代理来帮助用户A与用户B进行转账操作。也许这个代理就类似于代理矿工机制,挖矿成功的人才能够进行tx的打包操作(这是个人想法)。

首先这个函数会传入几个关键的参数:address _from, address _to, uint256 _value, uint256 _feeSmt。这里分别代表了用户A的地址(转账人)、用户B的地址(收款人的地址)、转账金额、手续费。

之后我们进入第一层判断:balances[_from] < _feeSmt + _value。即转账人是否能支付得起手续费+转账费用。

之后的一些其他判断就省略了。直到后面。

        balances[_to] += _value;
//收款人账户添加上转账金额
        Transfer(_from, _to, _value);
​//函数调用者账户增加上手续费金额
        balances[msg.sender] += _feeSmt;
        Transfer(_from, msg.sender, _feeSmt);
        balances[_from] -= _value + _feeSmt;

而我们的攻击具体发生在if(balances[_from] < _feeSmt + _value) revert();处。

因为_feeSmt_value参数是我们可控的,可以手动进行传输的。所以我们可以控制参数的传入。而我们发现参数定义为uint256,所以2^256。所以如果我们传入的_feeSmt + _value的值等于 2^256+h。所以_feeSmt + _value = h。所以当我们的h设置的很小的时候,balances[_from] < h便可以绕过(即使我并没有_feeSmt + _value这么多钱)。

 例如:_feeSmt = 8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ,
 value = 7000000000000000000000000000000000000000000000000000000000000001
// _feeSmt和value均是uint256无符号整数,相加后最高位舍掉,结果为0。

之后_from的账户就要向_to账户转账7000000000000000000000000000000000000000000000000000000000000001;向msg.sender转账8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff。瞬间没有钱了就。

那我们如何对代码进行修复呢?这里参考一篇博客的做法:

// 在这里做整数上溢出检查
        if(balances[_to] + _value < balances[_to]
            || balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
​
        // 在这里做整数上溢出检查 ,防止交易费用 过大
        if(_feeSmt + _value < _value ) revert();
​
        // 在这里做整数上溢出检查 ,防止交易费用 过大
        if(balances[_from] < _feeSmt + _value) revert();
​
        uint256 nonce = nonces[_from];
        bytes32 h = keccak256(_from,_to,_value,_feeSmt,nonce);
        if(_from != ecrecover(h,_v,_r,_s)) revert();
​
        // 条件检查尽量 在开头做
        // if(balances[_to] + _value < balances[_to]
        //     || balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
        balances[_to] += _value;
        Transfer(_from, _to, _value);
​
        balances[msg.sender] += _feeSmt;
        Transfer(_from, msg.sender, _feeSmt);
​
        balances[_from] -= _value + _feeSmt;
        nonces[_from] = nonce + 1;
        return true;

增加判断来避免上述情况产生。

3 BEC合约安全漏洞

这个漏洞利用跟上述内容类似,均属于控制传入内容来达成利用。

  function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
    uint cnt = _receivers.length;
    uint256 amount = uint256(cnt) * _value;
    require(cnt > 0 && cnt <= 20);
    require(_value > 0 && balances[msg.sender] >= amount);
​
    balances[msg.sender] = balances[msg.sender].sub(amount);
    for (uint i = 0; i < cnt; i++) {
        balances[_receivers[i]] = balances[_receivers[i]].add(_value);
        Transfer(msg.sender, _receivers[i], _value);
    }
    return true;
  }
}

这里我们可控的内容为_receivers与转账金额value

所以我们可以通过控制传入cnt的大小来控制amount的值。(uint256 amount = uint256(cnt) * _value;

使amount向上溢出,成为一个极小值。之后绕过require(_value > 0 && balances[msg.sender] >= amount)​。从而使系统向所有的_receivers[]转账。具体的代码大家可以参考:
https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code

四、竞赛题目

我们可以查看题目https://ethernaut.zeppelin.solutions/level

题目代码:

pragma solidity ^0.4.18;

contract Token {

  mapping(address => uint) balances;
  uint public totalSupply;

  function Token(uint _initialSupply) public {
    balances[msg.sender] = totalSupply = _initialSupply;
  }

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}

这个题目是十分简单的。根据我们上面的分析,这个题目存在整数溢出。题目要求我们获得额外的大量token。

我们根据代码知道合约起始会给用户20个token,所以我们起始是拥有20金币的。我们再看内部的转账函数是在transfer ()中。在这里我们需要balances[msg.sender] - _value >= 0。且为 20 - value >=0所以我们可以传入21 。根据溢出使balances[msg.sender] -= _value=>‘20 - 21’ ---->上溢。

所以我们将合约部署。

image.png

之后我们调用函数:

image.png

我们可以查看我们player的账户金额。

image.png

之后提交合约。

image.png

五、参考资料

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

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

推荐阅读更多精彩内容