code4rena-2024-08-chakra-h07
[H-07] Anyone can manipulate user nonce (nonce_manager) in settlement contract
Summary
한 체인에 nonce를 관리하는 두 컨트랙트의 nonce의 싱크가 맞지 않게 할 수 있다. 이로 인해 서명을 이용하는 기능이 마비된다.
Keyword
access control, nonce, signature, dos
Vulnerability
- cairo/handler/src/handler_erc20.cairo#L183
- cairo/handler/src/settlement.cairo#L289
- cairo/handler/src/settlement.cairo#L415
- solidity/settlement/contracts/ChakraSettlement.sol#L117-L118
- solidity/handler/contracts/ChakraSettlementHandler.sol#L135
한 체인에 nonce를 관리하는 두 컨트랙트가 있고, 이들은 싱크가 되어야 한다. 그런데 그중 한 쪽을 임의로 업데이트시켜 nonce의 싱크가 맞지 않게 할 수 있다. 이로 인해 서명을 이용하는 기능이 마비된다. 동일한 취약점이 Solidity와 Cairo 버전의 코드에 있었다.
Solidity에서 취약점
유저는 일반적으로 ChakraSettlementHandler.cross_chain_erc20_settlement 를 호출하여 크로스체인 토큰 이동을 요청한다. 이 함수의 마지막에 ChakraSettlement.send_cross_chain_msg 를 호출하여 Validator가 인식 가능한 이벤트를 발생시킨다. 그리고 이때 txid 를 계산하기 위해 nonce를 이용하며, nonce는 ChakraSettlementHandler 와 ChakraSettlement 가 각자의 스토리지 변수로 관리한다.
ChakraSettlementHandler.cross_chain_erc20_settlement 와 ChakraSettlement.send_cross_chain_msg는 호출될 때마다 해당 유저의 nonce를 1씩 증가하므로 일반적으로는 ChakraSettlementHandler 와 ChakraSettlement 의 nonce가 동일하게 유지된다. 하지만 ChakraSettlement.send_cross_chain_msg 는 누구나 호출할 수 있으며 from 파라미터도 마음대로 설정할 수 있다. 즉, 공격자가 유저의 ChakraSettlement.nonce를 증가시켜 ChakraSettlementHandler.nonce 와 불일치하게 만들 수 있다.
function send_cross_chain_msg(
string memory to_chain,
@> address from_address,
uint256 to_handler,
PayloadType payload_type,
bytes calldata payload
@> ) external {
@> nonce_manager[from_address] += 1;
address from_handler = msg.sender;
uint256 txid = uint256(
keccak256(
abi.encodePacked(
contract_chain_name, // from chain
to_chain,
from_address, // msg.sender address
from_handler, // settlement handler address
to_handler,
@> nonce_manager[from_address]
)
)
);
create_cross_txs[txid] = CreatedCrossChainTx(
txid,
contract_chain_name,
to_chain,
from_address,
from_handler,
to_handler,
payload,
CrossChainMsgStatus.Pending
);
emit CrossChainMsg(
txid,
from_address,
contract_chain_name,
to_chain,
from_handler,
to_handler,
payload_type,
payload
);
}nonce가 불일치하면 동일한 요청에 대한 txid 가 ChakraSettlementHandler 와 ChakraSettlement 에서 다르게 계산된다. 이는 목적지 체인에서 토큰을 수령 또는 실패한 뒤 source 체인의 ChakraSettlement.receive_cross_chain_callback 을 호출하여 결과를 등록할 때 문제를 일으킨다.
동일한 메시지에 대한 txid 가 source 체인의 ChakraSettlementHandler 에서는 0x01, ChakraSettlement 에서는 0x02로 계산된다고 하자. CrossChainMsg 이벤트에 쓰이는 txid 는 0x02 의 것이고, 따라서 목적지 체인에서 사용되는 txid 역시 0x02 이다. 목적지 체인에서 메시지를 성공 또는 실패하면 source 체인의 ChakraSettlement .receive_cross_chain_callback 를 호출하며 이때 사용되는 txid 는 0x02 이다.
ChakraSettlement.receive_cross_chain_callback 에서는 ChakraSettlementHandler.receive_cross_chain_callback 를 호출하는데, ChakraSettlementHandler 에는 0x02 인 txid가 등록된 적이 없다. 따라서 트랜잭션은 revert 된다.
function receive_cross_chain_callback(
uint256 txid,
string memory from_chain,
uint256 from_handler,
CrossChainMsgStatus status,
uint8 /* sign_type */, // validators signature type / multisig or bls sr25519
bytes calldata /* signatures */
) external onlySettlement returns (bool) {
// from_handler need in whitelist
if (is_valid_handler(from_chain, from_handler) == false) {
return false;
}
require(
@> create_cross_txs[txid].status == CrossChainTxStatus.Pending,
"invalid CrossChainTxStatus"
);
if (status == CrossChainMsgStatus.Success) {
if (mode == SettlementMode.MintBurn) {
_erc20_burn(address(this), create_cross_txs[txid].amount);
}
create_cross_txs[txid].status = CrossChainTxStatus.Settled;
}
if (status == CrossChainMsgStatus.Failed) {
create_cross_txs[txid].status = CrossChainTxStatus.Failed;
}
return true;
}만약 Handler 가 MintBurn 모드이고, 목적지 체인에서 메시지 전달에 성공했다면 유저가 맡겼던 토큰을 burn 해야 한다. 하지만 receive_cross_chain_callback 가 실패하므로 토큰을 소각할 수 없다. 공격자가 다수의 유저에게 공격하여 nonce를 불일치시키면 토큰이 민팅만 되고 소각되지 않아 언젠가 토큰의 totalSupply 가 overflow 되어 더이상 민팅할 수 없게 되고, 이 체인은 메시지를 더이상 받을 수 없게 된다.
만약 목적지 체인에서 메시지 전달에 실패했다면 CrossChainTxStatus.Failed 상태를 저장한 뒤 메시지를 보낼 때 맡겼던 토큰을 돌려주는 등의 처리를 할 수 있다. 하지만 receive_cross_chain_callback 가 실패하므로 CrossChainTxStatus.Failed 상태를 기록할 수 없고 후처리를 할 수 없게 된다.
Cairo에서 취약점
Handler와 Settlement 컨트랙트에서 txid를 각자 계산한다. txid(cross_chain_settlement_id)를 계산할 때는 nonce 값이 사용되는데, 이 값이 다르다다면 동일한 메시지임에도 txid가 다르게 계산된다. Handler 와 Settlement 는 각자 nonce 값을 msg_count 와 tx_count 변수에 따로 저장한다. 따라서 여러 핸들러가 Settlement에 연동되거나 공격자가 Settlement.send_cross_chain_msg 를 호출하면 Settlement의 nonce 가 더 높아져 동일한 메시지의 txid가 서로 다르게 계산되게 된다.
fn cross_chain_erc20_settlement(ref self: ContractState, to_chain: felt252, to_handler: u256, to_token: u256, to: u256, amount: u256) -> felt252{
...
@> let tx_id = LegacyHash::hash(get_tx_info().unbox().transaction_hash, self.msg_count.read());
...
self.msg_count.write(self.msg_count.read()+1);
...
} fn send_cross_chain_msg(
ref self: ContractState, to_chain: felt252, to_handler: u256, payload_type :u8,payload: Array<u8>,
) -> felt252 {
let from_handler = get_caller_address();
let from_chain = self.chain_name.read();
@> let cross_chain_settlement_id = LegacyHash::hash(get_tx_info().unbox().transaction_hash, self.tx_count.read());
self.created_tx.write(cross_chain_settlement_id, CreatedTx{
tx_id:cross_chain_settlement_id,
tx_status: CrossChainMsgStatus::PENDING,
from_chain: from_chain,
to_chain: to_chain,
from_handler: from_handler,
to_handler: to_handler
});
self
.emit(
CrossChainMsg {
@> cross_chain_settlement_id: cross_chain_settlement_id,
from_address: get_tx_info().unbox().account_contract_address,
from_chain: from_chain,
to_chain: to_chain,
from_handler: from_handler,
to_handler: to_handler,
payload_type: payload_type,
payload: payload
}
);
@> self.tx_count.write(self.tx_count.read()+1);
return cross_chain_settlement_id;
}콜백 호출 시 txid 를 통해 메시지를 조회하고 성공 또는 실패 처리를 한다. Settlement에서 계산해 사용했던 txid 는 Handler 의 txid와 다르므로 callback은 정상적으로 처리될 수 없다.
다음은 Handler.receive_cross_chain_callback 함수이다. 이 함수에서는 cross_chain_msg_id 가 Pending 상태임을 확인하지 않으므로 등록된 적 없는 txid 를 사용하여도 revert 되지는 않는다. 하지만 잘못된 txid에 데이터를 덮어쓰고 있으며, MintBurn 모드인 경우 callback에서 토큰을 소각하지 못하므로(created_tx.amount 가 0) 언젠가 totalSupply 가 오버플로우되어 더이상 메시지를 받을 수 없게 될 것이다.
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');
let erc20 = IERC20MintDispatcher{contract_address: self.token_address.read()};
if self.mode.read() == SettlementMode::MintBurn{
@> erc20.burn_from(get_contract_address(), self.created_tx.read(cross_chain_msg_id).amount);
}
let created_tx = self.created_tx.read(cross_chain_msg_id);
@> self.created_tx.write(cross_chain_msg_id, CreatedCrossChainTx{
tx_id: created_tx.tx_id,
from_chain: created_tx.from_chain,
to_chain: created_tx.to_chain,
from:created_tx.from,
to:created_tx.to,
from_token: created_tx.from_token,
to_token: created_tx.to_token,
amount: created_tx.amount,
tx_status: CrossChainTxStatus::SETTLED
});
return true;
}Impact
정상적인 서명이 처리될 수 없도록 막아 receive_cross_chain_callback 가 정상 작동 할 수 없다.
Mitigation
이 버그는 공격자가 의도적으로 ChakraSettlement.send_cross_chain_msg 를 호출하여 공격했을 때 뿐만 아니라 다수의 핸들러가 Settlement 컨트랙트에 연동되었을 때도 발생한다. 따라서 ChakraSettlement.send_cross_chain_msg 의 접근을 제한하는 것보다는 nonce를 관리하는 컨트랙트를 따로 두어 공유하여 사용하도록 하기를 권장한다.
tags: bughunting, chakra, smart contract, starknet, cairo, solidity, access control vulnerability, dos, bridge, cross chain, signature, severity high