《我学区块链》—— 12、以太坊安全之 Parity 第一次安全事件漏洞分析

12、以太坊安全之 Parity 第一次安全事件漏洞分析

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

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

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

攻击分析

       第一步:成为合约的 ownerjson

// 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 权限。less

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

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,进而使得合约的全部者被改为黑客。ui

解决方案:

       经过上面的分析能够看到,核心问题在于越权的函数调用,那修复方法即是对initWallet及与之相关的接口方法initDaylimitinitMultiowned从新定义访问权限:

// 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 的第二次安全事件。