מה גרם להשבתת CrowdStrike?
ב-19 ביולי 2024, כ- 8.5 מיליון מחשבים ברחבי העולם המריצים Windows ,
קרסו ולא הצליחו לאתחל מחדש, נשארים ב "מסך הכחול של המוות".
ההשבתה השפיעה על עסקים וממשלות ברחבי הגלובוס, פגעה ברוב הענפים בתחבורה, שירותים פיננסיים, בריאות ועוד.
מסך כחול של Windows (מקור: Wikipedia)
כצפוי, זה העלה מיד חששות מפני מתקפת סייבר רחבת היקף. האם זו הייתה מתקפת ההאקרים העולמית הארוכה שתמיד היה חשש מפניה, שנועדה לשבש את עולמנו המבוסס על מחשבים ולגרום לכאוס עולמי?
למרבה המזל, לא.
בתוך שעות לאחר ההשבתה, CrowdStrike אישרו כי עדכון פגום בתוכנת הגנת הנקודות הקצה שלהם, במיוחד חיישן ה- Falcon, גרם לבעיה.
למרות שקוד המקור המושפע לא פורסם, פוסט הבלוג הזה מסכם מה ש-CrowdStrike אישרו בפומבי, ובוחן בעיות קוד שעלולות היו להוביל להשבתה עולמית זו. המטרה שלנו היא להאיר על סוגי הבאגים שיכולים לגרום לבעיות רציניות באמינות תוכנה באופן כללי, ולמה לתפוס בעיות קוד בשלב מוקדם בתהליך הפיתוח חשוב כמו לתפוס פגיעויות אבטחה (vulnerabilities).
מה קרה: מה אנו יודעים עד כה?
חיישן Falcon של CrowdStrike הוא סוכן קל-משקל (lightweight agent) שאוסף נתוני נקודת קצה ומגן על מחשב מפני מתקפות סייבר. כדי לנטר תהליכי מערכת, לזהות פעילות זדונית ולתגובה לאיומים בזמן אמת, הוא זקוק לגישה לפונקציות מערכת נמוכות רמה. זה מחייב אותו להריץ דרייבר קרנל
(kernel driver) של Windows, שנכתב בדרך כלל ב-C ו- ++C. מכיוון שאין לאפשר לו לנטרל את ההגנה בקלות, דרייבר זה מסומן כדרייבר אתחול, מה שהופך אותו לחיוני לאתחול Windows.
משמעות הדבר היא שהפלקון הופך לרכיב חשוב ורגיש של מערכת ההפעלה ברגע שהוא מותקן. לסיכום:
- דרייבר הקרנל נדרש כדי ש-Windows יאותחל.
- דרייבר הקרנל יש יכולות נרחבות לאינטראקציה ישירה עם חומרה, ניהול משאבי מערכת וגישה לזיכרון מוגן.
- דרייבר הקרנל משפיע על התנהגות הליבה של מערכת ההפעלה.
בגלל האחריות העצומה והאמון שניתן לדרייברי קרנל, הם חייבים לעבור בדרך כלל בדיקות מקיפות באמצעות תוכנית Windows Update של Microsoft.
חבילות דרייבר שעוברות את הבדיקות של Windows Hardware Lab Kit נחתמות דיגיטלית על ידי Microsoft ומסומנות כמהימנות. למרות שהדרייבר של פלקון עצמו גם חתום, בדיקה מלאה באמצעות Windows Hardware Lab Kit דורשת זמן. כדי להגיב במהירות לטכניקות חדשות של תוקפי סייבר, פלקון צריך לאמץ גישה גמישה יותר לשינויי דרייבר הקרנל שלו.
לצורך זה, CrowdStrike מספקים תוכן תגובה מהירה שמגיע בצורת עדכון תצורת תוכן. עדכונים אלו מכילים קבצי ערוץ שהדרייבר טוען באופן דינמי. קבצים אלו משפיעים על אופן פעולתם של דרייברי הקרנל.
העדכון שגרם להשבתה הכיל קובץ ערוץ פגום, מה שגרם לדרייבר הקרנל לקרוא זיכרון מחוץ לתחום [source]. בעוד שיישום ברמת המשתמש היה פשוט קורס בשל בעיה כזו, דרייבר קרנל הנמצא בלב מערכת ההפעלה גורם לכל המערכת לקרוס – מה שגורם למסך הכחול הידוע לשמצה שראינו במהלך ההשבתה.
חקירת גורמים אפשריים בקוד
התקרית סיקרנה מומחים ברחבי העולם שהתעניינו בקביעת הגורם המדויק לבעיה זו של קריאת זיכרון מחוץ לתחום. למרות שכמה מאלו כבר הוכחו כשגויים, ו-CrowdStrike לא פרסמו את קוד המקור הפגום, אנו נבחן תרחישים שעלולים לגרום לבעיה כזו.
השמטת מצביע ריק (Null Pointer Dereference)
מצביע ב- C ו- ++C הוא משתנה שמאחסן כתובת זיכרון, מה שמאפשר מניפולציה ישירה של נתונים וניהול זיכרון יעיל. מצביע לריק, הידוע גם כמצביע null, נוצר על ידי אתחול אובייקט מצביע ל-0, NULL, או במקרה של ++C, זהו nullptr.
מצביע null אינו מצביע לאובייקט או לזיכרון חוקי, ובשל כך ניהולו או גישה לזיכרון שאליו הוא מצביע היא התנהגות לא מוגדרת, שלרוב גורמת לקריסת המערכת כולה במקרה של דרייבר קרנל:
int deref() {
int* ptr = 0;
// Noncompliant: dereference of a null pointer
return *ptr;
}
בנוסף לשימוש באופרטור *, גישה ל-member במבנה (באמצעות ->) או לאלמנט במערך (באמצעות []) גם כן מבצעים ניהול של המצביע וסביר להניח שיגרמו לקריסה אם מבוצעים על מצביע ל-null:
struct Aggregate {
int x;
int y;
};
int memberAccess() {
struct Aggregate* ptr = 0;
// Noncompliant: member access on a null pointer
return ptr->x;
}
תוכלו למצוא עוד על השמטת מצביעים ריקים בתיעוד כלל S2259 שלנו. בעוד שקהילת האבטחה החשיבה תחילה השמטת מצביע ריק כגורם להשבתה [source], זה הוכח מאוחר יותר כשגוי [source]. במקום זאת, יש חשד שגורם השורש הוא משתנה לא מאותחל.
משתנים לא מאותחלים
משתנים מקומיים ב-C ו- ++C חייבים להיות מוכרזים כדי להקצות זיכרון ויכולים להתאכלס בערך מסוים בעת ההכרזה. משתנה מקומי מסוג מובנה (כמו int, float ומצביעים), המוכרז ללא ערך ראשוני, אינו מאותחל לערך מסוים מכיוון שתהליך זה מוסיף עומס חישובי קל. כתוצאה מכך, אם לא מוקצה ערך כזה למשתנה קודם, המשתנה מחזיק ערך שרירותי שנשאר במיקום הזיכרון שלו מפעולות קודמות של התוכנית, מה שגורם להתנהגות לא מכוונת:
int addition() {
// x is not initialized
int x;
// Noncompliant: value of x undefined
return x + 10;
}
int dereference() {
// p is not initialized
int* p;
// Noncompliant: value of p undefined
return *p;
}
באופן דומה, מבנים שמאגדים פשוט משתנים מסוג מובנה, כמו מערכים או סוגי struct/class ללא בנאי, לא יאתחלו את חבריהם כשהם מוכרזים ללא מאתחל:
struct Aggregate {
int i;
float f;
};
void aggregates() {
// each element of array is not initializer
int* intArray[5];
// members aggr.i, agrr.f are not initialized
Aggregate aggr;
// members of each element are not initialized
Aggregate aggrArray[2];
}
לבסוף, הקצאת אובייקטים מסוג מובנה או סוגי אגרגטים כאלה על הערמה גם לא מאתחלת את ערכיהם:
void usingMalloc() {
// each of 10 allocated integers is not initialized
int* intArr = (int*)malloc(sizeof(int) * 10);
}
זה גם חל כאשר new משמש ב- ++C :
void usingNew() {
// members of allocated Aggregate are not initialized
Aggregate* aggrPtr = new Aggregate;
Aggregate* aggrArr = new Aggregate[5];
}
תוכלו למצוא עוד על משתנים לא מאותחלים בתיעוד כלל rule documentation) S836) שלנו.
חוסר אתחול משתנים הוא סוג אחד של בעיה שיכולה להוביל לקריאת זיכרון מחוץ לתחום, כפי שמוזכר בסקירת הפוסט-תקרית המקדמית של [CrowdStrike [source. אבל בעיות אחרות גם יכולות להוביל לקריאות זיכרון מחוץ לתחום. אנו נבחן בעיות אלו באופן כללי.
גישה לזיכרון מחוץ לתחום
מערכים ומאגרי נתונים הם בלוקים רציפים של זיכרון שניתן לגשת אליהם באמצעות אינדקסים מספריים כדי להתייחס לאלמנטים בודדים. חריגות מערך וחריגות מאגר מתרחשות כאשר גישה לזיכרון עולה במקרה על הגבול של המערך או המאגר שהוקצה. גישות חריגות אלו גורמות לחלק מהתקלות המזיקות והקשות ביותר למעקב. לא רק שגישות אלו מהוות התנהגות לא מוגדרת, הן גם מכניסות לעיתים קרובות פגיעויות אבטחה.
בעיה מסוג זה יכולה, לדוגמה, להתרחש כאשר מתייחסים לאלמנטים של מערך:
void access_exceeds(void) {
int id_sequence[3];
id_sequence[0] = 100;
id_sequence[1] = 200;
id_sequence[2] = 300;
// Noncompliant: memory access is out of bounds
id_sequence[3] = 400;
// Accessed memory exceeds upper limit of memory block
}
באופן דומה, מצביע יכול לגשת לזיכרון מחוץ לתחום:
void access_precedes(int x) {
int buf[100];
int *p = buf;
--p;
// Noncompliant: memory access is out of bounds
p[0] = 9001;
// Accessed memory precedes memory block
}
בנוסף, קריאות לא בטוחות לפונקציות כמו memcpy עשויות להכניס גישה לזיכרון מחוץ לתחום:
void memcpy_example(void) {
char src[] = {1, 2, 3, 4};
char dst[10];
// Noncompliant: memory copy function accesses out-of-bound array element
memcpy(dst, src, 5);
}
תוכלו למצוא עוד על גישה לזיכרון מחוץ לתחום בתיעוד כלל S3519 שלנו.
מה ניתן ללמוד מהשבתת CrowdStrike?
באגים הם חלק בלתי נמנע מהפיתוח תוכנה ומתרחשים באופן קבוע בקוד – כל קוד פגיע. כאן, הצגנו איך שלושה סוגי באגים שונים יכולים להוביל להשבתה בדיוק כמו זו. למרות שקוד המקור המושפע לא פורסם, ברור כי תיקון כל הבעיות הללו הוא חיוני.
השבתה זו מזכירה לנו את ההשפעה של בעיית קוד קטנה – הנזק הכלכלי בלבד עשוי להגיע לעשרות מיליארדי דולרים [source].
מושקעת תשומת לב רבה לאבטחת תוכנה וקוד, אבל בעיות אמינות ותחזוקה לעיתים קרובות מוזנחות. תוכלו לדבר עם הצוות שלנו על מציאת ותיקון בעיות אלו בשלב מוקדם בתהליך הפיתוח כאן.