Am 12. Februar 2025 wurde zkLend [1], ein Kreditprotokoll auf StarkNet, durch eine ausgeklügelte Manipulation seines Akkumulatormechanismus um rund 10 Millionen US-Dollar ausgenutzt. Der Angreifer nutzte Flash-Kredite und Rundungsfehler aus, um die Werte der Sicherheiten künstlich aufzublähen und andere Vermögenswerte vom Protokoll zu leihen, um davon zu profitieren.
Es besteht jedoch nach wie vor ein Mangel an detaillierten und genauen technischen Analysen aus Sicherheitssicht. Trotz bestehender Analysen von anderen Sicherheitsexperten, die wertvolle Einblicke lieferten, bestehen einige Missverständnisse fort – insbesondere hinsichtlich der Angriffsanalyse. Die spätere Veröffentlichung des offiziellen Post-Mortems [2] von zkLend bietet eine vereinfachte Beschreibung, aber es fehlt eine detaillierte technische Analyse. In diesem Blogbeitrag wollen wir eine umfassende Untersuchung zur Klärung des Vorfalls liefern.
Wichtigste Erkenntnisse (TL;DR)
-
Die Hauptursache dieses Vorfalls liegt in der Kombination der folgenden drei Probleme:
- Die leere Marktinitialisierung ermöglicht beliebige Einzahlungen von Vermögenswerten.
- Der spezifische Spendenmechanismus im Flash-Kredit von zkLend ermöglicht die Manipulation des Akkumulators, einer globalen Variablen als Skalierungsfaktor zur dynamischen Anpassung der Sicherheitenwerte der Nutzer.
- Präzisionsverlust durch Abschneiden (Truncation). Im Gegensatz zum klassischen Präzisionsverlust bei der Division beginnt der Nenner bei 1, wurde aber auf einen sehr großen Wert aufgebläht, was zu einer Unterschätzung beim Verbrennen des Share-Tokens führte.
-
Der Angreifer profitierte nicht von wstETH, das von anderen Nutzern eingezahlt wurde. Stattdessen nutzte der Angreifer die Schwachstellen aus, um den Sicherheitenwert zu manipulieren und nutzte eine kleine Menge wstETH als Anfangskapital, um den Sicherheitenwert auf über 7.000 wstETH zu erhöhen und dadurch das Leihen anderer Vermögenswerte aus dem Protokoll zu ermöglichen.
In den folgenden Abschnitten werden wir zunächst einige wichtige Hintergrundinformationen zu zkLend geben. Anschließend werden wir eine eingehende Analyse der Probleme und des damit verbundenen Angriffs durchführen.
0x1 Hintergrund: Verständnis des Kernprotokolls von zkLend
zkLend ist ein Kreditprojekt auf StarkNet, das gängige Kreditprotokolle wie besicherte Kredite und Flash-Kredite unterstützt. Tauchen wir in die Implementierungsdetails dieser beiden Protokolle ein.
0x1.1 Besicherte Kredite
Ein besicherter Kredit bezieht sich auf den Prozess, bei dem Nutzer bestimmte Vermögenswerte als Sicherheit an das Protokoll einzahlen, um im Gegenzug andere Vermögenswerte zu leihen. Der Wert der Sicherheit wird verwendet, um die Kreditkapazität zu bestimmen. Es ist wichtig zu beachten, dass Kreditprotokolle normalerweise nicht den Wert des Sicherheitenvermögens direkt speichern; stattdessen berechnen sie ihn nach der Formel:
collateral_balance = lending_accumulator * raw_balance
Insbesondere ist der lending_accumulator ein Skalierungsfaktor, der den Sicherheitenwert jedes Nutzers dynamisch anpasst, während raw_balance den tatsächlichen Anteil repräsentiert, den der Nutzer im Markt hält. raw_balance wird unter Verwendung des lending_accumulator aus dem collateral_balance abgeleitet.
Was ist der Zweck dieses Designs? Es ermöglicht dem Protokoll, den Sicherheitenwert effizient zu verwalten und gleichzeitig Nutzer zur Einzahlung von Vermögenswerten zu ermutigen. Durch die Zuweisung eines Teils der Einnahmen des Protokolls an die Sicherheitenanbieter erhöht sich der lending_accumulator, wodurch der Wert aller Sicherheiten der Nutzer proportional und gleichzeitig vervielfacht wird.
0x1.2 Flash-Kredite auf zkLend
Ein Flash-Kredit ist eine Art unbesicherter Kredit, bei dem Nutzer Vermögenswerte für sehr kurze Zeit, typischerweise innerhalb einer einzigen Transaktion, vom Protokoll leihen können. Gelingt es dem Kreditnehmer nicht, den Kredit zurückzuzahlen oder die festgelegten Bedingungen zu erfüllen, wird die gesamte Transaktion rückgängig gemacht und der Kredit nicht ausgeführt.
Bei der Flash-Kredit-Implementierung von zkLend gibt es einen einzigartigen Spenden-Mechanismus. Wenn Nutzer Vermögenswerte zurückzahlen, können sie nicht nur den erforderlichen Mindestbetrag zurückzahlen, sondern auch zusätzliche Mittel als Spende beisteuern. Das Protokoll verfolgt diese gespendeten Mittel und aktualisiert den lending_accumulator entsprechend. Dieser Prozess wird in der Funktion thesettle_extra_reserve_balance() implementiert. Die Formel zur Aktualisierung des lending_accumulator lautet wie folgt:
new_accumulator = (reserve_balance + totaldebt - amount_to_treasury) / ztoken_supply
reserve_balance: Der Gesamtbetrag des zugrundeliegenden Tokens (z. B. wstETH), der im Vertrag gehalten wird, einschließlich des von Nutzern gespendeten Token-Betrags.totaldebt: Die Gesamtschulden aller Kreditnehmer.amount_to_treasury: Der Betrag der Protokolleinnahmen.ztoken_supply: Der Gesamtvorrat des Share-Tokens (z. B. zwstETH). Wenn Nutzer wstETH einzahlen, prägt der zkLend-ztoken-Vertrag einen entsprechenden Betrag an zwstETH.
Nachdem wir das Kernprotokoll von zkLend verstanden haben, werden wir nun formell erklären, wie der Angreifer seine Sicherheitenwerte durch Manipulation der Variablen lending_accumulator und raw_balance manipulierte.
0x2 Angriffsanalyse
Der Angreifer nutzte die folgenden Mechanismen und Schwachstellen im zkLend-Vertrag aus, um den Wert der Sicherheit zu manipulieren:
- Manipulation des
lending_accumulator- Leerer Markt: Vor dem Angriff war der zkLend-Markt für wstETH-Token leer, was die perfekten Bedingungen für eine Manipulation schuf. Darüber hinaus erlaubt der zkLend Market Contract jedem, beliebige Mengen an Vermögenswerten in einen leeren Markt einzuzahlen. Der Angreifer zahlte eine kleine Menge an Vermögenswerten ein, um den Wert des
lending_accumulatorsignifikant aufzublähen. - Spendenmechanismus: Die
flash_loan()-Funktion des zkLend Market Contracts verfügt über einen einzigartigen Spenden-Mechanismus. Insbesondere wenn ein Nutzer einen Flash-Kredit zurückzahlt, berechnet der Market Contract die überschüssigen zurückgezahlten Gelder und erhöht die globale Variablelending_accumulator, wodurch die Sicherheitenwerte für alle Nutzer im Vertrag vervielfacht werden.
- Leerer Markt: Vor dem Angriff war der zkLend-Markt für wstETH-Token leer, was die perfekten Bedingungen für eine Manipulation schuf. Darüber hinaus erlaubt der zkLend Market Contract jedem, beliebige Mengen an Vermögenswerten in einen leeren Markt einzuzahlen. Der Angreifer zahlte eine kleine Menge an Vermögenswerten ein, um den Wert des
- Manipulation von
raw_balance- Rundungsverhalten: Die Divisionsoperation während des Verbrennungsprozesses des Share-Tokens verwendet Abschneiden (Truncation), was zu einer Unterschätzung der Änderung des
raw_balancedes Nutzers bei Entnahmen führt.
- Rundungsverhalten: Die Divisionsoperation während des Verbrennungsprozesses des Share-Tokens verwendet Abschneiden (Truncation), was zu einer Unterschätzung der Änderung des
Durch die Manipulation beider Variablen konnte der Angreifer den Sicherheitenwert auf über 7.000 wstETH erhöhen und andere Vermögenswerte vom Markt leihen, um davon zu profitieren.
0x2.1 Manipulation der Variable lending_accumulator
0x2.1.1 Leere Marktinitialisierung
Bei der Untersuchung des Transaktionsdatensatzes des Market Contracts vor dem Angriff können wir beobachten, dass der Angreifer zunächst 1 wei wstETH in den wstETH Market Contract eingezahlt hat. Bei Überprüfung der internen Aufrufe dieser Transaktion ist ersichtlich, dass der wstETH Market Contract 0 wstETH enthielt und die Gesamtzahl der zwstETH ebenfalls 0 betrug.
Daher können wir bestätigen, dass es keine vorherigen Einzahlungen oder Kredite auf dem zkLend wstETH-Markt gab. Sowohl der reserve_balance als auch ztoken_supply befanden sich auf ihren Anfangswerten von 0, und der Anfangswert des lending_accumulator war 1. Dieses leere Marktszenario schuf die Bedingungen für den nachfolgenden Angriff, indem es dem Angreifer ermöglichte, den lending_accumulator mit einer minimalen Menge wstETH erheblich aufzublähen.
0x2.1.2 Manipulation des lending_accumulator über Flash-Kredit
Als Nächstes ruft der Angreifer in dieser Transaktion die flash_loan()-Funktion auf, leiht sich 1 wei wstETH und zahlt 1000 wei wstETH zurück. Die überschüssigen 999 wei werden als Spende behandelt und im reserve_balance des Vertrags verbucht.
Nach der Formel zur Berechnung des lending_accumulator bewirkt diese Transaktion, dass sich der lending_accumulator von 1 auf 851,0 erhöht.
0x2.1.3 Wiederholte Ausführung von flash_loan()
Der Angreifer führt insgesamt 10 flash_loan()-Aufrufe durch, leiht jedes Mal nur 1 wei wstETH, zahlt aber einen größeren Betrag zurück. Infolgedessen steigt der lending_accumulator auf einen astronomischen Wert von 4.069.297.906.051.644.020 (4,069 × 10^18), was zufällig mit der Dezimalgenauigkeit von wstETH übereinstimmt.
0x2.2 Manipulation der Variable raw_balance
Nachdem der lending_accumulator auf etwa 4,069 × 10^18 manipuliert wurde, rief der Angreifer die deposit()-Funktion des Market Contracts mit 4,069297906051644020 wstETH auf. Basierend auf dem neuesten Wert des lending_accumulator wurde der raw_balance des Angreifer-Vertrags 2.
0x2.2.1 Die erste Transaktion, die raw_balance manipuliert
In dieser Transaktion rief der Angreifer die callflashloandraaan()-Funktion des Angreifer-Vertrags auf. Obwohl dieser Vertrag nicht Open Source ist, kann basierend auf der internen Aufrufverfolgung vermutet werden, dass die Logik dieser Funktion eine Schleife beinhaltet, die folgende Aktionen durchführt:
- Einzahlung: Der Angreifer zahlt eine bestimmte Menge wstETH in den Marktvertrag ein.
- Entnahme: Der Angreifer entnimmt den spezifischen Betrag wstETH.
Analyse der Token-Transfer-Aufzeichnungen
Es ist zu beobachten, dass die vom Angreifer eingezahlte Menge an wstETH immer ein ganzzahliges Vielfaches des lending_accumulator ist, z. B. das 2-fache des Wertes (z. B. 8,13859) des lending_accumulator.
Die entnommene Menge an wstETH ist jedoch das 1,5-fache des Wertes (z. B. 6,10394) des lending_accumulator.
Durch Berechnungen können wir feststellen, dass die entnommene Menge an wstETH die eingezahlte Menge übersteigt. Warum passiert das?
Rundungsverhalten
Bei der Überprüfung der Implementierung der deposit()- und withdraw()-Methoden sehen wir, dass diese beiden Methoden die Prägung und das Verbrennen von zwstETH beinhalten. Hier ist, wie das funktioniert:
`mint()`-Funktion im Market Contract
`burn()`-Funktion im Market Contract
Die Prozesse mint() und burn() beinhalten beide eine Skalierungslogik (scale down logic). Die Skalierungslogik beinhaltet ganzzahlige Division mit Abrundung (floor rounding), was eine Schlüsselrolle beim Exploit spielt.
Wenn der Angreifer eine bestimmte Menge zwstETH verbrennt, wird die Skalierungslogik angewendet. Aufgrund des manipulierten Wertes des lending_accumulator, der außerordentlich hoch ist (rund 4.069.297.906.051.644.020), führt diese Division dazu, dass der raw_balance des Angreifers nur um 1 Einheit abnimmt, obwohl über 6 zwstETH verbrannt wurden.
Die Änderungen des raw_balance des Angreifers sind in der folgenden Tabelle zusammengefasst:
Wir können beobachten, dass der Angreifer in dieser Transaktion wiederholt die Einzahlungs-Entnahme-Logik ausführt und dabei den Präzisionsverlust während der withdraw()-Funktion ausnutzt, was zu einer Unterschätzung der raw_balance-Differenz führt. Letztendlich stieg der raw_balance des Nutzers von 2 auf 3, wodurch eine zusätzliche Einheit gewonnen wurde.
0x2.2.2 Nachfolgender Angriffsprozess
Nachfolgende Angriffstransaktionen folgten dem gleichen Muster wie der erste Angriff: Der Angreifer durchlief wiederholt Einzahlungs-Entnahme-Transaktionen, um wstETH zu erwerben.
Das erworbene wstETH wird wieder in den Markt eingezahlt, wodurch der raw_balance weiter erhöht wird, was dazu führt, dass der Wert der Sicherheit des Angreifers weiter steigt.
Beispielhafte Erklärung
Wir verwenden die folgende Transaktion zur Veranschaulichung.
- Es wurden insgesamt 30 Einzahlungen getätigt, wobei jedes Mal 4,069 wstETH eingezahlt wurden.
- Es wurden insgesamt 30 Entnahmen getätigt, wobei jedes Mal 6,104 wstETH entnommen wurden.
- Nach diesem Zyklus konnte der Angreifer laut Berechnungen 61,39 wstETH entziehen.
Darüber hinaus ist anzumerken, dass zwischen diesen Angriffstransaktionen mehrere increase()-Methoden aufgerufen wurden. Diese Methoden wurden verwendet, um eine bestimmte Menge wstETH vom Konto des Angreifers auf den Angreifer-Vertrag zu übertragen, der dann die Mittel für nachfolgende Einzahlungen in den Market Contract bereitstellte.
Diese Aktionen steigerten den Wert von raw_balance und ermöglichten es dem Angreifer, den Sicherheitenwert weiter zu erhöhen. Schließlich erreichte der raw_balance des Angreifers 1.724 mit einem Wert von 7.015,4 wstETH, was ausreichte, um andere Vermögenswerte vom Markt zu leihen.
0x3 Gewinnanalyse
0x3.1 Leihen anderer Gelder
Nachdem der Wert der Sicherheit manipuliert worden war, lieh sich der Angreifer andere Arten von Geldern vom Markt und führte die folgenden Transaktionen durch (Auszug):
0x3.2 Überbrückung der geliehenen Gelder zu Layer1
Durch die Inspektion der Bridge-Transaktionen des Angreifer-Vertrags kann beobachtet werden, dass der Angreifer einen Teil der geliehenen Gelder auf Layer 1 überbrückt hat.
0x4 Fazit
Zusammenfassend unterstreicht dieser Angriff auf das zkLend-Protokoll mehrere wichtige Implikationen für das Design und die Sicherheit von dezentralen Kreditprotokollen:
- Marktinitialisierung und Bedingungen für die Einzahlung von Vermögenswerten:
Der zu Beginn leere Markt ermöglichte es dem Angreifer, eine kleine Menge wstETH einzuzahlen und den
lending_accumulatorzu manipulieren, wodurch er einen Hebel für den Exploit erhielt. Die Sicherstellung einer ausreichenden Liquiditätsbasis oder die Beschränkung von Asset-Spenden in frühen Marktphasen könnte ähnliche Angriffe verhindern. - Bedeutung korrekter Akkumulatormechanismen:
Der Angreifer nutzte den Spendenmechanismus in der
flash_loan()-Funktion aus, um denlending_accumulatorzu manipulieren und die Sicherheitenwerte für alle Nutzer aufzublähen. Protokolle mit akkumulatorbasierten Mechanismen sollten vor einer einfachen Manipulation von Skalierungsfaktoren geschützt sein. - Rundungsverhalten und Präzisionsverlust:
Ein Rundungsproblem beim Verbrennen von zwstETH-Tokens führte zu Präzisionsverlust und Unterschätzung von
raw_balance, was es dem Angreifer ermöglichte,raw_balancezu manipulieren. Protokolle sollten höhere Präzision oder Validierungsprüfungen verwenden, um solche Exploits zu verhindern.
Auch dieser Vorfall unterstreicht erneut die Bedeutung von zeitnahen Benachrichtigungen bezüglich der Initialisierungs- und Betriebszustände sowie der proaktiven Bedrohungsprävention zur Minderung potenzieller Verluste.
Referenzen
[1] https://zklend.com/
[2] zkLends Sicherheitsvorfall Post-Mortem: https://drive.google.com/file/d/10i1dh_J89tPPw7KRcmFIVM6iNrJZAyfi/view



