מתחת למכסה המנוע של קבצי הרצה - מבינים PE חלק ב'
הקדמה
במאמר הקודם (https://oh-isecurity.blogspot.com/2019/11/pe.html) הבנו את מבנה הפורמט PE. כעת הגיע הזמן לוודא את
הבנתנו באמצעות כתיבת תכנית ב C שתפרסר את ה Header-ים, את ה Imports וה Exports של קובץ הרצה כלשהו. אני
מאמין שכתיבת תכניות ב C על מנת להבין קונספט
מסוים היא דרך מצוינת ללמוד בצורת Hands-on ולצאת מעט מגבולות
התאוריה. במאמר זה, אסביר את הקוד שכתבתי כדי לפרסר את הקובץ. מראש, אני לא מתחייב
לכך שזה הקוד המושלם ביותר לבצע מטלה זו וזו גם לא מטרת הקוד. מטרת הקוד היא
להצליח לפרסר את החלקים המבוקשים ותוך כדי לשמור על קוד קריא וקליל במידה
המקסימלית.
הלינק לקוד המלא: (https://drive.google.com/open?id=1jxf3JU0qv8wdsscpB9gW5_raDhmy2q0u)
פונקציית ה main
התכנית מקבלת כפרמטר נתיב לקובץ PE אותו היא תפרסר. לכן
תחילה הקוד בודק שאכן התקבל נתיב ובמידה ולא – מודפסת הודעה על כך והתכנית מפסיקה
לרוץ. לאחר מכן, במידה וכן התקבל נתיב, הקוד מחלץ מהפרמטרים את הנתיב ושומר אותו.
הקוד מדפיס הודעה למשתמש. עד כאן לא מעניין במיוחד. כעת נשתמש בפונקציה GetHandleToMappedFile כדי למפות את הקובץ לזיכרון ולקבל את ה Image Base שלו. לאחר מכן נשתמש בפונקציה PrintPEMetadata על מנת להדפיס את ה Header-ים של הקובץ, בפונקציה PrintImports על מנת להדפיס את ה Import-ים של הקובץ, ואז
בפונקציה PrintExports על מנת להדפיס את ה Export-ים של הקובץ.
הפונקציה GetHandleToMappedFile
פונקציה זו אחראית למפות את הקובץ לזיכרון ולהחזיר Handle לקובץ הממופה. הקוד מתחיל ב CreateFileA שמחזירה לנו Handle לקובץ. אם ה Handle שהוחזר לא תקין, כלומר
ישנה בעיה, הפונקציה ExitAfterError נקראת (נסביר אותה בסוף
המאמר, כרגע תבינו את המשמעות מהשם שלה). אם ה Handle תקין, הפונקציה CreateFileMappingA נקראת. פונקציה זו אחראית למפות את הקובץ
לזיכרון. נקודה שחשוב לשים לב אליה היא הפרמטר SEC_IMAGE שמציין שהקובץ הממופה הוא
קובץ PE (מסקנה שהבנתי בעת כתיבת הקוד היא שחשוב
לקרוא את התיעוד של הפונקציות בהן אני משתמש, מההתחלה ועד הסוף כדי לא לפספס Flag-ים חשובים כאלה ולעשות Debugging שעה סתם). אם קיבלנו Handle תקין לקובץ הממופה ממשיכים הלאה, אחרת ExitAfterError נקראת. החלק האחרון של
הפונקציה הוא לקבל את ה Image Base של הקובץ הממופה, אנו
עושים זאת בעזרת הפונקציה MapViewOfFile. נסכם את הפונקציה –
השתמשנו ב CreateFileA כדי לקבל Handle לקובץ. לאחר מכן נשתמש ב Handle הזה בכך שנעביר אותו
לפונקציה CreateFileMappingA שתמפה את הקובץ לזיכרון
ותחזיר לנו Handle לזה. בסוף השתמשנו ב Handle האחרון שהתקבל, בכך שהעברנו אותו ל MapViewOfFile אשר החזירה לנו את ה ImageBase של הקובץ.
הפונקציה PrintImports
פונקציה זו מורכבת ממספר חלקים. נתחיל בחלק הראשון שלה.
שתי ההוראות הראשונות כבר מוכרות לנו ולכן לא נסביר אותן בשנית. השלב
הבא שלנו זה לקבל את ה RVA של תחילת ה Imports. נעשה את זה באמצעות קבלת ההפניה שמצוינת ב DataDirectory באינדקס אחד בשדה ה VirtualAddress. כדי לקבל את ה Section
Header נשתמש בפונקציה GetSectionHeaderByRVA אותה נסביר מאוחר יותר,
כעת נסתפק בכך שנבין שהיא מחזירה לנו כתובת ל Section Header של ה Section שהיא מקבלת. את התוצאה נשים במצביע למבנה PIMAGE_SECTION_HEADER. לאחר מכן נבדוק אם הוא null. אם כן, נדפיס שאין Imports ואם לא – נמשיך הלאה.
החלק הזה של הפונקציה מסתיים בכך שניצור שני מצביעים. הראשון הוא למבנה מסוג PIMAGE_DATA_DIRECTORY שיקבל את הכתובת של ה DataDirectory של ה Imports, מהפונקציה GetDataDirectoryPtr. המצביע השני הוא מצביע
למבנה מסוג ptrImportDescriptor והוא מכיל הצבעה ל ImportDescriptor הראשון. כאן מסתכם החלק הראשון של הפונקציה.
כעת נעבור לחלק השני.
החלק הזה אחראי להדפיס את ה Import-ים באמצעות ריצה על כל ה Import Directories הקיימים. הלולאה נעצרת כשהיא מזהה ImportDirectory ריק שמעיד על סיום השרשרת. ארבעת ההוראות
הראשונות אחראיות להדפיס פרטים על הספרייה המיובאת. לאחר מכן ניצור שני מצביעים,
אחד ל INT מהכתובת שמכיל השדה OriginalFirstThunk ב Import
Descriptor, ואחד ל IAT מהכתובת שמכילה השדה FirstThunk ב Import Descriptor. כעת נתחיל להדפיס את הפונקציות
המיובאות באמצעות לולאה. כעת נבדוק אם היבוא הוא ייבוא מספרי או שמי. אם מספרי,
נדפיס את המספר. אם שמי ניצור מצביע לשם באמצעות השדה addressOfData שמכיל RVA ל data פלוס ה Image Base שלנו. כעת נדפיס את הפרטים. בסוף הלולאה הזו
נקדם את המצביעים ל INT ול IAT כדי לעבור לפונקציה הבאה
בספרייה הזו. בסוף הלולאה החיצונית (שרצה על ה Import
Descriptors) נקדם את המצביע ל Import Descriptor כדי לעבור להבא בשרשרת.
הפונקציה PrintExports
חמשת השורות הראשונות מוכרות לנו מהפונקציה הקודמת – אותו קונספט אין
צורך לחזור על ההסבר. נשים את האצבע רק על השורה הרביעית, אותה לא ראינו בפירוש
בפונקציה הקודמת. כאן אנו משיגים את ה RVA של הסוף של ה Exports באמצעות סכימת הגודל של ה Exports אותו אנו מקבלים מהשדה Size שנמצא ב DataDirectory של ה Exports עם ה RVA ההתחלתי של ה Exports. לאחר מכן נבדוק שאכן יש Exports, אם לא נדפיס זאת ונצא,
אם כן נמשיך הלאה. כעת נקבל את ה Export Directory ונשים במצביע למבנה PIMAGE_EXPORT_DIRECOTORY. לאחר מכן נדפיס את כמה פרטים לגבי ה Export Section שלנו ונעבור לקבלת הכתובת של ה ENT ו EAT. את אלו נקבל באמצעות
קבלת הכתובות (כל כתובות ליעד שלה) מהשדות AddressOfFunctions, AddressOfNames ו- AddressOfNameOrdinals של המבנה PIMAGE_EXPORT_DIRECTORY. כמו כן, נוסיף לכתובות את ה Image Base שהרי הן RVA-ים. נעבור לחלק השני.
המטרה של חלק זה היא להדפיס את ה Exports וכן לבדוק אם זה Forwarded Export ואם כן, להדפיס לאן הוא Forwarded. נתחיל מהגדרת הלולאה – כאן אנו מגדרים
שהלולאה תרוץ כמספר הפונקציות המצוין בשדה NumberOfFunctions ב Export Directory. נתחיל בלבדוק שה Export לא מכין ערך 0. אם הוא כן
פשוט נמשיך ל Export הבא, זה לא אמור לקרות
במצב תקין. על כל Export שאנו מוצאים, ראשית נדפיס
את ה Entry point ואת ה Ordinal שלו. עכשיו יש חלק קצת
טריקי – ניצור לולאה שתרוץ על כל השמות (לפי השדה NumberOfNames ב ExportDirectory) ותבדוק אם יש התאמה בין
הערך ה Ordinal-י שמצוין במערך ה ExportsNamesOrdinals ל Export עליו אנו מצביעים כרגע ב EAT. אם יש התאמה מדובר על ה Ordinal התואם ל Export הזה. זה אומר שאפשר לקחת את המיקום שבדיוק הגענו אליו (את המשתנה J בעצם) ולחפש ב ENT את הכתובת של השם של ה Export. נקבל RVA אליו נוסיף את ה Image Base ויש לנו את הכתובת של השם של ה Export. נדפיס אותו. עכשיו נעבור לנושא ה Forwarded
Exports. כאן נצטרך לבדוק אם הכתובת של הפונקציה היא מחוץ לטווח הכתובות
של ה Section עצמו, אם כן זה אומר שה Export הוא Forwarded Export ולכן יש לציין את זה. בכך
נגמר הדפסת ה Export-ים.
הפונקציה GetSectiobHeaderByRVA
ראשית נשתמש ב macro שמחזיר לנו מצביע ל Section הראשון. לאחר מכן נרוץ על מספר ה Section הקיימים כאשר בכל ריצה של
הלולאה נבדוק את הטווח כתובות של ה Section אותו אנו בודקים כעת,
מכיל את הכתובת ששלחנו כפרמטר לבדוק את ה Section שלה. בעצם אנו שולחים RVA לפונקציה, הפונקציה בודקת בכל Section אם ה RVA שהיא קיבלה נמצא בטווח של אותו ה Section. אם כן מוחזר ה Section בצורת מבנה מהסוג PIMAGE_SECTION_HEADER, אם לא עוברים ל Section הבא וכן הלאה.
הפונקציה ExitAfterError
הפונקציה אחראית להדפיס את error שקרה ולצאת. ה system(“pause”) זו דרך להשאיר את ה CMD פתוח לפני שהוא נסגר עם ExitProcess.
סיכום
במאמר זה הבנו באמצעות קוד ב C חלקים מהמבנה של הפורמט PE. המאמר יכול להוות בסיס לתכניות אחרות שמטרתן לעבוד מול הפורמט.
לדוגמה לתכנית שתבצע IAT Hooking. מקווה שהמאמר עזר לכם,
כמו תמיד אשמח לשמוע מה אתם חושבים דרך המייל שלי orih90@gmail.com.
תגובות
הוסף רשומת תגובה