GammaCyber

Sockets ורשת ב-C

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

// סקירה

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 פגיע + וולנרביליטים
🎯 איך זה מופיע במיוני גאמא
◄ Black Box CTF — פגיעה בשירות TCP שמריץ על Port מוגדר (Pwn Service)
◄ שאלון C — socket(), bind(), accept(), send/recv, Byte Order
◄ Reverse Engineering — זיהוי Network Server ב-Assembly, מציאת Protocol
◄ Exploit Development — Reverse Shell דרך Socket, Format String over Network
// פרק 1

יסודות: Socket, Address ו-Byte Order

Socket הוא File Descriptor לתקשורת. כמו open() מחזיר FD לקובץ, socket() מחזיר FD לחיבור רשת. ניתן לקרוא/לכתוב עליו עם read/write (או send/recv).

סוגקבועמאפייניםשימוש
TCPSOCK_STREAMאמין, סדרתי, חיבור דו-כיווניHTTP, SSH, CTF Pwn
UDPSOCK_DGRAMמהיר, ללא חיבור, ייתכן אובדןDNS, VoIP, Gaming
RAWSOCK_RAWגישה ישירה ל-Packets, דורש rootPing, 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 קודם!
✏תרגול עצמי3 שאלות
שאלה 1.1 — Byte Order
מדוע חייבים לקרוא htons(8080) לפני הכנסת ה-Port ל-sockaddr_in?
✓ תשובה: א) — 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) — TCP = אמין, מסודר, מבטיח הגעת Packets בסדר. CTF Pwn Services כמעט תמיד TCP. UDP לא מבטיח סדר או הגעה.
שאלה 1.3 — inet_pton
מה ההבדל בין inet_pton ל-inet_ntop?
✓ תשובה: ד) — 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

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);
💡 server_fd לעומת client_fd — לא לבלבל!
server_fd = Listening Socket — רק מקבל Connections, לא מדבר עם Client!
client_fd = Connected Socket — FD לשיחה עם Client ספציפי
accept() יוצר client_fd חדש לכל Client. 100 Clients = server_fd אחד + 100 client_fds.
שגיאה נפוצה: send/recv על server_fd במקום client_fd → שגיאה!
✏תרגול עצמי3 שאלות
שאלה 2.1 — סדר שלבי Server
מהו הסדר הנכון לבניית TCP Server?
✓ תשובה: ד) socket → bind → listen → accept → send/recv — הסדר קבוע ומחייב: קודם מייצרים FD, אחר כך קושרים לPort, אחר כך מאזינים, ורק אז מקבלים חיבורים.
שאלה 2.2 — accept()
מה מחזירה accept() בהצלחה?
✓ תשובה: ג) — accept() חוסם עד שClient מתחבר ואז מחזיר FD חדש (Connected Socket). כל Client = FD נפרד. שגיאה: -1. server_fd נשאר פתוח לחיבורים נוספים.
שאלה 2.3 — SO_REUSEADDR
מדוע מוסיפים setsockopt(SO_REUSEADDR) לכל Server?
✓ תשובה: א) — כש-Server מתרסק, הPort נשאר ב-TIME_WAIT state עד ~2 דקות. SO_REUSEADDR מאפשר bind מחדש מיד. חובה בכל Server שמתפתח ומופעל שוב ושוב.
// פרק 3

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 לא צריך זה! } }
🔑 חשוב לגאמא — Forking Server ב-CTF
שירותי CTF Pwn כמעט תמיד Forking Servers!
◄ ASLR: כל Child מקבל אותו Layout כמו Parent (fork=COW) → Brute-Force כתובות!
◄ Canary: אותו Canary לכל Children → Canary Brute-Force byte-by-byte
◄ FDs: חובה לסגור client_fd ב-Parent ו-server_fd ב-Child!
✏תרגול עצמי3 שאלות
שאלה 3.1 — Client vs Server
מה ההבדל המרכזי בין בניית TCP Client לבניית TCP Server?
✓ תשובה: ב) — Server: socket→bind→listen→accept (פסיבי, ממתין). Client: socket→connect (אקטיבי, יוזם). לאחר accept()/connect() שניהם משתמשים בsend/recv זהה.
שאלה 3.2 — Forking Server ו-ASLR
מדוע Forking Server מאפשר Brute-Force על ASLR ב-CTF?
✓ תשובה: ג) — fork() מעתיק את הזיכרון (Copy-On-Write). ה-ASLR seed נקבע פעם אחת לParent ולא משתנה ב-fork. כל Child = אותה כתובת libc/heap. שלח 256 ניסיונות לשבור byte אחד של Canary/Address.
שאלה 3.3 — FD בForking
בForking Server, מה חייב ה-Child לסגור ומה חייב ה-Parent לסגור?
✓ תשובה: ב) — Child מטפל בClient → לא צריך server_fd → close(server_fd). Parent ממשיך לacceptוחדשים → לא צריך client_fd → close(client_fd). אחרת: FD Leak + Client לא מקבל EOF.
// פרק 4

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/threadProcess/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);
✏תרגול עצמי3 שאלות
שאלה 4.1 — select()
מה מחזיר select() כש-Timeout עבר ולא היה שום אירוע?
✓ תשובה: ג) 0 — select() מחזיר: מספר FDs מוכנים (>0), 0 ב-Timeout, -1 בשגיאה. FD_ISSET() לאחר select() בודק אם FD ספציפי מוכן.
שאלה 4.2 — UDP vs TCP
מה ההבדל בין UDP Server לTCP Server בצד הקוד?
✓ תשובה: ג) — UDP: SOCK_DGRAM. אין listen/accept (אין חיבור). recvfrom() מקבל Packet + כתובת שולח. sendto() שולח לכתובת ספציפית. כל Packet עצמאי.
שאלה 4.3 — epoll vs select
מדוע Nginx/Redis/Node.js משתמשים ב-epoll() ולא ב-select()?
✓ תשובה: ג) — select() סורק את כל FD Set בכל קריאה = O(n). עם 10,000 connections = 10,000 בדיקות. epoll: Kernel שולח רק FDs שינוים = O(1). גם: select מוגבל ל-1024 FDs.
// פרק 5

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 socketTCP Socket נוצר
bind()push 16; lea [addr]; call bindServer קושר Port
listen()push backlog; push fd; call listenServer מאזין
accept()call accept + loop עם jmp אחורהServer מחכה לClients
htons(8080)push 0x901FPort 8080 ב-Network Order
Dry Run ניתוח Server פגיע — מצא 3 וולנרביליטים ›
void handle_client(int fd) { char name[32]; char greeting[64]; send(fd, "Name: ", 6, 0); recv(fd, name, 128, 0); // (A) send(fd, "Message: ", 9, 0); recv(fd, greeting, 64, 0); // (B) printf(greeting); // (C) char response[128]; sprintf(response, "Hi %s\n", name); // (D) send(fd, response, strlen(response), 0); }

(A) Stack Buffer Overflow: name[32] אבל recv קורא עד 128 bytes! → RCE

(B) בטוח: greeting[64] ו-recv קורא עד 64 — OK

(C) Format String: printf(greeting) ללא format string → תוקף שולח "%x %x %n" → R/W לזיכרון

(D) Stack Buffer Overflow: response[128] אבל name יכול להיות 128 bytes → sprintf כותב יותר מ-128

✏תרגול עצמי — גאמא סגנון3 שאלות
שאלה 5.1 — זיהוי ב-Assembly
ב-Assembly רואים: push 0x901F לפני call bind. מה ניתן להסיק?
✓ תשובה: ד) Port 8080 — 0x901F ב-Network Byte Order = htons(8080). בדיקה: ntohs(0x901F) = 0x1F90 = 8080. זו אחת הדרכים לזהות Protocol ב-IDA/Ghidra!
שאלה 5.2 — Format String
מה הבעיה ב-printf(greeting) כשgreeting מגיע מ-recv?
✓ תשובה: ב) — 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?
✓ תשובה: א) — dup2(sock_fd, 0/1/2) מחליף stdin/stdout/stderr בSocket. אחרי execve("/bin/sh"): ה-Shell קורא input מהSocket (קלט מהתוקף) ושולח output חזרה לSocket (אל התוקף). Reverse Shell מוחלט!
// אתגר מעשי

אתגר מעשי — רשתות — פרסור כותרת פרוטוקול

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

T3 · בינוני350 נק'~22 דק'algorithmic
פרסור כותרת פרוטוקול רשת
ממש parse_header(data) לכותרת רשת. data הוא רשימת בתים: בית 0 = version, בית 1 = type, בתים 2–3 = length (unsigned 16-bit, big-endian / network byte order), ואחריהם length בתי payload. החזר מילון {'version','type','length','payload'}. אם הכותרת קצרה מדי או ה-payload קטום → None.
דוגמאות
parse_header([1,2,0,3,10,20,30]) → {'version':1,'type':2,'length':3,'payload':[10,20,30]}
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס פרסור כותרת פרוטוקול רשת ›
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]}
// סיכום

סיכום שיעור 38

✓מה למדנו בשיעור 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)
🎯 השיעור הבא
שיעור 39 — Multithreading ב-C: pthread, Mutex, Race Conditions ו-Deadlock
שאלות תרגול ותשובות (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 מוחלט!