Análisis de código | ¿Cómo resuelven las constantes codificadas de Ethereum el riesgo de "ataques de reentrada"?


Autor original: Jordan Earls

Traductor: Xu Jincheng

Descargo de responsabilidad: este artículo es solo la opinión personal del personal técnico y no representa la posición de la Fundación Qtum

 

Cuando estaba escribiendo otro artículo no relacionado, entré en contacto con los supuestos establecidos en el ecosistema Ethereum. En este artículo, hablaré sobre por qué la suposición de Ethereum es defectuosa y daré las soluciones correspondientes. Primero, necesitamos saber qué es la hipótesis de Ethereum. El contenido hipotético es enviar ETH al contrato inteligente de Ethereum. Al mismo tiempo, para evitar ataques de reentrada, el límite de gas para llamar al contrato inteligente no debe ser superior a 2300.

 

Número mágico y STATICALL

 

Cada contrato inteligente moderno que utiliza la función de transferencia para enviar dinero (si mal no recuerdo, después de Solidity 3.0), tiene una constante codificada de forma rígida: 2300. Por ejemplo, en este sencillo ejemplo:

 

 

 

Probador de contratos {

función () externo {

dirección pagadera paymentAddress = 0x5A0b54D5dc17e0AadC383d2db43B0a0D3E029c4c; PaymentAddress.transfer (5);

}

}

 

 

El bit de transferencia se traduce en código de bytes EVM de la siguiente manera:

 

 

LLAME 2300, dirección, ....

 

 

 

¿Por qué es tan importante este número? Hay razones para esta decisión.

 

Esta solía ser la forma más eficiente de prevenir un tipo de vulnerabilidad de contrato inteligente que se clasificaba como "reentrante". El concepto de reentrada es que un contrato inteligente llama a otro contrato inteligente y, finalmente (en el mismo proceso de ejecución) se vuelve a llamar al contrato inteligente original. La reentrada es la principal vulnerabilidad explotada en el infame hack de DAO. La solución propuesta en ese momento era no permitir que el contrato previniera este comportamiento cambiando el protocolo Ethereum, sino en última instancia cambiando Solidity para que el comportamiento predeterminado de enviar ETH al contrato inteligente use una cantidad muy pequeña de gas, de modo que el problema de reentrada ya no pueda ser explotado. Por supuesto, también hay un efecto secundario: este cambio permite que el contrato inteligente que recauda dinero solo registre un evento en el registro y no pueda cambiar el estado ni hacer nada más.

 

Pero recientemente Ethereum introdujo STATICCALL como una panacea para prevenir problemas de reentrada. ¿Es realmente una panacea? La respuesta es: no todos.

 

Primero, intentamos forzar a Solidity a usar STATICCALL en la función de respaldo de nuestro contrato de prueba:

 

 

 

solidez del pragma ^ 0.5.9;

 

Probador de contratos {

función () vista externa {

}

function foo () vista externa {

}

}

 

 

Luego, el compilador recompensó nuestro espíritu aventurero con el siguiente error:

 

 

