Back to Blog

~$39,5M verloren: Allbridge, Wanchain & mehr | BlockSec Wöchentlich

Code Auditing
July 30, 2026
17 min read
Key Insights

In der vergangenen Woche (2026/07/20 - 2026/07/26) werden die folgenden 8 bemerkenswerten Sicherheitsvorfälle vorgestellt, mit einem Gesamtschaden von ca. 39,5 Mio. USD.

Datum Vorfall Typ Geschätzter Schaden
2026/07/19* Allbridge Fehlerhafte Eingabevalidierung ~1,65 Mio. USD
2026/07/20 Zilliqa Fehlerhafte Nonce-Generierung ~400.000 USD
2026/07/20 Wanchain Fehlerhafte Nachrichtenkodierung ~500.000 USD
2026/07/22 42DAO Kompromittierter privater Schlüssel ~900.000 USD
2026/07/22 AFX Trade Kompromittierter privater Schlüssel ~24,15 Mio. USD
2026/07/22 B² Network Kompromittierter privater Schlüssel ~3,8 Mio. USD
2026/07/23 Verus Kompromittierter privater Schlüssel ~7,6 Mio. USD
2026/07/24 Lien Finance Fehlerhafte Validierungslogik ~542.000 USD

*Der Allbridge-Vorfall ereignete sich am 19. Juli (17:51 UTC) und wurde im Bericht der Vorwoche nicht behandelt. Er wird hier der Vollständigkeit halber aufgeführt.

  • Zilliqa wurde ausgewählt, weil eine lange unentdeckte Schwachstelle in einer Off-Chain-Wallet-Implementierung aufgedeckt wurde, die Nutzer älterer nativer ZIL-Konten seit 2019 dem Risiko einer Kompromittierung des privaten Schlüssels ausgesetzt hatte.
  • Allbridge wurde ausgewählt, weil es eine Account-Aliasing-Schwachstelle demonstriert, die durch Solanas positionsbasiertes Kontenmodell ermöglicht wird. Die Schwachstelle musste aus dem bereitgestellten Programm-Binary rekonstruiert werden, da der Quellcode nicht verfügbar war.
  • Wanchain wurde ausgewählt, weil es zeigt, wie eine Bridge ohne Kompromittierung der Kryptografie geleert werden kann. Die Signaturen waren gültig und korrekt verifiziert, dennoch ermöglichte eine nicht-injektive, trennzeichenfreie Nachrichtenkodierung, dass eine Autorisierung für 3.097,56 Token auf Cardano als 203.001.692,164714 Token umgedeutet wurde.
  • Lien Finance wurde ausgewählt, weil es veranschaulicht, wie eine Validierung, die prüft, ob Salden mit Zählern übereinstimmen, ohne die Identität und Multiplizität der abgeglichenen Elemente zu prüfen, umgangen werden kann.

Bester Sicherheitsprüfer für Web3

Design, Code und Geschäftslogik vor dem Launch validieren

Wöchentlicher Schwerpunkt: Allbridge Core

Dieser Vorfall wird hervorgehoben, weil das Account-Aliasing-Muster auf jedes Solana-Programm zutrifft, das dasselbe veränderliche Konto in zwei Instruktionsrollen akzeptiert. Der zugrunde liegende Mechanismus – zwei veränderliche Sichten auf denselben Zustand, bei dem das zweite Schreiben das erste stillschweigend überschreibt – entspricht der gleichen Struktur wie der klassische ERC-20-Selbsttransfer-Bug (transferFrom, bei dem from == to). Der Post-Mortem-Bericht des Projekts [1] liefert eine übergeordnete Zusammenfassung, beschreibt jedoch nicht den Mechanismus auf Code-Ebene. Die folgende Analyse wurde aus dem bereitgestellten Programm-Binary rekonstruiert, um diese Lücke zu schließen.

Am 19. Juli 2026 (17:51 UTC) wurde Allbridge Core, ein Cross-Chain-Bridge-Protokoll auf Solana, für ca. 1,65 Mio. USD (~1,12 Mio. USD in USDC und ~539.000 USD in USDT) ausgenutzt. Die Grundursache war, dass die Swap-Instruktion dasselbe Pool-Konto sowohl in der Send- als auch in der Receive-Rolle akzeptierte, ohne zu erzwingen, dass die beiden unterschiedlich sind. Wenn dasselbe Konto zweimal übergeben wurde, wurden interne Buchungsaktualisierungen einer Rolle stillschweigend von der anderen überschrieben, während die tatsächlichen Token-Transfers bereits abgeschlossen waren. Fünf Selbst-Swaps verzerrten den aufgezeichneten Zustand des Pools so weit, dass der Angreifer einen kleinen USDT-Einsatz in ca. 2,24 Mio. USD USDC umwandeln konnte.

