GammaCyber

Processes ו-Signals ב-Linux

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

// סקירה

Processes ו-Signals ב-Linux: fork, exec, wait, pipe ו-IPC

Process הוא מופע רץ של תוכנה עם זיכרון משלו, FDs משלו ו-PID ייחודי. שיעור זה מלמד fork(), exec(), Signals, ו-IPC עם pipe/dup2 — נושאים קריטיים להבנת CTF ב-Pwn, ניתוח בינאריים, וכתיבת Exploits.

✓נושאי השיעור
  • Process — מבנה, PID, PCB, Process Tree
  • fork() — שכפול Process, Copy-on-Write, ערכי החזרה
  • wait() / waitpid() — המתנה לChild, Zombie Process
  • exec() — החלפת תוכנה ב-Process, fork+exec Pattern
  • Signals — SIGKILL, SIGSEGV, SIGCHLD, signal handlers
  • pipe() — תקשורת חד-כיוונית בין Processes
  • dup2() — הפניית FDs, Reverse Shell Pattern
  • זיהוי fork/exec/Signals ב-Assembly (RE)
🎯 קשר למיוני גאמא
שיעור זה מכין לשלבים: שאלון C — מה מחזיר fork(), הבדל בין Parent לChild, מה exec() מחליף · Black Box CTF — Exploit על Forking Server · Reverse Engineering — זיהוי fork/exec ב-Assembly, זיהוי Signal Handlers · Pwn — Reverse Shell עם dup2+execve
// יסודות מערכת ההפעלה

Kernel Mode ו-User Mode: הגבול הקריטי

זהו הנושא המרכזי של השיעור — ובסיס כל תחום אבטחת המחשבים. ה-CPU עצמו אוכף הפרדה בין קוד עם הרשאות מלאות לקוד עם הרשאות מוגבלות.

CPU Privilege Rings

Ringשםמי משתמשהרשאות
Ring 0Kernel Modeה-Kernel וה-OSגישה מלאה — כל כתובת זיכרון, כל פקודת CPU
Ring 3User Modeכל תוכנת משתמשגישה מוגבלת — רק לזיכרון הווירטואלי שלה
הרחבה מה מותר בכל מצב ›
Kernel Mode (Ring 0)User Mode (Ring 3)
גישה לכל כתובת זיכרון פיזיתגישה רק לזיכרון הווירטואלי של התהליך
הרצת פקודות מיוחסות (CLI, HLT, IN, OUT)אין גישה ישירה לחומרה
שליטה ישירה בחומרה (DMA, Interrupts)לא ניתן לגשת לזיכרון תהליכים אחרים
שינוי Page Tablesכל גישה למשאב עוברת דרך System Call
ניהול תהליכים וזיכרוןChrome, Word, Malware — כולם רצים כאן
💡 אנלוגיה: בית חולים
Kernel Mode = מנתחים בחדר ניתוח. גישה לכל ציוד, כל מטופל. User Mode = מבקרים בלובי. יכולים לדבר עם אחות, לא ניגשים לתרופות ישירות. כדי לקבל תרופה — מבקשים מהאחות (System Call).
! Rootkit — האיום הקשה ביותר ›

אם malware מצליח לרוץ ב-Kernel Mode — הוא מקבל גישה מלאה לכל הזיכרון, לכל התהליכים, לכל ה-Hardware. יכול לקרוא סיסמאות, להשבית אנטי-וירוס, להסתתר לחלוטין מה-OS. זה נקרא Rootkit.

לזכור: קוד שרץ ב-Kernel Mode גם מסוכן יותר — באג בו יכול לקרוס את כל המחשב (Blue Screen / Kernel Panic).
✏תרגול עצמי3 שאלות
שאלה ב׳.1 [קל]
באיזה Ring רץ קוד ה-Kernel?
✓ תשובה: ב) Ring 0 — Ring 0 = Kernel Mode, Ring 3 = User Mode. Rings 1,2 לרוב אינם בשימוש במחשבים מודרניים.
שאלה ב׳.2 [קל]
מה קורה כאשר תוכנית User Mode מבצעת הוראת SYSCALL?
✓ תשובה: ב) — SYSCALL/INT 0x80 = מעבר חומרתי מRing 3 לRing 0. ה-Kernel מטפל בבקשה ומחזיר תשובה.
שאלה א׳.2
מה ההבדל בין Kernel Mode (Ring 0) ל-User Mode (Ring 3)?
✓ תשובה: ד) — Kernel Mode = גישה מלאה לכל החומרה. User Mode = מוגבל לזיכרון הווירטואלי של התהליך, ללא גישה ישירה לחומרה.
// יסודות מערכת ההפעלה

System Calls: הגשר בין User לKernel

System Call הוא מנגנון מסודר שבו תוכנת User Mode מבקשת מה-Kernel לבצע פעולה בשמה — הממשק הרשמי בין עולם המשתמש לעולם ה-Kernel.

הרחבה המסע של printf() — שלב אחרי שלב ›
שלב 1: printf("Hello") ← קריאה ל-C Standard Library (User Mode Ring 3) שלב 2: printf → מפרמטת, קוראת write(fd=1, buf, len) (עדיין User Mode) שלב 3: write() מריצה הוראת SYSCALL ← מכניסה syscall number לRAX (ב-Linux write=1) ← מכניסה ארגומנטים לRDI, RSI, RDX שלב 4: ה-CPU מחליף לRing 0 (Kernel Mode)! שומר את כל registers של User program שלב 5: ה-Kernel: syscall #1 = sys_write מריץ sys_write() → כותב למסך שלב 6: מסיים, שומר ערך חזרה בRAX ה-CPU מחליף חזרה לRing 3! שלב 7: write() מקבלת ערך החזרה, מחזירה לprintf()

טבלת Syscalls חשובים (Linux x86-64)

מספרשםמה עושהחשוב לסייבר
0readקריאה מ-file descriptor–
1writeכתיבה ל-file descriptor–
2openפתיחת קובץmalware פותח /etc/passwd
59execveהרצת תוכנה חדשהShellcode: execve('/bin/sh')
60exitסיום תהליך–
🎯 Syscalls בגאמא
CTF Pwn: Shellcode קלאסי = syscall execve('/bin/sh') → Shell! RAX=59 + RDI='/bin/sh' = הרצת shell. ב-Forensics: strace מציג כל syscall שmalware מבצע.
✏תרגול עצמי3 שאלות
שאלה א׳.3
מה זה System Call ומדוע הוא נדרש?
✓ תשובה: ב) — System Call = בקשה רשמית. כי User Mode לא יכול לגשת לחומרה ישירות — כל גישה לקובץ, רשת, מסך עוברת דרך Kernel.
שאלה ב׳.7 [בינוני]
printf("Hello") ב-C — כמה Syscalls מבוצעים לפחות?
✓ תשובה: ג) 1 — write() — printf → write() → SYSCALL sys_write. STDOUT (fd=1) כבר פתוח — אין צורך ב-open.
שאלה ב׳.17 [קשה]
execve('/bin/sh',NULL,NULL) כ-Syscall — מה ה-RAX שיכניס ה-Shellcode ב-x86-64 Linux?
✓ תשובה: ד) 59 — RAX=59 = syscall number של execve ב-x86-64 Linux. זהו הלב של Shellcode קלאסי: 'תן לי shell'.
// יסודות מערכת ההפעלה

תהליכים (Processes) ו-Threads

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

ProcessThread
מרחב זיכרון נפרדחולק זיכרון עם Threads אחרים
יצירה יקרה (~1ms, מאות KB)יצירה זולה (~10µs, כמה KB)
תקשורת ע"י IPC (pipes, sockets)תקשורת ע"י shared memory (מהירה)
crash בProcess אחד לא פוגע באחרThread שקורס יכול להרוג את כל ה-Process
הרחבה PCB — כרטיס הזהות של תהליך ›

ה-OS שומר עבור כל תהליך מבנה נתונים הנקרא PCB (Process Control Block):

שדהתוכןחשוב לסייבר
PIDמספר זיהוי ייחודימזהה תהליך לכל syscall
Process StateRunning/Ready/Waiting/Terminatedה-Scheduler משתמש בזה
CPU Registersערכי כל הרגיסטרים כשהתהליך לא רץשמור ב-Context Switch
Memory Infoמצביע ל-Page Tableמרחב הזיכרון הוירטואלי
Open Filesרשימת file descriptorsחוקר malware — זה מגלה הכל
Parent PIDמי יצר אותועץ תהליכים — חשוב לForensics
✏תרגול עצמי3 שאלות
שאלה ב׳.4 [קל]
מה ההבדל המרכזי בין Process לThread?
✓ תשובה: ג) — Process = בועה נפרדת עם זיכרון עצמאי. Thread = ביצוע בתוך הבועה — Threads חולקים Heap ו-Code, Stack נפרד.
שאלה ב׳.8 [בינוני]
מה קורה כש-Thread קורס בתוך Process?
✓ תשובה: א) — Thread קורס → SIGSEGV/Exception נשלח ל-Process → ה-Process מסתיים. לכן Chrome מריץ כל Tab כ-Process נפרד!
שאלה ב׳.18 [קשה]
Thread A ו-Thread B בProcess אחד. A כותב למשתנה גלובלי. B קורא אותו. מה ייתכן?
✓ תשובה: ד) Race Condition — שני Threads גולשים לאותו משאב ללא Lock/Mutex. בסייבר: Race Conditions = חולשות (TOCTOU attacks).
// יסודות מערכת ההפעלה

Context Switch וה-Scheduler: אשליית המקביליות

במחשב עם מעבד אחד, רק תהליך אחד רץ ברגע נתון. ה-OS מחליף בין תהליכים מאות פעמים בשנייה — כל כך מהיר שמרגיש במקביל.

הרחבה שלבי Context Switch ›
תהליך A רץ... שלב 1: Timer Interrupt מופעל (כל ~10ms) ה-CPU עובר לKernel Mode שלב 2: ה-Kernel שומר מצב תהליך A ב-PCB(A): − כל הרגיסטרים (EAX, EBX, ..., EIP, ESP) − מצב הזיכרון הווירטואלי שלב 3: ה-Scheduler בוחר תהליך B להריץ הבא שלב 4: טוען PCB(B): משחזר כל הרגיסטרים של B מחליף את ה-Page Table לזו של B שלב 5: ה-CPU חוזר לUser Mode EIP מצביע לאן תהליך B הפסיק תהליך B ממשיך כאילו לא קרה כלום...
⚠️ עלות Context Switch
כל Context Switch עולה ~1,000–10,000 מחזורי שעון ללא עבודה שימושית. מחשב עם 1,000 תהליכים יהיה איטי יותר מ-10 תהליכים — הרבה זמן מתבזבז על החלפה.
✏תרגול עצמי2 שאלות
שאלה ב׳.6 [בינוני]
מדוע Context Switch יקר?
✓ תשובה: ד) — CPU לא עושה עבודה 'אמיתית' בזמן Context Switch. עם 1000 תהליכים, חלק גדול מזמן CPU הולך על Switching.
שאלה א׳.5
מה קורה ב-Context Switch? תאר בקצרה.
✓ תשובה: ג) — Context Switch: שמור PCB(A), בחר B, טען PCB(B), החלף Page Table, חזור לUser Mode. B ממשיך כאילו לא הופסק.
// יסודות מערכת ההפעלה

זיכרון וירטואלי: כל תהליך בעולמו

כל תהליך מקבל מרחב כתובות וירטואלי פרטי. כתובות וירטואליות מתורגמות על ידי החומרה (MMU) לכתובות פיזיות. לכל תהליך מפת תרגום שונה — בידוד מלא.

מפת הזיכרון של תהליך

כתובת גבוהה 0xFFFFFFFF ┌───────────────────┐ │ Kernel Space │ ← User לא גולש 0xC0000000 │───────────────────│ │ Stack │ ← גדל כלפי מטה ↓ (local vars, return addresses!) │ ↓ │ │ (מרווח ריק) │ │ ↑ │ │ Heap │ ← גדל כלפי מעלה ↑ (malloc, new) │───────────────────│ │ BSS segment │ ← גלובלים לא מאותחלים │ Data segment │ ← גלובלים מאותחלים │ Text segment │ ← קוד התוכנית (read-only!) כתובת נמוכה 0x00000000 └───────────────────┘
אזורהרשאותתוכןחשוב לסייבר?
Text (Code)r-xקוד הבינאריBuffer Overflow לא יכול לכתוב לכאן
Heaprw-malloc, newHeap Overflow, Use-After-Free
Stackrw-local vars, return addrBuffer Overflow — הכי נפוץ!
Kernel Space—Kernel code/dataKernel Exploit נדרש לגשת
! Segfault ≠ NULL בלבד ›

Segmentation Fault קורה בכל גישה לעמוד לא ממופה או חסר הרשאה. NULL pointer = עמוד 0 לא ממופה → Segfault. אבל גם גישה לזיכרון שנמחק (Use-After-Free) וגם Buffer Overflow שחרג לאזור לא ממופה — כולם Segfault.

