Back to Blog

Tödliche Integration: Schwachstellen in Hooks durch riskante Wechselwirkungen

Code Auditing
November 20, 2023
9 min read

Wie in unserem vorherigen Artikel hervorgehoben, weisen über 30% der Projekte im Awesome Uniswap v4 Hooks-Repository[1] Schwachstellen auf. Es ist erwähnenswert, dass sich die hier genannten Schwachstellen speziell auf die Uniswap v4-Interaktionen beziehen. Dementsprechend werden wir in diesem Artikel die sichere Hook-Interaktionslogik aus den folgenden zwei Perspektiven untersuchen:

  • Fehlerhafte Zugriffskontrolle
  • Unzureichende Eingabevalidierung

Für jede Kategorie beginnen wir mit einer Analyse der Schwachstelle und demonstrieren deren potenzielle Ausnutzung anhand des entsprechenden Proof-of-Concept (PoC). Anschließend werden mögliche Mitigationsstrategien diskutiert.

Fehlerhafte Zugriffskontrolle

Im Allgemeinen können Interaktionen im Zusammenhang mit Uniswap v4-Hooks danach klassifiziert werden, ob der Hook als Locker agiert und eine Sperre im PoolManager erwirbt, um Operationen in Pools durchzuführen. Zwei primäre Interaktionsszenarien erfordern eine ordnungsgemäße Zugriffskontrolle:

  • Hook-PoolManager-Interaktion: Dies umfasst Interaktionen zwischen den offiziellen Callback-Funktionen und dem PoolManager. Zu den Callback-Funktionen gehören acht Pool-Aktions-Callbacks (d.h. initialize, modifyPosition, swap und donate) sowie der Lock-Callback (d.h. lockAcquired).
  • Hook-Internal-Interaktion: Dies betrifft Interaktionen, die innerhalb des Hook-Contracts (in der Rolle des Lockers) stattfinden.

Hook-PoolManager-Interaktionen sind vergleichsweise unkompliziert. Hier agiert der Hook rein als Hook und akzeptiert die acht Pool-Aktions-Callbacks. Die Logik im Hook beeinflusst keine zugehörigen Pools, d.h. es gibt keine Geldflüsse zwischen dem Hook und den Pools. Die von den Callback-Funktionen bereitgestellten Parameter werden verwendet, um notwendige Speicherinhalte zu ändern oder als wichtige Funktionsparameter zu dienen. Die entscheidende Überlegung ist, ob die Callback-Parameter manipuliert werden können.

Hook-Internal-Interaktionen sind etwas komplexer. In der Praxis tun viele Hook-Prototypen mehr als nur als reine Hooks zu fungieren. Einige Entwickler erlauben den Hooks, Fondsverwaltungsfunktionen für ihre Nutzer bereitzustellen. Diese Funktionen müssen nicht zwingend im Hook-Contract implementiert sein, wir können sie in diesem Zusammenhang jedoch kollektiv als Hooks betrachten. In diesen Fällen akzeptiert ein Hook Nutzerfonds und führt Pool-Operationen wie Liquiditätsmanagement oder Swaps durch. Das bedeutet, dass der Contract eine Sperre vom PoolManager erwerben muss, wodurch der Hook zum Locker wird. Die Uniswap Foundation hat diese Situation berücksichtigt und eine Funktion in ihre Hook-Vorlage integriert. Konkret stellt die BaseHook-Vorlage die lockAcquired-Funktion als Lock-Callback bereit, wie folgt:

    function lockAcquired(bytes calldata data) external virtual poolManagerOnly
returns (bytes memory) {
        (bool success, bytes memory returnData) = address(this).call(data);
        if (success) return returnData;
        if (returnData.length == 0) revert LockFailure();
        // if the call failed, bubble up the reason
        /// @solidity memory-safe-assembly
        assembly {
            revert(add(returnData, 32), mload(returnData))
        }
    }