Hintergrund

Allbridge Core ist eine Cross-Chain-Bridge, die Swaps über eine interne Buchhaltungseinheit namens vUSD abwickelt. Ein Swap besteht aus zwei Buchungsschritten:

Quell-Token -- swap_to_v_usd(send_pool) --> vUSD
vUSD        -- swap_from_v_usd(receive_pool) --> Ziel-Token

vUSD ist kein SPL-Token. Er existiert nur als Feld innerhalb der On-Chain-Daten jedes Pools. Jeder Pool verfolgt token_balance, v_usd_balance und reserves. Der Send-Schritt überträgt den Quell-Token vom Nutzer zum Send-Bridge-Vault, erhöht die tokenseitige Buchung des Send-Pools und berechnet den vUSD-Output. Der Receive-Schritt fügt diesen vUSD zum Receive-Pool hinzu, verringert dessen tokenseitige Buchung und überträgt den Ziel-Token vom Receive-Bridge-Vault an den Nutzer.

Solana-Programme empfangen ihre Konten als positionelles Array vom Aufrufer. Der Instruktionshandler des Programms gibt an, welche Array-Positionen welchen Rollen entsprechen (send_pool, receive_pool, send_mint usw.), aber die Laufzeit verhindert nicht, dass ein Aufrufer dieselbe Kontoadresse in zwei Positionen übergibt. Wenn das Programm Konten an zwei verschiedenen Positionen deserialisiert, erzeugt jede Deserialisierung ein separates In-Memory-Objekt. Mutationen an einem Objekt wirken sich nicht auf das andere aus. Wenn dasselbe Konto zweimal übergeben wird, starten beide Objekte mit denselben On-Chain-Daten, divergieren jedoch, sobald eines davon mutiert wird.

Wenn der Handler abgeschlossen ist, serialisiert die Exit-Logik des Programms (z. B. Anchors AccountsExit) jedes Kontoobjekt in einer festen Reihenfolge zurück in die Kontodaten. Wenn zwei Objekte auf dasselbe Konto verweisen, überschreibt die zweite Serialisierung die erste vollständig.

Schwachstellenanalyse

Der genaue Quellcode für das betroffene Allbridge Core-Deployment war nicht verfügbar. Die Analyse wurde aus der 1.770.736 Byte großen ELF-Datei rekonstruiert, die in ProgramData gespeichert ist (SHA-256: 40f776...346bb6). Der letzte Deployment-Slot (204.727.029) liegt vor dem Angriffs-Slot (433.941.722).

Das fehlerhafte Programm ist BrdgN2...ceWB. In den Swap-Instruktionen der Angriffstransaktion (Instruktionen 3 bis 7) waren die vier Send- und Receive-Kontopaare identisch:

Rollen Konto
send_mint / receive_mint Es9vMF...wNYB
send_pool / receive_pool DW4a2E...wCX
send_bridge_token / receive_bridge_token 2xY9TD...vohV
send_user_token / receive_user_token 817UdW...CVct

Der Parser enthält 16 wiederhergestellte 32-Byte-Vergleiche, die jede Rolle an ihre erwartete Mint, ihren Vault, Eigentümer, PDA, Autorität oder das Token-Programm binden. Es gibt jedoch keinen Vergleich, der send_pool.key() != receive_pool.key() erzwingt. Die beiden Pool-Rollen werden unabhängig voneinander deserialisiert, bevor der Handler ausgeführt wird.

Die bereitgestellte ELF zeigt zwei Pool-Konstruktionen, die Send- und Receive-Berechnungen sowie zwei Pool-Exits in einer festen Reihenfolge:

L54249-L54252  function_9881(accounts[5])     -> send_pool lokal
L54294-L54297  function_9881(accounts[6])     -> receive_pool lokal

L74177-L74195  function_12623(send_pool)      -> swap_to_v_usd
L74368-L74389  function_12940(receive_pool)   -> swap_from_v_usd

L55932-L55936  function_8300(send_pool)       -> vollständige Pool-Serialisierung
L55940-L55944  function_8300(receive_pool)    -> vollständige Pool-Serialisierung