🎯 מפת הזיכרון בגאמא
Buffer Overflow (שיעור 45): ה-Buffer נמצא ב-Stack. Return Address נמצא ב-Stack מעליו. Overflow דורס אותו → EIP מוחלף. זה הלב של ה-exploit.
✏תרגול עצמי4 שאלות
שאלה ב׳.5 [קל]
מה הוא Page Fault?
✓ תשובה: ג) — Page Fault = MMU לא מצא את העמוד ב-RAM. ה-Kernel יכול להביא אותו מSwap (OK) או למצוא שאין ממפה → Segfault.
שאלה ב׳.13 [בינוני]
מה ה-MMU?
✓ תשובה: ג) — MMU = חלק מה-CPU שמבצע כל גישה לזיכרון. משתמש ב-Page Tables ו-TLB. בלי MMU — אין Virtual Memory.
שאלה ב׳.12 [בינוני]
מה אזור ה-Stack מכיל?
✓ תשובה: ב) — Stack גדל כלפי מטה. מכיל stack frames: local vars, saved registers, return address. זה אזור ה-Buffer Overflow הקלאסי!
שאלה ב׳.20 [קשה]
Buffer על ה-Stack גולש. מה הנתון הקריטי שנדרס ראשון?
✓ תשובה: א) — Stack layout: [local buffer][saved EBP][return address]. Overflow ב-Buffer → דורס saved EBP ואז return address → כשהפונקציה מסיימת, EIP = כתובת שלנו.
// יסודות מערכת ההפעלה

Windows לעומת Linux: ההבדלים המרכזיים

שני ה-OS הנפוצים ביותר שונים בארכיטקטורה, פילוסופיה ומימוש. בגאמא תיתקל בשניהם.

נושאLinuxWindows
KernelMonolithic Kernel, Open SourceHybrid Kernel, Closed Source
קבציםהכל הוא קובץ: /dev/sda, /proc/1Drive letters: C:\, Registry
תהליכיםfork() + exec()CreateProcess()
CLIBashCMD / PowerShell
Configקבצי טקסט ב-/etc/Registry
הרחבה Registry ב-Windows וחשיבותו לסייבר ›

Registry = מסד נתונים היררכי המרכז כל הגדרות Windows. כל הגדרת תוכנה, הגדרת מערכת, מה מופעל עם האתחול — הכל ב-Registry.

Hiveתוכןחשוב לסייבר
HKEY_LOCAL_MACHINE (HKLM)הגדרות מחשב — לכל המשתמשיםMalware שמתמיד עצמו כאן
HKLM\SOFTWARE\...\Runמה מופעל עם WindowsPersistence location קלאסי!
HKLM\SYSTEMהגדרות Kernel, DriversDriver exploit analysis
✏תרגול עצמי3 שאלות
שאלה ב׳.9 [בינוני]
malware כותב ל-HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run — מה הוא מנסה לעשות?
✓ תשובה: ג) Persistence — Run Key = רשימת תוכניות שWindows מפעיל בכל login/startup. Malware שמוסיף לכאן = ישרוד Reboot.
שאלה ב׳.10 [בינוני]
מה ההבדל בין fork() ב-Linux לCreateProcess() ב-Windows?
✓ תשובה: ג) — fork() = copy-on-write clone של תהליך האב. CreateProcess() = תהליך חדש לגמרי. fork()+exec() = הפעלת binary חדשה.
שאלה ב׳.19 [קשה]
strace של malware מראה: open('/etc/passwd', O_RDONLY) → read(3,...) → connect(4,{AF_INET,1.2.3.4:80}). מה malware עושה?
✓ תשובה: א) — open('/etc/passwd')+read = קריאת קובץ הסיסמאות. connect לIP חיצוני = שידור לServer של התוקף. Exfiltration קלאסי.
// יסודות מערכת ההפעלה

תרגול נוסף — Kernel, Syscalls וזיכרון וירטואלי

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

✏תרגול נוסף3 שאלות
שאלה ג׳.1 [קל]
תוכנה קוראת open("/etc/passwd"). באיזה Ring רץ הקוד שמבצע את הפתיחה בפועל?
✓ תשובה נכונה: ב — User Mode (Ring 3) שולח Syscall → ה-CPU עובר ל-Ring 0 → ה-Kernel בודק הרשאות, פותח את הקובץ ומחזיר FD. קוד ה-User לעולם לא נוגע בדיסק ישירות.
שאלה ג׳.2 [בינוני]
מה קורה כאשר Process ניגש לכתובת וירטואלית שה-Page שלה לא נמצא כרגע ב-RAM?
✓ תשובה נכונה: ג — Page Fault הוא מנגנון תקין. Segfault מגיע רק אם הכתובת אינה מופיעה ב-Page Table כלל. Page Fault = 'יש דף, הוא פשוט על הדיסק'.
שאלה ג׳.3 [קשה]
Malware רוצה לכתוב Shellcode לתוך הזיכרון של Process אחר ב-Linux. באיזה Syscall הוא ישתמש?
✓ תשובה נכונה: ד — ptrace הוא הממשק ל-Process Injection. GDB משתמש בו ל-Debug, Malware משתמש בו ל-Code Injection. הגנה: ptrace_scope=1 ב-Linux מגביל ל-Parent בלבד.
// יסודות מערכת ההפעלה

תרגול נוסף — זיכרון וירטואלי מתקדם

שאלות סיכום על מרחב הכתובות הווירטואלי ועל ההפרדה בין מרחב המשתמש למרחב הקרנל.

✏תרגול סיום2 שאלות
שאלה ב׳.16 [קשה]
Process מנסה לגשת לכתובת 0xC0000000 ב-Linux 32bit. מה יקרה?
✓ תשובה: ד) — ב-Linux 32bit, 0xC0000000 ומעלה = Kernel Space. גישה מUser Mode = Page Fault + SIGSEGV. ה-OS מגן על Kernel Space!
שאלה ב׳.11 [בינוני]
Process A ב-Virtual Address 0x00401000. Process B באותה כתובת. מה נכון?
✓ תשובה: ד) — Virtual Memory = כל Process חי 'בעולמו'. אותה כתובת וירטואלית = Physical addresses שונות לחלוטין. זה הנקודה של Virtual Memory.
// פרק 1

Process ו-fork(): שכפול תהליך

כל Process ב-Linux הוא ישות עצמאית עם מרחב כתובות נפרד. fork() יוצר Child Process שהוא עותק כמעט מושלם של ה-Parent — ההבדל היחיד הוא ערך החזרה.

מבנה Process — PCB

שדה ב-PCBמשמעותדוגמה/ערך
PIDProcess ID — מזהה ייחודי1234
PPIDParent Process ID1 (init/systemd)
UID/GIDUser/Group ID — הרשאות0 = root, 1000 = user
Address Spaceמיפוי הזיכרון (TEXT/DATA/HEAP/STACK)/proc/PID/maps
File Descriptor Tableכל הFDs הפתוחים0=stdin, 1=stdout, 2=stderr
Signal Handlersטבלת Signal → פונקציית טיפולSIGSEGV → default_handler
Exit Statusקוד יציאה (8 ביטים)0 = הצלחה, 1+ = שגיאה
#include <unistd.h> #include <stdio.h> // pid_t fork(void); // מחזיר: ב-PARENT: PID של Child | ב-CHILD: 0 | שגיאה: -1 pid_t pid = fork(); if (pid < 0) { perror("fork"); // שגיאה return 1; } else if (pid == 0) { // ─── Child Process ────────────────────────────────── printf("Child: PID=%d, Parent=%d\n", getpid(), getppid()); exit(0); // חשוב: child חייב לצאת! } else { // ─── Parent Process ───────────────────────────────── printf("Parent: PID=%d, Child=%d\n", getpid(), pid); } // ─── מה fork() מעתיק ──────────────────────────────────── // ✓ זיכרון (Copy-on-Write), File Descriptors, Signal Handlers // ✗ Threads (רק Thread אחד עובר לChild), Locks (→ Deadlock!) // ─── wait(): Parent ממתין ל-Child ─────────────────────── int status; pid_t finished = waitpid(pid, &status, 0); if (WIFEXITED(status)) { int code = WEXITSTATUS(status); // קוד יציאה printf("Child exited with code %d\n", code); }
הרחבה Copy-on-Write ו-Zombie Processes ›

Copy-on-Write (COW): fork() לא מעתיק כל הזיכרון מיידית. Parent ו-Child חולקים Pages פיזיים (Read-Only) — רק כשאחד כותב לPage הקרנל יוצר עותק. תוצאה: fork() מהיר גם לProcesses גדולים. ב-CTF: Child רואה אותם ערכי Stack/Heap כמו Parent לפני כתיבה — דולף מידע!

Zombie Process: Child שסיים אבל Parent לא קרא wait() = ZOMBIE. תופס שורה בProcess Table אבל לא משאבים. הרבה Zombies = בעיה (מנתקים PID slots).

// ─── מניעת Zombies ────────────────────────────────────── // שיטה 1: תמיד קרא waitpid() waitpid(child_pid, &status, 0); // שיטה 2: התעלם מ-SIGCHLD (קרנל מנקה אוטומטית) signal(SIGCHLD, SIG_IGN); // ─── Dry Run: מה הפלט? ────────────────────────────────── int x = 10; pid_t pid = fork(); if (pid == 0) { x += 5; printf("Child: x = %d\n", x); // → "Child: x = 15" exit(0); } else { wait(NULL); printf("Parent: x = %d\n", x); // → "Parent: x = 10" } // מרחבי זיכרון נפרדים! שינוי ב-Child לא משפיע על Parent
✏תרגול עצמי3 שאלות
שאלה 1.1 — fork() Return Value
קוד קורא ל-pid_t pid = fork();. מה ערך pid ב-Child Process?
✓ תשובה: ב) 0 — זוהי ה-API של fork(). Child מקבל 0 → בדוק if(pid==0) לדעת שאתה ב-Child. Parent מקבל PID של Child → מאפשר waitpid(pid, ...) ספציפי. -1 = שגיאה (פחות FDs, Limit הגיע).
שאלה 1.2 — Copy-on-Write
מדוע fork() מהיר גם כשהProcess הוא 500MB?
✓ תשובה: א) COW — Copy-on-Write: במקום להעתיק 500MB, הקרנל יוצר Page Table חדשה שמצביעה לאותם Pages פיזיים, Read-Only. רק כשChild כותב ל-Page, הקרנל מעתיק אותו (Page Fault). תוצאה: fork() = O(1) כמעט, לא O(n) לפי גודל הזיכרון.
שאלה 1.3 — Dry Run: fork output
int main() { pid_t pid = fork(); if (pid == 0) { printf("Child\n"); exit(0); } else { wait(NULL); printf("Parent\n"); } return 0; }מה הפלט ומה הסדר המובטח?
✓ תשובה: א) Child\nParent — מובטח — wait(NULL) = Parent חוסם עד שChild מסיים. סדר: 1) fork() → שני Processes רצים. 2) Child מדפיס "Child" ו-exit(0). 3) wait() מחזיר ל-Parent. 4) Parent מדפיס "Parent". ללא wait(): הסדר לא מובטח.
// פרק 2

exec(): החלפת תוכנה ב-Process

exec() מחליפה את הקוד הרץ ב-Process הנוכחי בתוכנה אחרת. ה-PID נשאר זהה, אבל כל הזיכרון והקוד מוחלפים. exec() לא חוזרת אם הצליחה.

#include <unistd.h> // ─── משפחת exec ──────────────────────────────────────────── // execl: ארגומנטים בנפרד, נתיב מלא // execv: ארגומנטים כמערך, נתיב מלא // execvp: ארגומנטים כמערך, מחפש ב-PATH // execve: ארגומנטים + environment (System Call ישיר) // ─── fork + exec: Pattern הקלאסי ─────────────────────────── pid_t pid = fork(); if (pid == 0) { // Child: החלף עצמנו ב-ls char *args[] = {"ls", "-la", NULL}; execvp("ls", args); perror("exec failed"); // רק אם exec נכשל! exit(1); } else { // Parent: ממתין ל-Child שסיים int status; waitpid(pid, &status, 0); printf("ls finished\n"); } // ─── מה exec שומר ומה מוחק ───────────────────────────────── // ✓ שומר: PID, FDs הפתוחים*, Signal Mask, UID/GID // ✗ מוחק: קוד, Stack, Heap, BSS, Signal Handlers // * FDs עם O_CLOEXEC נסגרים אוטומטית
🎯 חשוב לגאמא — execve ב-Exploit
הGoal הקלאסי של Exploitation: execve("/bin/sh", ["/bin/sh"], NULL) ← מחליף את התוכנה הפגיעה ב-shell!
◄ ב-32bit x86: int 0x80 עם eax=11 (SYS_execve), ebx=path, ecx=argv, edx=envp
◄ ב-64bit x64: syscall עם rax=59, rdi=path, rsi=argv, rdx=envp
◄ ב-RE: CALL execve עם "/bin/sh" = backdoor ב-100%
! exec() לא חוזרת — מלכודת נפוצה ›

