GammaCyber

Multithreading ב-C

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

// סקירה

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
🎯 איך זה מופיע במיוני גאמא
◄ שאלון C — pthread_create/join, מה Threads חולקים, Mutex
◄ Black Box CTF — Race Condition Exploit (שני Requests בו-זמנית)
◄ Reverse Engineering — זיהוי pthread_create ב-Assembly, Critical Sections
◄ Pwn — Double-Free ממספר Threads, Use-After-Free מ-Thread שני
// פרק 1

Thread לעומת Process: ההבדל המהותי

ההבדל הקריטי: Thread חולק זיכרון עם Thread אחרים באותו Process. Process (fork) מקבל זיכרון נפרד לחלוטין.

מאפייןProcess (fork)Thread (pthread)
זיכרוןנפרד לחלוטין (COW)משותף! (Heap, Data, FDs)
יצירהfork() — כבד יחסיתpthread_create() — קל ומהיר
Stackנפרד לכל Processנפרד לכל Thread (אך באותו AS)
CrashChild crash לא הורג ParentThread crash הורג כל Process!
ביטחוןSandboxedThread אחד יגשל לזיכרון של אחר
// ─── מה 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!
✏תרגול עצמי3 שאלות
שאלה 1.1 — מה משותף?
מה Threads באותו Process חולקים?
✓ תשובה: ב) — 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 — ++ = LOAD+ADD+STORE = שלוש פעולות לא-אטומיות. שני Threads יכולים ל-LOAD אותו ערך ולהחזיר אותו ערך אחרי ADD → Lost Update. תוצאה: 1,000,000–2,000,000, לא צפויה.
שאלה 1.3 — Crash
Thread אחד גורם ל-Segmentation Fault. מה קורה לשאר ה-Threads?
✓ תשובה: א) — Segfault = Signal SIGSEGV → מסיים את כל ה-Process. כל ה-Threads נהרגים. בניגוד ל-fork: Child crash לא משפיע על Parent. זה הופך Thread crashes לקטסטרופליים יותר.
// פרק 2

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]); }
✏תרגול עצמי3 שאלות
שאלה 2.1 — pthread_create
מה חתימת Thread Function שמועברת ל-pthread_create?
✓ תשובה: ד) void *func(void *arg) — חתימה קבועה ומחייבת. void* מאפשר להעביר כל סוג ולהחזיר כל סוג (עם Cast). השתמש ב-pthread_join לקבלת ה-return value.
שאלה 2.2 — pthread_join
מה קורה אם לא קוראים ל-pthread_join אחרי pthread_create?
✓ תשובה: ד) — ללא 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); }מה יודפס?
✓ תשובה: ב) 1 — pthread_join חוסם עד שה-Thread מסיים. רק אז ממשיכים לprintf. Thread אחד עשה x++ → x=1. אין Race Condition כי ממתינים ל-join לפני הגישה.
// פרק 3

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 // ... }
✏תרגול עצמי4 שאלות
שאלה 3.1 — Mutex עקרון
מה קורה כש-Thread מנסה לקרוא ל-pthread_mutex_lock() כשMutex כבר נעול על ידי Thread אחר?
✓ תשובה: ד) — lock() חוסם עד שMutex זמין. זה מבטיח שרק Thread אחד ב-Critical Section בכל רגע. לחלופין: trylock() מחזיר EBUSY מיידית במקום לחסום.
שאלה 3.2 — Critical Section
מדוע Critical Section צריכה להיות קצרה ככל האפשר?
✓ תשובה: א) — Critical Section ארוכה = Threads אחרים ממתינים = ביצועים של Single-Thread! עקרון: Lock → עדכן מינימלי → Unlock. חישובים כבדים, I/O, sleep — מחוץ ל-Lock.
שאלה 3.3 — Deadlock מניעה
מהו הפתרון הפשוט ביותר למניעת Deadlock עם שני Mutexes?
✓ תשובה: ג) — Consistent Lock Ordering = הפתרון הקלאסי. אם כל Thread נועל תמיד באותו סדר (A→B), לא יווצר Circular Wait — אחד מה-Threads תמיד יצליח לנעול את שניהם.
שאלה 3.4 — Recursive Deadlock
מה קורה כש-Thread קורא ל-pthread_mutex_lock(m) פעמיים ברצף (ללא unlock ביניהם)?
✓ תשובה: ד) Deadlock — Mutex רגיל (non-recursive) + Thread מחזיק + Thread מנסה לנעול שוב = חסימה לנצח. הפתרון: PTHREAD_MUTEX_RECURSIVE attr, או עיצוב מחדש שפונקציה פנימית לא נועלת.
// פרק 4

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; }
🔑 Race Condition ב-CTF — Pattern קלאסי
◄ שלח שני HTTP Requests בו-זמנית לנקודת קצה שמשנה מצב
◄ דוגמה: "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
✏תרגול עצמי3 שאלות
שאלה 4.1 — TOCTOU
מה זה TOCTOU (Time Of Check To Time Of Use)?
✓ תשובה: ג) — 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?
✓ תשובה: ג) — 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. מה הוולנרביליטי?
✓ תשובה: א) Use-After-Free — Thread A: free(ptr) → ptr חוזר ל-Allocator. Thread B: ptr->data → גישה לזיכרון שנמסר. אם Allocator הקצה אותו לThread אחר → B כותב על זיכרון של מישהו אחר = UAF. ב-CTF: UAF → Heap Exploitation.
// פרק 5

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
✏תרגול עצמי — גאמא סגנון3 שאלות
שאלה 5.1 — Semaphore
מה ההבדל בין Mutex לבין Semaphore עם ערך 1?
✓ תשובה: ג) — Mutex: Ownership — רק Thread שנעל יכול לשחרר. Semaphore(1): כל Thread יכול post. זה מאפשר Signal בין Threads (Producer-Consumer). Semaphore(N): מאפשר N גישות בו-זמנית = Thread Pool.
שאלה 5.2 — Thread-Safe
מדוע strtok() אינה Thread-Safe?
✓ תשובה: ב) — 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 + code + unlock = Critical Section Pattern. ה-global var = Shared Resource שמוגן. ב-RE: הבן מה מגן הMutex = הבן את ה-Shared State של התוכנית. זה עוזר לזהות Race Conditions פוטנציאליים.
// תרגול נוסף