Jeder function_9881-Aufruf allokiert ein separates lokales 176-Byte-Pool-Objekt und kopiert die dekodierten Felder hinein. Wenn die Kontoschlüssel gleich sind, starten beide Objekte mit denselben Daten, aber eine Mutation des Send-Objekts ändert das Receive-Objekt nicht.

Rekonstruiertes Pseudo-Rust (bereitgestelltes Verhalten, kein wiederhergestellter Quellcode) [2] [3]:

let mut send_pool = Account::<Pool>::try_from(&accounts[5])?;
let mut receive_pool = Account::<Pool>::try_from(&accounts[6])?;

require_keys_eq!(send_pool.mint, send_mint.key());
require_keys_eq!(receive_pool.mint, receive_mint.key());

// Vault-, Eigentümer-, PDA-, Autoritäts- und Token-Programm-Bindungen werden geprüft.
// FEHLT: require_keys_neq!(send_pool.key(), receive_pool.key());

token::transfer(send_user_token, send_bridge_token, amount)?;
let v_usd = swap_to_v_usd(&mut send_pool, amount)?;
let receive_amount =
    swap_from_v_usd(&mut receive_pool, v_usd, receive_amount_min)?;
token::transfer(receive_bridge_token, receive_user_token, receive_amount)?;

// AccountsExit nach Rückkehr des Handlers:
send_pool.exit(program_id)?;     // erster Schreibvorgang
receive_pool.exit(program_id)?;  // zweiter Schreibvorgang, überschreibt den ersten

function_8300 startet seinen Writer an Position 0 und serialisiert alle 131 definierten Pool-Bytes. Der erste Exit schreibt send_pool; der zweite Exit schreibt receive_pool über dieselben Bytes. Der endgültige Kontozustand ist daher der veraltete Receive-seitige Wert und keine Zusammenführung der beiden lokalen Aktualisierungen.

Die SPL-Token-Transfers sind separate CPIs. Sie aktualisieren Vault-Konten, die dem SPL-Token-Programm gehören, bevor die Pool-Exits erfolgen, sodass die zweite Pool-Serialisierung den Eingabe-Transfer nicht rückgängig machen kann. Jeder aliasierte Swap lässt die übertragenen Token im Vault, während die entsprechende sendseitige Pool-Aktualisierung verworfen wird. Die empfangsseitige Aktualisierung bleibt bestehen und verschiebt den aufgezeichneten Pool-Zustand in Richtung höherer v_usd_balance und niedrigerer tokenseitiger Buchung.

Der grundlegende Fehler liegt darin, dass das Programm nicht prüft, ob send_pool und receive_pool unterschiedliche Konten sind. Alle anderen Validierungen (Mint-Bindung, Vault-Eigentümerschaft, PDA-Ableitung) sind erfolgreich, weil beide Rollen legitim zum selben Pool gehören.

Angriffsanalyse

Die folgende Analyse basiert auf der Transaktion 3LNLaG...Y39Q.

Der Exploit wurde in einer einzigen Transaktion abgeschlossen, die einen Kamino-Flash-Borrow, sieben Allbridge-Swap-Instruktionen und eine Kamino-Rückzahlung enthielt.

  • Schritt 1: Der Angreifer lieh sich ~1,12 Mio. USDC von Kamino und tauschte sie über Allbridge gegen ~949.000 USDT (Instruktionen 1-2). Dieser erste Swap lieferte den für die folgenden Selbst-Swaps benötigten USDT und veränderte sowohl den USDC- als auch den USDT-Pool-Zustand.
  • Schritt 2: Der Angreifer führte fünf ~100.000 USDT-Selbst-Swaps durch, wobei er identische Mint-, Pool-, Vault- und Nutzer-Token-Konten für Send und Receive verwendete (Instruktionen 3-7). Da der Receive-Pool bei jedem Aufruf zuletzt serialisiert wurde, verzeichnete die persistierte Buchung die empfangsseitige Reduzierung, nicht jedoch die sendseitige Addition. Die beobachteten Ausgaben nahmen mit jeder Transaktion ab, da sich die Verzerrung aufaddierte:
Selbst-Swap Eingabe Protokollierter vUSD Preis (vUSD/USDT) USDT-Ausgabe
1 ~100K ~162K ~5,24 ~47,8K
2 ~100K ~256K ~16,6 ~25,4K
3 ~100K ~471K ~57,2 ~12,5K
4 ~100K ~918K ~190 ~5.730
5 ~100K ~1,82M ~563 ~2.360

