Vulnerabilidad encontrada en el contrato inteligente `vaults.sx` (ataque EOS SX Vault)

Nuestro análisis del ataque al EOS SX Vault en Mayo 2021.

EOS Costa Rica
8 min readJul 1, 2021

Una cálida y bella mañana en Costa Rica, un viernes 14 de mayo, este mensaje fue posteado en el grupo de Telegram de EOS Nation:

Estamos investigando un ataque a la bóveda. La mayoría de EOS y USDT en la bóveda han sido robados.❗️SX ataque a la bóvedaNO DEPOSITAR en bóvedaActualizaremos EOSX para evitar que las personas depositen más lo antes posible.Le proporcionaremos un análisis completo tan pronto como completamos nuestra investigación.

Aparentemente, un atacante había secado la bóveda SX, explotando una vulnerabilidad en sus contratos inteligentes. Posteriormente, se brindaron más detalles en el mismo grupo de Telegram:

EOS Nation ofrece una recompensa de 100.000 USDT al hacker de sombrero blanco que identificó el ataque de reentrada en el contrato inteligente flash.sx.La recompensa se transferirá a la cuenta de su elección una vez que los 1'180,142.5653 el EOS y el 461,796.8968 USDT se devuelvan a la cuenta flash.sx.

Parece que EOS Nation estaba tratando de convencer al atacante de que devolviera los fondos robados ofreciendo una recompensa. La cuenta EOS del atacante también fue revelada:

https://www.bloks.io/account/potghpfcmocs

Aunque ya existe un buen análisis del método de explotación utilizado por el atacante, decidimos realizar nuestro propio análisis para aprender del error y entender cómo evitarlo en el futuro. También nuestra intención es dar muchos más detalles involucrados que no fueron mencionados aquí, que nos costó entender. Por lo tanto, el lector puede obtener conocimientos más profundos al respecto. Además, este tipo de ataques importantes necesitan una visión y un análisis desde diferentes perspectivas, para poder mejorar la seguridad de los sistemas involucrados.

El contrato inteligente víctima se puede encontrar aquí: `vaults.sx`. Allí, “los usuarios pueden enviar tokens EOS a` vaults.sx` para recibir tokens SXEOS. Y adicionalmente, “los usuarios pueden enviar tokens SXEOS a` vaults.sx` para recibir EOS + cualquier interés acumulado durante el período de tiempo en que se mantiene el activo SXEOS “. El otro contrato utilizado en el ataque fue `flash.sx` donde los usuarios “piden prestada cualquier cantidad de liquidez al instante por tarifas cercanas a cero y sin garantía “.

Usamos Dfuse explorer para rastrear las actividades del atacante. Dado que EOS, como muchas de las redes de blockchain más populares, es pública, todas las actividades, como las transacciones y las llamadas a acciones de los contratos, son visibles públicamente. Esto ayuda mucho a realizar un análisis forense. A continuación, damos detalles sobre lo deducimos de los movimientos del atacante y las vulnerabilidades explotadas:

* El atacante `potghpfcmocs` deposita un determinado token, como USDT, y recibe el token alternativo SXUSDT, todo en orden hasta aquí.

# Solo un ejemplopotghpfcmocs → vaults.sx 2 USDTvaults.sx → flash.sx 2 USDTtoken.sx issue 20 SXUSDT a vaults.sxvaults.sx → potghpfcmocs 20 SXUSDT

Este conjunto de acciones se llevan a cabo dentro de `vaults.sx`, en
la función `on_transfer`. Esta función monitorea la transacciones entrantes y salientes en todos los tokens involucrados: `[[eosio :: on_notify (“ * :: transfer “)]]`. La parte de depósito de la acción se procesa en el primer condicional:

// deposit - handle issuance (ex: EOS => SXEOS)if ( deposit_itr != _vault.end() ) {

* Ahora que el atacante tiene los fondos del token alternativo, puede transfer de vuelta la mitad al contrato `vaults.sx` para obtener el token original, que en teoría habría cobrado intereses a lo largo del tiempo, pero el atacante lo hace todo de inmediato:

potghpfcmocs → vaults.sx 10 SXUSDT

Sin embargo, esta transferencia no es procesada directamente por “vaults.sx”. Primero va a las acciones de transferencia de `token.sx` que administra fondos de tipo `SX…`. Esta sección de código agrega tanto el atacante como a `vaults.sx`, a las cuentas a las que se les notificará cuando se completen las acciones de transferencia:

