GammaCyber

Stack ופונקציות

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

// סקירה

Stack ופונקציות — Calling Conventions ו-Stack Frames

שיעור זה הוא המפתח להבנת Reverse Engineering ו-Exploitation. כאן תלמד בדיוק מה קורה כשפונקציה נקראת — מה נדחף לStack, איפה יושבים הארגומנטים, ואיך Return Address הוא המטרה בכל Buffer Overflow.

  • Stack Mechanics: כיוון גדילה, PUSH/POP, מה ESP תמיד מצביע
  • ESP ו-EBP: Stack Pointer לעומת Frame Pointer — תפקיד כל אחד
  • Prologue ו-Epilogue: שלושת השורות שכל פונקציה מתחילה ומסתיימת בהן
  • Stack Frame Layout: RetAddr, SavedEBP, Args, Locals — בדיוק באיזו כתובת
  • Calling Conventions: cdecl (Linux) לעומת stdcall (Windows API)
  • רקורסיה: Stack Frames נערמים — Stack Overflow
  • Stack Smashing: מה Buffer Overflow ולמה Return Address הוא המטרה
  • Stack Canary: ההגנה הקלאסית נגד Overflow
// פרק 1

Stack Mechanics — כיצד ה-Stack בנוי ועובד

ה-Stack הוא מבנה נתונים LIFO שחי ב-RAM ומנוהל אוטומטית על ידי המעבד. כדי להבין Buffer Overflow ו-Calling Conventions — חייבים להבין תחילה איך ה-Stack עובד ברמה הפיזית.

1.1 — מיקום ה-Stack בזיכרון

ל-Stack מאפיין ייחודי: הוא גדל כלפי מטה — כלומר לכתובות קטנות יותר.

כתובותאזורתוכןכיוון גדילה
HighStackפרמטרים, כתובות חזרה, משתנים מקומיים↓ גדל למטה
—Heapזיכרון דינמי (malloc/new)↑ גדל למעלה
—BSSמשתנים גלובליים לא מאותחלים—
—Data Segmentמשתנים גלובליים מאותחלים—
LowText Segmentקוד התוכנית (Read-Only)—
אנלוגיה: Stack כ'ערמת צלחות' הרחבה▶

מסעדה שמחזיקה ערמת צלחות נקיות — כל צלחת נוספת מונחת על הקודמת. הצלחת שנלקחת ראשונה היא תמיד זו שנוספה אחרונה (LIFO). ב-Stack של תוכנית: כשדוחפים נתון (PUSH), הוא מגיע לראש. כשמוציאים (POP), לוקחים מהראש. הבדל חשוב: ב-x86 הערמה גדלה מטה — PUSH מוריד את ESP (כתובת נמוכה יותר).

1.2 — ESP: מצביע ה-Stack

ESP (Extended Stack Pointer) מצביע תמיד לראש ה-Stack — לכתובת הנמוכה ביותר שבה יש נתון. כל PUSH מוריד את ESP ב-4, וכל POP מעלה אותו ב-4.

; מצב התחלתי: ESP = 0x1000 push eax ; ESP = 0x0FFC; memory[0x0FFC] = EAX push ebx ; ESP = 0x0FF8; memory[0x0FF8] = EBX push ecx ; ESP = 0x0FF4; memory[0x0FF4] = ECX ; Stack עכשיו: ; 0x1000 | ... (מעל ה-Stack) ; 0x0FFC | EAX ← (ראשון שנדחף) ; 0x0FF8 | EBX ; 0x0FF4 | ECX ← ESP מצביע כאן (ראש) pop edx ; EDX = ECX; ESP = 0x0FF8 ; ⚠️ POP לא מוחק נתונים — רק מעלה את ESP!
!כלל ברזל: אל תשנו ESP ידנית!▶

ESP מנוהל אוטומטית על ידי PUSH, POP, CALL, RET. שינוי ידני של ESP ישבש את מבנה ה-Stack.