Um benutzerdefinierte Logik auszuführen, akzeptiert lockAcquired data-Bytes und führt einen Low-Level-Aufruf an sich selbst mit diesen data durch. Die data hängen von der Geschäftslogik des Hooks ab und können von Nutzern manipuliert werden, was aufgrund von Hook-Internal-Interaktionen, die durch lockAcquired ausgelöst werden, zu Sicherheitsproblemen führen kann. Zu beachten ist, dass das Hook-Design so flexibel ist, dass wir nicht alle möglichen Szenarien in dieser Situation abdecken können. Unser Hauptaugenmerk liegt hier auf dem Hook, der eine Sperre erwirbt, und seinen anschließenden internen Interaktionen. Das Eingehen auf andere mögliche Geschäftslogiken würde die Situation für diese Diskussion zu komplex machen.

In beiden Szenarien hat die Behebung fehlerhafter Zugriffskontrollen, die potenziell zur Ausnutzung führen könnten, Priorität, da diese Funktionen klare Interaktionseinheiten aufweisen. In den nachfolgenden Unterabschnitten werden wir jedes Szenario der Reihe nach untersuchen und die notwendigen Zugriffskontrollen diskutieren, um eine sicherere Interaktionslogik zu gewährleisten.

Schwachstellenanalyse

Zugriffskontrollen dienen als hocheffiziente und unkomplizierte Sicherheitslösungen für viele Projekte. Wenn eine Funktion dafür ausgelegt ist, von bestimmten Einheiten aufgerufen zu werden, sollte sie eine Zugriffskontrolle beinhalten. Das bekannteste Beispiel für Zugriffskontrolle ist der Ownable-Contract der OpenZeppelin-Bibliothek, der verlangt, dass privilegierte Funktionen nur vom Contract-Eigentümer aufgerufen werden. Es ist klar, dass die beiden oben diskutierten Szenarien geeignete Fälle für diese Art von Kontrolle sind.

Hook-PoolManager-Interaktion: Für sichere Interaktionen mit dem PoolManager sollten Hooks eine notwendige Zugriffskontrolle für diese Callback-Funktionen durchsetzen. Konkret sollten diese Callbacks ausschließlich vom PoolManager und nicht von anderen Accounts aufrufbar sein. Das Versäumnis, solche Kontrollen einzurichten, kann dazu führen, dass diese sensiblen Schnittstellen potenziell durch böswillige Akteure ausgenutzt werden.

Neben den acht Pool-Aktions-Callbacks muss auch der Lock-Callback (d.h. lockAcquired), der benutzerdefinierte Logik nach dem Erwerb der Sperre vom PoolManager ausführt, dieses Problem adressieren.

Hook-Internal-Interaktion: Die an den Hook-Internal-Interaktionen beteiligten Funktionen sind ebenfalls dafür ausgelegt, von bestimmten Aufrufern aufgerufen zu werden. Wie bereits erwähnt, enthält dieses Szenario zwei Phasen. Zuerst wird die lockAcquired-Funktion des Lockers vom PoolManager aufgerufen, was darauf hinweist, dass die Funktion verlangen sollte, dass der msg.sender der PoolManager ist. Anschließend verteilt der Hook den Funktionsaufruf entsprechend. Basierend auf dem Design von BaseHook wird dies durch Low-Level-Aufrufe an den Hook selbst implementiert. Dies bedeutet, dass diese Funktionen als external definiert sein müssen und der Aufrufer die Adresse des Hooks sein muss.

Nehmen wir eines der vom Awesome Uniswap v4 Hooks-Repository aufgelisteten Beispiele, d.h. Stop Loss Order, als Beispiel[2]:

Direkt in die Uniswap V4-Pools integriert, werden Stop-Loss-Orders on-chain gepostet und über den afterSwap()-Hook ausgeführt. Es sind keine externen Bots oder Akteure erforderlich, um die Ausführung zu gewährleisten.

Betrachten wir seine afterSwap-Callback-Funktion:

Abbildung 1: Die afterSwap-Funktion von Stop Loss Order
Abbildung 1: Die afterSwap-Funktion von Stop Loss Order

