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亿美元以太坊:这个锅谁该来扛?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值