_ .-') _     ('-.   ('-.     _ .-') _        .-. .-')               .-') _     ('-.    .-')
( (  OO) )  _(  OO) ( OO ).-.( (  OO) )       \  ( OO )             (  OO) )  _(  OO)  ( OO ).
 \     .'_ (,------./ . --. / \     .'_        ;-----.\  ,--.   ,--./     '._(,------.(_)---\_)
 ,`'--..._) |  .---'| \-.  \  ,`'--..._)       | .-.  |   \  `.'  / |'--...__)|  .---'/    _ |
 |  |  \  ' |  |  .-'-'  |  | |  |  \  '       | '-' /_).-')     /  '--.  .--'|  |    \  :` `.
 |  |   ' |(|  '--.\| |_.'  | |  |   ' |       | .-. `.(OO  \   /      |  |  (|  '--.  '..`''.)
 |  |   / : |  .--' |  .-.  | |  |   / :       | |  \  ||   /  /\_     |  |   |  .--' .-._)   \
 |  '--'  / |  `---.|  | |  | |  '--'  /       | '--'  /`-./  /.__)    |  |   |  `---.\       /
 `-------'  `------'`--' `--' `-------'        `------'   `--'         `--'   `------' `-----'
                                                                                  ~ xcellerator
[ תורגם ע"י eternaleclipse ]

שלומות, עמיתיי חובבי ה-ELF! במאמר זה, אני רוצה להציג ספריה קטנה שעבדתי עליה בשם LibGolf. היא התחילה
פשוט ככלי להבנה יותר טובה של ה-ELF וה-program headers, אך מאז הפכה למשהו שימושי למדי. היא מקלה מאוד
על יצירת בינארי שמורכב מ-ELF header, שלאחריו program header יחיד, ולאחריו סגמנט יחיד שניתן לטעינה.
כברירת מחדל, כל השדות ב-headerים נקבעים לערכים הגיוניים, אבל יש דרך פשוטה לשחק עם הערכים האלה -
ובזה בדיוק עוסק המאמר הזה! אני עומד להדגים איך השתמשתי ב-LibGolf כדי למנות בדיוק איזה בייטים נחוצים
ומאיזה מתעלם ה-loader של לינוקס. למרבה המזל, נראה שה-loader הוא אחד מה-parserים הכי פחות בררניים
באוסף הכלים הסטנדרטי של לינוקס. לפני שנסיים, נראה מספר כלים פופולריים לניתוח סטטי מתפוררים מלפני
ה-ELF שהשחתנו, בזמן שה-loader ממשיך לטעון ולקפוץ בעליצות לבייטים שבחרנו.

+----------------------+
|--[ מבוא ל-LibGolf ]--|
+----------------------+

לפני כמה זמן, שיחקתי עם כתיבת ELFים ידנית ב-NASM. כמה שזה היה כיף לזמן מה (ובהחלט היו לזה את
היתרונות שלו), הבנתי שאני מפספס את כל הכיף שיש ל-structים ב-C להציע. בפרט, כפי שאני בטוח שקוראים
וקוראות רבים ללא ספק יודעים, <linux/elf.h>, מפוצץ בדברים כיפיים כמו `Elf64_Ehdr` ו`Elf32_Phdr`
בשלים להצהרה.

ברצוני לא לתת ל-headerים כל כך שימושיים להזרק לפח, בחרתי לקחת אותם, ולעשות בהם שימוש טוב. מהמאמצים
האלו, הגיעה libgolf.h, ספריה שמאפשרת בקלות לזרוק shellcode לתוך executable מתפקד. אני יודע מה אתם
חושבים - "זה נשמע כמו linker נוראי!", ואתם אולי צודקים. למרות זאת, מה שנחמד פה זה שאתם יכולים לשנות
את ה-headers בקלות *לפני* שהבינארי נבנה.

בואו נראה איך זה עובד. אם תרצו לעקוב בבית, תוכלו למצוא את קוד המקור לכל זה ב[0]. תוכלו למצוא את
הקוד שבמאמר הזה תחת 'examples/01_dead_bytes'. ההתקנה הבסיסית דורשת שני קבצים; קובץ קוד מקור ב-C,
ו-shellcode.h. לגבי ה-shellcode, אני אוהב להשתמש ב-'b0 3c 48 31 ff 0f 05' הישן והנאמן, שמתורגם ל:
        mov al, 0x3c    @ b0 3c
        xor rdi, rdi    @ 48 31 ff
        syscall         @ 0f 05
(כן - לקרוא לזה "shellcode" זו קצת הגזמה!)

הקוד הזה פשוט קורא ל-exit(0). זה נחמד כי נוכל בקלות לבדוק שהבייטים האלה רצו בהצלחה בעזרת ה-shell
expansion $?.

זרקו את זה או איזשהו shellcode אחר (אבל ודאו שהוא PIC - אין תמיכה ב-relocatable symbols עדיין!)
לתוך buffer בשם buf[] ב-shellcode.h וקפצו חזרה לקובץ ה-C. אם רק רציתם לקבל בינארי שמריץ את
ה-shellcode שלכם, אז זה כל מה שאתם צריכים:
        #include "libgolf.h"
        #include "shellcode.h"

        int main(int argc, char **argv)
        {
            INIT_ELF(X86_64,64);

            GEN_ELF();
            return 0;
        }
קימפול של זה והרצה של הבינארי שהתקבל יספקו לכם קובץ .bin - זהו ה-ELF החדש והנוצץ שלכם! די פשוט,
נכון? פשטות לעיתים מלווה עם שיעמום, כפי שזה המקרה גם כאן, אז בואו נעשה משהו יותר מעניין!

לפני שנמשיך, כדאי להסביר מה שני ה-macroים האלה עושים מאחורי הקלעים. קודם כל, INIT_ELF() מקבל שני
ארגומנטים, ה-ISA והארכטיקטורה. כרגע, LibGolf תומכת ב-x86_64, ARM, ו-AARCH64 כ-ISAים תקינים, ועבור
הארכיטקטורה 32 או 64. היא קודם מקימה כמה structs שמשמשים למעקב פנימי, ומחליטה האם להשתמש באובייקטי
Elf32_* או Elf64_* בשביל ה-headers. היא גם אוטומטית מייצרת מצביעים ל-ELF headers ו-program headers,
שנקראים ehdr ו-phdr בהתאמה. באלו בדיוק נשתמש כדי לשנות בקלות את השדות. מלבד זאת, היא גם מעתיקה את
buffer ה-shellcode, ומאכלסת את ה-ELF headers ו-program headers לפני חישוב נקודת כניסה שפויה. כעת
מגיע GEN_ELF() שפשוט מדפיס קצת נתונים נחמדים ל-stdout ואז כותב את ה-structים הנדרשים לקובץ ה-.bin.
שם ה-.bin נקבע על ידי argv[0].

אז, אחרי שהשתמשנו ב-macro ה-INIT_ELF(), יש לנו ehdr ו-phdr מוכנים לשימוש. נניח שהיינו רוצים לשנות את
שדה ה-e_version ב-ELF header. כל מה שנצטרך לעשות זה להוסיף שורה אחת:
        #include "libgolf.h"
        #include "shellcode.h"

        int main(int argc, char **argv)
        {
            INIT_ELF(X86_64);

            // Set e_version to 12345678
            ehdr->e_version = 0x78563412;

            GEN_ELF();
            return 0;
        }
עוד קימפול מהיר והרצה, ויהיה לכם עוד קובץ .bin שמחכה לכם. אם תעיפו מבט על הקובץ ב-xxd, hexyl, או
עורך הבינארים האהוב עליכם, תראו '12 34 56 78' יפה שמביט בכם בחזרה ומתחיל במיקום 0x14. נכון זה היה
קל?

כדי לגרום לדברים לזוז יותר מהר, אני אוהב להשתמש ב-Makefile הבא:
        .PHONY golf clean

        CC=gcc
        CFLAGS=-I.
        PROG=golf

        golf:
        	@$(CC) -o $(PROG) $(PROG).c
        	@./$(PROG)
        	@chmod +x $(PROG).bin

        	@rm $(PROG) $(PROG).bin
(זה ה-Makefile שתמצאו ב-repo ‏[0]‏)

+---------------------------+
|--[ ליפול במכשול הראשון ]--|
+---------------------------+

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

מקום טוב להתחיל ממנו הוא ההתחלה, כלומר ה-ELF header. בהתחלה של כל קובץ ELF נמצא כמובן, 0x7f המוכר
שאחריו ELF, שידוע לחבריו כ-EI_MAG0 עד EI_MAG3. באופן לא מפתיע, שינוי של כל אחד מארבעת הבייטים האלה
גורם ל-loader של לינוקס לסרב לטעון את הקובץ. תודה לאל על כך!

מה לגבי בייט 0x5? התקן הנאמן שלנו אומר לנו שזהו בייט ה-EI_CLASS והוא קובע את את ארכטיקטורת היעד.
ערכים תקינים הם 0x01 ו-0x02, עבור 32- ו-64-ביט בהתאמה. אני אגיד שוב: ערכים תקינים הם 0x01 או 0x02.
מה אם נשים בו 0x58 (או "X" למי שמעדיפים ASCII)? ניתן לעשות זאת ע"י הוספת:
(ehdr->e_ident)[EI_CLASS] = 0x58
לקובץ ה-C המג'נרט שלנו. (למה 0x58? כי זה מופיע בבירור בפלט של xxd/hexyl!)

כאשר יש לנו את קובץ ה-.bin שלנו לשחק איתו, לפני שננסה להריץ אותו, בואו ננסה כמה כלים מוכרים אחרים
לפירסור ELF בחיפוש אחר עבריינים נוספים. הראשון ברשימה הוא gdb. תמשיכו, אני מחכה פה בינתיים. ראיתם
מה קרה?
"not in executable format: file format not recognized"
באופן דומה, objdump יתן לכם תשובה דומה. נראה שה-parserים האלה עושים את עבודתם נכון. כעת, בואו ננסה
להריץ את הבינארי רגיל.

<ספוילר>הוא עובד באופן מושלם.<\ספוילר>

אם השתמשתם ב-shellcode מהדוגמה שלי, אז התייעצות עם $? תודיע לכם למצער כי הבינארי סיים לרוץ וביצע
יציאה בהצלחה. אותם פשעים מבוצעים גם כאשר מציבים ערכים לא תקינים ב-EI_DATA ו-EI_VERSION.

+---------------------+
|--[ משחיתים ברבאק ]--|
+---------------------+

אז, כמה רחוק אנחנו יכולים ללכת עם זה? מתוך כמה ELF headers ו-program headers יתעלם ה-loader של
לינוקס? כבר כיסינו את EI_CLASS, EI_DATA ו-EI_VERSION, אבל נראה שמ-EI_OSABI גם מתעלמים בבטחה. זה
מביא אותנו למיקום 0x8. לפי התקן, הבאים בתור הם EI_ABIVERSION ו-EI_PAD, שביחד, לוקחים אותנו כל הדרך
עד לבייט 0xf. נראה שלאף אחד לא אכפת מהם, אז אנחנו יכולים לשים בהם 0x58 ללא פחד.

נמשיך לצעוד קדימה, ואנחנו מגיעים לשדה שמראה התנגדות לשינויים: e_type. באופן מובן, ה-loader של
לינוקס לא אוהב אם אנחנו לא אומרים לו איזה סוג קובץ ELF אנחנו מספקים לו (נחמד לדעת שיש לו *איזשהם*
סטנדרטים! - משחק מילים מכוון). אנחנו צריכים ששני הבייטים האלה ישארו 0x0002 (או ET_EXEC בשפת הקבועים
ב-elf.h).  הבא בתור הוא עוד בייט בררן, במיקום 0x12 הכה מוכר: e_machine, שקובע את ה-ISA של היעד. עד
כמה שזה נוגע אלינו, על ידי ציון X86_64 כארגומנט הראשון ל-INIT_ELF(),  הבייט הזה כבר אוכלס עבורנו
בערך 0x3f על ידי LibGolf.

לפתע, הופיע e_version פראי! אנחנו ניצבים בפני עוד פורע חוק, שלכאורה תמיד אמור להיות 0x00000001.
למרות זאת, נראה שבפועל זה לא מעניין אף אחד, אז בואו נמלא אותו ב-0x58585858 במקום.

בהמשך לשרשרת הכפירה הזאת, יש לנו מספר שדות חשובים שנראים חסינים לניצול; e_entry ו-e_phoff. אני בטוח
שאני לא צריך לפרט יותר מדי לגבי e_entry; זוהי נקודת הכניסה של הבינארי, שאליה הריצה בסופו של דבר
מועברת לאחר טעינת ה-sections לזכרון. למרות שאולי היה אפשר להניח שה-loader ידע להסתדר בלי לדעת את
המיקום של ה-program header, נראה שהוא לא חכם מספיק כדי להבין אותו בעצמו בלי שיאכילו אותו בכפית.
עדיף שנשאיר את שני השדות האלה לבד.

LibGolf עדיין לא תומכת ב-section headerים (ובהנתן הפוקוס שלה על יצירת בינארים *קטנים*, היא ככל
הנראה לא תתמוך בהם בעתיד). זה אומר, שכאשר היא נתקלת בכל header שקשור אליהם, אנחנו יכולים לשחק בהם
כאוות נפשנו. זה כולל את e_shoff, e_shentsize, eh_shnum ואפילו e_shstrndx. אם אין לנו שום section
headerים, אי אפשר להאשים אותנו בהשחתה שלהם!

שאר השדות שנראה שהם בעלי חשיבות כלשהי ל-loader של לינוקס הם e_ehsize, e_phentsize ו-e_phnum. שוב,
זה לא מאוד מפתיע, בהתחשב בכך שהם קשורים באופן ישיר לטעינת הסגמנט היחיד לזיכרון לפני שמועברת השליטה
לתוכנית. אם אתם צריכים רענון, e_ehsize הוא הגודל של ה-ELF header (שהוא 0x34 או 0x40 ל 32- ו-64 ביט
בהתאמה), eh_phentsize הוא הגודל של ה-program header שמגיע בהמשך (שוב, מקודד מראש ל-0x20 או 0x32
עבור ארכיטקטורות 32- או 64-ביט). אם ה-loader היה קצת יותר בררן לגבי EI_CLASS, הוא לא היה צריך את
שני השדות האלה. לבסוף, e_phnum הוא פשוט מספר הרשומות ב-program header - עבורנו זה תמיד 0x1. ללא
ספק, מתבצע בו שימוש באיזושהי לולאה ברוטינות הטעינה לזכרון, אבל לא חקרתי יותר לעומק עדיין.

יש עדיין שדה אחד שנשאר ב-ELF header שלא נגעתי בו, והוא e_flags. הסיבה היא פשוטה למדי, הוא תלוי
ארכיטקטורה. ל-x86_64, הוא לא משנה כלל כי הוא לא מוגדר (למרות שהוא *כן* חשוב בפלטפורמות ARM מסוימות!
הביטו בדוגמה של arm32 ב-[0]).

זה מביא אותנו לסוף ה-ELF header. למי שלא סופר, ה-loader מתעלם מקצת יותר מ-50% מה-ELF header. אבל מה
לגבי ה-program header? נראה שב-program header-ים יש הרבה פחות מרווח תמרון, אבל לא מהסיבות שהייתם
עשויים לצפות. אמנם נכון הוא, *כל* השחתה של ה-program header לא משפיעה בפועל על ה-loader של לינוקס.
אך היזהר, הרפתקן נועז, שחק עם הבייט הלא נכון ותיזרק למרתף ה-faulty segmentation!

אז, האם יש בכלל משהו שניתן לסחוט באיומים ב-program header? מתקיימים שני שדות, שללא אשמתם, פשוט אינם
רלוונטיים יותר: p_paddr ו-p_palign. הראשון היה חשוב בימים העליזים לפני זיכרון וירטואלי, בהם 4GB של
RAM לא היו יותר מחלומות בהקיץ ולכן היה חשוב ליידע את ה-loader איפה בזכרון הפיזי הסגמנט צריך להטען.

Alignment לזכרון זה אחד מצחיק. כביכול, p_vaddr אמור להיות שווה ל-p_offset מודולו p_align. נראה
שקבצי ELF "תקניים" (לפחות אלו שמקומפלים על ידי GCC) פשוט קובעים את p_offset שיהיה שווה ל-p_align
וממשיכים הלאה. זה גם מה ש-LibGolf עושה בברירת מחדל וזה הופך את p_align ללגמרי מיותר!

בסך הכל, לא כיף כמו ה-ELF header, אבל עדיין הצלחות קטנות. קוד ה-C שמג'נרט בינארי נראה עכשיו כך:
        #include "libgolf.h"
        #include "shellcode.h"

        int main(int argc, char **argv)
        {
            INIT_ELF(X86_64,64);

            /*
             * שובר כלים נפוצים לניתוח סטטי כמו gdb ו-objdump
             */
            (ehdr->e_ident)[EI_CLASS] = 0x58;   // ארכיטקטורה
            (ehdr->e_ident)[EI_DATA] = 0x58;    // סדר הבייטים
            (ehdr->e_ident)[EI_VERSION] = 0x58; // נראה שתמיד 0x01
            (ehdr->e_ident)[EI_OSABI] = 0x58;   // מערכת ההפעלה של היעד

            // Loop over the rest of e_ident
            int i;
            for ( i = 0 ; i < 0x10 ; i++ )
                (ehdr->e_ident)[i] = 0x58;

            ehdr->e_version = 0x58585858;       // נראה שתמיד 0x00000001

            // Section headers? אנחנו לא צריכים שום section headers מסריחים!
            ehdr->e_shoff = 0x5858585858585858;
            ehdr->e_shentsize = 0x5858;
            ehdr->e_shnum = 0x5858;
            ehdr->e_shstrndx = 0x5858;

            ehdr->e_flags = 0x58585858;         // ל-x86_64 אין דגלים מוגדרים

            phdr->p_paddr = 0x5858585858585858; // מתעלמים מהכתובת הפיזית
            phdr->p_align = 0x5858585858585858; // p_vaddr = p_offset, אז לא רלוונטי

            GEN_ELF();
            return 0;
        }
אם נקמפל ונריץ את התוכנית הזו, נקבל את הבינארי הבא:
        00000000: 7f45 4c46 5858 5858 5858 5858 5858 5858  .ELFXXXXXXXXXXXX
        00000010: 0200 3e00 5858 5858 7800 4000 0000 0000  ..>.XXXXx.@.....
        00000020: 4000 0000 0000 0000 5858 5858 5858 5858  @.......XXXXXXXX
        00000030: 5858 5858 4000 3800 0100 5858 5858 5858  XXXX@.8...XXXXXX
        00000040: 0100 0000 0500 0000 0000 0000 0000 0000  ................
        00000050: 0000 4000 0000 0000 5858 5858 5858 5858  ..@.....XXXXXXXX
        00000060: 0700 0000 0000 0000 0700 0000 0000 0000  ................
        00000070: 5858 5858 5858 5858 b03c 4831 ff0f 05    XXXXXXXX.<H1...
הקובץ הזה הוא 127 בייטים בגודלו, אבל הצלחנו להחליף מתוכו סך של 50 בייטים ב-'X', כלומר ה-loader של
לינוקס מתעלם מכמעט 40% מהבינארי הזה! מי יודע מה נוכל לעשות ב-50 בייטים?

מסתבר - שדי הרבה. מחקר מלפני מספר שנים של netspooky הדגים איך ניתן לערום חלקים של ה-program header
לתוך ה-ELF header. בשילוב אחסון ה-shellcode שלכם בתוך אחד מאותם שטחים של בייטים מתים, וכמה טריקים
יפים נוספים, ניתן למזער ELF עד ל-84 בייטים - ירידה של 34% ביחס למאמץ הטוב ביותר ש-LibGolf יודע לתת
כרגע. אכוון אתכם לסדרת ה-"ELF Mangling" המדהימה שלו ב-[1].

יש עוד אספקט מעניין של הטכניקות האלה שקל מאוד לפסוח עליו. למרות שנראה של-loader של לינוקס אכפת מעט
מאוד מהמבנה של ELF מעבר למה שהוא צריך כדי להגיע לקוד המכונה, כלים אחרים הם הרבה יותר בררניים. כבר
הסתכלנו על objdump ו-gdb, אבל הרבה פתרונות AV גם מתפוררים כאשר הם ניצבים בפנים ELF מושחת. במחקר
שלי, המוצר היחיד ש(בערך) עושה את זה נכון הוא ClamAV, עם תוצאה חיובית עבור
"Heuristics.Broken.Executable". כמובן, בנוגע לניתוח דינמי אין לדעת.

+---------------------+
|--[ ממשיכים קדימה ]--|
+---------------------+

x86_64 הוא לא ה-ISA היחיד שנתמך ע"י LibGolf! ניתן להשתמש בה גם כדי ליצור executables קטנים
לפלטפורמות ARM32 ו-AARCH64. ב-repo ב-GitHub [0], תמצאו דוגמאות עבור שתי הארכיטקטורות של ARM (כולל
הדוגמה עם הבייטים המתים מהמאמר הזה).

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

+---------------------------------+
|--[ הערה בנוגע לשחזור התוצאות ]--|
+---------------------------------+

לאורך המחקר הזה, בדקתי בעיקר על Ubuntu 20.04 עם קרנל 5.4.0-65-generic, אבל גם וידאתי שניתן להשיג את
אותן התוצאות על 5.11.11-arch1-1. שמעתי שדברים מוזרים יכולים לקרות לפעמים בקרנלים של WSL, אבל לא
חקרתי אותם - אולי אתם יכולים!

+-------------+
|--[ תודות ]--|
+-------------+

"אהוי" מיוחד לכולם ב-Thugcrowd, Symbolcrash וקבוצת התמיכה הנפשית ל-ELF!

+---------------+
|--[ אזכורים ]--|
+---------------+
[0] https://www.github.com/xcellerator/libgolf
[1] https://n0.lol/ebm/1.html