Es ist offensichtlich, dass die obige Funktion dafür ausgelegt ist, sensible Operationen durchzuführen. Aufgrund fehlerhafter Zugriffskontrolle könnte sie jedoch von böswilligen Akteuren ausgenutzt werden, die die Argumente (z.B. den key und die params) manipulieren, was zu unerwartetem Verhalten führt. Zum Beispiel könnte der afterSwap-Callback unter der Annahme operieren, dass der Swap bereits im PoolManager stattgefunden hat. Anschließend könnte er Aktionen einleiten, um wesentliche Zustandsinformationen aufzuzeichnen, wie den aktuellen Preis oder gesammelte Swap-Gebühren. Wenn afterSwap seine Aufrufe jedoch nicht strikt auf den PoolManager beschränkt, könnten böswillige Akteure den params-Parameter fälschen, was zu verzerrten aufgezeichneten Zuständen führt.

Exploit & PoC

Der Einfachheit halber verwenden wir einen grundlegenden PoC, um dieses Zugriffskontrollproblem zu veranschaulichen. Im Allgemeinen akzeptiert der beforeInitialize-Hook einen Parameter vom Typ PoolKey, der die Adresse dieses Hooks in seinem hooks-Feld enthalten muss (da der PoolManager dieses Feld verwendet, um die aufzurufende Hook-Adresse zu bestimmen).

Der Screenshot zeigt einen PoC, der die Ausnutzung eines Hooks mit fehlerhafter Zugriffskontrolle demonstriert, wie in DiamondHookPoC [3] zu sehen. Da die beforeInitialize-Callback-Funktion keine Zugriffsbeschränkungen aufweist, können böswillige Akteure einen beliebigen poolKey an diese Funktion übergeben. Der Hook überprüft nicht, ob der Hook dieses poolKey mit der aktuellen Hook-Adresse übereinstimmt.

Abbildung 2: PoolKey.hooks kann auf eine Null-Adresse gesetzt werden beforeInitialize_poolKey_no_hooks_validation.webp

Auch wenn der Exploit in diesem Szenario möglicherweise keine finanziellen Verluste für den Hook verursacht, verdeutlicht er dennoch eindrucksvoll, wie der Zustand des Hooks durch ungeschützte Callback-Funktionen manipuliert werden kann.

Mitigationsmaßnahmen

Um die Sicherheit der Hook-PoolManager-Interaktionen zu gewährleisten, sollten sowohl die Hook-Callbacks als auch der Lock-Callback ihren Zugang ausschließlich auf den PoolManager beschränken.

Glücklicherweise bietet Uniswap v4 Best Practices über den BaseHook in seinem v4-periphery-Repository[4]. Der BaseHook stellt den poolManagerOnly-Modifier bereit, um Aufrufe strikt auf den PoolManager zu beschränken:

    /// @dev Only the pool manager may call this function
    modifier poolManagerOnly() {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        _;
    }

Dieser Modifier kann effektiv eingesetzt werden, um eine ordnungsgemäße Zugriffskontrolle für die sensiblen Hook- und Lock-Callbacks durchzusetzen.

Andererseits erfordert das Vorhandensein der Hook-Internal-Interaktionen, dass alle wichtigen zustandsändernden Funktionen, die über den lockAcquired-Callback aufgerufen werden, wie vom BaseHook vorgegeben, nicht beliebig aufrufbar sein sollten.

Um diese Anforderung zu erfüllen, bietet der BaseHook einen selfOnly-Modifier. Dieser Modifier beschränkt die Zugänglichkeit der deklarierten Funktion auf den Hook selbst und verhindert, dass externe Contracts diese sensiblen Funktionen direkt für böswillige Zwecke aufrufen.

    /// @dev Only this address may call this function
    modifier selfOnly() {
        if (msg.sender != address(this)) revert NotSelf();
        _;
    }

Zusammenfassend können benutzerdefinierte Hooks durch die Vererbung von BaseHook diese eingebauten Zugriffskontroll-Modifier und -Callbacks nutzen, um eine ordnungsgemäße Zugriffskontrolle durchzusetzen.

Unzureichende Eingabevalidierung

Der BaseHook in v4-periphery[4] bietet eine Lösung für sicherere Interaktionslogik, die Hook-Entwickler nutzen können. Wir beobachten jedoch weiterhin Fälle unsachgemäßer Verwendung, die neue Angriffsvektoren in bestehenden Hooks eröffnen.

