Nejprve vám pomůžu jednoduchým způsobem pochopit, co je reentrancy útok a jak mu můžete zabránit, a poté se ponořím hlouběji do příkladů kódu, abych ukázal, kde jsou zranitelnosti, jaký by byl kód útočníka a hlavně vám ukážu nejnovější ověřené metody, jak ochránit nejen jednu, ale všechny smart kontrakty ve vašem projektu.

Spoiler: Pokud jste již slyšeli o modifikátoru nonReentrant(), pokračujte ve čtení, protože se chystáte objevit pár řádků pod modifikátorem globalNonReentrant() a vzorem checks-effects-interactions.



Na obrázku výše máme ContractA a ContractB. Nyní, jak víte, inteligentní smlouva může interagovat s jinou inteligentní smlouvou, jako v tomto případě, ContractA může volat ContractB. Úplně základní myšlenkou opětovného vstupu je tedy to, že ContractB je schopen zavolat zpět do ContractA, zatímco ContractA stále probíhá.

Jak toho tedy může útočník využít?

Nahoře máme ContractA, který má 10 etherů a vidíme, že ContractB má uložen 1 ether ve ContractA. V tomto případě bude ContractB moci použít funkci stažení z ContractA a poslat Ether zpět sobě, když projde kontrolou, kde je jeho zůstatek větší než 0, aby byl jeho celkový zůstatek upraven na 0.



Pojďme se nyní podívat, jak může ContractB využít reentrancy k využití funkce stažení a ukrást všechny ethery ze ContractA. V zásadě bude útočník potřebovat dvě funkce: attack() a fallback().

V Solidity je záložní funkce externí funkce bez názvu, parametrů ani návratových hodnot. Kdokoli může zavolat záložní funkci: Zavoláním funkce, která v kontraktu neexistuje; Volání funkce bez předání požadovaných dat; Odeslání Etheru bez jakýchkoli dat do smlouvy.

Reentrancy funguje (postupme podle šipek krok za krokem) tak, že útočník zavolá funkci attack() , která uvnitř volá funkci výběr () z ContractA. Uvnitř funkce ověří, zda je zůstatek ContractB větší než 0, a pokud ano, bude pokračovat v provádění.



Vzhledem k tomu, že zůstatek ContractB je větší než 0, pošle tento 1 ether zpět a spustí záložní funkci. Všimněte si, že v tuto chvíli má ContractA 9 etherů a ContractB již 1 ether.



Poté, když se spustí záložní funkce, znovu se spustí funkce stažení ContractA, přičemž se znovu zkontroluje, zda je zůstatek ContractB větší než 0. Pokud znovu zkontrolujete obrázek výše, zjistíte, že jeho zůstatek je stále 1 ether.



To znamená, že kontrola projde a pošle další ether do ContractB, který spustí nouzovou funkci. Všimněte si, že protože řádek, kde máme „zůstatek=0“ není nikdy proveden, bude to pokračovat, dokud nezmizí veškerý éter ze smlouvy A.

___________

Podívejme se nyní na chytrou smlouvu, kde můžeme identifikovat znovuvstup pomocí kódu Solidity.



Ve smlouvě EtherStore máme funkci deposit(), která ukládá a aktualizuje zůstatky odesílatele, a poté funkci withdrawAll() , která vezme všechny uložené zůstatky najednou. Všimněte si prosím implementace withdrawAll() , kde nejprve zkontroluje požadavek, zda je zůstatek větší než 0, a hned poté, co odešle ether, znovu, přičemž na konec ponechá aktualizaci zůstatku odesílatele na 0.



Zde máme kontrakt Attack, který využije reentrancy k vyčerpání kontraktu EtherStore. Pojďme analyzovat jeho kód:

  • Ve svém konstruktoru předá útočník adresu EtherStore, aby vytvořil instanci a mohl tak používat její funkce.

  • Zde vidíme funkci fallback() , která bude volána, když EtherStore odešle Ether této smlouvě. Uvnitř to bude volání výběr z EtherStore, dokud bude zůstatek roven nebo větší než 1.

  • A uvnitř funkce attack() máme logiku, která bude využívat EtherStore. Jak vidíme, nejprve zahájíme útok tím, že se ujistíme, že máme dostatek éteru, poté vložíme 1 éter, abychom měli v EtherStore zůstatek větší než 0, a proto projdeme kontrolami před zahájením výběru.

