מ-C לאסמבלי — Reverse Engineering בסיסי
מ-C לאסמבלי — Reverse Engineering בסיסי
בעולם הסייבר האמיתי, אתה לא מקבל קוד מקור — אתה מקבל קובץ בינארי. שיעור זה מלמד איך לזהות תבניות (Patterns) מוכרות ב-Assembly: השמות, תנאים, לולאות, וקריאות פונקציה. זהו הצעד הראשון ב-Reverse Engineering אמיתי.
- כלי RE: gcc -O0 -S, objdump -d — Disassembly בסביבת לינוקס
- Pattern Recognition — זיהוי if/else מ-cmp+Jcc בAssembly
- היפוך לוגיקה — הקומפיילר כותב את ההפך! JNE = if(==)
- זיהוי לולאות — while/for לפי קפיצה אחורה לכתובת נמוכה
- Stripped Binaries — ניתוח ללא שמות פונקציות, עם Strings כרמזים
- Decompilation — שחזור קוד C מ-Assembly שורה-שורה
כלי RE: gcc ו-objdump
כמו ארכיאולוג שחופר שכבות אבן כדי להבין עיר עתיקה, ה-RE מבצע Disassembly: ממיר בינארי לAssembly קריא. הכלים העיקריים הם gcc ו-objdump.
gcc -O0: קימפול ללא אופטימיזציה
# ─── קימפול רגיל (מייצר Assembly "נקי") ──────────────────
gcc -O0 secret.c -o secret.bin # ללא אופטימיזציה!
gcc -O0 -m32 secret.c -o secret.bin # 32bit — קל יותר לקריאה
gcc -O0 -S secret.c -o secret.asm # רק Assembly — לא מריץ
# ─── למה -O0? ─────────────────────────────────────────────
# ב-O2/O3: הקומפיילר "חכם" — ממזג פעולות, מסיר קוד, משנה לוגיקה
# ב-O0: כל שורת C הופכת לפקודות Assembly ישירות → קל לעקוב!
# לדוגמה: int x = 5; if (x > 3) x = 10;
# O0 → 4 פקודות Assembly ברורות
# O2 → הקומפיילר מבין x תמיד > 3 → מוציא: int x = 10; ישירות!
# ─── objdump: Disassembly ─────────────────────────────────
objdump -d secret.bin # Disassemble כל sections
objdump -d -M intel secret.bin # Intel Syntax (קל יותר!)
objdump -d -M intel secret.bin > secret.asm # שמור לקובץ
# ─── פלט אופייני של objdump ─────────────────────────────
08048416 <main>:
08048416: 55 push ebp
08048417: 89 e5 mov ebp,esp
0804841b: 83 ec 10 sub esp,0x10 ; הקצאת Stack frame
0804841e: c7 45 fc 39 05 00 00 mov DWORD PTR [ebp-0x4],0x539 ; x=1337
08048425: 81 7d fc 39 05 00 00 cmp DWORD PTR [ebp-0x4],0x539 ; if x==1337?
# ↑ כתובת ↑ bytes גולמיים ↑ Assembly פקודה08048416: 55 push ebp. ה-strings command מציג strings, לא Assembly.Pattern Recognition: if/else בAssembly
הסוד ב-RE הוא לא לקרוא שורה-שורה כמו רובוט — אלא לזהות תבניות (Patterns). ה-if/else הוא הPattern הראשון והחשוב ביותר: cmp + קפיצה מותנית.
הכלל הזהב: היפוך הלוגיקה
הקומפיילר עובד על דרך השלילה. אם כתוב if (a == 5), הקומפיילר אומר: "אם a לא שווה ל-5 (JNE), דלג מעל הבלוק."
; ─── קוד C ───────────────────────────────────────────────
int a = 5;
if (a > 3) { a = 10; }
; ─── Assembly מקביל (Intel Syntax) ──────────────────────
mov DWORD PTR [ebp-4], 5 ; int a = 5
cmp DWORD PTR [ebp-4], 3 ; השווה a ל-3
jle .L2 ; היפוך לוגיקה! אם a ≤ 3 → דלג
mov DWORD PTR [ebp-4], 10 ; גוף ה-if: a = 10
.L2: ; המשך התוכנית
; ─── כלל: Jcc ↔ C Operator (ההפך!) ──────────────────────
; JE (Jump if Equal) ← אתה כותב: if (x != y)
; JNE (Jump if Not Equal) ← אתה כותב: if (x == y)
; JL (Jump if Less) ← אתה כותב: if (x >= y)
; JLE (Jump if Less/Equal) ← אתה כותב: if (x > y)
; JG (Jump if Greater) ← אתה כותב: if (x <= y)
; JGE (Jump if Greater/Equal) ← אתה כותב: if (x < y)
; ─── if/else מלא ──────────────────────────────────────────
; C: if (x == y) { do_A(); } else { do_B(); }
cmp eax, ebx ; השווה x ל-y
jne .else_block ; אם לא שווה → קפוץ ל-else
call do_A ; ← True branch (x == y)
jmp .end
.else_block:
call do_B ; ← False branch (x != y)
.end:! מלכודת — JNE לא אומר "אם לא שווה" בהכרח ›
JNE = "Jump if Not Equal" — אבל מה שנקרא בפועל הוא לפי ZF (Zero Flag). cmp a, b מחסר b מ-a ומציב את הflags. אם התוצאה 0 → ZF=1 → JE יקפוץ. אם התוצאה אינה 0 → ZF=0 → JNE יקפוץ.
המשמעות: cmp eax, ebx / jne .label = "קפוץ אם eax ≠ ebx". הקוד C המקורי היה if (eax == ebx) { ... } — כלומר הJNE דולג מעל הif כשהתנאי שגוי.
cmp eax, 10
jge .skip
; ... block ...
.skip:מה קוד ה-C המקביל?mov DWORD PTR [ebp-4], 5 ; x = 5
mov DWORD PTR [ebp-8], 10 ; y = 10
mov eax, DWORD PTR [ebp-4]
cmp eax, DWORD PTR [ebp-8]
jge .else_label
mov eax, DWORD PTR [ebp-8]
mov DWORD PTR [ebp-4], eax ; x = y
jmp .end
.else_label:
add DWORD PTR [ebp-8], 1 ; y++
.end:מה קוד ה-C המשוחזר?mov DWORD PTR [ebp-4], 0x539
cmp DWORD PTR [ebp-4], 0x539
jne .fail
; ... success code ...
.fail:מה הפרמטר ש-C בדק, ומהו הערך שמייצג 0x539?Pattern Recognition: לולאות בAssembly
לולאה = תנאי IF שמבקש מהמעבד לחזור אחורה. הסימן המובהק ביותר ללולאה ב-Assembly הוא קפיצה לכתובת נמוכה יותר — "קפיצה אחורה".
While Loop — הPattern הבסיסי
; ─── C: while (count > 0) { count--; } ─────────────────────
; Assembly:
mov DWORD PTR [ebp-4], 10 ; count = 10
jmp .L_CHECK ; קפוץ לבדיקה מיד
.L_BODY:
sub DWORD PTR [ebp-4], 1 ; count = count - 1
.L_CHECK:
cmp DWORD PTR [ebp-4], 0 ; count > 0 ?
jg .L_BODY ; אם כן → חזור לGODY (קפיצה אחורה!)
; ← אחרי הלולאה: count = 0
; ─── סימנים לזיהוי while ──────────────────────────────────
; 1. cmp + Jcc בתחילת / אמצע הקוד
; 2. JMP לכתובת נמוכה יותר (אחורה!) — זה הסימן הכי חזק
; ← תמיד: קפיצה אחורה = לולאה
; ─── For Loop — while עם counter ──────────────────────────
; C: for (int i = 0; i < 5; i++) { SAY(i); }
mov DWORD PTR [ebp-4], 0 ; i = 0 ← אתחול לפני לולאה
.L_CHECK:
cmp DWORD PTR [ebp-4], 5 ; i < 5 ?
jge .L_END ; אם i ≥ 5 → יצא
; body...
add DWORD PTR [ebp-4], 1 ; i++ ← increment בסוף גוף
jmp .L_CHECK ; חזור לבדיקה
.L_END:
; ─── זיהוי for vs while ────────────────────────────────────
; For: יש אתחול משתנה לפני הלולאה + increment קבוע בגוף
; While: רק בדיקת תנאי, ללא increment מובנהmov DWORD PTR [ebp-4], 10 ; count = ?
jmp .L_CHECK
.L_BODY:
sub DWORD PTR [ebp-4], 1
.L_CHECK:
cmp DWORD PTR [ebp-4], 0
jg .L_BODYמה קוד ה-C המקביל?Decompilation — שחזור קוד C מ-Assembly
Decompilation היא האמנות של שחזור קוד C מ-Assembly. הכלים (Ghidra, IDA) עושים חלק מהעבודה אוטומטית — אבל ידע ידני חיוני כשהכלים כושלים.
מתודולוגיית Decompilation — 5 צעדים
; ─── 5 צעדים לDecompilation ─────────────────────────────
; 1. זהה גבולות פונקציות: push ebp / mov ebp,esp = תחילת פונקציה
; 2. מפה משתנים: [ebp-4]=var1, [ebp-8]=var2, [ebp+8]=arg1
; 3. זהה Patterns: CMP+Jcc → if/else, קפיצה אחורה → לולאה
; 4. זהה קריאות API: CALL printf, CALL malloc, CALL strcmp
; 5. כתוב C ותוודא Logic
; ─── דוגמה מלאה: Assembly → C ───────────────────────────
0804841b: mov DWORD PTR [ebp-4], 0x5 ; step2: var1 = 5 (x)
08048422: mov DWORD PTR [ebp-8], 0xa ; step2: var2 = 10 (y)
08048429: mov eax, DWORD PTR [ebp-4] ; eax = x
0804842c: cmp eax, DWORD PTR [ebp-8] ; step3: cmp x, y
0804842f: jge 0804843b ; step3: if (x < y) → jge skips
08048431: mov eax, DWORD PTR [ebp-8] ; eax = y
08048434: mov DWORD PTR [ebp-4], eax ; x = y
08048437: jmp 0804843f ; skip else
0804843b: add DWORD PTR [ebp-8], 1 ; y++ (else branch)
0804843f: nop ; end
; ─── C משוחזר ────────────────────────────────────────────
int x = 5;
int y = 10;
if (x < y) { // jge = ≥ → הפוך = <
x = y; // True branch
} else {
y++; // False branch (else)
}| Assembly Jcc | קפיצה כאשר | תנאי C (ההפך) |
|---|---|---|
| JE / JZ | שווה (ZF=1) | if (x != y) |
| JNE / JNZ | לא שווה (ZF=0) | if (x == y) |
| JL / JNGE | קטן מ (Signed) | if (x >= y) |
| JLE / JNG | קטן/שווה | if (x > y) |
| JG / JNLE | גדול מ | if (x <= y) |
| JGE / JNL | גדול/שווה | if (x < y) |
mov DWORD PTR [ebp-4], 0 ; i = 0
.L_CHECK:
cmp DWORD PTR [ebp-4], 10
jge .L_END
mov eax, DWORD PTR [ebp-4]
call print_num ; print(i)
add DWORD PTR [ebp-4], 1 ; i++
jmp .L_CHECK
.L_END:מה קוד C המשוחזר?; Stripped function (no names!)
08048500: push ebp
08048501: mov ebp, esp
08048503: mov eax, DWORD PTR [ebp+8] ; arg1
08048506: cmp eax, 0
08048508: je 0804850f
0804850a: neg eax
0804850c: jmp 08048511
0804850f: mov eax, 0
08048511: pop ebp
08048512: retמה עושה הפונקציה (בC)?אתגר מעשי — Decompilation — קריאת Assembly
שיא השיעור: היפוך, ניתוח וזיהוי — כלי ה-RE וה-Low-Level בפעולה. ההגשה נבדקת אוטומטית.
סיכום + Dry Run סגנון גאמא
הנה סיכום כל ה-Patterns הקריטיים לRE, ואחריהם שאלות מסכמות בסגנון מיון גאמא.
- gcc -O0: קמפול ללא אופטימיזציה — Assembly מקביל 1:1 לC. objdump -d -M intel: Disassembly
- הכלל הזהב: הקומפיילר כותב הפך! JNE → if(==). JGE → if(<). JLE → if(>)
- if/else Pattern: cmp + Jcc (קפיצה קדימה) + [True block] + [Else block]
- לולאה Pattern: cmp + Jcc + body + JMP לכתובת נמוכה (קפיצה אחורה!)
- for vs while: for = יש אתחול counter לפני + increment קבוע בגוף
- Stripped Binary: ללא שמות → בלש! חפש strings ו-API calls
- מיפוי Stack: [ebp-N] = local var, [ebp+8] = arg1, [ebp+4] = return addr
- ב-RE: תמיד כתוב C מקביל לצד ה-Assembly — שורה-שורה, בזמן ריצה
cmp eax, ebx
jne .L_else
; block A
jmp .L_end
.L_else:
; block B
.L_end:מה מבנה ה-C?mov [ebp-4], 1 ; result = 1
mov [ebp-8], 1 ; i = 1
.L_CHECK:
cmp [ebp-8], [ebp+8] ; i vs arg1
jg .L_END
mov eax, [ebp-4]
imul eax, [ebp-8] ; result *= i
mov [ebp-4], eax
add [ebp-8], 1 ; i++
jmp .L_CHECK
.L_END:
mov eax, [ebp-4]
retמה הפונקציה עושה?שאלות תרגול ותשובות (10)
שאלה 1 · למה מקמפלים עם הדגל -O0 כשלומדים Reverse Engineering?
- א. כי הקוד נשאר קרוב למקור — כל משתנה יושב במקומו על ה-Stack והלולאות לא נדחסות
- ב. כי הקובץ המתקבל קטן יותר ולכן קל יותר לפתוח אותו
- ג. כי -O0 מוסיף שמות פונקציות לקובץ גם אם הוא Stripped
- ד. כי בלי -O0 המהדר לא מייצר קוד אסמבלי בכלל
-O0 מבטל אופטימיזציות, ולכן ההתאמה בין שורת C לבין קטע האסמבלי כמעט אחד-לאחד. עם -O2 המהדר משלב משתנים לרגיסטרים, פורש לולאות ומוחק חישובים — והקוד כבר לא נראה כמו המקור.
שאלה 2 · מה עושה הפקודה הבאה?
objdump -d -M intel secret.bin
- א. מבצעת Disassembly לקוד ומציגה אותו בתחביר Intel
- ב. מריצה את הקובץ ומדפיסה את הפלט שלו
- ג. משחזרת קוד C מלא מתוך הקובץ הבינארי
- ד. מסירה את טבלת הסמלים מהקובץ
-d = disassemble לקטעי הקוד, -M intel = תחביר Intel (יעד ראשון, מקור שני) במקום AT&T. objdump לא מריץ כלום ולא מייצר C — הוא רק מתרגם בייטים לפקודות.
שאלה 3 · פתחת קובץ ב-objdump ובמקום שמות פונקציות אתה רואה רק כתובות. מה קרה?
- א. הקובץ Stripped — טבלת הסמלים הוסרה, והקוד עצמו נשאר תקין
- ב. הקובץ פגום ולא ניתן לניתוח
- ג. הקובץ מוצפן וצריך מפתח כדי לקרוא אותו
- ד. objdump הופעל בלי הרשאות מתאימות
strip מוחק את שמות הסמלים בלבד. הקוד רץ בדיוק אותו דבר, ואת התפקיד של כל פונקציה מזהים לפי מה שהיא עושה — קריאות מערכת, מבנה לולאות והפרמטרים שנדחפים לפניה.
שאלה 4 · מה מבצע קטע האסמבלי הבא במונחי C?
cmp eax, 10 jge .skip ; ... block ... .skip:
- א. if (eax < 10) { block }
- ב. if (eax >= 10) { block }
- ג. while (eax >= 10) { block }
- ד. המשמעות זהה ל-if (eax == 10)
המהדר קופץ מעל הבלוק כשהתנאי המקורי לא מתקיים. jge מדלג כאשר eax ≥ 10, ולכן הבלוק רץ רק כש-eax < 10 — כלומר תנאי הקפיצה הוא ההיפוך של תנאי ה-if. 🎯 דגש למבחן: קראו את cmp כאופרטור השוואה פשוט משמאל לימין — cmp eax, ebx מציב את השאלה "eax ביחס ל-ebx", והקפיצה שאחריו (jge/jl/jne) היא זו שמגדירה את התנאי בפועל.
שאלה 5 · מה יהיה הערך ב-[ebp-8] בסוף הריצה?
mov DWORD PTR [ebp-4], 5 mov DWORD PTR [ebp-8], 1 cmp DWORD PTR [ebp-4], 3 jle .end add DWORD PTR [ebp-8], 10 .end:
- א. 11
- ב. 1
- ג. 10
- ד. 5
[ebp-4]=5, וההשוואה מול 3 נותנת 5 > 3 — לכן jle לא קופץ, השורה add מתבצעת, ו-[ebp-8] עולה מ-1 ל-11.
שאלה 6 · מה הסימן המובהק ביותר לכך שקטע אסמבלי הוא לולאה?
- א. קפיצה אחורה — יעד הקפיצה הוא כתובת נמוכה מהפקודה הנוכחית
- ב. שימוש ברגיסטר ecx כלשהו בקוד
- ג. הופעה של הפקודה cmp בקוד
- ד. קיום תווית עם שם באנגלית
זרימת קוד רגילה מתקדמת קדימה. קפיצה לכתובת נמוכה יותר מחזירה את הביצוע לאחור — וזה בדיוק מה שיוצר חזרתיות. cmp ותוויות מופיעים גם ב-if רגיל.
שאלה 7 · איך מזהים שהלולאה היא for ולא while פשוטה?
- א. יש אתחול של המונה לפני הכניסה ללולאה ועדכון קבוע שלו בסוף כל סיבוב
- ב. הלולאה משתמשת ב-jmp במקום ב-jne
- ג. גוף הלולאה קצר מ-5 פקודות
- ד. הלולאה נמצאת בסוף הפונקציה
שלישיית ה-for — אתחול, תנאי, עדכון — משאירה חתימה ברורה: mov למונה לפני הלולאה, cmp+קפיצה בתנאי, ו-add/inc קבוע ממש לפני החזרה לבדיקה. ב-while האתחול והעדכון פשוט לא קיימים כתבנית סדורה.
שאלה 8 · כמה פעמים ירוץ גוף הלולאה?
mov DWORD PTR [ebp-4], 0 .L_CHECK: cmp DWORD PTR [ebp-4], 5 jge .L_END ; body add DWORD PTR [ebp-4], 1 jmp .L_CHECK .L_END:
- א. 5 פעמים (i = 0..4)
- ב. 6 פעמים (i = 0..5)
- ג. 4 פעמים (i = 1..4)
- ד. אינסוף — הלולאה לא מסתיימת
המונה מתחיל ב-0, הלולאה יוצאת כש-i ≥ 5, והעדכון הוא +1 בכל סיבוב. כלומר הגוף רץ עבור 0,1,2,3,4 — חמש פעמים, בדיוק כמו for (i=0; i<5; i++).
שאלה 9 · באסמבלי 32 ביט רואים [ebp-4], [ebp-8] ו-[ebp+8]. מה הם?
- א. [ebp-4] ו-[ebp-8] הם משתנים מקומיים, [ebp+8] הוא פרמטר של הפונקציה
- ב. כולם פרמטרים שהועברו לפונקציה
- ג. כולם משתנים גלובליים בזיכרון
- ד. [ebp+8] הוא כתובת החזרה של הפונקציה
ה-Stack גדל כלפי מטה: מתחת ל-EBP (היסטים שליליים) יושבים המשתנים המקומיים, ומעליו (היסטים חיוביים) הפרמטרים — כאשר [ebp+4] הוא ה-Return Address ו-[ebp+8] הפרמטר הראשון. 🎯 דגש למבחן: משתנה שניגשים אליו דרך EBP הוא מקומי או פרמטר; משתנה שניגשים אליו דרך כתובת סטטית קבועה (למשל ds:0x804a01c או סגמנט .data/.bss) הוא גלובלי — ההבחנה הזו היא מפתח לשחזור נכון של קוד C.
שאלה 10 · איזה קוד C משחזר את הקטע הבא?
mov DWORD PTR [ebp-4], 1 mov DWORD PTR [ebp-8], 1 .L_CHECK: cmp DWORD PTR [ebp-8], 4 jg .L_END mov eax, DWORD PTR [ebp-4] imul eax, DWORD PTR [ebp-8] mov DWORD PTR [ebp-4], eax add DWORD PTR [ebp-8], 1 jmp .L_CHECK .L_END:
- א. result = 1; for (i = 1; i <= 4; i++) result *= i;
- ב. result = 1; for (i = 1; i < 4; i++) result += i;
- ג. result = 0; while (i > 4) result *= i;
- ד. result = 1; for (i = 4; i > 0; i--) result *= i;
שני המשתנים מאותחלים ל-1, היציאה היא כש-i > 4 (כלומר התנאי הוא i ≤ 4), הכפל imul מצטבר לתוך [ebp-4] והמונה עולה ב-1 — זהו בדיוק חישוב עצרת של 4.