GammaCyber

Dynamic Memory

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

// סקירה

Dynamic Memory: malloc, free, Heap ו-Memory Errors

ב-C יש שתי שיטות לניהול זיכרון: Stack (אוטומטי) ו-Heap (ידני). Heap גמיש ועוצמתי — אך טעויות בניהולו הן מקור לרוב הווולנרביליטים: Buffer Overflow, Use-After-Free, ו-Double Free.

✓נושאי השיעור
  • Stack לעומת Heap — מיקום בזיכרון, ניהול, גודל, מהירות
  • malloc(n) — הקצאת n bytes, ערכי זבל, בדיקת NULL חובה
  • calloc(n, size) — הקצאה עם אפסים, הגנה מ-Integer Overflow
  • realloc(ptr, new_size) — שינוי גודל, תמיד דרך tmp!
  • free(p) — שחרור זיכרון, p = NULL אחרי free
  • Memory Leak — שכחנו free, RAM הולך לאיבוד
  • Use-After-Free (UAF) — גישה לאחר free = Undefined Behavior → Exploit
  • Double Free — free פעמיים = Heap Corruption → Exploit
  • Heap Overflow — כתיבה מעבר לבלוק = שחיתות Heap Metadata
  • Valgrind ו-AddressSanitizer — גילוי שגיאות זיכרון
🎯 איך זה מופיע במיוני גאמא
Black Box CTF — ניצול Heap Overflow, Use-After-Free, Double Free. שאלון C — שאלות על malloc/free ומה קורה לאחר free. Reverse Engineering — זיהוי malloc/free ב-Assembly (CALL + TEST EAX,EAX).
// פרק 1

Stack לעומת Heap

שני אזורי זיכרון נפרדים בכל תהליך — עם מאפיינים שונים לחלוטין. הבנת ההבדל ביניהם היא בסיס להבנת Buffer Overflows ושאלות RE.

מאפייןStackHeap
ניהולאוטומטי (push/pop)ידני — malloc/free
גודלקטן (1–8 MB)גדול (כמעט כל ה-RAM)
מהירותמאוד מהיר (הזזת מצביע)איטי יותר (קריאה ל-OS)
חיי הנתוןעד סוף הפונקציהעד קריאה ל-free
OverflowStack OF → שליטה ב-EIP/RIPHeap OF → שחיתות Metadata
// Stack: אוטומטי — נוצר ונהרס עם הפונקציה void stack_example() { int x = 5; // נוצר ב-Stack char buf[64]; // גם ב-Stack — 64 bytes אוטומטית } // ← כשהפונקציה מסתיימת — x ו-buf נהרסים אוטומטית // Heap: ידני — נוצר עם malloc, נהרס עם free void heap_example() { int *p = malloc(sizeof(int)); // מקצה 4 bytes ב-Heap if (p == NULL) return; // חובה לבדוק NULL! *p = 5; free(p); // חייב לשחרר! } // ← p (Stack) נהרס, אבל הזיכרון ב-Heap שוחרר ע"י free // מבנה זיכרון תהליך: // High Addr | STACK ↓ (קריאות פונקציות, משתנים מקומיים) // | ... // | HEAP ↑ (malloc/free — גדל כלפי מעלה) // | BSS (משתנים גלובליים לא מאוחלים) // | DATA (משתנים גלובליים מאוחלים) // Low Addr | TEXT (קוד התוכנה)
הרחבה זיהוי Stack vs Heap ב-RE ובדיבאגר ›

Stack: כתובות גבוהות מאוד — 32bit: ~0x7FFFFF..; 64bit: ~0x7fffffff....

Heap: כתובות נמוכות יותר — 32bit: ~0x0804...; 64bit: ~0x555555...

ב-IDA/Ghidra: משתנים [ebp-N] = Stack; תוצאת CALL malloc = Heap.

טעות קלאסית ב-CTF: להחזיר Pointer למשתנה Stack מפונקציה = Dangling Pointer!

✏תרגול עצמי2 שאלות
שאלה 1.1
מה ההבדל העיקרי בין Stack ל-Heap בניהול חיי הנתון?
✓ תשובה: ב) — Stack: push/pop אוטומטי עם CALL/RET. נהרס כש-RET מוצא frame. Heap: malloc מקצה, free משחרר — המתכנת אחראי. שכחת free = Memory Leak. הוסף: Stack בכתובות גבוהות, Heap בנמוכות — ב) נכון לשני חלקיו.
שאלה 1.2 — RE
ב-IDA רואים: push 64 / call malloc / test eax,eax / jz .fail / mov [ebp-8], eax. מה קורה כאן?
✓ תשובה: ד) — push 64 = ארגומנט ל-malloc. CALL malloc = הקצאה. TEST EAX,EAX + JZ = NULL check (malloc נכשל?). MOV [ebp-8],EAX = שמור ה-Pointer. זו ה-Pattern הקלאסית של malloc ב-RE.
// פרק 2

malloc, calloc, realloc

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

malloc — הקצאת זיכרון בסיסית

#include <stdlib.h> // לכל פונקציות ההקצאה // malloc(size): מקצה size bytes ב-Heap. לא מאפס! int *p = malloc(sizeof(int)); // הקצה 4 bytes if (p == NULL) { exit(1); } // חובה לבדוק NULL! *p = 42; free(p); p = NULL; // Best Practice: אפס אחרי free // הקצאת מערך: int n = 10; int *arr = malloc(n * sizeof(int)); // 10 * 4 = 40 bytes if (!arr) { exit(1); } // arr[0]..arr[9] — ערכי זבל לא מוגדרים! for (int i = 0; i < n; i++) arr[i] = i * i; free(arr); // הקצאת Struct: typedef struct { int x; int y; } Point; Point *pt = malloc(sizeof(Point)); if (!pt) { exit(1); } pt->x = 3; pt->y = 4; free(pt);

