截止目前,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