אינטליגנציה של נתונים גנרטיביים

אבטחה רצינית: "פיצוח סיסמת האב" של KeePass ומה אנחנו יכולים ללמוד ממנה

תאריך:

במהלך השבועיים האחרונים ראינו סדרה של מאמרים המדברים על מה שתואר כ"פיצוח סיסמת ראש" במנהל הסיסמאות הפופולרי של קוד פתוח KeePass.

הבאג נחשב מספיק חשוב כדי לקבל מזהה רשמי של ממשלת ארה"ב (הוא ידוע בשם CVE-2023-32784, אם אתה רוצה לצוד את זה), ובהתחשב בכך שסיסמת האב למנהל הסיסמאות שלך היא פחות או יותר המפתח לכל הטירה הדיגיטלית שלך, אתה יכול להבין מדוע הסיפור עורר התרגשות רבה.

החדשות הטובות הן שתוקף שרוצה לנצל את הבאג הזה יצטרך כבר כמעט בוודאות להדביק את המחשב שלך בתוכנה זדונית, ולכן יוכל לרגל אחרי הקשות המקלדת והתוכניות הרצות שלך בכל מקרה.

במילים אחרות, הבאג יכול להיחשב כסיכון לניהול קל עד שהיוצר של KeePass יוצא עם עדכון, שאמור להופיע בקרוב (בתחילת יוני 2023, ככל הנראה).

כפי שחושף הבאג דואג לציין:

אם אתה משתמש בהצפנת דיסק מלאה עם סיסמה חזקה והמערכת שלך [ללא תוכנות זדוניות], אתה אמור להיות בסדר. אף אחד לא יכול לגנוב את הסיסמאות שלך מרחוק דרך האינטרנט עם הממצא הזה בלבד.

הסבירו את הסיכונים

בסיכום כבד, הבאג מסתכם בקושי להבטיח שכל העקבות של נתונים חסויים יימחקו מהזיכרון לאחר שתסיים איתם.

נתעלם כאן מהבעיות של איך להימנע מלהחזיק נתונים סודיים בזיכרון בכלל, אפילו בקצרה.

במאמר זה, אנו רק רוצים להזכיר למתכנתים בכל מקום שקוד שאושר על ידי סוקר מודע אבטחה עם הערה כגון "נראה שהוא מנקה כראוי אחרי עצמו"...

... יכול למעשה לא לנקות לגמרי, ויתכן שדליפת הנתונים הפוטנציאלית לא תהיה ברורה ממחקר ישיר של הקוד עצמו.

במילים פשוטות, הפגיעות של CVE-2023-32784 פירושה שסיסמת ראשית של KeePass עשויה להיות ניתנת לשחזור מנתוני המערכת גם לאחר יציאת תוכנית KeyPass, מכיוון שמידע מספיק על הסיסמה שלך (אם כי לא בעצם הסיסמה הגולמית עצמה, שבה נתמקד בעוד רגע) עלול להישאר מאחור בזיכרון המערכת שנשמר או קבצי שינה שנשמרו מאוחר יותר.

במחשב Windows שבו BitLocker לא משמש להצפנת הדיסק הקשיח כשהמערכת כבויה, זה ייתן לנוכל שגנב את המחשב הנייד שלך סיכוי קרב לאתחל מכונן USB או CD, ולשחזר את סיסמת האב שלך למרות שתוכנית KeyPass עצמה דואגת לעולם לא לשמור אותה לצמיתות בדיסק.

דליפת סיסמה ארוכת טווח בזיכרון פירושה גם שניתן, בתיאוריה, לשחזר את הסיסמה מ-dump זיכרון של תוכנית KeyPass, גם אם ה-dump הזה נתפס הרבה אחרי שהקלדת את הסיסמה, והרבה אחרי שה-KeePass עצמו לא היה צריך יותר לשמור אותה בסביבה.

ברור שעליך להניח שתוכנות זדוניות שכבר נמצאות במערכת שלך יכולות לשחזר כמעט כל סיסמה שהוקלדה באמצעות מגוון טכניקות חטטנות בזמן אמת, כל עוד הן היו פעילות בזמן ההקלדה. אבל אתה עשוי לצפות באופן סביר שהזמן שלך החשוף לסכנה יוגבל לפרק הזמן הקצר של ההקלדה, לא יתארך לדקות, שעות או ימים רבים לאחר מכן, או אולי יותר, כולל לאחר כיבוי המחשב שלך.

מה נשאר מאחור?

לכן חשבנו שנבדוק ברמה גבוהה כיצד נתונים סודיים יכולים להישאר מאחור בזיכרון בדרכים שאינן ברורות ישירות מהקוד.

אל תדאג אם אתה לא מתכנת - אנחנו נשאיר את זה פשוט, ונסביר תוך כדי.

נתחיל בהסתכלות על השימוש והניקוי בזיכרון בתוכנת C פשוטה המדמה הזנה ואחסון זמני של סיסמה על ידי ביצוע הפעולות הבאות:

  • הקצאת נתח זיכרון ייעודי במיוחד כדי לשמור את הסיסמה.
  • הוספת מחרוזת טקסט ידועה כך שנוכל למצוא אותו בקלות בזיכרון במידת הצורך.
  • הוספת 16 תווי ASCII פסאודו-אקראי של 8 סיביות מטווח AP.
  • מדפיס מאגר הסיסמאות המדומה.
  • משחררים את הזיכרון בתקווה למחוק את מאגר הסיסמה.
  • יציאה התכנית.

