מעבדת Low-Level מסכמת
מעבדת Low-Level מסכמת — ניתוח Assembly ללא קוד מקור
השיעור האחרון ביחידה 4. כאן תרכוש מתודולוגיה לניתוח קטעי Assembly ארוכים תחת לחץ זמן — בדיוק כמו בסדנת גאמא האמיתית. Function Call Chains, Recursive Functions, אלגוריתמי מיון, ופעולות מחרוזת — הכל Assembly גולמי.
- מתודולוגיה 5 שלבים — ניתוח 50+ שורות ב-10 דקות
- Function Call Chains — ניתוח Bottom-Up של 3 פונקציות קשורות
- Recursive Function ב-Assembly — זיהוי ו-Dry Run
- Bubble Sort ו-Selection Sort — זיהוי Pattern מהקוד
- strlen, strcmp, strcpy — זיהוי פונקציות מחרוזת מה-Assembly
- 4 תרגילי RE מלאים — Fibonacci, String Reverse, Collatz, count positive/negative
- טכניקות לחץ זמן — 30 שניות לתמונה כללית
מתודולוגיה — ניתוח קטע ארוך תחת לחץ
הסדנה האמיתית של גאמא נותנת קטע Assembly ושואלת שאלות עליו. אין אינטרנט, לפעמים אין מחשב — רק עט ונייר. פרק זה נותן את השיטה לפצח 50+ שורות ב-5-10 דקות.
1.1 — 5 שלבי ניתוח מהיר
שלב 1: סריקה ראשונית — 30 שניות
→ כמה פונקציות? (חפשו: push ebp / mov ebp,esp)
→ יש CALLים לפונקציות?
→ יש JMP אחורה? (= לולאה)
→ כמה args? (ספרו PUSHים לפני CALL)
שלב 2: זיהוי גבולות פונקציות
→ כל push ebp + mov ebp,esp = תחילת פונקציה
→ כל ret = סיום פונקציה
→ תנו שם זמני: func_A, func_B, func_C
שלב 3: ניתוח Bottom-Up — מהפנימי לחיצוני
→ נתחו קודם פונקציות שלא קוראות לאחרות (עלים)
→ ואז עלו לפונקציות שקוראות להן
שלב 4: Dry Run מדויק על פונקציה אחת
→ טבלת רגיסטרים: EAX, EBX, ECX, EDX, ESP, EBP, FLAGS
→ טבלת Stack: מה בכל כתובת
שלב 5: תרגום ל-C ומענה לשאלות
→ כתבו C מקביל בצד — עוזר לאמת הבנה1.2 — Calling Signature: ניחוש מה הפונקציה עושה לפני שקוראים אותה
| Signature | ניחוש ראשוני | Pattern לאמת |
|---|---|---|
| f(ptr, int) | פעולה על מערך/מחרוזת בגודל N | mov + loop over [ptr+i] |
| f(ptr, ptr) | השוואה או העתקה בין שניים | cmp byte [p1], [p2] + loop |
| f(int) → int | חישוב מתמטי על מספר | imul/add/sub + ret |
| f(int) → int + CALL עצמה | רקורסיה! | cmp + jle base + call self |
| f(ptr, ptr, int) | memcpy / strncpy סגנון | mov byte + loop + counter |
Function Call Chains — מעקב אחרי שרשרת פונקציות
בקוד אמיתי, פונקציות קוראות לפונקציות. כדי להבין את התוכנית, חייבים לעקוב אחרי השרשרת Bottom-Up — קודם פנימיות, אחר כך חיצוניות.
2.1 — קוד לניתוח: 3 פונקציות קשורות
; ── func_C (הפנימית ביותר):
func_C:
push ebp / mov ebp, esp
mov eax, [ebp+8] ; a
mov ecx, [ebp+12] ; b
cmp eax, ecx
jle .c_swap
sub eax, ecx ; a > b: return a-b
jmp .c_done
.c_swap:
mov eax, ecx
sub eax, [ebp+8] ; a <= b: return b-a
.c_done:
pop ebp / ret
; ── func_B (אמצעית):
func_B:
push ebp / mov ebp, esp / sub esp, 4
mov eax, [ebp+8] ; x
imul eax, eax ; x*x
mov [ebp-4], eax ; local = x*x
push [ebp+12] ; push y
push [ebp-4] ; push x*x
call func_C ; func_C(x*x, y)
add esp, 8
leave / ret
; ── func_A (החיצונית):
func_A:
push ebp / mov ebp, esp / sub esp, 8
push [ebp+12] ; push b
push [ebp+8] ; push a
call func_B ; func_B(a, b)
add esp, 8
mov [ebp-4], eax ; local1 = func_B(a,b)
push [ebp+16] ; push c
push [ebp-4] ; push local1
call func_B ; func_B(local1, c)
add esp, 8
mov [ebp-8], eax ; local2
mov eax, [ebp-8]
leave / ret2.2 — ניתוח Bottom-Up
שלב 1: func_C
→ a>b: return a-b
→ a<=b: return b-a
→ זוהי: |a - b| (ערך מוחלט של הפרש)
שלב 2: func_B(x, y)
→ local = x*x
→ קוראת func_C(x*x, y) = |x²-y|
→ זוהי: f(x,y) = |x²-y|
שלב 3: func_A(a, b, c)
→ local1 = func_B(a,b) = |a²-b|
→ local2 = func_B(local1,c) = ||a²-b|²-c|
→ זוהי: g(a,b,c) = ||a²-b|²-c|
בדיקה: g(2,3,5) = ||4-3|²-5| = |1-5| = 42.3 — Dry Run: func_A(2, 3, 5)
| שלב | EAX | [ebp-4] | [ebp-8] | הסבר |
|---|---|---|---|---|
| func_B(2,3) → x² | 4 | — | — | 2²=4 |
| func_C(4,3) | 1 | — | — | |4-3|=1 |
| local1=1 | 1 | 1 | ? | שמור תוצאה |
| func_B(1,5) → x² | 1 | — | — | 1²=1 |
| func_C(1,5) | 4 | — | — | |1-5|=4 |
| local2=4, return | 4 | 1 | 4 | תשובה: 4 |
func_B(3,5): 3²=9, |9-5|=4 → local1=4
func_B(4,2): 4²=16, |16-2|=14 → local2=14
func_A(3,5,2) = 14
g(a,b,c) = ||a²-b|²-c| = ||9-5|²-2| = |16-2| = 14
Recursive Function ב-Assembly — זיהוי וניתוח
רקורסיה ב-Assembly מזוהה על ידי סימן אחד בלבד: CALL לעצמה. ברגע שמזהים CALL לשם הפונקציה הנוכחית — מיד יודעים שמדובר ברקורסיה, ומחפשים את ה-Base Case.
3.1 — קוד לניתוח: פיבונאצ'י רקורסיבי
mystery_recursive:
push ebp / mov ebp, esp / sub esp, 4
; ── Base Case 1: n==0 ─────────────────────
cmp DWORD PTR [ebp+8], 0 ; (A)
jne .not_base
mov eax, 0 ; (C) return 0
jmp .done
.not_base:
; ── Base Case 2: n==1 ─────────────────────
cmp DWORD PTR [ebp+8], 1 ; (D)
jne .recurse
mov eax, 1 ; (F) return 1
jmp .done
.recurse:
; ── f(n-1) ───────────────────────────────
mov eax, [ebp+8]
sub eax, 1
push eax
call mystery_recursive ; ← CALL לעצמה!
add esp, 4
mov [ebp-4], eax ; local = f(n-1)
; ── f(n-2) ───────────────────────────────
mov eax, [ebp+8]
sub eax, 2
push eax
call mystery_recursive ; ← שוב CALL לעצמה!
add esp, 4
add eax, [ebp-4] ; EAX = f(n-2) + f(n-1)
.done:
leave / ret3.2 — ניתוח
שתי קריאות רקורסיביות + חיבור תוצאות + Base Cases ב-0 ו-1 = סדרת פיבונאצ'י!
// C מקביל:
int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n-1) + fib(n-2);
}3.3 — Dry Run: fib(4)
| קריאה | n | f(n-1) | f(n-2) | תוצאה |
|---|---|---|---|---|
| fib(4) | 4 | fib(3) | fib(2) | ? |
| fib(3) | 3 | fib(2) | fib(1) | ? |
| fib(2) | 2 | fib(1) | fib(0) | ? |
| fib(1) | 1 | — | — | 1 (Base) |
| fib(0) | 0 | — | — | 0 (Base) |
| fib(2) חוזר | 2 | 1 | 0 | 1 |
| fib(1) | 1 | — | — | 1 (Base) |
| fib(3) חוזר | 3 | 1 | 1 | 2 |
| fib(2) חוזר | 2 | — | — | 1 |
| fib(4) חוזר | 4 | 2 | 1 | 3 |
Sorting ו-String Operations ב-Assembly
אלגוריתמי מיון ופעולות מחרוזת הם מהמבנים הכי שכיחים בשאלות RE. לכל אחד יש Pattern מוכר שאפשר לזהות תוך שניות.
4.1 — Bubble Sort — Pattern זיהוי
✓ השוואה arr[j] עם arr[j+1] (שכן ישיר!)
✓ SWAP: 3 MOVים (tmp=a; a=b; b=tmp) בתוך הלולאה הפנימית
✓ inner loop עד n-i-1 (קצרה יותר בכל סיבוב חיצוני)
4.2 — Selection Sort — הבדל מ-Bubble
✓ לולאה פנימית מחפשת min_idx — לא מחליפה!
✓ SWAP קורה פעם אחת בלבד בסוף הלולאה החיצונית
| תכונה | Bubble Sort | Selection Sort |
|---|---|---|
| מה מושווה | arr[j] עם arr[j+1] | arr[j] עם arr[min_idx] |
| מתי SWAP | בכל השוואה שמוצאת גדול | פעם אחת לסיבוב חיצוני |
| סימן מזהה | jle .no_swap בפנים | min_idx משתנה, swap בסוף |
4.3 — String Operations — Pattern לכל פונקציה
| פונקציה | סימן Assembly | מחזיר | סיכון |
|---|---|---|---|
| strlen | MOVZX byte + TEST + JZ עד null | אורך (int) | אין |
| strcmp | 2x MOVZX + CMP + JNE | 0 / pos / neg | אין |
| strcpy | MOVZX read + MOV BYTE write | dst pointer | Buffer Overflow! |
; strlen — Pattern:
.loop:
movzx ecx, BYTE PTR [edx+eax] ; ECX = s[i]
test ecx, ecx ; s[i] == 0?
jz .done ; כן → סיום
add DWORD PTR [ebp-4], 1 ; i++
jmp .loop
; מחזיר: EAX = מונה
; strcpy — Pattern (שים לב לכתיבה!):
.loop:
movzx ecx, BYTE PTR [edx+eax] ; READ מ-src
mov BYTE PTR [edx+eax], cl ; WRITE ל-dst ← כתיבה!
test ecx, ecx
jz .done
add DWORD PTR [ebp-4], 1
jmp .loopתרגילי RE מלאים — סימולציית סדנה
4 תרגילים מלאים, מדורגים, כפי שמופיעים בסדנת גאמא. לכל אחד: קטע Assembly, שאלות, ופתרון מלא. השתמשו בשיטת 5 השלבים.
תרגיל א׳ — Fibonacci Iterative (★★★☆☆)
func_ex1:
push ebp / mov ebp, esp / sub esp, 8
mov DWORD PTR [ebp-4], 1 ; (1) a = 1
mov DWORD PTR [ebp-8], 1 ; (2) b = 1
cmp DWORD PTR [ebp+8], 1
jle .done ; n<=1: return a
mov ecx, [ebp+8]
sub ecx, 1 ; (5) counter = n-1
.loop:
cmp ecx, 0
jle .done
mov eax, [ebp-4]
add eax, [ebp-8] ; tmp = a + b
; b = a (שני שלבים):
mov [ebp-8], [ebp-4]
mov [ebp-4], eax ; a = tmp
dec ecx
jmp .loop
.done:
mov eax, [ebp-4]
leave / reta=1,b=1 → a=2,b=1 → a=3,b=2 → a=5,b=3 → a=8,b=5 → a=13,b=8. תשובה: EAX=13=fib(7).
תרגיל ב׳ — String Reverse (★★★★☆)
func_ex2: ; (s, len)
push ebp / mov ebp, esp / sub esp, 8
mov DWORD PTR [ebp-4], 0 ; i = 0
mov eax, [ebp+12]
sub eax, 1
mov DWORD PTR [ebp-8], eax ; j = len-1
.loop:
mov eax, [ebp-4] ; i
mov ecx, [ebp-8] ; j
cmp eax, ecx
jge .done ; i >= j? סיים
; החלפת s[i] ↔ s[j]:
mov edx, [ebp+8]
movzx eax, BYTE PTR [edx + [ebp-4]] ; AL = s[i]
movzx ecx, BYTE PTR [edx + [ebp-8]] ; CL = s[j]
mov BYTE PTR [edx + [ebp-4]], cl ; s[i] = s[j]
mov BYTE PTR [edx + [ebp-8]], al ; s[j] = s[i]
add DWORD PTR [ebp-4], 1 ; i++
sub DWORD PTR [ebp-8], 1 ; j--
jmp .loop
.done:
leave / retתרגיל ג׳ — Collatz Conjecture (★★★★★)
func_helper: ; (n) → n/2 or 3n+1
push ebp / mov ebp, esp
mov eax, [ebp+8]
test eax, 1 ; n זוגי?
jnz .odd
sar eax, 1 ; זוגי: n/2 (SAR = /2 signed)
jmp .done
.odd:
imul eax, 3
add eax, 1 ; אי-זוגי: 3n+1
.done:
pop ebp / ret
func_main: ; (n) → מספר צעדים עד 1
push ebp / mov ebp, esp / sub esp, 8
mov eax, [ebp+8]
mov [ebp-4], eax ; current = n
mov DWORD PTR [ebp-8], 0 ; steps = 0
.loop:
cmp DWORD PTR [ebp-4], 1
je .done ; current == 1? סיים
push [ebp-4]
call func_helper
add esp, 4
mov [ebp-4], eax ; current = helper(current)
add DWORD PTR [ebp-8], 1 ; steps++
jmp .loop
.done:
mov eax, [ebp-8] ; return steps
leave / retתרגיל ד׳ — ספירת חיוביים/שליליים (★★★★★)
mystery_full: ; (arr, len) → count_pos - count_neg
push ebp / mov ebp, esp / sub esp, 12
mov ecx, [ebp+12] ; len
mov DWORD PTR [ebp-4], 0 ; count_pos
mov DWORD PTR [ebp-8], 0 ; count_neg
mov DWORD PTR [ebp-12], 0 ; i
.loop:
cmp [ebp-12], ecx
jge .done
mov eax, [ebp-12]
mov edx, [ebp+8]
mov eax, [edx + eax*4] ; arr[i]
test eax, eax
jz .next ; == 0: skip
js .negative ; SF=1: שלילי
add DWORD PTR [ebp-4], 1 ; count_pos++
jmp .next
.negative:
add DWORD PTR [ebp-8], 1 ; count_neg++
.next:
add DWORD PTR [ebp-12], 1 ; i++
jmp .loop
.done:
mov eax, [ebp-4]
sub eax, [ebp-8] ; return count_pos - count_neg
leave / retאתגר מעשי — RE — Crackme רקורסיבי ורקורסיה
שיא השיעור: היפוך, ניתוח וזיהוי — כלי ה-RE וה-Low-Level בפעולה. ההגשה נבדקת אוטומטית.
✓ מה למדנו בשיעור 30
- מתודולוגיה 5 שלבים: סריקה → גבולות → Bottom-Up → Dry Run → תרגום C
- Function Chains: נתחו Bottom-Up — קודם פנימיות, אחר כך חיצוניות
- Recursive ASM: סימן = CALL לעצמה. 2 CALLים = Fibonacci/Tree
- Bubble Sort: שתי לולאות + SWAP בכל השוואה + j עד n-i-1
- Selection Sort: שתי לולאות + min_idx + SWAP פעם אחת בסוף
- strlen: MOVZX byte + TEST + JZ עד null — מחזירה מונה
- strcmp: שני MOVZX + CMP + JNE — מחזירה 0/pos/neg
- strcpy: MOVZX read + MOV BYTE write — כותבת! סיכון Buffer Overflow
- Quick Patterns: 30 שניות — ספרו push ebp, JMP אחורה, CALL לעצמה
- כשתקועים: ענו חלקית, Dry Run לn קטן, זהו מה זה אינו
שיעור 31: מבוא ללינוקס CLI — ניווט, קבצים והרשאות
שאלות תרגול ותשובות (10)
שאלה 1 · איך מזהים במהירות פונקציה רקורסיבית בפלט objdump?
- א. היא מכילה CALL לכתובת של עצמה
- ב. היא משתמשת רק ברגיסטרים ולא ב-Stack
- ג. אין בה פקודת RET
- ד. היא מתחילה בלולאת jmp אחורה
רקורסיה = פונקציה שקוראת לעצמה, ולכן ב-Disassembly רואים CALL שהיעד שלו הוא כתובת ההתחלה של אותה פונקציה. לולאה רגילה משתמשת ב-jmp, לא ב-CALL.
שאלה 2 · מה מקבלים מניתוח Bottom-Up של שרשרת קריאות?
- א. מבינים קודם מה עושות פונקציות העלה הפשוטות, ואז מרכיבים את התמונה הכוללת
- ב. חוסכים את הצורך לקרוא את הפונקציה הראשית
- ג. מגלים את שמות הפונקציות גם בקובץ Stripped
- ד. מזהים מיד פגיעויות אבטחה
הפונקציות התחתונות בשרשרת קצרות וברורות. אחרי שמפענחים אותן, כל CALL אליהן הופך למשמעות ידועה, והפונקציה הקוראת נקראת הרבה יותר בקלות.
שאלה 3 · מה מחזירה הפונקציה עבור הקריאה fib(4)?
fib(n): if (n < 2) return n; return fib(n-1) + fib(n-2);
- א. 3
- ב. 4
- ג. 5
- ד. 2
fib(4) = fib(3)+fib(2) = (fib(2)+fib(1)) + (fib(1)+fib(0)) = (1+1) + (1+0) = 3. בסדרה 0,1,1,2,3 — האיבר הרביעי הוא 3.
שאלה 4 · איזה Pattern מאפיין Bubble Sort באסמבלי?
- א. שתי לולאות מקוננות עם השוואה והחלפה של איברים שכנים
- ב. לולאה אחת שסורקת ומוצאת מינימום, ואז החלפה אחת בסוף
- ג. קריאה רקורסיבית שמחלקת את המערך לשניים
- ד. לולאה שמעתיקה את המערך למקום חדש
Bubble Sort מבצע השוואה בין arr[j] ל-arr[j+1] ומחליף מיד — ולכן רואים בלולאה הפנימית cmp על שתי כתובות סמוכות ורצף של mov לביצוע ההחלפה.
שאלה 5 · מהו ההבדל המזהה בין Selection Sort ל-Bubble Sort באסמבלי?
- א. ב-Selection ההחלפה מתבצעת פעם אחת בסוף כל מעבר, אחרי שנמצא המינימום
- ב. ב-Selection אין לולאות מקוננות כלל
- ג. ב-Bubble אין פקודת cmp
- ד. ב-Selection המערך נסרק מהסוף להתחלה
שני האלגוריתמים בנויים משתי לולאות, אבל ב-Selection הלולאה הפנימית רק מעדכנת אינדקס מינימום, וההחלפה (רצף ה-mov) מופיעה מחוץ ללולאה הפנימית — פעם אחת בכל מעבר.
שאלה 6 · איזה Pattern מזהה את הפונקציה strlen באסמבלי?
- א. לולאה שמתקדמת בבייט אחד בכל סיבוב ועוצרת כשהערך הנקרא הוא 0
- ב. לולאה שמעתיקה בייטים ממקום למקום
- ג. השוואה בין שני מצביעים עד שהם שווים
- ד. חישוב הפרש בין שתי כתובות בלי לולאה
strlen פשוט סופר עד ה-Null Terminator: קריאת בייט, בדיקה מול 0, קידום מונה. הפסקת הלולאה על ערך 0 היא החתימה הכי ברורה שלה.
שאלה 7 · ראית קריאה ל-strcpy בפונקציה שמעתיקה קלט משתמש לתוך מערך מקומי. למה זה מסוכן?
- א. strcpy לא בודקת את גודל היעד ותמשיך לכתוב עד ה-Null Terminator של המקור
- ב. strcpy איטית יותר מהעתקה ידנית בלולאה
- ג. strcpy מוסיפה תווים אקראיים לסוף המחרוזת
- ד. strcpy עובדת רק על מחרוזות באורך קבוע
strcpy מעתיקה עד שהיא פוגשת \0 במקור, בלי שום מושג מה גודל היעד — קלט ארוך מהחוצץ ידרוס את Saved EBP ואת ה-Return Address. 🎯 דגש למבחן: כשמחפשים Buffer Overflow, סרקו קודם את הפונקציות חסרות בדיקת האורך — strcpy, gets, sprintf, strcat. הגרסאות הבטוחות (strncpy, snprintf) מקבלות פרמטר גודל, ולכן באסמבלי יופיע push נוסף של קבוע הגודל לפני ה-CALL. היעדר הפרמטר הזה הוא הנורה האדומה.
שאלה 8 · בקובץ בינארי אין אף מחרוזת קריאה, אך התוכנית מדפיסה הודעות ברורות. מה ההסבר הסביר?
- א. המחרוזות מוצפנות ומפוענחות בזמן ריצה על ידי לולאת XOR קצרה
- ב. המחרוזות נמחקו מהקובץ בזמן הקימפול
- ג. ההודעות נוצרות באופן אקראי בכל הרצה
- ד. המחרוזות נטענות מהאינטרנט
זו טכניקת הסתרה נפוצה: המחרוזת שמורה כבייטים מעורבלים, ולפני השימוש בה רצה לולאה שמבצעת XOR מול מפתח ומחזירה את הטקסט המקורי. 🎯 דגש למבחן: את ה-Decryption Stub מזהים לפי לולאה קצרה מעל אזור נתונים שמכילה xor עם קבוע או עם בייט מפתח מתחלף. איתרו את הלולאה, קראו את המפתח, והריצו XOR ידנית על הבייטים — כך משחזרים את המחרוזת בלי להריץ את התוכנית.
שאלה 9 · לפי איזה סימן מזהים באסמבלי שפונקציה מקבלת מצביע ולא ערך?
- א. הפרמטר נטען לרגיסטר ואז ניגשים לזיכרון דרכו, למשל mov eax, [ebp+8] ואחריו mov edx, [eax]
- ב. הפרמטר נדחף ל-Stack בעזרת push
- ג. הפרמטר נשמר במשתנה מקומי
- ד. הפונקציה מחזירה ערך ב-EAX
גישה כפולה לזיכרון היא החתימה של מצביע: קודם קוראים את הכתובת עצמה מהפרמטר, ואז ניגשים לתוכן שבכתובת הזו. ערך רגיל נצרך ישירות בלי ה-Dereference השני.
שאלה 10 · איזה קוד C משחזר את הקטע הבא?
mov ecx, DWORD PTR [ebp+8] xor eax, eax .L_LOOP: cmp BYTE PTR [ecx], 0 je .L_END add eax, 1 add ecx, 1 jmp .L_LOOP .L_END:
- א. ספירת אורך מחרוזת — מעבר בייט-בייט עד ל-0 והחזרת המונה
- ב. העתקת מחרוזת מכתובת אחת לאחרת
- ג. השוואת שתי מחרוזות והחזרת ההפרש
- ד. היפוך סדר התווים במחרוזת
[ebp+8] הוא מצביע למחרוזת, EAX מאופס כמונה, והלולאה מתקדמת בבייט בכל סיבוב עד לתו \0. זהו מימוש קלאסי של strlen.