Avval reentrancy hujumi nima ekanini va uning oldini olish mumkinligini sodda qilib tushuntiraman. So‘ng zaif joylar qayerda ekanini va hujumchi kodi qanday bo‘lishini ko‘rsatish uchun kod misollarini batafsil ko‘rib chiqaman. Eng muhimi, loyihangizdagi bitta emas, balki barcha smart-kontraktlarni himoya qilishning eng so‘nggi, sinovdan o‘tgan usullarini ko‘rsataman.
Spoiler: Agar nonReentrant() modifikatori haqida avval eshitgan bo‘lsangiz, o‘qishda davom eting — quyida globalNonReentrant() modifikatori va checks-effects-interactions namunasi bilan tanishasiz.
Yuqoridagi rasmda ContractA va ContractB’ni ko‘ryapmiz. Ma’lumki, aqlli kontrakt boshqa aqlli kontrakt bilan o‘zaro aloqa qilishi mumkin; masalan, bu holatda ContractA ContractB’ni chaqira oladi. Qayta kirishning eng asosiy g‘oyasi shuki, ContractA hali bajarilayotgan paytda ContractB unga qayta murojaat qila oladi.
Xo‘sh, hujumchi bundan qanday foydalanishi mumkin?
Yuqorida 10 ta Eterga ega ContractA borligini, ContractB esa ContractA’da 1 Eter saqlaganini ko‘ryapmiz. Bunday vaziyatda ContractB ContractA’dagi withdraw funksiyasidan foydalanib, Eterlarni o‘ziga qaytarib yubora oladi, chunki uning balansi 0 dan katta ekanini tekshiruvchi shartdan o‘tadi. Shundan so‘ng uning umumiy balansi 0 ga o‘zgaradi.
Endi ContractB withdraw funksiyasidan foydalanib, qayta kirish orqali ContractA’dagi barcha Eterlarni qanday o‘g‘irlashi mumkinligini ko‘rib chiqaylik. Aslida, hujumchiga ikkita funksiya kerak bo‘ladi: attack() va fallback().
Solidity’da fallback funksiyasi — nomi, parametrlari va qaytariladigan qiymatlari bo‘lmagan tashqi funksiya. Fallback funksiyasini har kim quyidagi holatlarda chaqirishi mumkin: kontraktda mavjud bo‘lmagan funksiyani chaqirganda; zarur ma’lumotlarni uzatmasdan funksiya chaqirganda; kontraktga hech qanday ma’lumotsiz Eter yuborganda.
Qayta kirish quyidagicha ishlaydi (strelkalarni bosqichma-bosqich kuzataylik): hujumchi attack() funksiyasini chaqiradi, uning ichida esa ContractA’dagi withdraw() funksiyasi chaqiriladi. Funksiya ichida ContractB balansining 0 dan katta ekanligi tekshiriladi va shunday bo‘lsa, bajarilish davom etadi.
ContractB balansi 0 dan katta bo‘lgani uchun u o‘sha 1 Eterini qaytarib yuboradi va fallback funksiyasini ishga tushiradi. Shu paytda ContractA’da 9 Eter bor, ContractB’da esa 1 Eter allaqachon mavjud.
Keyin fallback funksiyasi bajarilganda ContractA’dagi withdraw funksiyasi yana ishga tushadi va ContractB balansining 0 dan katta ekanligi qayta tekshiriladi. Yuqoridagi rasmga yana qarasangiz, uning balansi hali ham 1 Eter ekanini ko‘rasiz.
Demak, tekshiruvdan o‘tiladi va ContractB’ga yana bir Eter yuboriladi. Bu esa fallback funksiyasini ishga tushiradi. “balance=0” qatori hech qachon bajarilmasligiga e’tibor bering. Bu jarayon ContractA’dagi barcha Eterlar tugaguncha davom etadi.
___________
Endi Solidity kodida qayta kirishni aniqlash mumkin bo‘lgan aqlli kontraktni ko‘rib chiqaylik.
EtherStore kontraktida yuboruvchining balansini saqlaydigan va yangilaydigan deposit() funksiyasi, shuningdek, saqlangan barcha balanslarni birdaniga yechib oladigan withdrawAll() funksiyasi mavjud. withdrawAll() funksiyasining amalga oshirilishiga e’tibor bering: avval require orqali balans 0 dan katta ekanini tekshiradi, darhol undan keyin Eter yuboradi, yuboruvchining balansini 0 ga yangilashni esa oxiriga qoldiradi.
Bu yerda Attack kontrakti EtherStore kontraktidagi Eterlarni talon-toroj qilish uchun qayta kirishdan foydalanadi. Keling, uning kodini tahlil qilamiz:
Hujumchi konstruktorida nusxa yaratish va uning funksiyalaridan foydalana olish uchun EtherStore manzilini uzatadi.
Bu yerda EtherStore ushbu kontraktga Eter yuborganda chaqiriladigan fallback() funksiyasini ko‘ryapmiz. Uning ichida balans 1 ga teng yoki undan katta bo‘lsa, EtherStore’dagi withdraw funksiyasi chaqiriladi.
attack() funksiyasining ichida esa EtherStore’dan foydalanib hujum uyushtiradigan mantiq bor. Ko‘rib turganimizdek, avval yetarlicha Eterimiz borligiga ishonch hosil qilib, hujumni boshlaymiz. So‘ng EtherStore’dagi balansimiz 0 dan katta bo‘lishi va yechib olishni boshlashdan oldin tekshiruvlardan o‘tish uchun 1 Eter kiritamiz.
Yuqorida ContractA va ContractB misolida kod qanday ishlashini bosqichma-bosqich tushuntirdim. Endi, qisqacha umumlashtiraylik. Avval hujumchi attack() ni chaqiradi, uning ichida EtherStore’dagi withdrawAll() chaqiriladi, so‘ng Eter Attack kontraktining fallback funksiyasiga yuboriladi. Shu yerda qayta kirish boshlanib, EtherStore balansi talon-toroj qilinadi.
Xo‘sh, kontraktlarimizni qayta kirish hujumlaridan qanday himoya qila olamiz?
Ularni to‘liq himoya qilish uchun uchta oldini olish usulini ko‘rsataman. Bitta funksiya ichidagi qayta kirish, funksiyalararo qayta kirish va kontraktlararo qayta kirishning oldini olishni ko‘rib chiqaman.
Bitta funksiyani himoyalashning birinchi usuli — noReentrant deb nomlangan modifikatordan foydalanish.
Modifikator — boshqa funksiyalarning xatti-harakatini o‘zgartirish uchun ishlatiladigan maxsus funksiya turi. Modifikatorlar butun funksiyani qayta yozmasdan, unga qo‘shimcha shartlar yoki funksionallik qo‘shish imkonini beradi.
Bu yerda funksiya bajarilayotgan paytda kontraktni qulflaymiz. Shu tariqa bitta funksiyaga qayta kira olmaydi, chunki qayta kirish uchun funksiya kodidan o‘tib, so‘ng require ichidagi tekshiruvdan yana o‘tish maqsadida locked holat o‘zgaruvchisini false ga o‘zgartirish kerak bo‘ladi.
___________
Ikkinchi usul — kontraktlarimizni funksiyalararo qayta kirishdan himoya qiladigan Checks-Effects-Interactions patternidan foydalanish. Yuqoridagi yangilangan EtherStore kontraktida nimalar o‘zgarganini payqadingizmi?
Check-Effects-Interaction patternini chuqurroq o‘rganish uchun https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html sahifasini o‘qishni tavsiya qilaman.
Yuqorida chapdagi rasmda Eter yuborilgandan keyin balans yangilanadigan, zaif kod bilan taqqoslashni ko‘ryapmiz. Ko‘rib turganingizdek, bunday kodga umuman yetib borilmasligi mumkin. O‘ng tomonda esa balances[msg.sender] = 0 (ya’ni o‘zgarish) require(bal > 0) (tekshiruv) dan keyin, ammo Eter yuborilishidan (o‘zaro ta’sirdan) oldin bajariladigan qilib o‘zgartirilgan.
Shunday qilib, boshqa funksiya withdrawAll() ga murojaat qilgan taqdirda ham, bu kontrakt hujumchidan himoyalangan bo‘ladi, chunki Eter yuborilishidan oldin balans doimo yangilanadi.
Naqsh muallifi: https://twitter.com/GMX_IO
Sizga ko‘rsatmoqchi bo‘lgan uchinchi usul — kontraktlararo qayta kirishdan himoyalanish uchun GlobalReentrancyGuard kontraktini yaratish. Shuni tushunish muhimki, bu usul bir nechta kontrakt o‘zaro aloqada bo‘ladigan loyihalarda qo‘llanadi.
Bu yerdagi g‘oya birinchi usulda tushuntirgan noReentrant modifikatoridagi kabi: funksiya modifikatorga kiradi, kontraktni qulflash uchun o‘zgaruvchini yangilaydi va kod bajarilishi tugamaguncha uni qulfdan chiqarmaydi. Katta farqi shundaki, bu yerda funksiya chaqirilgan yoki chaqirilmaganini tekshirish uchun alohida kontraktda saqlanadigan o‘zgaruvchidan foydalanamiz.
Bu yerda g‘oyani tushunishga yordam berishi uchun haqiqiy kodsiz, faqat funksiya nomlaridan foydalanib misol yaratdim. Tajribamga ko‘ra, bu vaziyatni shunchaki so‘z bilan tushuntirishdan ko‘ra yaxshiroq tasavvur qilishga yordam beradi.
Bu yerda hujumchi ScheduledTransfer kontraktidagi funksiyani chaqiradi. Shartlar bajarilgach, funksiya ko‘rsatilgan Eterlarni AttackTransfer kontraktiga yuboradi. Natijada fallback funksiyasi ishga tushadi va ScheduledTransfer kontrakti nuqtayi nazaridan tranzaksiya “bekor qilinadi”, ammo hujumchi Eterlarni oladi. Shu tariqa u ScheduledTransfer’dagi barcha Eterlarni talon-toroj qilguncha siklni boshlaydi.
Yuqorida aytib o‘tgan GlobalReentrancyGuard’dan foydalanish bunday hujum ssenariysining oldini oladi.
__________________
Aqlli kontraktlar, Web3 xavfsizligi, Solidity, aqlli kontraktlarni audit qilish va boshqa mavzulardagi kundalik yangiliklar uchun Twitter’da @TheBlockChainer sahifasini toping.
__________________