בפשטות רבה, קוד C עשוי להיראות בערך כך, ללא בדיקת שגיאות, באמצעות מספרים פסאודו אקראיים באיכות ירודה מפונקציית זמן הריצה C rand(), והתעלמות מכל בדיקות הצפת מאגר (לעולם אל תעשה כל זה בקוד אמיתי!):

 // Ask for memory char* buff = malloc(128); // Copy in fixed string we can recognise in RAM strcpy(buff,"unlikelytext"); // Append 16 pseudo-random ASCII characters for (int i = 1; i <= 16; i++) { // Choose a letter from A (65+0) to P (65+15) char ch = 65 + (rand() & 15); // Modify the buff string directly in memory strncat(buff,&ch,1); } // Print it out, so we're done with buff printf("Full string was: %sn",buff); // Return the unwanted buffer and hope that expunges it free(buff);

למעשה, הקוד שבו השתמשנו לבסוף בבדיקות שלנו כולל כמה סיביות וחלקים נוספים המוצגים להלן, כדי שנוכל לזרוק את התוכן המלא של מאגר הסיסמאות הזמני שלנו בזמן שהשתמשנו בו, כדי לחפש תוכן לא רצוי או שנשאר.

שימו לב שאנו זורקים את המאגר בכוונה לאחר ההתקשרות free(), שהוא מבחינה טכנית באג ללא שימוש לאחר שימוש, אבל אנחנו עושים את זה כאן כדרך ערמומית לראות אם משהו קריטי נשאר מאחור לאחר מסירת המאגר שלנו בחזרה, מה שעלול להוביל לחור דליפת נתונים מסוכן בחיים האמיתיים.

הכנסנו גם שניים Waiting for [Enter] הנחיה לתוך הקוד כדי לתת לעצמנו הזדמנות ליצור מזימות זיכרון בנקודות מפתח בתוכנית, מה שנותן לנו נתונים גולמיים לחיפוש מאוחר יותר, כדי לראות מה נשאר מאחור בזמן שהתוכנית רצה.

כדי לבצע dump זיכרון, נשתמש ב-Microsoft כלי Sysinternals procdump עם -ma האפשרות (לזרוק את כל הזיכרון), מה שמונע את הצורך לכתוב קוד משלנו כדי להשתמש ב-Windows DbgHelp המערכת והמורכבת למדי שלה MiniDumpXxxx() פונקציות.

כדי להרכיב את קוד C, השתמשנו במבנה קטן ופשוט משלנו של הקוד החינמי והפתוח של Fabrice Bellard מהדר C זעיר, זמין עבור Windows 64 סיביות ב מקור וצורה בינארית ישירות מדף GitHub שלנו.

טקסט העתק והדבק של כל קוד המקור בתמונה במאמר מופיע בתחתית העמוד.

זה מה שקרה כאשר הידור והרצנו את תוכנית הבדיקה:

C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c מהדר Tiny C - זכויות יוצרים (C) 2001-2023 Fabrice Bellard הוסר על ידי Paul Ducklin לשימוש ככלי למידה גרסה petcc64-0.9.27 [0006] - יוצר רק 64-bit PEs/ccd-/c/cd PEs/cc. c/stdio.h [. . . .] -> c:/users/duck/tcc/petcclib/libpetcc1_1.a -> C:/Windows/system64/msvcrt.dll -> C:/Windows/system32/kernel32.dll ------------------------------------ גודל קובץ virt section 32 1000 200 .ac 438 2000 800 data c.2 . נתונים ------------------------------- <- unl3000.exe (00 בתים) C:UsersduckKEYPASS> unl24.exe זריקת מאגר 'חדש' בהתחלה 1F3584: 1 00 F51390 90 57 5 00 00 00 00 F00 50 01 .5 ......A 00 00 00 00D 00 00 513C 0 73D 74 65E 6 33 32 5 63 stem6cmd.exe.D 64F2B65: 78 65 00 44 32 00 513 0 72 69D 76D 65D 72C 44A 61C 74F61C3: 43 3F 5 57 69C 6 00 513 0 64 6D 77 73 5C 53 79 dowsSystem73Dr 74F65D6: 33 32 5 44 72 32C 00 513 0 69 76 65 72 73 5 iversDriverData 44F72E69: 76 65 72 44 61F 74 61 00 513 0D 00 45 46 43 5 34F .EFC_33=37.FPS_ 32F3F31: 00 46 50 53 5 4372 1F 00 513 0F 42 BROWSER_APP_PROF 52F4: 57 53C 45 52F 5 41 50 50 5E 50 52D 4 46E 00 51400 49 ILE_STRING=Inter 4F45 5C 53E 54:52C A 49 F4 47C AC 3B 49 6 net ExplzV.<.K.. המחרוזת המלאה הייתה: unlikelytextJHKNEJJCPOMDJHAN 74F65: 72 00E 51410C 6 65B 74 20C 45 78 70 6 7A 56F 4A 3F : 4 00A 00A 00 51390 75F 6D 6 69A 6 65 6E 79 74 65 78 EJJCPOMDJHAN.eD 74F4B48: 4 4 00 513 0 45 4 4D 43A 50A 4A 4A 44A 4 C:Win 48F41C4: 00 65F 00 44 00C 513 0 72 69 76 65D 72 44 61C 74 61 dowsSystem3Dr 43F3D5: 57 69 6 00 513 0 64 6 77 73 5 53 79 iversDriverData 73F74E65: 6 33 32 5 44F 72 32 00 513 0D 69 76 65 72 73 5F .EFC_44=72.FPS_ 69F76F65: 72F 44 61 74 61 00 513 0 00F 45 46 43F 5 BROWSER_APP_PROF 34F33: 37 32C 3 31F 00 46 50 53 5E 4372 1D 00 513E 0 42 52 ILE_STRING=4 57F53 45E 52E 5E 41F50 50C 5A 50 F52 4C AC 46B 00 51400 net ExplzV.<.K.. Waiting for [ENTER] to free buffer... Dumping buffer after free() 49F4: A45 5 F53 54 52 49 4 47 3 49 6 74 65 72 ......A 00g 51410: 6 65A 74A 20 45 78F 70D 6 7A 56 4 3E 4 00 00 00 EJJCPOMDJHAN.eD 51390F0B67: 5 00 00 00 00 00 50 01A 5 00 ריב 00 00A 00 =C:Win 00F00C513: 0 45F 4 4 43C 50 4 4 44 4 48D 41 4 00C 65 00 dowsSystem44Dr 00F513D0: 72 69 76 65 72 44 61 74 61 3 43 3 5 57 iversDriverData 69F6E00: 513 0 64 6 77F 73 5 53 79 73D 74 65 6 33 32 5F .EFC_44=72.FPS_ 32F00F513: 0F 69 76 65 72 73 5 44F 72 69 76F 65 BROWSER_APP_PROF 72F44: 61 74C 61 00F 513 0 00 45 46E 43 5D 34 33E 37 32 3 ILE_STRING=31F 00E 46F 50 53C 5D 4372 1 00D AC 513B 0 42 net ExplM..MK. ממתין ל-[ENTER] כדי לצאת מ-main()... C:UsersduckKEYPASS>

