מתחת למכסה המנוע של קבצי הרצה - מבינים PE

הקדמה

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


Portable Executable Overview

ראשית נעשה מעין Overview על גלגל החיים של ה Portable Executable, או בקיצור ה PE:

בתחילת גלגל החיים של ה PE ישנו Source Code שיכול להיות כתוב בקובץ אחד או ביותר, בשפה כלשהי. לאחר מכן ה Source Code הזה נכנס ל Compiler שמוציא כתוצר Object File. ה Object Files הם כבר קבצים בינאריים אשר נכנסים ל Linker שתפקידו להפוך את כל ה Object Files לקובץ הרצה בינארי אחד שלם. הקובץ הבינארי הזה יכול להיות קובץ exe, dll, sys ועוד. כאשר הקובץ הבינארי מתבקש להפוך לתכנית הוא עובר דרך רכיב במערכת ההפעלה שנקרא Loader. רכיב זה אחראי להכין את כל מה שהבינארי דורש כדי לרוץ בהצלחה.
ננסה להבין את ה Linker באמצעות הסרטוט הבא:
בתרשים ניתן לראות כי נכנסים שני קבצי Object ל Linker. ה-Linker מעבד אותם ויודע להוציא תוצר אחד – קובץ בינארי שמכיל את המידע משני קבצי ה-Object לפי פורמט ה PE.
כעת ננסה להבין את ה Loader באותו אופן:
כאן יש לנו את התכנית MyApp.exe שמבקשת לייבא את Lib1 ואת Lib2 כאשר Lib1 מבקש לייבא את Lib3. ה Loader יברר אם יש בזיכרון, מבעוד מועד, מי מהספריות והאם הן זמינות לשיתוף על ידי תהליכים אחרים. את מי שכבר זמין בזיכרון, ה Loader לא יטען אותו וכך יחסוך במשאבים. במקום לטעון הוא ישתף אותו למרחב הווירטואלי הפרטי של התהליך. הספריות שלא נמצאות בזיכרון ייטענו . תמונת הזיכרון המלאה תראה כמו בתרשים בסופו של דבר (המלבן הכחול מייצג את הזיכרון הווירטואלי כך שכל חלק שתחום בקו מקווקו הוא למעשה חלק בזיכרון).
הערה לחידוד - מה שה Loader יעשה בפועל, במידה ויש dll כלשהו שכבר טעון, זה לקחת את ה Physical Address של אותו dll ולשים אותה ב Virtual Address Space של התהליך שמבקש אותו (במקרה הזה - MyApp.exe). 

שיעור בהיסטוריה

הפורמט המשמש אותנו כיום באופן כללי בקבצי הרצה, קבצי אובייקט וקבצי קישור דינאמי (DLL-ים) נקרא Portable Executable או בקיצור PE. הפורמט עצמו נגזר מפורמט Unix-י בשם COFF שהוצג לראשונה ב Unix system v.
במערכת ההפעלה Windows יש לנו כמה סוגים של של קבצים בינאריים. אלו שמעניינים אותנו במסגרת המאמר הם:
  1. Executable - סוג הבינארי הזה יודע להפיק תכנית שרצה בצורה עצמאית. היא מכילה את כל מה שדרוש לה כדי לרוץ או דורשת ספריות חיצוניות כדי לרוץ (נרחיב על נושא זה בהמשך בפסקת ה Linking). אם היא מבקש ספריות חיצוניות, על ה Loader לספק לה אותן (לטעון אותם לזיכרון או לוודא שהן כבר טעונות ומשותפות) על מנת שתרוץ בצורה תקינה. Executable files ב Windows הם הקבצים עם הסיומת המוכרת EXE.
  2.  Dynamic Linking Library - נקרא בקיצור DLL. בשונה מ Executable ה-DLL אינו מיועד או אמור להפיק תכנית שרצה בצורה עצמאית. מטרת הבינארי הזה היא להוות ספריה חיצונית שבה ממומשים כל מיני API-ים (פונקציות שמתכנת אחר יוכל להשתמש בהן כשהוא כותב תכנית) לשימוש חיצוני. כדי לגרום לקוד בתוך ה- DLL לרוץ, תהליך, שנקרא תהליך מארח, צריך לבקש ממערכת ההפעלה לטעון את ה DLL לזיכרון. כעת הוא מסוגל להשתמש בפונקציות שה DLL מנגיש לו. יש לשים לב כי אולם ה-DLL DLL לא אמור להריץ קוד בצורה דיפולטית אך זה אפשרי בהחלט. אם נממש פונקציית DllMain ב Source Code של ה DLL , היא תרוץ כאשר הוא ייטען לזיכרון. המטרה הלגיטימית של פונקציית ה DllMain היא לבצע כל מיני אתחולים הדרושים לריצה תקינה של ה-DLL כמו לדוגמא אתחול משתנים גלובליים. DLL-ים מקבלים את הסיומת .dll.
  3. Static Library - אוסף של Object Files עם Header שמסביר כיצד הקובץ מאורגן - מעין מפה. גם קובץ זה משמש כספריה בדומה ל-DLL אך בא לידי שימוש רק כאשר מבצעים Linking בצורה סטטית (נכסה את הנושא בהמשך). Static Library ב Windows הם קבצים עם הסיומת .lib.
  4. ישנם סוגים נוספים של קבצים בינאריים כגון SYS לדרייברים ו SCR לשומרי מסך אך עליהם הם לא חלק ממסגרת המאמר ולכן לא נרחיב עליהם.
כעת שיש לנו מעט יותר מושג לגבי קבצי PE נכנס לעובי הקורה של הפורמט.
דבר אחרון לפני שנתחיל – במהלך הקריאה שלכם אני רוצה שתפתחו את הלינק הבא שיוביל אתכם ל-PDF עם פריסה של מרבית פורמט ה PE ויהפוך את הכלל ויזואלי. אם כל תמונה שווה אלף מילים, הסרטוט הזה שווה מיליון. להלן הלינק: http://www.openrce.org/reference_library/files/reference/PE%20Format.pdf 

PE DOS Header

אלו הם 64 bytes הראשונים בקובץ והם כאן למקרה שהתכנית רצה בטעות ב DOS mode (DOS, Disk Operating System היא מערכת הפעלה בסיסית ביותר שרצה על מחשבים אישיים). במצב כזה, שהתכנית אכן רצה ב DOS mode, החלק הזה למעשה יתפקד כתכנית עצמאית. למעשה קבצי PE, ברוב המקרים (לא יצא לי להיתקל בPE שיוצא מן הכלל באופן זה) יכילו בחלק הזה של הקובץ את המחרוזת: “This program cannot run In DOS mode” שתודפס במקרה שאכן התכנית רצה ב DOS mode (פתחו את קובץ ה EXE הראשון שאתם רואים באמצעות עורך טקסט כלשהו ותראו את המשפט הזה).
להלן מבנה ה DOS Header:
במבנה זה יש שני שדות שנרצה להכיר:
1.     e_magic – שדה זה מכיל את ה Magic Number של הקובץ שהוא MZ. Magic Number הוא בדרך כלל מספר שרירותי קבוע (או שרירותי לכאורה במקרה שלנו). במקרה של קבצים, Magic Number מעיד על סוג הקובץ. כך בקבצי Hive (הקבצים שבהם משתמש ה registry) ה Magic Number הוא regf. ה MZ מייצג את הראשי תיבות שמו של אחד ממובילי פיתוח ה MS-DOS Mark Zbikowski.
2.     e_lfanew – שדה זה הוא העיקרי בשבילנו ב Header הזה. הוא מכיל את הכתובת למבנה הבא, ה IMAGE_NT_HEADER

PE NT Header, File Header