calloc — הקצאה עם אפסים

// calloc(n, size): מקצה n*size bytes, מאפס ל-0 int *arr = calloc(10, sizeof(int)); // כולם = 0 ודאי! // arr[0]=0, arr[1]=0, ..., arr[9]=0 // vs malloc: malloc(10*sizeof(int)) → ערכי זבל לא ידועים // calloc מגן גם מ-Integer Overflow: // malloc(n * size) → n=1000000, size=4096 → overflow! // calloc(n, size) → בודק overflow → NULL אם overflow

realloc — שינוי גודל

int *arr = malloc(5 * sizeof(int)); for (int i = 0; i < 5; i++) arr[i] = i; // {0,1,2,3,4} // תמיד דרך tmp! int *tmp = realloc(arr, 10 * sizeof(int)); if (tmp == NULL) { free(arr); exit(1); } // arr עדיין תקף! arr = tmp; // עדכן arr לכתובת החדשה (אולי שונה!) // arr[0..4] = 0..4 (הועתק); arr[5..9] = ערכי זבל free(arr);
! realloc ישירות על המשתנה — Memory Leak! ›

✗ שגוי: arr = realloc(arr, new_size);

אם realloc נכשל → מחזיר NULL → arr = NULL → איבדנו ה-Pointer המקורי = Memory Leak!

נכון: tmp = realloc(arr, new_size); if (tmp) arr = tmp; else { free(arr); return; }
✏תרגול עצמי3 שאלות
שאלה 2.1
מה ההבדל בין malloc(10*sizeof(int)) לבין calloc(10, sizeof(int))?
✓ תשובה: ד) — malloc: לא מאפס = חייבים לאחל ידנית. calloc: מאפס הכל ל-0 + בודק Integer Overflow בפנימי (malloc(n*size) יכול לoverflow ל-0 ואז malloc(0) = "הצלחה" עם buffer קטן!).
שאלה 2.2
למה חובה לבדוק if (p == NULL) אחרי כל malloc?
✓ תשובה: ב) — malloc מחזיר NULL כשהמערכת אין לה RAM פנוי. *p = 5 אחרי NULL = SEGFAULT מיידי. בקוד Production — תמיד NULL check. ב-CTF לרוב לא מוצגים כשלי malloc, אבל ב-Exploit: malloc failure = DoS.
שאלה 2.3 — Dry Run
int *arr = malloc(3 * sizeof(int)); arr[0]=10; arr[1]=20; arr[2]=30; int *tmp = realloc(arr, 5 * sizeof(int)); arr = tmp; arr[3]=40; arr[4]=50; printf("%d %d", arr[0], arr[4]);מה הפלט?
✓ תשובה: ב) 10 50 — malloc הקצה 3 ints. arr[0]=10, arr[1]=20, arr[2]=30. realloc הגדיל ל-5 ints — arr[0..2] הועתקו. arr[3]=40, arr[4]=50. printf: arr[0]=10, arr[4]=50 → "10 50".
// פרק 3

free — שחרור זיכרון

free מחזיר את הזיכרון ל-Heap Manager. חשוב להבין מה הוא עושה ומה לא — כי הטעויות מובילות לווולנרביליטים קריטיים.

int *p = malloc(sizeof(int)); *p = 5; free(p); // מה free עושה: // 1. מסמן את הבלוק כ-'פנוי' ב-Heap Manager // 2. אולי מחזיר זיכרון ל-OS (תלוי בגודל הבלוק) // 3. כותב נתוני Heap Manager לתוך הבלוק (Free List pointers!) // מה free לא עושה: // 1. לא מאפס את הזיכרון ל-0 // 2. לא משנה את p — p עדיין מחזיק את הכתובת הישנה! // → p נקרא Dangling Pointer לאחר free // Best Practice: אפס ל-NULL אחרי free free(p); p = NULL; // עכשיו p = NULL, לא Dangling Pointer // free(NULL) בטוח לחלוטין — לא עושה כלום int *q = NULL; free(q); // לא קורס! // לכן: p=NULL → free(p) שוב בטוח
✏תרגול עצמי2 שאלות
שאלה 3.1
מה קורה לאחר free(p) — האם p שווה NULL?
✓ תשובה: ד) — free רק משחרר את הזיכרון שp מצביע עליו, אבל לא משנה את p עצמו. p ממשיך להחזיק את הכתובת הישנה (Dangling Pointer). כל גישה ל-*p אחרי free = Use-After-Free. פתרון: p = NULL ידנית.
שאלה 3.2
מה הסיבה שכותבים p = NULL אחרי free(p)?
✓ תשובה: ד) — שתי הגנות ב-NULL אחד: (1) Use-After-Free: *p = dereference NULL = Segfault מיידי (ניתן לזהות) במקום UB שקט. (2) Double Free: free(NULL) = בטוח לחלוטין, לא Heap Corruption.
// פרק 4

Memory Errors — הוולנרביליטים הגדולים

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

Memory Leak — שכחנו free

