În primul rând, vă voi ajuta să înțelegeți într-un mod simplu ce este un atac de reintrare și cum îl puteți preveni, iar apoi, voi aprofunda în exemple de cod pentru a arăta unde sunt vulnerabilitățile, care ar fi codul atacatorului. și, cel mai important, vă voi arăta cele mai recente metode verificate pentru a proteja nu numai unul, ci toate contractele inteligente din proiectul dumneavoastră.

Spoiler: Dacă ați auzit deja despre modificatorul nonReentrant(), continuați să citiți pentru că sunteți pe cale să descoperiți câteva rânduri sub modificatorul globalNonReentrant() și modelul verificări-efecte-interacțiuni.



În imaginea de mai sus avem ContractA și ContractB. Acum, după cum știți, un contract inteligent poate interacționa cu un alt contract inteligent, ca în acest caz, ContractA poate apela ContractB. Așadar, ideea de bază a reintrării este că ContractB poate apela înapoi la ContractA în timp ce ContractA încă se execută.

Deci, cum poate atacatorul să folosească asta?

Mai sus avem ContractA care are 10 Eteri și vedem că ContractB a stocat 1 Ether în ContractA. În acest caz, ContractB va putea să utilizeze funcția de retragere din ContractA și să-și trimită Ether înapoi pe măsură ce trece de verificarea în care soldul său este mai mare de 0, pentru a avea apoi soldul total modificat la 0.



Să vedem acum cum poate ContractB să folosească reintrarea pentru a exploata funcția de retragere și a fura toți Eterii din ContractA. Practic, atacatorul va avea nevoie de două funcții: attack() și fallback().

În Solidity, o funcție de rezervă este o funcție externă fără nume, parametri sau valori returnate. Oricine poate apela o funcție de rezervă prin: Apelând o funcție care nu există în contract; Apelarea unei funcții fără a transmite datele necesare; Trimiterea Ether fără date la contract.

Modul în care funcționează reintrarea (să urmăm săgețile pas cu pas) este cu atacatorul apelând funcția attack() care în interior apelează funcția withdraw() din ContractA. În interiorul funcției, va verifica dacă soldul Contractului B este mai mare decât 0 și dacă da va continua execuția.



Întrucât soldul ContractB este mai mare decât 0, acesta trimite acel 1 Eter înapoi și declanșează funcția de rezervă. Observați că în acest moment ContractA are 9 Eteri și ContractB are deja 1 Ether.



Apoi, când funcția de rezervă este executată, declanșează din nou funcția de retragere a ContractA, verificând din nou dacă soldul Contractului B este mai mare de 0. Dacă verificați din nou imaginea de mai sus, veți observa că soldul său este încă 1 Eter.



Asta înseamnă că verificarea trece și trimite un alt Ether către ContractB, care declanșează funcția de rezervă. Observați că, deoarece linia în care avem „balance=0” nu este niciodată executată, aceasta va continua până când tot Ether din ContractA va dispărea.

___________

Să aruncăm o privire acum la un contract inteligent în care putem identifica reintrarea cu codul Solidity.



În contractul EtherStore, avem funcția deposit() care stochează și actualizează soldurile expeditorului și apoi funcția withdrawAll() care va prelua toate soldurile stocate simultan. Vă rugăm să observați implementarea withdrawAll() în cazul în care verifică mai întâi cu cerința că soldul este mai mare decât 0 și imediat după trimite Ether-ul, din nou, lăsând pentru sfârșit actualizarea soldului expeditorului la 0.



Aici avem contractul Attack care va folosi reintranța pentru a se scurge contractul EtherStore. Să analizăm codul acestuia:

  • În constructorul său, atacatorul va trece adresa EtherStore pentru a crea o instanță și astfel a putea folosi funcțiile acesteia.

  • Acolo vedem funcția fallback() care va fi apelată atunci când EtherStore trimite Ether la acest contract. În interior, se va apela retragere din EtherStore atâta timp cât soldul este egal sau mai mare decât 1.

  • Și în cadrul funcției atack() avem logica care va exploata EtherStore. După cum vedem, mai întâi vom iniția atacul asigurându-ne că avem suficient eter, apoi depunem 1 eter pentru a avea un sold mai mare de 0 în EtherStore și, prin urmare, trecem verificările înainte de a începe retragerea.