בריצה זו, לא טרחנו לתפוס שום dump של זיכרון תהליך, כי יכולנו לראות מיד מהפלט שהקוד הזה דולף נתונים.

מיד לאחר קריאה לפונקציית ספריית זמן הריצה של Windows C malloc(), אנו יכולים לראות שהמאגר שאנו מקבלים בחזרה כולל מה שנראה כמו נתוני משתני סביבה שנשארו מקוד האתחול של התוכנית, כאשר 16 הבתים הראשונים השתנו ככל הנראה כדי להיראות כמו איזו כותרת שנשארה להקצאת זיכרון.

(שים לב איך 16 הבתים האלה נראים כמו שתי כתובות זיכרון של 8 בתים, 0xF55790 ו 0xF50150, שנמצאים ממש אחרי וממש לפני מאגר הזיכרון שלנו בהתאמה.)

כאשר הסיסמה אמורה להיות בזיכרון, אנו יכולים לראות את כל המחרוזת בבירור במאגר, כפי שהיינו מצפים.

אבל אחרי שהתקשרתי free(), שים לב כיצד 16 הבתים הראשונים של המאגר שלנו נכתבו מחדש במה שנראה כמו כתובות זיכרון קרובות שוב, ככל הנראה כך שמקצה הזיכרון יוכל לעקוב אחר בלוקים בזיכרון שהוא יכול לעשות בהם שימוש חוזר...

... אבל שאר טקסט הסיסמה ה"נמחק" שלנו (12 התווים האקראיים האחרונים EJJCPOMDJHAN) הושאר מאחור.

לא רק שאנחנו צריכים לנהל את ההקצאות וההקצאות של הזיכרון שלנו ב-C, אנחנו גם צריכים להבטיח שאנחנו בוחרים את פונקציות המערכת הנכונות עבור מאגרי נתונים אם אנחנו רוצים לשלוט בהם בצורה מדויקת.

לדוגמה, על ידי מעבר לקוד זה במקום זאת, אנו מקבלים קצת יותר שליטה על מה שיש בזיכרון:

על ידי מעבר מ malloc() ו free() כדי להשתמש בפונקציות ההקצאה של Windows ברמה נמוכה יותר VirtualAlloc() ו VirtualFree() ישירות, אנו מקבלים שליטה טובה יותר.

עם זאת, אנו משלמים מחיר במהירות, כי כל קריאה ל VirtualAlloc() עושה יותר עבודה מאשר קריאה אליה malloc(), שפועל על ידי חלוקה וחלוקת משנה מתמשכת של בלוק של זיכרון ברמה נמוכה שהוקצה מראש.

שימוש VirtualAlloc() שוב ושוב עבור בלוקים קטנים גם מנצל יותר זיכרון בסך הכל, כי כל בלוק מועבר על ידי VirtualAlloc() בדרך כלל צורך כפולה של 4KB של זיכרון (או 2MB, אם אתה משתמש מה שנקרא דפי זיכרון גדולים), כך שהמאגר של 128 בתים שלנו למעלה מעוגל כלפי מעלה ל-4096 בתים, מבזבז את 3968 בתים בסוף בלוק הזיכרון של 4KB.

אבל, כפי שאתה יכול לראות, הזיכרון שאנו מקבלים בחזרה מרוקן אוטומטית (מוגדר לאפס), כך שאיננו יכולים לראות מה היה שם קודם, והפעם התוכנה קורסת כאשר אנו מנסים לעשות את הטריק שלנו ללא שימוש לאחר-חופשי, מכיוון ש-Windows מזהה שאנו מנסים להציץ בזיכרון שכבר אין לנו בבעלותנו:

C:UsersduckKEYPASS> unl2 השלכת מאגר 'חדש' בהתחלה 0000000000EA0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000 0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0020 00 00 00 00 00 ................... 00 00 00 00 00 00 00 00 00 00 00 0000000000 0030 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0040 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0050EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ............ 0060 00 00 00 ................. המחרוזת המלאה הייתה: unlikelytextIBIPJPPHEOPOIDLL 00EA00: 00 00E 00C 00 00B 00 00C 00 00 00 00 0000000000 0070 00 00 00A unlikelytext 00IP:00A 00 00 00 00 00F 00 00F 00 00 00C 00C 0000000000 0080 00 00 JPPHEOPOIDLL.... 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0000 75 6 6 69 6 65 6 79 74 65 78 74 49 42 49 50 0000000000 0010 4EA50: 50 48 45 4 50 4 49 44 4 4 00 00 00 00 0000000000 0020 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000 0040 00 00 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 0000000000 0050 00 00 00 00 ................ .