// Memory Leak קלאסי void leaky_function() { int *p = malloc(100 * sizeof(int)); // ... שימוש ... // ← שכחנו free(p)! 400 bytes אובדים בכל קריאה } // Leak ב-return מוקדם — טעות נפוצה מאוד! int process(int n) { char *buf = malloc(n); if (n < 0) return -1; // ← Leak! שכחנו free לפני return if (n == 0) return 0; // ← Leak שוב! // ... free(buf); return 1; } // תיקון נכון: free לפני כל return int process_fixed(int n) { char *buf = malloc(n); if (!buf) return -2; if (n < 0) { free(buf); return -1; } if (n == 0) { free(buf); return 0; } free(buf); return 1; }

Use-After-Free (UAF) — שימוש לאחר שחרור

// UAF קלאסי int *p = malloc(sizeof(int)); *p = 42; free(p); *p = 100; // 💥 Use-After-Free! — Undefined Behavior // UAF ב-CTF — Pattern קלאסי לניצול: typedef struct { char data[32]; void (*handler)(void); // Function Pointer } Obj; Obj *o = malloc(sizeof(Obj)); o->handler = safe_function; free(o); // ← Heap Manager מציע שוב אותה כתובת! // תוקף: malloc אותו גודל עם controlled data: char *attack = malloc(sizeof(Obj)); memcpy(attack+32, &evil_addr, 4); // דרוס handler o->handler(); // 💥 UAF → RCE!

Double Free — שחרור כפול

// Double Free קלאסי int *p = malloc(sizeof(int)); free(p); free(p); // 💥 Double Free! — Heap Corruption // Double Free עקיף — נפוץ יותר: int *a = malloc(sizeof(int)); int *b = a; // b = a — שניהם אותה כתובת! free(a); free(b); // 💥 Double Free! // הגנה: תמיד NULL לשניהם: free(a); a = NULL; // b עדיין מחזיק כתובת! חייבים: free(a); a = NULL; b = NULL; // שניהם לNULL

Heap Overflow — כתיבה מחוץ לבלוק

// Heap Overflow קלאסי char *buf = malloc(16); // הקצה 16 bytes strcpy(buf, "AAAAAAAAAAAAAAAA!!!!!"); // כותב יותר מ-16! // → משחית Heap Metadata של Chunk הבא // CTF Pattern: Adjacent Structs typedef struct { char input[8]; } UserData; typedef struct { void (*fn)(void); } Config; UserData *u = malloc(sizeof(UserData)); // 8 bytes Config *c = malloc(sizeof(Config)); // 4 bytes c->fn = safe_handler; // Overflow ב-u->input → דורס Chunk header + c->fn → RCE!
🔑UAF ב-CTF — Pattern של ניצול
  • free שלא מאפס ל-NULL, ואחריו שימוש נוסף ב-Pointer
  • Heap Allocator מחזיר אותה כתובת ל-malloc הבא עם אותו גודל
  • Pattern: free(obj) → malloc(same_size) עם controlled data → obj->fn() → RCE
  • UAF ב-Browsers (Chrome, Firefox): אחד הוקטורים הנפוצים 2021–2024
✏תרגול עצמי4 שאלות
שאלה 4.1
מה גורם ל-Memory Leak?
✓ תשובה: ד) — Memory Leak = מקצים זיכרון ולא משחררים. הזיכרון "אבוד" — לא ניתן להקצות אותו שוב. בשרתים: Leak → RAM מתמלא → קריסה אחרי שעות/ימים. גילוי: Valgrind --leak-check=full.
שאלה 4.2
מה ה-Undefined Behavior ב: int *p = malloc(4); free(p); printf("%d", *p);?
✓ תשובה: א) Use-After-Free — free(p) שחרר את הזיכרון. *p ניגש לזיכרון שכבר לא שלנו. יכול: לקרוס, להדפיס ערך ישן, להדפיס נתוני Heap Manager (Free List pointers!) — Undefined Behavior.
שאלה 4.3 — Dry Run
זהה את כל שגיאות הזיכרון: int *arr = malloc(5 * sizeof(int)); for (int i = 0; i <= 5; i++) arr[i] = i; // (A) int *q = arr; free(arr); printf("%d", *q); // (B) free(q); // (C)
✓ תשובה: א) — (A) i <= 5: malloc הקצה 5 ints (0..4). i=5 כותב arr[5] = מחוץ לבלוק = Heap Overflow. (B) q=arr, free(arr) → *q = Use-After-Free. (C) q ו-arr אותה כתובת, free(arr) כבר שחרר → free(q) = Double Free!
שאלה 4.4 — CTF
ב-CTF מצאתם קוד עם: free(obj) ואחריו obj->handler() (ללא NULL אחרי free). מהו הווקטור לניצול?
✓ תשובה: א) — זה ה-UAF Pattern הקלאסי: free(obj) → malloc(sizeof(Obj)) → Heap Allocator נותן אותה כתובת → כתוב evil_addr ב-offset של handler → obj->handler() = CALL evil_addr = RCE. זה קורה ב-Chrome, Firefox bugs 2021–2024.
// פרק 5

Valgrind ו-AddressSanitizer

שני כלים קריטיים לגילוי שגיאות זיכרון. מנטרים כל גישה לזיכרון ומדווחים על Memory Leaks, UAF, Double Free, Heap Overflow ועוד.