תרגול נוסף — Windows: Registry, PID ו-DLL

שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.

✏תרגול נוסף3 שאלות
שאלה 6.1 [קל]
Malware רוצה לשרוד Restart. לאיזה מפתח Registry הוא כותב?
✓ תשובה נכונה: ד — מפתחות Run (ב-HKLM וב-HKCU) מריצים EXE בכל Login → Persistence קלאסי. Autoruns של Sysinternals חושף ערכים כאלה בבדיקת Forensics.
שאלה 6.2 [בינוני]
מה מיוחד ב-SYSTEM process (PID 4) ב-Windows?
✓ תשובה נכונה: ד — SYSTEM (PID 4) = Windows Kernel. אין Parent, חשבון SYSTEM, זיכרון Kernel. Malware שמזריק אליו = Kernel-mode Rootkit מסוכן ביותר.
שאלה 6.3 [קשה]
Process Monitor מציג שתוכנה קוראת C:\Windows\System32\calc.exe ממיקום חריג. מה זה יכול להעיד?
✓ תשובה נכונה: ב — בינארי לגיטימי (calc.exe) בהקשר/מיקום חריג = דגל אדום. שיטות: DLL Hijacking, Masquerading, או Living-off-the-Land. Baseline מזהה חריגה.
// אתגר מעשי

אתגר מעשי — Threads — Deadlock ו-Race

שיא השיעור: תכנות מערכת ברמה נמוכה — זיכרון, קבצים, תהליכים, מקבילות. ההגשה נבדקת אוטומטית.

T3 · בינוני400 נק'~25 דק'algorithmic
זיהוי Deadlock (Circular Wait)
ממש has_deadlock(threads) שמזהה אפשרות ל-deadlock. threads היא רשימה של רצפי-נעילה (שמות מנעולים לפי סדר תפיסה בכל thread). בנה גרף מכוון: לכל שני מנעולים עוקבים a,b ב-thread הוסף קשת a→b (מחזיק a, מבקש b). קיים מעגל בגרף ⇐ Circular Wait ⇐ deadlock פוטנציאלי. החזר True/False.
דוגמאות
has_deadlock([['A','B'], ['B','A']]) → True
has_deadlock([['A','B'], ['A','B']]) → False
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס זיהוי Deadlock (Circular Wait) ›
from collections import defaultdict def has_deadlock(threads: list) -> bool: g = defaultdict(set) for locks in threads: # קשת a->b: מחזיק a ומבקש b for a, b in zip(locks, locks[1:]): g[a].add(b) WHITE, GRAY, BLACK = 0, 1, 2 color = defaultdict(int) def dfs(u): color[u] = GRAY for v in g[u]: if color[v] == GRAY: return True # מעגל = deadlock פוטנציאלי if color[v] == WHITE and dfs(v): return True color[u] = BLACK; return False return any(color[n] == WHITE and dfs(n) for n in list(g))
T3 · בינוני300 נק'~15 דק'analysis
Race Condition — הערך המינימלי
שני threads מריצים counter += 1 מיליון פעמים כל אחד, ללא נעילה (counter מתחיל ב-0). counter += 1 אינו אטומי (קריאה-שינוי-כתיבה). מהו הערך *המינימלי* האפשרי של counter בסיום, בהינתן שזירה עוינת? הגישו מספר.
דוגמאות
(פורמט) → מספר
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס Race Condition — הערך המינימלי ›
בשזירה הגרועה ביותר כמעט כל העדכונים 'אובדים'. המינימום התיאורטי לשני threads הוא 2 (כל thread מבצע לפחות כתיבה אחת שנספרת).
// סיכום

סיכום שיעור 39

✓מה למדנו בשיעור 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
🎯 השיעור הבא
שיעור 40 — Buffer Overflow מתקדם: Stack Smashing, ROP Chains ו-Shellcode | יחידה 5
שאלות תרגול ותשובות (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 מזהה חריגה.