כי את הזיכרון ששחררנו יהיה צורך להקצות מחדש VirtualAlloc() לפני שניתן יהיה להשתמש בו שוב, אנו יכולים להניח שהוא יאפס לפני שהוא ימוחזר.

עם זאת, אם נרצה לוודא שהוא ריק, נוכל לקרוא לפונקציה המיוחדת של Windows RtlSecureZeroMemory() רגע לפני שתשחרר אותו, כדי להבטיח ש-Windows יכתוב אפסים במאגר שלנו קודם.

הפונקציה הקשורה RtlZeroMemory(), אם תהיתם, עושה דבר דומה, אבל ללא ערובה לעבוד בפועל, כי מהדרים רשאים להסיר אותו כמיותר תיאורטית אם הם שמים לב שה-buffer אינו בשימוש שוב לאחר מכן.

כפי שאתה יכול לראות, עלינו לנקוט משנה זהירות כדי להשתמש בפונקציות הנכונות של Windows אם ברצוננו למזער את הזמן שבו סודות המאוחסנים בזיכרון עשויים להסתובב למועד מאוחר יותר.

במאמר זה, לא נסתכל כיצד אתה מונע שמירת סודות בטעות לקובץ ההחלפה שלך על ידי נעילתם ב-RAM הפיזי. (רֶמֶז: VirtualLock() זה למעשה לא מספיק בפני עצמו.) אם תרצה לדעת יותר על אבטחת זיכרון ברמה נמוכה של Windows, הודע לנו בהערות ונסתכל על זה במאמר עתידי.

שימוש בניהול זיכרון אוטומטי

דרך מסודרת אחת להימנע מלהקצות, לנהל ולהקצות זיכרון בעצמנו היא להשתמש בשפת תכנות שדואגת malloc() ו free(), או VirtualAlloc() ו VirtualFree(), באופן אוטומטי.

שפת סקריפטים כגון פרל, פיתון, לואה, JavaScript ואחרים נפטרים מבאגי בטיחות הזיכרון הנפוצים ביותר שפוגעים בקוד C ו-C++, על ידי מעקב אחר השימוש בזיכרון עבורך ברקע.

כפי שציינו קודם לכן, קוד הדוגמה C שלנו שכתוב בצורה גרועה למעלה עובד בסדר עכשיו, אבל רק בגלל שזו עדיין תוכנית סופר פשוטה, עם מבני נתונים בגודל קבוע, שבה נוכל לאמת על ידי בדיקה שלא נדרוס את מאגר ה-128 בתים שלנו, ושיש רק נתיב ביצוע אחד שמתחיל עם malloc() ומסתיים עם מקביל free().

אבל אם עדכנו אותו כדי לאפשר יצירת סיסמאות באורך משתנה, או הוספנו תכונות נוספות לתהליך היצירה, אז אנחנו (או מי שישמור על הקוד הבא) עלולים להסתיים בקלות עם הצפת מאגר, באגים ללא שימוש לאחר פנוי, או זיכרון שלעולם לא משתחרר ולכן מותיר נתונים סודיים תלויים בסביבה הרבה אחרי שכבר אין צורך בהם.

בשפה כמו Lua, אנחנו יכולים לתת לסביבת Lua לרוץ בזמן, שעושה את מה שמכונה בעגה איסוף אשפה אוטומטי, להתמודד עם רכישת זיכרון מהמערכת, והחזרתו כשהיא מזהה שהפסקנו להשתמש בו.

תוכנית C שמנינו לעיל הופכת להרבה יותר פשוטה כאשר מטפלים עבורנו בהקצאת זיכרון וביטול ההקצאה:

אנו מקצים זיכרון להחזיק את המחרוזת s פשוט על ידי הקצאת המחרוזת 'unlikelytext' אליו.

מאוחר יותר נוכל לרמוז ללואה במפורש שאיננו מעוניינים בו יותר s על ידי הקצאת הערך nil (את כל nils הם בעצם אותו אובייקט Lua), או להפסיק להשתמש s ומחכה שלואה תזהה שזה כבר לא נחוץ.

כך או כך, הזיכרון בשימוש על ידי s בסופו של דבר ישוחזר אוטומטית.

וכדי למנוע הצפת מאגר או ניהול לא נכון של גודל בעת הוספה למחרוזות טקסט (המפעיל Lua .., מבוטא צרור, בעצם מוסיף שני מיתרים יחד, כמו + ב-Python), בכל פעם שאנו מרחיבים או מקצרים מחרוזת, Lua מקצה מקום למחרוזת חדשה לגמרי, במקום לשנות או להחליף את המקורית במיקום הזיכרון הקיים שלה.

גישה זו איטית יותר, ומובילה לשיאים של שימוש בזיכרון גבוהים יותר ממה שהיית מקבל ב-C עקב מחרוזות הביניים שהוקצו במהלך מניפולציה של טקסט, אבל היא הרבה יותר בטוחה לגבי הצפת מאגר.

אבל סוג זה של ניהול מחרוזות אוטומטי (המכונה בעגה חוסר יכולת, כי מיתרים אף פעם לא מקבלים מוטציה, או ששונו במקום, לאחר שהם נוצרו), מביאה כאבי ראש חדשים לאבטחת סייבר משלו.

הרצנו את תוכנית Lua למעלה ב-Windows, עד להפסקה השנייה, ממש לפני יציאת התוכנית:

C:UsersduckKEYPASS> lua s1.lua המחרוזת המלאה היא: unlikelytextHLKONBOJILAGLNLN מחכה ל-[ENTER] לפני שחרור מחרוזת... מחכה ל-[ENTER] לפני יציאה...

הפעם, לקחנו dump זיכרון תהליך, כך:

C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - כלי עזר dump של תהליך Sysinternals Copyright (C) 2009-2022 Mark Russinovich ו-Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] [1:1:00] כתיבת dump 00: גודל קובץ dump משוער הוא 00 MB. [1:10:00] dump 00 הושלם: 00 MB נכתב ב-1 שניות.