browser / test.sol: 4: 5: TypeError: La función de reserva debe ser pagadera o no pagadera, pero es "ver". 
function () external view { 
^ (La parte de fuente relevante comienza aquí y se extiende a lo largo de varias líneas).

 

 

 

Además, es interesante que no exista una palabra clave clara "no pagadera" para identificar claramente una función como no pagadera. Eliminemos la palabra clave de vista de la función de respaldo y verifiquemos esta ABI:

 

 

[ 
{ 
"constante": verdadero, 
"entradas": [], 
"nombre": "foo", 
"salidas": [], 
"pagadero": falso, 
"estadoMutabilidad": "vista", 
"tipo": "función " 
}, 
{ 
" payable ": false, 
" stateMutability ":" nonpayable ", 
" type ":" fallback " 
} 
]

 

Por lo tanto, Solidity determina que la mutación de estado de una función de reserva solo puede ser no pagadera o pagadera, no "ver".

 

Pero pretendemos que Solidity no es tan malo, pero lo permitimos. Entonces puedes dejar que Solidity use STATICCALL para transferir dinero al respaldo de un contrato ¿Es todo normal? O no. El diseño de STATICCALL es bastante especial. Por supuesto, puede usar la función de respaldo en su lógica, pero la intención del diseño de STATICCALL es permitir llamadas de contrato externas sin efectos secundarios y solo devolver los datos del resultado del cálculo. La función de reserva en realidad no tiene el concepto de devolver datos, aunque esto se puede lograr si baja al nivel de ensamblado de la persona que llama y la persona que llama. Entonces, STATICCALL es inútil en este escenario, a menos que esté realizando una operación poco común. Pero si desea hacer algo inusual, ¿por qué no escribir una llamada de función regular y usar la función de reserva? Está bien, estoy divagando.

 

STATICCALL enfatiza que no hay efectos secundarios. Esto significa que no puede hacer lo siguiente:

  • Cambiar Estado

  • Llame a otro contrato que cambiará el estado

  • Crear contrato

  • Autodestruir un contrato

  • Registra eventos en el registro

  • Envíe ETH a otro contrato o cambie el saldo de un contrato

  • Recibió ETH porque un contrato era STATICCALL

 

Puede predecir que el estado no se puede cambiar, porque es desde esta perspectiva que previene directamente la aparición de errores reentrantes. Aunque, no esperaba que la grabación de eventos en el registro también estuviera prohibida. La grabación de eventos en el registro no tiene efectos secundarios reales visibles para el contrato inteligente. Una vez que se registra un evento en el registro, un contrato inteligente externo (o interno) no puede ver que el estado está registrado. Esta es una salida completamente vacía. Envió datos al vacío, pero ya no puede recibir los datos, ni siquiera observar que los datos fueron enviados. Este efecto secundario solo es visible en el mundo fuera de la cadena de bloques. Los eventos se usan generalmente para notificar a las interfaces externas que ingresan a la cadena de bloques, como decir "algo que puede ser de su interés ha sucedido aquí".

 

Por lo tanto, asumiendo que en nombre de la pureza tecnológica, STATICCALL hace cumplir estrictamente la regla de "sin efectos secundarios". Los efectos secundarios de ningún efecto secundario incluyen efectos secundarios que solo son visibles externamente. Otro efecto, por supuesto, es que ETH no se puede transferir en STATICCALL. Esto efectivamente rompió su estatus como competidor para resolver el problema de reentrada. En términos generales, al llamar a la función de reserva, cometió un error o quiso enviar ETH a un contrato. Cuando un contrato recibe ETH, generalmente registra un evento en el registro para decirle al programa externo "Oye, recibí una suma de dinero, es posible que quieras hacer algo al respecto, enviar un mensaje a ese usuario, etc. ". Cuando hay un límite mágico de 2300 gas, no puede enviar parte de ETH a otro contrato, ni puede cambiar el estado en el contrato, como actualizar la variable "saldo estimado". El único uso de STATICCALL es evitar ataques de reentrada desde la función generalizada del contrato que no desea enviar ETH.

 

Esto significa que la única forma de prevenir eficazmente los ataques de reentrada y enviar ETH y permitir la creación de eventos sigue siendo el método anterior, utilizando la constante de límite de gas mágico-2300. ¿Por qué es esto un gran problema? Esto no quiere decir que los números codificados de forma rígida se consideren malos en informática (pista: esto es realmente malo), pero que los números codificados de forma rígida hacen que algunos contratos hagan algo en el ecosistema alrededor de Ethereum. Se vuelve inoperable cuando se cambia.

 

Constantes en blockchain dinámica

 

Esta publicación de reddit [1] proporciona algunos detalles útiles, señalando que el costo mínimo del comando CALL y algunos otros comandos se agregó en una actualización de Ethereum antes (si mal no recuerdo, el costo mínimo era 100 en ese momento, y ahora es 500) . Este tipo de inquietud se seguirá aplicando en todos los ajustes futuros del precio del gas. Nick Johnson señaló que "las llamadas con un límite de gas claro son muy raras". Cuando se dictó esta sentencia, Solidity transferiría todo el gas disponible a un contrato al hacer la misma operación que transferir, dejando así la posibilidad de utilizar operaciones reentrantes para atacar. Solidity introdujo un límite de gas de 2300 después del ataque DAO, para evitar que ocurran incidentes similares. Ahora, para ser justos, al llamar a funciones de contrato externas (sin transferencia) de forma predeterminada, Solidity seguirá enviando todo el gas de forma predeterminada. Y hay innumerables advertencias en el documento sobre esta operación. La constante mágica del límite de gas de 2300 solo refuerza el problema señalado en la publicación de reddit.

 

Por ejemplo, imagine que un contrato tiene una función de reserva pagadera. Esta función utiliza algunos códigos de operación que son lo suficientemente baratos para ejecutarse dentro del límite de 2300 gas durante el despliegue. Sin embargo, para hacer frente a un ataque o un problema que no se había descubierto antes, el precio de estos códigos de operación se incrementó posteriormente significativamente. Este contrato quedará inutilizable y no será posible aceptar ETH de contratos que no hayan elevado explícitamente el límite de gas por encima de 2300 (comportamiento de advertencia de solidez). Para empeorar las cosas, el aumento explícito del límite de gas expondrá el contrato de llamada al peligro de ataques de reentrada. Por lo tanto, este contrato puede tener que abandonarse. De acuerdo con la lógica real de recibir ETH (como confiar en un contrato específico para enviarle ETH), es probable que esté bloqueado en una pared y el dinero que contiene no se puede retirar.

 

Esta suposición de límite de gas 2300 no solo es perjudicial para la compatibilidad con versiones anteriores. También perjudica las posibles innovaciones futuras dentro del protocolo Ethereum. Por ejemplo, EIP-1293 [2], la medición neta de gas de SSTORE, es una mejora de protocolo innovadora que reducirá el costo de muchas actividades de contratos inteligentes, incluido el almacenamiento. Permite que el costo de almacenamiento del gas refleje el costo real en la cadena de bloques, lo que significa que cuando se escribe una clave de almacenamiento por segunda vez en una ejecución, costará menos gas. Esto está en línea con el sentido común, porque desde la perspectiva de la cadena de bloques, la segunda modificación de estado casi no tiene costo, y la primera modificación de estado casi pagó lo suficiente por la cadena de bloques. Esta propuesta se incluyó en la bifurcación de Constantinopla, pero se eliminó en el último momento [3], porque se descubrió que llevaría una gran cantidad de contratos inteligentes existentes a riesgos peligrosos [4]. Este juicio es correcto: esta mejora reducirá el costo del almacenamiento estatal, lo que a su vez traerá peligros ocultos de ataques reentrantes, incluso si solo hay un límite de gas de 2300 muy conservador. La ironía de este incidente es que el diseño propuesto en realidad reducirá el costo del gas de la protección de reentrada de contrato inteligente, y este es también su escenario de aplicación principal.

 

Ahora, la solución propuesta para el problema de reentrada de EIP-1283 es ​​EIP-1706 [5]. Los cambios en esta propuesta se pueden resumir de la siguiente manera: La reducción de costos provocada por la medición del valor neto del gas no se ejecutará cuando el límite de gas actualmente ejecutado sea inferior a 2300. Por lo tanto, ahora esta constante mágica se ha arraigado más en el protocolo de consenso de Ethereum. Esto forzará efectivamente a cualquier lenguaje EVM futuro a utilizar un límite de gas 2300 codificado de forma rígida al llamar a contratos para evitar el peligro de ataques de reentrada.

 

Esta suposición mágica básicamente elimina la posibilidad de que el almacenamiento se vuelva más barato, sin mencionar que Ethereum solucionará los problemas de escalabilidad y cualquier otro problema en el futuro. Incluso si un día se inventa un método mágico para mover todo el almacenamiento fuera de la cadena y hacer que el almacenamiento sea básicamente gratuito, el costo real de almacenamiento del gas aún no puede ser inferior a 2300; de lo contrario, estará expuesto al peligro de ataques de reentrada.

 

Solución posible

 

He dicho mucho, pero esta es realmente una pregunta difícil, ¿verdad? Estamos hablando de blockchain y todas las tecnologías relacionadas con blockchain son difíciles. Esto también es un hecho, pero al mismo tiempo, también tiendo a estar en desacuerdo con la pureza técnica que el equipo de Ethereum siempre enfatiza para mejorar el protocolo de consenso. De hecho, creo que EIP-1706 no será aceptado por la red principal de Ethereum debido a problemas de codificación rígida, no es lo suficientemente puro. Personalmente, predigo que EIP-1283 se extenderá indefinidamente y eventualmente se agregará a Ethereum 2.0.

 

¿Cómo solucionaría el problema de la reentrada? Hay dos escenarios posibles.

 

El primero es relativamente simple: agregue otro código de operación, pero agregue otro útil. Mi propuesta es agregar este código de operación:

 

 

 

MAGICCALL SIN REENTRANCYEXPLOITS

 

 

De hecho, este es un nombre conciso que lleva mucho tiempo leerse. Pero en serio, el efecto de este código de operación en CALL es básicamente el mismo, excepto por los siguientes cambios:

 

  • Permitir SSTORE (cambio de estado), como STATICCALL

  • Todo lo demás está permitido

 

Para ser honesto, no me gusta mucho esta solución. Prefiero resolver problemas esenciales y permitir la reentrada de cualquier contrato inteligente. Idealmente, puede haber un código de operación muy simple, como este:

 

KILLMEIFREENTRANT

 

Esto detendrá la ejecución cuando el contrato actual ya esté en la pila de llamadas. Esta función solo requiere que un desarrollador sofisticado complete el trabajo durante la noche y luego una prueba de seguridad durante el día. Esto permitirá que no intervenga almacenamiento en la prevención de ataques de reentrada, y el siguiente código simple se puede usar para implementar si callstack.exists (currentAddress) y luego lanzarlo de manera muy económica. Pero, de nuevo, creo que tal código de operación no es lo suficientemente "puro" Nunca se considerará para su adopción en Ethereum.

 

Hay alternativas más puras, como exponer todas las pilas de llamadas a contratos inteligentes. Si sabe qué hay en la pila de llamadas, simplemente puede escribir una función de Solidez para iterar en la pila y verificar si su propia dirección está incluida en ella para demostrar que la ejecución existente es reentrante. Y, por supuesto, si no se espera, el contrato lanzará una excepción para evitar cualquier comportamiento no deseado o inesperado. Esto también permitirá que se agreguen otras características al contrato inteligente. Por ejemplo, imagina que organizas un crowdfunding en el que pueden participar los contratos inteligentes. Sin embargo, usó la lista negra para bloquear algunos contratos inteligentes relacionados con terroristas. Los terroristas pueden simplemente implementar un contrato inteligente "aprobado", y luego dejar que el contrato inteligente bloqueado por la lista negra llame al contrato "aprobado", y finalmente pueden llamar a su contrato de financiación colectiva. Si hay información sobre la pila de llamadas, se puede encontrar este comportamiento. Ahora en Ethereum, es completamente imposible detectar esto con la lógica de contrato inteligente en la cadena.

 

Casi todos los diseños de Ethereum para prevenir ataques de reentrada tienen riesgos de seguridad inherentes.

 

Por lo general, cuando se ejecuta el contrato, una variable se establecerá en 1, lo que indica que se está ejecutando. Cuando se complete la ejecución, esta variable se restablecerá a 0. De esta forma, si ejecuta una llamada de contrato en el proceso, si el contrato externo quiere volver a ingresar el contrato existente, el contrato verá que la variable se establece en 1, y luego abortará la ejecución. Sin embargo, ¿qué pasa si algún problema lógico hace que la variable se restablezca a 0 al final de la ejecución? Básicamente, este contrato inteligente está aislado y ya no se puede operar porque siempre piensa que está siendo atacado.

 

La reentrada es el problema número uno en el ecosistema de Ethereum y el problema más discutido. Algunos sitios web también lo enumeran como el problema de seguridad número uno que debe tenerse en cuenta al crear contratos inteligentes. Este es el culpable que provocó el ataque DAO y algunos otros ataques y anomalías. Este es también uno de los problemas más difíciles de abordar correctamente para los desarrolladores de contratos inteligentes. La mayoría de los contratos inteligentes simplemente eliminan esta posibilidad de la causa raíz. Para mí, es increíble que Ethereum no haya implementado ningún método directo para evitar la reentrada. Por el contrario, confiar en el mecanismo STATICCALL muy restringido o en la constante mágica de límite de gas 2300 parece tener una mayor prioridad. En mi opinión, esto merece una solución de primera clase, no una modificación desagradable basada en suposiciones.

 

 

 

Lectura relacionada

 

1. Publicación de Reddit : https://www.reddit.com/r/ethereum/comments/57n2ql/why_i_believe_the_eip150_hardfork_may_break/

2. EIP 1283:

https://eips.ethereum.org/EIPS/eip-1283

3. 《¿Qué está pasando con la actualización de la bifurcación dura de Ethereum Constantinople?》 :

https://hackernoon.com/what-is-going-on-with-the-ethereum-hard-fork-update-constantinople-f453af698c0c

4. 《Constantinople habilita un nuevo ataque de reentrada》 : https://medium.com/chainsecurity/constantinople-enables-new-reentrancy-attack-ace4088297d9

5. EIP-1706;

https://github.com/ethereum/EIPs/pull/1706

 

 

Enlace original: https://medium.com/@earlz/a-frustrating-assumption-keeping-ethereum-secure-462ffb1d3201

 

Supongo que te gusta

Origin blog.csdn.net/weixin_42667079/article/details/101516282
Recomendado
Clasificación