GammaCyber

מ-C לאסמבלי — Reverse Engineering בסיסי

Low-Level: C, Assembly ו-RE · 5–6 שעות

// סקירה

מ-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 שורה-שורה
🎯 קשר למיוני גאמא
שיעור זה מכין לשלבים: Black Box CTF בסדנה — קריאת Assembly של binary לא מוכר · Reverse Engineering (שיעור 44) — זיהוי Patterns בGhidra/IDA · שאלון ידע — שאלות על cmp/jne/jge, זיהוי if/loop מAssembly · Pwn — הבנת מה הקוד הפגיע עושה לפני הניצול
// פרק 1

כלי 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 פקודה
הרחבה Stripped Binaries — ניתוח ללא שמות ›

כשהאקר מפיץ תוכנה זדונית, הוא מפעיל strip binary — מסיר את כל שמות הפונקציות והמשתנים. במקום <main> רואים <0x08048416> בלבד.

איך להתמודד? הופכים לבלשים: (1) מחפשים Strings עם strings binary | grep -i pass ועוקבים מי משתמש בהן. (2) מזהים קריאות לספריות מוכרות — CALL printf, CALL socket → מסיקים מה הפונקציה עושה. (3) מחפשים Magic Numbers (0x539, 0xDEADBEEF) כרמזים.

# ─── Stripped Binary: ניתוח ───────────────────────────── $ strings malware.bin | grep -i "password\|flag\|admin" $ objdump -d malware.bin | grep -A5 "call.*printf" # חפש: CALL printf → מה הארגומנט? → מה הפונקציה עושה!
✏תרגול עצמי3 שאלות
שאלה 1.1 — gcc -O0
למה מקמפלים עם -O0 כשמתרגלים Reverse Engineering?
✓ תשובה: ד) — -O0 = Zero Optimization. הקומפיילר לא "חכם" — כל שורת C הופכת לפקודות Assembly ישירות. ב--O2: הקומפיילר ממזג, מסיר, וישנה לוגיקה — Assembly קשה הרבה יותר לפענוח. לכן ב-RE אנחנו תמיד מתחילים עם -O0 לתרגול.
שאלה 1.2 — objdump
מה עושה הפקודה objdump -d -M intel secret.bin?
✓ תשובה: ד) — objdump -d = Disassemble. -M intel = Intel Syntax (MOV dest, src — קריא יותר מ-AT&T Syntax). הפלט: 08048416: 55 push ebp. ה-strings command מציג strings, לא Assembly.
שאלה 1.3 — Stripped Binary
פתחת binary ב-objdump ובמקום <main> רואים רק <0x08048416>. מה קרה?
✓ תשובה: ב) strip — strip binary מסיר Symbol Table. ה-RE נהיה "בלש": מחפש strings (strings binary), מזהה API calls (printf, socket, strcmp), ומסיק מהן מה הפונקציה האלמונית עושה. זהו ה-Skill האמיתי של Reverse Engineering.
// פרק 2

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 כשהתנאי שגוי.

כלל: תמיד כתוב את ה-Jcc ואז כתוב את ההפך שלו כתנאי ה-C. JNE → אתה כותב ==. JGE → אתה כותב <.
✏תרגול עצמי3 שאלות
שאלה 2.1 — היפוך לוגיקה
ב-Assembly רואים: cmp eax, 10 jge .skip ; ... block ... .skip:מה קוד ה-C המקביל?
✓ תשובה: ב) if (x < 10) — jge = Jump if Greater or Equal. אם x≥10 → קופץ ל-.skip → הblock לא רץ. הblock רץ רק כשx<10. לכן קוד ה-C הוא if (x < 10) { block }. כלל: jge → תכתוב <. jle → תכתוב >. jne → תכתוב ==. תמיד ההפך!
שאלה 2.2 — Dry Run: if/else
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 המשוחזר?
✓ תשובה: ד) if (x < y) { x = y; } else { y++; } — x=5, y=10. jge .else כשx≥y → x=5<y=10 → jge לא קופץ → Fall-through: x=y=10. בסיום: x=10, y=10. אם היה x=15≥10 → jge קופץ ל-.else → y++ → y=11. זהו הפתרון לשאלה 2 בתרגול המקורי של השיעור!
שאלה 2.3 — Assembly → C: זיהוי השמה
mov DWORD PTR [ebp-4], 0x539 cmp DWORD PTR [ebp-4], 0x539 jne .fail ; ... success code ... .fail:מה הפרמטר ש-C בדק, ומהו הערך שמייצג 0x539?
✓ תשובה: ג) — 0x539: 5×256 + 3×16 + 9 = 1280 + 48 + 9 = 1337. קוד C: if (passcode == 1337) { success; }. הקומפיילר מתרגם ל: cmp passcode, 0x539 + jne .fail. זה בדיוק הדוגמה מהתרגול המעשי בשיעור — מספר הpasscode מופיע כ-hex בAssembly!
// פרק 3

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 מובנה
הרחבה Do-While — סדר שונה של בדיקה ›

