Am 12. Februar 2025 wurde zkLend [1], ein Kreditprotokoll auf StarkNet, durch eine ausgeklügelte Manipulation seines Akkumulatormechanismus um rund 10 Millionen US-Dollar kompromittiert. Der Angreifer nutzte Flash-Kredite und Rundungsfehler, um die Werte der Sicherheiten künstlich aufzublähen und andere Vermögenswerte vom Protokoll zu leihen, um Gewinne zu erzielen.
Es fehlt jedoch eine detaillierte und genaue technische Analyse aus Sicherheitssicht. Trotz vorhandener Analysen anderer Sicherheitsexperten, die wertvolle Einblicke lieferten, bestehen weiterhin einige Missverständnisse – insbesondere hinsichtlich der Angriffsanalyse. Die spätere Veröffentlichung des offiziellen Post-Mortems von zkLend [2] bietet eine vereinfachte Beschreibung, es fehlt jedoch eine detaillierte technische Analyse. In diesem Blogbeitrag möchten wir eine umfassende Untersuchung liefern, um den Vorfall zu klären.
Wichtigste Erkenntnisse (TL;DR)
-
Die Hauptursache dieses Vorfalls liegt in der Kombination der folgenden drei Probleme:
- Die leere Markinitialisierung ermöglicht die Einzahlung beliebiger Vermögenswerte.
- Der spezifische Spendenmechanismus in zkLends Flash-Kredit ermöglicht die Manipulation des Akkumulators, einer globalen Variablen als Skalierungsfaktor zur dynamischen Anpassung der Sicherheitenwerte der Benutzer.
- Präzisionsverlust tritt durch Abschneiden auf. Im Gegensatz zum klassischen Präzisionsverlust bei der Division beginnt der Nenner bei 1, wird aber auf einen sehr großen Wert aufgebläht, was zu einer Unterschätzung beim Verbrennen des Anteil-Tokens führt.
-
Der Angreifer profitierte nicht von wstETH, das von anderen Benutzern hinterlegt wurde. Stattdessen nutzte der Angreifer die Schwachstellen aus, um den Sicherheitenwert zu manipulieren und eine kleine Menge wstETH als Anfangskapital zu verwenden, um den Sicherheitenwert auf über 7.000 wstETH zu erhöhen, was die Aufnahme anderer Vermögenswerte aus dem Markt ermöglichte.
In den folgenden Abschnitten werden wir zunächst einige wichtige Hintergrundinformationen zu zkLend liefern. 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. Lassen Sie uns in die Implementierungsdetails dieser beiden Protokolle eintauchen.
0x1.1 Besicherte Kredite
Ein besicherter Kredit bezieht sich auf den Prozess, bei dem Benutzer bestimmte Vermögenswerte als Sicherheit an das Protokoll hinterlegen und im Gegenzug andere Vermögenswerte leihen. Der Wert der Sicherheit bestimmt die Kreditkapazität. Es ist wichtig zu beachten, dass Kreditprotokolle normalerweise nicht den Wert des Sicherheitenvermögens direkt speichern; stattdessen berechnen sie ihn mit der Formel:
collateral_balance = lending_accumulator * raw_balance
Insbesondere ist der lending_accumulator ein Skalierungsfaktor, der den Sicherheitenwert jedes Benutzers dynamisch anpasst, während raw_balance den tatsächlichen Anteil darstellt, den der Benutzer auf dem Markt hält. raw_balance wird aus dem collateral_balance unter Verwendung des lending_accumulator abgeleitet.
Was ist der Zweck dieses Designs? Es ermöglicht dem Protokoll, den Sicherheitenwert effizient zu verwalten und gleichzeitig die Benutzer zur Einzahlung von Vermögenswerten zu ermutigen. Durch die Zuweisung eines Teils der Einnahmen des Protokolls an die Sicherheitenanbieter steigt der lending_accumulator, wodurch der Wert aller Sicherheiten proportional und gleichzeitig verstärkt wird.
0x1.2 Flash-Kredite auf zkLend
Ein Flash-Kredit ist eine Art unbesicherter Kredit, bei dem Benutzer Vermögenswerte für einen sehr kurzen Zeitraum, normalerweise innerhalb einer einzigen Transaktion, vom Protokoll leihen können. Gelingt es dem Kreditnehmer nicht, den Kredit zurückzuzahlen oder die angegebenen Bedingungen zu erfüllen, wird die gesamte Transaktion rückgängig gemacht und der Kredit wird nicht ausgeführt.
In der Flash-Kredit-Implementierung von zkLend gibt es einen einzigartigen Spenden-Mechanismus. Wenn Benutzer Vermögenswerte zurückzahlen, geben sie nicht nur den erforderlichen Mindestbetrag zurück, sondern können auch zusätzliche Gelder als Spende beitragen. Das Protokoll verfolgt diese gespendeten Gelder 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 zugrunde liegenden Tokens (z. B. wstETH), der im Vertrag gehalten wird, einschließlich der von Benutzern gespendeten Token.totaldebt: Die Gesamtschuld aller Kreditnehmer.amount_to_treasury: Der Betrag der Protokolleinnahmen.ztoken_supply: Die Gesamtmenge des Anteil-Tokens (z. B. zwstETH). Wenn Benutzer wstETH einzahlen, prägt der zkLend ztoken-Vertrag einen entsprechenden Betrag an zwstETH.
Nachdem wir das Kernprotokoll von zkLend verstanden haben, werden wir nun formal erläutern, wie der Angreifer seine Sicherheitenwerte manipulierte, indem er die 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 von
lending_accumulator- Leerer Markt: Vor dem Angriff war der zkLend-Markt für wstETH-Token leer, was die perfekte Bedingung für die Manipulation schuf. Darüber hinaus erlaubt der zkLend Market-Vertrag jedem, beliebige Mengen von Vermögenswerten in einen leeren Markt einzuzahlen. Der Angreifer zahlte eine geringe Menge an Vermögenswerten ein, um den Wert des
lending_accumulatorerheblich aufzublähen. - Spendenmechanismus: Die Funktion
flash_loan()des zkLend Market-Vertrags verfügt über einen einzigartigen Spenden-Mechanismus. Insbesondere wenn ein Benutzer einen Flash-Kredit zurückzahlt, berechnet der Market-Vertrag die überschüssigen zurückgegebenen Mittel und erhöht die globale Variablelending_accumulator, wodurch die Sicherheitenwerte für alle Benutzer im Vertrag verstärkt werden.
- Leerer Markt: Vor dem Angriff war der zkLend-Markt für wstETH-Token leer, was die perfekte Bedingung für die Manipulation schuf. Darüber hinaus erlaubt der zkLend Market-Vertrag jedem, beliebige Mengen von Vermögenswerten in einen leeren Markt einzuzahlen. Der Angreifer zahlte eine geringe Menge an Vermögenswerten ein, um den Wert des
- Manipulation von
raw_balance- Rundungsverhalten: Die Division im Prozess der Verbrennung von Anteil-Tokens verwendet Abschneiden (Truncation), was zu einer Unterschätzung der Änderung des
raw_balancedes Benutzers bei Entnahmen führt.
- Rundungsverhalten: Die Division im Prozess der Verbrennung von Anteil-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 Gewinne zu erzielen.
0x2.1 Manipulation der Variable lending_accumulator
0x2.1.1 Leere Markinitialisierung
Durch die Untersuchung des Transaktionsprotokolls des Market-Vertrags vor dem Angriff können wir beobachten, dass der Angreifer zunächst 1 wei wstETH in den wstETH Market-Vertrag eingezahlt hat. Bei Durchsicht der internen Aufrufe dieser Transaktion wird deutlich, dass der wstETH Market-Vertrag 0 wstETH hielt und die Gesamtmenge an zwstETH ebenfalls 0 betrug.
Daher können wir bestätigen, dass es im zkLend wstETH-Markt keine vorherigen Einzahlungen oder Kredite gab. Sowohl der reserve_balance als auch die ztoken_supply befanden sich in ihren Anfangswerten von 0, und der Anfangswert des lending_accumulator war 1. Dieses leere Marktszenario schuf die Voraussetzungen für den nachfolgenden Angriff und ermöglichte es dem Angreifer, den lending_accumulator mit einer minimalen Menge an wstETH erheblich zu verstärken.
0x2.1.2 Manipulation von lending_accumulator über Flash-Kredit
Als Nächstes ruft der Angreifer in dieser Transaktion die Funktion flash_loan() auf, leiht 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.
Gemäß der Formel zur Berechnung des lending_accumulator erhöht diese Transaktion den lending_accumulator von 1 auf 851,0.
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 der Dezimalpräzision von wstETH entspricht.
0x2.2 Manipulation der Variable raw_balance
Nachdem der lending_accumulator auf etwa 4,069 × 10^18 manipuliert wurde, rief der Angreifer die Funktion deposit() des Market-Vertrags mit 4.069297906051644020 wstETH auf. Basierend auf dem neuesten Wert des lending_accumulator wurde das raw_balance des Angreifervertrags zu 2.
0x2.2.1 Die erste Transaktion zur Manipulation von raw_balance
In dieser Transaktion rief der Angreifer die Funktion callflashloandraaan() des Angreifervertrags auf. Obwohl dieser Vertrag nicht Open Source ist, kann basierend auf der internen Aufrufsprotokollierung spekuliert werden, dass die Logik dieser Funktion eine Schleife enthält, die folgende Aktionen durchführt:
- Einzahlung: Der Angreifer zahlt eine bestimmte Menge wstETH in den Marktvertrag ein.
- Entnahme: Der Angreifer entnimmt den spezifischen Betrag an wstETH.
Analyse der Token-Übertragungsaufzeichnungen
Es kann beobachtet werden, dass der vom Angreifer eingezahlte Betrag an wstETH immer ein ganzzahliges Vielfaches des lending_accumulator ist, zum Beispiel das 2-fache des Wertes (z. B. 8.13859) des lending_accumulator.
Der entnommene wstETH-Betrag ist jedoch das 1,5-fache des Wertes (z. B. 6.10394) des lending_accumulator.
Durch Berechnungen können wir feststellen, dass der entnommene wstETH-Betrag den eingezahlten Betrag übersteigt. Warum passiert das?
Rundungsverhalten
Bei Überprüfung der Implementierung der Methoden deposit() und withdraw() sehen wir, dass diese beiden Methoden die Prägung und Verbrennung von zwstETH beinhalten. So funktioniert es:
`mint()` Funktion im Market-Vertrag
`burn()` Funktion im Market-Vertrag
Die Prozesse mint() und burn() beinhalten beide eine Skalierungslogik. Die Skalierungslogik beinhaltet Ganzzahldivision mit Abrundung (Abrundung auf die nächste ganze Zahl), 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ßergewöhnlich hoch ist (etwa 4.069.297.906.051.644.020), führt diese Division dazu, dass das raw_balance des Angreifers nur um 1 Einheit sinkt, obwohl über 6 zwstETH verbrannt werden.
Die Änderungen am raw_balance des Angreifers sind in der folgenden Tabelle zusammengefasst:
Wir können beobachten, dass der Angreifer in dieser Transaktion wiederholt die Einzahlung - Entnahme-Logik ausführt und dabei den Präzisionsverlust während der Funktion withdraw() ausnutzt, was zu einer Unterschätzung der Differenz des raw_balance führt. Letztendlich stieg das raw_balance des Benutzers von 2 auf 3, was eine zusätzliche Einheit einbrachte.
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 das raw_balance weiter erhöht wird, was dazu führt, dass der Wert der Sicherheiten des Angreifers weiter steigt.
Beispielhafte Erläuterung
Wir verwenden die folgende Transaktion als 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 entnehmen.
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 an den Angreifervertrag zu überweisen, der dann die Mittel für nachfolgende Einzahlungen in den Market-Vertrag bereitstellte.
Diese Aktionen steigern den Wert von raw_balance und ermöglichen es dem Angreifer, den Sicherheitenwert weiter zu erhöhen. Schließlich erreichte das 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 Finanzierungsarten
Nach der Manipulation des Sicherheitenwertes lieh der Angreifer andere Arten von Geldern vom Markt und fuhr mit den folgenden Transaktionen fort (Auszug):
0x3.2 Überbrücken der geliehenen Gelder auf Layer1
Bei der Überprüfung der Bridge-Transaktionen des Angreifervertrags kann beobachtet werden, dass der Angreifer einen Teil der geliehenen Gelder auf Layer 1 überbrückte.
0x4 Fazit
Zusammenfassend unterstreicht dieser Angriff auf das zkLend-Protokoll mehrere wichtige Implikationen für das Design und die Sicherheit von dezentralen Kreditprotokollen:
- Markinitialisierung und Bedingungen für die Einzahlung von Vermögenswerten:
Der anfänglich leere Markt ermöglichte es dem Angreifer, eine geringe 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 Begrenzung von Vermögensspenden in frühen Marktphasen könnte helfen, ähnliche Angriffe zu verhindern. - Wichtigkeit korrekter Akkumulatormechanismen:
Der Angreifer nutzte den Spendenmechanismus in der Funktion
flash_loan()aus, um denlending_accumulatorzu manipulieren und die Sicherheitenwerte aller Benutzer aufzublähen. Protokolle mit akkumulatorbasierten Mechanismen sollten vor einer einfachen Manipulation von Skalierungsfaktoren geschützt sein. - Rundungsverhalten und Präzisionsverlust:
Ein Rundungsproblem während der Verbrennung von zwstETH-Tokens führte zu Präzisionsverlust und Unterschätzung des
raw_balance, was es dem Angreifer ermöglichte, dasraw_balancezu manipulieren. Protokolle sollten eine höhere Präzision oder Validierungsprüfungen verwenden, um solche Exploits zu verhindern.
Dieser Vorfall unterstreicht erneut die Bedeutung von rechtzeitigen Benachrichtigungen bezüglich der Initialisierungs- und Betriebsstati sowie der proaktiven Gefahrenabwehr zur Minderung potenzieller Verluste.
Referenz
[1] https://zklend.com/
[2] zkLends Sicherheitsvorfall Post-Mortem: https://drive.google.com/file/d/10i1dh_J89tPPw7KRcmFIVM6iNrJZAyfi/view



