המדריך למזריק (DLL) המתחיל – DLL Injection Using Create Remote Thread – מאמר ראשון בסדרת הזרקות קוד ו-Hooking

הקדמה

במאמר הקרוב אציג הקדמה לגבי Process Injection ואפרט על שיטת הזרקה מסוג DLL Injection. השיטה שאדבר עליה היא DLL Injection using CreateRemoteThread – שיטה ברמת מתחילים. זהו המאמר הראשון בסדרת מאמרים שאני מקווה להוציא בנושא הזרקות ו-Hooking לתהליכים. המאמר יכיל הסבר תאורתי של השיטה ולאחר מכן כתיבת קוד שלב אחר שלב. אם אתם מכירים את השיטה ולא מימשתם, אם מימשתם אבל לא הבנתם מה באמת אתם עושים ומה המשמעות של קוד שורת קוד שכתבתם – המאמר הזה מתאים לכם. אם מימשתם והבנתם בדיוק מה כתבתם, המאמר לא יחדש יותר מידיי, ובכל זאת אשמח שתקראו אותו כדי להעביר לי פידבק בנוגע לצורת ההסבר של הדברים. אז נפשיל שרוולים ובואו נתחיל.

Process Injection – מה זה ולמה זה עוזר לתקוף?

Process Injection היא שיטה שמטרתה הרצת קוד כלשהו ממרחב הכתובות של תהליך אחר. כלומר, הרצת קוד מ-Context של תהליך אחר. התוצאה של השיטה היא שכל הקוד שרץ, לא רץ מתוך תהליך זדוני אלא מתהליך לגיטימי שנבחר במערכת. בנוסף, כאשר קוד רץ מ-Context של תהליך אחר, יש לו גישה לכל משאבי התהליך – זיכרון התהליך, משאבים רשתיים, קבצים וכו'. לעיתים השיטה תעזור להסלים הרשאות אך בדרך כלל נצליח לממש את השיטה על תהליכים עם אותה רמת הרשאות כמונו.

DLL Injection – מה הקשר ל-Process Injection?

כמו שפירטתי במאמר PE (חובה לקרוא אם לא חזקים על המבנה של קבצי הרצה ב-Windows)), DLL הוא קובץ הרצה המייצג ספריה ממנה אפשר להריץ פונקציות אשר מיוצאות על ידי ה-DLL. ניתן לטעון DLL או בצורה דינאמית, כלומר כאשר התהליך נטען לזיכרון, או ממש בזמן הריצה של התהליך, כלומר לאחר שהוא נטען. תתארו מצב שבו אתם רוצים להשתמש ב-DLL כלשהו רק במצב שבו תנאי מתקיים בתכנית שלכם. למה שתטענו אותו סתם מההתחלה? או שהוא DLL קליל, או שהסבירות להתקיימות התנאי הזה / תנאים אחרים עם אותה תוצאה גבוהה ואז המתכנת יעדיף לטעון את ה-DLL בכל מקרה. בשורה התחתונה – ניתן לטעון DLL גם לאחר שהתהליך נטען לזיכרון ורץ, כלומר ב-Run Time.

שלבים לביצוע DLL Injection

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

אם אנחנו רוצים לגרום לתהליך להריץ Thread חדש אנחנו צריכים לקבל גישה לתהליך הזה קודם כל. כיוון שה-Thread אמור להריץ קוד מסוים שיגיד לתהליך לטעון DLL, אנחנו צריכים להריץ פונקציה שתטען DLL. כדי שהפונקציה תדע איזה DLL להריץ, הפונקציה צריכה לקבל כפרמטר את שם ה-DLL אך כנראה שהשם של ה-DLL שלנו לא יהיה כתוב סתם ככה במרחב הכתובות של התהליך הקורבן שלנו. לכן אנחנו גם צריכים לכתוב למרחב הכתובות של התהליך קורבן, את שם הנתיב ל-DLL אותו אנו מזריקים. בשביל לכתוב למרחב הכתובות של התהליך קורבן, תענו לי אתם, מה אנחנו צריכים? נכון! – לפתוח את התהליך, משמע Handle עם ההרשאות המתאימות. חזק שלכם, עכשיו שהבנו את הצריכים שלנו בואו נבנה תקיפה.

פרה פרה

