בייט אחד הפך את הכותרת מ-20 בייט ל-60, ו-32 בייט מזיכרון המכונה הפיזית יצאו לרשת: PwnEscape בקרנל לינוקס
הבייט הראשון בכל חבילת IPv4 אומר שני דברים בבת אחת: שזו גרסה 4 של הפרוטוקול, ושכותרת החבילה ארוכה חמש מילים של 32 ביט, כלומר 20 בייט. בכתיב הקסדצימלי הוא נראה 0x45. במחקר שפורסם השבוע הבייט הזה הפך ל-0x4f בדיוק אחרי שהקרנל של המכונה הפיזית סיים לבדוק אותו, קיבל על בסיסו החלטות ניתוב והעביר את החבילה הלאה. ספרת הגרסה נשארה 4, ולכן שום בדיקה לא נדלקה. אורך הכותרת, לעומת זאת, קפץ מ-20 בייט ל-60.
חוקרי חברת האבטחה pwn.ai פרסמו ב-15 באוקטובר את הניתוח המלא של החולשה שהם מכנים PwnEscape, שמתועדת כ-CVE-2026-99999 בציון CVSS 9.3. היא לא יושבת ב-hypervisor, הרכיב שאמור להפריד בין מכונות וירטואליות, אלא ב-stack הרשת של לינוקס עצמו. קוד שרץ בתוך מכונה וירטואלית של לקוח הצליח דרכה לקרוא זיכרון של המכונה הפיזית שמריצה אותו, להפיל אותה, ובמעבדה גם להריץ עליה פקודות עם הרשאות root. בהמשך נראה איך מטא-דאטה של checksum הופכת להרשאת כתיבה, למה דווקא פיצול חבילות הוא נקודת הכשל, ומה הקרנל תיקן בפועל.
מה מפריד בין לקוח אחד לשני בענן
ב-18 באוגוסט הכריזה Vercel על תחרות בפרס מיליון דולר שהזמינה חוקרים לנסות לפרוץ את גבול הבידוד של מוצר ה-Sandbox שלה, סביבה שבה לקוחות מריצים קוד זר. כל לקוח שם מקבל microVM נפרדת של Firecracker, מכונה וירטואלית קטנה עם קרנל לינוקס משל עצמה, שרצה על שרתי EC2 פיזיים של אמזון. החומה הזאת היא מה שמפריד בין לקוח אחד לשני, וגם בין לקוח לבין המכונה שמריצה אותו.
בדוח המסכם שפרסמה Vercel ב-14 באוקטובר היא מספרת על 1,285 דיווחים בשבועיים, 242 אלף דולר בפרסים, ובסך הכול ממצא אחד בדרגת Critical. הממצא הזה הוא PwnEscape, והוא הגיע ביום השלישי של התחרות. אנדי ריאנצ'ו, מהנדס אבטחה ראשי בחברה, מייחס את נפח הדיווחים לכך שחוקרים "מחפשים עכשיו במהירות של AI". את החולשה עצמה Vercel לא תולה בהגדרה שגויה אצלה, ו-pwn.ai מצדם כתבו שה-sandbox היה מוקשח מעל הסטנדרט המקובל בתעשייה ושלא מצאו בו תקלת הגדרה.
הבטחה לכתוב אחר כך
כדי לחסוך עבודה, לינוקס לא תמיד מחשב את ה-checksum של חבילה ברגע שהיא נוצרת. הוא יכול לסמן אותה כ"החישוב עוד לפנינו", לצרף אליה שני מספרים, ולהשאיר את החישוב בפועל לכרטיס הרשת או לפונקציית עזר בהמשך הדרך. המספר הראשון אומר מאיפה להתחיל לסכום, השני אומר כמה לזוז משם כדי להגיע לשדה שבו צריך לרשום את התוצאה. למצב הזה קוראים CHECKSUM_PARTIAL, ולשני השדות csum_start ו-csum_offset.
כאן נמצא שורש הבעיה. שני המספרים האלה לא מתארים את החבילה, הם מרשים כתיבה לתוכה. מקום הכתיבה הוא בדיוק csum_start פלוס csum_offset, ובתרחיש של מכונה וירטואלית שניהם מגיעים מה-guest, כלומר מהצד הלא מהימן. הבדיקה ההיסטורית בכניסה, בפונקציה __virtio_net_hdr_to_skb, השוותה את ההיסט הזה למקום שבו עומד מצביע הנתונים הנוכחי של החבילה. היא לא בדקה כמה רחוק ההיסט מכותרת הרשת עצמה, ואלה שני דברים שונים: חבילה שנכנסת עם כותרות שכבת קישור שעוד מחוברות אליה נראית אחרת לגמרי אחרי שהכותרות האלה מוסרות לצורך עיבוד L3. pwn.ai מתארים את זה כאי-התאמה בין שתי מערכות קואורדינטות.
הכותרת נבדקה, ואז היא השתנתה
החלק המעניין הוא סדר הפעולות. הקרנל של המכונה הפיזית מקבל את החבילה, מפרסר כותרת IPv4 רגילה באורך 20 בייט, ומריץ עליה הרכבה מחדש של פרגמנטים, מדיניות וניתוב. כל ההחלטות האלה מתקבלות על כותרת באורך 20 בייט. רק אחר כך, בשלב הפיצול ליציאה, הקוד מגיע ל-checksum שעוד לא חושב. בגרסה שלפני התיקון, הפונקציה ip_do_fragment השלימה את ה-checksum לפני שלקחה את המצביע לכותרת ה-IPv4 ואת אורכה.
זה הרגע שבו ההבטחה נפרעת לתוך המקום הלא נכון. פונקציית ההשלמה כותבת את התוצאה ב-csum_start פלוס csum_offset, וכשהערכים האלה הוזנו בזדון, הכתיבה נוחתת בתוך הכותרת שכבר נבדקה. ארבעה ביטים בבייט הראשון הם שדה IHL, שמונה את אורך הכותרת במילים של 32 ביט. 0x45 הפך ל-0x4f: חמש מילים נעשו חמש-עשרה, 20 בייט נעשו 60, וספרת הגרסה לא זזה. החוקרים קוראים לזה בעיית time-of-check/time-of-use במובן הסמנטי ולא מרוץ בין תהליכונים, כי אין כאן חלון זמן מקרי בכלל. "חלקים שונים של לינוקס הורשו להאמין לגרסאות שונות של אותה חבילה", הם כותבים.
ומאיפה מגיעים הבייטים שדולפים? במקרה שהחוקרים בנו, הנתונים הרציפים של החבילה הסתכמו ב-28 בייט: 20 של הכותרת המקורית ועוד 8 בייט מטען שהיו בפרגמנט הראשון. מכונת הפיצול, שכבר האמינה לכותרת בת 60 בייט, התייחסה ל-40 הבייטים שאחרי הכותרת המקורית כאל אפשרויות IPv4 והעתיקה אותם לחבילה היוצאת. שמונה מהם היו מטען אמיתי. 32 הבייטים הנותרים כבר לא היו חלק מהנתונים הרציפים של החבילה, אלא זיכרון של המכונה הפיזית שיושב אחריהם. הם יצאו לרשת בתוך שדה האפשרויות, כ-32 בייט בכל חבילה, וה-guest יכול להמשיך לשלוח עוד ועוד.
מה הוכח על שרתי ייצור, ומה רק במעבדה
ההבחנה הזאת חשובה, והחוקרים מקפידים עליה בעצמם. דליפת הזיכרון הודגמה על סביבות ה-Sandbox הפעילות של Vercel. ההפלה והרצת הקוד, לעומת זאת, הודגמו רק על מכונות KVM ו-Firecracker של החוקרים עצמם, שהוגדרו כדי להתאים לתצורה של Vercel. שתי חבילות הספיקו שם כדי להפיל מכונה פיזית ולהוריד איתה כל סביבות ה-sandbox שרצו עליה, ומאותה חולשה הגיעו החוקרים גם לשליטה מלאה על המכונה. pwn.ai מפרידים במפורש בין סימון root על מכונה שבשליטתם ובין תוצאה בסביבת ייצור של ספק.
לפי Vercel, אף דיווח בתחרות לא הראה קריאה של קבצים, סודות או נתוני אפליקציה של לקוח. החולשה דווחה בערוץ פרטי לפני כל פרסום, היא לא מופיעה בקטלוג ה-KEV של CISA, ולא דווח על ניצול שלה בעולם האמיתי. גם וקטור התקיפה ברשומת ה-CVE הוא מקומי: כדי להפעיל את זה צריך כבר להריץ קוד בתוך guest, בדיוק כמו החוקרים. זו לא חולשה שמישהו מנצל מהאינטרנט הפתוח.
ועוד פרט על הדרך שבה זה נמצא. pwn.ai מספרים שהמחקר רץ במסגרת פנימית שלהם בשם Omega-Pwn, שהמודל GPT-5.6 Sol שימש בה מנוע ההיגיון המרכזי לניתוח קוד המקור, ושהחוקרים הם שהציבו את השאלות ואימתו כל ממצא מול התנהגות אמיתית של הקרנל על מכונות שבשליטתם. מההכרזה על התחרות ועד הפרימיטיב הראשון עברו שלושה ימים.
התיקון, והחלק שעוד חסר
את התיקון שנכנס בסוף כתב מיכאל ס. צירקין מ-Red Hat, והגרסה שהוגשה ב-7 באוקטובר נקראת ipv4: validate checksum_start before completing checksum. היא מוסיפה פונקציית עזר קטנה בשם ip_check_csum_start שבודקת דבר אחד: שנקודת ההתחלה של ה-checksum לא נמצאת לפני סוף כותרת ה-IPv4. אם היא כן, החבילה נדחית. את הבדיקה הזאת הטלאי משבץ לפני השלמת ה-checksum בחמישה מקומות שונים בקוד הרשת, בהם ip_do_fragment, הפיצול של ה-bridge ב-netfilter ו-xfrm_output.
הטלאי מוגדר במפורש כהגנה בשכבות, ולא כפתרון שלם. בהודעת ה-commit עצמה כתוב ש"אימות מלא יותר בכניסה נמצא בדרך, אבל דורש עוד עבודה". הבדיקה מגנה על כותרת ה-IPv4 מפני כתיבה, והיא לא מתיימרת להוכיח שיעד הכתיבה הוא שדה checksum תקין של TCP או UDP. pwn.ai מזהירים בנפרד שלא לקרוא את התיקון כאילו הוא סוגר כל יעד אפשרי של השלמת checksum בכל פרוטוקול.
לפי רשומת ה-CVE שפרסם ה-CNA של לינוקס, החולשה קיימת בקרנל מגרסה 2.6.27 והלאה. הגרסאות הראשונות שמתוקנות בכל ענף יציב הן 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.57 ו-7.2.11, ובענף הראשי התיקון נכנס ב-7.3-rc7. הציון 9.3 מגיע מה-CNA של לינוקס ולא מ-NVD, שהרשומה אצלה עדיין מסומנת כ"התקבלה" ולא עברה ניתוח משלה. מה שדוחף את הציון למעלה הוא שינוי ההיקף: החולשה מתחילה ב-guest ומסתיימת בקרנל של המכונה שמאחסנת אותו.
מה זה אומר בפועל
אם אתם מריצים מכונות וירטואליות של גורמים שאתם לא סומכים עליהם, ובמיוחד אם אתם מפעילים סביבת הרצה לקוד של לקוחות או של סוכני AI, הקרנל שצריך לעדכן הוא זה של המכונה הפיזית. עדכון הקרנל בתוך ה-guest לא עושה כאן כלום, וצריך גם לאתחל אל הגרסה החדשה. מכיוון שהפצות לינוקס מגלגלות תיקונים כאלה בלי לשנות את מספר הגרסה, בדקו את הודעת האבטחה של ההפצה שלכם ולא רק את הפלט של uname. ולמי שלא מריץ וירטואליזציה של קוד זר, הלקח של החוקרים שווה יותר מהטלאי: מטא-דאטה שעוברת גבול אמון צריכה להישאר תקפה אצל כל צרכן שלה בהמשך הדרך, גם כשהוא רץ חצי מסלול אחרי הבדיקה שאישרה אותה.
תגובות