היוצאים מן הכלל: ADD ESP, N לניקוי Stack לאחר CALL (Calling Convention), ו-SUB ESP, N להקצאת מקום למשתנים מקומיים ב-Prologue.
✏תרגול — חימום Stack3 שאלות
שאלה 1
מה קורה ל-ESP כאשר מבצעים POP EAX?
✓ תשובה: ב) — POP = קרא מ-[ESP] ל-EAX, ואז ESP+=4. הנתון נשאר בזיכרון כ"זבל" — רק המצביע זז.
שאלה 2
מה עושה הפקודה CALL func מאחורי הקלעים?
✓ תשובה: ג) — CALL = PUSH [כתובת הבאה] + JMP לפונקציה. כתובת זו היא ה-Return Address שתשמש בסוף ב-RET.
שאלה 3 — Dry Run
mov eax, 10 mov ebx, 20 push eax push ebx pop eax pop ebxמה יהיו הערכים ב-EAX וב-EBX בסיום?
✓ תשובה: ד) EAX=20, EBX=10 — LIFO! דחפנו 10 ואז 20. pop eax שולף 20 (האחרון). pop ebx שולף 10. הערכים הוחלפו!
// פרק 2

Stack Frame — 'הבית' של כל פונקציה

כל פעם שפונקציה נקראת, היא מקבלת 'בית' משלה ב-Stack — Stack Frame. זה המקום שבו הפונקציה שומרת ארגומנטים, משתנים מקומיים, וכתובת החזרה.

2.1 — EBP: Frame Pointer

EBP (Extended Base Pointer) מצביע לבסיס ה-Stack Frame הנוכחי. בניגוד ל-ESP שמשתנה כל הזמן, EBP קבוע לאורך כל ריצת הפונקציה.

; [EBP + N] → מעל EBP = ארגומנטים (N חיובי) ; [EBP - N] → מתחת ל-EBP = משתנים מקומיים (N שלילי) ; מבנה Stack Frame: ; ┌────────────────────────┐ כתובת גבוהה ; │ arg3 [EBP + 16] │ ; │ arg2 [EBP + 12] │ ; │ arg1 [EBP + 8] │ ; │ Return Address [EBP+4]│ ← כתובת חזרה ; │ Saved EBP [EBP + 0] │ ← EBP מצביע כאן ; │ local1 [EBP - 4] │ ; │ local2 [EBP - 8] │ ; └────────────────────────┘ ← ESP מצביע כאן (ראש)
💡 מדוע EBP קיים? — ה-Stack זזה, EBP לא!
בתוך פונקציה, ESP משתנה כל הזמן עם כל PUSH ו-POP. אם ניגשנו ל-[ESP+8] בתחילת הפונקציה, ואחר כך עשינו PUSH — ESP ירד ב-4 ו-[ESP+8] מצביע למקום שונה! EBP פותר זאת: הוא קבוע, [EBP+8] תמיד = arg1.

2.2 — Prologue: פתיחת פונקציה

כל פונקציה מתחילה ב-Prologue — שלוש שורות Assembly שמגדירות את ה-Stack Frame:

my_function: push ebp ; (1) שמור EBP של הקורא ב-Stack mov ebp, esp ; (2) EBP = ESP — קבע את בסיס ה-Frame sub esp, 12 ; (3) הקצה מקום ל-3 משתנים מקומיים ; לאחר Prologue: ; [EBP + 0] = EBP השמור ; [EBP + 4] = Return Address ; [EBP + 8] = arg1 ; [EBP + 12] = arg2 ; [EBP - 4] = local1 ; [EBP - 8] = local2 ; [EBP - 12] = local3

2.3 — Epilogue: סיום פונקציה