כמו לכל פורמט של קובץ הרצה, גם ל PE יש אוסף של שדות במקום ידוע (או קל למציאה) שמגדיר איפה שאר הדברים נמצאים, ואיך שאר הקובץ מסודר. ה Header הזה, מכיל מיקומים וגדלים של אזורי קוד ומידע, לאיזו מערכת הפעלה הקובץ נועד, גודל מחסנית לאתחול ועוד פיסות מידע שמ"ה חייבת לדעת כדי להכין את הזיכרון לריצת הקובץ ואף להריץ אותו. PE Header הוא שם נפוץ למבנה IMAGE_NT_HEADER. צריך להבין כי מבנה זה הוא נקודת ההתחלה לכל יעד ב PE. בפשטות, זה אומר שכדי להגיע (נניח על מנת לפרסר) לאזור מסוים ב PE תאלצו לעבור דרך נקודה זו. כך נראה המבנה:
השדות כאן הם:
1.     Signature – מצביע לתווי ASCII “PE”. אם נפתח Hex Editor כלשהו (אני משתמש ב HxD) נוכל לראות את זה בקלות:
2.     FileHeader – מבנה מסוג IMAGE_FILE_HEADER שמורכב מכמה שדות. נפרט על מבנה זה עוד רגע.
3.     OptionalHeader – עוד מבנה שנדבר עליו עוד רגע.

File Header

IMAGE_FILE_HEADER הוא מבנה המכיל את המידע הבסיסי ביותר לגבי הקובץ. להלן מבנה זה:

השדות שמעניינים אותנו כאן הם:

1.     Machine – מציין את הארכיטקטורה שאליה נועד הקובץ – x64 או x86. אם הערך הוא 0x013C משמע הוא נועד לרוץ על 32bit ואם הערך הוא 0x8684 הוא נועד לרוץ על 64bit.

2.     TimeDateStamp – חותמת זמן שה Linker (או ה compiler ל Object Files) הפיק לקובץ. השדה הזה מחזיק את מספר השניות החל מ 1.1.1970 00:00:00.

3.     NumberOfSections – מספר ה section-ים בקובץ. לפי זה נוכל לדעת כמה שורות יש Section Headers Table עליה נדבר בהמשך.

4.     Characteristics – דגלים עם מידע על הקובץ.  אלו כל האפשרויות:


Optional Header
IMAGE_OPTIONAL_HEADER הוא מבנה המספק מידע הכרחי ל Loader, ובניגוד למשתמע משמו הוא אינו אופציונלי כלל. אולם השם נובע מהעובדה שיש קבצים בינאריים אחרים שלא מכילים אותו (לדוגמא object files) אך ב executables הוא חובה. יש לשים לב שהגודל של Header זה אינו קבוע. ה SizeOfOptionalHeader ב File Header מציין את גודלו. ה Optional Header נראה כך ל32bit64bit יש גדלים שונים בחלק מהשדות ואין את השדה BaseOfData, תראו את ההבדלים בפשטות אם תפתחו winnt.h, ה header file שמכיל את כל המבנים שנצטרך כדי לפרסר את PE):

