لیستی از حملات و نمونه کدهای مشکل دار
نوشته شده توسط @Heidari
حملات ورود مجدد (SWC-107)
حملات ورود مجدد می توانند مشکل ساز باشند زیرا فراخوانی قرارداد های هوشمند خارجی کنترل اجرا را به آن قرارداد ها منتقل می کند. قرارداد فراخوانی شده می تواند کنترل اجرا را از آن خود کند و در نهایت قرارداد هوشمند فراخواننده را به صورت تکراری فراخوانی کند.
// INSECURE
mapping (address => uint) private userBalances;
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
require(msg.sender.call.value(amountToWithdraw)()); // At this point, the caller's code is executed, and can call withdrawBalance again
userBalances[msg.sender] = 0;
}
آگر امکان حذف فراخوانی قرارداد های خارجی برای شما فراهم نیست، آسان ترین راه برای پیشگیری از این حمله آن است که تمام عملیات داخلی را قبل از فراخونی قرارداد خارجی انجام دهیم.
mapping (address => uint) private userBalances;
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
userBalances[msg.sender] = 0;
require(msg.sender.call.value(amountToWithdraw)()); // The user's balance is already 0, so future invocations won't withdraw anything
}
راه حل دیگر، استفاده از الگوی طراحی withdrawal و تفکیک منطق حسابداری قرارداد، از منطق انتقال آن است.
موضوع دیگری که باید از آن آگاه باشیم وجود بالقوه cross function re-entrancy است. این موضوع در صورتی که قرارداد شما دارای چندین تابع باشد که همگی یک وضعیت واحد را تغییر می دهند، می تواند مشکل ساز باشد.
// INSECURE
mapping (address => uint) private userBalances;
function transfer(address to, uint amount) {
if (userBalances[msg.sender] >= amount) {
userBalances[to] += amount;
userBalances[msg.sender] -= amount;
}
}
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
require(msg.sender.call.value(amountToWithdraw)()); // At this point, the caller's code is executed, and can call transfer()
userBalances[msg.sender] = 0;
}
در این مورد، مهاجم می تواند در زمانیکه کد او با یک فراخوانی خارجی در withdrawBalance اجرا می شود، تابع transfer() را فراخوانی کند. به دلیل اینکه موجودی هنوز به صفر تغییر داده نشده است، مهاجم قادر خواهد بود با وجود اینکه قبلا withdrawal را دریافت کرده است، توکن ها را منتقل کند. این آسیب پذیری همچنین در حمله DAO مورد استفاده قرار گرفته بود.
حال به این موضوع بیاندیشید که اگر چندین تابع از قرارداد فراخوانی شوند چه روی خواهد داد!
راه های مختلفی وجود دارند که می توانند از شدت این مساله بکاهند.
یک ایده خوب کلی این است که مانند الگوی طراحی withdrawal ، تغییر وضعیت داخلی قرارداد خود را پیش از فراخوانی قراردادهای خارجی انجام دهید. از الگوهای طراحی که جواب خود را در گذشته پس داده اند استفاده کنید، از اشتباهات دیگران درس بگیرید و به راهنمایی های آنها توجه کنید.
یک راه حل پیچیده تر میتواند پیاده سازی دسترسی انحصاری، یا mutex، باشد. این روش به شما اجازه می دهد که وضعیت را قفل کنید و تنها امکان تغییر وضعیت توسط صاحب قفل امکان پذیر باشد.
برای درک عمیق تر این حملات شناخته شده می توانید به لینک زیر مراجعه کنید:
Ethereum Smart Contract Best Practices - Known Attacks
وابستگی به ترتیب و برچسب زمانی تراکنش ها (SWC-114)
نمونه های قبلی ذکر شده شرایط رقابتی، در مورد شرایطی هستند که مهاجم یک کد مخرب را در یک تراکنش واحد اجرا می کند. اینجا تمرکز ما بر چگونگی گنجانده شدن تراکنش ها در بلاک چین و ملاحظاتی حول این فرآیند است.
تراکنش های ارشال شده روی شبکه که هنوز در یک بلاک گنجانده نشده اند در mempool قرار دارند.
تصمیم گیری در مورد ترتیب انتخاب تراکنش ها از درون mempool به درون یک بلاک، توسط ماینر آن بلاک انجا می شود. همچنین، از آنجا که تراکنش ها قبل از قرار گرفتن در بلاک در mempool قرار دارند، هر کسی می داند که چه تراکنش هایی در آستانه اجرا در شبکه قرار دارند. این موضوع می تواند در مواردی مانند بازار های غیرمتمرکز مساله ساز شود. محافظت در برابر این مساله سخت است و شما احتمالا نیاز خواهید داشت که از راه حل های های خاصی استفاده کنید.
بازار های غیرمتمرکز می توانند با پیاده سازی حراج های دسته ای یا استفاده از شمای pre-commit، که در آن جزییات بعد از ثبت تایید شده تراکنش ثبت می شوند، نگرانی ها را کاهش دهند.
اطلاعات بیشتر در مورد وابستگی به ترتیب تراکنش ها و نمونه های آن را می توانید در لینک زیر ببینید:
Overflow و Uderflow اعداد(SWC-101)
اعداد میتوانند در EVM دچار overflow یا underflow شوند. این اتفاق زمانی رخ می دهد که یک مقدار حسابی از حداقل یا حداکثر اندازه یک نوع تجاوز می کند.
مقدار حداکثر برای یک عدد صحیح بدون علامت برابر است با 2^256-1که تقریبا 1.15 برابر 10^77 است. اگر یک عدد صحیح overflow کند، مقدار آن به صفر بازمیگردد. برای مثال، مقدار یک متغیر با نام score که نوع آن uint8 و مقدار ذخیره شده در آن برابر 255 است با یک بار افزایش برابر با صفر خواهد شد.
نگرانی یا عدم نگرانی در مورد overflow اعداد صحیح کاملا بستگی به قرارداد هوشمند شما دارد.
یک متغیر که می تواند توسط کابر تغییر کند باید برای overflow چک شود، در حالیکه overflow متغیری که فقط مقدارش افزایش پیدا می کند تقریبا غیرممکن است.
Underflow مشابه با Overflow است. در حالتی که مقدار یک uint به پایین تر از مقدار حداقل برسد، مقدار آن به مقدار حداکثر تغییر می کند.
انواع داده ای کوچک تر مانند uint8، uint16، و …احتیاج به مراقب بیشتری دارند زیرا خیلی سریع تر به مقدار حداکثر خود می رسند.
اطلاعات بیشتر و نمونه ای این موضوع را میتوانید در لینک زیر بیابید:
عدم پذیرش سرویس با فراخوانی ناموفق (SWC-113)
یک خطر بالقوه در ارتباط با انتقال اجرا به یک قرارداد دیگر حمله عدم پذیرش سرویس است.
// INSECURE
contract Auction {
address currentLeader;
uint highestBid;
function bid() payable {
require(msg.value > highestBid);
require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert
currentLeader = msg.sender;
highestBid = msg.value;
}
}
در مثال بالا، highestBidder می تواند یک قرارداد دیگر باشد و انتقال وجه به آن قرارداد می تواند تابع fallback آن قرارداد را راه اندازی کند. اگر تابع fallback همیشه یک خطا برگرداند، تابع bid از قرارداد Auction بی استفاده و همیشه با خطا مواجه خواهد شد. تابع bid برای اجرای کامل نیازمند اجرای موفقیت آمیز عملیات transfer است.
قرارداد موجود در آدرس highestBidder یک exception برمیگرداند، اجرا متوقف می شود و exception به قرارداد فراخواننده منتقل می شود و جلوی اجرای بیشتر را می گیرد.
این مشکل با استفاده از الگوی طراحی withdrawal قابل پیشگیری است.
عدم پذیرش سرویس با Block Gas Limit یاstartGas (SWC-128)
در مورد میزان محاسباتی که می تواند در یک بلاک اتریوم گنجانده شود محدودیتی وجود دارد که در حال حاضر برابر با ارزش 10,000,000 gas است. این یعنی که اگر قرارداد شما به وضعیتی برسد که یک تراکنش برای اجرا نیازمند بیش از 10,000,000 gas است، آن تراکنش هرگز با موفقیت اجرا نخواهد شد و همواره پیش از پایان به سقف مجاز gas برای بلاک خواهد رسید.
به طور مشابه، اگر gas مورد نیاز برای قرارداد کمتر از 8,000,000، اما نزدیک به آن باشد، شما ممکن است برای گنجانده شدن تراکنش در یک بلاک توسط یک ماینر با مشکل مواجه شوید. وقتی شما یک تراکنش با startGas نزدیک به 8,000,000 را به شبکه ارسال می کنید، بسیار محتمل است که هیچ ماینری آن را برای قراردادن در بلاک انتخاب نکند.
در مورد گزینه های مربوط به ترتیب پیش فرض mining تراکنش ها در محبوب ترین کلاینت ها(geth و parity) می توانید اطلاعات بیشتری در لینک زیر بدست بیاورید.
StackOverflow: what-is-the-default-ordering-of-transactions-during-mining
این وضعیت در صورتی که قرارداد شما دارای حلقه روی یک آرایه با اندازه نامشخص باشد، محتمل خواهد بود. اگر اندازه این آرایه بسیار بزرگ شود، این حلقه هرگز به طور کامل اجرا نخواهد شد. یک نمونه که در آن وجود یک آرایه با اندازه پویا منجر به عدم پذیرش سرویس می شود را در لینک زیر میتوانید مشاهده کنید:
ارسال اجباری ether
یک خطر دیگر استفاده از منطقی است که وابسته به موجودی قرارداد است.
توجه داشته باشید که ارسال ether به یک قرارداد بدون راه اندازی تابع fallback آن امکان پذیر است.
استفاده از تابع selfdestruct در یک قرارداد دیگر و استفاده از قرارداد هدف به عنوان دریافت کننده، قرارداد نابود شده را مجبور می کند که موجودی اش را به قرارداد هدف ارسال کند.
همچنین احتمال پیش-محاسبه آدرس یک قرارداد و ارسال ether به آن پیش از بارگزاری قرارداد وجود دارد. در این صورت موجودی قرارداد پیش از بارگزاری بزرگ تر از صفر خواهد بود.
منابع بیشتر
- حملات شناخته شده
https://consensys.github.io/smart-contract-best-practices/known_attacks/
- مروری بر حملات به قرارداد های هوشمند اتریوم
- لیست باگ های شناخته شده از مستندات Solidity
https://docs.soliditylang.org/en/develop/bugs.html
- نقاط ضعف قراردادهای هوشمند
- در مورد این آسیب پذیری ها با بکارگیری Ethernet و با استفاده از قرارداد های بارگزاری شده روی testnet یاد بگیرید
https://solidity-05.ethernaut.openzeppelin.com/
- چالش ها را با استفاده از آسیب پذیری های موجود در CaptureTheEther CTF حل کنید
- مروری بر آسیب پذیری های شناخته شده با نمونه ها و چگونکی رفع آنها