Dynamic Memory
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 — גילוי שגיאות זיכרון
Stack לעומת Heap
שני אזורי זיכרון נפרדים בכל תהליך — עם מאפיינים שונים לחלוטין. הבנת ההבדל ביניהם היא בסיס להבנת Buffer Overflows ושאלות RE.
| מאפיין | Stack | Heap |
|---|---|---|
| ניהול | אוטומטי (push/pop) | ידני — malloc/free |
| גודל | קטן (1–8 MB) | גדול (כמעט כל ה-RAM) |
| מהירות | מאוד מהיר (הזזת מצביע) | איטי יותר (קריאה ל-OS) |
| חיי הנתון | עד סוף הפונקציה | עד קריאה ל-free |
| Overflow | Stack OF → שליטה ב-EIP/RIP | Heap 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 (קוד התוכנה)push 64 / call malloc / test eax,eax / jz .fail / mov [ebp-8], eax. מה קורה כאן?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 אם overflowrealloc — שינוי גודל
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!
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]);מה הפלט?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) שוב בטוח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; // שניהם לNULLHeap 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!- free שלא מאפס ל-NULL, ואחריו שימוש נוסף ב-Pointer
- Heap Allocator מחזיר אותה כתובת ל-malloc הבא עם אותו גודל
- Pattern: free(obj) → malloc(same_size) עם controlled data → obj->fn() → RCE
- UAF ב-Browsers (Chrome, Firefox): אחד הוקטורים הנפוצים 2021–2024
int *p = malloc(4); free(p); printf("%d", *p);?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)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| קריטריון | Valgrind | AddressSanitizer |
|---|---|---|
| מהירות | איטי מאוד (x10–50) | מהיר (x2–5) |
| Compile flag | לא נדרש | חובה: -fsanitize=address |
| פרטים | line numbers מלאים | stack trace מלא |
| Memory Leak | ✅ מצוין | ✅ טוב |
| UAF / Double Free | ✅ | ✅ |
Invalid read of size 4 — block of size 4 free'd. מה זה אומר?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)test eax, eax / jz 0x401234. מה המשמעות?אתגר מעשי — Heap — malloc, realloc ובאגי זיכרון
שיא השיעור: היפוך, ניתוח וזיהוי — כלי ה-RE וה-Low-Level בפעולה. ההגשה נבדקת אוטומטית.
שאלות תרגול ותשובות (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!