; שיטה 1: mov + pop mov eax, result ; ערך החזרה → EAX mov esp, ebp ; שחזר ESP לבסיס ה-Frame pop ebp ; שחזר EBP של הקורא ret ; POP כתובת חזרה → JMP לשם ; שיטה 2: LEAVE (קיצור מובנה) leave ; = mov esp,ebp + pop ebp ret
🔑 Prologue + Epilogue — Pattern לזיהוי ב-Disassembly
תחילת פונקציה: push ebp → mov ebp, esp → sub esp, N
סיום פונקציה: leave (או mov+pop) → ret
כל פעם שרואים push ebp + mov ebp,esp — זה תחילת פונקציה חדשה!
✏תרגול — Stack Frame3 שאלות
שאלה 1 — [EBP+N]
לפונקציה void test(int x, int y) שנקראת כ-test(5, 7) — מה הערך ב-[EBP+8]?
✓ תשובה: א) 5 — [EBP+0]=SavedEBP, [EBP+4]=RetAddr, [EBP+8]=arg1=x=5, [EBP+12]=arg2=y=7. [EBP-4]=local1 ו-[EBP-8]=local2.
שאלה 2 — Prologue
מה עושה sub esp, 16 ב-Prologue?
✓ תשובה: ד) — SUB ESP, N מוריד את ESP ב-N בתים, ומשריין חלל ריק ב-Stack לשימוש משתנים מקומיים. N=16 = מקום ל-4 int (כל אחד 4 bytes).
שאלה 3 — Bug Hunt
הפונקציה הבאה מקריסה תמיד ב-RET — מדוע?
my_add: push ebp mov ebp, esp mov eax, [ebp+8] add eax, [ebp+12] ret
✓ תשובה: ד) — הפונקציה דחפה EBP ב-Prologue, אבל שכחה לשחזרו (leave / mov esp,ebp + pop ebp) לפני RET. RET שולף את EBP השמור כ-"כתובת חזרה" ועושה JMP לכתובת שגויה → Crash.
// פרק 3

Calling Conventions — מי מנקה את ה-Stack?

Calling Convention הוא 'חוזה' בין הפונקציה הקוראת (Caller) לנקראת (Callee): איך מעבירים ארגומנטים, ומי מנקה את ה-Stack. ללא Convention קבוע, קוד שונה לא יכול לשתף פעולה.

3.1 — cdecl: Convention של Linux/GCC

כללcdecl — הפרטים
העברת ארגומנטיםדחיפה ל-Stack בסדר הפוך (arg האחרון נדחף ראשון)
מי מנקה Stackהקורא (Caller) — ADD ESP, N לאחר CALL
ערך החזרהב-EAX (32 ביט)
רגיסטרים שמוריםEBX, ESI, EDI, EBP — Callee חייב לשמרם
יתרוןתומך ב-Variadic functions (printf, scanf)
; cdecl — קריאה ל-add(3, 7) push 7 ; arg2 נדחף ראשון (סדר הפוך!) push 3 ; arg1 נדחף שני call add_func ; CALL = PUSH RetAddr + JMP add esp, 8 ; Caller מנקה: 2 args × 4 = 8 add_func: push ebp mov ebp, esp mov eax, [ebp+8] ; arg1 = 3 add eax, [ebp+12] ; arg2 = 7 → EAX = 10 pop ebp ret ; חזרה — Caller מנקה!

3.2 — stdcall: Convention של Windows API

כללstdcall — הפרטים
מי מנקה Stackהנקראת (Callee) — RET N (לא ADD ESP בצד הקורא)
Windows כתיב__stdcall בC, או WINAPI macro
חיסרוןאי-אפשר לעשות Variadic functions
; stdcall — קריאה ל-WinAPI push 0 ; arg4 push offset caption_str ; arg3 push offset message_str ; arg2 push 0 ; arg1: hWnd = NULL call MessageBoxA ; ⚠️ Caller לא מנקה! ; MessageBoxA עצמה עשתה: ret 16 = RET + ESP+=16

3.3 — System V AMD64 (64 ביט)

בקוד 64 ביט, ארגומנטים מועברים דרך רגיסטרים — לא Stack:

ConventionArg 1Arg 2Arg 3Arg 4Arg5+
System V AMD64 (Linux)RDIRSIRDXRCXStack
Microsoft x64 (Windows)RCXRDXR8R9Stack
🔑 חשוב לגאמא: איך לזהות Convention?
PUSH args לפני CALL + ADD ESP אחרי CALL → cdecl
PUSH args לפני CALL + אין ADD ESP אחרי CALL → stdcall
MOV RDI/RSI לפני CALL → System V AMD64 (Linux x64)
MOV RCX/RDX לפני CALL → Microsoft x64
✏תרגול — Calling Conventions3 שאלות
שאלה 1
מה ההבדל העיקרי בין cdecl ל-stdcall?
✓ תשובה: ב) — cdecl (Linux/GCC): Caller מנקה עם ADD ESP,N. stdcall (Windows API): Callee מנקה עם RET N. זה הסימן לזיהוי ב-Disassembly.
שאלה 2 — cdecl ניקוי
קוד cdecl קורא ל-calc(10, 3). מה מופיע אחרי ה-CALL בצד הקורא?
✓ תשובה: ד) add esp, 8 — 2 ארגומנטים × 4 bytes = 8. ADD ESP,8 מנקה את הארגומנטים שנדחפו לפני CALL.
שאלה 3 — Dry Run cdecl
לפני CALL, ESP = 0x1000. לאחר: push 7, push 3, call add_func — מה יהיה ESP ברגע הכניסה לפונקציה (לפני Prologue)?
✓ תשובה: ד) 0x0FF4 — push 7: ESP=0x0FFC. push 3: ESP=0x0FF8. CALL דוחף RetAddr: ESP=0x0FF4. ברגע הכניסה לפונקציה ESP=0x0FF4.
// פרק 4

דוגמה מלאה — פונקציה עם ארגומנטים ומשתנים מקומיים

נבנה דוגמה מלאה שמציגה את כל המרכיבים: Prologue, גישה לארגומנטים, הקצאת משתנים מקומיים, חישוב, ו-Epilogue.

4.1 — קוד C

int calculate(int a, int b, int c) { int temp = a * b; // local 1 int result = temp + c; // local 2 return result; } int main() { int x = calculate(3, 4, 5); // x = (3*4)+5 = 17 return x; }

4.2 — Assembly המקביל (cdecl)

main: push ebp mov ebp, esp sub esp, 4 ; מקום ל-x push 5 ; arg3 = c push 4 ; arg2 = b push 3 ; arg1 = a call calculate add esp, 12 ; Caller מנקה: 3 × 4 = 12 mov [ebp-4], eax ; x = ערך החזרה leave ret calculate: push ebp mov ebp, esp sub esp, 8 ; מקום ל-temp ו-result ; [EBP+8] = a=3, [EBP+12] = b=4, [EBP+16] = c=5 ; [EBP-4] = temp, [EBP-8] = result mov eax, [ebp+8] ; EAX = a = 3 imul eax, [ebp+12] ; EAX = a*b = 12 mov [ebp-4], eax ; temp = 12 add eax, [ebp+16] ; EAX = temp+c = 17 mov [ebp-8], eax ; result = 17 mov eax, [ebp-8] ; return result leave ret

4.3 — מעקב Stack צעד-אחר-צעד

שלבפקודהESPEBPStack
לפני CALLpush 5,4,30x0FF0(main)[5][4][3]...
CALLcall calculate0x0FEC(main)[RetAddr][5][4][3]
PUSH EBPpush ebp0x0FE8(main)[EBP_main][RetAddr][5][4][3]
MOV EBP,ESPmov ebp,esp0x0FE80x0FE8EBP → ראש
SUB ESP,8sub esp,80x0FE00x0FE8[temp][result][EBP_m][RetAddr]...
LEAVEleave0x0FEC(main)[RetAddr][5][4][3]
RETret0x0FF0(main)חזרנו ל-main!
Cleanupadd esp,120x0FFC(main)Stack נקי
💡 עצה: ניתוח Stack Frame בשאלות מיון
כאשר שואלים "מה הערך ב-[EBP+8]?" — בנו את ה-Frame בשרטוט:
1. מצאו את ה-CALL — ספרו כמה args נדחפו
2. ציירו: [argN]...[arg1][RetAddr][SavedEBP][local1]...
3. EBP מצביע תמיד ל-SavedEBP
4. +4=RetAddr, +8=arg1, +12=arg2, -4=local1, -8=local2
✏תרגול — דוגמה מלאה2 שאלות
שאלה 1 — Dry Run
עבור compute(a=3, b=10) בדוגמה להלן — מה יהיה EAX בסיום?
compute: push ebp / mov ebp,esp / sub esp,4 mov eax, [ebp+8] ; a mov ecx, [ebp+12] ; b imul eax, ecx ; a*b mov [ebp-4], eax ; temp add eax, [ebp+8] ; temp + a leave / ret
✓ תשובה: ד) 33 — EAX=a=3, ECX=b=10, imul: EAX=3×10=30, temp=30, add EAX+a: 30+3=33. הפונקציה מחשבת a×(b+1).
שאלה 2 — כתיבה
כתבו מסגרת Assembly (Prologue + Epilogue) לפונקציה שמקבלת 3 args ומגדירה 4 משתנים מקומיים. בלי קוד לוגיקה — רק המעטפת.
✓ פתרון:
my_func: ; Prologue push ebp mov ebp, esp sub esp, 16 ; 4 locals × 4 bytes = 16 ; [EBP+8] = arg1 ; [EBP+12] = arg2 ; [EBP+16] = arg3 ; [EBP-4] = local1 ; ... ; (לוגיקה כאן) ; Epilogue leave ret
// פרק 5

