【智能合約系列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 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章