Wormhole漏洞分析
Posted mutourend
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Wormhole漏洞分析相关的知识,希望对你有一定的参考价值。
1. 引言
前序博客有:
Wormhole为Solana上的跨链bridge。
Wormhole中引入了Validator角色——即guardians。
Wormhole不是区块链网络,其仅依赖共识和其bridge的链的finalization。
Wormhole中没有leader角色,所有的guardians都对其监听到的on-chain event执行相同的计算,同时对Validator Action Approval (VAA)签名。若有⅔+的大多数guardian节点使用各自私钥对同一event签名,则在所有链上的Wormhole合约都将自动认为其是有效的,并触发相应的mint/burn操作。(当前采用19个guardian,需达到13+个签名。)
Wormhole bridge采用wrapped token 方式,来lock tokens in one blockchain into a smart contract。
2022年2月3日,Wormhole中价值超过3亿美金的加密资产被盗。根本原因在于:Wormhole后台没有正确验证其guardian accounts。通过创建a fake signature account,黑客在Solana链上mint了12万个WETH(价值3亿美金),然后通过一系列操作,将其中的93,750个ETH转移至以太坊的私人钱包0x629e7da20197a5429d30da36e77d06cdf796b71a中:
- https://etherscan.io/tx/0xacd309b02e4b533484d148de9ab0adf367ed4e70ed751d1ff036152dc3bc0479:由黑客直接调用以太坊wrapped ether合约的
withdraw
方法。 - https://etherscan.io/tx/0x4d5201dd4a377f20e61fb8f42e6f929ec16bcec918f0584e39241d15b254a80f:由黑客直接调用以太坊Wormhole: Token Bridge合约的
completeTransferAndUnwrapETH
方法。对应的Solana上交易为:(https://solscan.io/tx/5UaqPus91wvAzKNve6L8YAHsESomZQ7GWi37gPFyzTHcXNMZA641bb8m8txo7bS7A5cAnzKDKYyiKcQC8GgDcAuf)。 - https://etherscan.io/tx/0xd31b155e259a403ebe69831fae0ec2b4bd33dfa090c43b605a57d5c72c4fbbc7:由黑客直接调用以太坊Wormhole: Token Bridge合约的
completeTransferAndUnwrapETH
方法。
在Wormhole 里面要mint ETH 的流程是要执行 complete_wrapped -> 然后需要transfer message -> transfer message 是透过post_vaa 这个function 产生-> 透过verify_signatures 去验证签名是不是合法的-> 然后用到了solana sdk 提供的一个function load_instruction_at,也是这次漏洞发生的主因,不需要透过系统的地址就可以执行。
骇客就先试打了0.1 ETH 拿到正常verify_signatures 的参数去做伪造,反正系统不会检查,这点相当的聪明。
然而Wormhole 也在被hack 之前就准备要更新成Solana 1.9.4 版本,骇客抓准了修复漏洞之前开始攻击,应该是已经潜伏已久。
所以这件事情其实影响到的范围是所有有用到load_instruction_at 的Dapp,如果还有其他协议没有更新新版的话应该还会有其他锅会爆炸。
Solana端Wormhole合约提供的接口函数主要有:
//Solana Wormhole brdige,适于token bridge和nft bridge
solitaire!
Initialize(InitializeData) => initialize,
PostMessage(PostMessageData) => post_message,
PostVAA(PostVAAData) => post_vaa,
SetFees(SetFeesData) => set_fees,
TransferFees(TransferFeesData) => transfer_fees,
UpgradeContract(UpgradeContractData) => upgrade_contract,
UpgradeGuardianSet(UpgradeGuardianSetData) => upgrade_guardian_set,
VerifySignatures(VerifySignaturesData) => verify_signatures,
//token_bridge
solitaire!
Initialize(InitializeData) => initialize,
AttestToken(AttestTokenData) => attest_token,
CompleteNative(CompleteNativeData) => complete_native,
CompleteWrapped(CompleteWrappedData) => complete_wrapped,
TransferWrapped(TransferWrappedData) => transfer_wrapped,
TransferNative(TransferNativeData) => transfer_native,
RegisterChain(RegisterChainData) => register_chain,
CreateWrapped(CreateWrappedData) => create_wrapped,
UpgradeContract(UpgradeContractData) => upgrade_contract,
核心关键在于:黑客如何在Solana上mint出了12万个WETH?
mint 12万个WETH的交易为:https://solscan.io/tx/2zCz2GgSoSS68eNJENWrYB48dMM1zmH8SZkgYneVDv2G4gRsVfwu5rNXtK5BKFxn7fSqX9BvrBc1rdPAeBEcD6Es
在该笔交易中,会调用complete_wrapped
函数,该函数需要a valid VAA。
fn claimable_vaa(
bridge_id: Pubkey,
message_key: Pubkey,
vaa: PostVAAData,
) -> (AccountMeta, AccountMeta)
let claim_key = Claim::<'_, AccountState::Initialized >::key(
&ClaimDerivationData
emitter_address: vaa.emitter_address,
emitter_chain: vaa.emitter_chain,
sequence: vaa.sequence,
,
&bridge_id,
);
(
AccountMeta::new_readonly(message_key, false),
AccountMeta::new(claim_key, false),
)
pub fn complete_wrapped(
program_id: Pubkey,
bridge_id: Pubkey,
payer: Pubkey,
message_key: Pubkey,//GvAarWUV8khMLrTRouzBh3xSr8AeLDXxoKNJ6FgxGyg5 利用了该地址之前的有效VAA。
vaa: PostVAAData,
payload: PayloadTransfer,
to: Pubkey,
fee_recipient: Option<Pubkey>,
data: CompleteWrappedData,
) -> solitaire::Result<Instruction>
let config_key = ConfigAccount::<'_, AccountState::Uninitialized >::key(None, &program_id);
let (message_acc, claim_acc) = claimable_vaa(program_id, message_key, vaa.clone());
let endpoint = Endpoint::<'_, AccountState::Initialized >::key(
&EndpointDerivationData
emitter_chain: vaa.emitter_chain,
emitter_address: vaa.emitter_address,
,
&program_id,
);
let mint_key = WrappedMint::<'_, AccountState::Uninitialized >::key(
&WrappedDerivationData
token_chain: payload.token_chain,
token_address: payload.token_address,
,
&program_id,
);
let meta_key = WrappedTokenMeta::<'_, AccountState::Uninitialized >::key(
&WrappedMetaDerivationData mint_key ,
&program_id,
);
let mint_authority_key = MintSigner::key(None, &program_id);
Ok(Instruction
program_id,
accounts: vec![
AccountMeta::new(payer, true),
AccountMeta::new_readonly(config_key, false),
message_acc,
claim_acc,
AccountMeta::new_readonly(endpoint, false),
AccountMeta::new(to, false),
if let Some(fee_r) = fee_recipient
AccountMeta::new(fee_r, false)
else
AccountMeta::new(to, false)
,
AccountMeta::new(mint_key, false),
AccountMeta::new_readonly(meta_key, false),
AccountMeta::new_readonly(mint_authority_key, false),
// Dependencies
AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
// Program
AccountMeta::new_readonly(bridge_id, false),
AccountMeta::new_readonly(spl_token::id(), false),
],
data: (crate::instruction::Instruction::CompleteWrapped, data).try_to_vec()?,
)
但是攻击者如何获得有效的VAA呢?message_key
(一个有效的VAA account)为之前已调用Solana端bridge主合约的post_vaa
函数创建,具体见交易:
https://solscan.io/tx/2SohoVoPDSdzgsGCgKQPByKQkLAXHrYmvtE7EEqwKi3qUBTGDDJ7DcfYS7YJC2f8xwKVVa6SFUpH5MZ5xcyn1BCK
pub fn post_vaa(ctx: &ExecutionContext, accs: &mut PostVAA, vaa: PostVAAData) -> Result<()>
let msg_derivation = PostedVAADerivationData
payload_hash: accs.signature_set.hash.to_vec(),
;
accs.message
.verify_derivation(ctx.program_id, &msg_derivation)?;
accs.guardian_set
.verify_derivation(ctx.program_id, &(&vaa).into())?;
if accs.message.is_initialized()
return Ok(());
// Verify any required invariants before we process the instruction.
check_active(&accs.guardian_set, &accs.clock)?;
check_valid_sigs(&accs.guardian_set, &accs.signature_set)?;
check_integrity(&vaa, &accs.signature_set)?;
// Count the number of signatures currently present.
let signature_count: usize = accs.signature_set.signatures.iter().filter(|v| **v).count();
// Calculate how many signatures are required to reach consensus. This calculation is in
// expanded form to ease auditing.
let required_consensus_count =
let len = accs.guardian_set.keys.len();
// Fixed point number transformation with one decimal to deal with rounding.
let len = (len * 10) / 3;
// Multiplication by two to get a 2/3 quorum.
let len = len * 2;
// Division to bring number back into range.
len / 10 + 1
;
if signature_count < required_consensus_count
return Err(PostVAAConsensusFailed.into());
// Persist VAA data
accs.message.nonce = vaa.nonce;
accs.message.emitter_chain = vaa.emitter_chain;
accs.message.emitter_address = vaa.emitter_address;
accs.message.sequence = vaa.sequence;
accs.message.payload = vaa.payload;
accs.message.consistency_level = vaa.consistency_level;
accs.message.vaa_version = vaa.version;
accs.message.vaa_time = vaa.timestamp;
accs.message.vaa_signature_account = *accs.signature_set.info().key;
accs.message
.create(&msg_derivation, ctx, accs.payer.key, Exempt)?;
Ok(())
但是,攻击者如何能通过post_vaa
中的check_active,check_valid_sigs,check_integrity
等签名检查呢?攻击者需要提供相应的SignatureSet
。
攻击者在另一笔交易中调用了Solana端Wormhole bridge主合约中的verify_signatures
函数,具体交易见:
https://solscan.io/tx/25Zu1L2Q9uk998d5GMnX43t9u9eVBKvbVtgHndkc2GmUFed8Pu73LGW6hiDsmGXHykKUTLkvUdh4yXPdL3Jo4wVS
pub fn verify_signatures(
ctx: &ExecutionContext,
accs: &mut VerifySignatures,
data: VerifySignaturesData,
) -> Result<()>
.......
let current_instruction = solana_program::sysvar::instructions::load_current_index(
&accs.instruction_acc.try_borrow_mut_data()?,
);
if current_instruction == 0
return Err(InstructionAtWrongIndex.into());
// The previous ix must be a secp verification instruction
let secp_ix_index = (current_instruction - 1) as u8;
let secp_ix = solana_program::sysvar::instructions::load_instruction_at(//此处。自Solana 1.8起已deprecate,Unsafe because the sysvar accounts address is not checked, please use load_instruction_at_checked instead
secp_ix_index as usize,
&accs.instruction_acc.try_borrow_mut_data()?,
)
.map_err(|_| ProgramError::InvalidAccountData)?;
// Check that the instruction is actually for the secp program
if secp_ix.program_id != solana_program::secp256k1_program::id()
return Err(InvalidSecpInstruction.into());
........
在verify_signatures
函数中,会将guardians提供的a set of signature pack为 a SignatureSet
,但是,在该函数中并不会直接进行验签,而是将验签操作委托给secp256k1
合约。问题就出在这,solana_program::sysvar::instructions
mod意味着与Instruction sysvar(a sort of precompile on Solana)一起使用。但是,Wormhole中使用的solana_program
版本中,并未验证所使用的合约地址:
solana_program::sysvar::instructions::load_instruction_at(//此处。自Solana 1.8起已deprecate,Unsafe because the sysvar accounts address is not checked, please use load_instruction_at_checked instead
使用过期的load_instruction_at
方法,意味着,黑客可创建自己的account,存储与Instructions sysvar中相同的数据,然后在调用’verify_signatures’时将该帐户替换为Instruction sysvar,就可完全绕过签名验证。
事实上,黑客就是按如上方法操作的,在数小时前,黑客创建了相应的account,包含了a single serialized instruction corresponding to a call to the Secp256k1 contract,然后将该account传入作为Instruction sysvar,创建该account的交易见:
https://solscan.io/account/2tHS1cXX2h1KBEaadprqELJ6sV9wLoaSdX68FqsrrZRd
将该account作为Instructions sysvar参数传入调用verify_signatures
,实际可验签通过:
至此,黑客就拥有了造假的SignatureSet
,可用该假的SignatureSet
来生成有效的VAA,并触发unauthorized mint to their own account。
参考资料
[1] How $323M in crypto was stolen from a blockchain bridge called Wormhole
[2] Wormhole漏洞解析
[3] Wormhole漏洞分析
[4] 被盗的3.2亿美元以太坊:这个锅谁该来扛?
以上是关于Wormhole漏洞分析的主要内容,如果未能解决你的问题,请参考以下文章