Stack Smashing — למה Return Address הוא המטרה

עכשיו שמבינים איך Stack Frame בנוי, ניתן להבין את הפגיעות הקלאסית ביותר: Buffer Overflow. פרק זה נותן את הבסיס התיאורטי — שיעור 45 יפרט Exploitation מלא.

5.1 — Buffer ב-Stack

; C: void vulnerable(char *input) { ; char buf[16]; ; strcpy(buf, input); // ללא בדיקת גודל! ; } vulnerable: push ebp mov ebp, esp sub esp, 16 ; הקצה 16 bytes ל-buf ; Stack Frame: ; [EBP + 8] = input (מצביע לסטרינג) ; [EBP + 4] = Return Address ← 🎯 המטרה! ; [EBP + 0] = Saved EBP ; [EBP - 4] = buf[12..15] ← buf מתחיל כאן ; ... ; [EBP - 16] = buf[0..3] ← ראש ה-Buffer lea eax, [ebp-16] ; EAX = כתובת buf push [ebp+8] push eax call strcpy ; 💀 מעתיק עד null — ללא בדיקה! add esp, 8 leave ret ; ← POP כתובת חזרה — מה אם שוכתבה?

5.2 — מה קורה ב-Overflow

עבור char buf[16]: 16 bytes למלא Buffer + 4 bytes לדרוס Saved EBP = 20 bytes של זבל. הבתים הבאים (21-24) ידרסו את Return Address!

Offsetמה נדרסתוצאה
0-15buf[0..15]בתוך ה-Buffer — בסדר
16-19Saved EBPEBP של הקורא נהרס
20-23Return Address ← 🎯EIP = ערך התוקף!

5.3 — Stack Canary: ההגנה הקלאסית

ערך אקראי שהקומפיילר מכניס בין ה-Buffer לבין ה-Return Address. לפני RET, הפונקציה בודקת שהCanary לא שונה:

; Stack Frame עם Stack Canary (-fstack-protector): ; [EBP + 4] = Return Address ; [EBP + 0] = Saved EBP ; [EBP - 4] = *** CANARY *** (ערך אקראי) ← ← ← ; [EBP - 20] = buf[0..15] ; בדיקה לפני RET: mov eax, [ebp-4] ; קרא Canary מה-Stack xor eax, gs:0x14 ; השווה לערך המקורי jz .ok call __stack_chk_fail ; ← Buffer Overflow זוהה!
🔑 Stack Smashing — עקרונות בסיס
1. Buffer גדל לכתובות נמוכות — buf[0] בכתובת נמוכה
2. Return Address נמצא מעל Buffer
3. Overflow כותב מה-Buffer כלפי מעלה — מגיע לSavedEBP ואז לRetAddr
4. שכתוב Return Address = שינוי לאן ה-CPU קופץ אחרי RET
הגנות: Stack Canary, ASLR, NX/DEP — נלמד בשיעור 45
✏תרגול — Buffer Overflow3 שאלות
שאלה 1
מדוע Buffer Overflow נחשבת לחולשה קריטית?
✓ תשובה: ב) — גלישה מעבר לBuffer מקומי → דריסת SavedEBP → דריסת RetAddr → כשהפונקציה מגיעה ל-RET, EIP = כתובת התוקף → הרצת Shellcode.
שאלה 2 — חישוב Overflow
פונקציה מכילה char buf[8]. Stack Frame: [buf 8 bytes][SavedEBP 4 bytes][RetAddr 4 bytes]. כמה bytes צריך Input כדי לדרוס בדיוק את RetAddr?
✓ תשובה: ד) — 8 bytes לBuffer + 4 bytes לדרוס SavedEBP = 12 bytes זבל. הבתים 13-16 ידרסו את RetAddr. Payload: 12×'A' + כתובת הTarget.
שאלה 3 — ASLR
ASLR מופעל. התוקף עשה Overflow ודרס את RetAddr. מה יקרה?
✓ תשובה: ב) — ASLR מערבב כתובות בכל הרצה. RetAddr נדרס בזבל → JMP לכתובת לא חוקית → Segfault. ASLR הופך RCE לDoS. לא מונע הגלישה עצמה.
// אתגר מעשי