# Valgrind — לא דורש compile flag gcc -g -o prog prog.c valgrind --leak-check=full --show-leak-kinds=all ./prog # פלט Memory Leak: # ==PID== LEAK SUMMARY: # ==PID== definitely lost: 40 bytes in 1 blocks # ==PID== at 0x4C2FB0F: malloc (in vgpreload_memcheck-*.so) # ==PID== by 0x10865: leaky_function (prog.c:5) # פלט Use-After-Free: # ==PID== Invalid write of size 4 # ==PID== at 0x10878: main (prog.c:12) # ==PID== Address 0x5204040 is 0 bytes inside a block of size 4 free'd # AddressSanitizer — מהיר יותר, דורש compile flag gcc -fsanitize=address -fsanitize=undefined -g -o prog prog.c ./prog # פלט ASan — Heap Buffer Overflow: # ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000010 # WRITE of size 1 at 0x602000000010 thread T0 # #0 0x... in main prog.c:8
קריטריוןValgrindAddressSanitizer
מהירותאיטי מאוד (x10–50)מהיר (x2–5)
Compile flagלא נדרשחובה: -fsanitize=address
פרטיםline numbers מלאיםstack trace מלא
Memory Leak✅ מצוין✅ טוב
UAF / Double Free✅✅
✏תרגול עצמי2 שאלות
שאלה 5.1
Valgrind מדווח: Invalid read of size 4 — block of size 4 free'd. מה זה אומר?
✓ תשובה: ב) — "Invalid read" = קריאה לא חוקית. "block of size 4 free'd" = הבלוק כבר שוחרר. ביחד = Use-After-Free. Memory Leak = "definitely lost: N bytes". Double Free = "Invalid free".
שאלה 5.2
מתי עדיף להשתמש ב-ASan על Valgrind?
✓ תשובה: ב) — ASan מהיר (x2–5 לעומת x10–50 של Valgrind) אך דורש compilation מחדש עם -fsanitize=address. Valgrind = אין צורך בCompile flags, עובד על כל binary. בפועל: שניהם משלימים זה את זה.
// פרק 6

malloc/free ב-Assembly — מה רואים ב-RE

כשעושים Reverse Engineering, malloc ו-free מופיעים כ-CALL לפונקציות Library. הכרתם חיונית לזיהוי Allocation Patterns ו-Vulnerabilities ב-IDA/Ghidra.

; malloc(64) ב-Assembly (cdecl 32bit) push 64 ; ארגומנט: size = 64 call malloc ; קריאה ל-malloc add esp, 4 ; ניקוי Stack (cdecl) ; ← EAX = Pointer לזיכרון (או NULL) test eax, eax ; בדוק NULL (EAX == 0?) jz .fail ; אם NULL → שגיאה mov [ebp-8], eax ; שמור Pointer במשתנה מקומי ; שימוש בPointer mov eax, [ebp-8] ; טען הPointer mov dword [eax], 42 ; *p = 42 ; free(p) mov eax, [ebp-8] ; טען הPointer push eax ; ארגומנט ל-free call free add esp, 4 mov dword [ebp-8], 0 ; p = NULL ; סימנים לזיהוי Allocation ב-IDA/Ghidra: ; 1. CALL malloc/calloc/realloc לפני שימוש ב-Pointer ; 2. TEST EAX,EAX + JZ אחרי malloc = NULL check ; 3. CALL free עם Pointer כארגומנט ; 4. MOV [local_var], EAX אחרי malloc = שמור Pointer ; 5. MOV dword [var], 0 אחרי free = p=NULL (הגנה מ-UAF)
✏תרגול עצמי2 שאלות
שאלה 6.1 — RE
ב-IDA רואים אחרי CALL malloc: test eax, eax / jz 0x401234. מה המשמעות?
✓ תשובה: ד) — TEST EAX,EAX = AND בין EAX לעצמו, מגדיר ZF. JZ = קפוץ אם Zero (EAX=0 = NULL). זה NULL check קלאסי לאחר malloc. אם malloc נכשל → EAX=0 → ZF=1 → JZ = קפיצה ל-error path.
שאלה 6.2 — ביטחון RE
ב-IDA לא רואים MOV dword [ebp-8], 0 אחרי CALL free. מה זה מרמז?
✓ תשובה: א) — MOV [local_var], 0 אחרי free = p=NULL (best practice). אם חסר = ה-Pointer נשאר כ-Dangling Pointer. אם יש שימוש נוסף ב-Pointer = UAF Vulnerability. חפשו: CALL free → ללא MOV 0 → שימוש נוסף ב-Pointer = Bug!
// אתגר מעשי

אתגר מעשי — Heap — malloc, realloc ובאגי זיכרון

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

T3 · בינוני400 נק'~25 דק'build
מערך דינמי ב-C (malloc/realloc)
כתבו תוכנית C שקוראת מספרים שלמים מ-stdin (מופרדים ברווחים, עד EOF), שומרת אותם במערך שגדל דינמית עם realloc, ומדפיסה אותם בסדר הפוך (מופרדים ברווח, ירידת שורה בסוף). שחררו את הזיכרון עם free.
דוגמאות
1 2 3 4 5 → 5 4 3 2 1
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס מערך דינמי ב-C (malloc/realloc) ›
#include <stdio.h> #include <stdlib.h> int main(){ int cap=2, n=0, *a=malloc(cap*sizeof(int)), x; while(scanf("%d",&x)==1){ if(n==cap){ cap*=2; a=realloc(a, cap*sizeof(int)); } // גדילה דינמית a[n++]=x; } for(int i=n-1;i>=0;i--) printf("%d%s", a[i], i?" ":"\n"); free(a); // שחרור נכון return 0; }
T3 · בינוני300 נק'~15 דק'analysis
זיהוי באג זיכרון
איזה סוג באג זיכרון קיים בקוד? הגישו את שם הבאג (למשל use-after-free / double-free / memory-leak / buffer-overflow). int *p = malloc(sizeof(int)); *p = 42; free(p); printf("%d\n", *p); // <-- כאן הבאג
דוגמאות
(פורמט) → שם באג מנורמל
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס זיהוי באג זיכרון ›
הזיכרון שוחרר ב-free(p), ואז נקרא שוב ב-*p → use-after-free (אומת ב-AddressSanitizer).
שאלות תרגול ותשובות (15)