להלן ההסברים לשדות הרלוונטיים ביותר לנו:
1.     Magic – שדה זה קובע אם הקובץ הוא executable מסוג PE32 או PE32+. אם הוא PE32 אז הערך כאן יהיה 0x10b ו 0x20b ל PE32+. PE32+ מאפשר כתובות בגודל 64bit לצורך העניין. אל תשאלו למה זה נקרא PE32+ ולא PE64. אתם בטח שואלים את עצמכם למה שיהיה עוד שדה שאומר באיזה ארכיטקטורה הקובץ צריך לרוץ. זו שאלה טובה שהרי ראינו קודם לכן את השדה Machine שגם מספק את אותו המידע. אז זהו שה Machine נותן אינדיקציה ראשונית בלבד באיזה ארכיטקטורת מעבד הקובץ רץ בעוד שדה ה Magic אומר ל Loader איך להתייחס למבנים של הקובץ. זה אומר שלפי שדה זה ה Loader יבין שכתובות כאן מנוהלות בצורת 64bit ולא 32bit לדוגמה. המסקנה שנובעת מהאמור לעיל היא שאם נשנה את Magic בקובץ נראה שהקובץ אפילו לא נטען. לראיה notepad.exe של 64bit לפני שאני משנה אותו:
לאחר השינוי:
כעת אנסה להריץ אותו ואקבל את ההודעה הבאה:
לעומת זאת אם אבצע את השינוי המקביל ב Machine הכל רץ כרגיל. אתם מוזמנים לנסות (למטרת הניסוי השתמשתי ב CFF Explorer, ניתן ומומלץ להוריד בחינם https://ntcore.com/?page_id=388 )
2.     AddressOfEntryPoint RVA ל-bit הראשון של הקוד שה Loader צריך להריץ.
דרך אגב, RVA - Relative Virtual Address היא כתובת יחסית למיקום מסוים. במקרה שלנו הנקודה אליה אנו מתייחסים כנקודת האפס היא ה ImageBase (עוד רגע גם תדעו מה זה ImageBase, בינתיים תמשיכו לקרוא) במבנה ה Optional Header. לכם אם ה RVA ל-X כלשהו היא 0x1234 וה-ImageBase היא 0x00100000 אז הכתובת הוירטואלית, הלא רלטיבית היא 00x00101234 שזה הסכום של שניהם בעצם.
כאן הקוד שירוץ בסופו של דבר אחרי שהקובץ נטען לזיכרון נמצא. שימו לב, לא בהכרח שכאן תהיה פונקציית ה main אך סביר שכן. אם נרצה לדבג פוגען, זו יכולה להיות נקודת התחלה טובה ל Breakpoint. לא תמיד זו תהיה נקודת ההתחלה של ה .text section, עליה נדבר בהמשך. ב Driver-ים זו הכתובת של פונקציית האתחול. ב DLL זה אופציונלי שתהיה Entry Point אך לא חובה. במצב ששדה זה אינו מציין ערך הוא יהיה 0 (סטייל none).
3.     SizeOfImage – גודל הקובץ ב byte-ים כולל כל ה header-ים. ערך זה חייב להיות מכפלה של הערך ב Section Alignment.
4.     SectionAlignment – מספר ה byte-ים שכל section מקבל צריך להיות מכפלה של שדה זה. זה בעצם המספר שגדלי ה section-ים צריכים להיות "מיושרים" אליו. אם שדה זה הוא 0x1000 אז נראה section-ים שמתחילים לדוגמא ב 0x1000, x02000, 0x8000, x017000 וכו'. זה מתייחס למצב ש section-ים נטענים לזיכרון.
5.     FileAlignment – אותו דבר כמו SecionAlignment רק על הדיסק. כלומר, כאשר ה section מקבלים גדלים על הדיסק הגדלים האלה יהיו "מיושרים" לערך הזה.
·        ImageBase - כתובת בזיכרון הווירטואלי אליה יעדיף הקובץ להיטען. בהתאם לכתובת זו כל ה RVA יהפכו ל VA (Virtual Address – כתובת וירטואלית מוחלטת, כלומר לא רלטיבית). אם הכתובת תהיה פנויה בזיכרון, הביט הראשון של הקובץ בזיכרון יתחיל שם אחרת, ה-Loader יחפש נקודה אחרת אליה יוכל הקובץ להטען. מכונה Preferred Address או בעברית – כתובת מועדפת של הקובץ.
6.     DllCharacteristics – דגלים שנותנים מידע נוסף למ"ה. דוגמה לאחד יכולה להיות הדגל IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE שמציין את הערך 0x0040 שאומר שכתובות ה DLL יכולות להיות ממקומות מחדש בעת טעינה לזיכרון (נרחיב על relocation מאוחר יותר במאמר). למרות השם של השדה זה לא רק שדות רלוונטיים ל DLL-ים.
7.     DataDirectory – מערך בגודל 16, של מבנים מסוג IMAGE_DIRECTORY_ENTRY שמורכב כך:
א.     VirtualAddress RVA
ב.     Size – גודל ב bytes.
כל מבנה אחראי להיות נקודות ניווט לחלק חשוב בקובץ. כאשר נרצה להגיע לכל מיני נקודות חשובות בקובץ, לדוגמא ה Import Address Table שנדבר עליה בהרחבה בהמשך, נעבור דרך המבנה במערך שאחראי לנווט אליה. כך ה RVA באותו מבנה יגיד לנו לאן ללכת וה Size יגיד לנו מה הגודל של מה שאנחנו מחפשים.

בלינק ל PDF בתחילת המאמר אפשר לראות רשימה של כל ה-Data Directories. נדבר בהרחבה על חלק ניכר מהם בהמשך.

PE Sections ו- Section Headers


Sections 

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

עכשיו שאנחנו יודעים מה זה Section, נראה כל מיני section-ים נפוצים:

1.     .text – קוד להרצה. הקוד לא אמור לצאת מהזיכרון הפיזי.

2.     .data – מידע עם הרשאות read-write. כאן יהיו משתנים גלובליים.

3.     .rdata – מידע לקריאה בלבד. כאן יהיו משתנים לקריאה בלבד, כלומר אי אפשר לערוך אותם.

4.     .bss – מידע גלובלי שלא אותחל עדיין. דוגמא לכך יכולה להיות הגדרה של int ללא הצבת ערך אליו. בגלל שהוא יקבל ערך רק בזיכרון (כשהקוד ירוץ) אז חבל לתת לו מקום סתם על הדיסק. לכן section זה שונה בגודלו בין מצב שהוא בזיכרון לבין בדיסק.

5.     .idata – מידע על ייבוא שהקובץ עושה. נרחיב עליו כאשר נדבר על Import Address Table. לפעמים נכלל בתוך ה .text .

6.     .edata – בדיוק כמו ה idata רק הפוך. כלומר מידע על ייצוא שהקובץ עושה. נרחיב עליו כאשר נדבר על Export Address Table.

7.     .*PAGE – קיים בעיקר ב Kernel Drivers. מידע או קוד שלא יכול להיות Paged Out מהזיכרון לדיסק.

8.     .reloc – שינוי כתובות Hardcoded בעקבות אי קבלת ה preferred address. נרחיב על זה בהמשך שנדבר על relocations.

9.     .rsrc – מידע מסופח לקובץ. נרחיב על חלק זה כאשר נדבר על Resources.

Section Headers

Section Header הוא בעצם מידע על כל Section שצריך לדעת כדי לקרוא ולעבוד מול ה Section. למעשה מעין מפה של ה section כך שכאשר נקרא את ה section נוכל להיעזר ב section header של אותו ה section. המבנה שמייצג Section Header נראה כך:
שדות מעניינים:
1.     Name – מחרוזות בפורמט UTF-8 באורך 8 byte-ים. אם המחרוזת היא בדיוק באורך 8 byte-ים אז אין null-byte כלומר המחרוזות לא תהיה null-terminated. אם היא קטנה מ 8 byte-ים אז היא תהיה null-terminated. אם היא גדולה יותר מ 8 byte-ים השדה יכלול תו “/” שמיד אחריו יהיה תו ASCII שהייצוג הדצימלי שלו זה ה offset לתוך ה string table. אולם executables לא תומכים ב string table ולכן לא יהיה בהם Name גדול מאורך 8 byte-ים. אם object file הופך ל executable והוא מכיל Name ארוך מ 8 byte-ים אז השם פשוט ייקטם.
2.     VirtualAddress RVA ל Section מה Image Base.
3.     PointerToRawData – מצביע ל Section הזה בקובץ עצמו. RVA מתחילת הקובץ.
4.     SizeOfRawData – הגודל של ה Section בדיסק.
5.     Misc.VirtualSize – הגודל של ה Section בזיכרון.
6.     Characteristics – דגלים שמתארים את הsection. לדוגמא שהוא לא יכול להיות Paged Out.
ישנן שתי נקודות מאוד מעניינות שנובעות ממה שאמרנו עד עכשיו:
1.     למה ש Misc.VirtualSize יהיה גדול מ SizeOfRawData.
2.     למה SizeOfRawData יהיה גדול מ Misc.VirtualSize.
הפתרון של הנקודה הראשונה היא שמכוון שה - .bss מכיל מידע לא מאותחל על הדיסק אז הוא קטן יותר מאשר המקום שהוא ידרוש בזיכרון. זאת אומרת שאם אותו ה int שהגדרנו מקודם לא מקבל מקום בדיסק, בזיכרון הוא יקבל וכאן יהיה הפער.
הפתרון של הנקודות השנייה קצת יותר מורכב. ראשית נצא מנקודות הנחה של FileAlignment שווה ל 0x200 ו SectionAlignment שווה ל 0x100. במצב זה אם אני מניח את הקובץ בזיכרון וגודל section מסוים הוא 0x80 bytes אז הוא יקבל 0x100 bytes כאשר ה 0x20 bytes הנותרים יהיו padded (מרופדים בערכים בלתי רלוונטיים למידע). עכשיו כאשר הקובץ יהיה על הדיסק יקרא אותו דבר – הוא יקבל 0x200 bytes (כי זה היישור ל FileAlignment) כאשר 0x120 bytes יהיו padded. במצב כזה הגודל על הדיסק הוא 0x200 והגודל בזיכרון הוא 0x100.
עכשיו אני הולך להיכנס לנושא מעט מורכב ועמוס ולכן חשוב שתהיו ב100 אחוז פוקוס אז אם אתם צריכים אני אמתין תכינו לעצמכם קפה ונמשיך הלאה.

PE Imports


Linking

לפני שנבין את הנושא אנחנו צריכים להבין מה זה Linking. דיברנו על זה בקצרה בתחילת המאמר, ואמרנו ש Linking זו פעולה שמחברת את כל הקבצים הבינאריים לקובץ הרצה אחד שלם. כך אם יש לי עשרה קבצי object שכל אחד מכיל לי source code של מודול אחר בקוד שלי, אני אבצע להם Linking ויהיה לי קובץ אחד שלם. כאשר אני רוצה לעשות Linking יש בפניי שתי אפשרויות (לא באמת שתיים אבל הן הנפוצות והשלישית פחות רלוונטית לפסקה זו), Static Linking ו- Dynamic Linking.
Static Linking – זהו תהליך בו אני מבצע Linking כך שכל מה שהקובץ הסופי שלי צריך כדי לרוץ, יהיה בתוכו כבר. זה אומר שאם אני צריך להשתמש בספריה חיצונית, אני אקח את הקוד שלה ואשים אותו בתוך הקובץ שלי. נבין דוגמא זו טוב יותר באמצעות סרטוט קטן:
יש בזה יתרונות, לדוגמה, אני אף פעם לא אהיה תלוי בנוכחות ספריה חיצונית במחשב שהקובץ שלי ירוץ עליו. מצד שני גם חסרונות לא חסרים כאן. למשל הקובץ מאוד גדול כי הוא כולל את כל הקבצים שאני מייבא מהם קוד. יותר מזה, אם נמצאה חולשה אבטחתית בספריה שאני מייבא ממנה, ויצא עדכון – גרסה חדשה של אותה ספריה רק מתוקנת, אני חייב להיות מספיק אחראי כדי לבצע Linking מחדש ולהתאים את הקוד שלי לעדכון הזה ואז גם להפיץ את הקובץ שלי לכל המשתמשים. דיי כאב ראש..
Dynamic Linking – זהו תהליך שבוא אני מבצע Linking כך שבמקום שהקוד שאני רוצה לייבא יהיה ממש בתוך הקובץ שלי כמו ב Static Linking יש רק הפניה שאני צריך אותו ומה אני צריך ממנו. מעין הוראות ל Loader "אני צריך את shell32.dll, משם אני משתמש בפונקציות ShellMessageBoxA, CommandLineToArgvW ו-CreateProfile" ה Loader אחראי לבדוק שיש את ה DLL הזה בזיכרון ואם לא אז לטעון אותו. אם ניקח את הסרטוט הקודם, ב Dynamic Linking הוא יראה כך:
החסרונות כאן ברורים – אני חייב את ה DLL-ים שאני מבקש היכן שאני רץ (נסו להתקין VS לקמפל שם איזה קובץ C בהגדרות הדיפולטיות ואז תריצו את הקובץ הזה במכונת windows 7 שלא הותקן בה שום דבר חדש, תקבלו הודעה שהקובץ לא יכול לרוץ כי חסר DLL מסוים. ברכות קימפלתם דינאמית וחוויתם את החיסרון בשיטה J). מצד שני היתרונות הן בדיוק ההפך מהחסרונות ב Static Linking – הקובץ קטן, לא בהכרח צריך קימפול מחדש על כל עדכון (אלא אם כן הוא משנה את ה API עצמו) כדי להישאר מעודכן ולכן גם יותר מאובטח.
The Import Section – ה section שאחראי לייבוא של כל ה symbols שקובץ רוצה לייבא. אני אשתמש במונח  symbol כדי להתייחס באופן קולקטיבי לפונקציות או משתנים מיובאים. אם נחזור רגע למבנה של IMAGE_OPTIONAL_HEADER, לשדה DataDirectory ונעשה zoom-in על המבנה באינדקס 1, נוכל לראות שם בשדה ה VirtualAddress הצבעה למערך שבו כל אלמנט הוא מבנה מסוג IMAGE_IMPORT_DESCRIPTOR. כל מבנה במערך, מייצג DLL שהקובץ מייבא ממנו. כך אם אני מייבא מחמישה DLL-ים שונים, אני אמצא שישה מבניי IMAGE_IMPORT_DESCRIPTOR. האחרון מלא באפסים והוא מעיין null-terminator ענק שאומר שנגמר המערך. כך נראה IMAGE_IMPORT_DESCRIPTOR:
להלן ההסבר לשדות שמעניינם אותנו כרגע:
1.     OriginalFirstThunk RVA לטבלה בשם Import Name Table או בקיצור INT. טבלה זו מכילה את כל שמות ה Symbols המיובאים מה DLL הזה.
2.     FirstThunk RVA לטבלה בשם Import Address Table או בקיצור IAT. טבלה זו מכילה את כתובות ה Symbols המיובאים מה DLL הזה.
3.     Name RVA למחרוזת עם שם ה DLL.
עכשיו נבין את החיבור בין כל השדות האלה כדי להבין מה קורה בסופו של דבר שאני רוצה לקרוא לפונקציה כלשהי מ DLL כלשהו.
כאמור, ה INT זוהי טבלה של שמות של symbols ב DLL. הרשימה הזו מכילה בסופו של דבר, בכל איבר בה, מספר באורך 4bytes או 8bytes (4 ל x86 ו 8 ל x64). המבנה של המספר נקרא IMAGE_THUNK_DATA. המבנה נראה כך:
שם החלק
מה החלק
גודל החלק ב bit-ים
תיאור החלק
Ordinal/Name Flag
bit מספר 31  עבור PE32 ו bit מספר 63  עבור PE32+.
1
אם הביט הזה הוא דלוק, ה import הזה מיובא אך ורק בצורת מספר. כלומר יש מספר ייחודי ל import הזה שמזהה אותו ב dll. אם אני רוצה לקרוא ל import הזה אני צריך לקרוא לו במספר שלו. אם הביט הזה שווה 0, ה import הזה מיובא אך ורק בצורת שם. כלומר יש שם ייחודי ל import הזה שמזהה אותו ב dll.
Ordinal Number
bit-ים 0-15
16
מספר באורך 16 bit-ים. שדה זה בא לידי שימוש רק אם מוגדר שה import הזה מיובא על פי מספר. כלומר ה Ordinal/Name Flag דלוק. שאר ב bit-ים במספר הזה (בעיקרון מה שמתואר בשורה הבאה של הטבלה) יהיו מאופסים.
Hint/Name Table RVA
Bit-ים  0-30
31
אם ה Ordinal/Name Flag לא דלוק שדה זה בא לידי שימוש. שדה זה הוא מצביע למבנה אחר בשם IMAGE_IMPORT_BY_NAME אותו נסביר מיד אחרי הטבלה.

Import by Name


כאמור, אפשרי לייבא DLL בצורת מספר או בצורת שם (תלוי במי שבנה את ה DLL לא בי, הוא זה שקובע איך כל פונקציה מיוצאת. נדבר על זה לעומק כאשר נרחיב על Exports). כאשר symbol מיובא באמצעות שם, ה IMAGE_THUNK_DATA מכיל הפנייה לכתובת שבה יש לי מבנה בשם IMAGE_IMPORT_BY_NAME שבנוי כך:
כמו שאפשר לראות ישנם שני שדות – Hint ו Name. Hint מייצג את ה index של ה symbol בטבלת ה exports של ה DLL שממנו אני מייבא. אם ה Index הזה שביר, מסיבה כלשהי, הפונקציה מחופשת באמצעות ה Name שהוא null-terminated string שמייצגת את השם הפורמלי של הפונקציה ב DLL. כל זה מרכיב בסופו של דבר את ה INT – טבלה של זוגות של Name-ים ו Hint-ים שמי שמבציע אליה הוא ה OriginalFirstThunk.
עכשיו נבין את ה IAT. ראשית ה IAT היא טבלה שאמורה להכיל את הכתובת של ה symbols שאנחנו רוצים לייבא. כלומר אם אני אסתכל על הטבלה, בפועל אני אראה רשימה של כתובות כאשר כל כתובת מצביעה על symbol כלשהו. על הדיסק, ה IAT זהה בדיוק ל INT והם מצביעים לאותו מקום (שהרי מאיפה יכול לדעת הקובץ היכן יהיו הכתובות של מי שהוא רוצה לייבא? אז זהו שיש גם אופציה שהוא ינסה לדעת אבל ניגע בזה בהמשך). אולם בזיכרון הוא יצביע על כתובות אמיתיות. כדי לעשות את זה צריך להתבצע תהליך שנקרא Binding. Binding הוא תהליך שמזריק ב IAT את הכתובות האמתיות של ה symbols שהוא מבקש מ DLL-ים. כעת, אנחנו בסיטואציה שהקובץ בזיכרון ומה שה FirstThunk מכיל זה הפנייה לרשימה של כתובת. להלן ההבדל בין המצב של ה import tables בדיסק לבין ההבדל בזיכרון:
ככה זה נראה בדיסק:
ככה זה נראה בזיכרון:

 PE Bound Imports

Bound Imports הוא מצב שבו ה PE עובר Binding לפני שהוא נכנס לזיכרון (כמו שדיברנו לפני רגע). הרעיון כאן הוא לנסות לזרז את תהליך טעינת הקובץ בזיכרון בכך שהקומפיילר ינסה לחזות היכן יהיו הכתובות של ה symbols המיובאים בזיכרון. כאשר קובץ שיש לו Bound Imports נטען לזיכרון, ה loader חייב לעשות בדיקה שבאמת כל כתובות ה Bound Imports נכונות ואם הן לא נכונות, הוא משנה אותן בהתאם.


ה DataDirectory שלנו במקרה הזה הוא DataDirectory באינדקס 11 שנקרא IMAGE_DATA_DIRECTORY_ENTRY_BOND_IMPORT. הוא מכיל RVA למערך של מבני IMAGE_BOUND_IMPORT_DESCRIPTOR. כך הוא נראה:





כל אלמנט במערך תואם ל DLL שה Imports ממנו הם Bound Imports. ה TimeDateStamp חייב להיות תואם לזה של ה DLL שאליו מתייחס האלמנט הספציפי. אם הוא לא יהיה תואם יהיה צורך בכתיבה מחדש של הכתובות כאשר ה Loader ייטען את הקובץ לזיכרון. ה OffsetModuleName מכיל את ה Offset מה IMAGE_BOUND_IMPORT_DESCRIPTOR הראשון, לשם של ה DLL (שהוא null-terminated-string). ה  NumberOfModuleForwarderRefs מציין את המספר המבנים מסוג IMAGE_BOUND_FORWAREDER_REF שקיימים מיד אחרי המבנה הנוכחי. אותו מבנה מוגדר כך:




אם נבחן את IMAGE_BOUND_IMPORT_DESCRIPTOR מול IMAGE_BOUND_FORWARDER_REF נראה שהם ממש דומים. אולם יש סיבה להבדל הקטן ביניהם שטמונה בהבנה של הצורך במבנה הזה בכלל. הצורך במבנה הוא פתירת סיטואציה בה DLL שאנחנו מייבאים ממנו "מעביר" (עושה Forwarding) את הטיפול בפונקציונליות מסיימת ל DLL אחר. דוגמה טובה לזה היא kernel32.dll שעושה זאת ב HeapAlloc. במקרה זה kernel32.dll עושה forwarding ל ntdll.dll לפונקציה RltAllocateHeap. לכן אם ניצור Executable שעושה שימוש בפונקציה זו מ kernel32.dll ונעשה לו Binding מראש, נקבל שימוש במבנים אלו אחד לצד השני.



Delay Loaded DLL


כשדיברנו על שיטות Linking אמרנו שיש שיטה נוספת. עכשיו נתייחס אליה. Delay Loaded DLL הוא DLL שנטען רק כאשר יש ממש פניה ל Symbol שהוא מייצא. זה אומר שעד אשר ה Executable שלי לא צריך Symbol כלשהו מה DLL הזה, הוא לא ייטען לזיכרון. זה טוב למקרה שאני רוצה לטעון DLL מאוד מאוד גדול רק אם תנאי כלשהו מתקיים בתכנית שלי. אחרת חבל על הזמן טעינה שלו.
מנגנון זה אינו פיצ'ר של מ"ה אלא ממומש באמצעות קוד נוסף ב Linker וב Run-Time Library.זו הסיבה שלא נמצא הרבה רפרנסים אליו ב winnt.h. ה delay loaded data מסומן לנו באמעות ה DataDirectory באינדקס 15 בשם IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT. ה RVA שם מצביע למבנה לא רשמי בשם ImgDelayDesc שמוגדר ב DelayImp.h. ב winnt.h קיים מבנה תואם בשם   IMAGE_DELAYLOAD_DESCRIPTOR שנראה כך:
מה שמעניין אותנו כאן:
1.     DllNameRVA - RVA למחרוזת שמכילה את שם ה DLL. שם זה הוא זה שמועבר ל LoadLibrary.
2.     ImportAddressTableRVA RVA ל IAT של ה DLL הזה (ל IMAGE_THUNK_DATA)
3.     ImportNameTableRVA RVA ל INT של ה DLL הזה (IMAGE_THUNK_DATA.AddressOfData)
4.     BoundImportAddressTable RVA ל Bound IAT של ה DLL (אם קיים, אופציונלי).
מבנה זה קיים עבור כל Delay Loaded DLL. הIAT וה INT שאליהן יש לנו כאן RVA-ים, הן בסופו של דבר IAT/INT רגילות לגמרי רק שאינן מכילות מידע רלוונטי עדיין. הן יכילו ב Runtime כאשר ה DLL ייטען. כאשר ישנה קריאת API מ Delay Loaded DLL בפעם הראשונה, ה runtime library קוראת (במידת הצורך) ל LoadLibrary (פונקציה לטעינת ספריה) ולאחר מכן ל GetProcAddress (פונקציה שמחזירה כתובת של פונקציה מיוצאת או משתנה מיוצא מתוך DLL כלשהו). הכתובת המתקבלת מאוחסנת ב delayLoadedIAT ואז הכל מתנהל באותה צורה שמתנהלים מול IAT רגילה. 


PE Exports



דיברנו על צד אחד של העסקה – היבואן, עכשיו נעבור לצד השני – היצואן. כאמור, ניתן לייבא symbols מקבצים המייצאים אותם. בשונה ממה שהרבה חושבים וטועים, לא רק dll-ים או lib-ים מייצאים מידע שקבצים כמו Exe מייבאים. ייצוא המידע הוא חלק מהפורמט ולכן אפשרי למצוא גם קבצי exe, לצורך העניין, שמייצאים מידע. עכשיו נבין איך ייצוא מידע עובד בפורמט ה PE. כל קובץ שמייבא מידע צריך קובץ אחר שייצא את אותו המידע. אם ננסה לדמיין תמונה של איך נראה מבנה הייצוא, אחרי שהבנו את נראה מבנה הייבוא זה לא יהיה כזה מסובך. קונספטואלית בלבד, כל קובץ שרוצה לייצא מידע מחזיק טבלה של שמות ו RVA-ים כאשר כל שם זה שם של פונקציה או משתנה וה RVA מחזיר את הכתובת שלה או שלו. ככה אם אני רוצה לייבא פונקציה, אני אקרא מאותה טבלה של הקובץ שאני רוצה לייבא ממנו עד שאמצא שם את השם המתאים לשם שאני מחפש, אקח את הכתובת שלו אוסיף לה Image Base (כי זה RVA) והנה אני יכול להשתמש בפונקציה/משתנה. אז זהו שזה ככה אבל טיפה יותר מורכב.


ה DataDirectory שיוביל אותנו לאזור שמעניין אותנו הפעם בקובץ נקרא IMAGE_DIRECTORY_ENTRY_EXPORTS והוא מכיל RVA למבנה נוסף בשם IMAGE_EXPORT_DIRECTORY שממנו יש הצבעה למבנים נוספים שנפרט עליהם עוד רגע אבל ראשית, נבין את IMAGE_EXPORT_DIRECTORY. ככה הוא נראה:





נפרט על השדות שמעניינים אותנו במיוחד:

1.     TimeDateStamp – בשדה זה יש את חומת הזמן שנבדקת במצב של Bound Imports. החותמת הזאת עשויה להיות שונה מהחותמת של הקובץ ב FileHeader.

2.     NumberOfFunctions – שדה זה כשמו כן הוא, מספר הפונקציות המיוצאות. נשתמש בשדה זה אם נרצה לסרוק את כל הפונקציות שקובץ מייצא לדוגמה.

3.     NumberOfNames – שדה זה מייצג כמה exports מיוצאים באמצעות שם. מכיוון שיש אפשרות לייצא הן לפי שם והן לא לפי שם (לפי מספר), שדה זה לא בהכרח יהיה שווה לשדה NumberOfFunctions. אם נרצה לדעת כמה לא מיוצאים לפי שם אלא לפי מספר נעשה חיסור בין השניים.

4.     Base – המספר להחסיר ממספר סידורי (ordinal) של פונקציה כדי למצוא את ה index שלה מ – 0. המתכנת יכול לקבוע שה Ordinals של הפונקציות שהוא מייצא יתחיל מ 500,501,502,.. ולא בהכרח מ 0,1,2,... אולם כדי להגיע ל index האמתי של הפונקציה שאני רוצה לייבא כאשר אני מחפש אותה לפי ordinal, מה שצריך לעשות לדעת מאיפה התחילה הספירה, כלומר מה הבסיס ממנו מתחילים. לכן אבדוק את הערך הזה ואחסר אותו מה ordinal שאני מוצא (כל זה לא עושה המתכנת של הקובץ שייבא אלא מ"ה). נבין את זה טוב יותר עם סרטוט קטן.



5.     AddressOfFunctions RVA למערך של שמכיל את ה RVA-ים של תחילת כל symbol מיוצא. גודל המערך הוא NumberOfFunctions. המערך נקרא Export Address Tables – EAT.

6.     AddressOfNames RVA למערך שמכיל את ה RVA-ים שמבציעים לשמות של ה symbols שמיוצאים על פי שם. מערך זה הוא בגודל NumberOfNames. המערך נקרא Export Names Table – ENT.

7.     AddressOfNamesOrdinals RVA שמצביע למערך שמחזיק את המספרים הסידוריים של כל השמות.

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





כאשר אני רוצה לייבא את  Translate. אם קובץ מבקש לייבא את הפונקציה Translate, ה loader יצטרך ללכת ל Export Name Table ולבצע חיפוש בינארי אלפא-בתי (בגלל זה השמות מסודרים בסדר אלפא-בתי) עד שימצא את המחרוזת הזו. כעת שמצאתי, הוא בודק מה האינדקס של מה שמצא ב ENT ורואה שזה 2. הוא הולך ל Names Ordinals לאיבר באינדקס 2 ורואה ששם יש את הערך 0x1. מכאן הוא הולך ל Export Address Table באינדקס 0x1 ורואה שם את הכתובת (RVA). ככה הוא מצא את הכתובת שביקשתי ממנו.

Forwarded Exports

ישנה אופציה שאומרת שאפשר להעביר את הטיפול בפונקציה מסוימת ב DLL אחד ל DLL שני כמו שהזכרנו ב Bound Imports. זה אומר שאני יכול להגיד ל DLL שלי "ציין אצלך שהטיפול בפונקציונליות X נמצאת ב DLL אחר בפונקציה "SomeRandomNameForFunction" שלו. בצורה הנורמלית, ה AddressOfFunctions מצביע למערך של RVA לפונקציות כמו שכבר אמרנו. אולם אם ה RVA במערך הזה מצביע ל export section (נבדק לפי הכתובת בסיס והגודל של ה section) אז ההצבעה הנה למעשה על מחרוזת בפורמט הבא DLLToFowrardTo.FunctionName. לדוגמא ב kernel32.dll יש Forward Exports. אפשר לפתוח PEView ולראות זאת:
אפשר לראות לדוגמא את הפונקציה AddDllDirectorypfile=00073084) שמפנה ל api-ms-win-core-…. כדי לראות איך זה נראה בפועל נבחן את 0073084:
אפשר לראות את שם את ה Data שראינו בתמונה הקודמת – 00094FE6. נוסיף את זה ל Image Base ששווה ל 68B00000 ונקבל 6B894FE6. נחפש את הכתובת הזו ב section ונראה מה יש שם: 

הפונקציה לעשות אליה forward

Debug Information


ב DataDirectory[6] נוכל למצוא מבנה בשם IMAGE_DIRECTORY_ENTRY_DEBUG שמכיל RVA למבנה בשם IMAGE_DEBUG_DIRECTORY שנראה כך:




כאשר executable נוצר עם Debug information, ישנו מידע שאפשר לחלץ משם שיכול לעזור לנו להבין טוב יותר את ה executable הזה. אולם מערכת ההפעלה לא חייבת את המידע הזה כדי שהקובץ ירוץ בצורה תקינה. קובץ exe יכול שיהיו לו מספר צורות של debug information. ה IMAGE_DIRECTRY_ENTRY_DEBUG מצביע למעשה לתחילת מערך של מבנים של IMAGE_DEBUG_DIRECTORY. המערך מורכב מאיבר על כל Debug Information שהקובץ מכיל. השדות שמעניינים אותנו כאן הם:


1.     Type – שדה זה מכיל את סוג ה debug information. האופציות שיש הן:


הדרך לחשב את גודל המערך הזה יכולה להעשות באמצעות ה size ב IMAGE_DIRECTORY_ENTRY_DEBUG. ברוב המקרים ה debug information שנתקל בו יהיה ב PDB file. קבצי PDB הם קבצים לאחסון debugging information על תכנית (או על קבצים שלא הופכים לתכנית בעצמן כגון DLL לצורך העניין). הקבצים בדרך כלל מסתיימים בסיומת .pdb ונוצרים בתהליך הקומפילציה של קוד המקור. ה PDB מכיל רשימה של כל ה symbols של מודול יחד עם הכתובות שלהם בנוסף למידע אחר. הסבר מפורט יותר על קבצים מסוג זה אינו במסגרת מאמר זה (מוזמנים לעיין כאן https://stackoverflow.com/questions/3899573/what-is-a-pdb-file).

נדע לחפש קובץ PDB בכך שסוג ה debug information יהיה IMAGE_DEBUG_TYPE_COFF. אם תבחנו את המידע אליו מצביע המבנה, תראו שם, בין היתר של עוד כמה דברים, את הנתיב לקובץ ה PDB.

2.     SizeOfData – גודל ה debug information בקובץ לא כולל קבצי debug חיצוניים כגון קבצי PDB.

3.     AddressOfRawData – ה RVA לdebug information כאשר הקובץ ממופה. מוגדר ל – 0 כאשר הוא לא.

4.     PointerToRawData – ה offset ל debug information בקובץ.



Relocations




בהרבה מקומות ב Portable executables שלנו נמצא כתובת כמו ששמתם לב. כמו שכבר הסברנו, כאשר ה executable שלנו עובר linking, הוא מקבל preferred load address. הכתובות שנמצאות בו נכונות במצב שה executable באמת מקבל את הכתובת שהוא ביקש ונטען ל preferred load address שלו שמוגדר בשדה של ה ImageBase במבנה ה IMAGE_FILE_HEADER. אם ה loader צריך לטעון את ה executable למיקום שהוא לא ה preferred load address שלו, כל הכתובת ב executable יהיו שגויות שהרי הן מתייחסות למצב בו ה executeable כן נטען לאן שהוא מעדיף. הדבר מצריך עבודה נוספת מצד ה loader כדי לפתור את המצב. דוגמא לסיטואציה שנוצרת והיא בעייתית היא כאשר יש לנו את ההוראה הבאה בקוד:




כאן יש לנו הוראה בכתובת 0x00401020 באורך שישה byte-ים. ההוראה מורכבת מ opcode של ההוראה בשני ה byte-ים הראשונים (0x8b ו- 0x0d). ארבעת ה byte-ים הנותרים מייצגים את ה DWORD שמכיל את כתובת הפרמטר (0x0040D434). בדוגמא הזו, ההוראה מתייחסת למצב בו ה preferred address הנה 0x00400000 ולכן הפרמטר למעשה יושב ב RVA 0x0000D434. במידה והכתובת מתקבלת ההוראה תקינה ושום דבר לא צריך להשתנות ובמידה ולא, הרי שיש לשנות אותה. אז לצורך הבנת העניין בואו נאמר שהכתובת שה loader מצא היא 0x00600000. עכשיו ההוראה לא רלוונטית כמו כל כתובת אחר באותה צורה ולכן יש לשנות אותה. מה שה loader עושה במקרה הזה זה לחשב את הדלתא, את ההפרש, בין הכתובת בפועל ל preferred address. את הדלתא לקחת ולהוסיף לכל מקום שיש בו הפניה לכתובת "לא פתורה". אז ההפרש במקרה שלנו הוא 0x00200000 מה שאומר שהכתובת בפקודת אסמבלי למעלה צריכה להיות 0x0060D434. ה loader יבצע שינוי זה וההוראה תהפוך לתקינה.


כדי לראות מה צריך לשנות ה loader יעבור ב DataDirectory באינדקס 5. שם הוא ימצא מבנה בשם IMAGE_DIRECTORY_ENTRY_BASERELOC שמכיל RVA למערך של מבנים מסוג IMAGE_BASE_RELOCATION. המבנה הזה בנוי כך:




1.     VirtualAddress – כתובת שמצביעה לתחילת בלוק של מידע לגבי relocations. עבור כל בלוק יש מבנה כזה.

2.     SizeOfBlock – גודל כל בלוק. ניישר קו - יש צורך בשינוי הכתובות רק במידה והקובץ לא נטען לכתובת המועדפת שלו על ידי ה Loader, אחרת תהליך שינוי הכתובות לא רלוונטי. אם יש צורך בשינוי הכתובות אז קודם עלינו לדעת איזה כתובות לשנות ולאיזה כתובות לשנות. נדע לחשב את הכתובת החדשות באמצעות הוספת ההפרש בין הכתובת בסופו של דבר לכתובת המועדפת, לכתובת הישנה. כך תתקבל כתובת חדשה נכונה. המבנה IMAGE_DIRECTORY_ENTRY_BASERELOC מכיל הצבעה למערך של מבנים מסוג IMAGE_BASE_RELOCATIONS המבנה הזה מייצג בלוק מרשימת הכתובות שצריך לשנות. IMAGE_BASE_RELOCATION.SizeOfBlock. נכנה את הרשימה שמכילה את הכתובות לשינוי Fix-Up Table. כל בלוק מכיל את השינויים לטווח כתובות של 4k. כל Fix-up block (בלוק בטבלת ה Fix-up) מורכב ממבנה של IMAGE_BASE_RELOCATION ואחריו מספר רשומות של Type/Offset. גודל כל רשומה הוא 2 byte-ים. ומורכבת כמתואר בטבלה הבאה:

תיאור
שדה
גודל
Offset
מציין איזה סוג של fix-up צריך לבצע כאן.
Type
4 bit-ים
0
ה offset ממה שמצוין ה IMAGE_BASE_RELOCATION.VirtualAddress. מציין היכן השינויים צריכים להתבצע.
Offset
12 bit-ים
4 bit-ים


כך למעשה כאשר נרצה להבין רשומה בבלוק נבדוק את ה 4 bit-ים העליונים והם יגידו לנו מה סוג ה fix-up. הסוג שמעניין אותנו הוא IMAGE_REL_BASED_HIGHLOW. לאחר מכן 12 ה bit-ים הנותרים יגידו לנו מה ה offset שצריך לקחת מה RVA שב IMAGE_BASE_RELOCATION.VirtualAddress כדי לקבל את ה RVA של הכתובת לשנות. נדגים:




נפתח את ה .reloc section שם נמצא את ה IMAGE_BASE_RELOCATION (בפועל זה מערך של כאלה, תפתחו notpad.exe ותראו שיש כמה בלוקים). נבחן את הבלוק שמולנו (מתחיל ב RVA 0003c1bc). מציין לנו שה RVA של ה block הוא 00005000 וגודלו 0000001b4. עכשיו נרד לפריט הראשון בבלוק. אפשר לראות ש-4 ה bit-ים העליונים ב data נותנים את הספרה 3 מה שמבציע לנו שמדובר ב Type (סוג ה fix-up) IMAGE_REL_BASED_HIGHLOW. עכשיו נבחן את 12 ה bit-ים הנותרים ונראה כי ה offset שלנו הוא 014. ככה שני הדברים האחרונים מרכיבים את הערך 3014  - 3 בשביל ה type ו 014 בשביל ה offset. עכשיו נקח את ה RVA of Block נוסיף לזה את 014 ונקבל את ה RVA שבה יש את הכתובת לשנות.


Thread Local Storage



תהליך מורכב מ Thread-ים. כל Thread הוא מעיין תהליכון. Thread-ים רצים במקביל בתוך תהליכים. יש Thread אחד מרכזי שתמיד קיים והוא יוצר את כל ה Thread-ים האחרים (כמו פונקציות main בפייתון שבא יוצרים thread-ים נוספים). פונקציונליות זו מאופשרת במערכות הפעלה אשר תומכות ב multi-threading. זה כל ההקדמה שאני אעשה לכם בנושא אז אם בא לכם להרחיב כדי להבין יותר טוב גוגל עומד לרשותכם.מכיוון וכל thread רץ בנפרד לא בהכרח שיש התאמה בין הקוד בכל thread, כלומר עשויות להיות התנגשות בין שמות של משתנים גלובליים, בין פונקציות וכדומה. לכן יש צורך לבודד את מרחב הזיכרון של כל Thread. מרחב הזיכרון של כל Thread נקרא Thread Local Storage. כעיקרון לאף Thread אחר אין גישה למידע שה Thread שומר לעצמו ב  local storageשלו. אמנם לפעמים כן צריך להיות קשר בין Thread-ים ולכן יש מנגנונים שמתאימים לצורך זה.


אחרי שהבנו את הבעיה והכרנו את הפתרון (רעיונית בלבד) בואו נבין איך זה עובד מעט Under the Hood. אז כדי לפתור את הבעיה הבנו שצריך ליצור לכל Thread מרחב זיכרון שמקשור רק אליו ורק לו תהיה גישה למידה שם. ה PE Format מאפשר לנו ליצור פונקציות Callback להוספת פונקציונליות לאתחול (Initialization) ולסיום (Termination) של כל Thread. למרות שבדרך כלל יש פונקציית Callback (פונקציה שמונגשת לפונקציה אחרת ורצה לאחר פעילות מושלמת של הפונקציה הראשונה) אחת, ניתן לשייך ל TLS יותר מאחת וזה מאושר בזכות זה שהמימוש של אחסון הכתובות של הפונקציות הללו הוא null-terminated array, כלומר מערך של כתובות עד שיש איבר שהוא בעל אפסים בלבד (כמו null-terminated string)

ה DataDirectory שמוביל אותי נקרא IMAGE_DIRECTORY_ENTRY_TLS והוא מצביע למבנה מסוג IMAGE_TLS_DIRECTORY. ככה הוא נראה:


השדות שמעניינים אותנו:
1.     StartAddressOfRawData – הכתובת הוירטואלית שבה מתחיל המידע הזה (שימו לה זו כתובת מוחלטת לא RVA)
2.     EndAddressOfRawDAta – הכתובת הוירטואלית שבה נגמר המידע הזה (שימו לב זו כתובת מוחלטת לא RVA)
3.     AddressOfCallBacks – כתובת וירטואלית שמצביעה למערך של Callbacks. המערך מורכב ממבנים מסוג PIMAGE_TLS_CALLBACK. נראה כך:
שיטה מעניינת שפוגענים עלולים להשתמש בה היא הסתרת קוד זדוני בפונקציות Callback. הרעיון הוא לכתוב קוד זדוני לפונקציות הללו על מנת להקשות על החקירה של הקובץ. לא כל חוקר מתחיל יודע על קיומן ויותר מזה על העובדה שהן רצות עוד לפני ה AddressOfEntryPoint ב Optional Header, מה שהופך את זה לעוצמתי בהחלט.

Resources


מכל ה section-ים, לדעתי זהו ה section הכי קשה לניווט לכן, על אף שאתם בטח מותשים נסו להישאר מרוכזים כי אנחנו נכנסים לנושא מעט מורכב.
משאבים בקובץ PE הם מעין נספחים שהקובץ מצרף לעצמו כדי להשתמש בהם בכל סביבה שהוא מגיע אליה. כך לדוגמה icon של תכנית או dialog box יכול להיות resource-ים. מלבד הדברים הטריוויאלים, משאבים יכולים להיות כל דבר, החל מקבצי EXE, DLL או כל סוג אחר של קובץ ש PE שלנו ירצה להשתמש בו. דוגמא לכך היא נשיאת resource שהוא DLL שמשמיש חולשה כלשהי כך שהפוגען יספח אותו אליו, יחלץ אותו בעת ריצה ויטען אותו לזיכרון וכך יוכל להשתמש בו. ישנן כל מיני תוכנות לפרסור ה resource section כאשר התכנית שאני משתמש בה היא Resource Hacker (http://www.angusj.com/resourcehacker/).
כדי להגיע ל Section הזה נעבור ב DataDirectory בשם IMAGE_DIRECTORY_ENTRY_RESOURCE שמכיל RVA לפסקת המשאבים. המשאבים שלנו מאורגנים בצורה הדומה להיררכיה של מערכת קבצים – מעין היררכית תיקיות כך שבכל שלב, כלומר בכל תיקייה, יכולים להיות הן משאבים והן תיקיות נוספות שיכילו גם הן משאבים ותיקיות וכך הלאה עד שנגמר המקום. ה RVA ב IMAGE_DIRECTORY_ENTRY_RESOURCE מצביעה למבנה בשם IMAGE_RESOURCE_DIRECTORY הנראה כך:
המעניינים מבניהם הם NumberOfNamedEntries ו NumberOfIdEntires. לאחר מבנה זה יש לי רשימת מבנים מסוג IMAGE_RESOURCE_DIRECTORY_ENTRY. אורך הרשימה הוא הסכום של NumberOFNamedEntries פלוס NumberOfIdEntries. אחרי רשימה של IMAGE_RESOURCE_DIRECTORY_ENTRY-ים יש עוד IMAGE_RESOURCE_DIRECTORY ואחריו עוד רשימה וכך הלאה עד שנגמר השרשור.
נמחיש באמצעות תרשים:
כאן יש לנו מבנה ראשון מסוג IMAGE_RESOURCE_DIRECTORY שאומר שאחריו ברשימה יש שני רשומות של IMAGE_RESCOURCE_DIRECTORY_ENTRY שמיוצגות על פי שם ועוד רשומה אחת של IMAGE_RESOURCE_DIRECTORY_ENTRY שמיוצגת על פי מספר מזהה (ID). לאחר מכן עוד IMAGE_RESOURCE_DIRECTORY שאומר שבהמשך הרשימה יש עוד רשומה אחת של IMAGE_RESOURCE_DIRECTORY_ENTRY שמיוצגת על פי שם. הרשימה ממשיכה עד סוף השרשור.
הצעד הבא שלנו לקראת הבנת ה Section הזה הוא להבין טוב יותר את רשומות ה IMAGE_RESOURCE_DIRECTORY_ENTRY. כאמור כל IMAGE_RESOURCE_DIRECTORY אומר כמה Entry-ים יש אחריו, כמה מהם מיוצגים על פי שם וכמה על פי מספר סידורי. כל Entry מכיל הצבעה. הצבעה זו היא או ישירות למשאב (נגיד ל icon) או ל IMAGE_RESOURCE_DIRECTORY נוסף אחריו יהיו עוד Entry-ים כאלה שגם הם יוכלו להצביע או למשאב או ל IMAGE_RESOURCE_DIRECTORY נוסף. מזכיר מאוד עץ תיקיות שכל פריט יכול להצביע או לקובץ כלשהו או לתיקיה נוספת.. למזלנו Microsoft החליטה ליצור קונבנציה לאיך אמור להיראות עץ המשאבים הזה. ראשית יהיה מבנה של IMAGE_RESOURCE_DIRECTORY שיהיו בו Entries שיצביעו ל Directories נוספים ולא ישירות למשאבים. ההבדל בין כל Directory הוא ה Type של המשאבים שהוא מכיל, כלומר הסוג שלהם. כך לדוגמא הגיוני שיהיה לי Entry שיצביע ל Directory שמכיל רק משאבי DLL ו Directory אחרי שמכיל רק משאבי תמונות. זה השלב הראשון בהיררכיה – הפרדה על פי Type.
השלב השני מכיל רשומות שכל רשומה מצביע ל Directory כאשר ההפרדה כאן היא על פי שם. קצת מוזר אבל עוד שניה זה יהיה ברור למה שנעשה Directory עבור כל שם של משאב. אז השלב השני בהיררכיה – הפרדה על פי Name של משאב.
השלב השלישי והאחרון מכיל רשומות כאשר כל רשומה מצביעה לאותו משאב רק עבור שפה שונה. כלומר ההפרדה כאן היא על פי Language.
נסכם את זה באמצעות תרשים נחמד:
הערה: כנראה שכדאי שתקראו את ההסבר האחרון כמה פעמים. אחרי שעשיתם זאת תעברו לקיצור הבא.
נעבור בקצרה על מה שיש לנו כאן:
Directory Type אחד שאחריו יש רשימה של שלושה Entry-ים כאשר כל אחד מצביע על Directory Name. לאחר מכן יורדים רמה אחת יותר לעומק בהיררכיה – מיון לפי שם. לאחר מכן אותו התהליך רק שיורדים רמה אחת נוספת לעומק ההיררכיה – מיון על פי שפה. ואז בסוף יש הצבעות ישירות למשאבים עצמם.
נפתח PEView עם notepad.exe:
נביט ב resource section שם נמצא את ה Directory Type הראשון שאומר שיש אחריו רשימה עם שבע רשומות סך הכל – שלוש על פי שם וארבע על פי מספר מזהה. לאחר מכן אפשר לראות את שבעת הרשומות. כל רשומה מכילה offset ל directory. ה offset הזה הוא מתחיל ה section. לכן נבדוק לאן מוביל ה offset של icon (שווה ל 90, ה 8 מסמל שזה מצביע ל Directory ולא ל Resource). אז ה resource section מתחיל  ב RVA = 00022000, נוסיף לזה 90 ונגיע ל:
יש לנו כאן עוד Directory שמכיל 0 רשומות על פי שם ועוד 14 רשומות על פי מספר מזהה. נלך לשני לצורך העניין נראה שה offset שם הוא ל Directory (שוב, בגלל ה 8 ב MSB ולא בגלל שככה כתוב ב description שהרי זה משהו ש PEView מוסיף כדי שיהיה ברור יותר להבנה). ה offset הוא 1B0, נוסיף את זה ל 00022000 ונגיע ל:
ה Directory האחרון בהיררכיה. כאן מצוין שיש לנו רשומה אחת על פי מספר מזהה, נסתכל מיד אחרי ה Directory ונראה את הרשומה. היא מכילה offset ל ResourceMSB מוגדר ל – 0 ולא לא 8 כמו מקודם). ה offset הוא 00000358, נוסיף ל 00022000 ונגיע למבנה שמייצג ממש את המשאב:
כל משאב מיוצג על פי RESOURCE_DATA_ENTRY. המבנה הזה נראה כך:
השדות שמעניינים כאן הם ה-OffsetToData הוא ה RVA ליחידת המידע של ה Resource וה-Size מייצג את הגודל של יחידת המידע של ה Resource ב Byte-ים.

Load Configuration


מבנה שמכיל כמה פרטים שעשויים לעניין אותנו. נגיע אליו דרך DataDirectory בשם IMAGE_DIRECTORY_ENTRY. השדות בו שמעניינים:
1.     SecurityCookie  אם אתם יודעים מה זה GSCookie אז לכו למשפט האחרון בהסבר, אם לא אז תמשיכו למשפט הבא. סוג נפוץ של תקיפות זיכרון הוא Buffer Overflow או בקיצור BOF. מטרת התקיפה הזו היא לגלוש מגבולות תחום זיכרון מסוים כדי לדרוס מידע אחר. נראה דוגמה – אני בונה תכנית שקולטת מחרזות בת 5 תווים אך אין לי בקרה שבאמת נכנסו מקסימום 5 תווים. תוקף ינצל את זה ויכניס 10 תווים כך שהתווים 6-10 ידרסו מידע שנמצא בכתובות זיכרון סמוכות לאלו שהוקצו למחרוזת בת ה-5 תווים לכאורה. כך תוקף יכול לדרוס כתובות זיכרון. אחד המנגנוני הגנה שהמימוש שלהם עוד מתחיל ב PE (ובגלל זה אני חופר שעה בפסקה האחרונה), נקרא Security Cookie. הוא אומר בעצם שאם הקומפיילר מקבל את הדגל /GS הוא צריך לשים ערך כלשהו (נקרא לערך זה עוגייה) בין כתובות זיכרון של משתנים, אוגרים וכו'. ככה זה נראה:
בחלק העליון של הסרטוט אין לנו את ה security cookie ובחלק השני יש. כעת אם התוקף ידרוש ינסה לגלוש מה return address לדוגמה ל args הוא יאלץ לדרוס את הערך של העוגיה. כעת יש לנו אינדיקציה לזה שבוצע BOF ואפשר לטפל בזה. נחזור לעניינו - מה שהשדה מכיל זה את ה VA לעוגייה שמשומשת באימפלמנטציה של המנגנון.
2.     SEHandlerTable – לא נסביר מה זה SEH אז אם אתה לא יודע Google it. שדה זה מכיל VA לטבלה שלו.
3.     SEHandlerCount – אורך הטבלה מסעיף 2.

Code Signing

אזור זה ב PE מכיל את המיקום שבו ה certification של הקובץ נשמר. כלומר כאן תשמר ה Digital Signature של הקובץ. נגיע לשם דרך ה DataDirectory באינדקס 4 שנקרא IMAGE_DIRECTORY_ENTRY_SECURITY.

סיכום


מקווה שהמאמר עזר לכם להבין טוב יותר מהו פורמט ה Portable executable, ניסיתי לגעת בכמה יותר דברים אך עדיין לשמור את זה בגבולות הטעם הטוב. הידע שקיבלתם כאן יכול לעזור לכם לחקור קבצי PE בתהליך Forensics או כאשר אתם מרוורסים (עושים Reverse Engineering). בנוסף אם תכתבו פוגענים (מקווה שאם כן זה למטרה טובה בלבד) הידע שהועבר לכם יכול לתרום כדי להוסיף תחכום לפוגען (למשל קוד ב Callbacks של Thread Local Storage) ולהבין טוב יותר איך נראה הקובץ הסופי שלכם במידה והוא Portable Executable. זהו היה החלק הראשון במחקר שלי בקבצי PE. החלק הבא, כאמור בתחילת המאמר, יכלול כתיבת תכנית והסבר שלה לפירסור חלקים מה PE. מקווה שהמאמר עזר לכם, לכל שאלה או הערה אתם מוזמנים ליצור איתי קשר במייל Orih90@gmail.com.

בברכה, אורי חדד

תגובות

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