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中:

在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漏洞分析的主要内容,如果未能解决你的问题,请参考以下文章

A. Wormhole Sort

看我如何发现雅虎邮箱APP的存储型XSS漏洞

AMA预告6亿美金!被盗金额越来越大,DeFi还安全吗?

数字货币开始反弹?别急!静看分析!

Wormhole

Wormhole资产跨链项目代码解析