שאלה 1.1 · מה ההבדל העיקרי בין Stack ל-Heap בניהול חיי הנתון?

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

✓ תשובה: ב) — Stack: push/pop אוטומטי עם CALL/RET. נהרס כש-RET מוצא frame. Heap: malloc מקצה, free משחרר — המתכנת אחראי. שכחת free = Memory Leak. הוסף: Stack בכתובות גבוהות, Heap בנמוכות — ב) נכון לשני חלקיו.

שאלה 1.2 — RE · ב-IDA רואים: push 64 / call malloc / test eax,eax / jz .fail / mov [ebp-8], eax. מה קורה כאן?

push 64 / call malloc / test eax,eax / jz .fail / mov [ebp-8], eax
  • א. מקצה 64 בתים על ה-Stack, אף שהקריאה ל-malloc מקצה זיכרון דווקא מן ה-Heap ולא מן ה-Stack של הפונקציה
  • ב. קורא 64 בתים מתוך ה-Heap אל הרגיסטר, אף ש-malloc מקצה זיכרון חדש ואינו קורא כלל מתוך ה-Heap הקיים כאן
  • ג. משחרר 64 בתים מן ה-Heap, אף שהקריאה כאן היא ל-malloc שמקצה זיכרון ולא ל-free שמשחרר אותו בחזרה כאן כן
  • ד. מקצה 64 בתים ב-Heap, בודק אם ההקצאה הצליחה (NULL check), ושומר את המצביע שהתקבל אל [ebp-8] כאן
תשובה: ד

✓ תשובה: ד) — push 64 = ארגומנט ל-malloc. CALL malloc = הקצאה. TEST EAX,EAX + JZ = NULL check (malloc נכשל?). MOV [ebp-8],EAX = שמור ה-Pointer. זו ה-Pattern הקלאסית של malloc ב-RE.

שאלה 2.1 · מה ההבדל בין malloc(10*sizeof(int)) לבין calloc(10, sizeof(int))?

  • א. אין ביניהם כל הבדל, ושתי הפונקציות מקצות בדיוק את אותו הגודל ומאתחלות אותו באותו האופן בכל מקרה כאן
  • ב. פונקציית calloc היא פשוט המהירה יותר מבין השתיים, אף שההבדל האמיתי ביניהן הוא באיפוס הזיכרון שהוקצה כאן
  • ג. פונקציית malloc מיועדת למערכים גדולים ו-calloc למבנים, אף שאין זו כלל ההבחנה הנכונה בין שתי הפונקציות
  • ד. malloc אינו מאפס ומשאיר ערכי זבל, ואילו calloc מאפס את הזיכרון לאפס ומגן גם מפני Integer Overflow כאן
תשובה: ד

✓ תשובה: ד) — malloc: לא מאפס = חייבים לאחל ידנית. calloc: מאפס הכל ל-0 + בודק Integer Overflow בפנימי (malloc(n*size) יכול לoverflow ל-0 ואז malloc(0) = "הצלחה" עם buffer קטן!).

שאלה 2.2 · למה חובה לבדוק if (p == NULL) אחרי כל malloc?

  • א. מכיוון ש-malloc מחזיר NULL תמיד גם כשההקצאה מצליחה, אף שבמקרה של הצלחה הוא מחזיר כתובת תקינה כאן
  • ב. אם אין זיכרון RAM פנוי, malloc מחזיר NULL, וגישה (Dereference) אל NULL גורמת ל-Segfault מיידי בריצה
  • ג. בדיקה זו נדרשת רק במערכות משובצות (Embedded) בלבד, אף שהיא חשובה בכל תוכנית שמשתמשת בפונקציה malloc
  • ד. מכיוון ש-malloc עלול לחזור עם הכתובת 0x1 בעת כישלון, אף שבכישלון הוא מחזיר NULL שהוא הכתובת 0 בזיכרון
תשובה: ב

✓ תשובה: ב) — malloc מחזיר NULL כשהמערכת אין לה RAM פנוי. *p = 5 אחרי NULL = SEGFAULT מיידי. בקוד Production — תמיד NULL check. ב-CTF לרוב לא מוצגים כשלי malloc, אבל ב-Exploit: malloc failure = DoS.

שאלה 2.3 — Dry Run · int *arr = malloc(3 * sizeof(int)); arr[0]=10; arr[1]=20; arr[2]=30; int *tmp = realloc(arr, 5 * sizeof(int)); arr = tmp; arr[3]=40; arr[4]=50; printf("%d %d", arr[0], arr[4]);מה הפלט?

int *arr = malloc(3 * sizeof(int));
arr[0]=10; arr[1]=20; arr[2]=30;
int *tmp = realloc(arr, 5 * sizeof(int));
arr = tmp;
arr[3]=40; arr[4]=50;
printf("%d %d", arr[0], arr[4]);
  • א. 0 0: הנחה שגויה ש-realloc מאפסת את כל התוכן, אף שהיא משמרת את הערכים הישנים ומרחיבה את הבלוק הקיים כאן
  • ב. 10 50: הפונקציה realloc משמרת את התוכן הישן (arr[0]=10), והערכים החדשים נכתבים אחריו (arr[4]=50) כאן
  • ג. 10 0: הנחה שגויה שהערך arr[4] נותר אפס, אף שהוא נכתב במפורש לערך 50 לאחר ההרחבה של הבלוק בקוד כאן כן
  • ד. נותן שגיאה, אף שהשימוש ב-realloc כאן תקין לחלוטין ומרחיב את המערך מ-3 ל-5 איברים כראוי ללא כל בעיה כאן