אתגר מעשי — מנגנון ה-Stack — Offset

שיא השיעור: לרדת לרמת-המכונה — C, אסמבלי, היפוך. ההגשה נבדקת אוטומטית.

T3 · בינוני350 נק'~20 דק'analysis
חישוב Offset לכתובת החזרה
בפונקציה vuln() יש char buf[40], והבינארי קומפל ב-gcc -O0 -fno-stack-protector -no-pie. ה-disassembly מראה שהבאפר נטען ב-`lea -0x30(%rbp),%rax`. חשב כמה בתים צריך לכתוב מתחילת buf כדי להגיע *בדיוק* לכתובת החזרה (saved RIP). רמז: buf ב-rbp-0x30; אחרי saved rbp (8 בתים) באה כתובת החזרה. הגש מספר עשרוני.
דוגמאות
buf ב-rbp-0x30, saved rbp = 8 בתים → offset = 0x30 + 8
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס חישוב Offset לכתובת החזרה ›
buf ב-rbp-0x30 (48). עד saved rbp: 48 בתים. + 8 בתים של saved rbp = 56 עד כתובת החזרה. (שים לב: 40+8=48 היה שגוי — gcc יישר את הבאפר ל-0x30!)
// סיכום

✓ מה למדנו בשיעור 28

✓נקודות מפתח — שיעור 28
  • Stack גדל לכתובות נמוכות — PUSH = ESP-=4, POP = ESP+=4
  • ESP תמיד מצביע לראש; EBP קבוע לאורך כל הפונקציה
  • Prologue: push ebp / mov ebp,esp / sub esp,N
  • Epilogue: leave (=mov esp,ebp + pop ebp) + ret
  • Stack Frame: [EBP+0]=SavedEBP, [EBP+4]=RetAddr, [EBP+8]=arg1, [EBP-4]=local1
  • cdecl: Caller מנקה (ADD ESP,N) — Linux, GCC
  • stdcall: Callee מנקה (RET N) — Windows API
  • x64 System V: args ב-RDI/RSI/RDX/RCX — לא ב-Stack
  • Buffer ב-Stack נמצא מתחת ל-RetAddr — Overflow כותב כלפי מעלה
  • Stack Canary: ערך אקראי בין Buffer ל-RetAddr — בדיקה לפני RET
🎯 השיעור הבא
שיעור 29 — מ-C לאסמבלי: Reverse Engineering בסיסי עם objdump ו-Ghidra
שאלות תרגול ותשובות (7)

שאלה 1 · מה קורה למחסנית בזמן POP EAX?

pop eax
  • א. הערך בכתובת ESP נטען ל-EAX ואז ESP גדל ב-4
  • ב. ESP קטן ב-4 והערך נכתב למחסנית
  • ג. ESP לא משתנה, רק EAX
  • ד. EBP גדל ב-4
תשובה: א

המחסנית גדלה כלפי כתובות נמוכות: PUSH מקטין את ESP ב-4, ו-POP קורא את הערך ואז מגדיל את ESP ב-4.

שאלה 2 · מה מבצעת CALL מאחורי הקלעים?

call func
  • א. דוחפת את כתובת הפקודה הבאה למחסנית ואז קופצת ל-func
  • ב. רק קופצת ל-func
  • ג. דוחפת את EBP ואז קופצת
  • ד. שומרת את כל הרגיסטרים ואז קופצת
