לדלג לתוכן

7.1 הקדמה להנדסה הפוכה הרצאה

הקדמה

לאורך הקורס הזה למדנו לבנות תוכנות מלמטה למעלה - מאסמבלי 16 ביט (פרק 1), דרך Protected Mode ו-Paging (פרק 2), שפת C (פרק 3), ספריית libc (פרק 4), מערכת ההפעלה לינוקס (פרק 5), ועד הקרנל עצמו (פרק 6).

עכשיו נהפוך את הכיוון. במקום לבנות תוכנה - נלמד לפרק אותה. זה התחום שנקרא הנדסה הפוכה - reverse engineering.


מה זו הנדסה הפוכה - reverse engineering?

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

כשמישהו כותב תוכנית ב-C ומקמפל אותה, התוצאה היא קובץ ELF (כזכור מפרק 5.11) שמכיל הוראות מכונה. הקוד המקורי נעלם - אין שמות משתנים, אין הערות, אין מבנה של שורות C. כל מה שנשאר הוא רצף של בתים שהמעבד יודע להריץ.

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


למה ללמוד הנדסה הפוכה?

יש כמה סיבות טובות מאוד:

ניתוח נוזקות - malware analysis
כשמוצאים תוכנה זדונית, אין לך את קוד המקור שלה. כדי להבין מה היא עושה (גונבת סיסמאות? מצפינה קבצים? מתקשרת עם שרת שליטה?) - צריך לפרק אותה ולנתח את הקוד שלה.

מחקר חולשות - vulnerability research
חוקרי אבטחה מנתחים תוכנות כדי למצוא בהן חולשות (באגים שניתן לנצל). הם קוראים את האסמבלי, מחפשים קריאות מסוכנות (כמו strcpy בלי בדיקת גבולות), ובודקים אם אפשר לגרום לתוכנית לעשות משהו שהמפתח לא התכוון אליו.

הבנת תוכנה סגורה - closed-source software
לפעמים צריך להבין איך תוכנה עובדת בלי שיש לך את הקוד שלה - למשל כדי לכתוב דרייבר תואם, או לתקן באג בתוכנה שהחברה שכתבה אותה כבר לא קיימת.

תחרויות CTF
תחרויות Capture The Flag כוללות אתגרים שבהם מקבלים קובץ בינארי וצריך למצוא "דגל" (מחרוזת סודית) שמוסתר בתוכו. זה דרך מעולה לתרגל RE.

הבנה של קומפיילרים
כשאתה יודע לקרוא את הפלט של הקומפיילר, אתה מבין טוב יותר מה הוא עושה. זה הופך אותך למתכנת טוב יותר - אתה מבין מה קורה "מתחת למכסה המנוע".

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


היבטים משפטיים

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


חשיבת RE - מה צריך לדעת?

הנדסה הפוכה היא לא תחום בפני עצמו - היא מבוססת על כל מה שלמדתם עד כה:

ידע באסמבלי (פרקים 1 ו-2)
כשפותחים בינארי ב-disassembler, רואים אסמבלי. בלי להבין את שפת האסמבלי, אי אפשר להתקדם. למדנו אסמבלי 16 ביט בפרק 1, ואסמבלי 32 ביט בפרק 2. בפרק הזה נתמקד ב-x86-64 (אסמבלי 64 ביט) כי זו הארכיטקטורה הנפוצה ביותר היום.

ידע בשפת C (פרק 3)
רוב התוכנות שנפרק נכתבו ב-C (או בשפה דומה). כשאנחנו קוראים אסמבלי, אנחנו מנסים לשחזר את מבני ה-C המקוריים - if, for, while, structs, pointers, function calls.

הבנת פורמט ELF ומערכת ההפעלה (פרקים 5 ו-6)
צריך להבין איך קובץ הרצה בנוי (סקשנים, סגמנטים, טבלת סמלים), איך הוא נטען לזיכרון, ומה המערכת ההפעלה עושה כשמריצים אותו.

אם אתם מרגישים חלודים באחד מהנושאים האלה - עכשיו הזמן לחזור ולרענן.


סוגי ניתוח - analysis types

יש שתי גישות בסיסיות לניתוח בינארי:

ניתוח סטטי - static analysis

בניתוח סטטי בוחנים את הבינארי בלי להריץ אותו. פותחים את הקובץ בכלי דיסאסמבלי או דה-קומפיילר וקוראים את הקוד.

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

חסרונות:
- קשה להבין קוד שמשתמש ב-obfuscation (ערפול קוד)
- לא רואים ערכים בפועל של משתנים
- קוד self-modifying (שמשנה את עצמו בזמן ריצה) בלתי אפשרי לנתח סטטית

ניתוח דינמי - dynamic analysis

בניתוח דינמי בוחנים את הבינארי תוך כדי שהוא רץ. משתמשים בדיבאגר, עוקבים אחרי קריאות מערכת, בודקים ערכי זיכרון.

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

