Fuel Attackathon 中发现的五大漏洞

本文详细介绍了Fuel协议在Immunefi平台上举办的Attackathon活动中发现的五个关键漏洞,包括漏洞的严重性、影响、资产及修复情况。

从2024年6月17日到7月22日,Fuel protocol 在Immunefi平台上进行了一场为期一个月的Attackathon,欢迎顶尖的白帽人才来寻找Fuel第二层基础设施的漏洞。参与者们成功发现了各种严重程度的有趣漏洞,奖励池高达100万美元USDC

这些漏洞现在已被修复,如本公告所发布。

以下是由Immunefi的评审员Identified的前五个发现,展示了关键漏洞并增强了Fuel网络的安全性。

1. L1中的消息在回滚时被包含,允许从桥接中盗取 — 报告32965

严重性:关键

影响:直接损失资金

资源: https://github.com/FuelLabs/fuel-core/tree/v0.31.0

函数executor/src/executor.rs::update_execution_data即使在交易本身已回滚的情况下,也会将当前交易的MessageOut收据中的所有message_ids添加到执行数据中。

前5个漏洞来自于Fuel Attackathon 32965 – Medium

fn update_execution_data<Tx:Chargeable>(
&self,
tx:&Tx,
execution_data:&mutExecutionData,
receipts:Vec<Receipt>,
gas_price:Word,
reverted:bool,
state:ProgramState,
tx_id:TxId,
) -> ExecutorResult<()>{
let(used_gas, tx_fee) = self.total_fee_paid(tx,&receipts, gas_price)?;
execution_data.coinbase = execution_data
.coinbase
.checked_add(tx_fee)
.ok_or(ExecutorError::FeeOverflow)?;
execution_data.used_gas = execution_data
.used_gas
.checked_add(used_gas)
.ok_or(ExecutorError::GasOverflow)?;
execution_data
.message_ids
.extend(receipts.iter().filter_map(|r| r.message_id()));
let status = if reverted {
TransactionExecutionResult::Failed{
result:Some(state),
receipts,
total_gas: used_gas,
total_fee: tx_fee,
}
}else{
// 否则交易成功
TransactionExecutionResult::Success{
result:Some(state),
receipts,
total_gas: used_gas,
total_fee: tx_fee,
}
};

查看原始 Fuel Attackathon 32965的前5个漏洞.rs 托管 ❤ 由 GitHub

这意味着可以向L1发送消息,但将交易回滚,这会撤消桥接代币的销毁。这个伪造的提款可以反复进行,而消息仍然在L1上被转发,可能导致从桥中盗取所有代币。

攻击步骤如下:

  1. 攻击者和受害者都存入相同数量的相同代币
  2. 攻击者提款2次。第一次,他在结束时回滚他的交易,第二次则按照正常流程进行。他可以重复这个过程两次,这将导致攻击者拥有原始存入金额的双倍数量的代币。
  3. 随着受害者的提款,L2端的过程将会成功,他的代币将被销毁。由于下溢(没有剩余代币),L1上的转发过程将失败。

2. ABI超trait可在外部访问 — 报告33351

严重性:关键 影响:直接损失资金

资源: https://github.com/FuelLabs/sway/tree/v0.61.2

ABI超trait是正如文档所述,旨在使合同实现可组合。因此,它们允许定义可由实现该trait的合约继承的方法。然而,重要的是这些方法不应作为合约方法在外部可用。然而,事实并非如此,这些方法在外部是可用的。

正如我们在sway-core/src/language/ty/program.rs::validate_root中所见,ABI超trait不应向用户暴露其方法。

前5个漏洞来自于Fuel Attackathon 33351-1.rs – Medium

pubfnvalidate_root(...){
...
// ABI条目是合约类型的impl_traits中声明的所有函数
// 除了ABI超traits,它们不应将其方法暴露给
// 用户
...
}

查看原始 Fuel Attackathon 33351的前5个漏洞-1.rs 托管 ❤ 由 GitHub

以下示例演示如何renounce_ownership函数在继承Ownable traits时可在外部访问。攻击者将能够访问任何合约的超traits。

前5个漏洞来自于Fuel Attackathon 33351-2.rs – Medium

// main.sw
contract;
mod ownable;
use ownable::*;
storage{
owner: b256 = b256::zero(),
}
abi MyAbi:Ownable{
#[storage(read, write)]
fn init();
#[storage(read)]
fn owner() -> b256;
}
impl StorageHelpersforContract{
#[storage(read)]
fn get_owner() -> b256{
storage.owner.read()
}
#[storage(write)]
fnset_owner(owner:b256){
storage.owner.write(owner)
}
}
implOwnableforContract{}
implMyAbiforContract{
#[storage(read, write)]
fninit(){
Self::set_owner(ContractId::this().into());
}
#[storage(read)]
fnowner() -> b256{
returnSelf::get_owner();
}
}
abiTest{
fn renounce_ownership();
}
#[test]
fntest(){
let callerA = abi(MyAbi,CONTRACT_ID);
callerA.init();
assert(callerA.owner() == CONTRACT_ID);
let callerB = abi(Test,CONTRACT_ID);
callerB.renounce_ownership();// ! 可在外部访问
assert(callerA.owner() != CONTRACT_ID);
assert(callerA.owner() == b256::zero());
}

查看原始 Fuel Attackathon 33351的前5个漏洞-2.rs 托管 ❤ 由 GitHub

前5个漏洞来自于Fuel Attackathon 33351-3.rs – Medium

// ownable.sw
library;
pubstructOwnershipTransferred{
previous_owner:b256,
new_owner:b256,
}
pubtraitStorageHelpers{
#[storage(read)]
fnget_owner() -> b256;
#[storage(write)]
fnset_owner(owner:b256);
}
pubtraitOwnable:StorageHelpers{
}{
#[storage(read)]
fnowner() -> b256{
Self::get_owner()
}
#[storage(read)]
fnonly_owner(){
assert(msg_sender().unwrap() == Identity::Address(Address::from(Self::get_owner())));
}
#[storage(write)]
fnrenounce_ownership(){
Self::set_owner(b256::zero());
}
#[storage(read, write)]
fntransfer_ownership(new_owner:b256){
assert(new_owner != b256::zero());
let old_owner = Self::get_owner();
Self::set_owner(new_owner);
// 日志在这里不起作用
log(OwnershipTransferred{
previous_owner: old_owner,
new_owner: new_owner,
})
}
}

查看原始 Fuel Attackathon 33351的前5个漏洞-3.rs 托管 ❤ 由 GitHub

3. Sway编译器错误建模WQAM指令的寄存器使用 — 报告32269

严重性:

影响:Sway优化不正确,导致字节码错误

资源: https://github.com/FuelLabs/sway/tree/7b56ec734d4a4fda550313d448f7f20dba818b59

sway/sway-core/src/asm_lang /virtual_ops.rs::def_registers函数和sway/sway-core/src/asm_lang /virtual_ops.rs::use_registers函数用于定义给定指令将写入或读取的参数中的寄存器。这些信息在DCE 优化中使用,以决定哪些指令仅写入“死”寄存器并且可以被删除。

DCE是大多数编译器执行的一种优化过程,它消除无效代码(不会影响程序结果的代码)。这减少了程序大小并提高了程序效率,通过消除未使用的指令。然而,Sway DCE优化步骤错误地标记了一些具有被用作无效代码的结果的指令,从而将其从编译器的输出字节码中删除。

在函数sway/sway-core/src/asm_lang /virtual_ops.rs::use_registers中,我们可以看到WQAM被错误地认为会修改r1,但并未依赖其值,而WQAM指令的实际行为实际上是将r1用作内存指针。

前5个漏洞来自于Fuel Attackathon 32269.rs – Medium

/// 返回由指令self*读取的所有寄存器的列表。
pub(crate)fnuse_registers(&self) -> BTreeSet<&VirtualRegister>{
useVirtualOp::*;
(matchself{
/\ 算术/逻辑(ALU)指令 \/
...
WQAM(_, r2, r3, r4) => vec![r2, r3, r4],
...
})
.into_iter()
.collect()
}

查看原始 Fuel Attackathon 32269的前5个漏洞.rs 托管 ❤ 由 GitHub

4. 标准库中pow函数的无符号整数缺乏溢出保护 — 报告33227

严重性:

影响:任何用户资金的直接盗取,无论是静态的还是动态的,除了未申请的收益

资源: https://github.com/FuelLabs/sway/blob/ebc2ee6bf5d488e0ff693bfc8680707d66cd5392/sway-lib-std/src/math.sw

类型为u8u16u32pow函数实现,在Sway标准数学库中缺乏溢出保护。对于u64u256等更大数据类型的溢出由Fuel虚拟机(VM)处理,但VM不处理较小类型(u8u16u32)的溢出。因此,当pow函数的结果超过数据类型的最大值时,结果会环绕,而不会引发任何错误。这可能会导致合同的集成侧计算错误。

正如我们所见,问题在于以下类型u8u16u32的Power函数实现,在Sway标准库中,它没有处理溢出。

前5个漏洞来自于Fuel Attackathon 33227-1.rs – Medium

implPowerforu32{
fnpow(self,exponent:u32) -> Self{
asm(r1:self, r2: exponent, r3){
exp r3 r1 r2;
r3:Self
}
}
}
implPowerforu16{
fnpow(self,exponent:u32) -> Self{
asm(r1:self, r2: exponent, r3){
exp r3 r1 r2;
r3:Self
}
}
}
implPowerforu8{
fnpow(self,exponent:u32) -> Self{
asm(r1:self, r2: exponent, r3){
exp r3 r1 r2;
r3:Self
}
}
}

查看原始 Fuel Attackathon 33227的前5个漏洞-1.rs 托管 ❤ 由 GitHub

以下是演示数据类型u8u32的溢出漏洞的PoC。

前5个漏洞来自于Fuel Attackathon 33227-2(poc).rs – Medium

contract;
// 设置
use std::{
asset::mint_to,
constants::DEFAULT_SUB_ID
};
abiPowBug{
fn demonstrate_bug_u8(recipient:Identity, a: u8) -> AssetId;
fndemonstrate_bug_u32(a:u32) -> u32;
}
implPowBugforContract{
fndemonstrate_bug_u8(recipient:Identity,a:u8) -> AssetId{
let result_u8 = a.pow(2);
let coins_to_mint = result_u8.as_u64()*100;
if result_u8 > u8::max(){
revert(1337);
}
mint_to(recipient,DEFAULT_SUB_ID, coins_to_mint);
AssetId::new(ContractId::this(),DEFAULT_SUB_ID)
}
fndemonstrate_bug_u32(a:u32) -> u32{
let coins_to_keep = a.pow(4);
if coins_to_keep <= u32::max(){
revert(1337)
}
coins_to_keep
}
}

查看原始 Fuel Attackathon 33227的前5个漏洞-2(poc).rs 托管 ❤ 由 GitHub

以下测试用例演示了u8u32的值如何超过其各自类型的最大限制,导致溢出并产生错误的、环绕的值。

前5个漏洞来自于Fuel Attackathon 33227-3(test case).rs – Medium

#[tokio::test]
asyncfnpow_bug(){
// 设置
let(instance, _id) = get_contract_instance().await;
println!("------------u8 pow函数溢出----------------");
let asset_id = instance.methods().demonstrate_bug_u8(Identity::Address(instance.account().address().into()),20)
.append_variable_outputs(1)
.call()
.await
.unwrap()
.value;
let balance = instance.account().get_asset_balance(&asset_id).await.unwrap();
println!("余额: {:#?}", balance);
println!("------------u32 pow函数溢出----------------");
let coins_to_keep = instance.methods().demonstrate_bug_u32(1000).call().await.unwrap().value;
println!("coins_to_keep: {:#?}", coins_to_keep);
let coins_before = instance.account().get_coins(AssetId::zeroed()).await.unwrap()[0].amount;
println!("coins_before: {:#?}", coins_before);
instance.account().transfer(
&Bech32Address::from_str("fuel1glsm9rc8ysh9yjt8ljkuatalvdad3rs3wpqjznd3p7daydw2gg6sftwvvr").unwrap(),
coins_before - TryInto::<u64>::try_into(coins_to_keep).unwrap(),
AssetId::zeroed(),
TxPolicies::default()
).await.unwrap();
let coins_after = instance.account().get_coins(AssetId::zeroed()).await.unwrap()[0].amount;
println!("coins after: {:#?}", coins_after);
}

查看原始 Fuel Attackathon 33227的前5个漏洞-3(test case).rs 托管 ❤ 由 GitHub

5. 利用CCP指令以低成本的零填充代码复制清除内存 — 报告32465

严重性:

影响:修改超出设计参数的交易费用

资源: https://github.com/FuelLabs/fuel-vm/blob/c21af9c8eazfff020ddf468b51d9bcb58a0bb2295/fuel-vm/src/interpreter/blockchain.rs#L887

CCP(代码复制)指令将代码从指定的合约复制到内存,按合约的总大小收费,而不是实际复制的字节数。

通过操纵偏移和长度参数,用户可以触发一个场景,其中功能以零填充的方式清除大内存区域而不产生适当费用,实际上执行了便宜的内存清除操作。

此漏洞允许用户以显著降低的Gas费用执行昂贵的内存清理操作。通过以低价支付资源使用费用,这可能导致网络资源耗尽和潜在的拒绝服务攻击,从而危害系统的稳定性和安全性。

CCP指令调用的code_copy函数加载目标合约的字节码,根据其长度收取Gas费,然后使用不同的长度参数进行零填充数据复制。其工作原理如下:

copy_from_slice_zero_fill函数处理复制和零填充:通过选择一个超过合约字节码长度的大offsetdata切片变为空(unwrap_or_default()返回空切片)。copy_from_slice操作不执行任何操作,指定的len范围内的整个内存被填充为零:

用户可以以低成本清除大内存区域,绕过内存操作的适当费用。

以下是PoC测试用例演示CCP指令如何被利用以低成本清除内存:

*更正:报告#5错误标识为33519,已更正为32465。Gist也稍后会更新。

  • 原文链接: medium.com/immunefi/top-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ImmunefiEditor
ImmunefiEditor
江湖只有他的大名,没有他的介绍。