נלך מהסוף להתחלה בפסקה הקודמת. כדי לפתוח את התהליך, ולקבל Handle אליו אנו נשתמש בפונקציה OpenProcess. עכשיו אנחנו צריכים לחפש פונקציה שתדע לקבל פרמטר שהוא נתיב ל-DLL ולטעון את ה-DLL הזה, שמעתם על LoadLibrary? זוהי פונקציה שקיימת ב-Kernel32.dll, DLL שכל תהליך טוען משמע הפונקציה הזו קיימת בכל תהליך. כדי לקבל את הכתובת שלה נשתמש בפונקציה GetProcAddress שמקבלת שם של מודול (DLL זה מודול) ושם של פונקציה לחפש בו. מצאנו את הפונקציה, כעת נעביר לה את הנתיב. צודקים, צריך קודם לכתוב את הנתיב למרחב הכתובות של הזיכרון קורבן. נעשה זאת באמצעות הפונקציות VirtualAllocEx שיודעת לאלקץ זיכרון בתהליך מרוחק, ו-WriteProcessMemory שיודעת לכתוב לזכרון מאולקץ בתהליך מרוחק. כעת נעביר את הכתובת אליה כתבנו עם WriteProcessmemory את הנתיב שלנו, לכתובת של LoadLibrary. חסר לנו משהו אחד – מישהו צריך להריץ את כל זה. כדי להריץ זה ניצור Thread חדש בתהליך הקורבן. נעשה זאת באמצעות הפונקציה CreateRemoteThread אליה נעביר את הכתובת של LoadLibrary ואת הכתובת של הנתיב של ה-DLL שאנחנו רוצים להזריק.
נסכם את כל זה בקצרה – אנחנו יוצרים Thread חדש שמריץ את LoadLibrary עם הנתיב של ה-DLL שלנו כדי שה-DLL שלנו ייטען למרחב הכתובות של התהליך החדש ויריץ שם כל מה שנכתוב בו. כעת שהכל ברור, נעבור לכתיבת הקוד.

הגיע הזמן ללכלך את הידיים בקצת C

נתחיל מכתיבת ה-DLL. כדי לכתוב DLL פתחו Visual Studio בחרו ב-File, לאחר מכן New ומשם ל-Project. כעת סמנו את האופציה של Windows Desktop תחת Visual C++ ואז בחרו ב-Dynamic-Link Library (DLL). אתם יכולים לבחור בסביבות פיתוח אחרות - אתם יכולים לבחור לכתוב קוד עם notepad ולקמפל עם GCC – לא באמת משנה. פתחו את dllmain.cpp, מה שאתם רואים לפניכם זה את פונקציית ה-Main של ה-DLL. זוהי פונקציה שרצה בכל טעינה של ה-DLL. בתוך הפונקציה יש לכם Switch-Case שמתאר ארבע אפשרויות: טעינה ל-Thread, טעינה ל-Process, יציאה מ-Thread ויציאה מ-Process. עקרונית כדי לפשט את העניין כמה שיותר, נמחק את ה-Switch Case והקוד שלנו ירוץ בכל אחד מהמקרים האלה. ככה ה-dllmain.cpp שלכם אמור להיראות במצב הנוכחי:

שוב, כדי לפשט את העניין כל מה שנעשה ב-DLL יהיה להקפיץ חלון עם הודעה משלנו. נשתמש בפונקציה MessageBox כדי לעשות זאת:

תקמפלו עם Ctrl+Shift+b ובזה סיימנו לכתוב את ה-DLL שלנו. נעבור לחלק העיקרי – נכתוב את התכנית המזריקה שבעצם תממש את כל הלוגיקה שתיארנו קודם לכן.
פתחו שוב פרויקט חדש ב-VS ובחרו ב-Empty Project, צרו קובץ cpp חדש.