תשובה: א

כתובת החזרה נשמרת במחסנית כדי ש-RET תוכל לחזור אליה. זו בדיוק הכתובת שתוקף מנסה לדרוס ב-Buffer Overflow.

שאלה 3 · עבור void test(int x, int y) שנקראה כ-test(5, 7) במוסכמת cdecl — מה הערך ב-[EBP+8]?

  • א. 5 — הפרמטר הראשון
  • ב. 7 — הפרמטר השני
  • ג. כתובת החזרה
  • ד. ה-EBP השמור
תשובה: א

ב-cdecl הארגומנטים נדחפים מימין לשמאל, ולכן הראשון יושב הכי קרוב: ‎[ebp+8]=x ו-‎[ebp+12]=y. מתחת ל-EBP (‎[ebp-4] ומטה) יושבים המשתנים המקומיים.

שאלה 4 · מדוע הפונקציה הבאה קורסת ב-RET?

my_add:
  push ebp
  mov  ebp, esp
  sub  esp, 8
  mov  eax, [ebp+8]
  add  eax, [ebp+12]
  ret
  • א. היא לא שחזרה את ESP ואת EBP — חסר Epilogue לפני RET
  • ב. חסר push לפרמטרים
  • ג. אסור להשתמש ב-EAX לערך מוחזר
  • ד. חסר ADD ESP, 8 אצל הקורא
תשובה: א

🎯 דגש למבחן — LEAVE: ה-Prologue (push ebp / mov ebp,esp / sub esp,N) חייב Epilogue תואם. הפקודה LEAVE מבצעת פירוק מושלם של המסגרת בפעולה אחת — היא שקולה בדיוק ל-mov esp, ebp ואז pop ebp. בלעדיה ESP מצביע על המשתנים המקומיים, ו-RET "חוזרת" לזבל ומקריסה את התוכנית.

שאלה 5 · מה יהיה ESP מיד אחרי הקוד, אם לפניו ESP = 0x1000?

push 7
push 3
call add_func
  • א. 0x0FF4 — שלוש דחיפות של 4 בתים כל אחת
  • ב. 0x1000 — ללא שינוי
  • ג. 0x0FF8 — רק שני הפרמטרים
  • ד. 0x100C — המחסנית גדלה כלפי מעלה
תשובה: א

שני פרמטרים (8 בתים) ועוד כתובת החזרה שדחפה CALL (4 בתים) — סה"כ 12 בתים כלפי מטה: 0x1000-0xC = 0x0FF4. ב-cdecl הקורא ינקה בעצמו עם add esp, 8 אחרי החזרה.

שאלה 6 · בפונקציה עם char buf[8], המסגרת בנויה כך: [buf 8 בתים][EBP שמור 4][כתובת חזרה 4]. כמה בתים צריך לכתוב כדי להגיע בדיוק לכתובת החזרה, ובאיזה סדר נכתבת הכתובת?

  • א. 12 בתים מילוי ואז הכתובת ב-Little Endian — בייט נמוך ראשון
  • ב. 12 בתים מילוי והכתובת כפי שהיא נקראת (Big Endian)
  • ג. 8 בתים מילוי והכתובת ב-Big Endian
  • ד. 16 בתים מילוי, הסדר לא משנה
תשובה: א

🎯 דגש למבחן — Little Endian ב-Payload: אחרי 8+4=12 בתים מגיעים לכתובת החזרה. מעבד x86 שומר מספרים מהבייט הנמוך לגבוה, ולכן כתובת יעד 0xDEADBEEF נכתבת ב-Payload כרצף ‎\xEF\xBE\xAD\xDE. כתיבה בסדר "הטבעי" תקפיץ את התוכנית לכתובת שגויה לגמרי.

שאלה 7 · ASLR מופעל, והתוקף דרס את כתובת החזרה בכתובת קבועה שחישב מראש. מה צפוי לקרות?

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

ASLR לא מונע את הגלישה אלא את הניצול: מיקומי הזיכרון מוגרלים מחדש בכל הרצה. לכן תוקפים משלבים דליפת כתובות (Info Leak) או טכניקות יחסיות כמו ROP.