Okosszerződés-biztonság
Utolsó módosítás: @Satglow(opens in a new tab), 2024. május 22.
Az okosszerződések rendkívüli módon rugalmasak és képesek nagy mennyiségű értéket és adatot irányítani, miközben egy megváltoztathatatlan logika alapján, a blokkláncra telepített kód szerint futnak. Ezáltal létrejött a bizalmat nem igénylő és decentralizált alkalmazások élénk ökoszisztémája, mely számos előnyt kínál a hagyományos rendszerekkel szemben. Emellett lehetőséget is jelentenek a támadók számára, akik abból akarnak nyereséget szerezni, hogy kihasználják az okosszerződések gyenge pontjait.
A nyilvános blokkláncok, mint az Ethereum, tovább bonyolítják az okosszerződések biztosításának problémáját. A telepített szerződéskód általában nem módosítható, hogy ezzel a biztonsági kockázatokat elkerüljék, eközben az okosszerződésekből ellopott eszközöket rendkívül nehéz lekövetni és a legtöbb esetben visszaszerezhetetlenek a megváltoztathatatlanság miatt.
Bár a számok változnak, de úgy becsülik, hogy a biztonsági hibák miatt az okosszerződésből ellopott vagy onnan elvesztett értékek teljes összege könnyen meghaladhatja az 1 milliárd dollárt is. Ez magába foglal olyan nagy horderejű incidenseket is, mint amilyen a DAO-hackelés volt(opens in a new tab) (3,6 millió ETH-t loptak, ami meghaladja az 1 milliárd dollárt mai áron), Parity több aláírásos tárca hackelését(opens in a new tab) (30 millió USD-t veszett el), és a Parity befagyasztott tárcaproblémát(opens in a new tab) (300 millió USD-nyi ETH örökre elérhetetlenné vált).
Ezek az esetek kötelezővé teszik a fejlesztők számára, hogy folyamatosan azon dolgozzanak, hogy az okosszerződések biztonságosak, robusztusak és ellenállók legyenek. Az okosszerződésbiztonság komoly téma, melyet minden fejlesztőnek a maga érdekében meg kell ismerni. Ez az útmutató lefedi azokat a biztonsági megfontolásokat, amelyek az Ethereum-fejlesztőknek fontosak, és forrásokat tár fel az okosszerződésbiztonság továbbfejlesztésére.
Előfeltételek
Tisztában kell lennie az okosszerződés-fejlesztés alapjaival, mielőtt a biztonsági kérdésekkel foglalkozna.
Iránymutatások a biztonságos Ethereum-okosszerződések építéséhez
1. Tervezzen megfelelő hozzáférés-szabályozást
Az okosszerződésekben a public
(publikus) vagy external
(külső) jelölésű függvényeket bármelyik külső tulajdonú számla (EOA) vagy szerződésszámla meghívhatja. A függvényeket szükséges nyilvánossá tenni, ha Ön azt akarja, hogy mások interakcióba lépjenek a szerződésével. A private
(privát) jelölésű függvényeket csak az okosszerződésen belüli függvények hívhatják meg, külső számlák nem. Problémás lehet az összes hálózati résztvevőnek hozzáférést adni bizonyos szerződésfüggvényekhez, főleg ha így bárki végrehajthat fontos műveleteket (pl. új tokenek kibocsátása).
Ahhoz, hogy megakadályozzuk az okosszerződés függvényeinek nem hitelesített használatát, biztonságos hozzáférés-szabályozásra van szükség. A hozzáférés-szabályozás mechanizmusai az okosszerződés bizonyos függvényeinek használatát a jóváhagyott entitások csoportjára, például a szerződés kezeléséért felelős számlákra korlátozzák. A tulajdonosi minta és a szerepalapú irányítás két hasznos minta az okosszerződésben beállítható hozzáférés-szabályozásra:
Tulajdonosi minta (ownable pattern)
A tulajdonosi mintában beállítható egy cím, mint a szerződés „tulajdonosa” a szerződés létrehozása folyamán. A védett függvényekhez hozzárendelnek egy OnlyOwner
-módosítót, így a szerződés azonosítani fogja az identitását a hívást végző címnek, mielőtt végrehajtaná a függvényt. A védett függvények meghívását csak akkor engedi, ha az a szerződés tulajdonosának címéről érkezik, különben elveti azt, megakadályozva az akaratlan hozzáférést.
Szerepalapú hozzáférés-szabályozás
Ha az okosszerződésben egyetlen címet regisztrálnak, mint Owner
(tulajdonos), az a centralizáció kockázatát hordozza és felmerül az egyetlen meghibásodási pont lehetősége. Ha a tulajdonos számlakulcsa nyilvánossá válik, akkor a támadók hozzáférhetnek ehhez a tulajdonolt szerződéshez. Emiatt jobb opció lehet a szerepalapú hozzáférés-szabályozás mintája, ahol több adminisztratív számla van.
A szerepalapú hozzáférés-szabályozásban a fontos függvényekhez való hozzáférést elosztják a megbízott résztvevők között. Például az egyik számla felel a tokenek kibocsátásáért, miközben egy másik számla frissítéseket végez vagy megállítja a szerződést. A decentralizált hozzáférés-szabályozás ily módon kivédi az egyetlen meghibásodási pont lehetőségét és csökkenti a felhasználók részéről igényelt bizalmat.
Több aláírásos tárca használata
A biztonságos hozzáférés-szabályozásra egy másik megközelítés a több aláírásos számla használata, ami a szerződést kezeli. Az általános külső tulajdonú számlához (EOA) képest a több aláírásos számlákat több entitás birtokolja, és a tranzakciók végrehajtásához egy adott számú aláírásra van szükség, például 5-ből 3-ra.
Ennek használata egy újabb biztonsági réteget vezet be, mivel a szerződésen végrehajtandó akciókba több félnek is bele kell egyeznie. Ez különösen hasznos, ha a tulajdonosi mintát (ownable pattern) kell használni, mert még nehezebb a támadó vagy egy rosszhiszemű belső fél számára, hogy rossz célokra használja fel a fontos szerződésfüggvényeket.
2. Használja a require(), assert() és revert() parancsokat, hogy óvja a szerződés működését
Amint az okosszerződés telepítésre kerül a blokkláncon, bárki meg tudja hívni a benne lévő publikus függvényeket. Mivel nem lehet tudni előre, hogy a külső tulajdonú számlák hogyan fognak interakciókat folytatni a szerződéssel, ezért ideális esetben belső óvintézkedéseket kell tenni a problémás működésekkel kapcsolatban a telepítés előtt. Az okosszerződésben elő lehet írni a megfelelő viselkedést a require()
, assert()
és revert()
parancsokkal, hogy ha bizonyos feltételek nem teljesülnek, akkor leálljon és visszaforgassa a változásokat.
require()
: a require
(szükséges) parancsot a függvények elején kell meghatározni, és biztosítja, hogy a megadott feltételek teljesülnek, mielőtt a függvény végrehajtásra kerül. A require
parancs révén validálni lehet a felhasználó által adott adatokat, ellenőrizhetők az állapotváltozók, vagy hitelesíteni lehet a meghívó számla identitását, mielőtt a függvény elindulna.
assert()
: az assert()
(állítás) parancsot a belső hibák felderítésére használják, illetve a kódban lévő „konstansok” megsértését ellenőrzik ezáltal. A konstans egy logikai állítás a szerződés státuszáról, amelynek teljesülnie kell minden függvénymeghívás esetén. Például egy tokenszerződés maximális teljes kínálata vagy egyenlege. Az assert()
használata biztosítja, hogy a szerződés nem kerül sebezhető státuszba, és ha mégis, akkor az állapotváltozók visszaállnak a korábbi értékekre.
revert()
: a revert()
(visszatér) kódot egy if-else parancsban használhatjuk, ami egy kivételt ad, ha a szükséges feltételek nem teljesülnek. Az alábbi példaszerződés a revert()
kódot arra használja, hogy védje a függvények végrehajtását:
1pragma solidity ^0.8.4;23contract VendingMachine {4 address owner;5 error Unauthorized();6 function buy(uint amount) public payable {7 if (amount > msg.value / 2 ether)8 revert("Not enough Ether provided.");9 // Perform the purchase.10 }11 function withdraw() public {12 if (msg.sender != owner)13 revert Unauthorized();1415 payable(msg.sender).transfer(address(this).balance);16 }17}Összes megjelenítése
3. Tesztelje az okosszerződéseket és ellenőrizze a kód helyességét
Az Ethereum virtuális gépen érvényes kódváltoztathatatlanság miatt az okosszerződéseknél jelentős minőség-ellenőrzésre van szükség a fejlesztési időszakban. Tesztelje szerződését kiterjedt módon, és figyelje meg, hogy kap-e váratlan eredményeket, így fejlesztheti a biztonságot és megvédheti a felhasználókat hosszú távon is.
Ennek megszokott módja, hogy kicsi egységteszteket ír tesztadattal, melyet a szerződés a felhasználóktól kapna. Az egységtesztelés arra jó, hogy bizonyos függvények működését kipróbálja, és így biztosítja, hogy az okosszerződés az elvárt módon működik.
Sajnos az egységtesztelés minimálisan növeli az okosszerződés biztonságát, ha azt izolációban használják. Az egységteszt megmutathatja, hogy egy függvény megfelelően működik-e a tesztadatokra, de csak annyira hatásos, amennyire jó tesztet írnak hozzá. Nehéz beazonosítani a kimaradt eseteket és sebezhetőségeket, amelyek kompromittálhatják az okosszerződés biztonságát.
Jobb megközelítés az egységtesztelés tulajdonságalapú teszteléssel (property-based testing) való kombinálása, amely statikus és dinamikus elemzést használ. A statikus elemzés olyan alacsony szintű reprezentációkon alapul, mint amilyen a kontrollfolyamat-grafikon(opens in a new tab) és az absztrakt szintaxisfák(opens in a new tab), hogy elemezze az elérhető programstátuszokat és végrehajtási utakat. A dinamikus elemzési technikák, mint az okosszerződés fuzzing(opens in a new tab), a szerződéskódot véletlenszerű értékekkel hajtják végre, hogy feltárják azokat a működéseket, amelyek nem felelnek meg a biztonsági tulajdonságoknak.
A formális ellenőrzés (formal verification) egy másik technika az okosszerződések biztonsági tulajdonságainak igazolására. A megszokott teszteléshez képest a formális ellenőrzés képes egyértelműen bizonyítani, hogy nincsenek hibák az okosszerződésben. Ezt úgy éri el, hogy egy formális specifikációt hoz létre, amely a kívánt biztonsági tulajdonságokat rögzíti, majd bizonyítja, hogy a szerződések formális modellje megfelel ennek a specifikációnak.
4. Kérjen egy független átvizsgálást a kódjára
Miután tesztelte a szerződését, kérjen meg másokat is, hogy ellenőrizzék le a kódot a lehetséges biztonsági problémák szempontjából. A tesztelés nem tárja fel az okosszerződés minden hibáját, de egy független vizsgálat megnöveli annak valószínűségét, hogy kiderülnek a sebezhető pontok.
Auditok
Az okosszerződés auditálása az egyik módja a független kódvizsgálatnak. Az auditorok fontos szerepet játszanak abban, hogy az okosszerződések biztonságosak legyenek és ne legyenek bennük minőségi és tervezési hibák.
Mindazonáltal fontos megjegyezni, hogy az audit nem old meg minden problémát. Az okosszerződés-auditok nem tárnak fel minden egyes hibát, és a terv általában egy második körös ellenőrzés, hogy azokat a problémákat kiszúrja, ami a fejlesztőknek nem vált világossá a fejlesztés és tesztelés során. Kövesse a bevált gyakorlatokat az auditorokkal való munka kapcsán(opens in a new tab), mint amilyen a kód megfelelő dokumentálása és a sorokhoz kapcsolt kommentek, amelyek révén az okosszerződés-auditból a lehető legtöbb előnyt ki lehet hozni.
Hibavadászatok
Egy másik megoldás lehet a hibavadászat-program felállítása, amellyel külsődleges kódvizsgálatot lehet végezni. A hibavadászat pénzügyi jutalommal jár olyan egyéneknek (általában fehérkalapos hackereknek), akik sebezhető pontokat fedeznek fel az alkalmazásban.
Ez a jutalom a hibavadászatért, ha megfelelően használják, kellő motivációt jelenthet a hackerközösség bizonyos tagjai számára, hogy átnézzék az Ön kódját is kritikus hibákat keresve. Valós példa lehet a „végtelen mennyiségű pénz hiba”, ami egy támadónak lehetővé teszi, hogy határtalan mennyiségű ethert hozzon létre az Optimism-mal(opens in a new tab), egy második blokkláncréteg (L2) protokollal az Ethereumon. Szerencsére egy fehérkalapos hacker felfedezte a hibát(opens in a new tab) és értesítette a csapatot, amelyet jelentős pénzösszeggel jutalmaztak(opens in a new tab).
Hasznos stratégia lehet, ha a kifizetés összegét arányosan kezelik a hiba által veszélybe kerülő pénzeszközök értékével. Ezt „skálázódó hibavadászatnak(opens in a new tab)” is nevezhetjük, ami pénzügyi motivációt ad az egyéneknek, hogy inkább feltárják a gyenge pontokat és ne kihasználják azokat.
5. Kövesse a bevált gyakorlatokat az okosszerződésfejlesztés során
Az auditok és hibavadászatok nem csökkentik az Ön felelősségét, hogy jó minőségű kódot írjon. A megfelelő okosszerződés-biztonság azzal kezdődik, hogy megfelelő tervezési és fejlesztési folyamatokat követ:
Tárolja az összes kódot egy verziókövető rendszerben, mint amilyen a git
Minden kódmódosítást pull requesteken (változtatási kérelem) keresztül végezzen
A pull requesteknek legalább egy független ellenőrzője legyen – ha Ön egyedül dolgozik egy projekten, akkor fontolja meg, hogy más fejlesztőkkel összefogva elvégzik egymás számára a kódellenőrzéseket
Használjon fejlesztői környezetet az okosszerződések tesztelésére, átfordítására és telepítésére
Futtassa le a kódját olyan alapvető kódelemző eszközökön, mint a Cyfrin Aaderyn(opens in a new tab), Mythril és Slither. Ideális esetben ezt minden egyes pullrequest-beolvasztás előtt meg kell tenni, majd összehasonlítani az eredmények különbségeit
Biztosítsa, hogy a kód hibák nélkül kerül átfordításra, és a Solidity átfordító nem ad figyelmeztetéseket
Dokumentálja megfelelően a kódot (a NatSpec(opens in a new tab) használatában), és magyarázza el a részleteket a szerződés architektúrájáról egyszerű nyelven. Ezáltal könnyebb lesz másoknak auditálni és ellenőrizni a kódot.
6. Vezessen be komoly leállást követő helyreállítási tervet
A biztonságos hozzáférés-szabályozási terv, a függvénymódosítók bevezetése és más javaslatok fejlesztik az okosszerződés biztonságát, de nem zárhatják ki a lehetőségét egy ártó szándékú támadásnak. A biztonságos okosszerződés építése megkívánja azt is, hogy „felkészüljön a hibára”, és kidolgozzon egy tervet, amely alapján hatásosan tud reagálni egy támadásra. Egy megfelelő hibát vagy leállást követő helyreállítási terv (disaster recovery plan) a következő komponensek néhány vagy összes elemét tartalmazza:
Szerződésfrissítések
Miközben az Ethereum-okosszerződések alapvetően megváltozhatatlanok, mégis el lehet érni egy bizonyos fokú változtathatóságot a frissítési minták alkalmazásával. A szerződések frissítése elkerülhetetlen ha egy kritikus hiba miatt a régi szerződés használhatatlan lesz, és az új logika bevezetése a legjobb megoldás.
A szerződésfrissítési mechanizmusok másképp működnek, de a „proxyminta” az egyik legnépszerűbb megközelítés az okosszerződések frissítésére. A proxyminta(opens in a new tab) az alkalmazás státuszát és logikáját két szerződésre választja szét. Az első szerződés (a proxyszerződés) tárolja az állapotváltozókat (például a felhasználó egyenlegét), miközben a második szerződés (a logikaszerződés) tartalmazza a szerződés függvényeinek végrehajtási kódját.
A számlák a proxyszerződéssel kerülnek interakcióba, amely elküldi a függvénymeghívásokat a logikaszerződésbe a delegatecall()
(opens in a new tab) kódot, egy alacsony szintű meghívást használva. A delegatecall()
a megszokott üzenethíváshoz képest biztosítja, hogy a kód a logikaszerződés címén lefut a meghívó szerződés kontextusában. Tehát a logikaszerződés mindig a proxy tárhelyére ír (nem a sajátjába) és megőrzi a msg.sender
és msg.value
eredeti értékeit.
Ahhoz, hogy hívást lehessen delegálni a logikai szerződésnek, a címét el kell tárolni a proxyszerződés tárhelyén. Tehát a szerződés logikáját úgy lehet frissíteni, hogy egy új logikai szerződést kell telepíteni és eltárolni az új címet a proxyszerződésben. Mivel az ezt követő hívások a proxyszerződés felől automatikusan az új logikaszerződéshez kerülnek átirányításra, a kód változtatása nélkül végülis „frissítésre” kerül a szerződés.
Bővebben a szerződések frissítéséről.
Vészleállítások
Ahogy már említettük, sem a kiterjedt audit, sem a tesztelés nem képes felfedezni az okosszerződés összes hibáját. Ha a telepítés után sebezhető pont jelenik meg a kódjában, akkor azt nem lehet kijavítani, mert a szerződés címén futó kód megváltoztathatatlan. Emellett a frissítési mechanizmust (például a proxymintákat) időbe telik bevezetni (gyakran több jóváhagyást is igényelnek), ami csak időt ad a támadóknak, hogy több kárt okozzanak.
A radikális megoldás egy „vészleállítás” bevezetése, ami blokkolja azokat a hívásokat, melyek a szerződés sérülékeny függvényeire vonatkoznak. A vészleállítás általában a következő komponensekből áll:
Egy globális boolean változó, mely jelzi, ha az okosszerződés leállított állapotban van vagy nem. Ezt a változót
false
értékre állítják a szerződés telepítésekor, de átválttrue
értékre, amint a szerződés leáll.Függvények, melyek a boolean-változóra hivatkoznak a végrehajtásuk során. Ezek a függvények akkor érhetők el, amikor az okosszerződés nincs leállítva, és elérhetetlenné válnak, amikor a vészleállítás megtörténik.
Egy entitás, amelynek hozzáférése van a vészleállítási funkcióhoz, és a boolean változót
true
értékre állítja. Az esetleges visszaélés miatt ezt a funkciót csak egy megbízott cím hívhatja meg (például a szerződés tulajdonosa).
Amint a szerződés aktiválja a vészleállást, bizonyos függvényeket nem lehet meghívni. Ezt úgy érik el, hogy a select függvényeket becsomagolják egy módosítóba, amely a globális változóra hivatkozik. Alább egy példa(opens in a new tab) látható, amely ennek a mintának a szerződésbe való bevezetését mutatja be:
1// This code has not been professionally audited and makes no promises about safety or correctness. Use at your own risk.23contract EmergencyStop {45 bool isStopped = false;67 modifier stoppedInEmergency {8 require(!isStopped);9 _;10 }1112 modifier onlyWhenStopped {13 require(isStopped);14 _;15 }1617 modifier onlyAuthorized {18 // Check for authorization of msg.sender here19 _;20 }2122 function stopContract() public onlyAuthorized {23 isStopped = true;24 }2526 function resumeContract() public onlyAuthorized {27 isStopped = false;28 }2930 function deposit() public payable stoppedInEmergency {31 // Deposit logic happening here32 }3334 function emergencyWithdraw() public onlyWhenStopped {35 // Emergency withdraw happening here36 }37}Összes megjelenítéseMásolás
Ez a példa a vészleállás alapvető jellemzőit ismerteti:
Az
isStopped
egy boolean, melynek értékefalse
az elején éstrue
, amikor a szerződés vészmódba lép.Az
onlyWhenStopped
ésstoppedInEmergency
függvénymódosítók ellenőrzik azisStopped
változót. AstoppedInEmergency
azokat a függvényeket kontrollálja, amelyeknek elérhetetlennek kell maradniuk, amikor a szerződés sebezhető (például adeposit()
). Az ezekre a függvényekre vonatkozó hívások egyszerűen visszafordulnak.
Az onlyWhenStopped
azokhoz a függvényekhez használandó, amelyek vészhelyzetben is elérhetők (például az emergencyWithdraw()
). Ezek a függvények segíthetnek megoldani a helyzetet, ezért nem részei a „korlátozott függvények” listájának.
A vészleállítási lehetőség egy hatásos hézagpótlás ahhoz, hogy a fejlesztő a komoly sebezhetőségeket kezelni tudja az okosszerződésében. Ugyanakkor a felhasználóktól több bizalmat igényel a fejlesztők felé, hogy nem használják ki ezt a funkciót önös érdekeikre. Erre lehetséges megoldást jelenthet a vészleállítás decentralizált kontrollja, mint például egy láncon belüli szavazás, időzár alkalmazása vagy egy több aláírásos tárca általi jóváhagyás.
Eseményfigyelés
Az események(opens in a new tab) lehetővé teszik az okosszerződéshez érkező hívások trekkelését és az állapotváltozók változásának felügyeletét. Bevált gyakorlatnak számít, ha az okosszerződés mindig kiad eseményt, amikor valaki egy biztonságkritikus tevékenységet végez (például kiveszi a pénzeszközöket).
Az események naplózása és felügyelete láncon kívül betekintést enged a szerződés működésébe, valamint az ártalmas tetteket hamarabb fel lehet fedezni általuk. Így a csapat gyorsabban tud reagálni a hackelésre, és azonnal cselekedni tud, hogy a felhasználókat ez ne érintse negatívan, például leállíthatják a függvényeket vagy frissítést indíthatnak el.
Választhat egy előre összeállított felügyeleti eszközt, amely automatikusan figyelmeztetéseket küld, amikor valaki interakcióba lép az Ön szerződéseivel. Ezek az eszközök segítenek személyre szabott figyelmeztetéseket is létrehozni különféle paraméterek alapján, mint amilyen a tranzakciómennyiség, a függvénymeghívások gyakorisága vagy az érintett függvények. Például beállíthat egy figyelmeztetést, ha a kivett pénzmennyiség egy tranzakcióban egy bizonyos határ felett van.
7. Tervezzen biztonságos irányítási rendszert
Talán szeretné, hogy az alkalmazása decentralizált legyen, így a központi okosszerződések kontrollját a közösségi tagoknak adná. Ebben az esetben az okosszerződés rendszere felölel egy irányítási modult is – egy olyan mechanizmust, amellyel a közösségi tagok jóváhagyhatnak adminisztratív változásokat egy láncon belüli irányítási rendszer segítségével. Például azt a javaslatot, hogy a proxyszerződést egy új verzióra frissítsék, megszavaztathatja a tokennel rendelkező felhasználókkal.
A decentralizált irányítás előnyös lehet, főleg mivel összeegyezteti a fejlesztők és a felhasználók érdekeit. Mindazonáltal az okosszerződés irányításimechanizmusa új kockázatokat is jelenthet, ha nem megfelelően vezetik be. Kézenfekvő probléma, ha egy támadó nagyon magas szavazatierőt szerez (amit az általa birtokolt tokenek száma ad) azáltal, hogy villámhitelt vesz fel, majd egy ártó változásra tesz javaslatot.
A láncon működő irányítási modell problémáit meg lehet oldani az időzár használatával(opens in a new tab) is. Az időzár megakadályozza, hogy az okosszerződés végrehajtson bizonyos műveleteket addig, amíg nem telt el egy adott idő. Más stratégia lehet a tokenekhez rendelt „szavazati súly” az alapján, hogy azt mennyi időre kötötték le, vagy egy adott cím szavazati erejét hosszabb periódusra is nézhetik (például 2–3 korábbi blokkra) a jelenlegi blokk helyett. Ezek csökkentik a lehetőségét annak, hogy valaki gyorsan jelentős szavazati erőre tegyen szert, hogy a láncon zajló szavazást eltérítse.
Bővebben a biztonságos irányítási rendszerek tervezése(opens in a new tab) és a különféle szavazási mechanizmusok a DAO-knál(opens in a new tab) témákról.
8. Csökkentse a kód komplexitását a minimumra
A hagyományos szoftverfejlesztők elve az, hogy a lehető legegyszerűbb legyen a kód (KISS-elv), és így nem vezetnek be fölösleges bonyolításokat a tervben. Ennek alapja az az elgondolás, hogy az „összetett rendszerek összetett módokon vallhatnak kudarcot”, és sokkal hajlamosabbak a költséges hibákra.
A minél egyszerűbb megközelítés kiemelten fontos az okosszerződések írásánál is, mivel ezek nagy értékeket is kontrollálhatnak. Ennek eléréséhez érdemes létező könyvtárakat használni, mint amilyen az OpenZeppelin szerződések(opens in a new tab), amikor ez lehetséges. Mivel ezeket a könyvtárakat a fejlesztők már alaposan tesztelték, auditálták, így kisebb a hiba valószínűsége, mintha a nulláról kell megírni egy új funkcionalitást.
Másik követendő tanács az, hogy rövid függvényeket kell írni és a szerződést modulárisan kell felállítani, az üzleti logikát több szerződés között felosztva. Az egyszerű kódok írása kevesebb teret ad a támadásra, emellett a teljes rendszer helyességét is jobban lehet igazolni, és a lehetséges tervezési hibák is korán kiderülhetnek.
9. Védekezzen az okosszerződés általános sebezhetőségei ellen
Újrabelépés
Az EVM nem engedi a párhuzamosságot, tehát két szerződés egy üzenethívásban nem futhat egyszerre. Egy külső hívás megállítja a meghívó szerződés végrehajtását és memóriáját addig, amíg a hívás vissza nem tér, amikor is a végrehajtás normálisan megtörténik. Ezt a folyamatot hivatalosan úgy nevezik, hogy a kontrollfolyamat(opens in a new tab) átadása egy másik szerződésnek.
Habár általában nem jelent problémát, a kontrollfolyamat nem megbízható szerződéseknek való átadása okozhat némi gondot, például az újrabelépés lehetőségét. Újrabelépéses támadás akkor történik, amikor egy ártó szerződés visszahívást csinál egy sebezhető szerződésbe mielőtt az eredeti függvény meghívása lezárulna. Ezt a támadási fajtát a következő példával jobban elmagyarázzuk.
Vegyünk egy egyszerű okosszerződést („áldozat/victim”), ami megengedi, hogy bárki ethert helyezzen letétbe és vegyen ki:
1// This contract is vulnerable. Do not use in production23contract Victim {4 mapping (address => uint256) public balances;56 function deposit() external payable {7 balances[msg.sender] += msg.value;8 }910 function withdraw() external {11 uint256 amount = balances[msg.sender];12 (bool success, ) = msg.sender.call.value(amount)("");13 require(success);14 balances[msg.sender] = 0;15 }16}Összes megjelenítéseMásolás
Ez a szerződés elérhetővé teszi a withdraw()
(kivétel) függvényt a felhasználóknak, hogy a korábban letétbe helyezett ETH-t ki tudják venni. Amikor egy ilyen kivétel történik, a szerződés a következő műveleteket hajtja végre:
- Ellenőrzi a felhasználó ETH-egyenlegét
- Pénzeszközt küld a meghívó címére
- Átállítja az egyenleget 0-ra, hogy ne lehessen kivenni innen pénzt
A withdraw()
függvény az victim
(áldozat) szerződésében tehát egy „ellenőrzés-interakciók-eredmény” mintát követ. Ellenőrzi, hogy a végrehajtáshoz szükséges feltételek teljesülnek-e (a felhasználónak pozitív ETH-egyenlege van) és elvégzi az interakciót azáltal, hogy ETH-t küld a meghívó címére, majd a tranzakció eredményeit alkalmazza (lecsökkenti a felhasználó egyenlegét).
Ha a withdraw()
kódot egy külső tulajdonú számláról (EOA) hívják meg, akkor a vártnak megfelelően megy végbe: msg.sender.call.value()
ETH-t küld a meghívónak. Azonban, ha a msg.sender
egy okosszerződéses számla, ami meghívja a withdraw()
kódot, akkor a msg.sender.call.value()
révén indított pénzküldés szintén beindítja a címen tárolt programkódot.
Tegyük fel, hogy a szerződéscímen ez a kód van telepítve:
1 contract Attacker {2 function beginAttack() external payable {3 Victim(victim_address).deposit.value(1 ether)();4 Victim(victim_address).withdraw();5 }67 function() external payable {8 if (gasleft() > 40000) {9 Victim(victim_address).withdraw();10 }11 }12}Összes megjelenítéseMásolás
Ez a szerződés három dolgot csinál:
- Letétet fogad el egy másik számlától (valószínűleg a támadó/attacker EOA-ja)
- Letétbe helyez 1 ETH-t az áldozat szerződésében
- Kivesz 1 ETH-t, amelyet az okosszerződés tárol
Ebben még nincs semmi rossz, viszont a attacker
(támadó) szerződésben van egy másik függvény is, amely meghívja a withdraw()
kódot a victim
(áldozat) esetében újra, ha a maradék gáz a bejövő msg.sender.call.value
esetén több mint 40 000. Ezáltal a attacker
újra beléphet az victim
szerződésbe és kivehet több pénzt mielőtt a withdraw
(kivétel) első meghívása lezárulna. A ciklus így néz ki:
1- Attacker's EOA calls `Attacker.beginAttack()` with 1 ETH2- `Attacker.beginAttack()` deposits 1 ETH into `Victim`3- `Attacker` calls `withdraw() in `Victim`4- `Victim` checks `Attacker`’s balance (1 ETH)5- `Victim` sends 1 ETH to `Attacker` (which triggers the default function)6- `Attacker` calls `Victim.withdraw()` again (note that `Victim` hasn’t reduced `Attacker`’s balance from the first withdrawal)7- `Victim` checks `Attacker`’s balance (which is still 1 ETH because it hasn’t applied the effects of the first call)8- `Victim` sends 1 ETH to `Attacker` (which triggers the default function and allows `Attacker` to reenter the `withdraw` function)9- The process repeats until `Attacker` runs out of gas, at which point `msg.sender.call.value` returns without triggering additional withdrawals10- `Victim` finally applies the results of the first transaction (and subsequent ones) to its state, so `Attacker`’s balance is set to 0Összes megjelenítéseMásolás
Összességében, mivel a meghívó egyenlege nem lesz 0 mindaddig, amíg a függvényvégrehajtás nem zárul le, a rákövetkező meghívások sikeresek lesznek, és megengedik a meghívónak, hogy kivegye az egyenlegét többször is. Ez a támadás alkalmas arra, hogy egy okosszerződés pénzeszközeit kifolyassák, ahogy az a 2016-os DAO hackelésnél(opens in a new tab) megtörtént. Az újrabelépéses támadás még mindig kritikus probléma az okosszerződéseknél, ahogy azt az újrabelépéses támadások nyilvános listája(opens in a new tab) mutatja.
Hogyan lehet megakadályozni egy újrabelépéses támadást
Az újrabelépés ellen az ellenőrzés-eredmények-interakciók mintát(opens in a new tab) lehet alkalmazni. Ez a minta a függvények végrehajtását úgy rendezi, hogy az a kód jön először, amely a szükséges ellenőrzéseket végzi, azután a szerződés státuszát változtatják meg, végül a más szerződésekkel vagy külső tulajdonú számlákkal (EOA) való interakció következik.
Az ellenőrzés-eredmények-interakciók minta a következőképpen néz ki a victim
(áldozat) szerződésének új verziójában:
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}Másolás
Ez a szerződés ellenőrzi a felhasználó egyenlegét, érvényesíti a withdraw()
függvény eredményét (azáltal, hogy az egyenleget 0-ra állítja), és végül elvégzi az interakciót (ETH-t küld a felhasználó címére). Ezáltal a szerződés először befrissíti a tárolt adatot, és csak utána végzi a külső hívást, így nincs lehetőség az újrabelépésre, mint korábban. Az attacker
szerződés még mindig vissza tudja hívni a NoLongerAVictim
(nem áldozat) szerződést, de mivel a balances[msg.sender]
(egyenlege) már 0, a többi kivétel hibára fut.
Másik lehetőség egy kölcsönös kizárás (más néven mutex), amely lezárja a szerződés státuszának egy részét addig, amíg a függvénymeghívás teljesül. Ezt egy boolean változóval lehet bevezetni, ami először true
(igaz) a függvényvégrehajtás előtt, majd false
(hamis) lesz a meghívás befejeztével. Ahogy az alábbi példából látszik, a mutex használata megvédi a függvényt attól, hogy újra meghívják, miközben az eredeti meghívás még zajlik, így hatásosan kivédi az újrabelépést.
1pragma solidity ^0.7.0;23contract MutexPattern {4 bool locked = false;5 mapping(address => uint256) public balances;67 modifier noReentrancy() {8 require(!locked, "Blocked from reentrancy.");9 locked = true;10 _;11 locked = false;12 }13 // This function is protected by a mutex, so reentrant calls from within `msg.sender.call` cannot call `withdraw` again.14 // The `return` statement evaluates to `true` but still evaluates the `locked = false` statement in the modifier15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");1718 balances[msg.sender] -= _amount;19 bool (success, ) = msg.sender.call{value: _amount}("");20 require(success);2122 return true;23 }24}Összes megjelenítéseMásolás
Továbbá a „fizetéskérés”(opens in a new tab) rendszere is használható, amelynél a felhasználó vesz ki pénzt az okosszerződésből ahelyett, hogy a szerződés „fizetésküldést” végezne a számlák felé. Így nem lehet véletlenül elindítani egy kódot ismeretlen címeken (és bizonyos szolgálatmegtagadási támadásokat is ki tud védeni).
Egész szám túlfolyása lefelé vagy felfelé
Egy egész szám akkor folyik túl felfelé, amikor egy aritmetikai művelet eredménye kívül esik az elfogadható tartományon, így az „tovább gördül” a legalacsonyabb megjeleníthető értékre. Például egy uint8
csak 2^8-1=255 értéket tud tárolni. Az aritmetikai művelet, amelynek eredménye nagyobb mint 255
, túlfolyik és visszaállítja az uint
kódot 0
értékre, ahhoz hasonlóan, ahogy egy autóban a megtett távolságot mérő óra is 0-ra fordul át, ha elérte a maximális értékét (999 999).
Az egész szám lefelé való túlfolyása hasonló okokból következik be: az aritmetikai művelet eredménye az elfogadható tartomány alá esik. Tegyük fel, Ön szeretné lecsökkenteni a 0
értéket egy uint8
típusú mezőben, így az egyszerűen átfordul a maximális megjeleníthető értékre (255
).
Mindkét irányú túlfolyás a szerződés állapotváltozóiban váratlan változásokat eredményezhet, így nem tervezett végrehajtást okozhat. Az alábbi példa bemutatja, hogyan tudja egy támadó kihasználni az aritmetikai túlfolyást egy okosszerződésben, hogy érvénytelen műveletet hajtson végre:
1pragma solidity ^0.7.6;23// This contract is designed to act as a time vault.4// User can deposit into this contract but cannot withdraw for at least a week.5// User can also extend the wait time beyond the 1 week waiting period.67/*81. Deploy TimeLock92. Deploy Attack with address of TimeLock103. Call Attack.attack sending 1 ether. You will immediately be able to11 withdraw your ether.1213What happened?14Attack caused the TimeLock.lockTime to overflow and was able to withdraw15before the 1 week waiting period.16*/1718contract TimeLock {19 mapping(address => uint) public balances;20 mapping(address => uint) public lockTime;2122 function deposit() external payable {23 balances[msg.sender] += msg.value;24 lockTime[msg.sender] = block.timestamp + 1 weeks;25 }2627 function increaseLockTime(uint _secondsToIncrease) public {28 lockTime[msg.sender] += _secondsToIncrease;29 }3031 function withdraw() public {32 require(balances[msg.sender] > 0, "Insufficient funds");33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");3435 uint amount = balances[msg.sender];36 balances[msg.sender] = 0;3738 (bool sent, ) = msg.sender.call{value: amount}("");39 require(sent, "Failed to send Ether");40 }41}4243contract Attack {44 TimeLock timeLock;4546 constructor(TimeLock _timeLock) {47 timeLock = TimeLock(_timeLock);48 }4950 fallback() external payable {}5152 function attack() public payable {53 timeLock.deposit{value: msg.value}();54 /*55 if t = current lock time then we need to find x such that56 x + t = 2**256 = 057 so x = -t58 2**256 = type(uint).max + 159 so x = type(uint).max + 1 - t60 */61 timeLock.increaseLockTime(62 type(uint).max + 1 - timeLock.lockTime(address(this))63 );64 timeLock.withdraw();65 }66}Összes megjelenítése
Hogyan akadályozható meg egy egész szám túlfolyása lefelé vagy felfelé
A 0.8.0 verzió szerint a Solidity átfordító elutasítja azokat a kódokat, amelyek az egész szám túlfolyását eredményezik. Ugyanakkor az alacsonyabb verziójú átfordítóval készült szerződések esetén ellenőrizni kell azokat a függvényeket, amelyek aritmetikai műveleteket hajtanak végre, vagy egy olyan könyvtárat lehet használni (például SafeMath(opens in a new tab)), amely ellenőrzi a túlfolyásokat.
Orákulum manipulációja
Az orákulumok láncon kívüli információkat gyűjtenek és beküldik azokat a láncra, hogy az okosszerződések használhassák. Az orákulumok révén Ön olyan okosszerződéseket tervezhet, amelyek együtt tudnak működni láncon kívüli rendszerekkel, mint a tőkepiacok, ezzel nagy mértékben kiterjesztve az alkalmazási körüket.
Ha viszont az orákulum korrupttá válik és nem helyes információkat küld a láncra, az okosszerződések hibás bejövő adatok alapján fognak működni, ez pedig problémákat okoz. Ez az „orákulumprobléma” alapja, amely miatt biztosítani kell, hogy a blokklánc-orákulum által adott információ pontos, friss és időben elérhető legyen.
Az ehhez kapcsolódó biztonsági probléma az, amikor például egy decentralizált tőzsde a láncon belüli orákulumot használja arra, hogy megszerezze egy eszköz azonnali (spot) árát. A kölcsönző platformok a decentralizált pénzügyek (DeFi) iparágában gyakran csinálják ezt, hogy meghatározzák a felhasználó fedezetének értékét, és ezáltal a kölcsön mértékét.
A DEX árak gyakran igen pontosak, akár nagy mértékben is, mivel az arbitrázst kihasználók helyreállítják a piacokon az egyensúlyt. Ugyanakkor teret adnak a manipulációra, főleg ha a láncon futó orákulum az eszköz árát a korábbi kereskedelmi minták alapján számolja (ami általában igaz).
Például egy támadó mesterségesen fel tudja pumpálni egy eszköz azonnali árát azáltal, hogy egy villámkölcsönt vesz fel éppen a kölcsönszerződés megkötése előtt. Ekkor a DEX lekérdezés az eszköz áráról egy magasabb értéket fog mutatni (mivel a támadó nagy összegű vételi igénye elmozdította az eszköz keresletét), így magasabb kölcsönt vehetnek fel, mint amit lehetne. Az ilyen „villámkölcsön-támadások” kihasználták azt, hogy a DeFi alkalmazások az orákulumokra támaszkodnak az árakat tekintve, és így sok milliónyi elveszett pénzeszközt eredményeztek a protokolloknak.
Hogyan lehet elkerülni az oracle manipulációt
A minimum követelmény az oracle manipuláció elkerülésére(opens in a new tab) az, hogy decentralizált oracle-hálózatokat kell használni, amelyek több forrásból szerzik be az információkat, így elkerülhető az egyetlen meghibásodási pont lehetősége. A legtöbb esetben a decentralizált orákulumoknak beépített kriptogazdasági ösztönzőik vannak, hogy az orákulum-csomópontok a helyes információt jelentsék, így sokkal biztonságosabbak, mint a centralizált társaik.
Ha Ön azt tervezi, hogy egy láncon lévő orákulumot kérdez le eszközárakért, akkor használjon olyat, amely idővel súlyozott átlagárat (TWAP) számol. A TWAP-orákulum(opens in a new tab) egy adott eszköz árát két különböző időpontban (ami módosítható) kérdezi le, és a megszerzett átlaga alapján kalkulálja az azonnali árat. A hosszabb időtartomány használata megvédi a protokollt az ármanipulációtól, mert a közelmúltban végrehajtott nagy rendelések nem befolyásolják az árat.
Okosszerződés-biztonsággal kapcsolatos anyagok fejlesztők számára
Eszközök az okosszerződések elemzéséhez és a kód helyességének ellenőrzéséhez
Tesztelő eszközök és könyvtárak – Iparági standard eszközök és könyvtárak gyűjteménye az okosszerződések egységteszteléséhez, valamint a statikus és dinamikus elemzéséhez.
Formális ellenőrzési (formal verification) eszközök – Eszközök arra, hogy ellenőrizzék az okosszerződések funkcionális helyességét és az állandókat.
Okosszerződés auditálásra vonatkozó szolgáltatások – Szervezetek listája, amelyek auditszolgáltatást kínálnak okosszerződésekre az Ethereum fejlesztési projektek számára.
Hibavadász platformok – Platformok a hibavadászatok és a jutalmak koordinálására, hogy azok feltárják az okosszerződésekben lévő kritikus sebezhetőségeket.
Fork Checker(opens in a new tab) – Egy ingyenes online eszköz arra, hogy információt kapjon egy elágaztatott szerződésről.
ABI Encoder(opens in a new tab) – Egy ingyenes online szolgáltatás a Solidity szerződés függvényeinek és constructor parancsainak kódolására.
Eszközök az okosszerződések felügyeletére
OpenZeppelin Defender Sentinels(opens in a new tab) – Egy eszköz az okosszerződés automatikus felügyeletére, valamint az eseményekre, függvényekre és tranzakcióparaméterekre való válaszadásra.
Tenderly Real-Time Alerting(opens in a new tab) – Egy eszköz, amellyel valós idejű értesítést kaphat, amikor az okosszerződésén vagy tárcáján szokatlan vagy váratlan események történnek.
Eszközök az okosszerződések biztonságos adminisztrálásához
OpenZeppelin Defender Admin(opens in a new tab) – Interfész az okosszerződések adminisztrációjának kezeléséhez, beleértve a hozzáférés-kezelést, frissítéseket és leállítást is.
Safe(opens in a new tab) – Egy okosszerződéses tárca az Ethereumon, amelynél adott számú embernek jóvá kell hagynia a tranzakciót, mielőtt az megtörténhetne (N számú tagból M-nek).
OpenZeppelin Contracts(opens in a new tab) – Szerződéskönyvtárak az adminisztrációs jellemzők bevezetésére, beleértve a szerződés tulajdonlását, frissítéseket, hozzáférés-kezelést, irányítást, leállíthatóság és még sok mást.
Okosszerződés auditálására kínált szolgáltatások
ConsenSys Diligence(opens in a new tab) – Okosszerződés auditálására kínált szolgáltatások, amelyek támogatják a blokklánc-ökoszisztéma projektjeit, hogy a protokolljaik készen állnak-e a bevezetésre és úgy épültek-e meg, hogy védik a felhasználókat.
CertiK(opens in a new tab) – Egy blokkláncbiztonsággal foglalkozó cég, amely úttörőként használja az élvonalbeli formális ellenőrzés technológiáját az okosszerződésekre és a blokklánchálózatokra.
Trail of Bits(opens in a new tab) – Kiberbiztonsági cég, amely kombinálja a biztonsági kutatást és a támadói mentalitást, hogy csökkentse a kockázatot és megerősítse a kódot.
PeckShield(opens in a new tab) – Blokkláncbiztonsággal foglalkozó cég, amely a teljes blokklánc-ökoszisztémához kínál termékeket és szolgáltatásokat a biztonság, adatvédelem és használhatóság területein.
QuantStamp(opens in a new tab) – Auditszolgáltatás, amely elősegíti a blokklánctechnológia kiterjedt használatát a biztonsági és kockázatelemzési szolgáltatásokkal.
OpenZeppelin(opens in a new tab) – Okosszerződés-biztonsággal foglalkozó cég, amely a megosztott rendszerek számára biztosít biztonsági auditokat.
Runtime Verification(opens in a new tab) – Biztonsági cég, amely az okosszerződések formális modellezésére és ellenőrzésére specializálódott.
Hacken(opens in a new tab) – Web3 kiberbiztonsági auditor, amely 360 fokos megközelítést alkalmaz a blokkláncbiztonságban.
Nethermind(opens in a new tab) – Solidity és Cairo auditszolgáltatások, amelyekkel az okosszerződések integritása, valamint a felhasználók biztonsága is biztosíthat az Ethereumon és a Starkneten.
HashEx(opens in a new tab) – A HashEx a blokkláncok és okosszerződések auditálásra szakosodott a kriptovaluták biztonságának biztosítása céljából, illetve olyan szolgáltatásokat nyújt, mint az okosszerződés-fejlesztés, sérülékenység-vizsgálat, blokklánctanácsadás.
Code4rena(opens in a new tab) – Versenyképes auditplatform, amely arra ösztönzi az okosszerződés-biztonsági szakértőket, hogy sebezhetőséget találjanak és segítsenek a web3-at biztonságosabbá tenni.
CodeHawks(opens in a new tab) – Versenyképes auditplatform, amely okosszerződések auditálási versenyeit tartja a biztonsági szakértők számára.
Cyfrin(opens in a new tab) – Blokklánc-biztonsági és web3 oktatási cég, amely az EVM- és Vyper-alapú protokollokra összpontosít.
ImmuneBytes(opens in a new tab) – Web3 biztonsági cég, amely a blokkláncrendszerek biztonsági ellenőrzését kínálja tapasztalt auditorcsapattal és a legjobb eszközökkel.
Hibavadászplatformok
Immunefi(opens in a new tab) – Hibavadászplatform okosszerződésekhez és DeFi-projektekhez, ahol a biztonsági kutatók átnézik a kódot, kizárják a sebezhetőségeket, ezért jutalmat kapnak, és biztonságosabbá teszik a kripto világát.
HackerOne(opens in a new tab) – Sebezhetőségi koordináció és hibavadászplatform, amely összeköti a vállalkozásokat a sebezhetőségi tesztelőkkel és kiberbiztonsági kutatókkal.
HackenProof(opens in a new tab) – Szakértői hibavadászplatform kriptoprojektek (DeFi, okosszerződések, tárcák, CEX stb.) számára, ahol a biztonsági szakértők prioritási sorrendszolgáltatást nyújtanak, a kutatók pedig jutalmat kapnak a releváns, igazolt hibák jelentéséért.
Publikációk az okosszerződések ismert sebezhetőségeiről és azok kihasználásáról
Consensys: az okosszerződéseket ért ismert támadások(opens in a new tab) – Egyszerűen megfogalmazott magyarázat a legkomolyabb sérülékenységekről a szerződésekben, a legtöbb esetben mintakódokkal együtt.
SWC Registry(opens in a new tab) – A Közös gyengeségek felsorolásának (CWE) gondozott listája, amelyen az Ethereum okosszerződésekre vonatkozó tételek szerepelnek.
Rekt(opens in a new tab) – Rendszeresen frissített publikáció a nagy jelentőségű kriptohackelésekről és támadásokról, az esemény után készült részletes riportokkal.
Kihívások az okosszerződés-biztonság elsajátításában
Awesome BlockSec CTF(opens in a new tab) – Blokkláncbiztonsági háborús játékok, kihívások és szerezd meg a zászlót (Capture The Flag)(opens in a new tab) versenyek és megoldások gondozott listája.
Damn Vulnerable DeFi(opens in a new tab) – Háborús játék a DeFi okosszerződések támadó biztonságának elsajátításához, valamint készségek fejlesztéséhez a hibavadászatban és a biztonsági auditban.
Ethernaut(opens in a new tab) – Web3/Solidity-alapú háborús játék, ahol minden szint egy okosszerződés, amelyet meg kell „hackelni”.
Bevált gyakorlatok az okosszerződések biztonságossá tételére
ConsenSys: az Ethereum okosszerződés-biztonság bevált gyakorlatai(opens in a new tab) – Részletes útmutatók az Ethereum-okosszerződések biztonságossá tételére.
Nascent: Egyszerű biztonsági eszközrendszer(opens in a new tab) – Hasznos biztonságközpontú útmutatók és ellenőrző listák gyűjteménye okosszerződés-fejlesztéshez.
Solidity Patterns(opens in a new tab) – Biztonsági minták és bevált gyakorlatok hasznos gyűjteménye Solidity programnyelven írt okosszerződésekhez.
Solidity Docs: Biztonsági megfontolások(opens in a new tab) – Útmutatók a biztonságos okosszerződések írásához Solidity nyelven.
Smart Contract Security Verification Standard(opens in a new tab) – Egy tizennégy részes ellenőrző lista fejlesztők, architektúrával foglalkozók, biztonság-ellenőrzők és beszállítók számára az okosszerződések biztonságának szabványosításához.