הערה: בקוד שלי אני הולך להשתמש בספריה tchar.h כדי שאוכל לרשום תכנית שתעבוד גם עם תקומפל כ-Unicode וגם אם תקומפל ב-ANSI. בפועל זה אומר שפונקציות שלהן שתי וורסיות – אחת ל-Unicode ואחת ל-ANSI, יתורגמו לפונקציה הנכונה אוטומטית. לדוגמה אם אני רוצה להשתמש ב-LoadLibrary, אם תחפשו LoadLibrary ב-MSDN תראו או LoadLibraryA או LoadLibraryW. הראשונה היא עבור תכניות ANSI והשניה עבור תכניות Unicode. כאשר אני אשתמש בפונקציה LoadLibrary, בפועל או LoadLibraryA או LoadLibraryW תקרא – בהתאם להגדרות הקימפול שלי. איך כל זה קשור ל-tchar.h? TCHAR עוזרת לנו להתאים את הקוד שלנו לקונספט. נניח אם נרצה להעביר מחרוזת לפונקציה שלה יש שתי וורסיות, איך נעשה את זה? נוכל להגדיר מחרוזת של תווים מסוג TCHAR שבפועל אם ההגדרה שלנו תהיה Unicode אז זה יהיה מערך של תווים מסוג Unicode (מכילים בגודלם 2 בתים ועובדים לפי טבלה שונה, לא ASCII), ואם ההגדרה תהיה ANSI אז תווי ANSI. חשוב להבין את הנקודה כדי שהקוד יהיה ברור יותר.

יצירת ה-Injector

נגדיר שהקוד שלנו מקבל כפרמטר את ה-PID של התהליך שאנחנו רוצים להזריק אליו:
ניתן לראות בשורה האחרונה כי המרנו את הפרמטר שקיבלנו ממחרוזת למספר.
כעת נרצה לפתוח את התהליך קורבן, נעשה זאת באמצעות OpenProcess שמקבלת את ההרשאות איתן אנו מעוניינים לפתוח את התהליך, האם אנו נרצה שה-Handle שנקבל יורש ואת ה-PID של התהליך שאנחנו רוצים לפתוח. בהרשאות נציב PROCESS_ALL_ACCESS שמכיל את ההרשאות נצטרך, NULL עבור עניין ההרשאות ואת ה-PID שלנו. במקרה שהפונקציה תיכשל נדפיס הודעה מתאימה ונסיים את התכנית עם קוד יציאה 1.

כעת אנחנו רוצים לכתוב את הנתיב ל-DLL שלנו לזיכרון של התהליך הנתקף. נעשה זאת עם VirtualAllocEx. בפרמטר הראשון נעביר לה את ה-Handle שקיבלנו מ-OpenProcess קודם לכן, בפרמטר השני נעביר NULL כי לא אכפת לנו מאיזה כתובת להתחיל את הקצאת המקום, בשלישי נעביר את גודל המקום שנרצה להקצות (עד הפסקה הבאה תחשבו מאיפה אנחנו מביאים את הגודל הזה), ברביעי נגדיר איזה הקצאה אנחנו רוצים שתקרה – במקרה שלנו נעשה RESERVE ו- COMMIT, ובאחרון איזה הרשאות אנחנו רוצים שיהיו על הדפים שהקצנו.

חשבתם על גודל ההקצאה? אז בטח הבנתם שאנחנו צריכים להקצות מקום בגודל של הנתיב של ה-DLL שלנו פלוס תו אחד עבור ה-Null Terminator. נשתמש בפונקציה _tcslen כדי לקבל את האורך של הנתיב ל-DLL שלנו. נכפיל בגודל של כל TCHAR ונוסיף עוד TCHAR אחד. הפונקציה VirtualAllocEx תחזיר לנו את הכתובת ממנה התחיל ה-Buffer שביקשנו. אם הערך שהוחזר הוא NULL משמע הפונקציה כשלה ונצא מהתכנית.

אחרי שקיבלנו כתובת ו-Buffer בזיכרון של התהליך הנתקף, נכתוב ל-Buffer את הנתיב. נעשה זאת עם הפונקציה WriteProcessMemory שיודעת לקבל Handle לתהליך ולכתוב אליו. בפרמטר השני שהפונקציה מקבלת נרשום את הכתובת אליה נרצה לכתוב. בשלישי את הערך שנרצה לכתוב – הנתיב, ברביעי את האורך של מה שאנחנו כותבים – חישבנו אותו קודם לכן, ובאחרון נוכל לתת מצביע למשתנה שיכיל כמה מהמידע שרצינו לכתוב באמת נכתב – נרשום שם NULL למרות שבתכניות שהן לא למטרת לימוד תמיד נעדיף להיות בבקרה כמה שיותר ולכן הפרמטר יכול לעזור לנו.