חסרונות:
- מסוכן אם מדובר בנוזקה (צריך סביבה מבודדת)
- רואים רק את הpath שנלקח בהרצה הנוכחית
- תוכנות מתקדמות מזהות שהן רצות בתוך דיבאגר ומשנות התנהגות

שילוב של שניהם

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


ארגז הכלים - the RE toolbox

בואו נכיר את הכלים שנשתמש בהם לאורך הפרק:

כלים שכבר מכירים

כלי objdump - דיסאסמבלי בסיסי
כבר השתמשנו בו בפרק 5.11 כדי לראות את האסמבלי של קבצי ELF. הוא פשוט ויעיל לצפייה מהירה בקוד:

objdump -d -M intel ./binary

הדגל -M intel אומר להשתמש בתחביר Intel (שהוא הרבה יותר קריא מתחביר AT&T).

כלי GDB - דיבאגר
למדנו את הבסיס בפרק 3.9. בפרק 7.3 נלמד להשתמש בו ברמה מתקדמת הרבה יותר - דיבוג בלי קוד מקור, ניתוח פונקציות לא מוכרות, שינוי זרימת ריצה, וסקריפטים.

כלים חדשים

כלי Ghidra - דה-קומפיילר
Ghidra הוא כלי חינמי שפותח על ידי ה-NSA (סוכנות הביטחון הלאומי של ארצות הברית) ושוחרר כקוד פתוח. הוא עושה משהו מדהים - לוקח קוד אסמבלי ומנסה לשחזר ממנו קוד C. התוצאה לא זהה לקוד המקורי, אבל היא הרבה יותר קריאה מאסמבלי גולמי.

כלי radare2/rizin - מסגרת RE מבוססת שורת פקודה
כלי חזק מאוד שרץ מהטרמינל. מאפשר דיסאסמבלי, ניתוח גרפים, חיפוש דפוסים, ועוד. יש לו עקומת למידה תלולה אבל הוא מאוד גמיש.

כלי strace - מעקב אחרי קריאות מערכת
מציג את כל הsyscall-ים שתוכנית מבצעת בזמן ריצה. זה כלי דינמי שעוזר להבין מה תוכנית עושה בלי לקרוא את הקוד שלה:

strace ./binary

כלי ltrace - מעקב אחרי קריאות לספריות
דומה ל-strace, אבל מציג קריאות לפונקציות של ספריות משותפות (כמו printf, malloc, strcmp) במקום syscall-ים:

ltrace ./binary

פקודת strings - חילוץ מחרוזות
מחלצת את כל המחרוזות הקריאות (printable) מתוך קובץ בינארי. זה לעתים קרובות הצעד הראשון בניתוח - מחרוזות יכולות לחשוף הודעות שגיאה, כתובות URL, שמות קבצים, ורמזים על מה שהתוכנית עושה:

strings ./binary

פקודת file - זיהוי סוג קובץ
מזהה את סוג הקובץ - האם זה ELF, PE (Windows), סקריפט, תמונה, וכו'. גם מראה אם הקובץ הוא 32 או 64 ביט, האם הוא stripped (ללא סמלים), וכו':

file ./binary


דמו מהיר - ניתוח תוכנית ללא קוד מקור

בואו נדמה מצב אמיתי: מישהו נתן לנו קובץ הרצה ואנחנו לא יודעים מה הוא עושה. נשתמש בכלים שלנו כדי להבין.

נניח שיש לנו את הקובץ mystery. נתחיל:

שלב 1: מה סוג הקובץ?

file mystery

mystery: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
for GNU/Linux 3.2.0, not stripped

למדנו שזה קובץ ELF של 64 ביט, מקושר דינמית, ולא עבר strip - כלומר יש לנו סמלים (שמות פונקציות ומשתנים).

שלב 2: אילו מחרוזות יש בתוכו?

strings mystery

...
Enter password:
Access granted!
Access denied.
s3cr3t_p4ss
...

מעניין. נראה שזו תוכנית שמבקשת סיסמה ובודקת אותה. ואנחנו אפילו יכולים לראות מה הסיסמה - s3cr3t_p4ss.

שלב 3: אילו פונקציות יש?

objdump -d -M intel mystery | grep "^[0-9a-f].*<.*>:"

0000000000001149 <check_password>:
0000000000001189 <main>:

יש שתי פונקציות: check_password ו-main. השמות מאוד מרמזים.

שלב 4: מה main עושה?

objdump -d -M intel mystery

נסתכל על main ונראה שהוא קורא ל-puts (להדפיס "Enter password:"), ואז ל-scanf (לקבל קלט), ואז ל-check_password, ואז מדפיס "Access granted!" או "Access denied." לפי ערך ההחזרה.

שלב 5: אימות עם GDB

gdb ./mystery
(gdb) break check_password
(gdb) run
Enter password: test123
(gdb) disassemble
(gdb) info registers

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

זו דוגמה פשוטה, אבל היא מדגימה את התהליך: file -> strings -> objdump -> GDB. ברוב המקרים, בינאריים אמיתיים יהיו הרבה יותר מורכבים, אבל הגישה הבסיסית נשארת אותו דבר.


