Salt la conținutul principal
Change page

Securitatea contractelor inteligente

Ultima modificare: @nicklcanada(opens in a new tab), 15 august 2023

Contractele inteligente Ethereum sunt extrem de flexibile, capabile atât să dețină cantități mari de tokenuri (de multe ori peste 1 miliard USD), cât și să ruleze o logică imuabilă pe baza codului de contract inteligent implementat anterior. Pe de-o parte, acest lucru a creat un ecosistem vibrant și creativ de contracte inteligente ce nu necesită autorizarea, interconectate, dar este şi ecosistemul perfect pentru a atrage atacatorii care doresc să profite prin exploatarea vulnerabilității din contractele inteligente și a comportamentului neașteptat în Ethereum. Codul de contract inteligent, de obicei nu poate fi schimbat ca să fie remediate defectele de securitate; activele care au fost furate din contracte inteligente sunt irecuperabile, iar activele furate sunt extrem de dificil de urmărit. The total of amount of value stolen or lost due to smart contract issues is easily over $1B. Unele dintre cele mai mari pierderi din cauza erorilor de programare a contractelor inteligente includ:

Condiții prealabile

Se va vorbi despre securitatea contractelor inteligente, de aceea aveţi grijă să vă familiarizaţi cu contractele inteligente înainte de a aborda securitatea.

Cum să scrieţi coduri de contracte inteligente mai securizate

Înainte de a lansa orice cod pe Mainnet, este important să luaţi suficiente măsuri de precauție pentru a proteja lucrurile de valoare încredințate contractului dvs. inteligent. În acest articol vom discuta despre câteva atacuri specifice, vă vom oferi resurse pentru a afla mai multe despre diferite tipuri de atacuri și vă vom furniza câteva instrumente de bază și cele mai bune practici pentru a vă garanta funcționarea corectă a contractelor dvs.

Auditurile nu sunt o soluție perfectă

Cu ani înainte, instrumentele pentru scrierea, compilarea, testarea și implementarea contractelor inteligente erau foarte imature, permițând multor proiecte să scrie codul Solidity în moduri întâmplătoare, să îl arunce peste „gard” către un auditor care ar investiga codul pentru a se asigura că acesta funcționează securizat și conform aşteptărilor. În 2020, procesele de dezvoltare și instrumentele care acceptă scrierea Solidity sunt mult perfecţionate; valorificarea acestor bune practici nu numai că garantează facilitatea de gestionare a proiectuliu dvs., ci este o parte vitală a securității lui. Nu mai este suficient să auditaţi contractul inteligent când l-aţi terminat de scris ca singur aspect de care să ţineţi cont privind securitatea proiectului dvs. Securitatea începe înainte de a scrie prima linie de cod de contract inteligent, securitatea începe cu procese de proiectare și dezvoltare corespunzătoare.

Procesul de dezvoltare a contractelor Inteligente

Cerințe minime:

  • Toate codurile să fie stocate într-un sistem de control al versiunii, cum ar fi Git
  • Toate modificările de cod să fie efectuate prin Pull Request-uri
  • Toate Solicitările Pull să aibă cel puțin un examinator. Dacă aveţi un proiect solo, gândiţi-vă să găsiţi un alt autor solo și negociaţi cu el recenzii de coduri!
  • Să existe o singură comandă care să compileze, să implementeze și să rulează o suită de teste cu codul dvs. utilizând un mediu Ethereum de dezvoltare (vedeţi: Truffle)
  • Să rulaţi codul prin instrumente de analiză a codului de bază, cum ar fi Mythril și Slither, în mod ideal înainte ca fiecare Pull Request să fie acceptat, comparând diferențele rezultatelor
  • Solidity să nu emită NICIUN avertisment al compilatorului
  • Codul dvs. să fie bine documentat

