code4rena-2024-08-axelar-m02

[M-02] TokenBalance limit could be bypassed by deploying TokenManager

보고서

Summary

Axelar 에 연동된 한 체인이 해킹당하더라도 ITSHub에서 잔고 추적을 통해 브릿지되었던 토큰보다 많이 이동할 수 없도록 방어하는데, 이를 우회하여 토큰을 탈취할 수 있다.

Keyword

theft, bridge, access control

Vulnerability

브릿지된 목적지 체인이 해킹당하더라도 ITSHub에서 각 체인 별 잔액을 추적하여 기존에 브릿지했던 것보다 많은 토큰을 꺼내갈 수 없도록 방어한다. 따라서 특정 체인이 해킹당하더라도 다른 체인의 자금은 영향이 없도록 한다. 그런데 이 잔액 추적 로직을 우회할 수 있는 가능성이 있다.

이 취약점은 Axelar에 연동된 체인 중 하나가 해킹되었다고 가정한다. 하나의 체인이 해킹되었을 때, 다른 체인을 원본 체인으로 둔 토큰을 탈취할 수 있음을 보인다. 구체적으로 어떻게 해킹되었는지는 논의하지 않는데, 체인 자체나 거버넌스가 해킹되었다고 가정하고 논지를 전개하는 것 같다.

ITSHub는 체인별 잔고를 추적한다. 하지만 아직 연동되지 않은 체인에서의 잔고는 추적하지 않는다. 또한, 연동되지 않는 체인으로 토큰을 전송하였을 때 ITSHub에서 이를 드랍하지 않고 무시한다.

fn apply_balance_tracking(
    storage: &mut dyn Storage,
    source_chain: ChainName,
    destination_chain: ChainName,
    message: &ItsMessage,
) -> Result<(), Error> {
    match message {
        ItsMessage::InterchainTransfer {
            token_id, amount, ..
        } => {
            // Update the balance on the source chain
@>          update_token_balance(
                storage,
                token_id.clone(),
                source_chain.clone(),
                *amount,
                false,
            )
            .change_context_lazy(|| Error::BalanceUpdateFailed(source_chain, token_id.clone()))?;
 
            // Update the balance on the destination chain
@>          update_token_balance(
                storage,
                token_id.clone(),
                destination_chain.clone(),
                *amount,
                true,
            )
            .change_context_lazy(|| {
                Error::BalanceUpdateFailed(destination_chain, token_id.clone())
            })?
        }
        ...
    };
 
    Ok(())
}
 
pub fn update_token_balance(
    storage: &mut dyn Storage,
    token_id: TokenId,
    chain: ChainName,
    amount: Uint256,
    is_deposit: bool,
) -> Result<(), Error> {
    let key = TokenChainPair { token_id, chain };
 
    let token_balance = TOKEN_BALANCES.may_load(storage, key.clone())?;
 
    match token_balance {
        Some(TokenBalance::Tracked(balance)) => {
            let token_balance = if is_deposit {
                balance
                    .checked_add(amount)
                    .map_err(|_| Error::MissingConfig)?
            } else {
                balance
                    .checked_sub(amount)
                    .map_err(|_| Error::MissingConfig)?
            }
            .then(TokenBalance::Tracked);
 
            TOKEN_BALANCES.save(storage, key.clone(), &token_balance)?;
        }
@>      Some(_) | None => (), // 연동되지 않은(밸런스 초기화가 안된) 체인이면 잔고를 무시
    }
 
    Ok(())
}

또한 일단 Axelar에 연동된 체인 사이라면, A 체인에서 B 체인에게 특정 토큰과 연동된 TokenManager를 배포시키는 일이 가능하다. 메시지를 받은 B 체인은 파라미터로 받은 주소의 토큰을 tokenId와 연결하며 이 토큰으로의 브릿지를 관리하는 토큰 매니저를 배포한다. (원래 tokenId 조작은 불가하지만 체인이 해킹됨을 가정하여 임의의 tokenId 로 배포 요청할 수 있다고 하는 것 같다..)