תזכורת: צנרת הקומפילציה - compilation pipeline

כדי לעשות הנדסה הפוכה, צריך להבין מה קרה בדרך "קדימה". בואו נזכיר את תהליך הקומפילציה שלמדנו:

קוד מקור (.c)
    |
    v
[Preprocessor] -- מעבד #include, #define, מאקרואים
    |
    v
קוד מקור מעובד (.i)
    |
    v
[Compiler] -- מתרגם C לאסמבלי
    |
    v
קוד אסמבלי (.s)
    |
    v
[Assembler] -- מתרגם אסמבלי לקוד מכונה
    |
    v
קובץ אובייקט (.o)
    |
    v
[Linker] -- מחבר קבצי אובייקט וספריות
    |
    v
קובץ הרצה (ELF)

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


תזכורת: x86-64 עבור RE

בפרקים 1 ו-2 למדנו אסמבלי 16 ביט ו-32 ביט. ברוב עבודת הRE שנעשה, נעבוד עם x86-64 (64 ביט). בואו נזכיר כמה נקודות חשובות:

מוסכמת קריאה - calling convention (System V AMD64 ABI)

ב-x86-64 על לינוקס, הארגומנטים לפונקציה מועברים באוגרים (לא על המחסנית כמו ב-32 ביט):

ארגומנט אוגר
ראשון rdi
שני rsi
שלישי rdx
רביעי rcx
חמישי r8
שישי r9
ערך החזרה rax

אם יש יותר מ-6 ארגומנטים, השאר מועברים על המחסנית.

זה קריטי ל-RE: כשאתה רואה call בדיסאסמבלי, אתה יכול להסתכל על rdi, rsi, rdx וכו' כדי להבין אילו ארגומנטים הועברו לפונקציה. למשל, אם רואים:

lea rdi, [rip+0x123]    ; rdi = כתובת של מחרוזת
call puts

ידוע ש-puts מקבל ארגומנט אחד (מחרוזת), והוא הועבר ב-rdi.

פרולוג ואפילוג של פונקציה

כמעט כל פונקציה מתחילה ומסתיימת באותו דפוס:

פרולוג - prologue:

push rbp          ; שומר את rbp של הפונקציה הקוראת
mov rbp, rsp      ; מגדיר את rbp לראש המחסנית הנוכחי
sub rsp, 0x20     ; מקצה מקום למשתנים מקומיים (32 בתים בדוגמה)

אפילוג - epilogue:

leave             ; שקול ל: mov rsp, rbp; pop rbp
ret               ; חוזר לכתובת ההחזרה שעל המחסנית

כשאתה רואה push rbp; mov rbp, rsp - אתה יודע שזו תחילת פונקציה. כש רואה leave; ret - זה הסוף שלה.

מבנה מסגרת המחסנית - stack frame layout

כתובות גבוהות (למעלה)
+---------------------------+
| ארגומנט 7 (אם יש)        |  [rbp+0x10]
+---------------------------+
| כתובת חזרה                |  [rbp+0x8]
+---------------------------+
| rbp ישן (saved rbp)       |  [rbp]
+---------------------------+
| משתנה מקומי ראשון         |  [rbp-0x4]
+---------------------------+
| משתנה מקומי שני           |  [rbp-0x8]
+---------------------------+
| ...                       |
+---------------------------+
כתובות נמוכות (למטה)         <-- rsp

ב-RE, כשאתה רואה גישה ל-[rbp-0x4], אתה יודע שזה משתנה מקומי. כשאתה רואה גישה ל-[rbp+0x10], אתה יודע שזה ארגומנט שהועבר על המחסנית (אם יש יותר מ-6 ארגומנטים).

לעתים קרובות הקומפיילר משתמש ב-rsp ישירות במקום rbp (זה נקרא "frame pointer omission" - אופטימיזציה שחוסכת אוגר). במקרה כזה, כל הגישות לזיכרון הן יחסיות ל-rsp.


סיכום

בהרצאה הזו הנחנו את הבסיס לתחום ההנדסה ההפוכה:
- הנדסה הפוכה היא לקיחת בינארי מקומפל והבנה מה הוא עושה, בלי קוד מקור
- יש לכך שימושים רבים: ניתוח נוזקות, מחקר חולשות, הבנת תוכנה סגורה, CTF, ושיפור התכנות
- יש שני סוגי ניתוח - סטטי (בלי להריץ) ודינמי (תוך כדי ריצה) - ובפועל משלבים את שניהם
- הכלים העיקריים: file, strings, objdump, GDB, Ghidra, radare2, strace, ltrace
- הבסיס לRE הוא הידע שצברנו לאורך הקורס: אסמבלי, C, ELF, ומערכת ההפעלה
- חזרנו על calling convention של x86-64, פרולוג/אפילוג של פונקציות, ומבנה ה-stack frame

בהרצאה הבאה (7.2) נלמד לזהות דפוסים באסמבלי - איך if/else, לולאות, switch/case, structs ומערכים נראים אחרי קומפילציה.