תשובה: ב

✓ תשובה: ב) 10 50 — malloc הקצה 3 ints. arr[0]=10, arr[1]=20, arr[2]=30. realloc הגדיל ל-5 ints — arr[0..2] הועתקו. arr[3]=40, arr[4]=50. printf: arr[0]=10, arr[4]=50 → "10 50".

שאלה 3.1 · מה קורה לאחר free(p) — האם p שווה NULL?

  • א. כן: הפונקציה free מאפסת את המצביע p אל NULL באופן אוטומטי, אף שהיא רק משחררת את הזיכרון ולא את המצביע
  • ב. המצביע p מקבל את הערך 1- לאחר free, אף שהוא נותר עם הכתובת הישנה ואינו מקבל שום ערך מיוחד לאחר השחרור
  • ג. המצביע p נהרס ולא ניתן עוד לגשת אליו, אף שהמצביע עצמו נותר תקף ומחזיק את הכתובת הישנה גם אחרי free כאן
  • ד. לא: המצביע p עדיין מחזיק את הכתובת הישנה לאחר free, ולכן הוא הופך ל-Dangling Pointer מסוכן בקוד כאן
תשובה: ד

✓ תשובה: ד) — free רק משחרר את הזיכרון שp מצביע עליו, אבל לא משנה את p עצמו. p ממשיך להחזיק את הכתובת הישנה (Dangling Pointer). כל גישה ל-*p אחרי free = Use-After-Free. פתרון: p = NULL ידנית.

שאלה 3.2 · מה הסיבה שכותבים p = NULL אחרי free(p)?

// Memory Leak קלאסי
void leaky_function() {
    int *p = malloc(100 * sizeof(int));
    // ... שימוש ...
    // ← שכחנו free(p)! 400 bytes אובדים בכל קריאה
}

// Leak ב-return מוקדם — טעות נפוצה מאוד!
int process(int n) {
    char *buf = malloc(n);
    if (n < 0) return -1;   // ← Leak! שכחנו free לפני return
    if (n == 0) return 0;   // ← Leak שוב!
    // ...
    free(buf);
    return 1;
}

// תיקון נכון: free לפני כל return
int process_fixed(int n) {
    char *buf = malloc(n);
    if (!buf) return -2;
    if (n < 0) { free(buf); return -1; }
    if (n == 0) { free(buf); return 0; }
    free(buf);
    return 1;
}
  • א. מכיוון שהפונקציה free כלל אינה עובדת בלי איפוס המצביע לאחריה, אף שהיא עובדת היטב גם בלי האיפוס הזה כאן
  • ב. כדי לשחרר את הזיכרון של המצביע p עצמו, אף ש-free משחרר את הבלוק שהמצביע הצביע עליו ולא את המצביע עצמו
  • ג. מכיוון שאיפוס המצביע הוא פשוט מהיר יותר לביצוע, אף שמטרתו האמיתית היא מניעת שגיאות זיכרון מסוכנות כאן
  • ד. מונע Use-After-Free ו-Double Free: גישה תגרום ל-crash מיידי, וקריאת free שנייה הופכת ל-free(NULL) בטוח
תשובה: ד

✓ תשובה: ד) — שתי הגנות ב-NULL אחד: (1) Use-After-Free: *p = dereference NULL = Segfault מיידי (ניתן לזהות) במקום UB שקט. (2) Double Free: free(NULL) = בטוח לחלוטין, לא Heap Corruption.

שאלה 4.1 · מה גורם ל-Memory Leak?

  • א. קריאה אל malloc פעמיים עבור אותו מצביע, אף שדליפת זיכרון נגרמת דווקא מאי-שחרור ולא מהקצאה כפולה כאן
  • ב. גישה אל זיכרון לאחר שחרורו ב-free, אף שזוהי שגיאת Use-After-Free ולא דליפת זיכרון (Memory Leak) כלל כאן
  • ג. כתיבה אל מחוץ לגבול הבלוק שהוקצה, אף שזוהי שגיאת Heap Overflow ולא דליפת זיכרון (Memory Leak) כלל כאן
  • ד. הקצאה עם malloc ואי-שחרור עם free, כך שהזיכרון שהוקצה אינו חוזר לעולם אל המערכת לשימוש חוזר בהמשך
תשובה: ד

✓ תשובה: ד) — Memory Leak = מקצים זיכרון ולא משחררים. הזיכרון "אבוד" — לא ניתן להקצות אותו שוב. בשרתים: Leak → RAM מתמלא → קריסה אחרי שעות/ימים. גילוי: Valgrind --leak-check=full.

שאלה 4.2 · מה ה-Undefined Behavior ב: int *p = malloc(4); free(p); printf("%d", *p);?

int *p = malloc(4); free(p); printf("%d", *p);
  • א. Use-After-Free: גישה אל הזיכרון שעליו הצביע p לאחר שכבר שוחרר ב-free, ולכן תוכנו אינו מוגדר עוד בקוד
  • ב. Memory Leak: דליפת זיכרון, אף שכאן הזיכרון דווקא שוחרר ב-free ולא נשאר מוקצה ללא שחרור כלל בקוד הזה כאן
  • ג. Double Free: שחרור כפול, אף שכאן הפונקציה free נקראת רק פעם אחת בלבד ולא פעמיים על אותו המצביע בקוד כאן
  • ד. Heap Overflow: גלישה מעבר לגבול הבלוק, אף שכאן אין כלל כתיבה מעבר לגבול אלא גישה אל זיכרון לאחר שחרורו
