1,DAO
在以太坊上進行DAPP開發的人應該都知道這個著名的bug,它直接導致了以太坊的分叉。
該bug是由於使用了call方法引起的循環調用;
bug威力:直接導致以太坊分叉
源代碼
function splitDAO(uint _proposalID, address _newCurator) noEther onlyTokenholders returns (bool _success) {
// ...
uint fundsToBeMoved = (balances[msg.sender] * p.splitData[0].splitBalance) / p.splitData[0].totalSupply;
if (p.splitData[0].newDAO.createTokenProxy.value(fundsToBeMoved)(msg.sender) == false)
throw;
// ...
// Burn DAO Tokens
Transfer(msg.sender, 0, balances[msg.sender]);
withdrawRewardFor(msg.sender);
// XXXXX Notice the preceding line is critically before the next few
totalSupply -= balances[msg.sender];
balances[msg.sender] = 0;
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))
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)()) {
PayOut(_recipient, _amount);
return true;
} else {
return false;
}
}
簡化一下,代碼就是這個意思:
function splitDAO() {
if(balance[msg.sender] >= amount){
msg.sender.call.value(amount); //bug1: 應該使用transfer或者send來轉賬
balance[msg.sender] = 0; //bug2: 餘額清零應該放到轉賬前面
}
}
漏洞在於,假如msg.sender是一個合約地址,call操作會觸發合約的回退函數,而餘額的清零操作是在轉賬之後,回退函數再次進入splitDAO的時候,餘額根本沒來得及清零。
黑客合約可以這樣構造:
contract HackDAO{
DAO dao;
function withdraw() ownerOnly{
.........
}
function() public payable{
dao.splitDAO();
}
}
當然實際的情況會複雜得多,黑客首先得讓回退函數在DAO合約的餘額快不夠的時候停止攻擊,否則整個交易就會失敗。
如何防範:
function splitDAO() {
if(balance[msg.sender] >= amount){
balance[msg.sender] = 0;
msg.sender.transfer(amount);
}
}
使用transfer或者send方法轉賬有個好處:假如轉給的是一個合約並觸發了合約的回退函數,那麼回退函數中可用的gas只有2300——根部不夠一次函數調用的。
教訓:嚴禁使用call來轉賬
2,Parity 錢包
該bug是由於使用了delegatecall方法引起的,導致黑客拿到了owner權限
bug威力:黑客轉移幾千萬美元的eth,並銷燬了WalletLibrary 合約,導致一部分Wallet合約中的剩餘eth(300w個)永遠取不出來了。
部分源碼:
contract WalletLibrary {
modifier onlyowner {
if (isOwner(msg.sender))
_;
}
modifier onlymanyowners(bytes32 _operation) {
if (confirmAndCheck(_operation))
_;
}
modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
function isOwner(address _addr) constant returns (bool) {
return m_ownerIndex[uint(_addr)] > 0;
}
function initMultiowned(address[] _owners, uint _required) only_uninitialized {
m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1;
for (uint i = 0; i < _owners.length; ++i)
{
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
}
m_required = _required;
}
function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
}
contract Wallet {
uint public m_required;
uint public m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
address constant _walletLibrary = 0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4;
function Wallet(address[] _owners, uint _required, uint _daylimit) {
bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)"));
address target = _walletLibrary;
uint argarraysize = (2 + _owners.length);
uint argsize = (2 + argarraysize) * 32;
assembly {
mstore(0x0, sig)
codecopy(0x4, sub(codesize, argsize), argsize)
delegatecall(sub(gas, 10000), target, 0x0, add(argsize, 0x4), 0x0, 0x0)
}
}
function() payable {
if (msg.value > 0)
Deposit(msg.sender, msg.value);
else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data);
}
}
問題出在initWallet函數這裏,乍一看,有一個only_uninitialized modifier 防止多次調用。其實這裏有個隱藏的漏洞:
Wallet 合約通過delegate來調用walletLibrary 合約的initWallet方法是沒有問題的,only_uninitialized能夠防止Wallet 多次調用initWallet。但是,如果我們構造一個Wallet2合約來調用walletLibrary 合約的initWallet方法,此時only_uninitialized讀的owner是位於Wallet2中的owner——這個owner可以被任意賦值,於是Wallet2可以輕鬆拿到walletLibrary 中的owner權限。
黑客合約可以這樣構造:
contract Wallet2 {
uint public m_required;
uint public m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
mapping(uint => uint) m_ownerIndex;
address constant _walletLibrary = 0x863df6bfa4469f3ead0be8f9f2aae51c91a907b4;
function attack()public{
m_ownerIndex[this] = 1;
bytes4 sig = bytes4(sha3("initWallet(address[],uint256,uint256)"));
_walletLibrary.delegatecall(sig, [this], 0, 0);//這裏會順利調用到initWallet函數,並把this設置爲owner
..........//可以爲所欲爲了
}
}
根本原因在於:delegatecall調用其他合約的代碼,存儲仍然用的當前合約的環境,導致喪失owner權限。
教訓:慎用delegatecall,因爲它的行爲嚴重與人的直覺不符,極易造成漏洞。
另外,黑客在取得了owner權限之後,拿了一些eth,然後把_walletLibrary銷燬了。。。。其他人再也拿不出Wallet中剩餘的eth了,浪費啊。。。
3,美圖幣
該bug是由於在處理乘法操作時沒有使用safeMath,導致溢出。
bug威力:價值6億的BEC幣歸零,黑客在交易所邁出了千萬價值的BEC但後來遭到了回滾。
代碼:
紅框裏的乘法沒有做溢出檢驗,黑客構造了cnt = 2, _value = 0x8000000000000000000000000000000000000000000000000000000000000000
相乘之後溢出,amount變成了1.。。。。。。,然後程序給兩個賬戶各自轉了0x8000000000000000000000000000000000000000000000000000000000000000個BEC!
解決辦法:與餘額相關的操作一定要全部使用safeMath!!!