אם exec() הצליחה, הקוד שאחריה לא יגיע לעולם כי הקוד כולו הוחלף. לכן: perror("exec") רק מגיעים אליו אם exec() נכשלה.

execvp("ls", args); perror("exec"); // ← מגיעים לכאן רק אם exec נכשל exit(1); // ← רק אם exec נכשל // טעות נפוצה: לצפות לשורה אחרי exec() לרוץ execvp("ls", args); printf("this never prints\n"); // ← לא יודפס!
כלל: תמיד שים perror() ו-exit(1) לאחר exec() ב-Child. אחרת — שגיאות חבויות.
✏תרגול עצמי3 שאלות
שאלה 2.1 — exec() Basics
מה ה-PID של Process לאחר exec()?
✓ תשובה: ד) אותו PID — זהו הבדל חשוב בין fork() לexec(). fork(): PID חדש לChild. exec(): PID נשמר, הכל אחר מוחלף. לכן fork()+exec() = Process חדש עם Program חדש. בRE: אם ראית execve, ה-PID של הProcess לא השתנה.
שאלה 2.2 — execve ב-Exploit
ב-64bit Linux Exploit, מה ה-syscall number של execve?
✓ תשובה: ג) 59 — בx86_64: SYS_execve = 59. רגיסטרים: rax=59, rdi=pathname, rsi=argv[], rdx=envp[]. בx86 32bit: SYS_execve = 11, eax=11, ebx=path, ecx=argv, edx=envp. טעות נפוצה: לבלבל בין 32bit (11) ל-64bit (59).
שאלה 2.3 — Dry Run: fork+exec
pid_t pid = fork(); if (pid == 0) { char *args[] = {"/bin/echo", "hello", NULL}; execvp("/bin/echo", args); printf("EXEC FAILED\n"); exit(1); } else { wait(NULL); printf("DONE\n"); }מה הפלט אם execvp מצליח?
✓ תשובה: ב) "hello\nDONE" — execvp מצליח: Child הפך ל-/bin/echo → מדפיס "hello\n" ויוצא. "EXEC FAILED" ו-exit(1) לא מגיעים אליהם. Parent: wait() מחזיר אחרי Child יוצא → "DONE". סדר: echo (Child) → DONE (Parent, אחרי wait).
// פרק 3

Signals: תקשורת אסינכרונית

Signal הוא הודעה אסינכרונית לProcess. הקרנל שולח SIGSEGV ב-NULL dereference, Ctrl+C שולח SIGINT. Processes יכולים לתפוס ולטפל ב-Signals — חוץ מ-SIGKILL ו-SIGSTOP.

Signalמספרסיבה קלאסיתברירת מחדלניתן לתפוס?
SIGKILL9kill -9 — הרג מיידיסיום מיידילא! לעולם
SIGTERM15kill — בקשת סיום מנומסתסיוםכן
SIGINT2Ctrl+C מה-Terminalסיוםכן
SIGSEGV11Segmentation Fault (NULL deref)Core Dumpכן (לא מומלץ)
SIGCHLD17Child סייםהתעלמותכן
SIGUSR1/210/12שימוש מותאם אישיתסיוםכן
SIGSTOP19עצירה (Ctrl+Z, gdb)עצירהלא! לעולם
#include <signal.h> // ─── שליחת Signal ───────────────────────────────────────── kill(pid, SIGTERM); // שלח SIGTERM ל-pid kill(pid, SIGKILL); // הרג מיידי (לא ניתן לתפוס!) raise(SIGUSR1); // שלח Signal לעצמי // ─── Signal Handler ──────────────────────────────────────── void my_handler(int signum) { // ⚠️ רק async-signal-safe functions מותר כאן! write(1, "Signal!\n", 8); // write() בטוח. printf() לא! } signal(SIGINT, my_handler); // Ctrl+C → my_handler signal(SIGTERM, my_handler); // kill → my_handler signal(SIGINT, SIG_IGN); // התעלם מ-SIGINT signal(SIGINT, SIG_DFL); // חזור לברירת מחדל // ─── Pattern בטוח: Handler מציב flag ───────────────────── volatile sig_atomic_t g_stop = 0; void handler(int s) { g_stop = 1; } // בטוח! פעולה אטומית // main loop: while (!g_stop) { /* do work */ } // בדוק flag
! Signal Handler ו-Async Safety ›

Signal Handler נקרא אסינכרונית — תוך כדי כל פעולה אחרת! אסור לקרוא malloc(), free(), printf() בתוך Signal Handler — אם הSignal קטע malloc(), תהיה Deadlock על internal lock.

מותר: write(), _exit(), sem_post(), פעולות אטומיות. Pattern בטוח: Handler מציב flag (volatile sig_atomic_t), ה-main loop בודק.

כלל: Signal Handler = כמה שפחות קוד. רק הצב flag ותצא.
✏תרגול עצמי3 שאלות
שאלה 3.1 — SIGKILL vs SIGTERM
מדוע לא ניתן לתפוס SIGKILL?
✓ תשובה: א) — SIGKILL/SIGSTOP = Kernel-Level signals. הקרנל לא שולח ל-Process ומחכה לHandler — הוא פשוט מסיים/עוצר מיידית. זה מבטיח שתמיד ניתן להרוג Process. SIGTERM ניתן לתפיסה → תוכנות יכולות cleanup לפני יציאה. SIGKILL = "אין ברירה".
שאלה 3.2 — Signal Handler Safety
איזו פעולה בטוחה בתוך Signal Handler?
✓ תשובה: ג) volatile sig_atomic_t flag — printf(), malloc(), free() לא async-signal-safe: הם משתמשים ב-internal locks. אם Signal קטע אותן → Deadlock. write() היא async-signal-safe (גם היא בסדר). Pattern בטוח: Handler = flag בלבד, main loop = לוגיקה.
שאלה 3.3 — Dry Run: Signal ב-RE
ב-IDA/Ghidra רואים: CALL signal + push 11 + push handler_func. מה הקוד עושה?
✓ תשובה: א) — signal(SIGSEGV, handler_func): Signal 11 = SIGSEGV. מה זה אומר ב-RE? התוכנה מנסה לשרוד SIGSEGV (NULL deref, buffer overflow) ולהמשיך לרוץ במקום לקרוס. ב-Exploit: SIGSEGV Handler יכול לחסום ניסיונות ניצול. ב-CTF: חפש מה handler_func עושה — לפעמים מדפיס Flag!
// פרק 4

pipe ו-dup2: תקשורת בין Processes

pipe() יוצר ערוץ תקשורת חד-כיווני בין Processes. dup2() מפנה File Descriptors. יחד הם מאפשרים Piping בין Processes — ואת Pattern ה-Reverse Shell הקלאסי ב-CTF.

#include <unistd.h> // ─── pipe(): FD[0]=קריאה, FD[1]=כתיבה ──────────────────── int pipefd[2]; pipe(pipefd); pid_t pid = fork(); if (pid == 0) { // Child: כותב ל-Pipe close(pipefd[0]); // סגור קצה קריאה write(pipefd[1], "Hello!", 6); close(pipefd[1]); exit(0); } else { // Parent: קורא מ-Pipe close(pipefd[1]); // סגור קצה כתיבה char buf[128] = {0}; read(pipefd[0], buf, sizeof(buf)-1); printf("Got: %s\n", buf); // "Got: Hello!" close(pipefd[0]); wait(NULL); } // ─── dup2(): הפנית FDs ──────────────────────────────────── // dup2(oldfd, newfd): newfd יצביע על אותו קובץ כמו oldfd // ─── Pattern: redirect stdout → Pipe ───────────────────── if (pid == 0) { close(pipefd[0]); dup2(pipefd[1], STDOUT_FILENO); // stdout = write-end close(pipefd[1]); char *args[] = {"ls", "-la", NULL}; execvp("ls", args); // כל printf של ls → Pipe! } // ─── CTF: Reverse Shell ──────────────────────────────────── // dup2(sock_fd, 0); // stdin = socket // dup2(sock_fd, 1); // stdout = socket // dup2(sock_fd, 2); // stderr = socket // execve("/bin/sh", ...); // shell עם I/O דרך network!
🎯 חשוב לגאמא — fork+exec+dup2 ב-Exploit
Pattern קלאסי ב-Pwn/CTF: Reverse Shell
◄ פתח TCP socket ל-attacker
◄ dup2(sock, 0); dup2(sock, 1); dup2(sock, 2); ← redirect stdin/stdout/stderr
◄ execve("/bin/sh", ...); ← shell עם I/O דרך הsocket
← התוקף מקבל shell על המכונה הקורבן!
ב-RE: חפשו CALL dup2 + CALL execve סמוך = Reverse Shell Code
✏תרגול עצמי3 שאלות
שאלה 4.1 — pipe() FDs
לאחר int pipefd[2]; pipe(pipefd);, כיצד Process כותב ל-Pipe?
✓ תשובה: ג) pipefd[1] — מוסכמה: pipefd[0] = read-end (קריאה), pipefd[1] = write-end (כתיבה). כלל מנמוני: 0=קלט, 1=פלט (כמו stdin=0, stdout=1). Process שכותב: close(pipefd[0]) → write(pipefd[1], ...). Process שקורא: close(pipefd[1]) → read(pipefd[0], ...).
שאלה 4.2 — dup2 Reverse Shell
ב-CTF Exploit קוד: dup2(sock_fd, 0); dup2(sock_fd, 1); execve("/bin/sh", ...);מה זה עושה?
✓ תשובה: ב) Reverse Shell — dup2(sock_fd, 0): stdin (FD 0) = socket. dup2(sock_fd, 1): stdout (FD 1) = socket. execve("/bin/sh"): מריץ shell. עכשיו: מה שהתוקף כותב לsocket הולך ל-stdin של shell. מה שshell מדפיס הולך ל-stdout = socket = התוקף. תוצאה: shell מרוחק!
שאלה 4.3 — Dry Run: dup2+exec
int pipefd[2]; pipe(pipefd); pid_t pid = fork(); if (pid == 0) { close(pipefd[0]); dup2(pipefd[1], 1); // stdout → pipe close(pipefd[1]); execvp("echo", (char*[]){"echo","test",NULL}); } else { close(pipefd[1]); char buf[32]={0}; read(pipefd[0], buf, 31); printf("Got: %s", buf); wait(NULL); }מה יודפס?
✓ תשובה: ג) "Got: test\n" — dup2(pipefd[1], 1): stdout של Child = Pipe write-end. execvp("echo", ["echo","test"]): echo מדפיס "test\n" ל-stdout → הולך ל-Pipe. Parent: read(pipefd[0], buf, 31) → buf = "test\n". printf("Got: %s", buf) → "Got: test\n".
// פרק 5

fork/exec/Signals ב-Assembly: זיהוי ב-RE

ב-CTF ובמיון גאמא, עליך לזהות fork(), exec(), Signals ב-Assembly כשאין קוד מקור. הנה הפטרנים הקריטיים לזיהוי.

; ─── fork() ב-Assembly (32bit Linux) ────────────────────── ; System Call: eax=2 (SYS_fork) mov eax, 2 int 0x80 test eax, eax jz .child_code ; אם 0 → אנחנו ב-Child js .error ; אם שלילי → שגיאה ; Parent code... .child_code: ; Child code... ; ─── execve("/bin/sh") ב-Assembly (32bit) ───────────────── ; eax=11 (SYS_execve), ebx=path, ecx=argv, edx=envp xor eax, eax push eax ; NULL push eax ; NULL push offset sh_str ; "/bin/sh" mov ecx, esp ; argv = ["/bin/sh", NULL] xor edx, edx ; envp = NULL mov ebx, offset sh_str mov eax, 11 ; SYS_execve (32bit) int 0x80 ; ─── fork() ב-Assembly (64bit) ───────────────────────────── ; System Call: rax=57 (SYS_fork) או rax=56 (clone) mov rax, 57 syscall test rax, rax jz .child_code ; ─── סימנים לזיהוי ב-IDA/Ghidra ─────────────────────────── ; CALL fork + TEST EAX,EAX + JZ = fork pattern ; MOV EAX,11 / INT 0x80 + "/bin/sh" string = execve backdoor! ; CALL signal + push 11 (SIGSEGV) + push ptr = SEH/Signal handler ; CALL dup2 + CALL execve = Reverse Shell pattern!
הרחבה Forking Server וניצול ב-CTF ›

Forking Server: שרת שעבור כל connection עושה fork() ל-Child שמטפל בבקשה. CTF exploit pattern: כשהChild קורס (SIGSEGV), ה-Parent שורד. זה מאפשר לנסות ניצול שוב ושוב — ASLR נשאר זהה בין Connections כי מרחב הכתובות זהה (COW)!