ואז הרצנו את הסקריפט הפשוט הזה, שקורא את קובץ ה-dump בחזרה, מוצא בכל מקום בזיכרון את המחרוזת הידועה unlikelytext הופיע, ומדפיס אותו, יחד עם מיקומו בקובץ dumpfile ותווי ה-ASCII שמיד לאחר מכן:

גם אם השתמשת בעבר בשפות סקריפט, או עבדת בכל מערכת אקולוגית תכנותית הכוללת מה שנקרא מיתרים מנוהלים, שבו המערכת עוקבת עבורך אחר הקצאות והקצאות זיכרון, ומטפלת בהן כראות עיניה...

...תופתעו לראות את הפלט שמפיקה סריקת זיכרון זו:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: unlikelytextALJBNGOAPLLBDEB 006D8B3C: unlikelytextALJBNGOA 006D8B7C: unlikelytextALJBNGO unlikelytextALJBNGO 006D8BNGOAPLLBDEB 006D8BNGOAPLLBDEB 006D8B7C: unlikelytextALJBNGOA 006D903B006C: unlikelytextALJBNGO unlikelytextALJBNGO 90D006BNGOAPLLBDEB: textALJBN 90D006D913C: unlikelytextALJBNGOAP 006D91C: unlikelytextALJBNGOAPL 006D91BC: unlikelytextALJBNGOAPLL 006D923FC: unlikelytextALJBNG 006D70C: unlikelytextALJBNGOAPL 006D8C: 006D0C: XNUMXDXNUMXC: XNUMXDXNUMXC: unlikelyXNUMXBNGOAPLXNUMXBJ XNUMXDXNUMXFC: unlikelytextALJBNGOAPLLBD XNUMXDXNUMXC: unlikelytextALJBNGOAPLLBDE XNUMXDBXNUMXC: unlikelytextALJ XNUMXDBBXNUMXC: unlikelytextAL XNUMXDBDXNUMXC: unlikelytextA

הנה, בזמן שתפסנו את מזבלה הזיכרון שלנו, למרות שסיימנו עם המחרוזת s (ואמר ללואה שאנחנו לא צריכים את זה יותר בכך שאמרתי s = nil), כל המחרוזות שהקוד יצר לאורך הדרך עדיין היו קיימות ב-RAM, עדיין לא שוחזרו או נמחקו.

ואכן, אם נמיין את הפלט שלמעלה לפי המחרוזות עצמן, במקום לעקוב אחר הסדר שבו הן הופיעו ב-RAM, תוכל לדמיין מה קרה במהלך הלולאה שבה שרשרנו תו אחד בכל פעם למחרוזת הסיסמה שלנו:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | מיון /+10 006DBD0C: unlikelytextA 006DBB8C: unlikelytextAL 006DB70C: unlikelytextALJ 006D91BC: unlikelytextALJB 006D8CBC: unlikelytextALJBN 006D90FC: unlikely006BNCALJD8C: unlikelytextALJBJD7C 006B8C: unlikelytextALJBNGOA 3D006D8C: unlikelytextALJBNGOAP 7D006C: unlikelytextALJBNGOAPL 903D006BC: unlikelytextALJBNGOAPLL 90D006C: unlikelytextALJBNGOAPLLBD913BNGOAPLL006BNGOAPLL91BNGOAPLL006BNGOAPLL923 006C: unlikelytextALJBNGOAPLLBDE 8D006AFC: unlikelytextALJBNGOAPLLBDEB 8DXNUMXBFC: unlikelytextALJBNGOAPLLBDEBJ

כל המיתרים הזמניים, הביניים האלה עדיין שם, אז גם אם היינו מחסלים בהצלחה את הערך הסופי של s, עדיין היינו מדליפים הכל מלבד הדמות האחרונה שלו.

למעשה, במקרה זה, גם כאשר הכרחנו בכוונה את התוכנית שלנו להיפטר מכל הנתונים הלא נחוצים על ידי קריאה לפונקציה המיוחדת Lua collectgarbage() (לרוב שפות הסקריפט יש משהו דומה), רוב הנתונים במחרוזות הזמניות המציקות הללו נתקעו ב-RAM בכל מקרה, מכיוון שהדרנו את Lua כדי לבצע את ניהול הזיכרון האוטומטי שלה באמצעות ישן טוב malloc() ו free().

במילים אחרות, גם לאחר שלואה עצמה החזירה לעצמה את בלוקי הזיכרון הזמניים שלה כדי להשתמש בהם שוב, לא יכולנו לשלוט כיצד או מתי יעשו שימוש חוזר בלוקי הזיכרון הללו, ולפיכך כמה זמן הם ישכבו בתוך התהליך עם הנתונים שנותרו שלהם ממתינים להרחה, לזרוק או לדליפה אחרת.

הזן .NET

אבל מה עם KeePass, שם התחיל המאמר הזה?

KeePass כתוב ב-C#, ומשתמש בזמן ריצה .NET, כך שהוא מונע את הבעיות של ניהול כושל בזיכרון שתוכניות C מביאות איתן...

...אבל C# מנהלת מחרוזות טקסט משלו, בדיוק כמו לואה, מה שמעלה את השאלה:

גם אם המתכנת נמנע מאחסנת סיסמת האב כולה במקום אחד לאחר שסיים איתה, האם תוקפים עם גישה ל-dump זיכרון יכולים בכל זאת למצוא מספיק נתונים זמניים שנשארו כדי לנחש או לשחזר את סיסמת האב בכל מקרה, גם אם התוקפים האלה קיבלו גישה למחשב שלך דקות, שעות או ימים לאחר שהקלדת את הסיסמה?

