跨链桥,区块链的基础设施之一,所实现的功能是允许用户将自己的资产从一条链转移至另外一条链上,是连接不同的区块链的关键桥梁,常使用中心化的方式进行实现。由于跨链桥自身往往存储有用户所质押的巨额资产,是Web3黑客最热衷于攻击的目标之一。近年来,与跨链桥相关的攻击与漏洞屡见不鲜,本文将详述Harmony跨链桥黑客攻击事件和Wormhole代理合约未初始化漏洞。
Harmony跨链桥黑客攻击事件
2022年6月23日,Harmony链与以太坊间的跨链桥上发生了恶意攻击,攻击者控制了跨链桥所有者的私钥后,直接使用管理员权限通过特权函数转移走了该桥所持有的大量各类代币,导致Harmony链上价值约9700万美元的资产被盗。
该跨链桥由一个多签钱包所控制,其本意是通过要求至少有N位管理员的授权才能执行操作,来提高钱包的安全性和攻击门槛,在实际代码实现中N=2。
多签钱包实现为一个合约MultiSigWallet.sol,其中能够处理资产转移的函数executeTransaction()本身带有confirmed(transactionId, msg.sender)校验,需要2位不同的owner依次调用submitTransaction、confirmTransaction两函数,才能通过操作权限校验,以多签钱包身份发起资产转移等任意操作。
多签钱包地址:
https://etherscan.io/address/0x715CdDa5e9Ad30A0cEd14940F9997EE611496De6
/// @dev Allows anyone to execute a confirmed transaction.
/// @param transactionId Transaction ID.
function executeTransaction(uint256 transactionId)
public
ownerExists(msg.sender)
confirmed(transactionId, msg.sender)
notExecuted(transactionId)
{
if (isConfirmed(transactionId)) {
Transaction storage txn = transactions[transactionId];
txn.executed = true;
if (
external_call(
txn.destination,
txn.value,
txn.data.length,
txn.data
)
) emit Execution(transactionId);
else {
emit ExecutionFailure(transactionId);
txn.executed = false;
}
}
}
https://etherscan.io/address/0x715CdDa5e9Ad30A0cEd14940F9997EE611496De6#code
MultiSigWallet.sol
于是,保证这些owner私钥的保密性是Harmony跨链桥的安全瓶颈之一。
从目前证据来看,攻击者可能通过其他链下攻击手段控制了多签钱包中的2个所有者私钥,成为了事实上的桥的所有者,通过这些权限校验都已经不在话下。攻击者可以正常地使用所有者权限进行资产的管理,包括将所有资产转移至攻击者账户中。
实际发生的资产转移交易https://etherscan.io/tokentxns?a=0x0d043128146654c7683fbf30ac98d7b2285ded00
小结
在由密码学为基础建构起的区块链世界中,没有什么比确保私钥自身的保密性更加重要的事情了,私钥泄漏是最无解、同时也是危害最大的安全隐患。跨链桥的开发者需要注意项目本身中心化的风险,确保项目所私用的私钥的安全性。同时,区块链的每一个使用者也都必须清楚地明白:道路千万条、私钥保密第一条。
Wormhole代理合约未初始化漏洞
2022年2月24日,匿名白帽子黑客satya0x负责任地披露了Wormhole主桥(以太坊侧)的一个代理合约未初始化漏洞,一举拿下Web3漏洞赏金平台immunefi有史以来单笔最高的漏洞赏金:一千万美元。看到这一数字的读者朋友们想必会不由得发问:究竟是如何逆天的漏洞才能够收割这笔天价赏金?
据长亭科技区块链安全研究员分析,该漏洞是Wormhole跨链项目以太坊侧代理合约未初始化而导致的漏洞,黑客可以通过提前控制代理合约背后的实际实现合约,并调用更新函数迫使实现合约自毁,使得Wormhole项目所有的以太币都被冻结在原地址中,用户已经转移至Wormhole的资产也将会被永久冻结、再也无法取出。
根据国家区块链漏洞库区块链漏洞定级细则中智能合约漏洞定级细则v1.0,该漏洞会使得智能合约核心业务无法正常运行,造成他人资产大额冻结,在危害程度上划分为严重危害;同时利用成本较低,无特殊利用门槛,且漏洞可稳定触发,在利用难度上划分为低难度。综合这两个因素考虑,该漏洞定级为高危漏洞。
目前漏洞已修复,我们现在讨论一下本次wormhole漏洞的原理。
漏洞合约地址:
https://etherscan.io/address/0x736d2a394f7810c17b3c6fed017d5bc7d60c077d
什么是Wormhole
Wormhole跨链协议是连接solana和以太坊的桥,通过Wormhole,DeFi项目可以避开以太坊的拥堵和高费用,并享受Solana的低费用和高吞吐量、快速的交易体验。
——Solana Wormhole 项目主页
从Wormhole项目的介绍中我们可以知道,Wormhole是连接两条相互不兼容的公链:以太坊和Solana的一个协议,在以太坊和Solana上都部署有合约程序,在以太坊上运行的程序称为以太坊智能合约,在Solana上运行的程序称为Solana program,两个分别运行于不同公链的程序需要共同发挥作用,完成用户的资产跨链请求。本次代理合约未初始化漏洞发生在Wormhole项目的以太坊智能合约中。
什么是以太坊智能合约
以太坊(Ethereum)是目前最广为人知的公链系统,其原生货币以太币是市值第二的加密货币。以太坊智能合约是运行于以太坊EVM(Ethereum Virtual Machine)之上的程序。以太坊智能合约的主要功能是实现各类资产管理的功能。初次接触智能合约一词的读者可以在概念上直接将智能合约理解为Java编写的程序,在Java虚拟机(类比EVM)内运行。
以太坊的一大特色是一个智能合约程序一旦部署上链后将再也无法对其代码逻辑进行修改,换句话说,一个运行中的以太坊智能合约的代码逻辑是不可更新的。
使用代理合约进行合约更新
以太坊的智能合约在部署后不可更改,使得开发者在发现安全漏洞后如何更新自己的智能合约代码成了一个问题。不过,也不是完全无法实现智能合约的代码更新,通过一个特殊的以太坊EVM调用:Delegatecall,一个以太坊程序可以间接实现智能合约代码的更新。
以太坊智能合约使用代码与存储耦合的编程模型,换句话说,一个合约的代码逻辑和合约的数据存储都是存放在同一个地址上的,一个合约只能够通过自己地址上存储的代码逻辑去修改自己地址上的数据存储。
但情况并不总是如此。以太坊智能合约在相互调用时除了最常用最普通的Call调用之外,还有一种特殊的调用方式:Delegatecall,实现了智能合约代码和智能合约存储的分离,也就是说,用别人的代码来修改自己的数据存储。
1.合约间普通CALL,如下左图所示:A合约CALL调用了B合约,B合约的程序逻辑运行在B合约地址的数据存储中,所读取与修改的数据均为属于B地址的数据。
2.合约间委托调用DELEGATECALL,如下右图所示:A合约委托调用DELEGATECALL调用了B合约,运行的程序代码仍为B的代码,但是与CALL根本性的不同是所有的代码都是运行在A的上下文中的,使用DELEGATECALL时B的代码修改的是调用者A的数据存储状态。
可以看到,使用Delegatecall可以运行B的合约代码而修改A的合约存储,利用这一特性能够自然地实现合约的升级。此时,合约A为代理合约(Proxy),合约B为实现合约(Implementation),每次合约调用,都由代理合约A将实际的调用参数使用Delegatecall传递给合约A的数据存储中所指定的实现合约B,由B的合约代码来修改合约A的数据存储。当实现合约B的代码发现了严重漏洞需要升级时,将合约A的数据存储中所指定的实现合约从B更改为新的B‘,这样就实现了合约的升级,原现的合约B虽然仍旧在链上保存着,但是实际上已经被废弃,代理合约不会再将调用转发到B合约上。而原本B运行所需要的数据本来就都存储在A中,可以无缝地被新合约B’继续继承使用。
以上就是实现智能合约代码升级的基本原理,目前以太坊社区有两种主流的通过代理合约实现智能合约升级的标准:Transparent Proxy Pattern (TPP)和Universal Upgradeable Proxy Standard (UUPS)。
1.透明代理模式(TPP)中代理合约A内会实现合约升级相关的逻辑,缺点是开销大、无法调用代理合约与实现合约中同名的函数。
2.通用可升级代理标准 (UUPS) 中代理合约A被EIP-1822标准所统一,不实现任何逻辑,仅仅是将所有调用DELEGATECALL至目标合约,更新的逻辑也实现在目标合约中。
本文一开始所提到的Wormhole协议使用的是第二种UUPS模式。
UUPS代理合约模式
UUPS模式中代理合约A不实现任何逻辑,所有的合约都在实现合约B中由开发者自行实现,最主要的功能包括:合约初始化逻辑与合约更新逻辑。
contract Implementation { // 实际合约 B
address public upgrader;
// 初始化逻辑:设置管理员账号
function initialize() external onlyonce {
upgrader = msg.sender;
}
// 更新逻辑
function upgradeToAndCall(address newImplementation) external {
authorizeUpgrade(); // 鉴权
setImplementation(newImplementation); // 设置新的实现合约的地址
newImplementation.delegatecall('..'); // 调用新的实现合约的初始化函数
}
}
需要更新实际合约B时,由管理员预先部署好更新后的合约B‘,使用新合约B‘的地址作为参数调用upgradeToAndCall函数,该函数会将代理合约A中记录实现合约的存储从B修改为B’,随后便会使用delegatecall调用新合约B‘的初始化函数initialize完成初始化。在initialize中会更新A合约存储中的管理员账号。
UUPS模式使用时的风险
让我们一起来重点关注一下负责鉴权的upgrader变量在代理合约A和实现合约B中的作用:因为可更新的智能合约将原本在一个合约中完成的逻辑拆分到了两个合约中代理合约A和实际合约B,所以这两个合约都分别在自己的存储中维护了自己的upgrader存储变量。
当管理员通过调用upgradeToAndCall这一步骤完成更新之后,由于delegatecall的特性,代理合约A中的upgrader变量被顺利地更新了,但是实现合约B‘中的upgrader并没有被更新。虽然B’的存储在UUPS模式中不是实际的存储,但是B‘除了是代理合约A的实现合约之外,同时也是一个可以公开访问的普通合约。
实际上,在使用UUPS模式更新合约时,对于upgrader存储变量有两个分开的重要步骤:
1.将A的存储中的upgrader更新为msg.sender(upgradeToAndCall中已经实现)
2.将B’的存储中的upgrader更新为msg.sender(管理员需要的额外步骤)
管理员不仅需要通过调用原实现合约B中的upgradeToAndCall函数通过新实现合约B‘中的initialize函数更改A存储中的upgrader变量(第一步),同时也需要额外在外部独立调用一次initialize函数更改B’存储中的upgrader变量(第二步)。
在缺少第2步的情况下,相当于是代理合约A被正确的初始化了,用户无法通过代理合约A进行任何恶意的行为,但是B‘没有做任何的初始化,用户仍旧可以直接调用B’的初始化函数initialize,将B‘的存储中的upgrader更新为自己,通过控制B’的升级行为去调用自毁操作实现将B‘合约销毁的操作,使得A合约所指向的实现合约B’消失了,代理A合约所剩下的数据存储也将没有任何用处。
Wormhole的实际漏洞情况
Wormhole项目源码:
https://github.com/certusone/wormhole
Wormhole负责更新与鉴权的具体逻辑与上文所描述的思路来说稍复杂。其负责实现UUPS标准upgradeToAndCall函数实际名称为submitContractUpgrade,并且鉴权时使用了parseVM等与自定义虚拟机相关的操作:
abstract contract Governance .. {
function submitContractUpgrade(bytes memory _vm) public {
Structs.VM memory vm = parseVM(_vm);
...
setGovernanceActionConsumed(vm.hash);
upgradeImplementation(upgrade.newContract); // 设置新的实现合约的地址
}
}
wormhole/ethereum/contracts/Governance.sol
在实现合约中,initialize负责对管理员变量进行初始化:
contract Implementation is Governance {
function initialize(..., bytes32 governanceContract) ... {
...
setGovernanceContract(governanceContract); // 设置upgrader
}
}
wormhole/ethereum/contracts/Implementation.sol
Wormhole在上一次调用submitContractUpgrade( )更新在区块高度13818843(2021年12月16日),之后实际合约B‘始终处于未初始化的状态。
攻击者可以未授权调用实际合约B’的初始化函数initialize( )获取B’合约管理员权限,随后凭借所获得的管理员权限恶意地调用更新函数submitContractUpgrade( ),该更新函数中的delegatecall允许执行攻击者指定的任意代码,其中危害最大的是执行selfdestruct让实际合约B’自毁,使得Wormhole项目中的资产被冻结。
Wormhole项目方在高度14269474(2022年2月24日)调用了初始化函数后修复了该问题。
修复交易:
https://etherscan.io/tx/0x9acb2b580aba4f5be75366255800df5f62ede576619cb5ce638cedc61273a50f
使用UUPS代理模式时有需要特别注意的关键步骤,在没有完成必要的初始化的情况下,黑客能够通过初始化实现合约获取管理员权限,并恶意地并调用更新函数销毁实现合约,导致代理合约内的资产被永久锁定。
Side note. 你也想尝试独立发现这个千万美元悬赏的天价漏洞?以太坊安全练习平台Ethernaut提供了这一漏洞模式的练习环境,快来自己试试手吧!
总结
跨链桥因其自身的中心化特性而在去中心化的Web3世界中引入了中心化风险,其安全性很大程度上取决于跨链桥项目方自身。所以说,跨链桥的开发者,对跨链桥安全性起了决定性的作用。
参考链接
1.Wormhole Uninitialized Proxy Bugfix Review:
https://medium.com/immunefi/wormhole-uninitialized-proxy-bugfix-review-90250c41a43a
2.Wormhole project:
https://solana.com/zh/wormhole
3.以太坊:
https://zh.m.wikipedia.org/zh-hans/以太坊
4.Motorbike – ethernaut:
https://ethernaut.openzeppelin.com/level/0x58Ab506795EC0D3bFAE4448122afa4cDE51cfdd2
5.国家区块链漏洞库《区块链漏洞定级细则》:
https://bc.cnvd.org.cn/focus_info?num=e66b7041a4aa3a54e362b4accb076111
6.immunefi-team/wormhole-uninitialized:
https://github.com/immunefi-team/wormhole-uninitialized