Standardmäßig erlauben Hooks jedem Pool, sich über die initialize-Funktion im PoolManager zu registrieren. Wenn ein Hook jedoch die zugrunde liegenden Assets im registrierenden Pool nicht validiert, könnten böswillige Nutzer einen Pool mit gefälschten Token registrieren, was es ihnen ermöglicht, über die transfer-Funktion der Token erneut in den Hook einzutreten.

Diese Schwachstelle ist subtil, da der Hook selbst möglicherweise keine böswillige Logik ausführt. Wenn der Hook jedoch den PoolManager aufruft, könnten die Interaktionen zwischen dem PoolManager und den zugrunde liegenden Assets eines böswilligen Pools den Kontrollfluss über die take-Funktion im PoolManager an einen Angreifer übergeben.

    /// @inheritdoc IPoolManager
    function take(Currency currency, address to, uint256 amount) external override 
noDelegateCall onlyByLocker {
        _accountDelta(currency, amount.toInt128());
        reservesOf[currency] -= amount;
        currency.transfer(to, amount);
    }

Im Wesentlichen resultiert die Schwachstelle aus unzureichenden Validierungen des registrierten Pools, mit dem Hook-Nutzer interagieren möchten. Wir werden diese Schwachstelle anhand eines konkreten Beispiels vertiefen und mögliche Mitigationsstrategien diskutieren.

Schwachstellenanalyse

Take Profits Hook[5] ist ein vom Awesome Uniswap v4 Hooks aufgelisteter Hook:

In diesem Beispiel erstellen wir einen Hook, der es Nutzern ermöglicht, 'Take-Profit'-Positionen zu platzieren. Beispielsweise könnten Sie in einem ETH/DAI-Pool, wenn derzeit 1 ETH = 1500 DAI, eine Take-Profit-Order als "verkaufe all mein ETH wenn 1 ETH = 2000 DAI" platzieren, die automatisch ausgeführt wird.

Betrachten wir die _handleSwap-Funktion in diesem Hook. Diese Funktion führt einen Swap durch, um Take-Profit-Orders auszufüllen, nachdem eine Sperre erworben wurde.

Abbildung 3: Die _handleSwap-Funktion des Take Profits Hook[5]
Abbildung 3: Die _handleSwap-Funktion des Take Profits Hook[5]

Sie werden bemerken, dass diese Funktion durch keinen Zugriffskontroll-Modifier geschützt ist. Zeile 250 schränkt den Zugang jedoch effektiv so ein, dass diese Funktion nur aufgerufen werden kann, nachdem eine Sperre vom PoolManager erworben wurde. Andernfalls würde poolManager.swap fehlschlagen, da der Operator nicht der neueste Locker wäre. Mit anderen Worten, _handleSwap muss in einer bestimmten Reihenfolge aufgerufen werden, vorausgesetzt, die registrierten Pools werden validiert. Leider implementiert der Hook eine solche Validierung nicht.

Aufgrund dieser fehlerhaften Implementierung ist der Hook anfällig für einen Reentrancy-Angriff. Diese Schwachstelle könnte Angreifern ermöglichen, beliebige Swaps mit von Nutzern hinterlegten Mitteln zu erzwingen.

Exploit & PoC

Konkret kann der Angriff durch folgende Schritte durchgeführt werden:

  1. Der Angreifer registriert einen böswilligen Pool mit gefälschten Token und gibt den Take Profits Hook als Hook des Pools an.
  2. Der Angreifer platziert eine Stop-Profit-Order im böswilligen Pool über den Hook.
  3. Der Angreifer führt einen Swap im böswilligen Pool durch, der den fillOrder-Aufruf im afterSwap-Callback auslöst, um die Stop-Profit-Order des Angreifers zu erfüllen.
  4. Der Hook ruft die lock-Funktion des PoolManagers auf, um eine Sperre anzufordern, und ruft die _handleSwap-Funktion im lockAcquired-Callback auf.
  5. In der _handleSwap-Funktion lösen die Token-Übertragungen böswillige Logik im gefälschten Token-Contract aus, der die _handleSwap-Funktion erneut betritt. Dies ist möglich, da _handleSwap eine externe Funktion ohne Zugriffsbeschränkungen ist. Da die Sperre bereits erworben wurde, kann der Angreifer den Hook zwingen, beliebige Swaps in jedem Pool auszuführen, solange der Hook ausreichend zugrunde liegende Assets hält. Der Angreifer kann dann die Swaps sandwichen, um auf Kosten anderer Nutzer Gewinne zu erzielen.