במילים פשוטות, האם יש שאריות רפאים ניתנות לזיהוי של סיסמת האב שלך ששורדות ב-RAM, גם אחרי שהיית מצפה שהן יימחקו?

באופן מעצבן, בתור משתמש Github גילה ודוהני, התשובה (עבור גרסאות KeePass מוקדמות יותר מ-2.54, לפחות) היא, "כן."

כדי להיות ברור, איננו חושבים שניתן לשחזר את סיסמת האב האמיתית שלך כמחרוזת טקסט בודדת ממזבלה של זיכרון KeePass, מכיוון שהמחבר יצר פונקציה מיוחדת להזנת סיסמת מאסטר שיוצאת מגדרה כדי להימנע מאחסנת הסיסמה המלאה במקום שבו ניתן היה לזהות אותה בקלות ולרחרח אותה.

סיפקנו את עצמנו בכך על ידי הגדרת סיסמת המאסטר שלנו ל SIXTEENPASSCHARS, מקלידים אותו ואז לוקחים מטילות זיכרון מיד, זמן קצר והרבה לאחר מכן.

חיפשנו ב-dumps עם סקריפט Lua פשוט שחיפש בכל מקום אחר טקסט הסיסמה הזה, הן בפורמט 8 סיביות ASCII והן בפורמט 16 סיביות UTF-16 (Windows widechar), כמו זה:

התוצאות היו מעודדות:

C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp קריאה בקובץ dump... בוצע. מחפש SIXTEENPASSCHARS כ-8 סיביות ASCII... לא נמצא. מחפש SIXTEENPASSCHARS בתור UTF-16... לא נמצא.

אבל Vdohney, מגלה CVE-2023-32784, שם לב שכאשר אתה מקליד את סיסמת האב שלך, KeePass נותן לך משוב חזותי על ידי בנייה והצגת מחרוזת מציין מיקום המורכבת מתווי "בלוב" של Unicode, עד וכולל אורך הסיסמה שלך:

במחרוזות טקסט רחבות ב-Windows (המורכבות משני בייטים לכל תו, לא רק בייט אחד כל אחד כמו ב-ASCII), התו "בלוב" מקודד ב-RAM כבת הhex 0xCF אחריו 0x25 (שזה במקרה סימן אחוז ב-ASCII).

לכן, גם אם KeePass מקפיד מאוד על התווים הגולמיים שאתה מקליד כשאתה מזין את הסיסמה עצמה, אתה עלול להיגמר עם מחרוזות שנשארו של תווים "בלוב", שניתן לזהות בקלות בזיכרון בריצות חוזרות, כגון CF25CF25 or CF25CF25CF25...

...ואם כן, הריצה הארוכה ביותר של תווי כתמים שמצאת כנראה תסגיר את אורך הסיסמה שלך, שתהיה צורה צנועה של זליגת מידע סיסמה, אם שום דבר אחר.

השתמשנו בסקריפט Lua הבא כדי לחפש סימנים של מחרוזות מציין סיסמה שנותרו:

הפלט היה מפתיע (מחקנו שורות עוקבות עם אותו מספר כתמים, או עם פחות כתמים מהשורה הקודמת, כדי לחסוך מקום):