// ─── Forking Server (קלאסי ב-CTF Pwn) ───────────────────── while (1) { int client = accept(sock, ...); pid_t pid = fork(); if (pid == 0) { // Child: טפל בconnection handle_client(client); exit(0); } else { // Parent: המשך להאזין close(client); // לא wait() → Zombie process, אבל שרת ממשיך } } // ← CTF Trick: כל Child מקבל אותו מרחב כתובות כמו Parent // ASLR randomized פעם אחת בהפעלה → זהה בכל fork()! // → Brute Force של ASLR אפשרי!
✏תרגול עצמי3 שאלות
שאלה 5.1 — fork() ב-Assembly
ב-IDA רואים: mov eax, 2 int 0x80 test eax, eax jz loc_CHILDמה מתאר הקוד?
✓ תשובה: ד) fork() — SYS_fork = 2 ב-32bit Linux. int 0x80 = syscall. test eax,eax: אם eax=0 → ZF=1 → jz קופץ לChild code. אם eax>0 (PID of Child) → ZF=0 → jz לא קופץ → Parent code. זהו הPattern הסטנדרטי של fork() ב-Assembly. ב-64bit: rax=57 + syscall.
שאלה 5.2 — Backdoor זיהוי
ב-Ghidra רואים: CALL execve ובאזור ה-data: "/bin/sh". מסקנה?
✓ תשובה: ב) Backdoor — CALL execve + "/bin/sh" = Red Flag מיידי ב-Malware RE. execve("/bin/sh") = מריץ shell בהקשר של הProcess הנוכחי (uid, permissions). אם זה daemon שרץ כroot → shell כroot! בCTF: זה הGoal של Pwn challenges — להגיע לנקודה שexecve("/bin/sh") רץ.
שאלה 5.3 — Dry Run: Forking Server
Forking Server עושה fork() לכל connection. ב-CTF, ASLR מופעל. מדוע quand זה עדיין ניתן לניצול?
✓ תשובה: ב) COW + same address space — ASLR randomizes ב-exec(). fork() = Copy-on-Write, לא exec() → כתובות זהות לParent. כל Child מקבל אותו מרחב כתובות. תוצאה: Brute Force ASLR — ניסיון אחרי ניסיון, Child קורס ו-Parent מאזין לחיבור הבא. 256 ניסיונות לנחש 8 ביט (ASLR entopy נמוך בLinux 32bit).
// פרק 6

Dry Run מלא + סיכום

הנה Dry Run מלא לשיעור — שאלות בסגנון מיון גאמא. הכן אותן לפני שתמשיך לשיעור הבא.

// ─── שאלה: מה הפלט? ───────────────────────────────────────── #include <stdio.h> #include <unistd.h> #include <sys/wait.h> int x = 10; int main() { pid_t pid = fork(); if (pid == 0) { x += 5; printf("Child: x = %d\n", x); exit(0); } else { wait(NULL); printf("Parent: x = %d\n", x); } return 0; } // ─── ניתוח ──────────────────────────────────────────────── // fork() יוצר עותק של x = 10 לChild (Copy-on-Write) // Child: x += 5 → x = 15 ב-Child בלבד // Child מדפיס "Child: x = 15" ויוצא // Parent: wait() חוסם עד שChild מסיים // Parent: x של Parent = 10 (לא שונה!) // Parent מדפיס "Parent: x = 10" // ─── פלט (סדר מובטח כי Parent עושה wait) ──────────────── // Child: x = 15 // Parent: x = 10
✓מה למדנו בשיעור 37
  • Process: ישות עצמאית עם PID, Address Space, FDs, Signal Handlers משלה
  • fork(): מחזיר 0 ב-Child, PID ב-Parent, -1 בשגיאה. Copy-on-Write!
  • fork() מעתיק: זיכרון (COW), FDs, Handlers. לא מעתיק: Threads, Locks
  • wait()/waitpid(): Parent מחכה ל-Child. ללא wait() = Zombie Process
  • exec(): מחליף קוד+זיכרון. שומר PID+FDs. לא חוזרת אם הצליחה
  • fork()+exec() = Pattern ליצירת Process חדש שמריץ תוכנה אחרת
  • SIGKILL/SIGSTOP: לא ניתן לתפוס. SIGTERM/SIGINT: ניתן לתפוס
  • Signal Handlers: רק async-signal-safe! volatile sig_atomic_t לflags
  • pipe(): FD[0]=קריאה, FD[1]=כתיבה. חד-כיווני. Block עד שיש נתונים
  • dup2(old, new): הפנה FD. dup2(sock, 0/1/2) = Reverse Shell pattern
  • ב-RE: fork+test eax+jz = fork detection. execve+"/bin/sh" = Backdoor
🎯 השיעור הבא
שיעור 38 — Sockets ורשת ב-C: TCP/UDP, bind, accept, connect
✏תרגול סיום — גאמא סגנון3 שאלות
גאמא סגנון א׳ — Zombie
Server שעושה fork() לכל connection אבל לא קורא wait(). מה יקרה לאורך זמן?
✓ תשובה: ג) Zombie accumulation — Child שיצא ללא wait() = Zombie. Zombie = Entry ב-Process Table, לא משאבי CPU/Memory. PID Table מוגבל (default: 32768 PIDs). אחרי מספיק connections ללא wait(): כל ה-PIDs תפוסים → fork() חדש נכשל (errno=EAGAIN). פתרון: signal(SIGCHLD, SIG_IGN) → Kernel מנקה אוטומטית.
גאמא סגנון ב׳ — RE Pattern
ב-IDA רואים ב-binary חשוד: CALL dup2 (3 פעמים עם args 0,1,2) ואחריו CALL execve עם "/bin/sh". מה המסקנה?
✓ תשובה: ב) Reverse Shell — dup2(sock,0)+dup2(sock,1)+dup2(sock,2)+execve("/bin/sh") = Reverse Shell הקלאסי. כל stdin/stdout/stderr של shell הולך לsocket. ב-CTF: זה הGoal שאתה מנסה להגיע אליו. ב-RE/Malware Analysis: זוהי Backdoor ודאית — דווח מיד.
גאמא סגנון ג׳ — Dry Run: exec לא חוזרת
pid_t pid = fork(); if (pid == 0) { printf("A\n"); execvp("ls", (char*[]){"ls", NULL}); printf("B\n"); exit(0); } else { wait(NULL); printf("C\n"); }מה הפלט (הנח שls מצליח ומדפיס "file.txt")?
✓ תשובה: ד) A\nfile.txt\nC — Child: printf("A") → "A". execvp("ls"): מצליח → Child הפך ל-ls → מדפיס "file.txt". printf("B") לא מגיעים — exec הצליח! exit(0) לא מגיעים. Parent: wait() → ls יוצא → Parent מתעורר → printf("C") → "C". סדר: A → file.txt → C.
// תרגול נוסף

תרגול נוסף — אבטחת רשת: DNS, HSTS ו-TLS

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

✏תרגול נוסף3 שאלות
שאלה 6.1 [קל]
מה זו מתקפת DNS Cache Poisoning?
✓ תשובה נכונה: ג — הזרקת רשומה כוזבת ל-DNS Cache → כל הפונים ל-Resolver מנותבים לאתר זדוני. הגנה: DNSSEC (חתימות), ו-Randomized Query ID.
שאלה 6.2 [בינוני]
מה עושה HSTS (Strict-Transport-Security)?
✓ תשובה נכונה: ג — HSTS Header → הדפדפן זוכר: 'אתר זה HTTPS בלבד'. גם קישור http:// יומר אוטומטית ל-https://. מגן מ-SSL Strip (MITM שמוריד ל-HTTP).
שאלה 6.3 [קשה]
ב-Wireshark, בחיבור TLS 1.3, מה עדיין ניתן לראות?
✓ תשובה נכונה: א — TLS מצפין תוכן, לא מטא-נתונים. גלוי: IP, פורטים, גודל/תזמון פקטות, ו-SNI (שם הדומיין ב-ClientHello). ECH מנסה להצפין גם את ה-SNI.
// אתגר מעשי

אתגר מעשי — תהליכים — fork ו-IPC

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

T3 · בינוני300 נק'~15 דק'analysis
ספירת תהליכי fork
כמה פעמים תודפס השורה 'hi' כשהתוכנית רצה? (רמז: כל fork() מכפיל את מספר התהליכים.) הגישו מספר. #include <stdio.h> #include <unistd.h> int main(){ fork(); fork(); fork(); printf("hi\n"); return 0; }
דוגמאות
(פורמט) → מספר, למשל 8
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס ספירת תהליכי fork ›
3 קריאות fork() → 2^3 = 8 תהליכים, כל אחד מדפיס 'hi' → 8 שורות.
שאלות תרגול ותשובות (44)

שאלה ב׳.1 [קל] · באיזה Ring רץ קוד ה-Kernel?

  • א. Ring 3: הנחה שגויה שקוד הקרנל רץ ב-Ring 3, אף ש-Ring 3 הוא מצב המשתמש ואילו קוד הקרנל רץ דווקא ב-Ring 0 בעל הגישה המלאה בפועל בעצמו כאן
  • ב. Ring 0: זהו מצב הקרנל בעל הגישה המלאה, בעוד Ring 3 הוא מצב המשתמש, ואילו Rings 1 ו-2 לרוב אינם בשימוש במחשבים מודרניים כלל בפועל כאן
  • ג. Ring 1: הנחה שגויה שקוד הקרנל רץ ב-Ring 1, אף ש-Rings 1 ו-2 לרוב אינם בשימוש ואילו קוד הקרנל רץ דווקא ב-Ring 0 בעל הגישה המלאה בפועל כאן
  • ד. Ring 2: הנחה שגויה שקוד הקרנל רץ ב-Ring 2, אף ש-Rings 1 ו-2 לרוב אינם בשימוש ואילו קוד הקרנל רץ דווקא ב-Ring 0 בעל הגישה המלאה בפועל כאן
תשובה: ב

✓ תשובה: ב) Ring 0 — Ring 0 = Kernel Mode, Ring 3 = User Mode. Rings 1,2 לרוב אינם בשימוש במחשבים מודרניים.

שאלה ב׳.2 [קל] · מה קורה כאשר תוכנית User Mode מבצעת הוראת SYSCALL?

  • א. התוכנית קורסת מיד עם ההוראה: הנחה שגויה, אף שההוראה גורמת למעבר חומרתי ל-Ring 0 שבו הקרנל מטפל בבקשה ואינה גורמת לקריסת התוכנית כלל בפועל
  • ב. ה-CPU עובר ל-Ring 0 ומפעיל את מטפל ה-Syscall בקרנל, שכן זהו מעבר חומרתי ממצב המשתמש למצב הקרנל שבו הקרנל מטפל בבקשה ומחזיר תשובה כאן
  • ג. ה-CPU שולח הודעה אל ה-Driver: הנחה שגויה, אף שההוראה גורמת למעבר ל-Ring 0 שבו מטפל ה-Syscall בקרנל מטפל בבקשה ואינה שולחת הודעה ל-Driver
  • ד. התוכנית ממשיכה באותו מצב ללא שינוי: הנחה שגויה, אף שההוראה גורמת למעבר חומרתי ל-Ring 0 שבו הקרנל מטפל בבקשה ואינה ממשיכה באותו מצב כלל כאן
תשובה: ב

✓ תשובה: ב) — SYSCALL/INT 0x80 = מעבר חומרתי מRing 3 לRing 0. ה-Kernel מטפל בבקשה ומחזיר תשובה.

שאלה א׳.2 · מה ההבדל בין Kernel Mode (Ring 0) ל-User Mode (Ring 3)?

שלב 1:  printf("Hello")          ← קריאה ל-C Standard Library (User Mode Ring 3)
שלב 2:  printf → מפרמטת, קוראת write(fd=1, buf, len)   (עדיין User Mode)
שלב 3:  write() מריצה הוראת SYSCALL
        ← מכניסה syscall number לRAX (ב-Linux write=1)
        ← מכניסה ארגומנטים לRDI, RSI, RDX
שלב 4:  ה-CPU מחליף לRing 0 (Kernel Mode)!
        שומר את כל registers של User program
שלב 5:  ה-Kernel: syscall #1 = sys_write
        מריץ sys_write() → כותב למסך
שלב 6:  מסיים, שומר ערך חזרה בRAX
        ה-CPU מחליף חזרה לRing 3!
שלב 7:  write() מקבלת ערך החזרה, מחזירה לprintf()
  • א. מצב הקרנל מהיר יותר ומצב המשתמש ממתין: הנחה שגויה, אף שההבדל הוא שהקרנל בעל גישה מלאה לחומרה והמשתמש מוגבל לזיכרון הווירטואלי ולא במהירות כלל
  • ב. ההבדל הוא רק בכמות ה-RAM הזמין לכל מצב: הנחה שגויה, אף שההבדל הוא שהקרנל בעל גישה מלאה לחומרה והמשתמש מוגבל לזיכרון הווירטואלי ולא בכמות ה-RAM
  • ג. מצב המשתמש מהיר יותר והקרנל מיועד לקוד איטי: הנחה שגויה, אף שההבדל הוא שהקרנל בעל גישה מלאה לחומרה והמשתמש מוגבל לזיכרון הווירטואלי ולא במהירות
  • ד. מצב הקרנל בעל גישה מלאה לכל החומרה, ואילו מצב המשתמש מוגבל לגישה אל הזיכרון הווירטואלי של התהליך בלבד וללא גישה ישירה לחומרה כלשהי כאן