Sunt mult mai multe de spus despre procesul de dezvoltare, dar este bine să începem cu aceste elemente. Pentru a afla mai multe elemente și explicații detaliate, consultaţi lista de verificare a calității procesului furnizată de DeFiSafety(opens in a new tab). DefiSafety(opens in a new tab) este un serviciu public neoficial care publică recenzii despre diverse aplicații mari dApp de pe Ethereum publice. Un criteriu al sistemului de evaluare DeFiSafety este cât de bine aderă proiectul la această listă de verificare a calității procesului. Urmând aceste procese:

  • Veţi produce un cod mai securizat, prin teste automate, reproductibile
  • Veţi permite revizuirea proiectului în mod mai eficient de către auditori
  • O mai ușoară integrare a noilor dezvoltatori
  • Veţi permite dezvoltatorilor să itereze rapid, să testeze și să obțină feedback despre modificări
  • Va fi mai puţin probabil ca proiectul dvs. să regreseze

Atacuri și vulnerabilități

După ce aţi aflat cum să scrieţi cod Solidity folosind un proces de dezvoltare eficient, să analizăm câteva vulnerabilități comune din Solidity pentru a vedea unde se poate poticni.

Re-intrare

Re-intrarea este una dintre cele mai mari și mai semnificative probleme de securitate de luat în considerare atunci când se dezvoltă contracte inteligente. Deși EVM nu poate executa mai multe contracte simultan, un contract care apelează un alt contract întrerupe executarea contractului de apelare și starea memoriei până când apelul revine, moment în care executarea se desfășoară în mod normal. Această întrerupere și re-pornire poate crea o vulnerabilitate cunoscută sub numele de „re-intrare”.

Iată o versiune simplă a unui contract care este vulnerabil la re-intrare:

1// ACEST CONTRACT ARE VULNERABILITATE INTENȚIONATĂ, NU COPIA
2contract Victim {
3 mapping (address => uint256) public balances;
4
5 function deposit() external payable {
6 balances[msg.sender] += msg.value;
7 }
8
9 function withdraw() external {
10 uint256 amount = balances[msg.sender];
11 (bool success, ) = msg.sender.call.value(amount)("");
12 require(success);
13 balances[msg.sender] = 0;
14 }
15}
Afișează tot
Copiați

Pentru a permite unui utilizator să retragă ETH-ul stocat anterior în contract, această funcție

  1. Citește valoarea soldului unui utilizator
  2. Îi trimite valoarea soldului în ETH
  3. Resetează soldul la 0, deci nu își mai poate retrage din nou soldul.

If called from a regular account (such as your own MetaMask account), this functions as expected: msg.sender.call.value() simply sends your account ETH. Cu toate acestea, contractele inteligente pot efectua și ele apeluri. Dacă un contract personalizat rău intenționat este cel care apelează withdraw(), msg.sender.call.value() nu numai că va trimite amount de ETH, dar va apela implicit și contractul pentru a începe executarea codului. Imaginaţi-vă acest contract răuvoitor:

1contract Attacker {
2 function beginAttack() external payable {
3 Victim(VICTIM_ADDRESS).deposit.value(1 ether)();
4 Victim(VICTIM_ADDRESS).withdraw();
5 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(VICTIM_ADDRESS).withdraw();
10 }
11 }
12}
Afișează tot
Copiați

Apelarea Attacker.beginAttack() va începe un ciclu care arată cam aşa:

10.) Atacatorul EOA apelează Attacker.beginAttack() cu 1 ETH
20.) Attacker.beginAttack() depozitează 1 ETH în Victimă
3
4 1.) Atacant -> Victim.withdraw()
5 1.) Victim reads balances[msg.sender]
6 1.) Victima trimite ETH Atacantului (care execută funcția implicită)
7 2.) Atacant -> Victim.withdraw()
8 2.) Victim reads balances[msg.sender]
9 2.) Victima trimite ETH Atacantului (care execută funcția implicită)
10 3.) Atacant -> Victim.withdraw()
11 3.) Victim reads balances[msg.sender]
12 3.) Victima trimite ETH Atacantului (care execută funcția implicită)
13 4.) Atacantul nu mai are suficient gaz, se întoarce fără să apeleze din nou
14 3.) balances[msg.sender] = 0;
15 2.) balances[msg.sender] = 0; (a fost deja 0)
16 1.) balances[msg.sender] = 0; (a fost deja 0)
Afișează tot