Die fünf Aufrufe überwiesen ~500.000 USDT in den Vault und gaben ~93.800 USDT zurück. Die aufgezeichnete token_balance des Pools sank mit jedem Aufruf, während das tatsächliche Vault-Guthaben wuchs, was die Lücke vergrößerte, die der nächste Schritt ausnutzt.

  • Schritt 3: Der Angreifer lieferte nur ~3.990 USDT (Instruktion 8). Der verzerrte USDT-Pool erzeugte ~2,24 Mio. vUSD, die der USDC-Pool in ~2,24 Mio. USDC umwandelte. Die aufgeblähte vUSD-Ausgabe war möglich, weil das aufgezeichnete Token-Guthaben des Pools nach fünf Runden verworfener sendseitiger Aktualisierungen weit unter dem tatsächlichen Vault-Guthaben lag.
  • Schritt 4: Der Angreifer zahlte den ~1,12 Mio. USDC-Kamino-Flash-Loan-Kapitalbetrag und die ~11,2 USDC-Gebühr zurück (Instruktion 9). Das endgültige Angreiferguthaben betrug ~1,12 Mio. USDC und ~539.000 USDT.

Fazit

Die Grundursache dieses Vorfalls war die fehlende Validierung, dass send_pool und receive_pool auf unterschiedliche Konten verweisen. Ein einziger veränderlicher Pool wurde in zwei lokale Buchhaltungsobjekte deserialisiert, und die zweite vollständige Serialisierung überschrieb die erste, nachdem das Token-Programm die Vault-Transfers bereits abgeschlossen hatte. Dadurch wurde die aufgezeichnete Buchung des Pools von seinem tatsächlichen Vault-Guthaben getrennt. Die Konto-Aliasse, der ELF-Kontrollfluss, die Serialisierungsreihenfolge, die Transaktionsprotokolle und die Vault-Guthaben unterstützen allesamt denselben Mechanismus [1].