תשובה: ד

✓ תשובה: ד) — Kernel Mode = גישה מלאה לכל החומרה. User Mode = מוגבל לזיכרון הווירטואלי של התהליך, ללא גישה ישירה לחומרה.

שאלה א׳.3 · מה זה System Call ומדוע הוא נדרש?

  • א. פונקציה שמאיצה את ריצת התוכנית: הנחה שגויה, אף ש-System Call הוא בקשה רשמית מן הקרנל שכן מצב המשתמש אינו יכול לגשת לחומרה ישירות ואינו האצה
  • ב. בקשה רשמית של מצב המשתמש מן הקרנל לבצע פעולה, שכן מצב המשתמש אינו יכול לגשת לחומרה ישירות וכל גישה לקובץ או רשת או מסך עוברת דרך הקרנל כאן
  • ג. קריאה בין פונקציות בתוך מצב המשתמש: הנחה שגויה, אף ש-System Call הוא בקשה רשמית מן הקרנל שכן מצב המשתמש אינו יכול לגשת לחומרה ישירות בעצמו
  • ד. מנגנון להצפנת נתונים של מערכת ההפעלה: הנחה שגויה, אף ש-System Call הוא בקשה רשמית מן הקרנל לגישה לחומרה ואינו מנגנון הצפנה כלשהו בפועל כאן
תשובה: ב

✓ תשובה: ב) — System Call = בקשה רשמית. כי User Mode לא יכול לגשת לחומרה ישירות — כל גישה לקובץ, רשת, מסך עוברת דרך Kernel.

שאלה ב׳.7 [בינוני] · printf("Hello") ב-C — כמה Syscalls מבוצעים לפחות?

  • א. אפס שכן זו פונקציית שפת C ולא Syscall: הנחה שגויה, אף שהפקודה printf מתורגמת לקריאת write אחת לפחות אל הפלט הסטנדרטי ולכן מתבצע Syscall אחד
  • ב. שניים שהם קריאת open ואז write: הנחה שגויה, אף שהפלט הסטנדרטי כבר פתוח ולכן אין צורך ב-open ומתבצעת קריאת write אחת בלבד בסופו של דבר בפועל
  • ג. אחד, שכן הפקודה printf מתורגמת בסופו של דבר לקריאת write אחת אל הפלט הסטנדרטי שכבר פתוח, ולכן אין צורך בקריאת open נוספת לפניה בפועל כאן
  • ד. שלוש קריאות מערכת לפחות: הנחה שגויה, אף שהפלט הסטנדרטי כבר פתוח ולכן מתבצעת קריאת write אחת בלבד ולא שלוש קריאות מערכת בסופו של דבר בפועל כאן
תשובה: ג

✓ תשובה: ג) 1 — write() — printf → write() → SYSCALL sys_write. STDOUT (fd=1) כבר פתוח — אין צורך ב-open.

שאלה ב׳.17 [קשה] · execve('/bin/sh',NULL,NULL) כ-Syscall — מה ה-RAX שיכניס ה-Shellcode ב-x86-64 Linux?

  • א. 1: הנחה שגויה שזהו מספר קריאת המערכת של execve, אף שהמספר 1 שייך לקריאת write ואילו execve הוא 59 בארכיטקטורת x86-64 בלינוקס בפועל בעצמו
  • ב. 11: הנחה שגויה שזהו מספר קריאת המערכת של execve, אף שהמספר 11 אינו execve ואילו execve הוא 59 בארכיטקטורת x86-64 בלינוקס בפועל בעצמו כאן
  • ג. 60: הנחה שגויה שזהו מספר קריאת המערכת של execve, אף שהמספר 60 שייך לקריאת exit ואילו execve הוא 59 בארכיטקטורת x86-64 בלינוקס בפועל בעצמו
  • ד. 59: זהו מספר קריאת המערכת של execve בארכיטקטורת x86-64 בלינוקס, והוא הלב של Shellcode קלאסי שמטרתו לבקש מן המערכת לפתוח מעטפת פקודה כאן
תשובה: ד

✓ תשובה: ד) 59 — RAX=59 = syscall number של execve ב-x86-64 Linux. זהו הלב של Shellcode קלאסי: 'תן לי shell'.

שאלה ב׳.4 [קל] · מה ההבדל המרכזי בין Process לThread?

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

✓ תשובה: ג) — Process = בועה נפרדת עם זיכרון עצמאי. Thread = ביצוע בתוך הבועה — Threads חולקים Heap ו-Code, Stack נפרד.

שאלה ב׳.8 [בינוני] · מה קורה כש-Thread קורס בתוך Process?

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

✓ תשובה: א) — Thread קורס → SIGSEGV/Exception נשלח ל-Process → ה-Process מסתיים. לכן Chrome מריץ כל Tab כ-Process נפרד!

שאלה ב׳.18 [קשה] · Thread A ו-Thread B בProcess אחד. A כותב למשתנה גלובלי. B קורא אותו. מה ייתכן?

תהליך A רץ...
שלב 1: Timer Interrupt מופעל (כל ~10ms)
        ה-CPU עובר לKernel Mode
שלב 2: ה-Kernel שומר מצב תהליך A ב-PCB(A):
        − כל הרגיסטרים (EAX, EBX, ..., EIP, ESP)
        − מצב הזיכרון הווירטואלי
שלב 3: ה-Scheduler בוחר תהליך B להריץ הבא
שלב 4: טוען PCB(B): משחזר כל הרגיסטרים של B
        מחליף את ה-Page Table לזו של B
שלב 5: ה-CPU חוזר לUser Mode
        EIP מצביע לאן תהליך B הפסיק
תהליך B ממשיך כאילו לא קרה כלום...
  • א. החוט הקורא תמיד מקבל ערך נכון: הנחה שגויה, אף שללא סנכרון נוצר מצב מרוץ שבו החוט הקורא עלול לקרוא ערך חלקי או ישן ולא תמיד ערך נכון בפועל כאן
  • ב. הפעולה בטוחה שכן הם באותו תהליך: הנחה שגויה, אף ששיתוף משאב בין חוטים ללא נעילה יוצר מצב מרוץ ואינו בטוח רק מפני שהם באותו תהליך בפועל בעצמו
  • ג. מערכת ההפעלה מסנכרנת אותם אוטומטית: הנחה שגויה, אף שהמערכת אינה מסנכרנת אוטומטית וללא נעילה נוצר מצב מרוץ שבו נקרא ערך חלקי או ישן בפועל כאן
  • ד. מצב מרוץ, שכן ללא סנכרון החוט הקורא עלול לקרוא ערך חלקי או ישן, ובתחום הסייבר מצבי מרוץ מהווים חולשות מסוג בדיקה-מול-שימוש הידועות בפועל כאן
תשובה: ד

✓ תשובה: ד) Race Condition — שני Threads גולשים לאותו משאב ללא Lock/Mutex. בסייבר: Race Conditions = חולשות (TOCTOU attacks).

שאלה ב׳.6 [בינוני] · מדוע Context Switch יקר?

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

✓ תשובה: ד) — CPU לא עושה עבודה 'אמיתית' בזמן Context Switch. עם 1000 תהליכים, חלק גדול מזמן CPU הולך על Switching.

שאלה א׳.5 · מה קורה ב-Context Switch? תאר בקצרה.

כתובת גבוהה  0xFFFFFFFF
              ┌───────────────────┐
              │  Kernel Space      │  ← User לא גולש
0xC0000000   │───────────────────│
              │  Stack             │  ← גדל כלפי מטה ↓ (local vars, return addresses!)
              │         ↓          │
              │  (מרווח ריק)       │
              │         ↑          │
              │  Heap              │  ← גדל כלפי מעלה ↑ (malloc, new)
              │───────────────────│
              │  BSS segment       │  ← גלובלים לא מאותחלים
              │  Data segment      │  ← גלובלים מאותחלים
              │  Text segment      │  ← קוד התוכנית (read-only!)
כתובת נמוכה  0x00000000
              └───────────────────┘
  • א. ה-CPU נכבה ומופעל מחדש בכל מעבר: הנחה שגויה, אף שהמעבר כולל שמירת בלוק בקרת התהליך והחלפת טבלת דפים וחזרה למצב המשתמש ואינו כיבוי של המעבד כלל
  • ב. כל הזיכרון נמחק ומוקצה מחדש במעבר: הנחה שגויה, אף שהמעבר כולל שמירת בלוק בקרת התהליך והחלפת טבלת דפים ואינו מוחק ומקצה מחדש את כל הזיכרון בפועל
  • ג. שומרים את בלוק בקרת התהליך הנוכחי, המתזמן בוחר את הבא, טוענים בלוק חדש, מחליפים את טבלת הדפים, וחוזרים למצב המשתמש והתהליך ממשיך כרגיל בפועל
  • ד. תהליך חדש נוצר בכל מעבר בין תהליכים: הנחה שגויה, אף שהמעבר מחליף בין תהליכים קיימים על ידי טעינת בלוק הבקרה שלהם ואינו יוצר תהליך חדש בכל פעם
תשובה: ג

✓ תשובה: ג) — Context Switch: שמור PCB(A), בחר B, טען PCB(B), החלף Page Table, חזור לUser Mode. B ממשיך כאילו לא הופסק.

שאלה ב׳.5 [קל] · מה הוא Page Fault?

  • א. שגיאה בתוך טבלת הדפים עצמה: הנחה שגויה, אף ש-Page Fault הוא גישה לעמוד שאינו ב-RAM והקרנל מטפל בה ואינו שגיאה בתוך טבלת הדפים בפועל בעצמו כאן
  • ב. כשל חומרתי בזיכרון ה-RAM: הנחה שגויה, אף ש-Page Fault הוא גישה לעמוד שאינו ב-RAM והקרנל מטפל בה ואינו כשל חומרתי בזיכרון עצמו בפועל בעצמו כאן
  • ג. גישה לעמוד זיכרון שאינו נמצא ב-RAM באותו רגע, והקרנל מטפל בה על ידי הבאת העמוד מן ה-Swap או זיהוי שאין מיפוי מה שמוביל לתקלת הפרדה בפועל כאן
  • ד. שגיאה בקוד ה-Assembly של התוכנית: הנחה שגויה, אף ש-Page Fault הוא גישה לעמוד שאינו ב-RAM והקרנל מטפל בה ואינו שגיאת תחביר בקוד ה-Assembly כלל
תשובה: ג

✓ תשובה: ג) — Page Fault = MMU לא מצא את העמוד ב-RAM. ה-Kernel יכול להביא אותו מSwap (OK) או למצוא שאין ממפה → Segfault.

שאלה ב׳.13 [בינוני] · מה ה-MMU?

  • א. רכיב תוכנה המיועד לניהול ה-RAM: הנחה שגויה, אף שה-MMU הוא חומרה שהיא חלק מן ה-CPU ומתרגמת כתובות וירטואליות לפיזיות ואינו רכיב תוכנה בפועל בעצמו
  • ב. שם אחר עבור זיכרון ה-Cache: הנחה שגויה, אף שה-MMU הוא חומרה המתרגמת כתובות וירטואליות לפיזיות ואינו שם אחר ל-Cache שהוא זיכרון מהיר בפועל בעצמו
  • ג. יחידת ניהול הזיכרון שהיא חומרה המתרגמת כתובות וירטואליות לכתובות פיזיות, והיא חלק מן ה-CPU ומשתמשת בטבלאות דפים ובמטמון תרגום בכל גישה לזיכרון
  • ד. מנגנון קריאת המערכת של הקרנל: הנחה שגויה, אף שה-MMU הוא חומרה המתרגמת כתובות וירטואליות לפיזיות ואינו מנגנון קריאת המערכת של הקרנל בפועל בעצמו
תשובה: ג

✓ תשובה: ג) — MMU = חלק מה-CPU שמבצע כל גישה לזיכרון. משתמש ב-Page Tables ו-TLB. בלי MMU — אין Virtual Memory.

שאלה ב׳.12 [בינוני] · מה אזור ה-Stack מכיל?

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

✓ תשובה: ב) — Stack גדל כלפי מטה. מכיל stack frames: local vars, saved registers, return address. זה אזור ה-Buffer Overflow הקלאסי!

שאלה ב׳.20 [קשה] · Buffer על ה-Stack גולש. מה הנתון הקריטי שנדרס ראשון?

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

✓ תשובה: א) — Stack layout: [local buffer][saved EBP][return address]. Overflow ב-Buffer → דורס saved EBP ואז return address → כשהפונקציה מסיימת, EIP = כתובת שלנו.

שאלה ב׳.9 [בינוני] · malware כותב ל-HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run — מה הוא מנסה לעשות?

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

✓ תשובה: ג) Persistence — Run Key = רשימת תוכניות שWindows מפעיל בכל login/startup. Malware שמוסיף לכאן = ישרוד Reboot.