Das folgende detaillierte Diagramm veranschaulicht den Ablauf des Angriffs.

Abbildung 4: Der Angriffsablauf
Abbildung 4: Der Angriffsablauf

Wie bereits erwähnt, führt der Hook selbst keine böswillige Logik aus. Der einzige Fehler ist, dass der Hook nicht verhindert, dass nicht vertrauenswürdige Token-Pools sich im PoolManager-Contract registrieren. Indirekt wird die böswillige Logik im gefälschten Token-Contract über Token-Transfer-Operationen aufgerufen, was ebenfalls eine Art nicht vertrauenswürdiger externer Aufruf ist.

Mitigationsmaßnahmen

Es gibt drei praktikable Ansätze zur Minderung potenzieller Angriffe aufgrund unzureichender Eingabevalidierung:

  • Ordnungsgemäße Zugriffskontrolle. Durch die Nutzung der Bausteine aus dem BaseHook kann ein Hook die Funktionszugänglichkeit streng verwalten. Dies verhindert, dass beliebige Accounts sensible Funktionen aufrufen.

  • Reentrancy-Sperre. Im obigen Angriffsszenario kann dieser Ansatz zweifellos verhindern, dass die böswillige Token-Logik die sensiblen Funktionen erneut betritt. In einigen Fällen erfordert das Hook-Design jedoch, dass der Hook selbst wieder eingetreten werden kann. Konkret sollte ein Hook, wenn er Pool-Aktionen ausführen muss, dem PoolManager erlauben, seine Callbacks erneut zu betreten, um diese Aktionen abzuschließen. Eine Reentrancy-Sperre könnte diese beabsichtigte Funktionalität beeinträchtigen.

  • Whitelisting-Ansatz. Dies würde einen privilegierten Administrator erfordern, der genehmigte Pools in den Hooks auf eine Whitelist setzt. Der Administrator stellt sicher, dass auf der Whitelist befindliche Pools keine potenziellen Risiken einführen. Die Einschränkung besteht jedoch darin, dass Hook-Nutzer Operationen nur auf einer begrenzten Anzahl von vom Administrator genehmigten Pools über den Hook ausführen könnten. Obwohl der Whitelisting-Ansatz die Sicherheit verbessert, schränkt er die Funktionalität des Hooks erheblich ein.

Es ist schwierig, eine perfekte Lösung zu finden, die Sicherheit und Benutzerfreundlichkeit für Hooks ausbalanciert. Während wir mehrere Mitigationsansätze diskutieren, müssen Entwickler die Kompromisse in ihrem Hook-Design sorgfältig abwägen. Das Ziel sollte es sein, potenzielle Risiken so weit wie möglich zu mindern und dabei die beabsichtigte Funktionalität beizubehalten. Darüber hinaus behandelt unsere Diskussion nur Schwachstellen, die in den speziell auf Uniswap v4-Funktionen bezogenen Interaktionen liegen können. Praktische Anwendungen werden zweifellos umfassender sein. Stellen Sie immer sicher, dass Sie jede Zeile Ihrer Contracts verstehen, und bleiben Sie SAFU!

Fazit

In diesem Artikel untersuchen wir die Schwachstellen, die bei der Hook-Interaktionslogik auftreten, mit besonderem Fokus auf zwei Szenarien: fehlerhafte Zugriffskontrolle und unzureichende Eingabevalidierung. Wir präsentieren eine detaillierte Schwachstellenanalyse, veranschaulichen potenzielle Ausnutzungen zusammen mit ihren PoCs und diskutieren mögliche Mitigationsstrategien. Wir glauben, dass diese Erkenntnisse zur sicheren Entwicklung und Nutzung der Hooks beitragen und künftige Bemühungen zur Schwachstellenerkennung leiten können.

Referenzen

[1] Awesome Uniswap v4 Hooks

[2] Stop Loss Order

[3] DiamondHookPoC

[4] v4-periphery

[5] Take Profits

Weitere Artikel in dieser Serie

Best Security Auditor for Web3

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

BlockSec Audit