code4rena-2024-08-chakra-h06
[H-06] Forcing Starknet handlers to be whitelisted on the same chain allows exploit of BurnUnlock mode to drain handler funds
Summary
Starknet 에서는 메시지가 허용된 핸들러에게서 온 것인지, 처리할 핸들러가 허용된 핸들러인 지 확인한다. 그런데 이들을 동일한 변수 support_handler에 저장한다. support_handler에는 Starknet의 핸들러는 항상 등록될 것이므로 Starknet → Starknet 으로 메시지 전송을 요청했을 때에도 support_handler 확인을 통과할 수 있다. 따라서 메시지를 반복적으로 보내 핸들러에 예치된 ERC20을 모두 꺼내가 정상적인 유저가 사용할 수 없도록 막는다.
Keyword
bridge, cross chain, dos
Vulnerability
Cairo 컨트랙트에서 수신한 메시지를 처리하거나 송신했던 메시지의 처리 여부를 callback으로 받을 때, 핸들러가 허용 리스트에 등록된 것인지를 확인한다. from 체인과 to 체인의 핸들러 모두 확인한다.
fn receive_cross_chain_msg(ref self: ContractState, cross_chain_msg_id: u256, from_chain: felt252, to_chain: felt252,
from_handler: u256, to_handler: ContractAddress, payload: Array<u8>) -> bool{
assert(to_handler == get_contract_address(),'error to_handler');
assert(self.settlement_address.read() == get_caller_address(), 'not settlement');
@> assert(self.support_handler.read((from_chain, from_handler)) &&
@> self.support_handler.read((to_chain, contract_address_to_u256(to_handler))), 'not support handler');
...
}
fn receive_cross_chain_callback(ref self: ContractState, cross_chain_msg_id: felt252, from_chain: felt252, to_chain: felt252,
from_handler: u256, to_handler: ContractAddress, cross_chain_msg_status: u8) -> bool{
assert(to_handler == get_contract_address(),'error to_handler');
assert(self.settlement_address.read() == get_caller_address(), 'not settlement');
@> assert(self.support_handler.read((from_chain, from_handler)) &&
@> self.support_handler.read((to_chain, contract_address_to_u256(to_handler))), 'not support handler');
...
}Starknet의 핸들러는 support_handler에 등록되어야 한다. 상대 체인의 핸들러 역시 support_handler에 등록된다. 따라서 공격자는 동일한 체인(Starknet에서 Starknet)으로 크로스 체인 메시지를 전송할 수 있다.
공격자는 동일 체인 크로스체인 메시지로 BurnUnlock 모드를 악용할 수 있다. BurnUnlock 모드에서 크로스체인 메시지가 전송되면 사용자의 토큰이 소각되며, 메시지가 수신되면 동일한 양의 토큰이 잠금 해제된다. 이로 인해 공격자가 반복적으로 Starknet 사이의 크로스체인 메시지를 전송하여 토큰의 지속적인 잠금 해제를 유발할 수 있다.
fn receive_cross_chain_msg(ref self: ContractState, cross_chain_msg_id: u256, from_chain: felt252, to_chain: felt252,
from_handler: u256, to_handler: ContractAddress, payload: Array<u8>) -> bool{
// --SNIP
let erc20 = IERC20MintDispatcher{contract_address: self.token_address.read()};
let token = IERC20Dispatcher{contract_address: self.token_address.read()};
if self.mode.read() == SettlementMode::MintBurn{
erc20.mint_to(u256_to_contract_address(transfer.to), transfer.amount);
}else if self.mode.read() == SettlementMode::LockMint{
erc20.mint_to(u256_to_contract_address(transfer.to), transfer.amount);
}else if self.mode.read() == SettlementMode::BurnUnlock{
@> token.transfer(u256_to_contract_address(transfer.to), transfer.amount);
}else if self.mode.read() == SettlementMode::LockUnlock{
token.transfer(u256_to_contract_address(transfer.to), transfer.amount);
}
}Impact
공격자는 핸들러의 토큰을 소진시킬 수 있다. 핸들러에 예치된 ERC20이 고갈되면 다른 유저의 크로스체인 ERC20 거래는 실패하게 된다.
Mitigation
from과 to 체인이 서로 다른 지 확인하여 동일 체인 사이 브릿지 요청이 불가하게 한다.
Memo
기존 보고서에서 제안한 수정 사항은 문제를 고치치 못하는 것 같아 바꾸었다. 포인트는 알겠는데, 실제 크로스 체인 브릿지를 반복(A→B→다른브릿지로 A로 이동→B)해도 똑같이 핸들러의 토큰을 고갈시킬 수 있지 않나? 왜 이것은 공격으로 보지 않을까? 공격 비용이 낮다는 점? 어쨌든 동일 체인 사이 메시지 교환은 버그이긴 하다. → 소각이 된다는 점이 문제인 것 같다. 꺼냄 → 소각 → 꺼냄 → 소각 되어 고갈시키기 때문이다. 근데 애초에 BurnUnlock 모드는 메시지를 수신할 때는 토큰을 발행해야 한다. 좀 더 근본적인 취약점은 따로 있는데, 취약점을 분리해서 평가했다. 동일 체인 사이 메시지 전송이 가능한 점을 별개의 포인트로 본 것 같다.
tags: bughunting, chakra, smart contract, starknet, cairo, bridge, cross chain, severity high, dos