דלת אחורית שלא מתקשרת לאף שרת: SLEEPWALKER מחכה לחבילת רשת אחת, ומריצה תוכנית בשפה משלה
קובץ בשם dpapi.dll יושב בתיקייה של סוכן הניהול של ESET. הוא שוקל 59,904 בתים, אין לו חתימה דיגיטלית, ופרטי היצרן שבתוכו הועתקו מ-ESET עצמה עד לשורת זכויות היוצרים. בכל פעם שהשירות עולה הוא נטען לתוך ERAAgent.exe, ואז לא עושה כלום: לא פותח פורט להאזנה, לא פונה לשום שרת באינטרנט, ואין בתוכו שום מטען מוכן להרצה. מערכת שמחפשת תעבורה יוצאת לכתובות מוכרות לרעה לא תראה ממנו דבר, מהסיבה הפשוטה שאין תעבורה יוצאת.
את הניתוח של הקובץ הזה פירסם ב-24 באוגוסט Dominik Reichel, חוקר נוזקות גרמני שעבד עד 2025 בפאלו אלטו נטוורקס. הוא קרא לנוזקה SLEEPWALKER, ולפי הפרסום לא מצא לה שום דיווח פומבי קודם. מה שמייחד אותה הוא מה שמגיע ברגע שמעירים אותה: לא פקודה קריאה, אלא תוכנית קצרה בשפה שקיימת רק בתוך הקובץ הזה. נעבור על איך הוא נטען, איך מעירים אותו מרחוק, ומה אפשר להגיד לו לעשות משם.
למה דווקא הקובץ הזה של ESET
הטכניקה עצמה ותיקה: מניחים DLL זדוני בתיקייה של תוכנה לגיטימית, בשם שהיא מחפשת ממילא, והיא טוענת אותו בעצמה. כאן הקובץ מתחזה ל-dpapi.dll, רכיב הצפנה אמיתי של ווינדוס, ומייצא בדיוק את אותן שבע פונקציות שהמקור מייצא. כל אחת מהן היא קפיצה דרך טבלת מצביעים שמתחילה ריקה ואמורה להתמלא מקובץ בשם dpapisvc.dll, שם שלא שייך לשום רכיב אמיתי של ווינדוס. כשהטעינה נכשלת, הקוד מסיים את כל התהליך המארח.
לפני שהיא עושה משהו, הנוזקה בודקת דבר אחד בלבד: איך נקרא התהליך שטען אותה. אם השם אינו ERAAgent.exe היא נשארת רדומה, ולכן היא לא תרוץ בתוך debugger או ארגז חול אלא אם אלה נושאים במקרה בדיוק את השם הזה. את המחרוזת היא לא מחזיקה כטקסט קריא בקובץ אלא בונה מחדש מכמה מספרים בזמן ריצה, אותו תרגיל שהיא עושה לשלושה שמות פונקציות שלא מופיעים ברשימת הייבוא הרגילה שלה. מה שכן, אין בקובץ שום ניסיון להשיג הרשאות גבוהות יותר: השינויים שהיא מבצעת ברישום דורשים ממילא הרשאות מנהל מקומי, כלומר מי שהניח אותה שם כבר החזיק אותן. גם שמירת האחיזה מסתכמת בזה. כל עוד הקובץ נשאר בתיקייה, הוא ייטען שוב בכל הפעלה של השירות.
איך מעירים נוזקה שלא מאזינה לאף פורט
ההוראה היחידה שמאוחסנת בתוך הקובץ אומרת לו לעשות דבר אחד: להעביר את כרטיסי הרשת למצב שבו הם מוסרים לתוכנה כל חבילה שחולפת בהם, ולא רק חבילות שמופנות למחשב הזה, ואז לחכות. למצב הזה קוראים promiscuous. משם היא בוחנת כל חבילה שהיא רואה מול סדרת בדיקות, וכישלון בכל שלב מפיל את החבילה בשקט, בלי תשובה ובלי עקבות.
השלבים עצמם קפדניים למדי. החבילה צריכה להיות באורך 48 בתים לפחות. שני הערכים בני 16 הביט האחרונים בה עוברים XOR זה עם זה ואז עם הערך 0xAAAA, וכך מתקבל אורך מועמד שחייב ליפול בטווח חוקי. צמד הבתים שיושב במיקום שהאורך הזה מצביע עליו חייב להיות שווה לסכום של אותם שני ערכים, לא ל-XOR שלהם. הבלוק שמתקבל עובר בדיקת CRC-32 משלו, ורק אז מפוענח התוכן ב-AES-256-CCM עם תג אימות ומטופל כפקודה. חבילה שעוברת את כל השרשרת הזאת נקראת בעגה magic packet.
הבדיקות רצות על התוכן הגולמי, לפני שווינדוס מיינה את החבילה ל-TCP או ל-UDP, ולכן הטריגר יכול לנסוע כמעט בכל תעבורת IP. הנוזקה עוקבת אחרי שמונה ממשקי רשת לכל היותר, מדלגת על ה-loopback, וממתינה לפחות שלוש שניות בין טריגר לטריגר כדי לא לפעול פעמיים על אותה חבילה.
עד כאן היתרון של התוקף. הצד השני של המשוואה מעניין לא פחות: חבילה גולמית שמגיעה מהאינטרנט הפתוח נעצרת בדרך כלל בחומת האש או ב-NAT בכניסה לרשת, ולכן מי ששולח את הטריגר צריך כבר להיות ברשת הזאת, או להגיע אליה ממכונה שנפרצה קודם. יש חריגים, והמעניין שבהם הוא מכונה שמעבירה תעבורה בשביל אחרים, למשל שער רשת או שרת VPN: שם גם חבילה שמיועדת למחשב אחר לגמרי חולפת בממשק שהנוזקה מאזינה לו.
23 הוראות בתוך חבילה אחת
Reichel חילץ מהקובץ את מפתח ה-AES-256 ואת ה-nonce של התצורה המוטמעת, ופיענח בלוק תצורה בגודל 2,048 בתים שתג האימות שלו יצא תקין. מה שהיה שם בפועל הוא הוראה אחת בת חמישה בתים: 87 01 2A 00 00. הבית הראשון מפעיל את ההאזנה, האמצעיים אומרים שהממשק המבוקש הוא כוכבית, כלומר כולם, ושני האחרונים הם מספר השניות: אפס, בלי הגבלת זמן.
וכאן הנקודה שהופכת את הנוזקה לחריגה. אצל רוב הדלתות האחוריות, מי ששיחזר את מפתח ההצפנה קורא מיד את הפקודות, כי הן טקסט או רשימת הגדרות. כאן הפענוח מחזיר בייטקוד, רצף קודי פעולה בפורמט שלא קיים בשום מקום אחר. המפתח מסביר איך לקרוא את הבתים. רק הנדסה לאחור של השפה מסבירה מה הם אומרים.
השפה מונה 23 הוראות, וההיקף שלה הוא מה שמסביר למה הקובץ יכול להרשות לעצמו להגיע ריק. יש שם שליטה בסיסית והרצה במקביל בתהליכון נפרד, השהיות קבועות והשהיות באורך אקראי, ותזמון בסגנון cron שבודק דקה, שעה, יום בחודש ויום בשבוע מול ארבע מסכות שמורות. יש הוראות שליחה וקליטה בכל אחד מערוצי הרשת שנראה עוד מעט. ויש הקבוצה שעושה את העבודה בפועל: כתיבת חתיכות לאזור עבודה של 128 קילובייט, אימות מה שהצטבר שם מול טביעת אצבע SHA-256 לפני שמותר להריץ אותו, פריסת תוכנית שנשלחה דחוסה, הרצת shellcode בזיכרון, וקריאת תוכנית מקובץ שכבר יושב על הדיסק.
חלק מההוראות נושאות בתוכן תוכנית שלמה נוספת, ואת זה הכי קל לראות דרך התזמון. ההוראה המתוזמנת שהחוקר מפרק כדוגמה היא באורך 22 בתים, ארבעה שדות ממנה הם מסכות הזמן, והאחרון הוא תוכנית פנימית שמעורפלת ב-XOR מול מפתח קצר. המנוע מפענח אותה, מריץ אותה, ומיד מפעיל עליה שוב את אותו XOR. כלומר בין הרצה להרצה מתוזמנת, התוכנית הפנימית לא קריאה בזיכרון.
שישה ערוצים, ואחד מהם לא עובר ברשת בכלל
חמש מהוראות הרשת בודקות אם הכתובת שקיבלו מתחילה ב-vm:, ואם כן הן משתמשות בערוץ הפנימי של VMware לתקשורת בין מכונה וירטואלית למארח שלה, הידוע כ-VMCI, במקום בכתובת רשת רגילה. אם המכונה הנגועה היא אורחת על מארח VMware, פקודות עוברות בינה לבין המארח או בינה לבין אורחת אחרת מבלי שהתעבורה תופיע אי פעם ברשת. הקלטת תעבורה בין המכונות פשוט לא תכיל אותה.
בסך הכול מדובר בשישה אמצעי תקשורת: TCP, UDP, ICMP שמבריח מידע בתוך מטען של בקשות ping, named pipes של SMB שיכולים לעלות על שיתוף במחשב מרוחק עם פרטי אימות שהתוקף מספק (התנועה הרוחבית היחידה שהשפה מאפשרת), הערוץ של VMware, וה-socket הגולמי שקולט את החבילה המעוררת. אף אחד מהם לא נושא יעד קבוע: כל כתובת מגיעה בזמן ריצה, בתוך התוכנית שנשלחה.
קיים גם ערוץ טריגר שני, שמסתתר בתוך שאילתות DNS שנראות רגילות לגמרי: הפקודה מקודדת ב-Base32 ומפוצלת בין החלקים של שם הדומיין, וכל חלק כזה עטוף בשני תווי סימון שנושאים ביניהם בית ביקורת CRC-8. זה בדיוק סוג התעבורה שכמעט כל רשת מרשה החוצה גם כשהיא חוסמת את השאר. חשוב איך Reichel מנסח את זה: היכולת קיימת בקוד, אבל לא היא שהופעלה בדגימה שניתח. התצורה המוטמעת בחרה בהאזנה הגולמית בלבד.
כדי שערוץ ה-named pipes יעבוד גם בלי הזדהות, הנוזקה מחלישה בכוונה את המחשב עצמו: היא מדליקה את ההגדרה שגורמת להרשאות שניתנו ל-Everyone לחול גם על גישה אנונימית, מוסיפה את שם ה-pipe שלה לרשימת אלה שמותר לפנות אליהם בלי שם משתמש, ויוצרת את ה-pipes עם הרשאות ל-Everyone ול-Anonymous Logon. היא כן מנסה להחזיר את המצב לקדמותו אחר כך, אבל הרישום שהיא מנהלת שומר אם ההוספה הצליחה, לא אם הערך היה שם מלכתחילה. (הניקוי שלה עלול למחוק דווקא ערך לגיטימי שהיה בשרת לפניה.)
מה החוקר לא יודע
כל הניתוח הזה נשען על קובץ בודד, בלי רשומות אירוע ובלי הקלטת תעבורה, ו-Reichel מונה במפורש את מה שחסר. אין לו הקשר איסוף שקושר את הקובץ לפריצה מאומתת, ולכן אין קורבן, ענף או מדינה, וגם אין הוכחה שהדגימה בכלל הופעלה בשטח. גם דרך הכניסה הראשונית לא ידועה, כי שום דרופר לא נמצא בקובץ. והכי חשוב לענייננו: ההוראה היחידה שנמצאה בו מפעילה את ההאזנה, וכל השאר הן יכולות שהשפה מציעה, לא פעולות שנצפו. אף אחד לא יודע אילו פקודות באמת נשלחו.
גם ייחוס אין. "לא הצלחתי לייחס את הדגימה הזאת לקבוצה מסוימת, מכיוון שלא ראיתי קוד דומה בעבר ואין לי מידע על שרשרת התקיפה", כתב. ההערכה שלו היא שהשילוב של שתילה לתוך רכיב ניהול אבטחה, כמה ערוצים חבויים והמתנה פסיבית לחבילה אחת מתאים למבצע ממוקד עם משאבים ולא לפעילות אופורטוניסטית. "בזמן הפרסום לא מצאתי דיווח פומבי קודם על הדלת האחורית הזאת, וכיסוי הזיהוי של הקובץ נשאר נמוך", הוא כותב, ומוסיף שייתכן שהיא עדיין בשימוש ואף בפיתוח.
מה זה אומר בפועל
מי שמריץ ESET PROTECT בארגון יכול לבדוק את זה היום: האם יושב dpapi.dll באותה תיקייה שבה נמצא ERAAgent.exe. להתקנה תקינה אין סיבה להחזיק שם קובץ כזה. אם יש, כדאי להשוות את ה-SHA-256 שלו לזה שפורסם, לחפש גם dpapisvc.dll לידו, ולהשוות את שתי הגדרות הרישום מול תצורה תקינה ידועה, כי הן לבדן אינן הוכחה לכלום. Reichel פירסם לצד הניתוח חוקת YARA וסקריפט PowerShell שקורא בלבד, כך שאפשר להריץ אותו רוחבית לפני שמחליטים מה עושים.
והנקודה הרחבה יותר שווה יותר מהבדיקה הנקודתית. ההיגיון שלפיו מכונה שקטה כלפי חוץ היא מכונה נקייה עובד רק כל עוד הנוזקה צריכה לדבר עם מישהו. כאן היא לא צריכה.
תגובות