Výše jsem vysvětlil v příkladu ContractA a ContractB krok za krokem, jak bude kód běžet, takže nyní si udělejme shrnutí, jak to bude. Nejprve útočník zavolá attack(), který uvnitř zavolá drawAll() z EtherStore, který pak pošle Ether do záložní funkce Attack contract. A tam to zahájí reentrancy a vyčerpá zůstatek EtherStore.

Jak tedy můžeme chránit naše smlouvy před reentrancy útoky?

Ukážu vám tři preventivní techniky, jak je plně chránit. Budu se zabývat tím, jak zabránit opětovnému vstupu v rámci jedné funkce, opětovnému vstupu mezi funkcemi a opakovanému vstupu do křížové smlouvy.



První technikou ochrany jedné funkce je použití modifikátoru zvaného noReentrant.

Modifikátor je speciální typ funkce, kterou používáte k úpravě chování jiných funkcí. Modifikátory vám umožňují přidat další podmínky nebo funkce k funkci, aniž byste museli přepisovat celou funkci.

My zde zamkneme smlouvu, zatímco je funkce vykonávána. Tímto způsobem nebude moci znovu zadat jedinou funkci, protože bude muset projít kódem funkce a poté změnit proměnnou stavu uzamčení na hodnotu false, aby znovu prošel kontrolou provedenou v požadavku.

___________



Druhou technikou je použití vzoru Checks-Effects-Interactions, který ochrání naše smlouvy před opakovaným vstupem mezi funkcemi. Najdete v aktualizované smlouvě EtherStore výše, co se změnilo?

Chcete-li se hlouběji ponořit do vzoru Check-Effects-Interaction, doporučuji si přečíst https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html





Nahoře vidíme srovnání mezi zranitelným kódem z obrázku vlevo, kde byl zůstatek aktualizován po odeslání etheru, který, jak je vidět výše, nemohl být potenciálně nikdy dosažen, a vpravo bylo provedeno přesunutí zůstatků[ msg.sender] = 0 (nebo účinek) hned po požadavku (bal > 0) (zaškrtnout), ale před odesláním etheru (interakce).

Tímto způsobem zajistíme, že i když jiná funkce přistupuje k withdrawAll(), bude tato smlouva chráněna před útočníkem, protože zůstatek bude vždy aktualizován před odesláním etheru.

Vzor vytvořený https://twitter.com/GMX_IO

Třetí technikou, kterou vám ukážu, je vytvoření smlouvy GlobalReentrancyGuard, která chrání před opakovaným vstupem do křížové smlouvy. Je důležité pochopit, že to platí pro projekty s více smlouvami, které se vzájemně ovlivňují.

Myšlenka je stejná jako v modifikátoru noReentrant, který jsem vysvětlil v první technice, vstoupí do modifikátoru, aktualizuje proměnnou, aby uzamkla smlouvu, a neodemkne ji, dokud nedokončí kód. Velký rozdíl je v tom, že používáme proměnnou uloženou v samostatné smlouvě, která se používá jako místo pro kontrolu, zda byla funkce zadána nebo ne.







Vytvořil jsem zde příklad bez skutečného kódu a pouze s názvy funkcí, abych porozuměl myšlence, protože z mé zkušenosti může pomoci vizualizovat situaci více než jen její psaní slovy.

Útočník by zde zavolal funkci ve smlouvě ScheduledTransfer, která by po splnění podmínek odeslala zadaný Ether do smlouvy AttackTransfer, která by tedy vstoupila do záložní funkce a „zrušila“ transakci z bodu smlouvy ScheduledTransfer. vidět a přesto přijímat Éter. A tímto způsobem by to byl začátek hledání, dokud se nevyčerpají všechny ethery z plánovaného přenosu.

Použitím GlobalReentrancyGuard, který jsem zmínil výše, se takovému scénáři útoku vyhnete.

___________________

Twitter @TheBlockChainer, kde najdete další denní aktualizace o Smart Contracts, Web3 Security, Solidity, Auditing Smart Contracts a dalších.

___________________