ב-do { body } while(cond) הbody רץ תמיד פעם אחת לפחות. ב-Assembly: body קודם, ואז בדיקה + קפיצה אחורה. ב-while רגיל: בדיקה קודם.

; do { count--; } while (count > 0); .L_BODY: sub DWORD PTR [ebp-4], 1 ; body קודם! cmp DWORD PTR [ebp-4], 0 ; ואז בדיקה jg .L_BODY ; קפיצה אחורה ; ─── vs while ───────────────────────────────────────────── ; while: בדיקה → (אם כן) body → קפיצה אחורה ; do-while: body → בדיקה → (אם כן) קפיצה אחורה ; הבדל: ב-do-while אין JMP ראשוני לL_CHECK
✏תרגול עצמי3 שאלות
שאלה 3.1 — זיהוי לולאה
מה הסימן המובהק ביותר ללולאה ב-Assembly?
✓ תשובה: ב) קפיצה אחורה — בIF ובשאר הקוד: קפיצות תמיד קדימה (לכתובת גבוהה יותר = דילוג מעל בלוק). קפיצה לכתובת נמוכה יותר = חזרה = לולאה. זהו הPattern הכי מהיר לזיהוי בGhidra/IDA גם — ב-Function Graph רואים חץ ש"חוזר" למעלה.
שאלה 3.2 — For vs While
איך מזהים שלולאה היא for ולא while פשוטה?
✓ תשובה: ב) — Pattern ייחודי של for: (1) לפני הלולאה: mov [ebp-4], 0 — אתחול counter. (2) בגוף, לפני הקפיצה אחורה: add [ebp-4], 1 או inc [ebp-4] — increment. לולאת while ללא counter לא תראה increment קבוע.
שאלה 3.3 — Dry Run: Fill in the Blanks
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 המקביל?
✓ תשובה: ד) — mov ... 10 = count=10. jmp .L_CHECK = מיד קופץ לבדיקה (לא ל-body — Pattern של while). cmp ... 0 + jg .L_BODY = אם count>0 חוזר לbody. ההיפוך של jg הוא ≤ — כלומר הלולאה רצה כשcount>0. sub ... 1 = count=count-1. זהו הפתרון לתרגול fill-in-the-blanks בשיעור!
// פרק 4

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) }
🎯 טבלת Jcc → C Operator (לשמור בראש!)
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)
✏תרגול עצמי3 שאלות
שאלה 4.1 — מיפוי משתנים
ב-Assembly (32bit) רואים: [ebp-4], [ebp-8], [ebp+8]. מה הם?
✓ תשובה: ד) — Stack frame ב-32bit: ebp מצביע לבסיס הframe. [ebp-N] = local variables (N=4,8,12... כל int = 4 bytes). [ebp+8] = arg1 (מעבר ה-Push לפני CALL). [ebp+12] = arg2. [ebp+4] = return address (שמור אוטומטית). מפה זו חיונית לDecompilation!
שאלה 4.2 — Decompilation מלא
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 המשוחזר?
✓ תשובה: ב) for (i=0; i<10; i++) — Pattern של for: (1) mov i=0 לפני. (2) cmp i,10 + jge .END = תנאי i<10 (ההפך של jge). (3) add i,1 בגוף = i++. (4) jmp .L_CHECK = קפיצה אחורה. הloop רץ עבור i=0,1,2,...,9 (10 פעמים). i=10 → jge → יציאה.
שאלה 4.3 — גאמא: Decompilation תחת לחץ
; 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)?
✓ תשובה: ג) — ניתוח: eax = arg1. cmp eax, 0 + je .L1 → אם x==0 קופץ ל-0x50f (mov eax,0 → return 0). אם x≠0 → neg eax = eax = -eax → jmp .end → return -x. בC: return (x == 0) ? 0 : -x;. שים לב: neg 0 = 0 כך שהתנאי המיוחד לx=0 לא הכרחי, אבל הקומפיילר הוסיף אותו.
// אתגר מעשי

