Biztonság
Az Ethereum okosszerződések rendkívül flexibilisek, képesek nagy mennyiségű tokent tárolni (néha meghaladja az 1 Mrd. Usd-t) és megváltoztathatatlan logikát futtatni, mely korábban telepített okosszerződés kódon alapszik. Bár ez egy élénk és kreatív ökoszisztémát hozott létre a bizalom nélküli, egymással összekapcsolt okosszerződésekből, ugyanakkor tökéletes ökoszisztéma a profitra törekvő támadók számára is, aki az okosszerződések sebezhető pontjainak és az Ethereum váratlan viselkedésének kihasználásával szeretnének profitra szert tenni. Az okosszerződés kódot általában nem lehet megváltoztatni biztonsági hibák javítása céljából, az okosszerződésekből ellopott vagyont nem lehet visszaszerezni, és a lopott vagyont rendkívül nehéz nyomon követni. Az okosszerződés hibák miatt ellopott vagy elveszett érték teljes összege már könnyedén meghaladja az 1 Mrd. USD-t. A nagyobb okosszerződés hibák között van a:
- Parity multi-sig hiba #1 - 30 millió USD elveszett
- Parity multi-sig hiba#2 - 300 millió USD lekötve
- A TheDAO hack, 3.6M ETH! Több mint 1 Mrd. USD a mai ETH árfolyamon
Előfeltételek
Ez a cikk az okosszerződés biztonságról szól, így érdemes tisztában lenned az okosszerződésekkel, mielőtt belekezdenél a biztonságba.
Hogyan lehet biztonságosabb okosszerződés kódot írni
Mielőtt bármilyen kódot indítanánk a főhálózatra, fontos, hogy megfelelő elővigyázatossággal védjük meg az okosszerződésre rábízott értékeket. Ebben a cikkben néhány konkrét támadást megvitatunk, forrásokat biztosítunk további támadástípusok megismeréséhez, és hagyunk néhány alapvető eszközt és bevált gyakorlatot a szerződések megfelelő és biztonságos működéséhez.
Az audit nem gyógyír mindenre
Évekkel ezelőtt az okosszerződések írásának, fordításának, tesztelésének és telepítésének eszközei nagyon kiforratlanok voltak, ami sok projektet arra késztettet, hogy rendszertelenül írják a Solidity kódot, majd átadják egy auditornak, aki megvizsgálta a kódot annak biztosítása érdekében, hogy biztonságosan működik-e az elvárásoknak megfelelően. 2020-ban a Solidity írást támogató fejlesztési folyamatok és eszközök lényegesen jobbak; a bevált gyakorlatok felhasználása nemcsak a projekt könnyebb kezelhetőségét biztosítja, hanem a projekt biztonságának létfontosságú része is. Az okosszerződés megírásának végén végzett audit már nem elegendő, mint a projekted egyetlen biztonsági szempontja. A biztonság már az okosszerződés kód első sorának megírása előtt elkezdődik, a biztonság megfelelő tervezéssel és fejlesztési folyamatokkal kezdődik.
Okosszerződés fejlesztési folyamat
Minimum:
- Az összes kódot egy verzió követő rendszer tárolja, mint a git
- Minden kódmódosítást pull requesteken keresztül kell végezni
- Minden pull requestet át kell néznie legalább egy valakinek. Ha egyedül vagy a projekten, akkor keress valakit aki szintén egyedül van és cseréljetek kód review-kat.
- Egy egyedüli parancs fordítja, telepíti és futtatja a tesztek sorozatát a kódodra egy Ethereum fejlesztői környezet használatával (lásd Truffle)
- Végig futtatod a kódodat valamilyen alapszintű kód analitikai eszközzel, mint a Mythril vagy a Slither, ideálisan mielőtt az egyes pull requesteket mergeled, így össze tudod hasonlítani a végeredményeket
- A Solidity nem fog SEMMILYEN fordítói hibát visszaadni
- A kódot megfelelően dokumentációval kell ellátni
Sokkal többet el lehetne mondani még a fejlesztési folyamatról, de ezek a tételek jó kiindulópontot jelentenek. További szempontért és részletes magyarázatért tekintsd meg a folyamat minőségi checklistát a DeFiSafety által. A DefiSafety egy nemhivatalos közszolgálat, mely értékeléseket publikál különböző nagyobb, nyilvános Ethereum dappról. A DeFiSafety minősítési rendszer egyik része, hogy a projekt mennyire tartja be ezt a folyamat minőségi ellenőrzőlistát. Ezeket a folyamatokat követve:
- Biztonságosabb kódot állítasz elő úrjafelhasználható, automatizált tesztekkel
- Az auditorok hatékonyabban fogják átnézni a projektedet
- Könnyebben tudnak új fejlesztők becsatlakozni
- A fejlesztők gyorsabban tudnak iterálni, tesztelni és visszajelzést kapni a módosításokról
- Kisebb a valószínűsége, hogy projekted visszafejlődést szenved el
Támadások és sérülékenységek
Most, hogy a Solidity kódot már egy hatékony fejlesztési folyamat segítségével írod, nézzünk meg néhány általános Solidity biztonsági rést, hogy lássuk, mit ronthatunk el.
Újbóli belépés (re-entrancy)
Az újbóli belépés az egyik legnagyobb és legjelentősebb biztonsági probléma, melyet figyelembe kell venni okosszerződések fejlesztésekor. Míg az EVM nem tud egyszerre több szerződést futtatni, egy másik szerződést meghívó szerződés szünetelteti a hívó szerződés végrehajtását és memóriaállapotát, amíg a hívás vissza nem tér, ekkor a végrehajtás normálisan halad tovább. Ez a szüneteltetés és újraindítás egy "újbóli belépésnek" nevezett sérülékenységet eredményezhet.
Itt egy egyszerű szerződés verzió, mely ki van téve az újbóli belépésnek:
1// EZ A SZERZŐDÉS SZÁNDÉKOSAN SÉRÜLÉKENY, NE MÁSOLD LE2contract Victim {3 mapping (address => uint256) public balances;45 function deposit() external payable {6 balances[msg.sender] += msg.value;7 }89 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}16Összes megjelenítéseMásolás
Ahhoz, hogy egy felhasználó ETH-et utalhasson ki, amit korábban a szerződésben tárolt ez a függvény
- Leolvassa, a felhasználó egyenlegét
- Elküldi neki az egyenleg összegét ETH-ben
- Visszaállítja 0-ra az egyenleget, így nem tudja még egyszer kiutalni az összeget.
Ha meghívja egy normál számla (mint a saját Metamask számlád), akkor az elvártnak megfelelően működik: msg.sender.call.value() egyszerűen ETH-et küld a számládra. Azonban az okosszerződések is tudnak hívásokat intézni. Ha egy egyedi, rosszindulatú szerződés a withdraw()
függvény meghívója, akkor a msg.sender.call.value() nem csak amount
összegű ETH-et fog küldeni, hanem implicit módon meghívja a szerződést, hogy elindítsa a kód végrehajtást. Képzeld el ezt a következő rosszindulatú szerződést:
1contract 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}13Összes megjelenítéseMásolás
Az Attacker.beginAttack() meghívása egy ciklust fog beindítani, mely valahogy így néz ki:
10.) A támadó EOA-ja (external owned account) meghívja a Attacker.beginAttack() függvényt 1 ETH-tel20.) Az Attacker.beginAttack() beutal 1 ETH-et a Victim-be34 1.) Attacker -> Victim.withdraw()5 1.) Victim leolvassa a balanceOf[msg.sender] értéket6 1.) Victim ETH-et küld Attacker-nek (mely végrehajtja az alapértelmezett függvényt)7 2.) Attacker -> Victim.withdraw()8 2.) Victim leolvassa a balanceOf[msg.sender] értéket9 2.) Victim ETH-et küld Attacker-nek (mely végrehajtja az alapértelmezett függvényt)10 3.) Attacker -> Victim.withdraw()11 3.) Victim leolvassa a balanceOf[msg.sender] értéket12 3.) Victim ETH-et küld Attacker-nek (mely végrehajtja az alapértelmezett függvényt)13 4.) Attacker-nek nincs több gáza, hívás nélkül visszatér14 3.) balances[msg.sender] = 0;15 2.) balances[msg.sender] = 0; (már 0 volt az értéke)16 1.) balances[msg.sender] = 0; (már 0 volt az értéke)17Összes megjelenítése
Az Attacker.beginAttack meghívása 1 ETH-tel egy újbóli belépés támadást fog indítani Victim ellen, ezzel több ETH-et kiutalva, mint amennyit beletesz (melyet más felhasználók egyenlegéből vont le, így a Victim szerződés alulfedezetté válik)
Hogyan kezeljük az újbóli belépést (a rosszabb mód)
Fontolóra lehet venni az újbóli belépés kezelését azzal, hogy egyszerűen megakadályozzuk az okosszerződések interakcióját a kóddal. A stackoverflow-n az alábbi kód részletet lehet megtalálni rengeteg pozitív szavazattal:
1function isContract(address addr) internal returns (bool) {2 uint size;3 assembly { size := extcodesize(addr) }4 return size > 0;5}6Másolás
Értelmesnek tűnik: a szerződésnek van kódja, ha a hívónak van kódja, akkor nem engedi letétet elhelyezni. Adjuk hozzá ezt:
1// EZ A SZERZŐDÉS SZÁNDÉKOSAN SÉRÜLÉKENY, NE MÁSOLD LE2contract ContractCheckVictim {3 mapping (address => uint256) public balances;45 function isContract(address addr) internal returns (bool) {6 uint size;7 assembly { size := extcodesize(addr) }8 return size > 0;9 }1011 function deposit() external payable {12 require(!isContract(msg.sender)); // <- ÚJ SOR13 balances[msg.sender] += msg.value;14 }1516 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}23Összes megjelenítéseMásolás
Ebben az esetben ahhoz, hogy ETH letétet tudj elhelyezni, az címeden nem szabad okosszerződés kódnak lennie. Azonban ezt könnyen meg lehet kerülni a következő Attacker szerződéssel:
1contract ContractCheckAttacker {2 constructor() public payable {3 ContractCheckVictim(VICTIM_ADDRESS).deposit(1 ether); // <- Új sor4 }56 function beginAttack() external payable {7 ContractCheckVictim(VICTIM_ADDRESS).withdraw();8 }910 function() external payable {11 if (gasleft() > 40000) {12 Victim(VICTIM_ADDRESS).withdraw();13 }14 }15}16Összes megjelenítéseMásolás
Míg az első támadás a szerződés logikája elleni támadás volt, ez az Ethereum szerződések telepítési viselkedése elleni támadás. A konstrukció alatt a szerződés nem adja vissza a kódját telepítettként a címén, de a teljes EVM kontrollt megtartja a folyamat ALATT.
Technikailag lehetséges megakadályozni az okosszerződéseket, hogy meghívják a kódodat ezzel a sorral:
1require(tx.origin == msg.sender)2Másolás
Azonban ez még mindig nem egy jó megoldás. Az Ethereum egyik legizgalmasabb aspektusa az összeállíthatóság, amikor az okosszerződések integrálódnak és egymásra épülnek. A fenti sor használatával korlátozod a projekted hasznosságát.
Hogyan kezeljük az újbóli belépést (a jobb mód)
Egyszerűen a tárhely frissítés és a külső hívás sorrendjének felcserélésével meg tudjuk akadályozni az újbóli belépés feltételét, mely lehetővé tette a támadást. A withdraw visszahívása, amíg lehetséges, nem lesz jövedelmező a támadó számára, mivel a balances
tárhely már 0 értékre lesz állítva.
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}9Másolás
A fenti kód a "Checks-Effects-Interactions" tervezési mintát követi, amely segít megvédeni az újbóli belépéstől. Többet olvashatsz itt a Checks-Effects-Interactions-ról itt
Hogyan kezeljük az újbóli belépést (ágyúval verébre)
Bármikor amikor ETH-et küldesz egy nem megbízható címre vagy interakcióba lépsz egy ismeretlen szerződéssel (vagyis meghívod a transfer()
függvényét egy felhasználó által biztosított token címnek), kitetté válsz egy lehetséges újbóli belépéses támadásnak. Olyan szerződések tervezésével, melyek sem az ETH küldést, sem a nem megbízható szerződések hívását sem támogatják, megelőzhető egy lehetséges újbóli belépés!
Több támadás típus
A fenti támadástípusok az okosszerződések kódjához (újbóli belépés) és az Ethereum furcsaságaihoz kapcsolódnak (kód futtatása a szerződés konstruktoron belül, mielőtt a kód elérhető lenne a szerződés címén). Sok, sok más fajta támadás típus létezik, melyekre figyelni kell, mint a:
- Front-running
- ETH küldés elutasítás
- Integer overflow/underflow
További olvasnivaló:
- Consensys Okosszerződés Ismet Támadások - Egy nagyon olvasmányos magyarázat a legkomolyabb sérülékenységekről, a legtöbbhöz minta kóddal is.
- SWC Registry - A CWE válogatott listája, mely az Ethereumra és az okosszerződésekre is érvényes
Biztonsági eszközök
Bár nem helyettesítheti az Ethereum biztonsági alapismereteinek megértését és a szakmai auditáló cég bevonását a kód felülvizsgálatába, számos eszköz áll rendelkezésre a kódban felmerülő lehetséges problémák kiemelésére.
Okosszerződés Biztonság
Slither - Solidity statikus analízis keretrendszer Python 3-ban írva.
MythX - Biztonsági analízis API Ethereum okos szerződéseknek
Mythril - Biztonsági analitika eszköz EVM bájt-kódra.
SmartContract.Codes - Ellenőrzött Solidity forráskódok keresőmotora
Manticore - Egy Cli, ami egy szimbolikus futtató eszközt használ okosszerződésekre és binary-kre.
Securify - Biztonsági szkenner Ethereum okosszerződésekre.
ERC20 Verifier - Egy ellenőrző eszköz arra, hogy egy szerződés megfelel-e az ERC20 szabványnak.
Formális Ellenőrzés
Formális Ellenőrzés információ
- How formal verification of smart-contacts works July 20, 2018 - Brian Marick
- How Formal Verification Can Ensure Flawless Smart Contracts Jan 29, 2018 - Bernard Mueller
Eszközök használata
A két legnépszerűbb okosszerződés biztonsági analitikai eszköz:
- Slither a Trail of Bits által (hosztolt verzió: Crytic)
- Mythril a ConsenSys által (hosztolt verzió: MythX)
Mindkettő hasznos eszköz a kód elemzésére és a problémák jelentésére. Mindkettőnek van egy [commercial] hosztolt verziója, de ingyenesen is lehet őket lokálisan futtatni. Az alábbiakban bemutatunk egy gyors példát a Slither futtatására, amely egy kényelmes Docker image-dzsel érhető el: trailofbits/eth-security-toolbox
. Szükség lesz a Docker telepítésére, ha még nincs feltelepítve.
$ 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-toolboxdocker$ cd /sharedocker$ solc-select 0.5.11docker$ slither bad-contract.sol
Ez a kimenetet fogja generálni:
ethsec@1435b241ca60:/share$ slither bad-contract.solINFO: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-vulnerabilitiesINFO: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-callsINFO:Slither:bad-contract.sol analyzed (1 contracts with 46 detectors), 2 result(s) foundINFO:Slither:Use https://crytic.io/ to get access to additional detectors and Github integrationÖsszes megjelenítése
A Slither itt azonosította az újbóli belépés lehetőségét, meghatározta azokat a kulcsfontosságú sorokat, ahol a probléma felmerülhet, és linket adott a probléma további részleteihez:
Referencia: https://github.com/crytic/slither/wiki/Detector-Documentation#reentrancy-vulnerabilities
mely lehetővé teszi, hogy gyorsan megismerd a potenciális problémákat a kódoddal. Mint minden automatizált tesztelő eszköz, a Slither sem tökéletes, és a jelentések esetében hibázik túl sokat. Akkor is figyelmeztet potenciális újbóli belépésről, ha nincs is kihasználható sérülékenység. Gyakran a DIFFERENCE megtekintése a kód változtatások között a Slitherben rendkívüli felvilágosítással bírhat, mely segít felderíteni olyan sérülékenységeket, melyek sokkal korábban jöttek elő, minthogy a projekt kódja készen állt volna.
További olvasnivaló
Okosszerződés Biztonság Bevált Gyakorlatok Útmutató
- consensys.github.io/smart-contract-best-practices/
- GitHub
- A biztonsági ajánlások és a bevált gyakorlatok összesített gyűjteménye
Smart Contract Security Verification Standard (SCSVS)
Ismersz olyan közösségi anyagot, mely segített neked? Módosítsd az oldalt és add hozzá!
Kapcsolódó útmutatók
- Biztonságos fejlesztési workflow
- A Slither használata okosszerződés bugok felderítésére
- A Manticore használata okosszerződés bugok felderítésére
- Biztonsági irányelvek
- Token biztonság