Apelarea Attacker.beginAttack cu 1 ETH va ataca prin re-intrare Victima, retrăgând mai mult ETH decât a furnizat (luat din soldurile altor utilizatori, iar din această cauză contractul Victimă ajunge să fie sub-garantat)

Cum să abordaţi re-intrarea (modul greșit)

Aţi putea lua în calcul interziceţi re-intrarea împiedicând orice contract inteligent să interacționeze cu codul dvs. Când căutaţi stackoverflow, descoperiţi că acest fragment de cod (snippet) are tone de voturi pozitive:

1function isContract(address addr) internal returns (bool) {
2 uint size;
3 assembly { size := extcodesize(addr) }
4 return size > 0;
5}
Copiați

Pare să aibă sens: contractele au cod, iar dacă apelantul are vreun cod, nu i se permite să depună. Haideţi să îl adăugăm:

1// ACEST CONTRACT ARE VULNERABILITATE INTENȚIONATĂ, NU COPIA
2contract ContractCheckVictim {
3 mapping (address => uint256) public balances;
4
5 function isContract(address addr) internal returns (bool) {
6 uint size;
7 assembly { size := extcodesize(addr) }
8 return size > 0;
9 }
10
11 function deposit() external payable {
12 require(!isContract(msg.sender)); // <- LINIE NOUĂ
13 balances[msg.sender] += msg.value;
14 }
15
16 function withdraw() external {
17 uint256 amount = balances[msg.sender];
18 (bool success, ) = msg.sender.call.value(amount)("");
19 require(success);
20 balances[msg.sender] = 0;
21 }
22}
Afișează tot
Copiați

Acum nu trebuie să aveţi codul de contract inteligent în adresa dvs. pentru a depune ETH. Totuși, se poate depăşi această situaţie cu următorul contract Atacator:

1contract ContractCheckAttacker {
2 constructor() public payable {
3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- Linie nouă
4 }
5
6 function beginAttack() external payable {
7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();
8 }
9
10 function() external payable {
11 if (gasleft() > 40000) {
12 Victim(VICTIM_ADDRESS).withdraw();
13 }
14 }
15}
Afișează tot
Copiați

În timp ce primul atac a fost un atac asupra logicii contractelor, acesta este un atac asupra comportamentului de implementare a contractului Ethereum. În timpul construcției, un contract nu a răspuns prin codul său încă pentru a fi implementat la adresa sa, dar păstrează controlul complet EVM ÎN TIMPUL acestui proces.

Din punct de vedere tehnic, este posibil să împiedicaţi contractele inteligente să vă apeleze codul utilizând această linie:

1require(tx.origin == msg.sender)
Copiați

Totuși, aceasta nu este încă o soluție bună. Unul dintre cele mai interesante aspecte ale lui Ethereum este combinabilitatea: contractele inteligente se integrează unul cu altul și se construiesc bizuindu-se unul pe celălalt. Prin utilizarea liniei de mai sus, vă limitaţi utilitatea proiectului.

Cum să abordaţi re-intrarea (modul corect)

Prin simpla comutare a ordinii actualizării de stocare și a apelului extern, eliminăm condiția de re-intrare care a permis atacul. Apelarea înapoi la „withdraw” (retragere) cât timp este încă posibil nu va avantaja atacantul, deoarece soldul stocării va fi deja setat la 0.

1contract NoLongerAVictim {
2 function withdraw() external {
3 uint256 amount = balances[msg.sender];
4 balances[msg.sender] = 0;
5 (bool success, ) = msg.sender.call.value(amount)("");
6 require(success);
7 }
8}
Copiați

Codul de mai sus urmează modelul de concept „Verificări-Efecte-Interacțiuni”, care ajută la protejarea împotriva re-intrării. Puteţi citi mai multe despre Verificări-Efecte-Interacțiuni aici(opens in a new tab)

Cum să abordaţi re-intrarea (opțiunea nucleară)

De fiecare dată când trimiteți ETH la o adresă care nu este de încredere sau interacționaţi cu un contract necunoscut (cum ar fi apelarea transfer() a unei adrese de token furnizate de utilizator), faceţi loc chiar dvs. posibilităţii de re-intrare. Proiectând contracte care nici nu trimit ETH, și nici nu apelează contracte nesigure, eliminaţi posibilitatea re-intrării!

