Processes ו-Signals ב-Linux
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)
Kernel Mode ו-User Mode: הגבול הקריטי
זהו הנושא המרכזי של השיעור — ובסיס כל תחום אבטחת המחשבים. ה-CPU עצמו אוכף הפרדה בין קוד עם הרשאות מלאות לקוד עם הרשאות מוגבלות.
CPU Privilege Rings
| Ring | שם | מי משתמש | הרשאות |
|---|---|---|---|
| Ring 0 | Kernel Mode | ה-Kernel וה-OS | גישה מלאה — כל כתובת זיכרון, כל פקודת CPU |
| Ring 3 | User Mode | כל תוכנת משתמש | גישה מוגבלת — רק לזיכרון הווירטואלי שלה |
! Rootkit — האיום הקשה ביותר ›
אם malware מצליח לרוץ ב-Kernel Mode — הוא מקבל גישה מלאה לכל הזיכרון, לכל התהליכים, לכל ה-Hardware. יכול לקרוא סיסמאות, להשבית אנטי-וירוס, להסתתר לחלוטין מה-OS. זה נקרא Rootkit.
System Calls: הגשר בין User לKernel
System Call הוא מנגנון מסודר שבו תוכנת User Mode מבקשת מה-Kernel לבצע פעולה בשמה — הממשק הרשמי בין עולם המשתמש לעולם ה-Kernel.
טבלת Syscalls חשובים (Linux x86-64)
| מספר | שם | מה עושה | חשוב לסייבר |
|---|---|---|---|
| 0 | read | קריאה מ-file descriptor | – |
| 1 | write | כתיבה ל-file descriptor | – |
| 2 | open | פתיחת קובץ | malware פותח /etc/passwd |
| 59 | execve | הרצת תוכנה חדשה | Shellcode: execve('/bin/sh') |
| 60 | exit | סיום תהליך | – |
תהליכים (Processes) ו-Threads
תהליך הוא מופע רץ של תוכנית — עם מרחב זיכרון וירטואלי משלו, PID ייחודי, ומשאבים עצמאיים. Thread הוא יחידת ביצוע בתוך תהליך.
| Process | Thread |
|---|---|
| מרחב זיכרון נפרד | חולק זיכרון עם Threads אחרים |
| יצירה יקרה (~1ms, מאות KB) | יצירה זולה (~10µs, כמה KB) |
| תקשורת ע"י IPC (pipes, sockets) | תקשורת ע"י shared memory (מהירה) |
| crash בProcess אחד לא פוגע באחר | Thread שקורס יכול להרוג את כל ה-Process |
Context Switch וה-Scheduler: אשליית המקביליות
במחשב עם מעבד אחד, רק תהליך אחד רץ ברגע נתון. ה-OS מחליף בין תהליכים מאות פעמים בשנייה — כל כך מהיר שמרגיש במקביל.
זיכרון וירטואלי: כל תהליך בעולמו
כל תהליך מקבל מרחב כתובות וירטואלי פרטי. כתובות וירטואליות מתורגמות על ידי החומרה (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 לא יכול לכתוב לכאן |
| Heap | rw- | malloc, new | Heap Overflow, Use-After-Free |
| Stack | rw- | local vars, return addr | Buffer Overflow — הכי נפוץ! |
| Kernel Space | — | Kernel code/data | Kernel Exploit נדרש לגשת |
! Segfault ≠ NULL בלבד ›
Segmentation Fault קורה בכל גישה לעמוד לא ממופה או חסר הרשאה. NULL pointer = עמוד 0 לא ממופה → Segfault. אבל גם גישה לזיכרון שנמחק (Use-After-Free) וגם Buffer Overflow שחרג לאזור לא ממופה — כולם Segfault.
Windows לעומת Linux: ההבדלים המרכזיים
שני ה-OS הנפוצים ביותר שונים בארכיטקטורה, פילוסופיה ומימוש. בגאמא תיתקל בשניהם.
| נושא | Linux | Windows |
|---|---|---|
| Kernel | Monolithic Kernel, Open Source | Hybrid Kernel, Closed Source |
| קבצים | הכל הוא קובץ: /dev/sda, /proc/1 | Drive letters: C:\, Registry |
| תהליכים | fork() + exec() | CreateProcess() |
| CLI | Bash | CMD / PowerShell |
| Config | קבצי טקסט ב-/etc/ | Registry |
תרגול נוסף — Kernel, Syscalls וזיכרון וירטואלי
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
open("/etc/passwd"). באיזה Ring רץ הקוד שמבצע את הפתיחה בפועל?תרגול נוסף — זיכרון וירטואלי מתקדם
שאלות סיכום על מרחב הכתובות הווירטואלי ועל ההפרדה בין מרחב המשתמש למרחב הקרנל.
Process ו-fork(): שכפול תהליך
כל Process ב-Linux הוא ישות עצמאית עם מרחב כתובות נפרד. fork() יוצר Child Process שהוא עותק כמעט מושלם של ה-Parent — ההבדל היחיד הוא ערך החזרה.
מבנה Process — PCB
| שדה ב-PCB | משמעות | דוגמה/ערך |
|---|---|---|
| PID | Process ID — מזהה ייחודי | 1234 |
| PPID | Parent Process ID | 1 (init/systemd) |
| UID/GID | User/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);
}pid_t pid = fork();. מה ערך pid ב-Child Process?int main() {
pid_t pid = fork();
if (pid == 0) {
printf("Child\n");
exit(0);
} else {
wait(NULL);
printf("Parent\n");
}
return 0;
}מה הפלט ומה הסדר המובטח?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 נסגרים אוטומטית◄ ב-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"); // ← לא יודפס!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 מצליח?Signals: תקשורת אסינכרונית
Signal הוא הודעה אסינכרונית לProcess. הקרנל שולח SIGSEGV ב-NULL dereference, Ctrl+C שולח SIGINT. Processes יכולים לתפוס ולטפל ב-Signals — חוץ מ-SIGKILL ו-SIGSTOP.
| Signal | מספר | סיבה קלאסית | ברירת מחדל | ניתן לתפוס? |
|---|---|---|---|---|
| SIGKILL | 9 | kill -9 — הרג מיידי | סיום מיידי | לא! לעולם |
| SIGTERM | 15 | kill — בקשת סיום מנומסת | סיום | כן |
| SIGINT | 2 | Ctrl+C מה-Terminal | סיום | כן |
| SIGSEGV | 11 | Segmentation Fault (NULL deref) | Core Dump | כן (לא מומלץ) |
| SIGCHLD | 17 | Child סיים | התעלמות | כן |
| SIGUSR1/2 | 10/12 | שימוש מותאם אישית | סיום | כן |
| SIGSTOP | 19 | עצירה (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 בודק.
CALL signal + push 11 + push handler_func. מה הקוד עושה?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!◄ פתח 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
int pipefd[2]; pipe(pipefd);, כיצד Process כותב ל-Pipe?dup2(sock_fd, 0);
dup2(sock_fd, 1);
execve("/bin/sh", ...);מה זה עושה?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);
}מה יודפס?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!mov eax, 2
int 0x80
test eax, eax
jz loc_CHILDמה מתאר הקוד?CALL execve ובאזור ה-data: "/bin/sh". מסקנה?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- 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
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")?תרגול נוסף — אבטחת רשת: DNS, HSTS ו-TLS
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
DNS Cache Poisoning?HSTS (Strict-Transport-Security)?TLS 1.3, מה עדיין ניתן לראות?אתגר מעשי — תהליכים — fork ו-IPC
שיא השיעור: תכנות מערכת ברמה נמוכה — זיכרון, קבצים, תהליכים, מקבילות. ההגשה נבדקת אוטומטית.
שאלות תרגול ותשובות (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.