可重入漏洞是啥

以太坊

#1

以太坊中的可重入漏洞是啥


#2

我们看看下面这个函数,它可以用于从合约中提取调用者的总余额:

mapping (address => uint) private balances;
function payOut () {
require(msg.sender.call.value(balances[msg.sender])());
balances[msg.sender] = 0;
}

调用 call.value() 会导致合约外部代码的执行。即,若调用者是另一份合约,这就意味着合约回退措施的执行。这可能会在余额调整为 0 之前再次调用 payOut(),从而获得比可用资金更多的资金。
这种情况下的解决方法就是使用替代函数 send() 或 transfer(),后两者函数能为基础运作提供足够的 Gas,而想要再次调用 payOut() 时 Gas 就不足了。
注意,下面这句话也十分重要:
“若合约含有两个共享状态的函数,那么不需要重复调用函数也可能会发生相似的竞态条件(Race Conditions)。”
因此,最好的做法是在转账前更改状态,即转移资金前,在上述代码中把余额设为 0。
The DAO 攻击利用了该漏洞的一种变体。


#3

重入是编程中的一种现象,函数或程序被中断,然后在先前调用完成之前再次调用。在智能合约编程的情况下,当合约A调用合约B中的一个函数时,可能会发生重入,合约B又调用合约A中的相同函数,导致递归执行。在合约状态在关键性调用结束之后才更新的情况下,这可能是特别危险的。

为了理解这一点,想象一下通过钱包合约调用银行合约的提现操作。合约A在合约B中调用提现功能,试图提取金额X。这种情况将涉及以下操作:

  • 合约B检查A是否有必要的余额来提取X。
  • B将X传送到A的地址(运行A的payable fallback函数)
  • B更新A的余额以反映此次提现

无论何时向合约发送付款(如本例中),接收方合约(A)都有机会执行_payable_函数,例如默认的fallback函数。但是,恶意攻击者可以利用这种执行。想象一下,在A的payable fallback中,合约A_再次_调用B银行的提款功能。B的提现功能现在将经历重入,因为现在相同的初始交易正在引发循环调用。

"(1) A 调用 B (2) B 调用 A 的 payable 函数 (1) A 再次调用 B "

在B的退出提现函数的第二次迭代中,B将再次检查A是否有可用余额。由于步骤3(其更新了A的余额)尚未执行,所以对于B来说,无论该函数被重新调用多少次,A仍然具有可用资金来提现。只要有gas可以继续运行,就可以重复该循环。当A检测到gas量不足时,它可以在payable函数中停止呼叫B. B将最终执行步骤3,从A的余额中扣除X. 然而,这时,B可能已经执行了数百次转账,并且只扣除了一次费用。在这次袭击中,A有效地洗劫了B的资金。

这个漏洞因其与DAO攻击的相关性而特别出名。用户利用了这样一个事实,即在调用转移并提取价值数百万美元的ether后,合约中的余额才发生变化。