מחבואים - כיצד נוכל להחביא DLL לאחר שהזרקנו אותו לתהליך תמים
הקדמה
במאמר הבא נדבר על המבנה PEB, ועל הדרך בה מודולים שתהליך טען לזיכרון שלו, קשורה למבנה. נכתוב קוד שיפרסר את המבנים הרלוונטיים ולבסוף, כשהידע הנחוץ התקבל, נדבר על הסתרת מודול בתהליך של זיכרון ונממש זאת. למי מכם שציפה להמשך סדרת המאמרים על הזרקות DLL – לא שכחתי. מקווה שבשבוע-שבועיים הקרובים אוציא מאמר חדש בסדרה. בנתיים, בואו נתחיל.מבט מעל
כאן אפשר לראות פריסה של המבנים בזיכרון של תהליך. בפסקה הבאה נסביר את הפריסה הזו. נתחיל מה-PEB. ה-PEB, Process Environment Blcok, הוא מבנה שקיים עבור כל תהליך ומחזיק מידע שעוזר למערכת ההפעלה לנהל את התהליך (במאמר "Windows Process – תהליכים ומה שבניהם" המבנה מתואר מעט יותר בהרחבה). ב-PEB יש הפניה למבנה שנקרא PEB_LDR_DATA – אפשר לראות אותו בשורה האחרונה ב-Offset 0x18.מבנה זה הוא הראש של שלוש רשימות מקושרות דו-כיווניות כאשר כל רשימה מכילה מקבץ מבנים בשם LDR_DATA_TABLE_ENTRY. מבנה זה הוא הייצוג של כל מודול שנטען לזיכרון של תהליך כלשהו. לדוגמה, טענתי את NTDLL.DLL – כעת יוצר מבנה המתאר את המודול בזיכרון של התהליך. שלושת הרשימות הללו מכילו של דבר את אותם מודולים, כלומר את אותם מבנים שמייצגים את המודולים. ההבדל בין הרשימות זה רק הסדר של המבנים. כל רשומה מסדרת את המבנים בהתאם לייעוד הרשימה. חשוב להבין שיהיה מבנה LDR_DATA_TABLE_ENTRY אחד עבור כל מודול ולא שלושה כאלה ורק האינדקס שלו בין הרשימות משתנה. המבנה הזה מכיל מספר שדות שכדאי להכיר:
- FullDllName – מכיל את הנתיב המלא של המודול.
- BaseDllName – מכיל את השם של המודול ללא הנתיב מלא.
- DllBase – המיקום אליו נטען המודול.
- InMemoryOrderLinks – מבנה מסוג LIST_ENTRY שמכיל מצביע לאיבר הבא ולאיבר הקודם ברשימה. נרחיב על המבנה הזה עוד כמה משפטים.
כמו שניתן לראות, המבנה של השדה הוא _UINCODE_STRING שמאופיין על ידי שלוש שדות. השדה השלישי מכיל מחרוזת Unicode עם הנתיב המלא של המודול. ההסבר זהה עבור השדה BaseDllName רק שהוא מכיל שם ללא נתיב מלא, כמו שציינתי קודם.
השדה InMemoryOrderLinks בנוי מהמבנה LIST_ENTRY שנראה כך:
יש לנו כאן מצביע אחד ל-LIST_ENTRY הבא ברשימה ומצביע אחד ל-LIST_ENTRY הקודם. זה הבסיס לרשימה המקושרת. חשוב לשים לב לדבר אחד – ה-LIST_ENTRY הוא מצביע לשדה InMemoryOrderLinks ב-LDR_DATA_TABLE_ENTRY ולא לתחילת המבנה, לכן כאשר ננסה לרוץ על הרשימה צריך לקחת בחשבון להוריד את ה-Offset הנדרש על מנת להגיע לתחילת המבנה. הרשימה בנויה כך שהאיבר הראשון בה הוא המודול של התהליך הנוכחי (אפשר לראות גם בפריסה כי הוא מכיל הצבעה ל-ImageBase שגם ה-PEB מצביע אליו). לאחר מכן כל איבר ברשימה מצביע על מודול אחר שנטען לזיכרון. האיבר האחרון "ריק". כלומר אם ננסה לראות את התוכן ב-FullDllName נראה אפסים בתת-השדה Buffer. אולם אם נסתכל על ה-InMemoryOrderList נראה הצבעה לאיבר הראשון (זה שאמרנו שהוא של התהליך עצמו) ברשימה והצבעה גם לאיבר הקודם (האחרון ברשימה). כך נוצרת לנו רשימה דו-כיוונית שהיא גם מעגלית ואפשר לזהות את סופה לפי Buffer ריק. אז הכל נראה מאוד ברור עכשיו, רק שחסר לנו חלק אחד בפאזל – איך מגיעים ל-PEB? התשובה לשאלה הזו נמצאת במבנה TEB, Thread Environment Block, שמכיל הצבעה ל-PEB. כך כל Thread יכול להגיע ל-PEB. כדי להגיע ל-TEB כל מה שצריך לעשות זה לקחת את הכתובת מהאוגר FS (FS נקרא FS כי הוא מכיל יותר מידע מהאוגר ES שמכיל Extra data והוא נקרא FS כי F מגיעה אחרי E). כעת שהבנו את רצף הפעולות כדי להגיע לכל מבנה של מודול בזיכרון התהליך נכתוב קוד שמפרסר את ה-PEB ומדפיס את השם של כל מודול ואת הכתובת אליה נטען. יתרה מזו, נוסיף כי כדי למצוא כתובת של פונקציה בזיכרון של תהליך כל מה שנצטרך לעשות זה לפרסר את ה-EAT של ה-PE, ואת זה ביצענו במאמר "מתחת למכסה המנוע של קבצי הרצה - מבינים PE חלק ב". על מנת להדגים זאת, נוסיף לקוד שלנו פונקציה שיודעת לחפש שם של פונקציה בתוך מודול בזיכרון של תהליך.
הדפסת כל המודולים הטעונים לתהליך
לפני שנכתוב את הקוד אני רוצה שתחשבו איך אפשר לנצל את המידע שאנחנו צוברים עכשיו במהלך תקיפה. קחו רמז – זה יתלבש כמו כפפה על תקיפת DLL Injection אותה הדגמתי במאמר "המדריך למזריק (DLL) המתחיל – DLL INJECTION USING CREATE REMOTE THREAD – מאמר ראשון בסדרת הזרקות קוד ו-HOOKING". שנתחיל?פתחו פרויקט חדש ב-VS, בחרו ב-Empty Project, צרו פונקציית _tmain , ותוסיפו את הספריות הבאות. המצב אמור להיראות ככה:
הקוד שלנו מתאים לארכיטקטורה x86, הפערים בין הקוד שלנו לקוד שיתאים ל-x64 לא רציניים אך גם אין טעם להתייחס לזה במסגרת המאמר ולכן לא אעשה זאת. הדבר ראשון שנרצה לעשות זה להגיע ל-TEB של ה-Thread בו הקוד שנכתוב ירוץ. משם נמצא את הדרך ל-PEB שההצבעה אליו מוחזקת ב-TEB. נעשה זאת עם inline assembly, בו ניקח את הערך באוגר ה-FS, כדי לקבל את הכתובת של ה-TEB ואז נשתמש בשדה המתאים ב-TEB כדי להגיע ל-PEB:
מכאן נרצה להגיע ל-PEB_LDR_DATA שהוא ראש הרשימה, כך שממנו נוכל להתחיל לרוץ על כל הרשימה ולהדפיס כל מודול שהתהליך שלנו טען לזיכרון. מהרגע שיש לנו את ה- PEB_LDR_DATA, ניצור לעצנו שני מבנים לעבוד איתם: האחד הוא מסוג LIST_ENTRY והשני הוא מהסוג LDR_DATA_TABLE_ENTRY. ה-LIST_ENTRY ישמש אותנו כחוליה המקשרת אותנו לחוליה הבאה בשרשרת. ה-LDR_DATA_TABLE_ENTRY ישמש אותנו כמכולה לכל המבנה של כל רשומת מודול טעון. כך שבפועל, בכל איטרציה בה אנחנו רוצים להתקדם רשומה אחת קדימה, נפנה את ה-LIST_ENTRY שלנו ל-LIST_ENTRY הבא, ואת ה-LDR_DATA_TABLE_ENTRY שלנו נפנה ל-LIST_ENTRY החדש פחות ה-Offset הנכון כדי להגיע לתחילת המבנה. בתמונה הבאה נגדיר את ה-PEB_LDR_DATA לכתובת שלו, נגדיר גם את ה-LIST_ENTRY לחוליה הראשונה אליה מצביע ה-PEV_LDR_DATA ונסיים בלהגדיר את ה-LDR_DATA_TABLE_ENTRY לכתובת של ה-LIST_ENTRY פחות ה-Offset:
כעת ניצור את הלולאה. התנאי לעצירת הלולאה יהיה "האם השם של ה-DLL מכיל 0". בפועל נבדוק אם השדה Buffer של השדה FullDllName של הרשומה הנוכחית מכיל 0. אם כן זה אומר שהגענו לסוף הרשומה ואם לא נמשיך עוד איטרציה. הלולאה עצמה, בתור התחלה, תכיל שלוש שורות קוד. שורה ראשונה – נדפיס עם פונקציה מותאמת ל-Unicode את השם של המודול והמקום אליו הוא נטען. שורה שניה – נקשר את ה-LIST_ENTRY שלנו ל-LIST_ENTRY הבא. שורה שלישית – נקשר את ה-LDR_DATA_TABLE_ENTRY שלנו לכתובת של ה-LIST_ENTRY פחות ה-Offset. ככה זה נראה:
כעת אפשר לומר שהשגנו את המטרה העיקרית ואם נריץ את זה נקבל הדפסה של כל המודולים שהתהליך טען. אולם רצינו להוסיף פונקציה שתחפש כתובת של פונקציה כלשהי שמודול מסוים מייצא. כדי שזה יקרה נצטרך לבנות פונקציה שתחפש על פי מיקום של מודול ושם של פונקציה מיוצאת במודול, את הכתובת של אותה פונקציה. בדוגמה שלנו נחפש את הפונקציה LoadLibraryA במודול Kernel32.dll. תחילה, ניצור תנאי שבודק אם שם המודול הנוכחי הוא Kernel32.dll ואם כן נקרא לפונקציה שלנו:
שימו לב שאת הקטע קוד בתמונה אתם ממקמים לפני עדכון ה-LIST_ENTRY וה-LDR_DATA_TABLE_ENTRY.
הפונקציה שנבנה תקרא printFunctionAddressInModule כמו שאפשר להבין מהתמונה לעיל. נעבור לבנות את הפונקציה. כמתואר קודם לכן – הפונקציה תקבל שם של פונקציה וכתובת ולכן נגדיר אותה כך:
בשורות הבאות, כל מה שאני רוצה זה להגיע לטבלת ה-Exports כדי להשיג את המידע שלי. אין סיבה שאתאר שוב איך האזור הזה ב-PE בנוי, אם אתם לא סגורים על זה אתם מוזמנים לקרוא על זה במאמר "מתחת למכסה המנוע של קבצי הרצה - מבינים PE" שם דיברתי על הנושא בהרחבה. נתחיל מה-DOS_HEADER משם נעבור ל-PE_HEADERS ושם נמצא את הכתובת של ה-Data Directory הראשון (מפנה ל-Exports) ונשמור אותו. ככה זה נראה עד עכשיו:
מאחר שה-Exports Data Directory מכיל את הכתובת של שלושת הטבלאות שמייצגות את ה-Exports ב-PE, נוכל מאוד בקלות להשיג את הכתובות שלהן ולהתחיל את תהליך החיפוש.
כעת נבנה לולאה שתרוץ מספר פעמים השווה למספר הפונקציות שה-DLL הזה מייצא. נמצא את הערך הזה ב-Exports Data Directory.
בתוך הלולאה הזו, ניצור עוד לולאה. ללולאה הזו נגיד "עבור כל פונקציה בלולאה הגדולה, את אמורה לעבור על כל השמות בטבלת השמות". על פי ההוראה הזו – כך תראה ההגדרה של הלולאה הפנימית:
בתוך הלולאה הזו נרצה לבנות תנאי מעט מורכב במבט ראשוני אך הגיוני למדי. התנאי אומר את הדבר הבא: "במידה והאיבר באינדקס הנוכחי של הלולאה הפנימית, ברשימת ה-Name Ordinals, מכיל את האינדקס של הלולאה החיצונית וגם האיבר באינדקס הנוכחי של הלולאה הפנימית, ברשימת המצביעים לשמות, שווה לשם הפונקציה שהעברנו לפונקציה שלנו אז התנאי נכון". תחזרו על זה. תקראו את זה שוב לאט יותר. החלק הראשון של התנאי אומר במילים פשוטות "אני רוצה לוודא שהאיבר הנוכחי ברשומות ה-Name Ordinals מתכוון לאיבר הנכון בלולאה הגדולה אותו אני רוצה לברר כרגע". החלק השני של התנאי אומר "בהנחה שהחלק הראשון נכון – אני יודע שאני מדבר על האיבר הנכון. כעת אני רוצה לוודא גם שהאיבר באינדקס הזה, בטבלת המצביעים לשמות, מכיל הצבעה לשם זהה לשם הפונקציה שאני מחפש". כעת זה אמור להיות ברור יותר, ואם זה לא אז אני ממליץ לקרוא שוב את ההסברים שלי על ה-Exports של PE במאמר שציינתי קודם לכן – הסברתי את זה שם בפירוט ובצורה ברורה לחלוטין. ברגע שהתנאי מתקיים – אני מדפיס את הכתובת של הפונקציה ואת הפונקציה ושלום על ישראל – כלומר break. ככה יראה התנאי:
ככה יראה הבלוק של התנאי:
זהו זה.
עכשיו שכל חלקי הפאזל לפנינו והרכבנו את הפאזל הגיע הזמן לראות שהתמונה שלמה. נריץ את הקוד שלנו ולהלן הפלט:
הסבר איך נוכל לנצל את זה
לפני שהתחלנו לכתוב את הקוד, ביקשתי שתחשבו איך ננצל את זה בתקיפה. סביר להניח שהגעתם למסקנה אחת מאוד פשוטה – ריצה על הרשומות והבנת המבנים האלה יכולה לעזור לנו כאשר נעשה DLL Injection לתהליך ונרצה להסתיר את עצמנו מ-Debugger-ים שונים. נניח שחוקרים אותנו ב-OllyDbg, ובוחנים את המודולים הטעונים. אם נסיר את עצמנו מהרשימה הזו, סביר להניח שה-Debugger לא יראה אותנו ברשימה וכך נוכל להקשות על החוקר.מחבואים
ראשית נראה מה המצב הנוכחי כאשר אנו מחברים את OllyDbg לתהליך שלנו, בעת הצגת המודולים הטעונים:אפשר לראות את כל המודולים ובניהם את Kernel32.dll אותו ננסה להסתיר. לפני שנכתוב קוד, בואו נבין את התאוריה. ככה המצב נראה אם נפשט אותו:
ניתן לראות כי כל מודול מחובר להבא ולקודם כיאה לרשימה מקושרת דו-כיוונית. המצב לאחר ריצת הפונקציה שלנו צריך להיראות כך:
כלומר, אם המודול שלנו הוא המודול ה-N, ה-FLINK של המודול ה-N-1 יצביע ל-List Entry של המודול ה-N+1 ואילו ה-BLINK של המודול ה-N+1 יצביע ל-List Entry של המודול ה-N-1. עכשיו נממש. נתחיל מההגדרה של הפונקציה שתקבל מצביע לרשומה להסרה:
כעת נגדיר את ה-LIST_ENTRY הקודם ואת הבא:
בשלב הבא נגדיר את המודלים שאת ה-LIST_ENTRY שלהם הגדרנו הרגע:
לסיום – נחבר את הנקודות הנכונות כמו בתרשים לעיל:
זהו זה. הפונקציה מוכנה. בואו נקרא לה עבור המודול Kernele32.dll:
הקוד הזה נכנס במקום הקוד שקראנו לפונקציה printFunctionAddressInMemory (הקריאה ל-hideModule פשוט במקום הקריאה לprintFunctionAddressInMemory, התנאי הוא אותו תנאי). נריץ. נפתח OllyDbg והתוצאה כדלהלן:
תגובות
הוסף רשומת תגובה