function deployTokenManager(
    bytes32 salt,
    string calldata destinationChain,
    TokenManagerType tokenManagerType,
    bytes calldata params,
    uint256 gasValue
) external payable whenNotPaused returns (bytes32 tokenId) {
    // Custom token managers can't be deployed with native interchain token type, which is reserved for interchain tokens
    if (tokenManagerType == TokenManagerType.NATIVE_INTERCHAIN_TOKEN) revert CannotDeploy(tokenManagerType);
 
    address deployer = msg.sender;
 
    if (deployer == interchainTokenFactory) {
        deployer = TOKEN_FACTORY_DEPLOYER;
    }
 
    tokenId = interchainTokenId(deployer, salt);
 
    emit InterchainTokenIdClaimed(tokenId, deployer, salt);
 
    if (bytes(destinationChain).length == 0) {
        _deployTokenManager(tokenId, tokenManagerType, params);
    } else {
@>      _deployRemoteTokenManager(tokenId, destinationChain, gasValue, tokenManagerType, params);
    }
}

다음과 같은 상황을 생각해보자.

  • 3개의 체인 이더리움, BNB, WoofChain 이 있다고 하자.
  • TokenA는 이더리움, WoofChain 에 있는 ITS이다.
  • TokenA의 원본 체인은 이더리움이므로 이더리움에서의 잔고는 ITSHub에서 추적되지 않는다.
  • TokenA의 WoofChain 에서의 잔고는 1000이다.
  • WoofChain은 해킹되었다. 원래라면 WoofChain애서 1000 초과의 TokenA를 다른 체인으로 브릿지할 수 없다. (공격을 통해 이 방어를 깰 것이다)

이 상태에서 다음과 같이 공격하여 잔고를 조작할 수 있다. 공격 시나리오는 다음과 같다.

  • BNB에 FAKEToken을 배포한다. UINT256_MAX 만큼을 공격자에게 민팅한다.
  • WoofChain BNB 로 FAKEToken 을 관리하는 TokenManager 를 배포 요청한다.
    • WoofChain이 해킹되었음을 가정하므로, 공격자가 자유롭게 잘못된 tokenId(TokenA와 동일한 tokenId)로 설정한 TokenManager를 배포 요청할 수 있다고 가정하는 것 같다…
    • BNB 체인에는 FAKEToken이 연동된(브릿지시 소각 또는 락되는 토큰), 하지만 tokenId로 인해 TokenA로 취급되는 브릿지 경로가 만들어진다.
  • BNB에서 10억개의 FAKEToken을 소각 또는 락하며 브릿지를 요청한다. ITSHub에서 WoofChain.TokenA의 잔고가 1,000,000,1000 로 증가한다.
  • WoofChain에서 TokenA를 이더리움으로 브릿지하면 이더리움에 락되어 있던 토큰을 탈취할 수 있다.

발생할 수 있는 (연동된 체인에 발생할 수 있는)해킹 사고의 예로는 다음과 같다.

  • Bad precompiles or revert handling: Godwoken, evmos
  • Invalid transactions: frontier
  • Consensus or malicious validator: NEAR
  • Compiler bugs: vyper
  • Event spoofing
  • Contract compromise: e.g admin key, take over, bug
  • amplifier/routing architecture compromise: External Gateway, Gateway for X on Axelar, - Prover, Verifier, Relayer

Impact

ITSHub에서 잔고 추적을 우회하여 토큰을 탈취할 수 있다.

Mitigation

ITSHub는 각 ITS 의 tokenId의 원본 체인을 저장하고 원본 체인에서만 토큰 또는 TokenManager 배포를 허용해야 한다. 이렇게 하면 원격 체인의 전송에 대한 액세스가 제한됩니다.

Memo

사실 이해가 잘 안 되는데, 연동된 체인을 구체적으로 어떻게 해킹하는지에 대해서는 생략하고, 그렇다고 가정한 뒤 논지를 전개하는 게 어색해서 같다. 구체적인 PoC를 제공하지는 않았지만 하나의 체인으로 인해 다른 체인에 영향을 끼칠 수 있다는 점, 그리고 ITSHub를 통해 방어하던 것을 깰 수 있다는 점으로 취약점으로 인정된 것 같다.

스폰서 역시 Axelar에 체인을 연동하기 위해서는 거버넌스의 허용이 필요하며, 그러기 위해서는 연동될 체인이 안전한지를 확인하기 때문에 실현 가능성은 낮다고 했다.


tags: bughunting, axelar, smart contract, solidity, rust, cosmwasm, bridge, crypto theft, access control vulnerability, solo issue, severity medium