דילוג לתוכן
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

מבוא ל-Ghidra עם בינארי תמים שמקמפלים לבד

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

ביניים11 דקות קריאהמאת NotHoly

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

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

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

מהו Ghidra

Ghidra הוא framework להנדסה לאחור של תוכנה שפיתחה ה-U.S. National Security Agency, והוא מתפרסם ב-GitHub במאגר NationalSecurityAgency/ghidra. הוא רץ על Windows, macOS ו-Linux. עבור תוכנית מקומפלת הוא יודע לעשות ארבעה דברים שרלוונטיים כאן:

  • דיסאסמבלי של קוד המכונה להוראות אסמבלי.
  • דקומפילציה של פונקציות לפסאודו־קוד דמוי C בעזרת הדקומפיילר המובנה.
  • איתור מחרוזות והפניות צולבות (XREFs), שמראות איפה בתוכנית משתמשים במחרוזת או בכתובת.
  • שמירת השינויים שלך בפרויקט, כולל שמות שנתת לפונקציות ולמשתנים.

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

למה לנתח תוכנית שכתבת בעצמך

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

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

  1. אמת ידועה (ground truth). ידוע שהפונקציה check() משווה את הארגומנט אל "LAB-1234".
  2. שני builds של אותו קוד. אחד שומר מידע דיבוג וסימבולים. השני עובר strip ואופטימיזציה. ההשוואה ביניהם מראה מה הקומפיילר הסיר.
  3. יעד בטוח. הוא רק מדפיס ok או no. אין קריאות רשת ואין כתיבת קבצים.

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

הכנת המעבדה

מה צריך

  • קומפיילר C. ב-Linux, להתקין את gcc דרך מנהל החבילות. ב-macOS, הכלי clang מתוך כלי שורת הפקודה של Xcode מתאים למעבדה הזו, והדגלים זהים. ב-Windows, להשתמש ב-WSL או בקונטיינר עם GCC.
  • JDK בשביל Ghidra. Ghidra רץ על Java, ולכן צריך JDK מותקן לפני שה-launcher יעלה.
  • Ghidra מדף ה-releases הרשמי ב-GitHub.
  • טרמינל עם sha256sum. הכלי כלול ב-Git Bash ב-Windows.

התקנת JDK ו-Ghidra

יש להתקין את ה-JDK שגרסת Ghidra דורשת, ולא לסמוך על מדריך ישן בעניין המספר הזה. כשבדקתי את ה-README של המאגר ואת מדריך Getting Started בתאריך הכתיבה, שניהם דרשו JDK 25 של 64-bit, וה-README קישר ל-builds של Eclipse Temurin. צריך לקרוא את ה-README או את ה-release notes של הגרסה המדויקת שהורדת ולהשתמש ב-JDK שהם מציינים. אי־התאמה בגרסת Java היא סיבה נפוצה לכך ש-Ghidra לא עולה.

להוריד את Ghidra רק מדף ה-releases הרשמי תחת github.com/NationalSecurityAgency/ghidra. המאגר מקשר לקובצי הגרסה. יש לחלץ את הקובץ לתיקייה חדשה: מדריך Getting Started אומר לא לחלץ את Ghidra על גבי התקנה קיימת. מפעילים אותו עם ghidraRun ב-Linux וב-macOS, או עם ghidraRun.bat ב-Windows.

אימות ההורדה

לפני שמחלצים משהו, בודקים את הקובץ מול ערך SHA-256 שפורסם. את הפקודה מריצים בתיקייה שבה נשמר הקובץ:

bash
sha256sum ghidra_*.zip

משווים את ה-hash שהודפס לערך SHA-256 ש-GitHub מציג ליד הקובץ בדף הגרסה, או לערך שהפרויקט מפרסם. ה-hash חייב להיות זהה בדיוק. אם אין SHA-256 מפורסם לקובץ, צריך לרשום את זה ביומן המעבדה. hash שהועתק מ-mirror אקראי הוא לא checksum של המפרסם.

בדיקת hash מוכיחה רק שהקובץ תואם למה שהמפרסם רשם. היא לא מוכיחה שאפשר לסמוך על המפרסם. לכן משתמשים במקור הרשמי ושומרים ביומן את שם הקובץ ואת ה-hash.