Am explicat mai sus în exemplul ContractA și ContractB pas cu pas cum va rula codul, așa că acum, să facem un rezumat despre cum va fi. Mai întâi de toate, atacatorul va apela attack(), care în interior va apela withdrawAll() din EtherStore, care apoi va trimite funcția de rezervă a contractului Ether to Attack. Și acolo va începe reintrarea și va scurge echilibrul EtherStore.

Deci, cum ne putem proteja contractele de atacurile de reintrare?

Vă voi arăta trei tehnici de prevenire pentru a le proteja pe deplin. Voi vorbi despre cum să preveniți reintrarea într-o singură funcție, reintrența interfuncțională și reintrerea încrucișată.



Prima tehnică de a proteja o singură funcție este folosirea unui modificator numit noReentrant.

Un modificator este un tip special de funcție pe care îl utilizați pentru a modifica comportamentul altor funcții. Modificatorii vă permit să adăugați condiții sau funcționalități suplimentare unei funcții fără a fi nevoie să rescrieți întreaga funcție.

Ceea ce facem aici este bloca contractul în timp ce funcția este executată. În acest fel, nu va putea reintroduce singura funcție, deoarece va trebui să treacă prin codul funcției și apoi să schimbe variabila de stare blocat la false pentru a trece din nou verificarea efectuată în require.

___________



A doua tehnică constă în folosirea modelului Verificări-Efecte-Interacțiuni care ne va proteja contractele de reintrarea între funcții. Puteți observa în contractul EtherStore actualizat de mai sus ce s-a schimbat?

Pentru a explora modelul Check-Effects-Interaction, vă recomand să citiți https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html





Mai sus vedem comparația dintre codul vulnerabil din imaginea din stânga în care soldul a fost actualizat după trimiterea Etherului, care, așa cum s-a văzut mai sus, nu ar putea fi atins niciodată, și în dreapta ceea ce s-a făcut este să muți balanțele[ msg.sender] = 0 (sau efect) imediat după require(bal > 0) (bifare), dar înainte de a trimite eter (interacțiune).

În acest fel, ne vom asigura că, chiar dacă o altă funcție accesează withdrawAll(), acest contract va fi protejat de atacator, deoarece soldul va fi întotdeauna actualizat înainte de a trimite Ether-ul.

Model creat de https://twitter.com/GMX_IO

A treia tehnică pe care o voi arăta este crearea contractului GlobalReentrancyGuard pentru a vă proteja de reintrarea prin contracte încrucișate. Este important să înțelegeți că acest lucru este aplicabil proiectelor cu mai multe contracte care interacționează între ele.

Ideea aici este aceeași ca și în modificatorul noReentrant pe care l-am explicat în prima tehnică, intră în modificator, actualizează o variabilă pentru a bloca contractul și nu o deblochează până când nu termină codul. Marea diferență aici este că folosim o variabilă stocată într-un contract separat care este folosită ca loc pentru a verifica dacă funcția a fost introdusă sau nu.







Am creat aici un exemplu fără cod real și doar cu nume de funcții pentru a înțelege ideea, deoarece, din experiența mea, poate ajuta la vizualizarea situației mai mult decât doar scrierea cu cuvinte.

Aici, atacatorul ar apela funcția din contractul ScheduledTransfer care, după îndeplinirea condițiilor, ar trimite Etherul specificat la contractul AttackTransfer, care, prin urmare, ar intra în funcția de rezervă și, prin urmare, ar „anula” tranzacția din punctul contractului ScheduledTransfer. vedeți și totuși primiți Eterul. Și în acest fel ar fi început o privire până la golirea tuturor Eterilor din ScheduledTransfer.

Ei bine, folosind GlobalReentrancyGuard am menționat mai sus, va evita un astfel de scenariu de atac.

__________________

Twitter @TheBlockChainer pentru a găsi mai multe actualizări zilnice despre Smart Contracts, Web3 Security, Solidity, Auditing smart contracts și multe altele.

__________________