您当前的位置: 首页 > 

mutourend

暂无认证

  • 1浏览

    0关注

    661博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Wormhole漏洞分析

mutourend 发布时间:2022-02-07 16:17:18 ,浏览量:1

1. 引言

前序博客有:

  • Wormhole资产跨链项目代码解析

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::::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,
    data: CompleteWrappedData,
) -> solitaire::Result {
    let config_key = ConfigAccount::::key(None, &program_id);
    let (message_acc, claim_acc) = claimable_vaa(program_id, message_key, vaa.clone());
    let endpoint = Endpoint::::key(
        &EndpointDerivationData {
            emitter_chain: vaa.emitter_chain,
            emitter_address: vaa.emitter_address,
        },
        &program_id,
    );
    let mint_key = WrappedMint::::key(
        &WrappedDerivationData {
            token_chain: payload.token_chain,
            token_address: payload.token_address,
        },
        &program_id,
    );
    let meta_key = WrappedTokenMeta::::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  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亿美元以太坊:这个锅谁该来扛?

关注
打赏
1664532908
查看更多评论
立即登录/注册

微信扫码登录

0.0420s