כתיבת תוכנית הדוגמה

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

bash
mkdir -p ghidra-lab && cd ghidra-lab
cat > toy.c <<'EOF'
#include <stdio.h>
#include <string.h>
int check(const char *k){ return strcmp(k, "LAB-1234") == 0; }
int main(int c, char **v){ if(c>1 && check(v[1])) puts("ok"); else puts("no"); return 0; }
EOF

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

רושמים את ה-hash של קובץ המקור, כדי שאפשר יהיה להוכיח אחר כך איזה קובץ נותח:

bash
sha256sum toy.c

על המחשב שבו השתמשתי, הפלט היה:

text
d9746d8fb118fa954a46b4d501ddf9b0ed86e986e7c99adb05e4a1a60ca0071d  toy.c

ה-hash שלך יהיה שונה אם בקובץ יש רווחים או סיומות שורה אחרים.

קימפול פעמיים

בונים את אותו קוד מקור לשני בינאריים עם הגדרות שונות:

bash
gcc -O0 -g -o toy_debug toy.c
gcc -O2 -s -o toy_stripped toy.c

לכל דגל תפקיד אחר. כולם מתוארים במדריך GCC, שכדאי לקרוא פעם אחת:

  • -O0 מכבה את רוב האופטימיזציות. זו ברירת המחדל, ומדריך GCC מתאר אותה כהגדרה ששומרת על דיבוג צפוי. הקוד צמוד למקור.
  • -O2 מפעיל את רוב האופטימיזציות שלא מחליפות גודל במהירות. לפי המדריך הוא מפעיל גם inlining של פונקציות קטנות, כולל כאלה שלא הוגדרו inline. הקוד יכול להיראות שונה מאוד מהמקור.
  • -g מוסיף מידע דיבוג בפורמט הטבעי של מערכת ההפעלה. Ghidra יודע לקרוא חלק ממנו, ודיבאגרים משתמשים בכולו.
  • -s מסיר את טבלת הסימבולים מהפלט בזמן הקישור. שמות הפונקציות check וגם main כבר לא נשמרים בקובץ.

מה שהופך את המעבדה לשימושית הוא שני בינאריים מאותו מקור עם דגלים שונים. צריך לשמור את שניהם.

הרצת שני ה-builds

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

bash
./toy_debug LAB-1234
./toy_debug wrong
./toy_debug
./toy_stripped LAB-1234
./toy_stripped wrong

הרצתי את זה בתוך קונטיינר Debian עם GCC 14.2.0 על מארח x86-64. הפלט היה:

text
ok
no
no
ok
no

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

בדיקת הבינאריים משורת הפקודה

לפני Ghidra, כדאי לראות מה שמור בכל קובץ. מספיקים שני כלים: nm מציג סימבולים, strings מציג טקסט קריא.

bash
nm toy_debug | grep -E " (check|main)$"
nm toy_stripped
strings toy_stripped | grep LAB
ls -l toy_debug toy_stripped

על המחשב שבו השתמשתי, הפלט היה:

text
0000000000001149 T check
0000000000001175 T main
nm: toy_stripped: no symbols
LAB-1234
-rwxr-xr-x 1 root root 17384 Nov  8 09:17 toy_debug
-rwxr-xr-x 1 root root 14480 Nov  8 09:17 toy_stripped

כדאי לקרוא את השורות האלה בעיון:

  • ה-build עם הדיבוג עדיין כולל את השמות check וגם main. השמות האלה הגיעו מקוד המקור שלך.
  • ל-build שעבר strip אין סימבולים. לכלי nm אין מה להציג.
  • המחרוזת LAB-1234 נשארת בשני הקבצים. מחרוזת היא נתון, ו-strip לא מסיר נתונים שהתוכנית צריכה בזמן ריצה. המחרוזת נמצאת בקובץ כקבוע, ובגללה קל להתחיל עם המעבדה הזו.

עכשיו דיסאסמבלי של check ב-build עם הדיבוג:

bash
objdump -d -M intel toy_debug | grep -A14 "<check>:"

הפלט נראה כך:

text
0000000000001149 <check>:
    1149:	55                   	push   rbp
    114a:	48 89 e5             	mov    rbp,rsp
    114d:	48 83 ec 10          	sub    rsp,0x10
    1151:	48 89 7d f8          	mov    QWORD PTR [rbp-0x8],rdi
    1155:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
    1159:	48 8d 15 a4 0e 00 00 	lea    rdx,[rip+0xea4]        # 2004 <_IO_stdin_used+0x4>
    1160:	48 89 d6             	mov    rsi,rdx
    1163:	48 89 c7             	mov    rdi,rax
    1166:	e8 d5 fe ff ff       	call   1040 <strcmp@plt>
    116b:	85 c0                	test   eax,eax
    116d:	0f 94 c0             	sete   al
    1170:	0f b6 c0             	movzx  eax,al
    1173:	c9                   	leave
    1174:	c3                   	ret

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

עכשיו ה-build המאופטם. הרצתי דיסאסמבלי על toy_stripped באותה דרך. הקומפיילר הכניס את ההשוואה ישירות אל main, שיושבת בכתובת 0x1060 אצלי: cmp edi,0x1 עבור הבדיקה c>1, אחר כך mov rdi,[rsi+0x8] כדי לטעון את v[1], ואז call strcmp@plt. הוא עדיין שמר עותק נפרד של check בכתובת 0x1190, כי הפונקציה לא static וקובץ אחר יכול לקרוא לה. הכתובות ישתנו לפי ה-toolchain, אז זו מפה של מה לחפש ואין צורך שהמספרים יתאימו.

ייבוא ל-Ghidra

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

  1. להפעיל את Ghidra עם ghidraRun (או ghidraRun.bat).
  2. לבחור File > New Project. לבחור Non-Shared Project ולתת לפרויקט שם, למשל toy-lab. לשמור אותו בתיקייה שבשליטתך.
  3. לבחור File > Import File. לבחור את toy_debug. לאשר את הפורמט שזוהה אם הוא מתאים למערכת שלך. קובץ שנבנה על x86-64 Linux אמור להופיע כ-ELF עם מעבד x86.
  4. לחזור על הייבוא עבור toy_stripped. עכשיו יש בפרויקט שתי רשומות.
  5. לפתוח כל קובץ בכלי CodeBrowser. כש-Ghidra שואל אם לנתח את התוכנית, לאשר את ניתוח ברירת המחדל במעבדה הזו.

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

מציאת המחרוזת ומעקב אחרי ההפניות

מתחילים עם toy_stripped, כי זה המקרה הקשה יותר. אחר כך משווים מול toy_debug.

  1. לפתוח את Window > Defined Strings.
  2. למצוא את LAB-1234 ברשימה. ללחוץ עליה לחיצה כפולה כדי לקפוץ לכתובת שלה ב-listing.
  3. ללחוץ לחיצה ימנית על הכתובת של המחרוזת ולפתוח את תפריט ההפניות. לחפש פריט כמו References > Show References to. כל הפניה היא מקום בקוד שקורא את הכתובת הזו.
  4. לעקוב אחרי ההפניה. היא מובילה לפונקציה שמשווה את הקלט למחרוזת.

ב-build עם הדיבוג, Ghidra אמור להציג גם את שם הפונקציה check מטבלת הסימבולים. ב-build שעבר strip יופיע שם שנוצר אוטומטית, כמו FUN_00101190. המספר תלוי ב-build, אז צריך להשתמש בשם ש-Ghidra מציג אצלך על המסך.

אותו דבר עם המחרוזות "ok" וגם "no". שתיהן בשימוש בתוך main. מעקב אחריהן מתוך main נותן את אותה תמונה מהצד השני.

קריאת הדקומפיילר לצד קוד המקור

פותחים את חלונית הדקומפיילר עבור הפונקציה שמשווה את המחרוזת. מניחים את toy.c לידה בחלון אחר.

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

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