תשובה: א

✓ תשובה: א) Use-After-Free — free(p) שחרר את הזיכרון. *p ניגש לזיכרון שכבר לא שלנו. יכול: לקרוס, להדפיס ערך ישן, להדפיס נתוני Heap Manager (Free List pointers!) — Undefined Behavior.

שאלה 4.3 — Dry Run · זהה את כל שגיאות הזיכרון: int *arr = malloc(5 * sizeof(int)); for (int i = 0; i <= 5; i++) arr[i] = i; // (A) int *q = arr; free(arr); printf("%d", *q); // (B) free(q); // (C)

int *arr = malloc(5 * sizeof(int));
for (int i = 0; i <= 5; i++) arr[i] = i;  // (A)
int *q = arr;
free(arr);
printf("%d", *q);   // (B)
free(q);            // (C)
  • א. שלוש שגיאות: A היא Heap Overflow (i<=5 כותב אל arr[5]), B היא Use-After-Free, ו-C היא Double Free כאן
  • ב. רק B היא Use-After-Free, אף שקיימות בקוד גם שגיאת Heap Overflow בשורה A וגם שגיאת Double Free בשורה C
  • ג. רק A היא Heap Overflow, אף שקיימות בקוד גם שגיאת Use-After-Free בשורה B וגם שגיאת Double Free בשורה C
  • ד. רק A ו-B הן שגיאות, אף שגם שורה C מהווה שגיאת Double Free בשל קריאה שנייה ל-free על אותו המצביע ששוחרר
תשובה: א

✓ תשובה: א) — (A) i <= 5: malloc הקצה 5 ints (0..4). i=5 כותב arr[5] = מחוץ לבלוק = Heap Overflow. (B) q=arr, free(arr) → *q = Use-After-Free. (C) q ו-arr אותה כתובת, free(arr) כבר שחרר → free(q) = Double Free!

שאלה 4.4 — CTF · ב-CTF מצאתם קוד עם: free(obj) ואחריו obj->handler() (ללא NULL אחרי free). מהו הווקטור לניצול?

# Valgrind — לא דורש compile flag
gcc -g -o prog prog.c
valgrind --leak-check=full --show-leak-kinds=all ./prog

# פלט Memory Leak:
# ==PID== LEAK SUMMARY:
# ==PID== definitely lost: 40 bytes in 1 blocks
# ==PID== at 0x4C2FB0F: malloc (in vgpreload_memcheck-*.so)
# ==PID== by 0x10865: leaky_function (prog.c:5)

# פלט Use-After-Free:
# ==PID== Invalid write of size 4
# ==PID== at 0x10878: main (prog.c:12)
# ==PID== Address 0x5204040 is 0 bytes inside a block of size 4 free'd

# AddressSanitizer — מהיר יותר, דורש compile flag
gcc -fsanitize=address -fsanitize=undefined -g -o prog prog.c
./prog

# פלט ASan — Heap Buffer Overflow:
# ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000010
# WRITE of size 1 at 0x602000000010 thread T0
# #0 0x... in main prog.c:8
  • א. Use-After-Free: הקצאה בגודל זהה לאחר free מחזירה את אותה הכתובת, ואז כותבים כתובת זדונית אל שדה ה-handler
  • ב. Memory Leak: דליפת זיכרון, אף שהחולשה כאן היא גישה למצביע לאחר שחרורו ולא אי-שחרור של זיכרון שהוקצה כלל
  • ג. Heap Overflow: גלישה מעבר ל-obj, אף שהחולשה כאן היא שימוש במצביע ששוחרר ולא כתיבה מעבר לגבול הבלוק שהוקצה
  • ד. Double Free: שחרור כפול, אף שהחולשה כאן היא קריאה למתודה של אובייקט ששוחרר ולא שחרורו של האובייקט פעם נוספת
תשובה: א

✓ תשובה: א) — זה ה-UAF Pattern הקלאסי: free(obj) → malloc(sizeof(Obj)) → Heap Allocator נותן אותה כתובת → כתוב evil_addr ב-offset של handler → obj->handler() = CALL evil_addr = RCE. זה קורה ב-Chrome, Firefox bugs 2021–2024.

שאלה 5.1 · Valgrind מדווח: Invalid read of size 4 — block of size 4 free'd. מה זה אומר?

Invalid read of size 4 — block of size 4 free'd
  • א. Memory Leak: דליפת זיכרון, אף שההודעה מדווחת על קריאה מבלוק ששוחרר ולא על זיכרון שלא שוחרר כלל במהלך הריצה
  • ב. Use-After-Free: קריאה מתוך זיכרון שכבר שוחרר, כפי שמעידה ההודעה על גישה אל בלוק שכבר עבר free בקוד כאן
  • ג. Double Free: שחרור כפול, אף שההודעה מדווחת על קריאה מזיכרון משוחרר ולא על שחרור נוסף של אותו הבלוק בזיכרון
  • ד. Heap Overflow: גלישה מעבר לגבול, אף שההודעה מדווחת על קריאה מבלוק ששוחרר ולא על כתיבה מעבר לגבולו של הבלוק
תשובה: ב

✓ תשובה: ב) — "Invalid read" = קריאה לא חוקית. "block of size 4 free'd" = הבלוק כבר שוחרר. ביחד = Use-After-Free. Memory Leak = "definitely lost: N bytes". Double Free = "Invalid free".

