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