本文通过编写有漏洞的合约,来了解如何攻击并理解如何预防漏洞的发生。
- 原文:https://betterprogramming.pub/preventing-smart-contract-attacks-on-ethereum-delegatecall-e864d0042188
- 译文出自:登链翻译计划
- 译者:aisiji
- 校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
本文通过编写有漏洞的合约,来了解如何攻击并理解如何预防漏洞的发生。
图片来源: Arnold Francisca
与call
不同,用DELEGATECALL
进行函数调用时,其代码是在当前调用函数的环境里执行,因此,构建无漏洞自定义库并不像想象的那么简单。有时库代码本身可能是安全无漏洞的;然而当它应用到另一个合约的上下文中却有可能出现漏洞。我们来看一个复杂一点的例子:使用斐波那契数列。
备注:斐波那契数列是指从 0 1 开始,之后的数总是之前的和:0, 1, 1, 2, 3, 5, 8, 13 ...
下面是FibonacciLib.sol
中的一个库,它可以生成斐波那契数列或者类似的数列。
// library contract - calculates Fibonacci-like numbers
contract FibonacciLib {
// initializing the standard Fibonacci sequence
uint public start;
uint public calculatedFibNumber;
// modify the zeroth number in the sequence
function setStart(uint _start) public {
start = _start;
}
function setFibonacci(uint n) public {
calculatedFibNumber = fibonacci(n);
}
function fibonacci(uint n) internal returns (uint) {
if (n == 0) return start;
else if (n == 1) return start + 1;
else return fibonacci(n - 1) + fibonacci(n - 2);
}
}
-FibonacciLib.sol-
这个库提供了一个函数,它将生成数列中第n个斐波那契数。函数允许用户修改数列的起始数字(start
)并且计算新数列的第n个斐波那契数。
现在看看下面的合约FibonacciBalance.sol
如何使用这个库。
contract FibonacciBalance {
address public fibonacciLibrary;
// the current Fibonacci number to withdraw
uint public calculatedFibNumber;
// the starting Fibonacci sequence number
uint public start = 3;
uint public withdrawalCounter;
// the Fibonancci function selector
bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)"));
// constructor - loads the contract with ether
constructor(address _fibonacciLibrary) external payable {
fibonacciLibrary = _fibonacciLibrary;
}
function withdraw() {
withdrawalCounter += 1;
// calculate the Fibonacci number for the current withdrawal user-
// this sets calculatedFibNumber
require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
msg.sender.transfer(calculatedFibNumber * 1 ether);
}
// allow users to call Fibonacci library functions
function() public {
require(fibonacciLibrary.delegatecall(msg.data));
}
}
-FbonacciBalance.sol-
上面的合约允许参与者从合约提取以太币,取款数量等于参与者取款顺序对应的斐波那契数;也就是说:第一个取款的参与者可以得到1个以太币,第二个也可以得到1个,第三个可以得到2个,第四个可以得到3个,第五个可以得到5个,以此类推,直到合约余额比取款的斐波那契数小。
你可能已经注意到了,在库和主调合约中都使用了状态变量start
。在库合约中,start
被用于指定斐波那契数列的起始数字并被设置为0
,而在主调合约中它被设置为3
。你可能也注意到了,FibonacciBalance
合约中的fallback函数会把所有调用委托传递给库合约,这让库合约的setStart
函数也可以被调用。回想一下,我们保留了合约的状态,所以实际上允许在本地FibonnacciBalance
合约中修改start
变量的状态。如果这样的话,start
被修改为更大的值就可以提取更多的以太币,因为calculatedFibNumber
依赖于start
变量(如库合约所示)。事实上,在FibonacciBalance
合约中,setStart
函数不会(也不能)修改start
变量。这个合约的潜在漏洞比仅仅修改start
变量要糟糕的多,请继续往下看。
请注意,在第21行,withdraw
函数已经执行了fibonacciLibrary.delegatecall(fibSig,withdrawalCounter)
。调用了setFibonacci
函数,修改了存储slot[1]
,当前是calculatedFibNumber
(即,在执行后,calculatedFibNumber
已经被修改了)。但是注意,FibonacciLib
合约的start
变量存储在slot[0]
,其实是当前合约的fibonacciLibrary
地址。这意味着fibonacci
函数将返回一个非预期的结果,因为它引用了start
(即slot[0]
),当前是fibonacciLibrary
地址(它被解释为uint
时,会非常大)。因此,withdraw
函数会返回,因为合约余额不足uint(fibonacciLibrary)
因为calculatedFibNumber
会返回的非常大的值。
更糟糕的是,FibonacciBalance
合约让用户通过26行的fallback函数调用所有fibonacciLibrary
函数,包括setStart
函数,而这个函数允许任何人修改或者设置存储slot[0]
。这时,slot[0]
是fibonacciLibrary
地址。因此,攻击者可以创建一个恶意合约,修改这个地址为uint
(这可以在Python中用int('<address>',16)
轻松完成),然后调用setStart(<attack_contract_address_as_uint>)
。将fibonacciLibrary
地址改为攻击合约的地址。之后,任何时候,当用户调用withdraw
或者fallback函数时,恶意合约就会执行,并盗取合约的全部余额。
下面是一个攻击合约的例子:
contract Attack {
uint storageSlot0; // corresponds to fibonacciLibrary
uint storageSlot1; // corresponds to calculatedFibNumber
// fallback - this will run if a specified function is not found
function() public {
storageSlot1 = 0; // we set calculatedFibNumber to 0, so if withdraw
// is called we don't send out any ether
<attacker_address>.transfer(this.balance); // we take all the ether
}
}
-Attack.sol-
Solidity提供了一个library
关键字,用于实现库合约。这就确保了库合约是无状态且不可自毁的。
库合约:
contract WalletLibrary is WalletEvents {
...
// throw unless the contract is not yet initialized.
modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
// constructor - just pass on the owner array to multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit)
only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
...
}
-WalletLibrary.sol-
钱包合约:
contract Wallet is WalletEvents {
...
// METHODS
// gets called when no other function matches
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data);
}
...
// FIELDS
address constant _walletLibrary =
0xcafecafecafecafecafecafecafecafecafecafe;
}
-Wallet.sol-
请注意,Wallet
合约通过一个委托调用将所有调用传递给WalletLibrary
合约。代码中的_walletLibrary
常量地址作为实际部署WalletLibrary
合约的一个占位符(实际在0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4
)。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!