【智能合约系列003-以太坊安全之 Parity 第一次安全事件漏洞分析】

截止目前,Parity 多重签名钱包共发生过两次安全事件,第一次发生在 2017年07月19日,涉及 Parity 1.5 及以上版本,造成 15万以太币约 3000万美元被盗,第二次发生在 2017年11月07日,致使约 50万枚以太币被锁在合约中无法取出,当时价值大约 1.5亿美元,本篇先对发生于 7月19日的第一次安全漏洞做一下分析,下一篇再分析 2017年11月7日的安全漏洞。

       概括来说,黑客向每个有漏洞的合约发送了两笔交易:第一笔交易用来获取多重签名钱包的拥有权限,第二笔交易是转移合约上的全部资金。

       可从官方默认地址 paritytech/parity 检出代码,再切换到 tag v1.5.x 版本,或直接从这里 问题代码 git id 4d08e7b0aec46443bf26547b17d10cb302672835 进入,来查看完整代码。

攻击分析
       第一步:成为合约的 owner

// enhanced-wallet.sol
// 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);
}

       通过往这个合约地址转账一个value = 0, msg.data.length > 0的交易,以执行_walletLibrary.delegatecall分支。由于通过 json-rpc 调用以太坊智能合约时,to参数为合约地址,而要调用的合约方法会经编码后,放在data参数中,因此代码_walletLibrary.delegatecall(msg.data)理论上能无条件的调用合约内的任何一个函数,本次安全事件就是黑客调用了一个叫做initWallet的函数:

// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
}

       注意参数列表中的_owners,因为是多重签名合约,所以是address[]即地址数组,该函数原本的作用是用多重所有者的地址列表来初始化钱包,函数会继续向底层调用initMultiowned函数:

// constructor is given number of sigs required to do protected "onlymanyowners" transactions
// as well as the selection of addresses capable of confirming them.
function initMultiowned(address[] _owners, uint _required) {
    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;
}

       经过这一步,合约的所有者就被改变了,相当于获取了 Linux 系统的 root 权限。

       第二步: 转账,以owner身份调用execute函数,提取合约余额到黑客的地址:

function execute(address _to, uint _value, bytes _data) external onlyowner returns (bytes32 o_hash) {
    // first, take the opportunity to check that we're under the daily limit.
    if ((_data.length == 0 && underLimit(_value)) || m_required == 1) {
        // yes - just execute the call.
        address created;
        if (_to == 0) {
            created = create(_value, _data);
        } else {
            if (!_to.call.value(_value)(_data))
                throw;
        }
        SingleTransact(msg.sender, _value, _to, _data, created);
    } else {
        // determine our operation hash.
        o_hash = sha3(msg.data, block.number);
        // store if it's new
        if (m_txs[o_hash].to == 0 && m_txs[o_hash].value == 0 && m_txs[o_hash].data.length == 0) {
            m_txs[o_hash].to = _to;
            m_txs[o_hash].value = _value;
            m_txs[o_hash].data = _data;
        }
        if (!confirm(o_hash)) {
            ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data);
        }
    }
}

       注意函数第一行后面的修改器限制为onlyowner,黑客进行上面的动作就是为了突破该限制。

// simple single-sig function modifier.
modifier onlyowner {
    if (isOwner(msg.sender))
        _;
}

       因此,问题的关键就在于,上面的initWallet没有检查以防止在合约初始化后再次调用到initMultiowned,进而使得合约的所有者被改成黑客。

解决方案:
       通过上面的分析可以看到,核心问题在于越权的函数调用,那修复方法便是对initWallet及与之相关的接口方法initDaylimit和initMultiowned重新定义访问权限:

// throw unless the contract is not yet initialized.
modifier only_uninitialized {
    if (m_numOwners > 0) throw; 
        _;
}

       通过检查m_numOwners变量值,若已经初始化,则直接返回(旧版 solidity 中是抛出异常),不允许再执行initWallet等方法:

// constructor - stores initial daily limit and records the present day's index.
function initDaylimit(uint _limit) only_uninitialized {
    m_dailyLimit = _limit;
    m_lastDay = today();
}

// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
}

// constructor is given number of sigs required to do protected "onlymanyowners" transactions
// as well as the selection of addresses capable of confirming them.
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;
}

       可注意到,每个函数第一行的最后面都添加了限定修改器标识only_uninitialized,这就是 Parity 多重签名钱包,第一次安全事件的漏洞原理和解决办法,该漏洞发生于 2017年07月19日,致使大约 3000万美元资产被盗。下一篇我们分析 Parity 的第二次安全事件。


转自:https://blog.csdn.net/xuguangyuansh/article/details/80786691 
 

猜你喜欢

转载自blog.csdn.net/linshenyuan1213/article/details/86073137