אתגר מעשי — Decompilation — קריאת Assembly

שיא השיעור: היפוך, ניתוח וזיהוי — כלי ה-RE וה-Low-Level בפעולה. ההגשה נבדקת אוטומטית.

T3 · בינוני300 נק'~18 דק'analysis
Decompilation — מה מחשב הקוד?
ה-Assembly הבא מקבל n ב-rdi. עקבו אחריו והבינו מה הוא מחשב. עבור n=6 (rdi=6), מהו הערך הסופי ב-rax? הגישו מספר עשרוני. ; rdi = n (הקלט) xor eax, eax ; sum = 0 xor ecx, ecx ; i = 0 .loop: cmp ecx, edi jg .done ; אם i > n -> יציאה add eax, ecx ; sum += i inc ecx ; i++ jmp .loop .done: ; rax = התוצאה
דוגמאות
(פורמט) → מספר, למשל 21
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס Decompilation — מה מחשב הקוד? ›
הלולאה סוכמת i מ-0 עד n כולל → סכום משולשי. עבור n=6: 0+1+2+3+4+5+6 = 21.
// פרק 5

סיכום + Dry Run סגנון גאמא

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

✓מה למדנו בשיעור 29
  • 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 — שורה-שורה, בזמן ריצה
🎯 השיעור הבא
שיעור 30 — Buffer Overflows: Stack Smashing, Return Address Overwrite
✏תרגול סיום — גאמא סגנון3 שאלות
גאמא סגנון א׳ — זיהוי Pattern מהיר
ב-objdump רואים ב-0x8048430: jmp 0x8048410. ה-jmp מצביע לכתובת נמוכה יותר. מה זה?
✓ תשובה: ב) לולאה — 0x8048410 < 0x8048430 → קפיצה "אחורה" בזיכרון. זה הPattern האוניברסלי ללולאה ב-Assembly. ב-IDA/Ghidra Function Graph: נראה חץ שחוזר למעלה בblock. ב-objdump: JMP/Jcc לכתובת נמוכה = לולאה תמיד.
גאמא סגנון ב׳ — Decompilation מהיר
cmp eax, ebx jne .L_else ; block A jmp .L_end .L_else: ; block B .L_end:מה מבנה ה-C?
✓ תשובה: ג) if (eax == ebx) — jne = Jump if Not Equal = קופץ כשeax≠ebx → .L_else (Block B). אם eax==ebx → Fall-through לBlock A → jmp .L_end. בC: if (eax == ebx) { A } else { B }. הכלל: jne → כתוב == (ההפך).
גאמא סגנון ג׳ — Assembly ל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מה הפונקציה עושה?
✓ תשובה: ג) factorial — result=1, i=1. לולאה: i≤arg1 (jg = קופץ כשi>arg1). גוף: result*=i, i++. בC: int fact(int n) { int r=1; for(int i=1; i<=n; i++) r*=i; return r; }. זיהוי: imul = כפל מצטבר, i עולה ב-1 כל פעם, result מוכפל — factorial! Pattern קלאסי.
שאלות תרגול ותשובות (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.