Mai multe tipuri de atacuri

Tipurile de atac de mai sus tratează problemele de programare a contractelor inteligente (re-intrarea) și ciudățeniile din Ethereum (executarea codului în interiorul constructorilor de contracte înainte ca acest cod să fie disponibil la adresa contractului). Există multe, multe alte tipuri de atacuri despre care trebuie să știţi, cum ar fi:

  • Front-running
  • Refuzul de a trimite ETH
  • Depăşiri/sub-depăşiri de numere întregi

Referințe suplimentare:

Instrumente de securitate

Deși este de neînlocuit înțelegerea elementelor de bază ale securității Ethereum și angajarea unei firme de audit profesionale pentru a vă examina codul, există multe instrumente disponibile pentru a evidenția problemele potențiale cu codul dvs.

Securitatea contractelor inteligente

Slither - framework de analiză statică Solidity scris în Python 3.

MythX - API de analiză de securitate pentru contractele inteligente Ethereum.

Mythril - instrument de analiză de securitate pentru bytecode-ul EVM.

Manticore - o interfață tip linie de comandă care utilizează un instrument de execuție simbolică pe contracte inteligente și binare.

Securify - scanner de securitate pentru contractele inteligente Ethereum

ERC20 Verifier - un instrument de verificare folosit pentru a controla conformitatea unui contract cu standardul ERC20.

Verificare formală

Informații despre verificarea formală

Folosirea instrumentelor

Două dintre cele mai populare instrumente pentru analiza securității contractelor inteligente sunt:

Ambele sunt instrumente utile care vă analizează codul și raportează probleme. Fiecare are o versiune [commercial] găzduită, dar și o versiune gratuită pentru a rula local. Urmează un exemplu rapid al modalităţii de rulare a Slither, care este disponibil într-o imagine Docker convenabilă trailofbits/eth-security-toolbox. Va trebui să instalaţi Docker dacă nu l-aţi instalat deja(opens in a new tab).

$ mkdir test-slither
$ curl https://gist.githubusercontent.com/epheph/460e6ff4f02c4ac582794a41e1f103bf/raw/9e761af793d4414c39370f063a46a3f71686b579/gistfile1.txt > bad-contract.sol
$ docker run -v `pwd`:/share -it --rm trailofbits/eth-security-toolbox
docker$ cd /share
docker$ solc-select 0.5.11
docker$ slither bad-contract.sol

Va genera rezultatul următor:

ethsec@1435b241ca60:/share$ slither bad-contract.sol
INFO:Detectors:
Reentrancy in Victim.withdraw() (bad-contract.sol#11-16):
External calls:
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
State variables written after the call(s):
- balances[msg.sender] = 0 (bad-contract.sol#15)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities
INFO:Detectors:
Low level call in Victim.withdraw() (bad-contract.sol#11-16):
- (success) = msg.sender.call.value(amount)() (bad-contract.sol#13)
Reference: https://github.com/crytic/slither/wiki/Detector-Documentation#low-level-calls
INFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) found
INFO:Slither:Use https://crytic.io/ to get access to additional detectors and GitHub integration
Afișează tot

Slither a identificat aici potențialul de re-intrare, identificând liniile cheie în care ar putea apărea problema, oferindu-ne un link pentru mai multe detalii despre problemă:

Referință: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities(opens in a new tab)

permițându-vă să aflaţi rapid despre potențialele probleme cu codul. Ca toate instrumentele de testare automată, Slither nu este perfect și greșește, raportând prea mult. Poate avertiza despre o potențială re-intrare, chiar și atunci când nu există o vulnerabilitate exploatabilă. Adesea, revizuirea DIFERENȚEI modificării codului la rezultatele Slither este extrem de folositoare, ajutând la descoperirea de vulnerabilități introduse cu mult înainte decât aşteptând până la codificarea completă a proiectului dvs.

Referințe suplimentare

Ghid de bune practici pentru securitatea contractelor inteligente

Standardul de verificare a securității contractelor inteligente (SCSVS)

Cunoaşteţi o resursă comunitară care v-a ajutat? Editaţi această pagină și adăugaţi-o!

A fost util acest articol?