שאלה ב׳.10 [בינוני] · מה ההבדל בין fork() ב-Linux לCreateProcess() ב-Windows?

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

✓ תשובה: ג) — fork() = copy-on-write clone של תהליך האב. CreateProcess() = תהליך חדש לגמרי. fork()+exec() = הפעלת binary חדשה.

שאלה ב׳.19 [קשה] · strace של malware מראה: open('/etc/passwd', O_RDONLY) → read(3,...) → connect(4,{AF_INET,1.2.3.4:80}). מה malware עושה?

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

✓ תשובה: א) — open('/etc/passwd')+read = קריאת קובץ הסיסמאות. connect לIP חיצוני = שידור לServer של התוקף. Exfiltration קלאסי.

שאלה ג׳.1 [קל] · תוכנה קוראת open("/etc/passwd"). באיזה Ring רץ הקוד שמבצע את הפתיחה בפועל?

open("/etc/passwd")
  • א. Ring 3 (User Mode): הנחה שגויה, אף שה-User Code שולח בקשה בלבד, והפתיחה בפועל מתבצעת ב-Ring 0 של ה-Kernel כן
  • ב. Ring 0 (Kernel Mode) — הקוד שלנו ב-Ring 3 רק מבקש דרך Syscall, וה-Kernel ב-Ring 0 הוא שמבצע את הפתיחה עצמה
  • ג. Ring 1: הנחה שגויה, אף שברוב מערכות ההפעלה משתמשים רק ב-Ring 0 ו-Ring 3, ו-Ring 1 כמעט אף פעם אינו בשימוש
  • ד. אותו Ring, ללא מעבר: הנחה שגויה, אף שכל גישה לחומרה מחייבת מעבר מ-Ring 3 ל-Ring 0 דרך System Call בפועל כאן כן
תשובה: ב

✓ תשובה נכונה: ב — User Mode (Ring 3) שולח Syscall → ה-CPU עובר ל-Ring 0 → ה-Kernel בודק הרשאות, פותח את הקובץ ומחזיר FD. קוד ה-User לעולם לא נוגע בדיסק ישירות.

שאלה ג׳.2 [בינוני] · מה קורה כאשר Process ניגש לכתובת וירטואלית שה-Page שלה לא נמצא כרגע ב-RAM?

  • א. Segmentation Fault מיידי: הנחה שגויה, אף ש-Segfault קורה רק כשהכתובת אינה ב-Page Table כלל, ולא כשהיא בדיסק
  • ב. ה-CPU ממתין עד שה-RAM מתפנה: הנחה שגויה, אף שהמנגנון הוא Page Fault שטוען את הדף, ולא המתנה סתמית של המעבד
  • ג. Page Fault — ה-Kernel טוען את ה-Page מ-Swap או מהדיסק, מעדכן את ה-Page Table ומחזיר את השליטה לתהליך
  • ד. הכתובת הופכת אוטומטית ל-NULL: הנחה שגויה, אף שהמנגנון הוא Page Fault שמביא את הדף, והכתובת נשארת תקפה
תשובה: ג

✓ תשובה נכונה: ג — Page Fault הוא מנגנון תקין. Segfault מגיע רק אם הכתובת אינה מופיעה ב-Page Table כלל. Page Fault = 'יש דף, הוא פשוט על הדיסק'.

שאלה ג׳.3 [קשה] · Malware רוצה לכתוב Shellcode לתוך הזיכרון של Process אחר ב-Linux. באיזה Syscall הוא ישתמש?

  • א. mmap: הנחה שגויה, אף ש-mmap ממפה זיכרון בתוך התהליך הנוכחי בלבד, ואינו כותב לתוך המרחב של תהליך אחר כפי שנדרש כאן כן
  • ב. write: הנחה שגויה, אף ש-write כותב ל-File Descriptor של התהליך שלנו, ואינו מזריק נתונים לזיכרון של תהליך זר בפועל
  • ג. execve: הנחה שגויה, אף ש-execve מחליף את קוד התהליך הנוכחי בתוכנית אחרת, ואינו כותב לזיכרון של תהליך קיים אחר
  • ד. ptrace עם PTRACE_POKEDATA — זהו הממשק שמאפשר Debug והזרקת קוד לתוך תהליך אחר, ואף GDB עצמו משתמש בו בדיוק לשם כך
תשובה: ד

✓ תשובה נכונה: ד — ptrace הוא הממשק ל-Process Injection. GDB משתמש בו ל-Debug, Malware משתמש בו ל-Code Injection. הגנה: ptrace_scope=1 ב-Linux מגביל ל-Parent בלבד.

שאלה ב׳.16 [קשה] · Process מנסה לגשת לכתובת 0xC0000000 ב-Linux 32bit. מה יקרה?

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

✓ תשובה: ד) — ב-Linux 32bit, 0xC0000000 ומעלה = Kernel Space. גישה מUser Mode = Page Fault + SIGSEGV. ה-OS מגן על Kernel Space!

שאלה ב׳.11 [בינוני] · Process A ב-Virtual Address 0x00401000. Process B באותה כתובת. מה נכון?

#include <unistd.h>
#include <stdio.h>
// pid_t fork(void);
// מחזיר: ב-PARENT: PID של Child | ב-CHILD: 0 | שגיאה: -1

pid_t pid = fork();
if (pid < 0) {
    perror("fork");      // שגיאה
    return 1;
} else if (pid == 0) {
    // ─── Child Process ──────────────────────────────────
    printf("Child: PID=%d, Parent=%d\n", getpid(), getppid());
    exit(0);             // חשוב: child חייב לצאת!
} else {
    // ─── Parent Process ─────────────────────────────────
    printf("Parent: PID=%d, Child=%d\n", getpid(), pid);
}

// ─── מה fork() מעתיק ────────────────────────────────────
// ✓ זיכרון (Copy-on-Write), File Descriptors, Signal Handlers
// ✗ Threads (רק Thread אחד עובר לChild), Locks (→ Deadlock!)

// ─── wait(): Parent ממתין ל-Child ───────────────────────
int status;
pid_t finished = waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
    int code = WEXITSTATUS(status);  // קוד יציאה
    printf("Child exited with code %d\n", code);
}
  • א. הם חולקים את אותו זיכרון פיזי בדיוק: הנחה שגויה, אף שכל תהליך ממפה את אותה כתובת וירטואלית לכתובת פיזית שונה לחלוטין באמצעות טבלת דפים נפרדת בפועל
  • ב. הדבר גורם לקריסה של אחד התהליכים: הנחה שגויה, אף שאין קריסה שכן כל תהליך ממפה את אותה כתובת וירטואלית לכתובת פיזית שונה באמצעות טבלת דפים נפרדת
  • ג. לא ניתן לשני תהליכים להשתמש באותה כתובת: הנחה שגויה, אף שכן ניתן שכן כל תהליך ממפה את אותה כתובת וירטואלית לכתובת פיזית שונה באמצעות טבלת דפים נפרדת
  • ד. הם ממפים לכתובות פיזיות שונות לחלוטין באמצעות טבלאות דפים נפרדות, שכן הזיכרון הווירטואלי גורם לכל תהליך לחיות בעולמו וזוהי מהות הזיכרון הווירטואלי
תשובה: ד

✓ תשובה: ד) — Virtual Memory = כל Process חי 'בעולמו'. אותה כתובת וירטואלית = Physical addresses שונות לחלוטין. זה הנקודה של Virtual Memory.

שאלה 1.1 — fork() Return Value · קוד קורא ל-pid_t pid = fork();. מה ערך pid ב-Child Process?

pid_t pid = fork();
  • א. ה-PID של הבן עצמו: הנחה שגויה, אף שדווקא ב-Parent מוחזר ה-PID של הבן, ואילו בבן עצמו מוחזר הערך 0 בלבד כאן
  • ב. 0: הפונקציה fork מחזירה 0 בתהליך-הבן, ב-Parent היא מחזירה את ה-PID של הבן, ובשגיאה היא מחזירה 1- כאן
  • ג. ה-PID של ה-Parent: הנחה שגויה, אף ש-fork מחזירה 0 בבן ואת ה-PID של הבן ב-Parent ולא את ה-PID של ה-Parent
  • ד. 1- (שגיאה): הנחה שגויה, אף ש-fork מחזירה 1- רק במקרה של כישלון, ובהצלחה מחזירה 0 בבן ו-PID של הבן ב-Parent
תשובה: ב

✓ תשובה: ב) 0 — זוהי ה-API של fork(). Child מקבל 0 → בדוק if(pid==0) לדעת שאתה ב-Child. Parent מקבל PID של Child → מאפשר waitpid(pid, ...) ספציפי. -1 = שגיאה (פחות FDs, Limit הגיע).

שאלה 1.2 — Copy-on-Write · מדוע fork() מהיר גם כשהProcess הוא 500MB?

  • א. Copy-on-Write: fork אינו מעתיק דפים פיזית מיד, אלא Parent ו-Child חולקים אותם דפים עד שאחד מהם כותב אליהם
  • ב. מכיוון ש-fork מעתיק רק את ה-Stack שהוא קטן, אף שהוא משתף למעשה את כל מרחב הזיכרון בשיטת Copy-on-Write כאן
  • ג. מכיוון ש-fork דוחס את הזיכרון לפני ההעתקה, אף שהוא כלל אינו דוחס אלא משתף את הדפים בשיטת Copy-on-Write כאן
  • ד. מכיוון ש-Linux מגביל את fork ל-100MB בלבד, אף שאין כלל מגבלה כזו וה-fork מהיר בזכות שיטת ה-Copy-on-Write כאן
תשובה: א

✓ תשובה: א) COW — Copy-on-Write: במקום להעתיק 500MB, הקרנל יוצר Page Table חדשה שמצביעה לאותם Pages פיזיים, Read-Only. רק כשChild כותב ל-Page, הקרנל מעתיק אותו (Page Fault). תוצאה: fork() = O(1) כמעט, לא O(n) לפי גודל הזיכרון.

שאלה 1.3 — Dry Run: fork output · int main() { pid_t pid = fork(); if (pid == 0) { printf("Child\n"); exit(0); } else { wait(NULL); printf("Parent\n"); } return 0; }מה הפלט ומה הסדר המובטח?

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        printf("Child\n");
        exit(0);
    } else {
        wait(NULL);
        printf("Parent\n");
    }
    return 0;
}
  • א. Child ואז Parent, מובטח: הקריאה ל-wait(NULL) גורמת ל-Parent להמתין עד שהבן מסיים, ולכן הבן מדפיס ראשון תמיד
  • ב. Parent ואז Child כי ה-Parent תמיד ראשון: הנחה שגויה, אף שה-wait גורם ל-Parent להמתין לבן ולכן הבן מדפיס ראשון כאן
  • ג. Child או Parent והסדר אינו ידוע: הנחה שגויה, אף שהקריאה ל-wait מבטיחה שהבן יסיים ויודפס לפני ה-Parent בכל הרצה כאן
  • ד. Child פעמיים ואז Parent כי fork מדפיס פעמיים: הנחה שגויה, אף שכל תהליך מריץ את הענף שלו בלבד ולא את שני הענפים כאן
תשובה: א

✓ תשובה: א) Child\nParent — מובטח — wait(NULL) = Parent חוסם עד שChild מסיים. סדר: 1) fork() → שני Processes רצים. 2) Child מדפיס "Child" ו-exit(0). 3) wait() מחזיר ל-Parent. 4) Parent מדפיס "Parent". ללא wait(): הסדר לא מובטח.

שאלה 2.1 — exec() Basics · מה ה-PID של Process לאחר exec()?

  • א. PID חדש כי exec יוצרת תהליך חדש: הנחה שגויה, אף ש-exec מחליפה את תוכן התהליך הקיים ואינה יוצרת כלל תהליך חדש כאן
  • ב. PID השווה ל-0 כי התהליך מתאפס: הנחה שגויה, אף שה-PID של התהליך נשמר כמו שהוא ואינו מתאפס כלל לאחר הקריאה ל-exec
  • ג. PID השווה ל-PPID של ה-Parent: הנחה שגויה, אף שה-PID של התהליך נשמר כמו שהוא ואינו הופך ל-PID של תהליך ה-Parent כאן
  • ד. אותו PID: הפונקציה exec מחליפה רק את הקוד והזיכרון, ואילו ה-PID, ה-FDs וה-UID/GID של התהליך נשמרים כמו שהם
תשובה: ד

✓ תשובה: ד) אותו PID — זהו הבדל חשוב בין fork() לexec(). fork(): PID חדש לChild. exec(): PID נשמר, הכל אחר מוחלף. לכן fork()+exec() = Process חדש עם Program חדש. בRE: אם ראית execve, ה-PID של הProcess לא השתנה.