שאלה 5.2 · מתי עדיף להשתמש ב-ASan על Valgrind?

; malloc(64) ב-Assembly (cdecl 32bit)
push 64             ; ארגומנט: size = 64
call malloc         ; קריאה ל-malloc
add esp, 4          ; ניקוי Stack (cdecl)
; ← EAX = Pointer לזיכרון (או NULL)
test eax, eax       ; בדוק NULL (EAX == 0?)
jz .fail            ; אם NULL → שגיאה
mov [ebp-8], eax    ; שמור Pointer במשתנה מקומי

; שימוש בPointer
mov eax, [ebp-8]    ; טען הPointer
mov dword [eax], 42 ; *p = 42

; free(p)
mov eax, [ebp-8]    ; טען הPointer
push eax            ; ארגומנט ל-free
call free
add esp, 4
mov dword [ebp-8], 0 ; p = NULL

; סימנים לזיהוי Allocation ב-IDA/Ghidra:
; 1. CALL malloc/calloc/realloc לפני שימוש ב-Pointer
; 2. TEST EAX,EAX + JZ אחרי malloc = NULL check
; 3. CALL free עם Pointer כארגומנט
; 4. MOV [local_var], EAX אחרי malloc = שמור Pointer
; 5. MOV dword [var], 0 אחרי free = p=NULL (הגנה מ-UAF)
  • א. כאשר רוצים לבדוק קוד בלי לקמפל אותו מחדש כלל, אף ש-ASan דורש דווקא קימפול מחדש ואילו Valgrind אינו דורש
  • ב. כאשר המהירות חשובה (למשל ב-CI/CD או בתוכניות ארוכות) וכאשר ניתן לקמפל מחדש את קוד המקור יחד עם ASan
  • ג. תמיד עדיף להשתמש ב-ASan, שכן הוא גם המהיר יותר וגם המפורט יותר, אף שלכל אחד מן הכלים יש יתרונות משלו כאן
  • ד. רק כאשר יש בינארי קיים ללא קוד מקור, אף ש-ASan דורש דווקא קוד מקור לקימפול מחדש ולא בינארי מהודר בלבד כאן
תשובה: ב

✓ תשובה: ב) — ASan מהיר (x2–5 לעומת x10–50 של Valgrind) אך דורש compilation מחדש עם -fsanitize=address. Valgrind = אין צורך בCompile flags, עובד על כל binary. בפועל: שניהם משלימים זה את זה.

שאלה 6.1 — RE · ב-IDA רואים אחרי CALL malloc: test eax, eax / jz 0x401234. מה המשמעות?

test eax, eax / jz 0x401234
  • א. בודק אם הזיכרון שהוקצה מאופס, אף שהצירוף test/jz בודק כאן אם המצביע שהוחזר הוא NULL ולא את תוכן הזיכרון
  • ב. משווה את גודל הבלוק שהוקצה, אף שהצירוף test/jz בודק כאן אם הערך המוחזר הוא אפס ולא את גודלו של הבלוק כאן
  • ג. בודק אם המצביע שונה מן הבלוק הקודם, אף שהצירוף כאן בודק אם המצביע הוא NULL ואינו משווה כלל בין בלוקים כאן
  • ד. NULL check: אם הקריאה ל-malloc נכשלה והחזירה 0, הקפיצה jz מעבירה את השליטה אל מטפל השגיאות שבקוד כאן
תשובה: ד

✓ תשובה: ד) — TEST EAX,EAX = AND בין EAX לעצמו, מגדיר ZF. JZ = קפוץ אם Zero (EAX=0 = NULL). זה NULL check קלאסי לאחר malloc. אם malloc נכשל → EAX=0 → ZF=1 → JZ = קפיצה ל-error path.

שאלה 6.2 — ביטחון RE · ב-IDA לא רואים MOV dword [ebp-8], 0 אחרי CALL free. מה זה מרמז?

#include <stdio.h>
#include <stdlib.h>
int main(){
    int cap=2, n=0, *a=malloc(cap*sizeof(int)), x;
    while(scanf("%d",&x)==1){
        if(n==cap){ cap*=2; a=realloc(a, cap*sizeof(int)); }  // גדילה דינמית
        a[n++]=x;
    }
    for(int i=n-1;i>=0;i--) printf("%d%s", a[i], i?" ":"\n");
    free(a);                                                  // שחרור נכון
    return 0;
}
  • א. אין השמת p=NULL לאחר free, ולכן קיים פוטנציאל ל-Dangling Pointer, ל-Use-After-Free או ל-Double Free כאן
  • ב. הכול תקין והקוד בטוח, אף שהיעדר איפוס המצביע לאחר free משאיר Dangling Pointer מסוכן בקוד הזה בעת ריצתו
  • ג. דליפת זיכרון (Memory Leak) פוטנציאלית, אף שהזיכרון כאן דווקא שוחרר ב-free ולכן אין כלל דליפה של זיכרון כאן
  • ד. נעשה שימוש ב-realloc ולא ב-free, אף שהקוד קרא במפורש ל-free ולא ל-realloc על המצביע הזה במהלך ריצתו כאן
תשובה: א

✓ תשובה: א) — MOV [local_var], 0 אחרי free = p=NULL (best practice). אם חסר = ה-Pointer נשאר כ-Dangling Pointer. אם יש שימוש נוסף ב-Pointer = UAF Vulnerability. חפשו: CALL free → ללא MOV 0 → שימוש נוסף ב-Pointer = Bug!