עכשיו רושמים את ההבדלים. הבדלים נפוצים:

  • טיפוסים. הדקומפיילר עשוי להציג char * בתור char *, או להשתמש בטיפוס רופף יותר כמו undefined8. ייתכן שהוא לא יודע שהקלט הוא const.
  • שמות. הפרמטר k שלך יופיע בתור param_1 או placeholder דומה.
  • ערך החזרה. ביטוי ה-C strcmp(...) == 0 עשוי להופיע כהשוואה ואחריה return מותנה. המשמעות זהה, הצורה שונה.
  • טיפול בארגומנטים בתוך main. הבדיקה c > 1 שלך הופכת להשוואה על מספר הארגומנטים. כדאי לשים לב ל-off-by-one: argv[0] הוא שם התוכנית, בעוד v[1] הוא הארגומנט האמיתי הראשון.

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

שינוי שמות משתנים ב-build שעבר strip

עכשיו החלק שהופך בינארי אחרי strip לקריא.

  1. בחלונית הדקומפיילר, ללחוץ על הפרמטר שמחזיק את מחרוזת הקלט.
  2. ללחוץ לחיצה ימנית ולבחור Rename Variable, או לסמן את המשתנה וללחוץ L. לקרוא לו input.
  3. לשנות את שם הפונקציה עצמה, ב-listing או בעץ הסימבולים, לשם check_input.
  4. בתוך main, לשנות את שם המשתנה של מערך הארגומנטים לשם argv ואת שם המונה לשם argc.

Ghidra שומר את השמות האלה בפרויקט שלך. הבינארי בדיסק לא משתנה. כלומר, מתעדים את הקריאה שלך ולא עורכים את הקובץ שקומפל.

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

השוואת שני ה-builds

מניחים זו לצד זו את הדקומפילציה של ה-build עם הדיבוג ושל ה-build שעבר strip. ביומן צריך לענות על השאלות האלה:

  • אילו שמות פונקציות נשארו ב-build שעבר strip?
  • האם ה-build עם -O2 עשה inlining של check בתוך main, או שמר פונקציה נפרדת? לחפש ב-listing מקום קריאה נפרד של strcmp וגם ret נפרד.
  • אילו חלקים מהמקור שלך קל לראות בשתי התצוגות?
  • אילו חלקים קשה למצוא בתצוגה של ה-build שעבר strip?

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

טעויות נפוצות

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

לסמוך על השמות של הדקומפיילר. שם כמו FUN_00101190 או local_18 הוא placeholder. שינוי השם הוא טענה שלך, והיא צריכה ראיות.

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

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

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

לשכוח את גרסת ה-JDK. אם Ghidra לא עולה, בודקים את גרסת Java מול ה-release notes לפני שנוגעים בכל דבר אחר.

איך ממשיכים

אחרי שהדוגמה עובדת, אפשר להקשות את התרגיל בצורה מבוקרת:

  • להחליף את "LAB-1234" במחרוזת ארוכה יותר ולקמפל מחדש. למצוא אותה שוב בשני ה-builds.
  • להוסיף פונקציית בדיקה שנייה שמשווה את הקלט למספר, ואז למצוא איפה Ghidra מציג את ההמרה.
  • לבנות עם -O0 -s ולהשוות מול -O2 -s. ההבדל מראה כמה האופטימייזר משנה את צורת הקוד בלי להסיר את הלוגיקה.
  • לקרוא את התיעוד של Ghidra עצמו על הדקומפיילר ועל תצוגת ה-listing. האתר הרשמי ותיקיית docs ב-GitHub מכסים את הממשק לעומק רב יותר ממה שהמאמר הזה יכול.

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

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

מקורות

מקורות

  1. Ghidra: Getting Started (GitHub repository)
  2. Ghidra repository README
  3. Ghidra project site
  4. GCC manual: Optimize Options
  5. GCC manual: Debugging Options

עוד בנושא הנדסה לאחור

הנדסה לאחור

למה להשתמש ב-NotHoly להנדסה הפוכה

השאירו את Ghidra, radare2 או IDA. הדביקו את פלט ה-decompiler והמודל מסביר אותו וכותב את הסקריפט, בלי לסרב. triage סטטי למבט ראשון.

ביניים2 דקות קריאה