Multithreading ב-C
Multithreading — ריצה מקבילית בתוך Process
Thread הוא זרם הרצה בתוך Process. בניגוד ל-fork, Threads חולקים את אותו Address Space — כולל Heap, Data, FDs. זה מה שהופך אותם למהירים ולמסוכנים כאחד. Race Conditions ו-Deadlocks = מקורות לבאגים קשים ולוולנרביליטים.
- Thread לעומת Process — מה משותף, מה נפרד
- pthread_create / pthread_join — יצירת Threads
- Mutex — הגנה על Critical Section
- Deadlock — מעגל המתנה ומניעה
- Race Condition — TOCTOU ו-Exploit ב-CTF
- Semaphore ו-Condition Variable
- Thread-Safe Functions
- pthread ב-Assembly — זיהוי ב-IDA/Ghidra
◄ Black Box CTF — Race Condition Exploit (שני Requests בו-זמנית)
◄ Reverse Engineering — זיהוי pthread_create ב-Assembly, Critical Sections
◄ Pwn — Double-Free ממספר Threads, Use-After-Free מ-Thread שני
Thread לעומת Process: ההבדל המהותי
ההבדל הקריטי: Thread חולק זיכרון עם Thread אחרים באותו Process. Process (fork) מקבל זיכרון נפרד לחלוטין.
| מאפיין | Process (fork) | Thread (pthread) |
|---|---|---|
| זיכרון | נפרד לחלוטין (COW) | משותף! (Heap, Data, FDs) |
| יצירה | fork() — כבד יחסית | pthread_create() — קל ומהיר |
| Stack | נפרד לכל Process | נפרד לכל Thread (אך באותו AS) |
| Crash | Child crash לא הורג Parent | Thread crash הורג כל Process! |
| ביטחון | Sandboxed | Thread אחד יגשל לזיכרון של אחר |
// ─── מה Threads חולקים ────────────────────────────────
// ✓ Heap (malloc/free — Race Condition!)
// ✓ Global Variables
// ✓ File Descriptors
// ✓ Code (TEXT Section)
// ─── מה Threads לא חולקים ─────────────────────────────
// ✗ Stack (כל Thread = Stack משלו)
// ✗ Registers (Context Switch שומר State)
// ✗ errno (Per-Thread ב-glibc מודרני)
// ─── דוגמה: g_counter++ אינו Atomic! ─────────────────
int g_counter = 0; // shared — Race Condition!
// g_counter++ = 3 פעולות:
// LOAD g_counter → ADD 1 → STORE g_counter
// Thread 1: LOAD(5) → ADD → STORE(6)
// Thread 2: LOAD(5) → ADD → STORE(6) ← Lost Update!
// תוצאה: 6 במקום 7!pthread_create ו-pthread_join: יצירת Threads
קומפילציה: gcc prog.c -o prog -lpthread. Thread Function חייבת להיות void *func(void *arg).
#include <pthread.h>
void *worker(void *arg) {
int id = *(int *)arg; // Cast מ-void* ל-int*
printf("Thread %d: starting\n", id);
return (void *)(long)id; // החזר ערך
}
int main() {
pthread_t t1, t2;
int id1 = 1, id2 = 2;
// pthread_create(tid, attr, func, arg)
pthread_create(&t1, NULL, worker, &id1);
pthread_create(&t2, NULL, worker, &id2);
// t1 ו-t2 רצים במקביל עכשיו!
void *ret1, *ret2;
pthread_join(t1, &ret1); // חסום עד שt1 מסיים
pthread_join(t2, &ret2); // חסום עד שt2 מסיים
printf("t1 returned: %ld\n", (long)ret1); // 1
printf("t2 returned: %ld\n", (long)ret2); // 2
return 0;
}! העברת &i בלולאה — הטעות הנפוצה ביותר ›
// ✗ שגוי — כל Threads מקבלים כתובת של אותו i!
for (int i = 0; i < 5; i++)
pthread_create(&threads[i], NULL, worker, &i);
// i משתנה! Thread 3 עשוי לקרוא i=5
// ✓ נכון — מערך נפרד:
int ids[5];
for (int i = 0; i < 5; i++) {
ids[i] = i;
pthread_create(&threads[i], NULL, worker, &ids[i]);
}int x = 0;
void *inc(void *arg) { x++; return NULL; }
int main() {
pthread_t t;
pthread_create(&t, NULL, inc, NULL);
pthread_join(t, NULL);
printf("%d\n", x);
}מה יודפס?Mutex: הגנה על Critical Section
Mutex (MUTual EXclusion) = מנגנון נעילה: רק Thread אחד יכול להחזיק את ה-Mutex בכל רגע. כל שאר ה-Threads שמנסים לנעול — חוסמים (Block) עד שה-Mutex משתחרר.
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int g_counter = 0;
void *safe_worker(void *arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&mutex); // נעל — Critical Section
g_counter++; // בטוח! רק Thread אחד כאן
pthread_mutex_unlock(&mutex); // שחרר
}
return NULL;
}
// אחרי שני Threads: g_counter = תמיד 2,000,000!
// ─── trylock: נסה בלי לחסום ───────────────────────────
int rc = pthread_mutex_trylock(&mutex);
if (rc == 0) {
// הצלחנו לנעול!
pthread_mutex_unlock(&mutex);
} else {
// EBUSY — מישהו אחר מחזיק
}Deadlock — מבוי סתום
pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;
void *thread_one(void *arg) {
pthread_mutex_lock(&mutexA); // (1) נעל A
sleep(1);
pthread_mutex_lock(&mutexB); // (2) מחכה ל-B → BLOCKED!
// ...
}
void *thread_two(void *arg) {
pthread_mutex_lock(&mutexB); // (3) נעל B
sleep(1);
pthread_mutex_lock(&mutexA); // (4) מחכה ל-A → BLOCKED!
// DEADLOCK!
}
// ─── פתרון: סדר נעילה קבוע ──────────────────────────
// תמיד: נעל A קודם, אחר כך B — בכל Thread!
void *thread_two_fixed(void *arg) {
pthread_mutex_lock(&mutexA); // קודם A (כמו Thread 1)
pthread_mutex_lock(&mutexB); // אחר כך B
// ...
}Race Condition: הוולנרביליטי מ-Concurrency
Race Condition Vulnerability מאפשרת לתוקף לנצל את החלון שבין פעולות שאמורות להיות אטומיות. ב-CTF: שליחת שני Requests בו-זמנית לשבור Logic עסקי.
// ─── קוד פגיע: בנק — TOCTOU ──────────────────────────
int balance = 100;
pthread_mutex_t balance_mutex = PTHREAD_MUTEX_INITIALIZER;
// ✗ פגיע: בדיקה מחוץ ל-Mutex!
int withdraw_unsafe(int amount) {
if (balance >= amount) { // (1) Check — ללא Lock!
// ← Thread אחר קורא withdraw_unsafe כאן!
balance -= amount; // (2) Use
return 1;
}
return 0;
}
// Thread 1: if(100>=80)=TRUE
// Thread 2: if(100>=80)=TRUE ← נקטע לפני Update!
// Thread 1: balance=20
// Thread 2: balance=-60 💥
// ✓ בטוח: Check ו-Use תחת אותו Lock!
int withdraw_safe(int amount) {
int success = 0;
pthread_mutex_lock(&balance_mutex);
if (balance >= amount) { // Check תחת Lock
balance -= amount; // Use תחת אותו Lock
success = 1;
}
pthread_mutex_unlock(&balance_mutex);
return success;
}◄ דוגמה: "Redeem Coupon" — 100 requests מקביליים → כפל הנחה!
◄ בCode Review: חפשו if(check) { use() } ללא Mutex בין check לuse
◄ כלים: Python threading, curl --parallel, Burp Suite Repeater Group
◄ Pwn CTF: שני Threads — אחד malloc/free, השני ניגש לאותו Pointer = UAF
int credits = 10;
void *buy_item(void *arg) {
int cost = *(int*)arg;
if (credits >= cost) { // (A) Check
pthread_mutex_lock(&m);
credits -= cost; // (B) Use
pthread_mutex_unlock(&m);
}
return NULL;
}האם יש Race Condition?Semaphore, Condition Variable ו-pthread ב-Assembly
Semaphore = Mutex כללי (N גישות). Condition Variable = המתנה לאירוע. ב-Assembly: זיהוי pthread_create ו-Critical Sections.
Semaphore
#include <semaphore.h>
sem_t sem;
sem_init(&sem, 0, 3); // מאפשר עד 3 Threads בו-זמנית
void *worker(void *arg) {
sem_wait(&sem); // ↓ ספרה: אם 0 → חסום. אם >0 → המשך
printf("Working...\n");
sleep(1);
sem_post(&sem); // ↑ ספרה
return NULL;
}Condition Variable — Producer-Consumer
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
void *consumer(void *arg) {
pthread_mutex_lock(&mutex);
while (!ready)
pthread_cond_wait(&cond, &mutex); // שחרר Mutex + ישן
// כשמתעורר: Mutex ננעל שוב אוטומטית
printf("Consumer: data ready!\n");
pthread_mutex_unlock(&mutex);
return NULL;
}
void *producer(void *arg) {
sleep(1);
pthread_mutex_lock(&mutex);
ready = 1;
pthread_cond_signal(&cond); // העיר Thread ממתין אחד
// pthread_cond_broadcast() // העיר כולם
pthread_mutex_unlock(&mutex);
return NULL;
}pthread ב-Assembly
; ─── pthread_create (64bit) ─────────────────────────────
lea rdi, [rbp-8] ; &tid
xor esi, esi ; attr = NULL
lea rdx, worker ; Thread function
lea rcx, [rbp-12] ; &arg
call pthread_create
test eax, eax
jnz .error
; ─── Critical Section זיהוי ─────────────────────────────
lea rdi, [rip+mutex]
call pthread_mutex_lock
; ← כל הקוד כאן = Critical Section
lea rdi, [rip+mutex]
call pthread_mutex_unlock
; ─── סימנים לזיהוי ב-IDA/Ghidra ─────────────────────────
; CALL pthread_create + Pointer לפונקציה = Thread יוצא
; lock + code + unlock = Critical Section
; CALL sem_wait + ... + CALL sem_post = Semaphore| פונקציה | Thread-Safe? | חלופה בטוחה | סיבה |
|---|---|---|---|
| strtok() | לא! | strtok_r() | static buffer פנימי |
| rand() | לא! | rand_r() | שיתוף Seed גלובלי |
| ctime() | לא! | ctime_r() | static buffer גלובלי |
| printf() | כן | — | libc נועלת פנימית |
| malloc()/free() | כן | — | per-Arena locking |
תרגול נוסף — Windows: Registry, PID ו-DLL
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
Registry הוא כותב?SYSTEM process (PID 4) ב-Windows?C:\Windows\System32\calc.exe ממיקום חריג. מה זה יכול להעיד?אתגר מעשי — Threads — Deadlock ו-Race
שיא השיעור: תכנות מערכת ברמה נמוכה — זיכרון, קבצים, תהליכים, מקבילות. ההגשה נבדקת אוטומטית.
סיכום שיעור 39
- Thread = זרם הרצה עצמאי בתוך Process. חולק Heap, FDs, Globals — לא Stack
- g_counter++ = לא Atomic! LOAD+ADD+STORE = 3 פעולות → Race Condition
- pthread_create(&tid, NULL, fn, arg): יצור Thread שמריץ fn(arg)
- pthread_join(tid, &ret): המתן לThread שיסיים. ללא join = Resource Leak
- Mutex: Lock → Critical Section → Unlock. רק Thread אחד ב-Critical Section
- Critical Section: קצרה ככל האפשר! לא I/O, לא sleep, לא פונקציות שנועלות
- Deadlock: מעגל המתנה בין Threads. מניעה: סדר נעילה קבוע
- Race Condition: TOCTOU = check ללא lock → exploit ב-CTF
- Semaphore: sem_wait/sem_post. N גישות בו-זמנית = Thread Pool
- Condition Variable: pthread_cond_wait (חסום) + pthread_cond_signal (העיר)
- Non Thread-Safe: strtok, rand, ctime → השתמשו ב-*_r variants
שאלות תרגול ותשובות (19)
שאלה 1.1 — מה משותף? · מה Threads באותו Process חולקים?
- א. את ה-Stack בלבד: הנחה שגויה, אף שדווקא ה-Stack הוא הפרטי לכל Thread ואילו ה-Heap והמשתנים הגלובליים משותפים לכולם כאן
- ב. Heap, משתנים גלובליים, FDs וקוד, אך לא את ה-Stack: לכל Thread יש Stack משלו בעוד שאר המשאבים משותפים לכל החוטים כאן
- ג. את ה-Stack ואת הרגיסטרים: הנחה שגויה, אף שה-Stack והרגיסטרים הם דווקא הפרטיים לכל Thread ואינם משותפים בין כל החוטים כאן
- ד. את הכול כולל את ה-Stack: הנחה שגויה, אף שלכל Thread יש Stack פרטי משלו, ורק שאר המשאבים כמו ה-Heap משותפים לכל החוטים
✓ תשובה: ב) — Threads חולקים: Heap, BSS/Data (globals), FDs, TEXT (code). לא חולקים: Stack (כל Thread = Stack נפרד), Registers (Context נשמר בContext Switch), errno (Per-Thread).
שאלה 1.2 — g_counter++ ו-Atomicity · שני Threads מריצים g_counter++ מיליון פעמים כל אחד ללא Mutex. מה התוצאה הצפויה של g_counter?
- א. תמיד 2,000,000 בדיוק: הנחה שגויה, אף שללא Mutex מתרחשים Lost Updates והתוצאה הסופית קטנה מן הסכום המלא הצפוי בפועל כאן
- ב. יותר מ-2,000,000: הנחה שגויה, אף שה-Race Condition גורם לאיבוד עדכונים ולתוצאה קטנה מן הצפוי ולא לתוצאה גדולה ממנו כאן
- ג. תמיד 0: הנחה שגויה, אף שחלק מן העדכונים כן נשמרים והתוצאה היא ערך חיובי הקטן מ-2,000,000 ולא אפס מוחלט בסופו של דבר כאן
- ד. פחות מ-2,000,000: בשל Race Condition מתרחשים Lost Updates, כאשר שני ה-Threads דורסים זה את עדכוני זה ללא Mutex מגן כאן
✓ תשובה: ד) פחות מ-2,000,000 — ++ = LOAD+ADD+STORE = שלוש פעולות לא-אטומיות. שני Threads יכולים ל-LOAD אותו ערך ולהחזיר אותו ערך אחרי ADD → Lost Update. תוצאה: 1,000,000–2,000,000, לא צפויה.
שאלה 1.3 — Crash · Thread אחד גורם ל-Segmentation Fault. מה קורה לשאר ה-Threads?
#include <pthread.h>
void *worker(void *arg) {
int id = *(int *)arg; // Cast מ-void* ל-int*
printf("Thread %d: starting\n", id);
return (void *)(long)id; // החזר ערך
}
int main() {
pthread_t t1, t2;
int id1 = 1, id2 = 2;
// pthread_create(tid, attr, func, arg)
pthread_create(&t1, NULL, worker, &id1);
pthread_create(&t2, NULL, worker, &id2);
// t1 ו-t2 רצים במקביל עכשיו!
void *ret1, *ret2;
pthread_join(t1, &ret1); // חסום עד שt1 מסיים
pthread_join(t2, &ret2); // חסום עד שt2 מסיים
printf("t1 returned: %ld\n", (long)ret1); // 1
printf("t2 returned: %ld\n", (long)ret2); // 2
return 0;
}- א. כל התהליך נהרג וכל ה-Threads שבו מסתיימים יחד, שכן כל ה-Threads חולקים את אותו מרחב הזיכרון של התהליך המשותף כאן כן
- ב. רק ה-Thread הפגוע נהרג: הנחה שגויה, אף שכל ה-Threads חולקים מרחב זיכרון משותף ולכן קריסה של אחד מהם מפילה את כולם כאן
- ג. התהליך ממשיך בלי ה-Thread הפגוע: הנחה שגויה, אף שקריסת Thread אחד ב-Segfault מפילה את כל התהליך על כל ה-Threads שבו כאן
- ד. מערכת ההפעלה מחזירה את ה-Thread לנקודה בטוחה: הנחה שגויה, אף שה-Segfault מפיל את כל התהליך ואין שום החזרה לנקודה בטוחה
✓ תשובה: א) — Segfault = Signal SIGSEGV → מסיים את כל ה-Process. כל ה-Threads נהרגים. בניגוד ל-fork: Child crash לא משפיע על Parent. זה הופך Thread crashes לקטסטרופליים יותר.
שאלה 2.1 — pthread_create · מה חתימת Thread Function שמועברת ל-pthread_create?
- א. int func(int arg): הנחה שגויה, אף שהחתימה הנדרשת מקבלת ומחזירה מצביע מסוג void* ולא מספר שלם int כמו שמוצג כאן בסעיף
- ב. void func(void): הנחה שגויה, אף שהחתימה הנדרשת מקבלת ארגומנט מסוג void* ומחזירה void* ולא ריקה משני הצדדים כמו כאן
- ג. int *func(int *arg): הנחה שגויה, אף שהחתימה הנדרשת עובדת עם מצביעי void* גנריים ולא עם מצביעים מסוג int* מוגדרים כמו כאן
- ד. void *func(void *arg): הפונקציה מקבלת מצביע גנרי ומחזירה מצביע גנרי, וזוהי החתימה הנדרשת עבור pthread_create בדיוק כאן
✓ תשובה: ד) void *func(void *arg) — חתימה קבועה ומחייבת. void* מאפשר להעביר כל סוג ולהחזיר כל סוג (עם Cast). השתמש ב-pthread_join לקבלת ה-return value.
שאלה 2.2 — pthread_join · מה קורה אם לא קוראים ל-pthread_join אחרי pthread_create?
- א. ה-Thread ממשיך לרוץ לנצח: הנחה שגויה, אף שאם main מסתיים ה-Thread נהרג, ובכל מקרה משאביו אינם משוחררים בלי join כאן כן
- ב. תתקבל שגיאת קומפילציה: הנחה שגויה, אף שהקוד מתקמפל ורץ בהצלחה, והבעיה היא בדליפת המשאבים בזמן ריצה ולא בקומפילציה כאן
- ג. ה-Thread כלל לא יתחיל לרוץ: הנחה שגויה, אף שה-Thread מתחיל לרוץ מיד עם pthread_create, וההיעדר של join רק מדליף משאבים כאן
- ד. Resource Leak: משאבי ה-Thread אינם משוחררים, ואם main מסתיים לפני שה-Thread מסיים, ה-Thread נהרג יחד עם התהליך כאן
✓ תשובה: ד) — ללא join: Thread הוא "Detached" בפועל (אבל לא רשמית) → Resource Leak. אם main() מסיים לפני Thread → כל Process נסגר והThread נהרג. לחלופין: pthread_detach(tid) לשחרור resources אוטומטי.
שאלה 2.3 — Dry Run · int x = 0; void *inc(void *arg) { x++; return NULL; } int main() { pthread_t t; pthread_create(&t, NULL, inc, NULL); pthread_join(t, NULL); printf("%d\n", x); }מה יודפס?
int x = 0;
void *inc(void *arg) { x++; return NULL; }
int main() {
pthread_t t;
pthread_create(&t, NULL, inc, NULL);
pthread_join(t, NULL);
printf("%d\n", x);
}- א. 0: הנחה שגויה שהמשתנה x אינו משתנה, אף שה-Thread מבצע x++ וה-join מבטיח שהעדכון יסתיים לפני ההדפסה ולכן הערך הוא 1 כאן
- ב. 1: Thread יחיד מבצע פעולת x++ אחת, וה-pthread_join גורם ל-main להמתין לו לפני ההדפסה, ולכן הערך המודפס הוא 1 בדיוק כאן
- ג. Undefined Behavior: הנחה שגויה, אף שיש כאן Thread יחיד בלבד ללא כל תחרות (Race), ולכן ההתנהגות מוגדרת והתוצאה היא 1 כאן
- ד. 2: הנחה שגויה שהמשתנה מוגדל פעמיים, אף שרק Thread יחיד נוצר ומבצע x++ פעם אחת בלבד ולכן הערך המודפס הוא 1 בדיוק כאן כן
✓ תשובה: ב) 1 — pthread_join חוסם עד שה-Thread מסיים. רק אז ממשיכים לprintf. Thread אחד עשה x++ → x=1. אין Race Condition כי ממתינים ל-join לפני הגישה.
שאלה 3.1 — Mutex עקרון · מה קורה כש-Thread מנסה לקרוא ל-pthread_mutex_lock() כשMutex כבר נעול על ידי Thread אחר?
- א. מחזיר שגיאה מיידית: הנחה שגויה, אף שהקריאה הרגילה ל-lock חוסמת ומחכה עד לשחרור, ואינה מחזירה שגיאה מיידית כמו trylock כאן
- ב. גורם ל-Deadlock מיידי: הנחה שגויה, אף שהחסימה הרגילה עד לשחרור אינה Deadlock, אלא המתנה תקינה עד שה-Mutex יתפנה בהמשך כאן
- ג. עוקף את הנעילה וממשיך: הנחה שגויה, אף שה-Thread חוסם וממתין עד לשחרור ואינו עוקף כלל את הנעילה שמחזיק בה ה-Thread האחר כאן
- ד. חוסם (Block) את ה-Thread עד שה-Thread המחזיק בנעילה משחרר אותה עם unlock, ורק אז הוא ממשיך את ריצתו הלאה בקוד כאן
✓ תשובה: ד) — lock() חוסם עד שMutex זמין. זה מבטיח שרק Thread אחד ב-Critical Section בכל רגע. לחלופין: trylock() מחזיר EBUSY מיידית במקום לחסום.
שאלה 3.2 — Critical Section · מדוע Critical Section צריכה להיות קצרה ככל האפשר?
- א. ככל שה-Section ארוך יותר, כך Threads אחרים חוסמים וממתינים זמן רב יותר, מה שפוגע קשות בביצועים של התוכנית כולה כאן
- ב. ה-Mutex צורך יותר זיכרון RAM ב-Section ארוך: הנחה שגויה, אף שאורך ה-Section אינו משפיע כלל על צריכת הזיכרון של ה-Mutex
- ג. נעילה יכולה להחזיק ל-100ms בלבד: הנחה שגויה, אף שאין כל מגבלת זמן על משך החזקת הנעילה, והבעיה היא בחסימת שאר ה-Threads
- ד. אין כל חשיבות לאורך ה-Section: הנחה שגויה, אף שאורך ה-Critical Section משפיע ישירות על משך חסימת שאר ה-Threads ועל הביצועים
✓ תשובה: א) — Critical Section ארוכה = Threads אחרים ממתינים = ביצועים של Single-Thread! עקרון: Lock → עדכן מינימלי → Unlock. חישובים כבדים, I/O, sleep — מחוץ ל-Lock.
שאלה 3.3 — Deadlock מניעה · מהו הפתרון הפשוט ביותר למניעת Deadlock עם שני Mutexes?
- א. להשתמש ב-Mutex אחד בלבד: הנחה שגויה, אף שאין זה תמיד אפשרי כשצריך להגן על שני משאבים, והפתרון הכללי הוא סדר נעילה קבוע
- ב. להוסיף sleep בין הנעילות: הנחה שגויה, אף שהוספת השהיה אינה מונעת Deadlock באופן אמין אלא רק מקטינה במקצת את הסבירות שלו
- ג. סדר נעילה קבוע: תמיד לנעול את MutexA לפני MutexB בכל אחד מן ה-Threads, וכך נמנעת לחלוטין נעילה מעגלית שגורמת ל-Deadlock
- ד. להשתמש ב-trylock בלבד: הנחה שגויה, אף ש-trylock מסבך את הקוד ואינו הפתרון הפשוט ביותר, שהוא קביעת סדר נעילה עקבי בין ה-Mutexes
✓ תשובה: ג) — Consistent Lock Ordering = הפתרון הקלאסי. אם כל Thread נועל תמיד באותו סדר (A→B), לא יווצר Circular Wait — אחד מה-Threads תמיד יצליח לנעול את שניהם.
שאלה 3.4 — Recursive Deadlock · מה קורה כש-Thread קורא ל-pthread_mutex_lock(m) פעמיים ברצף (ללא unlock ביניהם)?
// ─── קוד פגיע: בנק — TOCTOU ──────────────────────────
int balance = 100;
pthread_mutex_t balance_mutex = PTHREAD_MUTEX_INITIALIZER;
// ✗ פגיע: בדיקה מחוץ ל-Mutex!
int withdraw_unsafe(int amount) {
if (balance >= amount) { // (1) Check — ללא Lock!
// ← Thread אחר קורא withdraw_unsafe כאן!
balance -= amount; // (2) Use
return 1;
}
return 0;
}
// Thread 1: if(100>=80)=TRUE
// Thread 2: if(100>=80)=TRUE ← נקטע לפני Update!
// Thread 1: balance=20
// Thread 2: balance=-60 💥
// ✓ בטוח: Check ו-Use תחת אותו Lock!
int withdraw_safe(int amount) {
int success = 0;
pthread_mutex_lock(&balance_mutex);
if (balance >= amount) { // Check תחת Lock
balance -= amount; // Use תחת אותו Lock
success = 1;
}
pthread_mutex_unlock(&balance_mutex);
return success;
}- א. הנעילה השנייה מצליחה כי ה-Mutex רקורסיבי: הנחה שגויה, אף ש-Mutex רגיל אינו רקורסיבי ולכן הנעילה השנייה גורמת ל-Deadlock כאן
- ב. מחזיר שגיאה: הנחה שגויה, אף ש-Mutex רגיל אינו מחזיר שגיאה אלא חוסם את ה-Thread על עצמו וגורם ל-Deadlock מוחלט במקרה הזה
- ג. מגדיל מונה פנימי: הנחה שגויה, אף שרק Mutex רקורסיבי מנהל מונה נעילות, ואילו Mutex רגיל נחסם על עצמו וגורם ל-Deadlock כאן כן
- ד. Deadlock: ה-Thread חוסם על עצמו, שכן הוא ממתין לשחרור נעילה שהוא עצמו מחזיק בה ולעולם לא ישחרר אותה תוך כדי ההמתנה כאן
✓ תשובה: ד) Deadlock — Mutex רגיל (non-recursive) + Thread מחזיק + Thread מנסה לנעול שוב = חסימה לנצח. הפתרון: PTHREAD_MUTEX_RECURSIVE attr, או עיצוב מחדש שפונקציה פנימית לא נועלת.
שאלה 4.1 — TOCTOU · מה זה TOCTOU (Time Of Check To Time Of Use)?
- א. כלי למדידת ביצועים של מערכת: הנחה שגויה, אף ש-TOCTOU היא חולשת אבטחה של חלון תחרות בין בדיקה לשימוש ולא כלי מדידה כלשהו
- ב. סוג של מתקפת Buffer Overflow: הנחה שגויה, אף ש-TOCTOU היא חולשת חלון תחרות בזמן ואינה קשורה כלל לגלישת חוצץ בזיכרון כאן
- ג. חלון של Race Condition בין בדיקת התנאי לבין השימוש בו, שבו תוקף יכול לשנות את המצב לפני שהתוכנית פועלת לפי מה שבדקה כאן
- ד. שגיאת קומפילציה: הנחה שגויה, אף ש-TOCTOU היא חולשת זמן ריצה של חלון תחרות בין בדיקה לשימוש ואינה כלל שגיאת קומפילציה כאן
✓ תשובה: ג) — TOCTOU: בדיקה (Check) ואז שימוש (Use) — בין שניהם המצב יכול להשתנות. דוגמה: if(balance>=100) [Thread2 משנה balance] balance-=100. פתרון: הכנס Check+Use תחת אותו Mutex.
שאלה 4.2 — CTF Race Condition · int credits = 10; void *buy_item(void *arg) { int cost = *(int*)arg; if (credits >= cost) { // (A) Check pthread_mutex_lock(&m); credits -= cost; // (B) Use pthread_mutex_unlock(&m); } return NULL; }האם יש Race Condition?
int credits = 10;
void *buy_item(void *arg) {
int cost = *(int*)arg;
if (credits >= cost) { // (A) Check
pthread_mutex_lock(&m);
credits -= cost; // (B) Use
pthread_mutex_unlock(&m);
}
return NULL;
}- א. לא כי ה-Mutex מגן על הכול: הנחה שגויה, אף שהבדיקה עצמה נמצאת מחוץ ל-Mutex ולכן אינה מוגנת ושני Threads עלולים לעבור אותה
- ב. לא כי הבדיקה מחוץ ל-Mutex תקינה: הנחה שגויה, אף שבדיקה מחוץ לנעילה יוצרת חלון TOCTOU שבו שני Threads עוברים את התנאי יחד
- ג. כן: הבדיקה (A) נמצאת מחוץ ל-Mutex ולכן זוהי חולשת TOCTOU, ושני Threads עוברים את הבדיקה עוד לפני שאחד מהם עדכן את credits
- ד. זה תלוי במערכת ההפעלה: הנחה שגויה, אף שחולשת ה-TOCTOU קיימת בכל מערכת שבה הבדיקה מתבצעת מחוץ לנעילה שמגנה על העדכון
✓ תשובה: ג) — Mutex מגן רק על ה-Use (B) — לא על ה-Check (A). שני Threads עם cost=8: שניהם רואים credits=10 ב-(A) ועוברים. Thread1: credits=2. Thread2: credits=-6! תיקון: הכנס גם (A) בתוך Mutex.
שאלה 4.3 — Use-After-Free · Thread A קורא free(ptr). Thread B (שרץ במקביל) קורא ptr->data. מה הוולנרביליטי?
#include <semaphore.h>
sem_t sem;
sem_init(&sem, 0, 3); // מאפשר עד 3 Threads בו-זמנית
void *worker(void *arg) {
sem_wait(&sem); // ↓ ספרה: אם 0 → חסום. אם >0 → המשך
printf("Working...\n");
sleep(1);
sem_post(&sem); // ↑ ספרה
return NULL;
}- א. Use-After-Free (UAF): ה-Thread השני משתמש בזיכרון שכבר שוחרר על-ידי הראשון, מה שעלול להוביל לקריסה או לניצול של החולשה
- ב. Double-Free: הנחה שגויה, אף שכאן רק Thread אחד קורא ל-free בעוד השני ניגש לזיכרון ששוחרר, וזוהי חולשת Use-After-Free כאן
- ג. Buffer Overflow: הנחה שגויה, אף שאין כאן כל כתיבה מעבר לגבול חוצץ, אלא גישה אל זיכרון ששוחרר, שהיא חולשת Use-After-Free
- ד. אין כאן שום בעיה כי ה-Threads מסונכרנים: הנחה שגויה, אף שאין כאן כל סנכרון בין ה-Threads וגישה לזיכרון ששוחרר היא חולשה מסוכנת
✓ תשובה: א) Use-After-Free — Thread A: free(ptr) → ptr חוזר ל-Allocator. Thread B: ptr->data → גישה לזיכרון שנמסר. אם Allocator הקצה אותו לThread אחר → B כותב על זיכרון של מישהו אחר = UAF. ב-CTF: UAF → Heap Exploitation.
שאלה 5.1 — Semaphore · מה ההבדל בין Mutex לבין Semaphore עם ערך 1?
- א. אין ביניהם כל הבדל: הנחה שגויה, אף שההבדל המהותי הוא בבעלות על השחרור, שקיימת ב-Mutex אך לא ב-Semaphore עם ערך 1 כאן
- ב. Semaphore הוא פשוט המהיר יותר מבין השניים: הנחה שגויה, אף שההבדל האמיתי הוא בבעלות על השחרור ולא במהירות של שני המנגנונים
- ג. Mutex: רק ה-Thread שנעל אותו יכול לשחררו, ואילו ב-Semaphore כל Thread יכול לקרוא ל-sem_post ולהעלות את הערך שלו כאן
- ד. Mutex יכול לקבל את הערך N: הנחה שגויה, אף שדווקא ה-Semaphore יכול לקבל ערך N של משאבים, ואילו ה-Mutex הוא בינארי בלבד כאן
✓ תשובה: ג) — Mutex: Ownership — רק Thread שנעל יכול לשחרר. Semaphore(1): כל Thread יכול post. זה מאפשר Signal בין Threads (Producer-Consumer). Semaphore(N): מאפשר N גישות בו-זמנית = Thread Pool.
שאלה 5.2 — Thread-Safe · מדוע strtok() אינה Thread-Safe?
- א. strtok היא פשוט איטית מדי: הנחה שגויה, אף שהבעיה האמיתית היא שמירת המצב במשתנה static פנימי משותף ולא מהירות הביצוע שלה
- ב. strtok שומרת מצב במשתנה static פנימי, ולכן שיתופה בין כמה Threads גורם ל-Race Condition ולשיבוש התוצאות של כל אחד מהם
- ג. strtok כלל אינה עובדת בסביבת לינוקס: הנחה שגויה, אף שהיא עובדת היטב, אך אינה בטוחה לחוטים בשל המשתנה ה-static הפנימי שבה
- ד. strtok אינה מכירה כלל את מנגנון ה-Threads: הנחה שגויה, אף שהבעיה הקונקרטית היא המשתנה ה-static המשותף שיוצר Race Condition
✓ תשובה: ב) — strtok() שומרת מצב (position בstring) ב-static local variable. שני Threads שקוראים strtok() בו-זמנית ידרסו את המצב אחד של השני. חלופה: strtok_r() מקבלת saveptr* מהcaller.
שאלה 5.3 — זיהוי ב-Assembly · ב-IDA רואים: call pthread_mutex_lock, קוד שגורש לglobal var, call pthread_mutex_unlock. מה ניתן להסיק?
- א. הקוד שבין ה-lock ל-unlock הוא Critical Section שמגן על המשתנה הגלובלי מפני Race Condition בעת גישה מקבילה של כמה Threads
- ב. המשתנה הגלובלי מוצפן: הנחה שגויה, אף שהנעילה מגנה על המשתנה מפני גישה מקבילה ואינה מצפינה כלל את תוכנו בזיכרון של התהליך
- ג. הפונקציה אינה בטוחה לחוטים: הנחה שגויה, אף שדווקא השימוש ב-lock וב-unlock סביב הגישה למשתנה הופך את הקטע הזה לבטוח לחוטים
- ד. הפונקציה נקראת רק פעם אחת: הנחה שגויה, אף שהשימוש ב-Mutex מעיד דווקא על ציפייה לקריאות מקבילות מכמה Threads ולא לקריאה יחידה
✓ תשובה: א) — lock + code + unlock = Critical Section Pattern. ה-global var = Shared Resource שמוגן. ב-RE: הבן מה מגן הMutex = הבן את ה-Shared State של התוכנית. זה עוזר לזהות Race Conditions פוטנציאליים.
שאלה 6.1 [קל] · Malware רוצה לשרוד Restart. לאיזה מפתח Registry הוא כותב?
Registry
- א. אל מפתח ה-Fonts: הנחה שגויה, אף שמפתח הגופנים אינו מריץ תוכניות בהפעלה, ו-Persistence מושגת דרך מפתח ה-Run שמריץ EXE בכל Login כאן כן
- ב. אל מפתח ה-Wallpaper: הנחה שגויה, אף שמפתח הרקע אינו קשור להרצה בהפעלה, ו-Malware מנצל את מפתח ה-Run כדי לשרוד אתחול של המערכת
- ג. אל מפתח ה-Timezone: הנחה שגויה, אף שאזור הזמן אינו מריץ קבצים כלל, וה-Persistence הקלאסית נעשית דרך מפתח ה-Run שמפעיל EXE בכל כניסה כאן
- ד. אל HKLM\Software\Microsoft\Windows\CurrentVersion\Run — כל ערך במפתח הזה מריץ קובץ EXE בכל התחברות, ולכן זו שיטת Persistence נפוצה
✓ תשובה נכונה: ד — מפתחות Run (ב-HKLM וב-HKCU) מריצים EXE בכל Login → Persistence קלאסי. Autoruns של Sysinternals חושף ערכים כאלה בבדיקת Forensics.
שאלה 6.2 [בינוני] · מה מיוחד ב-SYSTEM process (PID 4) ב-Windows?
SYSTEM process
- א. זו תוכנית אנטי-וירוס: הנחה שגויה, אף ש-PID 4 הוא ה-Kernel של Windows ולא כלי הגנה, ולכן הוא מריץ קוד Kernel-mode ואינו סורק וירוסים כן
- ב. זהו תהליך DNS מובנה: הנחה שגויה, אף ש-PID 4 הוא ה-Kernel עצמו ולא שירות שמות, ולכן תפקידו הוא הרצת קוד Kernel-mode ולא פענוח DNS
- ג. זהו שרת Web מובנה: הנחה שגויה, אף ש-PID 4 מריץ את ליבת מערכת ההפעלה ואינו מספק דפי אינטרנט, ולכן הוא ה-Kernel Process ולא שרת Web כן
- ד. זהו ה-Kernel Process שמריץ קוד ב-Kernel-mode, יש לו Handles לכל ה-Kernel Objects ולא ניתן להרוג אותו — הזרקה אליו שקולה ל-Rootkit
✓ תשובה נכונה: ד — SYSTEM (PID 4) = Windows Kernel. אין Parent, חשבון SYSTEM, זיכרון Kernel. Malware שמזריק אליו = Kernel-mode Rootkit מסוכן ביותר.
שאלה 6.3 [קשה] · Process Monitor מציג שתוכנה קוראת C:\Windows\System32\calc.exe ממיקום חריג. מה זה יכול להעיד?
C:\Windows\System32\calc.exe
- א. זו סתם פעילות תקינה: הנחה שגויה, אף שקריאה למחשבון ממיקום חריג ובהקשר לא צפוי היא דגל אדום שעשוי להעיד על החלפה זדונית או Hijacking
- ב. ייתכן DLL Hijacking או תחליף זדוני — תהליך שמפעיל calc.exe ממיקום לא צפוי, או ש-calc.exe עצמו הוחלף ב-Malware שמתחזה לתוכנית תמימה
- ג. המחשבון פשוט קרס: הנחה שגויה, אף שקריסה אינה מסבירה קריאה ממיקום חריג, והחשד האמיתי הוא החלפת הקובץ או Hijacking של רכיב תמים
- ד. זו עדכון מערכת שגרתי: הנחה שגויה, אף שעדכונים אינם מפעילים את calc.exe ממיקומים חריגים, וההסבר הסביר הוא ניצול תמים למטרה זדונית כאן כן
✓ תשובה נכונה: ב — בינארי לגיטימי (calc.exe) בהקשר/מיקום חריג = דגל אדום. שיטות: DLL Hijacking, Masquerading, או Living-off-the-Land. Baseline מזהה חריגה.