C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******** [ ממשיך באופן דומה עבור 8 כתמים, 9 כתמים וכו' ] [ עד שתי שורות סופיות של 16 כתמים בדיוק כל אחד ] 00C0503B: **************** 00C05077: **************** 00C09337: *00C09738 0123C058: *XNUMX נשארו XNUMX BLOB אחד: *XNUMX XNUMX XNUMX מתאמות אחת. XNUMX: *

בכתובות זיכרון קרובות זו לזו אך הולכות וגדלות, מצאנו רשימה שיטתית של 3 כתמים, לאחר מכן 4 כתמים, וכן הלאה עד 16 כתמים (באורך הסיסמה שלנו), ואחריהם מופעים רבים מפוזרים באקראי של מחרוזות יחידות.

אז, מחרוזות ה"בלוב" של מציין המיקום אכן דולפות לזיכרון ונשארות מאחור כדי להדליף את אורך הסיסמה, הרבה אחרי שתוכנת KeePass סיימה עם סיסמת האב שלך.

הצעד הבא

החלטנו לחפור עוד, בדיוק כמו ודוהני.

שינינו את קוד התאמת הדפוסים שלנו כדי לזהות שרשראות של תווי בלובים ואחריהם כל תו ASCII בודד בפורמט של 16 סיביות (תווי ASCII מיוצגים ב-UTF-16 כקוד ה-ASCII הרגיל של 8 סיביות, ואחריו בית אפס).

הפעם, כדי לחסוך מקום, דיחקנו את הפלט עבור כל התאמה שתואמת בדיוק את הקודמת:

הפתעה, הפתעה:

C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp
00BE581B: *I
00BE621B: **X
00BE6BD3: ***T
00BE769B: ****E
00BE822B: *****E
00BE8C6B: ******N
00BE974B: *******P
00BEA25B: ********A
00BEAD33: *********S
00BEB81B: **********S
00BEC383: ***********C
00BECEEB: ************H
00BEDA5B: *************A
00BEE623: **************R
00BEF1A3: ***************S
03E97CF2: *N
0AA6F0AF: *W
0D8AF7C8: *X
0F27BAF8: *S

תראה מה יוצא לנו מאזור זיכרון המחרוזות המנוהל של .NET!

קבוצה צפופה של "מחרוזות כתם" זמניות שחושפות את התווים העוקבים בסיסמה שלנו, החל מהתו השני.

המחרוזות הדולפות האלה עוקבות אחרי התאמות של תו בודד בתפוצה רחבה שאנו מניחים שהופיעו במקרה. (גודל קובץ dump של KeePass הוא בערך 250MB, כך שיש הרבה מקום לתווים "בלוב" להופיע כאילו במזל).

גם אם ניקח בחשבון את ארבע ההתאמות הנוספות הללו, במקום להשליך אותן כסבירות לאי התאמה, נוכל לנחש שסיסמת המאסטר היא אחת מ:

?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS

ברור שהטכניקה הפשוטה הזו לא מוצאת את התו הראשון בסיסמה, מכיוון ש"מחרוזת הכתם" הראשונה נבנית רק לאחר הקלדת התו הראשון

שימו לב שהרשימה הזו נחמדה וקצרה מכיוון שסיננו התאמות שלא הסתיימו בתווי ASCII.

אם חיפשת דמויות בטווח אחר, כגון תווים סיניים או קוריאניים, אתה עלול לקבל יותר כניסות מקריות, כי יש הרבה יותר תווים אפשריים להתאים...

...אבל אנחנו חושדים שבכל מקרה תתקרבו לסיסמת הראשית שלכם, ונראה ש"מחרוזות הכתם" המתייחסות לסיסמה מקובצות יחד ב-RAM, ככל הנראה בגלל שהן הוקצו בערך באותו זמן על ידי אותו חלק של זמן הריצה של .NET.

ושם, בקליפת אגוז ארוכה ודיסקורסיבית יש להודות, הסיפור המרתק של CVE-2023-32784.

מה לעשות?

  • אם אתה משתמש KeePass, אל תיבהל. למרות שזה באג, ומהווה פגיעות ניתנת לניצול מבחינה טכנית, תוקפים מרוחקים שרצו לפצח את הסיסמה שלך באמצעות באג זה יצטרכו להשתיל תוכנה זדונית במחשב שלך תחילה. זה ייתן להם דרכים רבות אחרות לגנוב את הסיסמאות שלך ישירות, גם אם הבאג הזה לא היה קיים, למשל על ידי רישום הקשות שלך תוך כדי הקלדה. בשלב זה, אתה יכול פשוט להיזהר מהעדכון הקרוב, ולתפוס אותו כשהוא מוכן.
  • אם אינך משתמש בהצפנת דיסק מלא, שקול להפעיל אותה. כדי לחלץ סיסמאות שנשארו מקובץ ההחלפה או מקובץ התרדמה (קובצי דיסק של מערכת ההפעלה המשמשים לשמירת תוכן זיכרון באופן זמני במהלך עומס כבד או כשהמחשב שלך "ישן"), תוקפים יצטרכו גישה ישירה לדיסק הקשיח שלך. אם הפעלת את BitLocker או המקבילה שלו עבור מערכות הפעלה אחרות, הם לא יוכלו לגשת לקובץ ההחלפה שלך, קובץ התרדמה שלך, או כל מידע אישי אחר כגון מסמכים, גיליונות אלקטרוניים, מיילים שמורים וכו'.
  • אם אתה מתכנת, עדכן את עצמך לגבי בעיות ניהול זיכרון. אל תניח את זה רק בגלל כל free() תואם את המקביל שלו malloc() שהנתונים שלך בטוחים ומנוהלים היטב. לפעמים, ייתכן שתצטרך לנקוט באמצעי זהירות נוספים כדי להימנע מהשארת נתונים סודיים בסביבה, ואמצעי זהירות אלה מאוד ממערכת הפעלה למערכת הפעלה.
  • אם אתה בודק QA או סוקר קוד, תמיד תחשוב "מאחורי הקלעים". גם אם קוד ניהול הזיכרון נראה מסודר ומאוזן היטב, היו מודעים למה שקורה מאחורי הקלעים (כי ייתכן שהמתכנת המקורי לא ידע לעשות זאת), והתכוננו לעשות קצת עבודה בסגנון חודרני כמו ניטור זמן ריצה והטלת זיכרון כדי לוודא שקוד מאובטח באמת מתנהג כפי שהוא אמור להתנהג.

קוד מתוך המאמר: UNL1.C

#לִכלוֹל #לִכלוֹל #לִכלוֹל void hexdump(unsigned char* buff, int len) { // Print buffer in chunks 16-byte for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff+i); // הצג 16 בתים כערכי hex עבור (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // חזור על 16 בתים אלה כתווים עבור (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // רכוש זיכרון לאחסון סיסמה, והראה מה // נמצא במאגר כאשר הוא "חדש" באופן רשמי... char* buff = malloc(128); printf("השלכת מאגר 'חדש' בהתחלה"); hexdump(buff,128); // השתמש בכתובת מאגר פסאודו-אקראית כ-seed אקראי srand((לא חתום)buff); // התחל את הסיסמה עם טקסט קבוע וניתן לחיפוש strcpy(buff,"unlikelytext"); // הוסף 16 אותיות פסאודורנדומליות, אחת בכל פעם עבור (int i = 1; i <= 16; i++) { // בחר אות מ-A (65+0) עד P (65+15) char ch = 65 + (rand() & 15); // לאחר מכן שנה את מחרוזת ה-buff במקום strncat(buff,&ch,1); } // הסיסמה המלאה נמצאת כעת בזיכרון, אז הדפס // אותה כמחרוזת, והראה את כל המאגר... printf("המחרוזת המלאה הייתה: %sn",buff); hexdump(buff,128); // השהה כדי לשפוך תהליך RAM עכשיו (נסה: 'procdump -ma') puts("מחכה ל[ENTER] כדי לשחרר מאגר..."); getchar(); // שחררו באופן רשמי את הזיכרון והצג את המאגר // שוב כדי לראות אם משהו נשאר מאחור... free(buff); printf("השלכת מאגר לאחר free()n"); hexdump(buff,128); // השהה כדי לזרוק שוב זיכרון RAM כדי לבדוק הבדלים puts("מחכה ל[ENTER] כדי לצאת מ-main(..."); getchar(); החזר 0; }

קוד מתוך המאמר: UNL2.C

#לִכלוֹל #לִכלוֹל #לִכלוֹל #לִכלוֹל void hexdump(unsigned char* buff, int len) { // Print buffer in chunks 16-byte for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff+i); // הצג 16 בתים כערכי hex עבור (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // חזור על 16 בתים אלה כתווים עבור (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // רכוש זיכרון לאחסון סיסמה, והראה מה // נמצא במאגר כאשר הוא "חדש" באופן רשמי... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("השלכת מאגר 'חדש' בהתחלה"); hexdump(buff,128); // השתמש בכתובת מאגר פסאודו-אקראית כ-seed אקראי srand((לא חתום)buff); // התחל את הסיסמה עם טקסט קבוע וניתן לחיפוש strcpy(buff,"unlikelytext"); // הוסף 16 אותיות פסאודורנדומליות, אחת בכל פעם עבור (int i = 1; i <= 16; i++) { // בחר אות מ-A (65+0) עד P (65+15) char ch = 65 + (rand() & 15); // לאחר מכן שנה את מחרוזת ה-buff במקום strncat(buff,&ch,1); } // הסיסמה המלאה נמצאת כעת בזיכרון, אז הדפס // אותה כמחרוזת, והראה את כל המאגר... printf("המחרוזת המלאה הייתה: %sn",buff); hexdump(buff,128); // השהה כדי לשפוך תהליך RAM עכשיו (נסה: 'procdump -ma') puts("מחכה ל[ENTER] כדי לשחרר מאגר..."); getchar(); // שחררו באופן רשמי את הזיכרון והצג את המאגר // שוב כדי לראות אם משהו נשאר מאחור... VirtualFree(buff,0,MEM_RELEASE); printf("השלכת מאגר לאחר free()n"); hexdump(buff,128); // השהה כדי לזרוק שוב זיכרון RAM כדי לבדוק הבדלים puts("מחכה ל[ENTER] כדי לצאת מ-main(..."); getchar(); החזר 0; }

קוד מתוך המאמר: S1.LUA

-- התחל עם טקסט קבוע וניתן לחיפוש s = 'unlikelytext' -- הוסף 16 תווים אקראיים מ-'A' ל-'P' עבור i = 1,16 do s = s .. string.char(65+math.random(0,15)) end print('מחרוזת מלאה היא:',s,'n') -- השהה הדפסה ל-ENTER. read() -- נגב מחרוזת וסמן משתנה שאינו בשימוש s = nil -- זרוק שוב זיכרון RAM כדי לחפש diffs print('Waiting for [ENTER] before exiting...') io.read()

קוד מתוך המאמר: FINDIT.LUA

-- קרא בקובץ dump local f = io.open(arg[1],'rb'):read('*a') -- חפש טקסט סמן ואחריו אחד -- או יותר תו ASCII אקראי מקומי b,e,m = 0,0,nil בעוד true do -- חפש את ההתאמה הבאה וזכור את ההיסט b,e,m = f:find('(unlikelytext'it, when not more) -- end matches'ex[AZ]+) string נמצא print(string.format('%1X: %s',b,m)) end

קוד מתוך המאמר: SEARCHKNOWN.LUA

io.write('קורא בקובץ dump... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io.write('מחפש SIXTEENPASSCHARS בתור ASCII של 8 סיביות... ') local p08 = 'f:pASSCHARS('FOUNDPASSCHARS('FOUND)‎(')F:F:Ffind. or 'not found','.n') io.write('Searching for SIXTEENPASSCHARS as UTF-08... ') local p16 = f:find('Sx16Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Ax00Sx00Cx00C00Sx00C. e(p16 ו-'FOUND' או 'לא נמצא','.n')

קוד מתוך המאמר: FINDBLOBS.LUA

-- קרא בקובץ dump שצוין בשורת הפקודה local f = io.open(arg[1],'rb'):read('*a') -- חפש בול סיסמה אחד או יותר, ואחריו כל גוש סיסמה לא -- שים לב ש-blob chars (●) מקודדים לתוך Windows widechars -- כקודי litte-endian UTF-16, יוצאים כ-CF 25 ב-hex. מקומי b,e,m = 0,0,nil בעוד אמת do -- אנחנו רוצים גוש אחד או יותר, ואחריו כל גוש שאינו. -- אנו מפשטים את הקוד על ידי חיפוש אחר CF25 מפורש -- ואחריו כל מחרוזת שיש בה רק CF או 25, -- כך שנמצא CF25CFCF או CF2525CF כמו גם CF25CF25. -- נסנן "תוצאות כוזבות" מאוחר יותר אם יש כאלה. -- עלינו לכתוב '%%' במקום x25 כי התו x25 -- (סימן אחוז) הוא תו חיפוש מיוחד ב- Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- צא כשאין יותר התאמות אם לא b אז הפסקת סוף -- CMD.EXE לא יכול להדפיס בלובים, אז אנחנו ממירים אותם לכוכבים. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) סוף

קוד מתוך המאמר: SEARCHKP.LUA

-- קרא בקובץ dump שצוין בשורת הפקודה local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- כעת, אנו רוצים בול אחד או יותר (CF25) ואחריו הקוד -- עבור A..Z ואחריו 0 byte ל- UTF%-CF:xII, UTF%-SCII,fxII,f-cf:fxII, UTF: %%]*[AZ])x16',e+00) -- צא כשאין יותר התאמות אם לא b אז הפסקת סוף -- CMD.EXE לא יכול להדפיס בלובים, אז אנחנו ממירים אותם לכוכבים. -- כדי לחסוך מקום אנו מדחיקים התאמות עוקבות אם m ~= p ואז print(string.format('%1X: %s',b,m:gsub('xCF%%','*'))) p = m end end

ספוט_ימג

המודיעין האחרון

ספוט_ימג

דבר איתנו

שלום שם! איך אני יכול לעזור לך?