El 19 de julio del 2017, un hacker logró el segundo mayor atraco en la historia de las monedas digitales.
Alrededor de las 12:00 PST, un atacante desconocido explotó una falla crítica en la billetera de varias firmas Parity en la red Ethereum, drenando tres billeteras masivas de más de $ 31,000,000 en Ether en cuestión de minutos. Con un par de horas más, el pirata informático podría haber conseguido más de $ 180,000,000 de billeteras vulnerables.
Pero alguien los detuvo.
Después de sonar las alarmas, un grupo de benevolentes hackers de sombrero blanco de la comunidad Ethereum se organizó rápidamente. Analizaron el ataque y se dieron cuenta de que no había forma de revertir los robos, sin embargo, muchas más billeteras eran vulnerables. El tiempo era esencial, por lo que solo vieron una opción disponible: piratear las billeteras restantes antes que el atacante.
Al explotar la misma vulnerabilidad, los sombreros blancos piratearon todas las billeteras en riesgo restantes y agotaron sus cuentas, evitando efectivamente que el atacante alcance cualquiera de los $150,000,000 restantes.
Sí, lo leiste bien.
Para evitar que el pirata informático robe más bancos, los sombreros blancos escribieron un software para robar a todos los bancos restantes en el mundo. Una vez que el dinero fue robado de manera segura, comenzaron el proceso de devolver los fondos a sus respectivos titulares de cuentas. Las personas que tenían su dinero ahorrado y pasaron por esta hazaña heroica tuvieron que pasar por el proceso de recuperar sus fondos.
Es una historia extraordinaria y tiene implicaciones significativas para el mundo de las criptomonedas.
Es importante comprender que este exploit no era una vulnerabilidad en Ethereum o en Parity en sí. Más bien, era una vulnerabilidad en el código de contrato inteligente predeterminado que el cliente de Parity le da al usuario para implementar billeteras con múltiples firmas.
¿Qué pasó exactamente?
Hay tres componentes básicos de esta historia: Ethereum, contratos inteligentes y billeteras digitales.
Ethereum es una moneda digital inventada en 2013, 4 años después del lanzamiento de Bitcoin. Desde entonces, se ha convertido en la segunda moneda digital más grande del mundo por capitalización de mercado: $ 20 mil millones, en comparación con los $ 40 mil millones de Bitcoin.
Como todas las criptomonedas, Ethereum es un descendiente del protocolo de Bitcoin y mejora el diseño de Bitcoin. Pero no se deje engañar: aunque es una moneda digital como Bitcoin, Ethereum es mucho más poderoso.
Mientras que Bitcoin usa su blockchain para implementar un libro mayor de transacciones monetarias, Ethereum usa su blockchain para registrar las transiciones de estado en una gigantesca computadora distribuida. La moneda digital correspondiente de Ethereum, ether, es esencialmente un efecto secundario de alimentar esta computadora masiva.
Para decirlo de otra manera, Ethereum es literalmente una computadora que abarca todo el mundo. Cualquiera que ejecute el software Ethereum en su computadora está participando en las operaciones de esta computadora mundial, la Máquina Virtual Ethereum (EVM). Debido a que el EVM fue diseñado para ser Turing completo (ignorando los límites de gas), puede hacer casi cualquier cosa que pueda expresarse en un programa de computadora.
Los contratos inteligentes son simplemente programas de computadora que se ejecutan en el EVM. En muchos sentidos, son como contratos normales, excepto que no necesitan abogados o jueces para interpretarlos. En cambio, se compilan en bytecode y el EVM los interpreta sin ambigüedades. Con estos programas, puede (entre otras cosas) transferir mediante programación moneda digital basándose únicamente en las reglas del código del contrato.
Por supuesto, hay cosas que los contratos normales hacen que los contratos inteligentes no pueden: los contratos inteligentes no pueden interactuar fácilmente con cosas que no están en la cadena de bloques. Pero los contratos inteligentes también pueden hacer cosas que los contratos normales no pueden hacer, como hacer cumplir un conjunto de reglas completamente a través de una criptografía irrompible.
Esto nos lleva a la noción de billeteras. En el mundo de las monedas digitales, las billeteras son la forma en que almacena sus activos. Obtiene acceso a su billetera utilizando esencialmente una contraseña secreta, también conocida como su clave privada.
Existen muchos tipos diferentes de billeteras que confieren diferentes propiedades de seguridad, como los límites de retiro. Uno de los tipos más populares es la billetera multi-firma.
En una billetera con varias firmas, hay varias claves privadas que pueden desbloquear la billetera, pero solo una clave no es suficiente para desbloquearla. Si su billetera con firma múltiple tiene 3 claves, por ejemplo, puede especificar que se deben proporcionar al menos 2 de las 3 claves para desbloquearla con éxito.
Esto significa que si usted, su padre y su madre son firmantes de esta billetera, incluso si un delincuente pirateó a su madre y le robó su clave privada, aún no podrían acceder a sus fondos. Esto lleva a garantías de seguridad mucho más fuertes, por lo que las firmas múltiples son un estándar en seguridad de billetera.
Este es el tipo de billetera que atacó el hacker.
Entonces, ¿qué salió mal? ¿Rompieron las claves privadas? ¿Usaron una computadora cuántica, o algún tipo de algoritmo de factorización de vanguardia?
No, toda la criptografía fue sólida. El exploit fue casi ridículamente simple: encontraron un error introducido por el programador en el código que les permitió reinicializar la billetera, casi como restaurarla a la configuración de fábrica. Una vez que hicieron eso, fueron libres de establecerse como los nuevos propietarios y luego salir con todo.
¿Cómo pasó esto?
Lo que sigue es una explicación técnica de exactamente lo que sucedió. Si no es un desarrollador, no dude en pasar a la siguiente sección, ya que esto va a ser pesado por la programación.
Ethereum tiene un modelo de programación bastante único. En Ethereum, escribe código publicando contratos (que puede pensar como objetos), y las transacciones se ejecutan llamando a métodos en estos objetos para mutar su estado.
Para ejecutar el código en Ethereum, primero debe implementar el contrato (la implementación es en sí misma una transacción), que cuesta una pequeña cantidad de Ether. Luego debe llamar a los métodos del contrato para interactuar con él, lo que cuesta más Ether. Como puede imaginar, esto incentiva a un programador a optimizar su código, tanto para minimizar las transacciones como para minimizar los costos de cómputo.
Una forma de reducir costos es usar bibliotecas. Al hacer que su contrato llame a una biblioteca compartida que se implementó en un momento anterior, no tiene que volver a implementar ningún código compartido. En Ethereum, mantener su código DRY directamente le ahorrará dinero.
La billetera predeterminada de múltiples firmas en Parity hizo exactamente esto. Tenía una referencia a una biblioteca externa compartida que contenía lógica de inicialización de billetera. La clave pública del contrato de la biblioteca hace referencia a esta biblioteca compartida.
// FIELDSaddress constant _walletLibrary = 0xa657491c1e7f16adb39b9b60e87bbb8d93988bc3;
La biblioteca se llama en varios lugares, a través de una instrucción EVM llamada DELEGATECALL, que hace lo siguiente: para cualquier método que llame a DELEGATECALL, llamará al mismo método en el contrato al que está delegando, pero usando el contexto del contrato actual. Es esencialmente como una súper llamada, excepto sin la parte de herencia. (El equivalente en JavaScript sería OtherClass.functionName.apply (this, args)).
Aquí hay un ejemplo de esto en su billetera multi-sig: el método isOwner simplemente delega al método isOwner de la biblioteca de billetera compartida, utilizando el estado del contrato actual:
function isOwner(address _addr) constant returns (bool) {return _walletLibrary.delegatecall(msg.data);}
Todo esto es lo suficientemente inocente. La billetera multi-sig en sí contenía todas las verificaciones de permisos correctas, y estaban seguros de hacer cumplir rigurosamente la autorización en todas las acciones sensibles relacionadas con el estado de la billetera.
Pero cometieron un error crítico.
Solidity le permite definir un "método alternativo". Este es el método que se llama cuando no hay ningún método que coincida con un nombre de método dado. Lo define al no darle un nombre:
function() { // do stuff here for all unknown methods}
El equipo de Parity decidió dejar que cualquier método desconocido que enviara a Ether al contrato simplemente depositara el Ether enviado.
function() payable { // payable is just a keyword that means this method can receive/pay Ether
if (msg.value > 0) { // just being sent some cash? Deposit(msg.sender, msg.value);} else { throw; }}
Pero lo llevaron un paso más allá, y aquí fue su error crítico. A continuación, se muestra el código real que fue atacado.
function() payable { // just being sent some cash? if (msg.value > 0) Deposit(msg.sender, msg.value); else if (msg.data.length > 0) _walletLibrary.delegatecall(msg.data);}
Básicamente:
- Si el nombre del método no está definido en este contrato ...
- Y no se envía ningún éter en la transacción ...
- Y hay algunos datos en la carga útil del mensaje ...
Luego llamará exactamente al mismo método si está definido en _walletLibrary, pero en el contexto de este contrato.
Con esto, el atacante llamó a un método llamado initWallet (), que no se definió en el contrato multi-firma, pero se definió en la biblioteca de billetera compartida:
function initWallet(address[] _owners, uint _required, uint _daylimit) { initDaylimit(_daylimit); initMultiowned(_owners, _required);}
Que llama al método initMultiowned ...
function initMultiowned(address[] _owners, uint _required) { m_numOwners = _owners.length + 1; m_owners[1] = uint(msg.sender); m_ownerIndex[uint(msg.sender)] = 1; for (uint i = 0; i < _owners.length; ++i) {m_owners[2 + i] = uint(_owners[i]); m_ownerIndex[uint(_owners[i])] = 2 + i; } m_required = _required;}
¿Ves lo que acaba de pasar allí? El atacante esencialmente reinicializó el contrato delegando a través del método de la biblioteca, sobrescribiendo a los propietarios en el contrato original. Ellos y cualquier grupo de propietarios que suministren como argumentos serán los nuevos propietarios.
Dado que ahora controlan toda la billetera, pueden extraer trivialmente el resto del saldo. Y eso es precisamente lo que hicieron.
El initWallet:
https://etherscan.io/tx/0x707aabc2f24d756480330b75fb4890ef6b8a26ce0554ec80e3d8ab105e63db07
La transferencia:
https://etherscan.io/tx/0x9654a93939e98ce84f09038b9855b099da38863b3c2e0e04fd59a540de1cb1e5
https://etherscan.io/tx/0x9654a93939e98ce84f09038b9855b099da38863b3c2e0e04fd59a540de1cb1e5
Entonces, ¿cuál fue en última instancia la vulnerabilidad? Se podría argumentar que había dos. Primero, initWallet e initMultiowned en la biblioteca de la billetera no se marcaron como internos (esto es como un método privado, que evitaría esta llamada delegada), y esos métodos no verificaron que la billetera no se haya inicializado. Cualquiera de los dos controles hubiera hecho imposible este truco.
La segunda vulnerabilidad fue la delegateCall sin procesar. Puede pensar en esto como un equivalente a una declaración de evaluación sin procesar, que se ejecuta en una cadena proporcionada por el usuario. En un intento de ser sucinto, este contrato utilizaba la meta programación para representar las posibles llamadas de método a una biblioteca subyacente. El enfoque más seguro aquí sería incluir en la lista blanca métodos específicos que el usuario puede llamar.
El problema, por supuesto, es que esto es más caro en costos de gas (ya que tiene que evaluar más condicionales). Pero cuando se trata de seguridad, probablemente tengamos que superar esta preocupación al redactar contratos inteligentes que muevan grandes cantidades de dinero.
Entonces ese fue el ataque.
Fue una captura inteligente, pero una vez que lo señala, parece casi elemental. El atacante luego aprovechó esta vulnerabilidad para tres de las billeteras más grandes que pudieron encontrar, pero a juzgar por los tiempos de transacción, lo hicieron de forma completamente manual.
El grupo de sombrero blanco estaba haciendo esto a escala usando scripts, y es por eso que pudieron vencer al atacante al instante. Dado esto, es poco probable que el atacante sea muy sofisticado en cómo planeo su ataque.
Sin embargo, puede hacer la pregunta: ¿por qué no simplemente hacen retroceder este truco, como lo hicieron con el truco DAO?
Lamentablemente, eso no es realmente posible. El hackeo de DAO fue único en que cuando el atacante drenó el DAO en una etapa inicial, los fondos se congelaron durante muchos días dentro de un contrato inteligente antes de que pudieran librarse del atacante.
Esto evitó que cualquiera de los fondos robados entrara en circulación, por lo que el éter robado se aisló efectivamente. Esto le dio a la comunidad de Ethereum mucho tiempo para llevar a cabo un quórum público sobre cómo lidiar con el ataque.
En este ataque, el hacker inmediatamente robó los fondos y pudo comenzar a gastarlos. Una bifurcación dura sería poca práctica. ¿Qué paso con las personas que intercambiaron activos inocentemente en el momento del ataque? Una vez que el éter que han robado se lava y entra en circulación, son como billetes falsos que circulan en la economía: es fácil de detener cuando todo está en un maletín, pero una vez que todos los billetes falsificados están en circulación es mas difícil, no se puede volver atrás el reloj.
Entonces la transacción no se revertirá. La pérdida de $ 31 millones se mantiene. Es una lección costosa pero necesaria.
¿Qué significa este ataque para Ethereum?
Hay varias conclusiones importantes aquí.
Primero, recuerde, esto no fue un defecto en Ethereum o en los contratos inteligentes en general. Más bien, fue un error del desarrollador en un contrato particular.
Entonces, ¿quiénes fueron los desarrolladores de crackpot que escribieron esto? Deberían haberlo sabido mejor, ¿verdad?
Los desarrolladores aquí fueron una colaboración cruzada entre la fundación Ethereum (literalmente los creadores de Ethereum), el equipo central de Parity y los miembros de la comunidad de código abierto. Se sometió a una extensa revisión por pares. Este es básicamente el estándar más alto de programación que existe en el ecosistema Ethereum.
Estos desarrolladores fueron humanos. Cometieron un error. Y también lo hicieron los revisores que auditaron este código.
He leído algunos comentarios en Reddit y HackerNews que dicen: “¡Qué error tan obvio!, ¿Cómo es posible que haya sucedido esto?” (Ignorando que la vulnerabilidad “obvia” se introdujo desde hace tiempo).
Cuando veo respuestas como esta, sé que las personas que comentan no son desarrolladores profesionales. Para un desarrollador serio, la reacción es: maldición, fue un error tonto. Me alegro de no haber sido yo quien lo hizo.
Errores de este tipo se hacen rutinariamente en la programación. Todos los programas conllevan el riesgo de error del desarrollador. Tenemos que descartar la mentalidad de "si fueran más cuidadosos, esto no hubiera sucedido". En cierta escala, la precaución no es suficiente.
A medida que los programas escalan a una complejidad no trivial, debe comenzar a considerar que los programas probablemente no son correctos. Ninguna cantidad de diligencia o prueba humana es suficiente para prevenir todos los posibles errores. Incluso organizaciones como Google o la NASA cometen errores de programación, a pesar del rigor extremo que aplican a su código más crítico.
Haríamos bien en tomar una página de las prácticas de confiabilidad del sitio en compañías como Google y Airbnb. Cada vez que hay un error de producción o interrupción, hacen un análisis post mortem y lo distribuyen dentro de la empresa. En estas autopsias, siempre existe el principio de nunca culpar a las personas.
Culpar de los errores a las personas no tiene sentido, porque todos los programadores, sin importar la experiencia, tienen una probabilidad distinta de cero de cometer un error. En cambio, el propósito de una autopsia es identificar qué permitió el error en el proceso.
El problema no era que el desarrollador olvidó agregar interno a la biblioteca de billetera, o que hicieron una delegateCall sin verificar qué método se estaba llamando.
El problema es que su cadena de herramientas de programación les permitió cometer estos errores.
A medida que el ecosistema de contratos inteligentes evoluciona, tiene que evolucionar en la dirección de hacer que estos errores sean más difíciles, y eso significa hacer que los contratos sean seguros por defecto.
Esto nos lleva al siguiente punto.
La fuerza es una debilidad cuando se trata de lenguajes de programación. Cuanto más fuerte y expresivo es un lenguaje de programación, más complejo se vuelve su código. Solidity es un lenguaje muy complejo, modelado para parecerse a Java.
La complejidad es el enemigo de la seguridad. Los programas complejos son más difíciles de razonar y más difíciles de identificar. Creo que idiomas como Viper (mantenido por Vitalik Buterin) son un paso prometedor en esta dirección. Viper incluye por defecto mecanismos de seguridad básicos, como construcciones de bucle limitado, desbordamientos de enteros y evita otros errores básicos sobre los que los desarrolladores no deberían tener que razonar.
Cuanto menos le permita el lenguaje, más fácil será analizar y probar las propiedades de un contrato. La seguridad es difícil porque la única forma de probar una declaración positiva como "este contrato es seguro" es refutar todos los posibles vectores de ataque: "este contrato no puede reinicializarse", "no se puede acceder a sus fondos excepto por los propietarios", etc. Cuantos menos vectores de ataque posibles tenga que tener en cuenta, más fácil será desarrollar un contrato seguro.
Un modelo de programación más simple también permite cosas como la verificación formal y la generación automática de pruebas. Estas son áreas bajo investigación activa, pero al igual que los contratos inteligentes han incorporado criptografía de vanguardia, también deberían comenzar a incorporar la vanguardia del diseño del lenguaje de programación.
Hay una lección más grande aquí también.
La mayoría de los programadores que están entrando en este espacio, provienen de un entorno de desarrollo web, y la cadena de herramientas blockchain está diseñada para ser familiar para los desarrolladores web. Solidity ha logrado una tremenda adopción en la comunidad de desarrolladores debido a su familiaridad con otras formas de programación. En cierto modo, esto puede terminar siendo su caída.
El problema es que la programación de blockchain es fundamentalmente diferente del desarrollo web.
Dejame explicar.
Antes de la era del modelo web cliente-servidor, la mayoría de la programación se realizaba para software de consumo empaquetado o en sistemas integrados. Esto fue antes del día de las actualizaciones automáticas de software. En estos programas, el producto enviado era definitivo: lanzaste una forma de tu software cada 6 meses, y si había un error, ese error tendría que permanecer hasta la próxima versión. Debido a este ciclo de desarrollo más largo, todas las versiones de software se probaron rigurosamente en todas las circunstancias imaginables.
El desarrollo web es mucho más indulgente. Cuando inserta un código incorrecto en un servidor web, no es un gran problema si hay un error crítico: puede revertir el código o avanzar con una solución, y todo está bien porque controla el servidor. O si sucede lo peor y hay una violación activa o una fuga de datos, siempre puede detener el sangrado apagando sus servidores y desconectándose de la red.
Estos dos modelos de desarrollo son fundamentalmente diferentes. Es solo por algo como el desarrollo web que puede obtener el lema "moverse rápido y romper cosas".
La mayoría de los programadores de hoy están capacitados en el modelo de desarrollo web. Desafortunadamente, el modelo de seguridad de blockchain es más parecido al modelo anterior.
En blockchain, el código es intrínsecamente irreversible. Una vez que implementa un contrato inteligente malo, cualquier persona es libre de atacarlo tanto como sea posible y no hay forma de recuperarlo si lo logran primero. A menos que incorpore mecanismos de seguridad inteligentes en sus contratos, si hay un error o un ataque exitoso, no hay forma de apagar sus servidores y corregir el error. Estar en Ethereum, por definición, significa que todos son dueños de su servidor.
Un dicho común en ciberseguridad es "el ataque siempre es más fácil que la defensa". Blockchain multiplica drásticamente este desequilibrio. Es mucho más fácil atacar porque tiene acceso al código de cada contrato, sabe cuánto dinero hay en él y puede demorar todo el tiempo que quiera intentar atacarlo. Y una vez que su ataque tiene éxito, puede robar todo el dinero del contrato.
Imagine que está implementando software para máquinas expendedoras. Pero en lugar de un error que le permite simplemente robar dulces de una máquina, el error le permite robar dulces simultáneamente de todas las máquinas del mundo que utilizan este software. Sí, así es como funciona blockchain.
En el caso de un ataque exitoso, la defensa es extremadamente difícil. Los sombreros blancos en el truco de Parity demostraron cuán limitadas eran sus opciones de defensa: no había forma de asegurar o desmantelar los contratos, o incluso de recuperar el dinero robado; todo lo que podían hacer era piratear los contratos vulnerables restantes antes de que lo hiciera el atacante.
Esto podría parecer un futuro oscuro.
Pero no creo que esta sea una sentencia de muerte para la programación de blockchain. Más bien, confirma lo que todos ya saben: este ecosistema es joven e inmaduro. Se necesitará mucho trabajo para desarrollar la capacitación y la disciplina para tratar los contratos inteligentes de la forma en que los bancos tratan su software de cajero automático. Pero tendremos que llegar allí para que blockchain tenga éxito a largo plazo.
Esto significa que no solo los programadores maduran y reciben más capacitación. También significa desarrollar herramientas e idiomas que faciliten todo esto y nos den garantías rigurosas sobre nuestro código.
Todavía es temprano. Ethereum es un trabajo en progreso, y está cambiando rápidamente. No debe tratar a Ethereum como un banco o como un reemplazo para la infraestructura financiera. Y ciertamente no debe almacenar dinero en una billetera caliente que no se sienta cómodo si la pierde.
Pero a pesar de todo eso, sigo pensando que Ethereum va a ganar a largo plazo. Y aquí está el por qué: la comunidad de desarrolladores en Ethereum es lo que lo hace tan poderoso.
Ethereum no vivirá ni morirá por el dinero que contiene. Vivirá o morirá en función de los desarrolladores que luchan por ello.
La liga de sombreros blancos que se unió y defendió las billeteras vulnerables no lo hizo por dinero. Lo hicieron porque creen en este ecosistema. Quieren que Ethereum prospere. Quieren ver su visión del futuro hecha realidad. Y después de toda la especulación, en última instancia, estas personas serán las que conducirán a la comunidad hacia su futuro. Son fundamentalmente por qué Ethereum ganará a largo plazo, o si abandonan Ethereum, su abandono será la razón por la que pierde.
Este ataque fue importante. Sacudió a la gente. Obligo a la comunidad a analizar detenidamente las mejores prácticas de seguridad. Obligo a los desarrolladores a tratar la programación de contratos inteligentes con mucho más rigor.
Pero este ataque no ha sacudido la fuerza de los constructores que están trabajando en estas cosas. Entonces, en ese sentido, es un revés temporal.
Al final, ataques como este son buenos para que la comunidad crezca. Te llaman a tus sentidos y te obligan a mantener los ojos abiertos. Duele, y la prensa probablemente hará un desastre de la historia. Pero cada herida fortalece a la comunidad y nos acerca a comprender realmente profundamente la tecnología de blockchain, tanto sus peligros como su sorprendente potencial.
Fuente:
freecodecamp.org - A hacker stole $31M of Ether — how it happened, and what it means for Ethereum




0 Comentarios