נרצה להבין כיצד ניתן להשיג מידע מהתהליך ההפוך כלומר לקחת את ה executable ולהתחיל לנתח את הקוד שלו. מעין די-קומפילצייה או במונחים מקצועיים יותר Disassembly ו reverse engineering.
נשתמש ב objdump
שזה בעצם CLI כדי להציג מידע על קבצי object במערכות הפעלה מבוססות UNIX. הוא משוייך לספריות העזר של GNU ולכן הוא מובנה בלינוקס, זאת הסיבה שגם נשתמש בו.
על ידי שימוש בפקודה
objdump-d a.out
נקבל קוד שדומה לאסמבלי שמייצג את הקוד שמופיע בקבצי
פקודה נוספת שיכולה להיות שימושית היא
objdump -t a.out
שתדפיס לנו את כל הsymbol table של הקובץ. הטבלה תכיל את כל הmetadata שקשורות לשמות המשתנים , הכתובות והשמות של המשתנים הגלובליים. הפקודה הזאת תביא לנו את כל ה labels בתוכנית.
הפקודה strings a.out
ידפיס את כל הסטרינגים בתוכנית.
כמה נקודות חשובות
a) בתהליך הדיסאסמבלי תמיד נקבל רק את הקוד של הפונקציות שנעשה בהם שימוש באותו קובץ.
b) לא נראה מימושים לפונקציות מערכת כמו printf ..
c) לא נראה ערכים של משתנים גלובלים או של סטרינגים.
d) הרבה פעמים יש אופטימיזציות או nops
שהקומפיילר מוסיף שלעתים מקשות על ההבנה. שתי דוגמאות למשל
a) nop
b) xchg %cx, %cx
nop- פקודה שלא עושה כלום no operation . יש לזה שימוש בהקשרים של תזמון וניהול זכרון או סנכרון של ה cpu .
נתחיל לנתח מהפונקצייה main (לעתים נראה פונקציות גלובליות נוספות כמו init ו start אבל נוכל להתעלם מהם).
ניתן לראות שתי דברים חשובים.
א) משמאל לפקודה יש את ערך הבייט שמייצג את הפקודה עצמה למשל push %rbp
מיוצג על ידי בייט אחד שערכו
ישנם תבניות שחוזרות על עצמן שקל לשים לב אליהן למשל הבייט שערכו mov to eax
. השורה החמישית הפקודה אומרת להזיז את הint (כי ארבעה בתים) שמיוצג על ידי
ל edi שמיוצג על ידי בייט אחד שערכו
כמו כן נשים לב שניתן לראות מהייצוג שמדובר במערכת Little Endian
(Little and Big Endian) .
ב) בעמודה הכי שמאלית ניתן לראות שמדובר בכתובות של השורות קוד עצמן ב text segment . והקפיצות הן לפי כמות הבייטים שהפקודה עצמה דורשת.
ג) נשים לב לקריאות callq
היא מראה את ה label של הפונקצייה שלה קוראים ואת הכתובת שלה בtext segment. נוכל ממש לחפש את הכתובת ולהגיע לקוד של hello .
כאן נשים לב שיש שתי דברים מעניינים. ראשית הפקודה
mov $0x4006bd, %edi
על פניו נראה טריוויאלי אבל אם נחפש את הכתובת הזאת אנחנו לא נמצא בניגוד לכתובת של hello שכן מצאנו.
מיד לאחר מכן אנחנו קוראים לכתובת puts
.
אם נראה מה היא עושה אנחנו רואים שהיא פשוט מדפיסה סטרינג. כמו כן אנחנו יודעים מ Functions in assembly ש puts
נראה ש
//The `puts()` function writes the given string to the standard output stream `stdout`; it also appends a new-line character to the output. The ending null character is not written.
int puts(const char *string)
אם כן אנחנו מבינים שהשורה מעל ה call מייצגת את הכתובת לסטרינג הזה. כפי שאמרנו אין לנו גישה לערכים של סטרינגים. כאן נכנס לתמונה gdb שיאפשר לנו להדפיס בזמן ריצה ערכים של כתובות.
חשוב לציין שגם ל puts אין לנו גישה באמת בתהליך הדיסאסמבלי בגלל שזאת פונקציית ספרייה
כעת נחזור בחזרה ל main של אחרי הקריאה ל hello.
ישנה קריאה נוספת:
callq 4005cd <even>
הבלוק המפחיד של הקוד הזה הוא הבלוק הבא
נשים לב שלפני כן יש את התהליך ההתחלתי הסטנדרטי להזזת המצביעים לראש המחסנית , מיד לאחר מכן אנחנו רואים שיש הקצאה של
נשים לב שבסוף התוכנית לפני ה״בלוק המוזר״ יש הכנסה של 0xa
ל edi כלומר מתכוננים לקריאת פונקצייה ואז
callq 400480 <putchar@plt>
נשים לב שה
כעת נתחיל להסתכל על ה״בלוק הבעייתי״ מלמעלה.
בשורה השלישית יש קפיצה לשורה מסויימת בתוך even מה שמרמז לנו על if else או לולאה בקוד המקורי. אם נלך לכתובת שאליה קופצים אנחנו נגיע לקוד הבא
cmpl $0x9, -0x4(%rbp)
jle 4005e1 <even+0x14>
כלומר הקפיצה הזאת היא לפקודה cmpl שאנחנו יודעים מ control flow in assembly שמדובר בהשוואה בין שתי ערכים, ניתן לראות שמדובר בהשוואה למספר 9 עם הערך שנמצא 4 בייטים מתחת לכתובת שעליה נמצא כעת rbp. למה מתחת? כי לפי Program structure in assembly אנחנו יודעים שהמחסנית מתחילה מכתובות גבוהות ויורדת בערכיה לאט לאט. כלומר מדובר באיזה ערך ששמרנו במחסנית בסקופ של הפונקצייה עצמה . סך הכל מה שקורה פה בקוד קריא:
arg1 = Mem[-0x14(%rbp)]; //this is an input value that is saved in stack
value = 0;
while (value <= 9) {
print("the number is %d" , arg1+value);
value += 2;
}
הסיבה שזה לולאה כלשהו היא בגלל השורה שמתחת להשוואה שמקפיצה אותנו בחזרה למעלה לשורה הראשונה מתחת ל jmp שמוביל לבדיקה.
מכאן אפשר להתחיל לנתח את המחסנית עבור הקוד הזה ולהבין איך מגיעים לקוד c הנ״ל.
לבסוף חוזרים ל main ומסיימים את התוכנית.
סך הכל הקוד המלא ייראה ככה (טיפה שונה מהניתוח שעשינו אבל זה הקטע בדיסאסמבלי שלא נגיע למימוש המדוייק ברוב המקרים אלא רק להבנה של מה הקוד עושה, רק עם שימוש ב gdb אפשר גם להבין מהם הערכים ששולחים כל הזמן).
void hello() {
printf("Hello there!\n");
}
void even(int a) {
int i;
for(i=0; i<10; i+=2) {
printf("%d", (a+i));
}
printf("\n")
}
נשים לב שאומנם קראנו ל printf
בקוד המקורי אבל באופטימיזציות קראנו ל puts
שהקומפיילר החליט שזה יותר יעיל.