שאלה 2.2 — execve ב-Exploit · ב-64bit Linux Exploit, מה ה-syscall number של execve?

  • א. 11: הנחה שגויה, אף שהמספר 11 הוא של execve דווקא בסביבת 32 ביט (int 0x80 עם eax=11) ולא בסביבת 64 ביט כאן כן
  • ב. 1: הנחה שגויה, אף שהמספר 1 בסביבת 64 ביט שייך לקריאת המערכת write ולא לקריאת המערכת execve שמספרה 59 כאן כן
  • ג. 59: בסביבת x86_64 מספר קריאת המערכת של execve הוא 59, כאשר rdi=path, rsi=argv ו-rdx=envp לפני ה-syscall כאן
  • ד. 0: הנחה שגויה, אף שהמספר 0 בסביבת 64 ביט שייך לקריאת המערכת read ולא לקריאת המערכת execve שמספרה דווקא 59 כאן
תשובה: ג

✓ תשובה: ג) 59 — בx86_64: SYS_execve = 59. רגיסטרים: rax=59, rdi=pathname, rsi=argv[], rdx=envp[]. בx86 32bit: SYS_execve = 11, eax=11, ebx=path, ecx=argv, edx=envp. טעות נפוצה: לבלבל בין 32bit (11) ל-64bit (59).

שאלה 2.3 — Dry Run: fork+exec · pid_t pid = fork(); if (pid == 0) { char *args[] = {"/bin/echo", "hello", NULL}; execvp("/bin/echo", args); printf("EXEC FAILED\n"); exit(1); } else { wait(NULL); printf("DONE\n"); }מה הפלט אם execvp מצליח?

pid_t pid = fork();
if (pid == 0) {
    char *args[] = {"/bin/echo", "hello", NULL};
    execvp("/bin/echo", args);
    printf("EXEC FAILED\n");
    exit(1);
} else {
    wait(NULL);
    printf("DONE\n");
}
  • א. EXEC FAILED ואז DONE: הנחה שגויה, אף ש-execvp מצליחה ומחליפה את הבן, ולכן השורה EXEC FAILED כלל אינה מודפסת כאן כן
  • ב. hello ואז DONE: הקריאה execvp מצליחה ומריצה echo hello שמדפיס hello, השורה EXEC FAILED לא מגיעה, וה-Parent מדפיס DONE
  • ג. hello ו-EXEC FAILED ואז DONE: הנחה שגויה, אף ש-execvp מצליחה ומחליפה את הבן, ולכן השורה EXEC FAILED כלל אינה מגיעה
  • ד. DONE ואז hello: הנחה שגויה, אף שהקריאה ל-wait גורמת ל-Parent להמתין לבן, ולכן hello מודפס לפני DONE בכל הרצה כאן
תשובה: ב

✓ תשובה: ב) "hello\nDONE" — execvp מצליח: Child הפך ל-/bin/echo → מדפיס "hello\n" ויוצא. "EXEC FAILED" ו-exit(1) לא מגיעים אליהם. Parent: wait() מחזיר אחרי Child יוצא → "DONE". סדר: echo (Child) → DONE (Parent, אחרי wait).

שאלה 3.1 — SIGKILL vs SIGTERM · מדוע לא ניתן לתפוס SIGKILL?

  • א. SIGKILL ו-SIGSTOP מטופלים ישירות על-ידי הקרנל, והתהליך אינו מקבל הזדמנות להריץ קוד, כדי שלא יוכל לחסום את הריגתו
  • ב. מכיוון שמספרו של SIGKILL הוא 9 ומספרים אי-זוגיים אינם ניתנים לתפיסה, אף שאין כל קשר בין זוגיות המספר ליכולת התפיסה
  • ג. מכיוון ש-SIGKILL נשלח מן הרשת בלבד, אף שהוא נשלח מקומית כמו כל אות אחר, והסיבה האמיתית היא הטיפול בו על-ידי הקרנל
  • ד. מכיוון ש-SIGKILL עובר דרך הקרנל ולא דרך התהליך, אף שכל האותות עוברים דרך הקרנל, וייחודו הוא שהתהליך אינו מריץ קוד עבורו
תשובה: א

✓ תשובה: א) — SIGKILL/SIGSTOP = Kernel-Level signals. הקרנל לא שולח ל-Process ומחכה לHandler — הוא פשוט מסיים/עוצר מיידית. זה מבטיח שתמיד ניתן להרוג Process. SIGTERM ניתן לתפיסה → תוכנות יכולות cleanup לפני יציאה. SIGKILL = "אין ברירה".

שאלה 3.2 — Signal Handler Safety · איזו פעולה בטוחה בתוך Signal Handler?

  • א. printf("Signal received"): הנחה שגויה, אף ש-printf אינה בטוחה לשימוש בתוך מטפל אות (אינה async-signal-safe) ועלולה לקרוס
  • ב. malloc(100) להקצאת זיכרון לתגובה: הנחה שגויה, אף ש-malloc אינה בטוחה לשימוש בתוך מטפל אות ועלולה לגרום ל-Deadlock כאן
  • ג. g_stop = 1 עם משתנה מסוג volatile sig_atomic_t: פעולה אטומית ובטוחה, שה-main loop בודק ומגיב עליה מחוץ למטפל האות
  • ד. free(ptr) לשחרור זיכרון שהוקצה קודם: הנחה שגויה, אף ש-free אינה בטוחה לשימוש בתוך מטפל אות ועלולה להשחית את ה-Heap כאן
תשובה: ג

✓ תשובה: ג) volatile sig_atomic_t flag — printf(), malloc(), free() לא async-signal-safe: הם משתמשים ב-internal locks. אם Signal קטע אותן → Deadlock. write() היא async-signal-safe (גם היא בסדר). Pattern בטוח: Handler = flag בלבד, main loop = לוגיקה.

שאלה 3.3 — Dry Run: Signal ב-RE · ב-IDA/Ghidra רואים: CALL signal + push 11 + push handler_func. מה הקוד עושה?

CALL signal + push 11 + push handler_func
  • א. רושם את handler_func כמטפל האות עבור אות מספר 11 (SIGSEGV), כך שבעת Segmentation Fault ייקרא המטפל במקום קריסה
  • ב. שולח את אות מספר 11 אל תהליך אחר: הנחה שגויה, אף שהקריאה ל-signal רושמת מטפל לאות ואינה שולחת כלל אות אל תהליך אחר
  • ג. יוצר Thread חדש שמאזין ל-SIGSEGV: הנחה שגויה, אף שהקריאה ל-signal רושמת מטפל אות ואינה יוצרת כלל חוט (Thread) חדש כאן
  • ד. מבטל את אות ה-SIGSEGV לחלוטין: הנחה שגויה, אף שהקוד רושם מטפל שיטפל באות ואינו מבטל כלל את אות ה-SIGSEGV לגמרי כאן
תשובה: א

✓ תשובה: א) — signal(SIGSEGV, handler_func): Signal 11 = SIGSEGV. מה זה אומר ב-RE? התוכנה מנסה לשרוד SIGSEGV (NULL deref, buffer overflow) ולהמשיך לרוץ במקום לקרוס. ב-Exploit: SIGSEGV Handler יכול לחסום ניסיונות ניצול. ב-CTF: חפש מה handler_func עושה — לפעמים מדפיס Flag!

שאלה 4.1 — pipe() FDs · לאחר int pipefd[2]; pipe(pipefd);, כיצד Process כותב ל-Pipe?

int pipefd[2]; pipe(pipefd);
  • א. write(pipefd[0], data, len): הנחה שגויה, אף שהקצה pipefd[0] הוא קצה הקריאה (read-end) ולא קצה הכתיבה של ה-pipe כאן
  • ב. pipe_write(pipefd, data, len): הנחה שגויה, אף שאין כלל פונקציה בשם pipe_write וכותבים ל-pipe באמצעות write רגיל כאן
  • ג. write(pipefd[1], data, len): הקצה pipefd[1] הוא קצה הכתיבה (write-end) של ה-pipe, ולכן כותבים אליו את הנתונים כאן
  • ד. write(pipefd, data, len): הנחה שגויה, אף שיש לכתוב אל קצה ספציפי pipefd[1] ולא אל כל המערך pipefd יחד בפעולה אחת כאן
תשובה: ג

✓ תשובה: ג) pipefd[1] — מוסכמה: pipefd[0] = read-end (קריאה), pipefd[1] = write-end (כתיבה). כלל מנמוני: 0=קלט, 1=פלט (כמו stdin=0, stdout=1). Process שכותב: close(pipefd[0]) → write(pipefd[1], ...). Process שקורא: close(pipefd[1]) → read(pipefd[0], ...).

שאלה 4.2 — dup2 Reverse Shell · ב-CTF Exploit קוד: dup2(sock_fd, 0); dup2(sock_fd, 1); execve("/bin/sh", ...);מה זה עושה?

dup2(sock_fd, 0);
dup2(sock_fd, 1);
execve("/bin/sh", ...);
  • א. פותח שני sockets לתקשורת מקבילה: הנחה שגויה, אף ש-dup2 רק מכפיל את ה-FD הקיים ואינו פותח כלל sockets חדשים במערכת
  • ב. מפנה את stdin (0) ואת stdout (1) אל sock_fd, כך שכשה-execve פותח מעטפת הקלט והפלט שלה עוברים דרך ה-socket אל התוקף
  • ג. מעתיק את sock_fd ל-FD מספרים 0 ו-1 ואז סוגר את sock_fd: הנחה שגויה, אף ש-dup2 מעתיק אך אינו סוגר כלל את ה-FD המקורי
  • ד. מפעיל את bin/sh כ-daemon ברקע: הנחה שגויה, אף שהקוד מפנה את הקלט והפלט אל ה-socket ומריץ מעטפת אינטראקטיבית לתוקף כאן
תשובה: ב

✓ תשובה: ב) Reverse Shell — dup2(sock_fd, 0): stdin (FD 0) = socket. dup2(sock_fd, 1): stdout (FD 1) = socket. execve("/bin/sh"): מריץ shell. עכשיו: מה שהתוקף כותב לsocket הולך ל-stdin של shell. מה שshell מדפיס הולך ל-stdout = socket = התוקף. תוצאה: shell מרוחק!

שאלה 4.3 — Dry Run: dup2+exec · int pipefd[2]; pipe(pipefd); pid_t pid = fork(); if (pid == 0) { close(pipefd[0]); dup2(pipefd[1], 1); // stdout → pipe close(pipefd[1]); execvp("echo", (char*[]){"echo","test",NULL}); } else { close(pipefd[1]); char buf[32]={0}; read(pipefd[0], buf, 31); printf("Got: %s", buf); wait(NULL); }מה יודפס?

int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
    close(pipefd[0]);
    dup2(pipefd[1], 1);  // stdout → pipe
    close(pipefd[1]);
    execvp("echo", (char*[]){"echo","test",NULL});
} else {
    close(pipefd[1]);
    char buf[32]={0};
    read(pipefd[0], buf, 31);
    printf("Got: %s", buf);
    wait(NULL);
}
  • א. test בלבד מה ש-echo מדפיס ישירות: הנחה שגויה, אף שהפלט של echo הופנה אל ה-Pipe ולא אל המסך, וה-Parent הוא שמדפיסו
  • ב. כלום כי dup2 מונע הדפסה: הנחה שגויה, אף ש-dup2 רק מפנה את הפלט אל ה-Pipe ואינו מונע כלל את עצם ההדפסה של echo כאן
  • ג. Got: test עם ירידת שורה: הפלט של echo הופנה אל ה-Pipe, ה-Parent קורא ממנו את "test" ומדפיס אותו לאחר "Got: " כאן
  • ד. Got: בלבד עם buf ריק: הנחה שגויה, אף שה-Parent קורא בהצלחה את "test" מן ה-Pipe ולכן buf אינו ריק בעת ההדפסה שלו כאן
תשובה: ג

✓ תשובה: ג) "Got: test\n" — dup2(pipefd[1], 1): stdout של Child = Pipe write-end. execvp("echo", ["echo","test"]): echo מדפיס "test\n" ל-stdout → הולך ל-Pipe. Parent: read(pipefd[0], buf, 31) → buf = "test\n". printf("Got: %s", buf) → "Got: test\n".

שאלה 5.1 — fork() ב-Assembly · ב-IDA רואים: mov eax, 2 int 0x80 test eax, eax jz loc_CHILDמה מתאר הקוד?

mov eax, 2
int 0x80
test eax, eax
jz  loc_CHILD
  • א. קריאה ל-exit() כי syscall 2 הוא exit: הנחה שגויה, אף שקריאת המערכת מספר 2 בסביבת 32 ביט היא fork ולא exit במערכת
  • ב. קריאה ל-open() כי syscall 2 הוא open: הנחה שגויה, אף שקריאת המערכת מספר 2 בסביבת 32 ביט היא fork ולא open במערכת
  • ג. קריאה ל-write() עם גודל 0: הנחה שגויה, אף שקריאת המערכת מספר 2 בסביבת 32 ביט היא fork ולא write עם גודל אפס כאן
  • ד. fork() בסביבת 32 ביט (SYS_fork=2): הבדיקה test eax,eax בודקת אם eax שווה ל-0, וה-jz קופץ אל קוד הבן אם כן כאן
תשובה: ד

