2016年6月20日,以太坊用于开发智能合约的静态语言 Solidity,被发现了一个安全漏洞,它可以影响整个以太坊,而不仅仅是 DAO。
还记得DAO漏洞吗,下面再简单复习一下,已经很熟悉的小伙伴可以直接跳过此部分。DAO 漏洞的完整文章可以 点击这里。
function splitDAO(uint _proposalID, address _newCurator) noEther onlyTokenholders returns (bool _success) {
// ...
// XXXXX Move ether and assign new Tokens. Notice how this is done first!
uint fundsToBeMoved = (balances[msg.sender] * p.splitData[0].splitBalance) / p.splitData[0].totalSupply;
if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
// XXXXX This is the line the attacker wants to run more than once
throw;
// ...
// Burn DAO Tokens
Transfer(msg.sender, 0, balances[msg.sender]);
withdrawRewardFor(msg.sender); // be nice, and get his rewards
// XXXXX Notice the preceding line is critically before the next few
totalSupply -= balances[msg.sender]; // XXXXX AND THIS IS DONE LAST
balances[msg.sender] = 0; // XXXXX AND THIS IS DONE LAST TOO
paidOut[msg.sender] = 0;
return true;
}
function withdrawRewardFor(address _account) noEther internal returns(bool _success) {
if ((balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply < paidOut[_account])
throw;
uint reward = (balanceOf(_account) * rewardAccount.accumulatedInput()) / totalSupply - paidOut[_account];
if (!rewardAccount.payOut(_account, reward)) // XXXXX vulnerable
throw;
paidOut[_account] += reward;
return true;
}
function payOut(address _recipient, uint _amount) returns (bool) {
if (msg.sender != owner || msg.value > 0 || (payOwnerOnly && _recipient != owner))
throw;
if (_recipient.call.value(_amount)()) { // XXXXX vulnerable
PayOut(_recipient, _amount);
return true;
} else {
return false;
}
}
最后一个函数 payOut 中,_recipient.call.value(_amount)()
这句会使用黑客地址的_recipient
去调用黑客的 fallback
函数,而黑客会在 fallback
中再去调用 DAO 的 splitDAO 函数,进而源源不断的将代币转移到黑客的地址。
以太坊合约常规性的调用其他合约。这是社区鼓励采用的行为,愿景是智能合约在任何地方进行互动,并减少重复内容在链上的资源占用和gas消耗。结果是,当一个以太坊合约与另一个合约交互时,它会丧失对自己程序状态的控制。
如果你使用Solidity的call
调用功能,同时你自己的合约有一个可以被外部调用的函数可以修改状态,那么你无法对你的合约在调用外部函数之后的状态做任何假设。
非常重要的一点,应区分这个漏洞和可重入性漏洞——一个已知的漏洞,并被用于攻击DAO。
可重入攻击(DAO漏洞)可简化成下面的形式:
/** * DAO 合约 */
contract Dao {
mapping(address => uint256) balances;
function splitDAO() public {
// addressH 为黑客地址,由于后面的 balances[addressH] = 0; 无法得到执行,导致 if 内容始终为 true
if (balances[addressH] > 0)
_recipientH.call.value(_amount)();
// addressH 为黑客地址,但这一句却不会被执行到,因为上面一句 _recipientH.call.value(_amount)(); 会执行黑客的 fallback,
// 而 fallback 又会从头调用 splitDAO.
balances[addressH] = 0;
}
}
/** * DAO 黑客合约 */
contract DaoH {
function() public {
_recipientDAO.call("splitDAO", _proposalID, _newCurator); // _recipientDAO 为 DAO 合约地址
}
}
太阳风暴攻击的一种情况会是下面的形式
/** * SolarStorm A 合约 */
contract SolarStormA {
mapping(address => uint256) balances;
function A (address _address) {
if (balances[_address] > 0)
_recipientB.call("B", ...arguments);
if (balances[_address] > 0) // balances[_address] > 0 将不再成立
...
else
...
}
function C (address _address) {
balances[_address] = 0;
}
}
/** * SolarStorm B 合约 */
contract SolarStormB {
function B (address _address) {
_recipientA.call("C", addressB);
}
}
即:
1 合约A,函数A调用合约B。
2 合约A有另外一个函数C,与函数A共享状态。
2 合约B 调用合约A,函数C。
所以,太阳风暴的一个更泛化描述是:
1 合约A调用任何外部合约
2 合约A有外部函数来修改状态(多数情况下都有)
1、类似太阳风暴的漏洞会冲击以太坊上的所有智能合约,而不仅仅是DAO。这是以太坊用于开发智能合约的类java-script语言 Solidity 的问题。
2、可能在以太坊已经发布的合约中存在这种漏洞。开发者应当检查是否他们的合约具有脆弱性,并采取相应措施(转移资金,发布新合约)。
3、开发者在未来的合约中,应当对外部调用极度谨慎。或能够避免外部调用,直到本问题被解决。