כעת יש לנו במרחב הכתובות של התהליך הנתקף את הנתיב ל-DLL שלנו, נשאר לגרום לזה שיוצר שם Thread שיריץ את LoadLibrary עם הנתיב. נתחיל מזה שנחפש את הכתובת של הפונקציה עם GetProcAddress. הפונקציה הזו מקבלת Handle למודול (שוב - DLL שנטען לתהליך הוא מודול) ושם של פונקציה. אין לנו את ה-Handle הנדרש אז נקרא לפונקציה GetModuleHandle שיודעת להחזיר לי Handle למודול על פי שם שהיא מקבלת. ניתן לה את השם kernel32.dll. את ה-Handle ניתן ל-GetProcAddress ובנוסף ניתן את השם LoadLibrary.

הערה: LOAD_LIBRARY_VERSION מצביע ל-LoadLibraryA או ל-LoadLibraryW בתלות בהגדרת הפרויקט.


חדי האבחנה מבניכם יתהו למה שהשורה הקודמת (זו עם ה-GetProcAddress) תעבוד – הרי הפונקציה תחזיר לנו את הכתובת של LoadLibrary בתוך Kernel32.dll במרחב כתובת של התהליך Injector. אתם צודקים, אני אקבל את הכתובת במרחב הכתובות שלי. אך זו תהיה אותה כתובת גם במרחב הכתובות של תהליכים אחרים. הסיבה לכך היא ש-Kernel32.dll נטען בין המודולים הראשונים ולכן הוא נטען תמיד לאותה כתובת (גם אם ASLR פועל, הכתובת תהיה רנדומלית בטעינה הראשונה אך לאחר מכן תהיה אותה כתובת במודולים נוספים שטוענים אותו, כל עוד אין שם מודול אחר שטעון לשם מה שסביר להניח שלא יקרה כי Kernel32.dll נטען מוקדם).

כל מה שנשאר זה ליצור את ה-Thread ולתת לו את הפרמטרים שהשגנו עד עכשיו. נקרא לפונקציה CreateRemoteThread שיוצרת Thread תהליך מרוחק. פרמטר ראשון – Handle לתהליך הנתקף. פרמטר שני – מצביע למבנה שמגדיר את פוליסת האבטחה של ה-Handle, נשאיר NULL וזה בעצם יגיד שה-Handle לא יורש. פרמטר שלישי – הגודל הראשוני של המחסנית של ה-Thread, נרשום שם 0 ונקבל את הגודל הדיפולטי. פרמטר רביעי – כתובת לפונקציה שיריץ ה-Thread, ניתן את מה שקיבלנו מ-GetProcAddress, כלומר את הכתובת ל-LoadLibrary. פרמטר חמישי – כתובת לארגומנטים שה-Thread יקבל, ניתן את הכתובת שאליה כתבנו את הנתיב ל-DLL. פרמטר שישי – דגלים שישפיעו על יצירת ה-Thread, ניתן כאן 0 שיגיד שה-Thread ירוץ מיידית. פרמטר שביעי – מצביע למשתנה שיקבל את ה-TID של ה-Thread החדש.

נבדוק שהכל תקין ונסיים:

עד לכאן בניית ה-Injector. תקמפלו, תפתחו notepad.exe תראו מה ה-PID, תריצו את ה-Injector ויקפוץ לכם החלון. שימו לב להריץ את גרסת ה-32bit של notepad.exe אם אתם מקמפלים את הקוד שלכם ל-32bit. כנ"ל לגבי 64bit.

זיהוי והגנה

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

סיכום

במאמר הסברנו מה זה Process Injection והעמקנו בשיטת DLL Injection מסוג CreateRemoteThread. הסברנו את התאוריה ואף מימשנו צעד אחר צעד את הקוד. כאמור, זוהי שיטה מאוד ידועה ולכן מאוד מנוטרת. אולם היא מעולה כדי להתנסות לראשונה ב-DLL Injection וכדי להבין מה אנחנו רוצים שיקרה בתקיפה כזו. מכיוון שבכוונתי לפרסם מספר מאמרים נוספים בתחום, החלטתי להתחיל מהפשוט ביותר ולאט לאט להתקדם לשיטות מורכבות יותר. כמו תמיד – אשמח לפידבקים, תוכלו לשלוח לי למייל: Orih90@gmail.com. בנוסף, כדי שתוכלו לדעת מתי מאמר חדש התווסף, תוכלו לעקוב אחריי ב-Twitter: https://twitter.com/OriHadad9. נתראה במאמר הבא.

תגובות

הוסף רשומת תגובה