智能合约的攻击与反攻击之重入攻击(Reentrancy)

in #cn4 years ago (edited)

上次我弄了个用智能合约替代OTC市场,里面提到的公链上智能合约的最大问题就是容易被黑客攻击,而且损失成本极高,所以我们不得不对安全问题进行认真研究。

一种最主要的攻击方式被称为“重入” (Reentrancy),以太坊智能合约都有个缺省的收钱函数叫做“fallback",收到以太币,就执行这个,或者不存在被调用函数,也要调用fallback,如果在fallback里继续调用被攻击的智能合约中代码,就可以源源不断把合约中钱捞出来啦。 举例说明,针对如下代码:

mapping (address => uint) private userBalances;

function withdrawBalance() public 
{
    uint amountToWithdraw = userBalances[msg.sender];
    bool success = msg.sender.call.value(amountToWithdraw)(""); // At this point, the caller's code is executed, and can call withdrawBalance again
    require(success);
    userBalances[msg.sender] = 0;
}

黑客伪代码如下:

import 'attackobject.sol'
contract iamhack 
{ 
     public dao = attackobject(0x2ae...);    
     address owner;     /*这里是黑客自己的钱包地址*/ 
     constructor() public 
     {        
        owner = msg.sender;    
     }
    /*fallback 函数,接受钱财*/    
    function() public 
    {         
        dao.withdrawBalance();    
    }        
    /*黑客调用这个函数把钱从智能合约中转移到他自己的钱包里去*/    
    function drainFunds() payable public
    {        
        owner.transfer(address(this).balance);    
    }
}

黑客在接受以太币后,再次调用被攻击合约的withdrawBalance函数,由于函数里没有及时检查客户账号里还有没有余额,导致黑客能源源不断地取钱。

著名的DAO被黑事件,就是因为这个fallback被利用了,导致了价值50 Million 美元的以太被盗,由此导致以太坊被硬分叉

A DAO is a Decentralized Autonomous Organization. Its goal is to codify the rules and decisionmaking apparatus of an organization, eliminating the need for documents and people in governing, creating a structure with decentralized control.

On June 17th 2016, The DAO was hacked and 3.6 million Ether ($50 Million) were stolen using the first reentrancy attack.

Ethereum Foundation issued a critical update to rollback the hack. This resulted in Ethereum being forked into Ethereum Classic and Ethereum.

重入攻击还有一些变体,比如Cross-function Reentrancy,但是其基本原理就是利用被攻击函数调用外部函数的时机来进行攻击。

解决方法之一就是在withdrawBalance一开始就更新客户账号余额,这样黑客接下来调用这个函数就没有钱可以捞了。

mapping (address => uint) private userBalances;

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    userBalances[msg.sender] = 0;
    bool success = msg.sender.call.value(amountToWithdraw)(""); // 因为一旦调用这个函数,就把客户账户余额至0,所以黑客接下去再调用这个函数也没用了。
    require(success);
}

这个解决方法的原理就是假设外在调用都是不安全的,原则就是完成所有内部工作,比如更新状态,然后再调用外部函数

解决方法之二是用‘锁’,就是锁定状态,只有合约本身可以改变这个锁的状态,这个在合约本身逻辑繁杂,而且有很多共享状态时候尤其管用。例子:

mapping (address => uint) private userBalances;
bool private lockBalances;

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    require(!lockBalances && amount > 0 && userBalances[msg.sender] >= amount);
    lockBalances = true;
    bool success = msg.sender.call.value(amountToWithdraw)(""); //在这里黑客去反复调用这个函数也然并卵,因为lockBalances不让程序继续执行下来。
    require(success);
    userBalances[msg.sender] = 0;
    lockBalances = false;
}

还有很多攻击和反攻击的机制,很有意思,下次再聊。

相关文章阅读:

Sort:  

收藏下,慢慢研究!