Für Solana-Programme sollte jede Instruktion, die denselben Kontotyp in zwei oder mehr veränderlichen Rollen akzeptiert, entweder Eindeutigkeit erzwingen (require_keys_neq!) oder Mutationen vor dem Exit zusammenführen. Das Constraint-System des Anchor-Frameworks (#[account]) erzwingt standardmäßig keine rollenübergreifende Eindeutigkeit, weshalb diese Prüfung explizit hinzugefügt werden muss. Das allgemeine Muster – zwei veränderliche Sichten auf denselben Zustand, bei dem das zweite Schreiben das erste stillschweigend überschreibt – kann überall dort auftreten, wo Programme ihre eigene Serialisierungsreihenfolge verwalten.

Referenzen

Erste Schritte mit Phalcon Explorer

Transaktionen analysieren, um kluge Entscheidungen zu treffen

Jetzt kostenlos testen

Weitere Vorfälle dieser Woche

Zilliqa Ledger Wallet

Am 20. Juli 2026 beobachtete Zilliqa On-Chain-Aktivitäten, die auf eine aktive Ausnutzung älterer nativer ZIL-Konten hindeuteten, mit bekannten Verlusten von ca. 400.000 USD. Die Grundursache war ein verzerrter Nonce im EC-Schnorr-Signierungspfad der Zilliqa Ledger-App: Der Nonce-Generierungscode kopierte nach der Modularreduktion die falschen 32 Bytes aus einem 40-Byte-Puffer, wodurch die signifikantesten 64 Bits auf null fixiert wurden. Mit mehreren öffentlichen Signaturen desselben Kontos konnte ein Angreifer den privaten Schlüssel mittels gitterbasierten Techniken wiederherstellen und das Konto leeren.

Hintergrund

Native Zilliqa-Transaktionen verwenden EC-Schnorr-Signaturen über secp256k1. Für jede Signatur muss der Unterzeichner einen frischen, vollbreiten, unvorhersehbaren ephemeren Nonce kk sampeln, wobei 0k<N0 \le k < N (die secp256k1-Kurvenordnung). Der Signierungsablauf erzeugt ein Commitment Q=compress(kG)Q = \text{compress}(kG), eine Challenge r=SHA256(QpublicKeymsg)Nr = \text{SHA256}(Q \| \text{publicKey} \| \text{msg}) \bmod N und eine Antwort s=krxNs = k - r \cdot x \bmod N, wobei xx der private Schlüssel ist. Nach der Übertragung ist (r,s)(r, s) öffentlich.

Die lineare Beziehung s=krxNs = k - rx \bmod N ist der kritische Punkt. Eine korrekte Implementierung bleibt sicher, weil jedes kk frisch und gleichmäßig verteilt ist. Wenn der Nonce verzerrt oder zu klein ist, gibt jede öffentliche Signatur Informationen über denselben privaten Schlüssel preis.

Schwachstellenanalyse

Der relevante Nonce-Generierungspfad wurde im Zilliqa Ledger-App-Commit "Bug fixes based on Ledger team review" eingeführt. Der beabsichtigte Ablauf war: 40 Bytes Zufälligkeit generieren, modulo der Kurvenordnung NN reduzieren und den resultierenden Skalar als kk verwenden. Das Generieren einer breiten Zufallszahl und das Reduzieren modulo der Kurvenordnung ist an sich kein Problem. Der Fehler trat beim Kopieren in den Nonce-Puffer auf:

unsigned char nonce[size+8];
cx_rng(nonce, size+8);
cx_math_modm(nonce, size+8, (unsigned WIDE char *) PIC(domain->n), size);
os_memcpy(T->K, nonce, size);

Nach cx_math_modm sitzt das 32-Byte-Skalarargebnis rechtsbündig innerhalb des 40-Byte-Puffers. os_memcpy(T->K, nonce, size) kopiert die ersten size (32) Bytes, wobei die führenden 8 Null-Padding-Bytes beibehalten und die letzten 8 Bytes der Entropie verworfen werden. Der generierte Nonce erfüllt daher k<2192k < 2^{192} anstatt k<N2256k < N \approx 2^{256}.

Das bedeutet, dass 64 Bits jedes Nonce auf null fixiert sind. Aus der Schnorr-Antwortgleichung ki=si+rixNk_i = s_i + r_i x \bmod N ergibt jede Signatur eine öffentliche modulare lineare Relation, bei der das Ergebnis durch 21922^{192} begrenzt ist. Dies ist eine Hidden-Number-Problem-(HNP)-Instanz. Mit ca. vier oder mehr betroffenen Signaturen desselben Schlüssels stellt ein Standard-Gitterreduktionsalgorithmus den privaten Schlüssel in Sekunden auf handelsüblicher Hardware wieder her [1].

Der Defekt war in jeder veröffentlichten Version der Zilliqa Ledger-App seit 2019 vorhanden, bis der Vorfall im Juli 2026 identifiziert wurde [2].

Für diesen Vorfall wird keine Angriffsanalyse bereitgestellt. Die Ausnutzung erfolgte vollständig Off-Chain: Der Angreifer stellte private Schlüssel aus öffentlich verfügbaren On-Chain-Signaturen mittels Gitterreduktion wieder her und unterzeichnete dann Standard-Überweisungstransaktionen. Es gibt keine mehrstufige On-Chain-Angriffsequenz zu analysieren.

Fazit

Dieser Vorfall wurde durch einen Off-Chain-Implementierungsfehler in der Signiersoftware verursacht, nicht durch einen On-Chain-Smart-Contract-Bug. Die Zilliqa Ledger-App erzeugte EC-Schnorr-Signaturen mit Nonces, die auf k<2192k < 2^{192} beschränkt waren, und gab nach mehreren nativen Transaktionen von demselben Konto genug strukturierte Informationen für die Wiederherstellung des privaten Schlüssels preis.

Die primäre Abhilfemaßnahme ist die Schlüsselablösung, nicht bloß die Aktualisierung der Ledger-App. Eine korrigierte App verhindert neue geschwächte Signaturen, kann jedoch die bereits öffentlich auf der Chain aufgezeichneten Signaturen nicht löschen. Jeder betroffene Schlüssel, der genug anfällige Signaturen erzeugt hat, muss als kompromittiert betrachtet werden [2].

Für Wallet- und Protokollteams: Behandelt die Nonce-Generierung und -Kodierung als sicherheitskritischen Code, bevorzugt nach Möglichkeit deterministische Nonce-Generierung nach einem gut geprüften Standard und stellt betroffenen Nutzern einen koordinierten Migrationspfad bereit, der das Front-Running durch Angreifer berücksichtigt, die möglicherweise bereits denselben privaten Schlüssel besitzen.

Referenzen


Wanchain Cardano Bridge

Am 20. Juli 2026 verlor die Wanchain Cardano Bridge ca. 515,2 Mio. NIGHT (~500.000 USD) aufgrund einer nicht-injektiven Nachrichtenkodierung im Cardano TreasuryCheck Plutus-Validator [1]. Der Post-Mortem-Bericht des Projekts [2] beschreibt den übergeordneten Mechanismus, liefert jedoch keine Kodierungsdetails auf Byte-Ebene oder Code; die folgende Analyse rekonstruiert die vollständige Kollision. Die Bridge-Node-Signaturen waren gültig und korrekt verifiziert, aber die trennzeichenfreie Konkatenation variabler Längenfelder ermöglichte es dem Angreifer, Bytegrenzen zwischen zwei benachbarten numerischen Feldern zu verschieben und eine Autorisierung für 3.097,56 NIGHT als Auszahlung von 203.001.692,164714 NIGHT umzudeuten.

Hintergrund

Die betroffene Komponente ist Wanchains Cardano-Cross-Chain-Treasury-System. Ein Quellkettennutzer verbrennt oder sperrt Token, Bridge-Nodes signieren eine aus den analysierten Redeemer-Feldern konstruierte Autorisierungsnachricht, und der signierte Beweis wird dem Cardano TreasuryCheck Plutus-Validator vorgelegt, um Vermögenswerte aus dem Treasury freizugeben.

Schwachstellenanalyse

Der TreasuryCheck-Validator verifiziert die Bridge-Node-Signatur über eine rohe Konkatenation von 14 analysierten Redeemer-Feldern:

hashRedeemer = sha3_256 $ mconcat
  [ toPkhPay, toPkhStk, policy, assetName
  , packInteger amount, packInteger adaAmount
  , txHash, packInteger index, packInteger mode
  , uniqueId, packInteger txType, packInteger ttl
  , packInteger outputCount, userData
  ]

Die von packInteger verwendete Ganzzahlkodierung ist variabler Länge, und die Konkatenation hat kein Längenpräfix, keinen Typtrennzeichen oder domänenseparierte typisierte Serialisierung. Unterschiedliche semantische Tupel können daher dieselbe signierte Byte-Zeichenkette erzeugen.

Bei dem repräsentativen Angriff signierten Bridge-Nodes eine normale Auszahlung:

amount    = 3097560000     -> packInteger = b8a103c0
adaAmount = 1206800        -> packInteger = 126a10
kombiniert                               = b8a103c0126a10

Der Angreifer übermittelte einen Cardano-Redeemer, der wie folgt analysiert wurde:

amount    = 203001692164714 -> packInteger = b8a103c0126a
adaAmount = 16              -> packInteger = 10
kombiniert                                 = b8a103c0126a10

Die Byte-Sequenzen sind identisch. Die Signaturprüfung war erfolgreich, und der Cardano-Vertrag interpretierte den Redeemer als eine Auszahlung, die etwa 65.000-mal größer war als autorisiert.

Angriffsanalyse

Die folgende Analyse verweist auf die repräsentative Cardano-Transaktion 0a4861...2ea1 und die BSC-Quelltransaktion 0xe90111...d5f26b.

  • Schritt 1: Der Angreifer erstellte normal aussehende Quellketten-Bridge-Anfragen. Im repräsentativen Fall verbrannte die BSC-Transaktion 3.110 NIGHT, und die Wanchain-API verzeichnete einen erwarteten Cardano-Empfangsbetrag von 3.097,56 NIGHT [3].

  • Schritt 2: Bridge-Nodes signierten den aus den rohen, konkatenantierten Feldern konstruierten Autorisierungs-Hash.

  • Schritt 3: Der Angreifer änderte die Cardano-Redeemer-Grenzen zwischen amount und adaAmount, während die signierte Byte-Sequenz unverändert blieb.

  • Schritt 4: Der Cardano TreasuryCheck-Validator analysierte den Redeemer als hochwertige Auszahlung und verifizierte die wiederverwendete Signatur erfolgreich. Die Transaktion zahlte 203.001.692,164714 NIGHT an den Angreifer aus.

  • Schritt 5: Dasselbe Muster wurde über mehrere Cardano-Transaktionen hinweg wiederholt, was zu einem Gesamtabfluss von ca. 515.206.545,426856 NIGHT (~500.000 USD) führte.

Fazit

Es wurde keine Kryptografie gebrochen. Die Unterzeichner erzeugten gültige Signaturen, und der TreasuryCheck-Validator verifizierte sie korrekt. Der Fehler lag in der Nachrichtenkonstruktion: hashRedeemer faltete 14 variable Längenfelder ohne Trennzeichen oder Längenpräfix in eine Byte-Zeichenkette, was die Kodierung nicht-injektiv machte. Unterschiedliche Feld-Tupel erzeugen denselben Preimage, denselben Hash und dieselbe gültige Signatur.

Die Lösung besteht darin, die signierte Kodierung injektiv zu machen, sodass genau ein Feld-Tupel eine gegebene Byte-Zeichenkette erzeugen kann. Festbreitige Ganzzahlkodierung oder Längenpräfixierung jedes Feldes fixiert die Grenzen, die der Angreifer verschoben hat. Ein standardisierter strukturierter Encoder erzielt dasselbe Ergebnis und ist schwerer falsch zu implementieren. Die allgemeine Regel: Signiert eine kanonische Serialisierung statt einer rohen Konkatenation. Dies gehört zur gleichen Fehlerklasse wie Soliditys abi.encodePacked mit variablen Längenargumenten, bei dem unterschiedliche Eingabe-Tupel identische Byte-Sequenzen erzeugen können. Es gilt für jede signierte Nachricht, die aus variablen Längenfeldern zusammengesetzt ist, und ist besonders relevant für Bridges, bei denen die Parteien, die die Nachricht konstruieren und analysieren, in unterschiedlichen Systemen arbeiten.

Referenzen


Lien Finance

Am 24. Juli 2026 wurde Lien Finance, ein dezentrales Bond-OTC-Protokoll auf Ethereum, für ca. 542.000 USD in USDC ausgenutzt. Die Grundursache war ein Validierungsfehler in der Bond-Austauschfunktion: Sie prüfte, ob die Gesamtzahl der gemeinsamen Bonds zwischen Eingabe- und Ausgabegruppen übereinstimmte, verfolgte jedoch nicht, welche spezifischen Bonds abgeglichen wurden. Dies ermöglichte es dem Angreifer, den Burn-Schritt für alle Eingabe-Bonds zu umgehen und gleichzeitig einen unbesicherten Bond zu prägen, der dann über die OTC-Pools des Protokolls für echtes USDC verkauft wurde.

Hintergrund

Lien Finance ist ein DeFi-Protokoll, das besicherte Bonds gegen ETH ausgibt. Sein BondMakerCollateralizedEth-Vertrag ermöglicht es Nutzern, ETH als Sicherheit zu sperren, um Bond-Token zu prägen. Die Auszahlung eines Bonds wird durch eine fnMap definiert, eine stückweise lineare Funktion, die den Sicherheitspreis bei Fälligkeit auf den auszuzahlenden Betrag abbildet.

Die bedeutungsvolle Einheit ist die Bond-Gruppe. registerNewBondGroup() verifiziert, dass die Auszahlungen aller Bonds in einer Gruppe bei jedem Breakpoint ihrer kombinierten fnMap dem ETH-Preis entsprechen. Eine Gruppe, die diese Bedingung erfüllt, rekonstituiert genau eine Einheit Sicherheit. Deshalb sind Gruppen austauschbar: Zwei beliebige Gruppen, die die Bedingung erfüllen, sind gleich viel wert, und der exchangeEquivalentBonds()-Konvertierungspfad verlässt sich auf diese Garantie.

Die Registrierung von Bonds und Gruppen ist erlaubnisfrei: Jede Adresse kann einen neuen Bond registrieren, indem sie eine Fälligkeit und eine beliebige fnMap liefert, und jede Adresse kann jede Liste bereits registrierter Bond-IDs als Gruppe registrieren, vorbehaltlich der Auszahlungssummenprüfung.

Schwachstellenanalyse

Die fehlerhaften Verträge sind 0xDA6F...BEf0 und 0x8432...7de0.

Wenn ein Bond sowohl in der Eingabe- als auch in der Ausgabegruppe vorkommt, ist das Verbrennen und sofortige Wiedereinprägen verschwendete Arbeit. Der exceptionBonds-Parameter benennt diese Bonds, damit der Austausch beide Schritte überspringen kann. Die Funktion erzwingt dies mit einem einzigen Zähler exceptionCount: Er wird beim Scannen der Eingabegruppe einmal pro Treffer erhöht und beim Scannen der Ausgabegruppe einmal pro Treffer verringert, ohne jemals aufzuzeichnen, welche bondID übereinstimmte.

Das bedeutet, dass die Eingabegruppe [BondA, BondB] und die Ausgabegruppe [BondC, BondA, BondA] mit exceptionBonds = [BondA, BondB] die Validierung bestehen. Der Zähler erreicht auf der Eingabeseite 2 (je ein Treffer für BondA und BondB) und kehrt auf der Ausgabeseite zu 0 zurück, aber beide Dekremente stammen von BondAs zwei Einträgen. Der Austausch verbrennt nichts und prägt nur BondC: Ausgabegruppen-Token, die ohne verbrauchte Eingabe erstellt werden.

Angriffsanalyse

Der Angriff ist auf zwei Transaktionen aufgeteilt: Schritt 1 erfolgt in 0xe8689a...284d0f, und die Schritte 2-5 erfolgen in 0xb96d57...48e0e7.

  • Schritt 1: Der Angreifer rief die erlaubnisfreie registerNewBond()-Funktion dreimal auf, um BondA, BondB und BondC mit derselben Fälligkeit zu definieren. BondA und BondB sind gehebelte Calls, die die Sicherheit unterhalb von 3.200 USD gleichmäßig aufteilen, wobei die Auszahlungen dem ETH-Preis entsprechen, wie _assertBondGroup() verlangt. BondC ist ein tief aus dem Geld liegender LBT mit einem Strike bei 3.200 USD gegenüber einem Spot von 1.870 USD: null innerer Wert, aber immer noch ca. 7,64 USD/IMT an Zeitwert als Option.

  • Schritt 2: Der Angreifer registrierte zwei Bond-Gruppen. registerNewBondGroup([BondA, BondB]) gab Gruppe 36 (Eingabe) zurück, und registerNewBondGroup([BondC, BondA, BondA]) gab Gruppe 37 (Ausgabe) zurück. BondCs Flat-Zero-Auszahlung fügt den Breakpoint-Summen nichts hinzu, sodass seine Anwesenheit die Äquivalenzbedingung nicht stört. BondA wird in Gruppe 37 zweimal aufgeführt, was dazu führt, dass der Austausch die Validierung besteht.

  • Schritt 3: Der Angreifer rief exchangeEquivalentBonds(inputGroup = 36, outputGroup = 37, amount = 6968565865905, exceptionBonds = [BondA, BondB]) auf. Beim Scannen der Eingabegruppe: BondA und BondB passen jeweils zu einer Ausnahme, sodass beide den Burn überspringen und exceptionCount 2 erreicht. Beim Scannen der Ausgabegruppe: BondC passt zu nichts und wird geprägt, dann dekrementieren BondAs zwei Einträge jeweils den Zähler auf 0.

  • Schritt 4: Der Angreifer verkaufte den geprägten BondC über die OTC-Pools des Protokolls (GeneralizedDotc) für USDC und erhielt 532.144 USDC.

  • Schritt 5: Der Angreifer wiederholte dasselbe Muster gegen den zweiten BondMakerCollateralizedEth-Vertrag und brachte den Gesamtgewinn auf ca. 542.144,63 USDC.

Fazit

Der exceptionBonds-Mechanismus, der das erneute Einprägen gemeinsamer Bonds überspringen sollte, konnte auf alle Eingabe-Bonds gleichzeitig angewendet werden, indem eine bondID in der Ausgabegruppe dupliziert wurde. Dies brach die Invariante, dass Bond-Token nur gegen hinterlegte Sicherheiten über issueNewBonds() erstellt werden. Die Lösung besteht darin, nachzuverfolgen, welche spezifischen bondIDs abgeglichen wurden (z. B. durch Verwendung einer Bitmap oder eines Sets), um sicherzustellen, dass jede Ausnahme in beiden Gruppen genau einmal verbraucht wird.

Erste Schritte mit Phalcon Security

Jede Bedrohung erkennen, relevante Warnungen ausgeben und Angriffe blockieren.

Jetzt kostenlos testen

Über BlockSec

BlockSec ist ein Full-Stack-Anbieter für Blockchain-Sicherheit und Krypto-Compliance. Wir entwickeln Produkte und Dienstleistungen, die Kunden dabei helfen, Code-Audits durchzuführen (einschließlich Smart Contracts, Blockchain und Wallets), Angriffe in Echtzeit abzufangen, Vorfälle zu analysieren, illegale Gelder zu verfolgen und AML/CFT-Verpflichtungen über den gesamten Lebenszyklus von Protokollen und Plattformen zu erfüllen.

BlockSec hat mehrere Blockchain-Sicherheitspapiere auf renommierten Konferenzen veröffentlicht, mehrere Zero-Day-Angriffe auf DeFi-Anwendungen gemeldet, mehrere Hacks blockiert und dabei mehr als 20 Millionen Dollar gerettet sowie Milliarden von Kryptowährungen gesichert.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit