十七、以太坊安全之 recursive(递归调用)
先看下面一段代码:
function withdrawBalance() {
amountToWithdraw = userBalances[msg.sender];
if (amountToWithdraw > 0) {
if (!(msg.sender.call.value(amountToWithdraw)())) {throw;}
userBalances[msg.sender] = 0;
}
}
这是一段给用户取款的代码,让用户一次性从你的智能合约里取回存款。例如,你的合约账户共有 1000 个 ether,而某用户存有 10 个 ether。因为该代码存有严重的递归调用漏洞,该用户可轻松地将你账户里的 1000 个 ether 全部提走。
首先,该段代码使用了addr.call.value()() 来发送 ether,而不是 send(),给 fallback 函数的调用提供了足够多的 gas。你只要将 fallback 函数写成如下的方式便可取走所有的 ether:
function() {
address addr = 0x6c8f2a135f6ed072de4503bd7c4999a1a17f824b;
if (COUNT < 100) {
addr.call("withdrawBalance");
COUNT++;
}
}
在这段 fallback 代码中,当计数器小于 100 时,递归调用 withdrawBalance 函数。在这种情况下,
msg.sender.call.value(amountToWithdraw)()
将被调用100次,从而取走 100 * 10 ether。
所以在写智能合约时,需要考虑到它可能被递归调用,在这个 case 里,我们可以这样调整代码以防止递归调用而出现的问题:
function withdrawBalance() {
amountToWithdraw = userBalances[msg.sender];
userBalances[msg.sender] = 0;
if (amountToWithdraw > 0) {
if (!(msg.sender.call.value(amountToWithdraw)())) {
userBalances[msg.sender] = amountToWithdraw;
throw;
}
}
}
是不是很眼熟,没错,知名的 DAO 漏洞,就是 fallback + recursive 的一个结合体,由于笔者之前收录的一份漏洞清单中,fallback 及 recursive 也独立的作为两个漏洞条目,因此,这里再针对性的分析一下。