require_recipient( from );require_recipient( to );

La sección de código anterior devuelve a la cuenta del atacante el control del flujo de ejecución antes de que el contrato inteligente del destinatario pueda realizar cualquier actualización de estado. ¡Aquí es donde se puede explotar la vulnerabilidad! En EOSIO, las notificaciones (invocadas por `require_recipient`) envían una copia de la transacción actual a las cuentas involucradas. Las cuentas de los destinatarios pueden luego realizar algún procesamiento basado en dichas notificaciones. Eso es lo que sucede en `on_transfer` del contrato `vaults.sx`. Sin embargo, las funciones invocadas también pueden invocar acciones en otros contratos denominados acciones en línea. El orden en el que se llaman las notificaciones y las acciones en línea en EOSIO es el siguiente:

Todas las notificaciones y su procesamiento interno se ejecutan primero. Luego, si las funciones que procesan las notificaciones invocan acciones en línea, estas se ejecutan después de todas las notificaciones. En primer lugar, las acciones en profundidad, es decir, todas las acciones en línea de la primera cuenta notificada se ejecutarán primero, luego las de la segunda y así sucesivamente.

Para comprender las implicaciones de esto para la vulnerabilidad actual, primero echemos un vistazo a cómo se vería el flujo de ejecución normal dentro de `vaults.sx`. La acción `on_transfer` recibiría la notificación, pero en este caso se seguiría la parte de retiro de fondos del condicional:

// withdraw - handle retire (ex: SXEOS => EOS)} else if ( supply_itr != _vault_by_supply.end() ) {

en este apartado del condicional se ejecuta una importante operación para la explotación:

const extended_asset out = calculate_retire( id, quantity );​extended_asset sx::vaults::calculate_retire( const symbol_code id, const asset payment )...const int64_t S0 = vault.deposit.quantity.amount;const int64_t R0 = vault.supply.quantity.amount;const int64_t p  = (uint128_t(payment.amount) * S0) / R0;return { p, vault.deposit.get_extended_symbol() };

La cantidad de fondos que el usuario puede retirar es proporcional a la cantidad actual de depósito del token específico guardado en la bóveda. Después de esto, la cantidad calculada se transfiere al usuario y se resta del depósito disponible:

// update internal deposit & supply_vault_by_supply.modify( supply_itr, get_self(), [&]( auto& row ) {row.deposit -= out;row.supply.quantity -= quantity;row.last_updated = current_time_point();

Finalmente, los fondos calculados serían devueltos al usuario:

transfer( account, get_self(), out, get_self().to_string() );// send underlying assets to sendertransfer( get_self(), from, out, get_self().to_string() );

Todo genial, ¡pero un momento! Antes dijimos que si la función notificada llamaba acciones en línea (como una transferencia), tendría que esperar hasta que todas las acciones en línea del primer notificado (el atacante) hubieran terminado su ejecución. Esto significa que en este punto, `vaults.sx` ha cambiado su estado interno como si hubiera transferido el fondo al usuario, pero en realidad la transferencia aún no ha ocurrido y tampoco los fondos han sido recolectados de` flash.sx`. El atacante, el primero en ser notificado, se aprovecha de esta inconsistencia invocando la acción `borrow` del contrato `flash.sx`:

flash.sx - borrow 0.0001flash.sx → potghpfcmocs 0.0001 USDTpotghpfcmocs → flash.sx 0.0002 USDTvaults.sx - update id: USDT

Esto sucede aquí. `flash.sx` presta una cierta cantidad de fondos al usuario y verifica que el usuario haya devuelto la misma cantidad o una mayor antes de que finalice la acción de `borrow`. No hay explotación directa en esta acción y todo sale como se esperaba. Sin embargo, la parte interesante llega cuando el flujo llama a la acción `update` en el contrato` vaults.sx`:

// get balance from accountconst asset balance = eosio::token::get_balance( contract, account, sym.code() );...// update balance_vault.modify( vault, get_self(), [&]( auto& row ) {row.deposit.quantity = balance + staked;row.staked.quantity = staked;row.last_updated = current_time_point();});

El contrato `vaults.sx` está actualizando el balance de estado interno basado en el balance actual de `flash.sx` que aún no se ha reducido. Después de que finaliza la acción de `borrow`, el control todavía está en el atacante, por lo que envía otra transferencia del token SXUSDT a `vaults.sx`, esta vez sin interrumpir el flujo normal. Si echamos un vistazo de nuevo a `calculate_retire` en` vaults.sx`:

const int64_t S0 = vault.deposit.quantity.amount;const int64_t R0 = vault.supply.quantity.amount;const int64_t p  = (uint128_t(payment.amount) * S0) / R0;

Vemos que tenemos una situación con un depósito inflado (dada la actualización de estado incorrecta) y un suministro que no se ha reducido consecuentemente. A medida que avanza la ejecución:

// Just an examplepotghpfcmocs → vaults.sx 10 SXUSDTvaults.sx → potghpfcmocs 2 USDT

el atacante retira una cantidad inflada, dada la actualización incorrecta. ¡De ahí que reciba el profit del ataque!. Después de que todas las acciones en línea del contrato del atacante terminan su ejecución, el control se devuelve al segundo notificado (`vaults.sx`), y se finaliza el primer retiro:

// Just an exampletoken.sx - retire quantity: 10 SXUSDTvaults.sx → potghpfcmocs 1 USDTtoken.sx - retire quantity: 10 SXUSDT

Esta vez, dado que el retiro se calculó antes del ataque con un estado normal, la cantidad es lo que se habría retirado normalmente. Al final, el estado interno se actualiza con los valores adecuados.

Dado que este mismo ataque se puede realizar automáticamente miles de veces en cuestión de segundos, el atacante logró robar 1’180,142.5653 EOS y 461,796.8968 USDT. Algunas horas después del incidente, este mensaje se publicó en el grupo EOS Nation Telegram:

Los productores de bloques llegaron a un consenso para defender la intención del código.Aproximadamente 1.2M EOS y 462,000 USDT fueron robados en un exploit de ataque de reentrada en el contrato inteligente de préstamo flash.sx que comenzó el 14 de mayo a las 11:28 UTC.Los contratos inteligentes de vaults.sx y flash.sx eran de código abierto, MSIGed y pasaron las auditorías de seguridad, sin embargo, no se identificó el exploit de reentrada.Todos los fondos están seguros bajo el control de eosio.prods y serán devueltos a los depositantes.

Por lo tanto, si todos los fondos y las posibles cuentas creadas se rastrearon con éxito, el atacante se quedó sin nada de los fondos robados. Sin embargo, esta es una medida extrema que a los productores de bloques les gustaría evitar a toda costa y no debe llevarse a cabo en todos los incidentes de seguridad en el blockchain de EOS o en ningún otro.

Se puede observar un buen ejemplo de una buena secuencia de seguimiento del ataque aquí. Aunque esto ha sido nombrado como un exploit de ataque de reentrada, podríamos argumentar que este es un escenario diferente. En una vulnerabilidad de reingreso típica, un contrato haría una transferencia al atacante, antes de actualizar realmente el estado interno del contrato. El atacante entonces llamaría recursivamente la misma acción. Lo que tenemos aquí es en realidad lo contrario. Tal vez esa sea la razón por la que las auditorías de seguridad no pudieron detectar el error en `vaults.sx`. El problema aquí parece más relacionado con la aplicación que administra el estado interno en dos contratos diferentes que se llaman entre sí,
y el flujo de ejecución de EOSIO que puede interrumpir el contrato en medio de una actualización del estado interno entre los dos contratos involucrados.

Para evitar este tipo de error en el futuro, los desarrolladores deberían tener cuidado de que una interrupción del flujo no interfiera en medio de una actualización de estado que un atacante pueda controlar. Quizás administrar las actualizaciones de estado en un solo contrato y dentro de una sola acción atómica sería una buena solución.

Puede encontrar un buen conjunto de prácticas de desarrollo seguro de EOSIO aquí.

— Escrito por Andrés Gómez, desarrollador de ciberseguridad en EOS Costa Rica.

EOS Costa Rica opera desde 2018. Nuestro equipo desarrolla soluciones basadas en blockchain con gran atención al detalle para crear aplicaciones centradas en el usuario para uso empresarial. Además, ofrecemos recursos a la infraestructura EOSIO y promovemos el ecosistema EOSIO local. Conversemos sobre cómo usted puede implementar esta tecnología en su organización. Contáctenos.

--

--

EOS Costa Rica

EOS Block Producer candidate in the heart of the Americas. We stand for liberty and equality. Mainnet BP: costaricaeos · https://t.me/eoscr