✓ תשובה: ד) fork() — SYS_fork = 2 ב-32bit Linux. int 0x80 = syscall. test eax,eax: אם eax=0 → ZF=1 → jz קופץ לChild code. אם eax>0 (PID of Child) → ZF=0 → jz לא קופץ → Parent code. זהו הPattern הסטנדרטי של fork() ב-Assembly. ב-64bit: rax=57 + syscall.

שאלה 5.2 — Backdoor זיהוי · ב-Ghidra רואים: CALL execve ובאזור ה-data: "/bin/sh". מסקנה?

CALL execve
  • א. התוכנה מריצה פקודות shell כחלק מפעולתה הרגילה: הנחה שגויה, אף ששילוב execve("/bin/sh") עם קוד חשוד מעיד בבירור על Backdoor
  • ב. זו Backdoor באופן ודאי: הקריאה execve("/bin/sh") מחליפה את התהליך במעטפת, ותוקף שמגיע לנקודה זו מקבל מעטפת על הקורבן
  • ג. התוכנה בודקת אם bin/sh קיים במערכת: הנחה שגויה, אף שהקריאה execve מריצה בפועל את המעטפת ואינה רק בודקת את קיומה כאן
  • ד. תלוי בהקשר ואולי מדובר בקוד לגיטימי: הנחה שגויה, אף ש-execve("/bin/sh") בתוך קוד חשוד הוא סימן מובהק וברור מאוד ל-Backdoor
תשובה: ב

✓ תשובה: ב) Backdoor — CALL execve + "/bin/sh" = Red Flag מיידי ב-Malware RE. execve("/bin/sh") = מריץ shell בהקשר של הProcess הנוכחי (uid, permissions). אם זה daemon שרץ כroot → shell כroot! בCTF: זה הGoal של Pwn challenges — להגיע לנקודה שexecve("/bin/sh") רץ.

שאלה 5.3 — Dry Run: Forking Server · Forking Server עושה fork() לכל connection. ב-CTF, ASLR מופעל. מדוע quand זה עדיין ניתן לניצול?

// ─── שאלה: מה הפלט? ─────────────────────────────────────────
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int x = 10;
int main() {
    pid_t pid = fork();
    if (pid == 0) {
        x += 5;
        printf("Child:  x = %d\n", x);
        exit(0);
    } else {
        wait(NULL);
        printf("Parent: x = %d\n", x);
    }
    return 0;
}

// ─── ניתוח ────────────────────────────────────────────────
// fork() יוצר עותק של x = 10 לChild (Copy-on-Write)
// Child:  x += 5 → x = 15 ב-Child בלבד
// Child מדפיס "Child:  x = 15" ויוצא
// Parent: wait() חוסם עד שChild מסיים
// Parent: x של Parent = 10 (לא שונה!)
// Parent מדפיס "Parent: x = 10"

// ─── פלט (סדר מובטח כי Parent עושה wait) ────────────────
// Child:  x = 15
// Parent: x = 10
  • א. מכיוון ש-ASLR כלל אינו עובד על שרתים: הנחה שגויה, אף ש-ASLR עובד היטב, והחולשה נובעת מכך שכל fork משמר את אותן כתובות
  • ב. fork יוצר Child עם Copy-on-Write מן ה-Parent, ולכן מרחב הכתובות זהה בכל fork (ASLR מרונדם פעם אחת), וניתן ל-Brute Force
  • ג. מכיוון שניתן לבטל את ה-ASLR עם kill -9: הנחה שגויה, אף שהפקודה kill רק הורגת תהליך ואינה משפיעה כלל על מנגנון ה-ASLR כאן
  • ד. מכיוון ש-Forking Servers תמיד רצים ללא ASLR: הנחה שגויה, אף שהם רצים עם ASLR, אך כל fork משמר את אותן כתובות הבסיס שלו
תשובה: ב

✓ תשובה: ב) COW + same address space — ASLR randomizes ב-exec(). fork() = Copy-on-Write, לא exec() → כתובות זהות לParent. כל Child מקבל אותו מרחב כתובות. תוצאה: Brute Force ASLR — ניסיון אחרי ניסיון, Child קורס ו-Parent מאזין לחיבור הבא. 256 ניסיונות לנחש 8 ביט (ASLR entopy נמוך בLinux 32bit).

גאמא סגנון א׳ — Zombie · Server שעושה fork() לכל connection אבל לא קורא wait(). מה יקרה לאורך זמן?

  • א. ביצועי השרת ישתפרו בשל פחות overhead: הנחה שגויה, אף שהיעדר wait דווקא גורם להצטברות תהליכי Zombie ולפגיעה במערכת כאן
  • ב. השרת יקרוס מיידית: הנחה שגויה, אף שהבעיה מצטברת בהדרגה עם הזמן ואינה גורמת לקריסה מיידית מיד עם החיבור הראשון שמתקבל
  • ג. יצטברו תהליכי Zombie: כל בן שסיים תופס שורה בטבלת התהליכים, ואחרי מספיק חיבורים ה-PIDs ייגמרו ו-fork חדש ייכשל כאן
  • ד. הקרנל מנקה תהליכי Zombie אוטומטית תמיד: הנחה שגויה, אף שהקרנל אינו מנקה Zombie שההורה שלו עדיין חי ולא קרא ל-wait עבורו
תשובה: ג

✓ תשובה: ג) Zombie accumulation — Child שיצא ללא wait() = Zombie. Zombie = Entry ב-Process Table, לא משאבי CPU/Memory. PID Table מוגבל (default: 32768 PIDs). אחרי מספיק connections ללא wait(): כל ה-PIDs תפוסים → fork() חדש נכשל (errno=EAGAIN). פתרון: signal(SIGCHLD, SIG_IGN) → Kernel מנקה אוטומטית.

גאמא סגנון ב׳ — RE Pattern · ב-IDA רואים ב-binary חשוד: CALL dup2 (3 פעמים עם args 0,1,2) ואחריו CALL execve עם "/bin/sh". מה המסקנה?

  • א. תוכנה שמנסה להשתמש במעטפת כמנוע scripting: הנחה שגויה, אף ששילוב dup2 עם execve("/bin/sh") הוא דפוס מובהק של Reverse Shell
  • ב. Reverse Shell: dup2 מפנה את stdin/stdout/stderr אל socket, ו-execve מריץ מעטפת, כך שהקלט והפלט של המעטפת עוברים אל התוקף
  • ג. שרת לגיטימי שמריץ פקודות shell: הנחה שגויה, אף שהפניית שלושת הזרמים אל socket יחד עם הרצת מעטפת מעידה על Backdoor זדוני
  • ד. קוד Debug שהושאר בטעות: הנחה שגויה, אף ששילוב הפניית הזרמים אל socket עם הרצת מעטפת הוא Reverse Shell קלאסי ולא קוד ניפוי
תשובה: ב

✓ תשובה: ב) Reverse Shell — dup2(sock,0)+dup2(sock,1)+dup2(sock,2)+execve("/bin/sh") = Reverse Shell הקלאסי. כל stdin/stdout/stderr של shell הולך לsocket. ב-CTF: זה הGoal שאתה מנסה להגיע אליו. ב-RE/Malware Analysis: זוהי Backdoor ודאית — דווח מיד.

גאמא סגנון ג׳ — Dry Run: exec לא חוזרת · pid_t pid = fork(); if (pid == 0) { printf("A\n"); execvp("ls", (char*[]){"ls", NULL}); printf("B\n"); exit(0); } else { wait(NULL); printf("C\n"); }מה הפלט (הנח שls מצליח ומדפיס "file.txt")?

pid_t pid = fork();
if (pid == 0) {
    printf("A\n");
    execvp("ls", (char*[]){"ls", NULL});
    printf("B\n");
    exit(0);
} else {
    wait(NULL);
    printf("C\n");
}
  • א. A ואז B ואז file.txt ואז C: הנחה שגויה, אף שהשורה B כלל אינה מודפסת מכיוון ש-exec הצליחה והחליפה את הבן לפני הדפסתה כאן
  • ב. A ואז file.txt ואז B ואז C: הנחה שגויה, אף שהשורה B כלל אינה מודפסת מכיוון ש-exec הצליחה והחליפה את קוד הבן במקומה כאן
  • ג. A ואז B ואז C: הנחה שגויה, אף שהפלט של ls שהוא file.txt כן מודפס והשורה B כלל אינה מודפסת בשל הצלחת הקריאה ל-exec כאן
  • ד. A ואז file.txt ואז C: השורה B אינה מודפסת כי exec הצליחה והחליפה את הבן, וה-Parent ממתין ומדפיס C לאחר ש-ls מסיים כאן
תשובה: ד

✓ תשובה: ד) A\nfile.txt\nC — Child: printf("A") → "A". execvp("ls"): מצליח → Child הפך ל-ls → מדפיס "file.txt". printf("B") לא מגיעים — exec הצליח! exit(0) לא מגיעים. Parent: wait() → ls יוצא → Parent מתעורר → printf("C") → "C". סדר: A → file.txt → C.

שאלה 6.1 [קל] · מה זו מתקפת DNS Cache Poisoning?

DNS Cache Poisoning
  • א. הצפת שרת ה-DNS בבקשות: הנחה שגויה, אף שהצפה היא מתקפת DoS, ו-Cache Poisoning דווקא מזריק רשומה כוזבת שמפנה משתמשים לאתר התוקף בפועל
  • ב. מחיקת כל רשומות ה-DNS: הנחה שגויה, אף שהמתקפה אינה מוחקת רשומות אלא מזייפת אותן ב-Cache, כדי לנתב את הקורבנות ליעד זדוני שנשלט בכל מקרה
  • ג. תוקף מזריק רשומת DNS כוזבת ל-Cache של ה-Resolver, וכתוצאה מכך כל המשתמשים שפונים לאותו Resolver ינותבו לאתר של התוקף במקום לאמיתי
  • ד. פיצוח סיסמת שרת ה-DNS: הנחה שגויה, אף שהמתקפה אינה מכוונת לסיסמה אלא להחדרת רשומה כוזבת ל-Cache שמנתבת משתמשים לאתר של התוקף
תשובה: ג

✓ תשובה נכונה: ג — הזרקת רשומה כוזבת ל-DNS Cache → כל הפונים ל-Resolver מנותבים לאתר זדוני. הגנה: DNSSEC (חתימות), ו-Randomized Query ID.

שאלה 6.2 [בינוני] · מה עושה HSTS (Strict-Transport-Security)?

HSTS
  • א. מצפין את בסיס הנתונים בשרת: הנחה שגויה, אף ש-HSTS עוסק בכפיית HTTPS בדפדפן בלבד, ואינו נוגע כלל להצפנת הנתונים המאוחסנים בשרת עצמו בכל מקרה
  • ב. חוסם עוגיות צד-שלישי: הנחה שגויה, אף שחסימת עוגיות נעשית ב-SameSite, ו-HSTS דווקא כופה על הדפדפן להשתמש ב-HTTPS בלבד לאותו אתר כאן בפועל
  • ג. אומר לדפדפן לעולם אל תתחבר אלי ב-HTTP אלא רק ב-HTTPS — כך הוא מגן מפני SSL Strip שבו תוקף מנסה להוריד את החיבור חזרה לפרוטוקול לא מוצפן
  • ד. מאמת את זהות המשתמש: הנחה שגויה, אף שאימות זהות נעשה ב-Cookies או Tokens, ו-HSTS רק מחייב את הדפדפן להתחבר לאתר דרך HTTPS בלבד כאן
תשובה: ג

✓ תשובה נכונה: ג — HSTS Header → הדפדפן זוכר: 'אתר זה HTTPS בלבד'. גם קישור http:// יומר אוטומטית ל-https://. מגן מ-SSL Strip (MITM שמוריד ל-HTTP).

שאלה 6.3 [קשה] · ב-Wireshark, בחיבור TLS 1.3, מה עדיין ניתן לראות?

TLS 1.3
  • א. רואים IP מקור ויעד, פורטים, את ה-ClientHello עם ה-SNI (שם הדומיין) וגדלי הפקטות — אך לא את תוכן הבקשות והתשובות עצמן שמוצפן
  • ב. רואים את כל תוכן הבקשות: הנחה שגויה, אף ש-TLS מצפין את גוף התעבורה, ולכן נחשפים רק המטא-נתונים כמו IP, פורטים ו-SNI ולא התוכן
  • ג. לא רואים שום דבר כלל: הנחה שגויה, אף שהמטא-נתונים כמו כתובות, פורטים וה-SNI נותרים גלויים, ולכן ניתן לראות מידע חלקי ולא ריק
  • ד. רואים את הסיסמאות בטקסט: הנחה שגויה, אף שגוף התעבורה מוצפן ב-TLS ולכן סיסמאות אינן חשופות, ורק מטא-נתונים כמו SNI נותרים גלויים
תשובה: א

✓ תשובה נכונה: א — TLS מצפין תוכן, לא מטא-נתונים. גלוי: IP, פורטים, גודל/תזמון פקטות, ו-SNI (שם הדומיין ב-ClientHello). ECH מנסה להצפין גם את ה-SNI.