GammaCyber

File I/O ב-C

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

// סקירה

File I/O ב-C — שתי שכבות, שתי עולמות

ב-C יש שתי שכבות של File I/O: שכבת הספרייה הסטנדרטית (stdio.h — FILE*, fopen/fread) ושכבת System Calls ישירות (unistd.h — File Descriptors, open/read). ב-CTF וב-RE נפגוש את שתיהן.

✓נושאי השיעור
  • stdio.h — FILE*, fopen, fread, fwrite, fclose
  • מודי פתיחה — r, w, a, rb, wb ומשמעותם
  • fseek, ftell, rewind — ניווט בקובץ
  • File Descriptors — open, read, write, close (POSIX)
  • mmap — מיפוי קובץ לזיכרון
  • Path Traversal, TOCTOU, /proc — אבטחה ו-CTF
  • File I/O ב-Assembly — זיהוי ב-IDA/Ghidra
🎯 איך זה מופיע במיוני גאמא
שיעור זה מכין לשלבים:
◄ שאלון C — שאלות על fopen/fread/fwrite, מודי פתיחה, מה FILE* מכיל
◄ Black Box CTF — קריאת קבצים בינאריים, מציאת פורמטים, Path Traversal
◄ Reverse Engineering — זיהוי I/O calls ב-Assembly, שחזור פורמט קובץ
◄ Exploit Development — Race Condition על קבצים, TOCTOU, /proc/self/mem
// פרק 1

stdio.h: שכבת הספרייה הסטנדרטית

stdio.h מספקת FILE* — מבנה Opaque שמנהל Buffering, מיקום נוכחי בקובץ, ומידע על מצב. כל פעולת I/O דרך FILE* עוברת דרך Buffer פנימי לפני שמגיעה לקרנל.

fopen ו-fclose

#include <stdio.h> // פרוטוטיפ: FILE *fopen(const char *path, const char *mode); // מחזיר: FILE* בהצלחה, NULL בכישלון FILE *f; f = fopen("data.txt", "r"); // Read — קריאה בלבד f = fopen("data.txt", "w"); // Write — כתיבה, מוחק תוכן קיים f = fopen("data.txt", "a"); // Append — מוסיף לסוף f = fopen("data.txt", "r+"); // Read+Write — חייב להתקיים f = fopen("data.bin", "rb"); // Binary Read — לקבצים בינאריים! f = fopen("data.bin", "wb"); // Binary Write // ─── בדיקת הצלחה — חובה! ────────────────────────────── f = fopen("config.txt", "r"); if (f == NULL) { perror("fopen"); // מדפיס: "fopen: No such file or directory" return -1; } // ─── fclose — חובה לסגור! ───────────────────────────── fclose(f); // מרוקן Buffer לדיסק (Flush) ומשחרר משאבים // שכחת fclose = File Descriptor Leak!
מודקריאהכתיבהיוצר קובץ?מוחק תוכן?
"r"✓✗✗ (שגיאה)✗
"w"✗✓✓✓
"a"✗✓✓✗ (מוסיף לסוף)
"r+"✓✓✗ (שגיאה)✗
"w+"✓✓✓✓
"rb"✓ (binary)✗✗✗
✏תרגול עצמי3 שאלות
שאלה 1.1 — מודי fopen
פתחנו קובץ עם fopen("data.txt", "w"). מה קורה אם הקובץ כבר קיים ומכיל נתונים?
✓ תשובה: ד) — "w" = Write mode: אם הקובץ קיים, תוכנו נמחק לחלוטין (O_TRUNC). אם לא קיים, נוצר חדש. לכתיבה ללא מחיקה: השתמש ב-"a" (Append).
שאלה 1.2 — Binary vs Text
מדוע חשוב להשתמש ב-"rb" ולא "r" לפתיחת קובץ ELF/PNG ב-Windows?
✓ תשובה: ב) — ב-Windows, Text Mode ("r") מתרגם \r\n ל-\n בקריאה. קובץ בינארי שמכיל 0x0D 0x0A יהפוך ל-0x0A — שחיתות נתונים! תמיד "rb"/"wb" לקבצים בינאריים.
שאלה 1.3 — fclose
מה קורה אם לא קוראים ל-fclose() לאחר fopen() וכתיבה עם fprintf()?
✓ תשובה: ב) — fclose() עושה Flush לBuffer ומשחרר FD. ללא fclose: הנתונים שב-Buffer הפנימי עשויים לא להגיע לדיסק. גם: FD Leak — Process מוגבל במספר FDs פתוחים (ברירת מחדל 1024).
// פרק 2

fread, fwrite ו-fseek: קריאה, כתיבה וניווט

fread ו-fwrite הם הדרך הכי גמישה לקריאה/כתיבה — עובדים על Structs, מערכים, וכל מבנה בינארי. חיוניים לפורמטים כמו PNG, ELF, PE.

fread ו-fwrite

// פרוטוטיפ: size_t fread(void *buf, size_t size, size_t count, FILE *f); // מחזיר: מספר ה-Elements שנקראו (לא bytes!). קטן מ-count = EOF/שגיאה // ─── קריאת קובץ בינארי לתוך Struct ───────────────────── typedef struct { uint32_t magic; // 4 bytes — סיגנטורה uint16_t version; // 2 bytes uint32_t data_size; // 4 bytes } FileHeader; FILE *f = fopen("data.bin", "rb"); FileHeader hdr; size_t n = fread(&hdr, sizeof(FileHeader), 1, f); if (n != 1) { fprintf(stderr, "Failed to read header\n"); fclose(f); return -1; } printf("Magic: 0x%08X\n", hdr.magic); // ─── כתיבת Struct לקובץ ────────────────────────────── FileHeader out_hdr = { 0xDEADBEEF, 1, 1024 }; FILE *wf = fopen("out.bin", "wb"); fwrite(&out_hdr, sizeof(FileHeader), 1, wf); fclose(wf);

fseek, ftell ו-rewind

// fseek: הזז את File Position Indicator // int fseek(FILE *f, long offset, int whence); // whence: SEEK_SET (מהתחלה), SEEK_CUR (מהמיקום הנוכחי), SEEK_END (מהסוף) FILE *f = fopen("data.bin", "rb"); fseek(f, 0, SEEK_END); // לך לסוף הקובץ long file_size = ftell(f); // = גודל הקובץ fseek(f, 0, SEEK_SET); // חזור לתחילת הקובץ (= rewind(f)) fseek(f, 16, SEEK_SET); // קפוץ ל-byte 16 fseek(f, -4, SEEK_END); // 4 bytes לפני הסוף // ─── Pattern קלאסי: קרא את כל הקובץ לזיכרון ──────── fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); char *buf = malloc(sz + 1); fread(buf, 1, sz, f); buf[sz] = '\0'; // NULL-terminate (לטקסט) fclose(f);
! feof — הטעות הנפוצה ביותר ›
// ✗ שגוי: while (!feof(f)) { fread(...); } // קורא פעם נוספת אחרי EOF! // ✓ נכון: while (fread(buf, size, 1, f) == 1) { ... } // fread מחזיר 0 ב-EOF/שגיאה — זו הבדיקה הנכונה
✏תרגול עצמי4 שאלות
שאלה 2.1 — fread מחזיר
int buf[100]; size_t n = fread(buf, sizeof(int), 100, f);הקובץ מכיל רק 37 ints. מה ערך n?
✓ תשובה: ד) 37 — fread מחזיר מספר Elements (לא bytes!). קשנו לקרוא 100 ints אבל יש רק 37 → מחזיר 37. לבדוק EOF: if (n < 100) — הגענו לסוף הקובץ.
שאלה 2.2 — גודל קובץ
מהו הPattern הנכון לחישוב גודל קובץ דרך FILE*?
✓ תשובה: ב) — הPattern הקלאסי: (1) fseek ל-SEEK_END, (2) ftell מחזיר offset = גודל הקובץ, (3) fseek חזרה ל-SEEK_SET לפני קריאה. rewind = fseek(f,0,SEEK_SET).
שאלה 2.3 — Dry Run fread
FILE *f = fopen("test.txt", "w"); fprintf(f, "Hello\nWorld\n"); fclose(f); f = fopen("test.txt", "r"); char buf[10]; fread(buf, 1, 5, f); printf("%s\n", buf);מה הבעיה בקוד?
✓ תשובה: ד) — fread קרא 5 bytes ('H','e','l','l','o'). buf[5] ואילך = ערכי זבל. printf("%s") ידפיס עד '\0' הראשון — UB! תיקון: buf[5] = '\0'; לפני printf.
שאלה 2.4 — fseek SEEK_CUR
לאחר קריאת header של 16 bytes, רוצים לדלג עוד 8 bytes קדימה ולקרוא מהמיקום הזה. מה הקריאה הנכונה?
✓ תשובה: ד) fseek(f, 8, SEEK_CUR) — SEEK_CUR = מהמיקום הנוכחי (16, אחרי קריאת ה-header) קדימה 8 → מיקום 24. א) SEEK_SET,8 = מיקום מוחלט 8 — בתוך ה-header! ג) SEEK_END = מסוף הקובץ. רק SEEK_CUR מבצע דילוג יחסי מהמיקום הנוכחי.
// פרק 3

System Calls: שכבת הקרנל הישירה

File Descriptor (FD) הוא מספר שלם שמזהה קובץ פתוח בתוך הPCB. System Calls ישירות — בלי Buffering — מהירות יותר ומשמשות לניצולים ו-CTF.

File Descriptors מוגדרים מראש

// FD 0 = stdin (Standard Input) // FD 1 = stdout (Standard Output) // FD 2 = stderr (Standard Error) // כל fopen יוצר FD חדש (3, 4, 5, ...) FILE *f = fopen("data.txt", "r"); int fd = fileno(f); // fd = 3 (בדרך כלל) // write ישירות ל-stdout = FD 1: write(1, "Direct to stdout\n", 17);

open, read, write, close

#include <fcntl.h> // לopen #include <unistd.h> // לread, write, close // ─── open ────────────────────────────────────────────── int fd = open("data.txt", O_RDONLY); int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644); // Flags נפוצים: // O_RDONLY=0, O_WRONLY=1, O_RDWR=2 // O_CREAT — צור קובץ אם לא קיים (דורש mode) // O_TRUNC — מחק תוכן קיים // O_APPEND — הוסף לסוף // ─── read ────────────────────────────────────────────── char buf[1024]; ssize_t n = read(fd, buf, sizeof(buf)); // n>0: bytes שנקראו. n==0: EOF. n==-1: שגיאה if (n > 0) { buf[n] = '\0'; printf("%s", buf); } // ─── write ───────────────────────────────────────────── const char *msg = "Hello, File!\n"; write(fd, msg, strlen(msg)); // ─── close ───────────────────────────────────────────── close(fd);
stdio (FILE*)System Call (FD)הבדל עיקרי
fopen()open()FILE* = Wrapper מסביב ל-FD
fread()read()fread: Buffered. read: ישיר לקרנל
fwrite()write()fwrite: Buffered. write: מיידי
fclose()close()fclose גם עושה Flush לBuffer
fseek()lseek()זהה בהתנהגות
✏תרגול עצמי3 שאלות
שאלה 3.1 — FDs מוגדרים מראש
מה מספר ה-FD של stderr?
✓ תשובה: ב) 2 — FD 0=stdin, FD 1=stdout, FD 2=stderr. שלושה אלה תמיד פתוחים בכניסה ל-main(). FD 3+ = קבצים שפותחים בזמן ריצה.
שאלה 3.2 — open flags
איזה flag נדרש כדי לפתוח קובץ לכתיבה ולוודא שהוא ייווצר אם לא קיים?
✓ תשובה: א) O_WRONLY | O_CREAT — O_WRONLY לכתיבה, O_CREAT ליצירה אם לא קיים (דורש argument שלישי — mode כמו 0644). O_WRONLY לבד ייכשל אם הקובץ לא קיים.
שאלה 3.3 — stdio vs syscall
מדוע לפעמים עדיף להשתמש ב-write() ישירות במקום fprintf() בקוד Exploit?
✓ תשובה: ד) — ב-Exploit Development: write() = System Call ישיר ללא תלות ב-libc internals. לא צריך לדאוג לBuff state. גם: ב-Shellcode, קוראים ל-syscall ישירות (syscall(1, ...) ב-x64 = write).
// פרק 4

mmap: מיפוי קובץ לזיכרון

mmap מאפשר למפות קובץ ישירות לזיכרון — גישה לתוכן הקובץ כאילו הוא מערך בזיכרון, ללא fread/fwrite. נפוץ מאוד ב-ELF Loaders, Database Engines, ו-Exploit Development.

#include <sys/mman.h> #include <sys/stat.h> // void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); // מחזיר: כתובת הזיכרון הממופה, MAP_FAILED בכישלון // ─── mmap קובץ לקריאה ──────────────────────────────── int fd = open("data.bin", O_RDONLY); struct stat st; fstat(fd, &st); size_t size = st.st_size; char *data = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); if (data == MAP_FAILED) { perror("mmap"); close(fd); return -1; } // עכשיו data הוא Pointer לכל הקובץ בזיכרון! printf("First byte: 0x%02X\n", (unsigned char)data[0]); // גישה כ-Struct ישירות! FileHeader *hdr = (FileHeader *)data; printf("Magic: 0x%08X\n", hdr->magic); // ─── ניקוי ─────────────────────────────────────────── munmap(data, size); close(fd); // ─── mmap לזיכרון אנונימי ──────────────────────────── void *anon = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); munmap(anon, 4096);
🔑 חשוב לגאמא — mmap ב-CTF ו-Exploit
◄ ELF Loader: הקרנל mmaps את ה-sections (TEXT, DATA, BSS) לזיכרון בטעינה
◄ /proc/self/maps: רשימת כל ה-mmaps של Process — כתובות, הרשאות, מקורות
◄ PROT_READ|PROT_WRITE|PROT_EXEC: mmap עם כל ההרשאות = Shellcode injection
◄ ב-CTF: mmap(0, 0x1000, PROT_RWX, ...) → כתוב Shellcode → קפוץ שם
◄ ב-RE: CALL mmap ב-Assembly = בקשת זיכרון/קובץ mapped — בדוק את ה-prot flags!
✏תרגול עצמי3 שאלות
שאלה 4.1 — mmap vs fread
מהו היתרון המרכזי של mmap לעומת fread לקריאת קובץ גדול?
✓ תשובה: ד) — עם mmap: גישה לoffset כלשהו בקובץ ישירות דרך pointer ארithmetic. הקרנל טוען רק את הדפים שנגשים אליהם (Demand Paging). fread מחייב buffer ו-IO explicit לכל קריאה.
שאלה 4.2 — PROT flags
מה זה PROT_READ | PROT_WRITE | PROT_EXEC בmmap?
✓ תשובה: ד) — PROT_RWX = Read+Write+Execute. כותבים Shellcode לזיכרון זה, ואז קופצים אליו — הוא ריץ כקוד! מנגנוני הגנה כמו NX/DEP מונעים PROT_EXEC על ה-Heap/Stack. mmap RWX עוקף זאת.
שאלה 4.3 — /proc/self/maps
ב-CTF, מה ניתן להשיג מקריאת /proc/self/maps?
✓ תשובה: א) — /proc/self/maps מראה כל memory mapping: כתובות start-end, הרשאות (r-xp), שם הקובץ. קריאתו חושפת base address של libc, stack, heap — ומאפשר לחשב כתובות גם עם ASLR מופעל!
// פרק 5

File Security: Path Traversal, TOCTOU ו-/proc

File I/O הוא וקטור תקיפה מרכזי ב-CTF ובאבטחה. Path Traversal מאפשר גישה לקבצים מחוץ לתיקייה המותרת; TOCTOU ניצל Race Condition בין בדיקה לשימוש.

Path Traversal — גישה לקבצים לא מורשים

// ─── קוד פגיע ───────────────────────────────────────── void read_file(char *filename) { char path[256]; snprintf(path, sizeof(path), "/safe/dir/%s", filename); FILE *f = fopen(path, "r"); } // תוקף שולח: filename = "../../etc/passwd" // path = "/safe/dir/../../etc/passwd" = "/etc/passwd"! // ─── הגנה: bדיקת Path ────────────────────────────── char resolved[PATH_MAX]; realpath(path, resolved); // מנרמל ומחסל ../ if (strncmp(resolved, "/safe/dir/", 10) != 0) { // Path traversal detected! return; }

TOCTOU — Race Condition על קבצים

// TOCTOU = Time Of Check To Time Of Use // ─── קוד פגיע ───────────────────────────────────────── if (access("safe_file.txt", R_OK) == 0) { // (1) בדיקה // ← תוקף: מחליף safe_file.txt → symlink ל-/etc/shadow FILE *f = fopen("safe_file.txt", "r"); // (2) שימוש → UH OH } // ─── הגנה: open() + fstat() על ה-FD ───────────────── int fd = open("safe_file.txt", O_RDONLY); struct stat st; fstat(fd, &st); // בדוק על ה-FD — לא על שם הקובץ! // אין חלון Race Condition בין open ל-fstat
הרחבה /proc — קבצים וירטואליים של Linux ›
// /proc/self/maps — כל memory mappings (ASLR bypass!) // /proc/self/mem — גישה ישירה לכל זיכרון הProcess // /proc/self/exe — symlink לבינארי עצמו // /proc/self/fd/ — כל ה-FDs הפתוחים // קריאת /proc/self/maps ב-CTF: FILE *maps = fopen("/proc/self/maps", "r"); char line[512]; while (fgets(line, sizeof(line), maps)) printf("%s", line); fclose(maps); // פלט: 08048000-08049000 r-xp 00000000 08:01 1234 /bin/prog // /proc/self/mem: כתיבה לזיכרון Process (דורש privileges) int mem_fd = open("/proc/self/mem", O_RDWR); lseek(mem_fd, target_addr, SEEK_SET); write(mem_fd, shellcode, shellcode_len);
✏תרגול עצמי3 שאלות
שאלה 5.1 — Path Traversal
תוכנה בונה path: "/uploads/" + user_input. אם תוקף שולח ../../etc/shadow, מה path הסופי?
✓ תשובה: א) /etc/shadow — /uploads/../../etc/shadow = /uploads → / → /etc/shadow. כל ../ עולה תיקייה אחת. הגנה: realpath() → בדיקת prefix, או הסרת ../ מה-input.
שאלה 5.2 — TOCTOU
מדוע access()+fopen() מסוכן? מה הבעיה בסדר הפעולות?
✓ תשובה: א) — TOCTOU = Time Of Check To Time Of Use. access() בודק הרשאות, אבל עד fopen() — חלון של מאות microseconds. תוקף עם thread/process יכול להחליף symlink. פתרון: open() ישיר + fstat() על ה-FD.
שאלה 5.3 — /proc CTF
ב-CTF נדרשים למצוא כתובת base address של libc בזמן ריצה (ASLR מופעל). מה הדרך הקלה ביותר?
✓ תשובה: ג) — /proc/self/maps מראה שורות כמו: 7f1234000000-7f1234100000 r-xp ... /lib/x86_64-linux-gnu/libc.so.6. הכתובת הראשונה = libc base. מכאן מחשבים offsets לfunctions כמו system().
// פרק 6

File I/O ב-Assembly ו-Dry Run

זיהוי קריאות I/O ב-Assembly חיוני ל-Reverse Engineering. fopen/fread/open/read — לכל אחד pattern מזוהה בקוד.

; ─── fopen + fread ב-Assembly (cdecl 32bit) ────────────── ; C: FILE *f = fopen("secret.txt", "r"); push offset aR ; push "r" push offset aSecretTxt ; push "secret.txt" call fopen add esp, 8 mov [ebp-4], eax ; [ebp-4] = FILE* f test eax, eax jz .error ; NULL = שגיאה ; C: fread(buf, 1, 256, f); push [ebp-4] ; push f push 256 ; push count push 1 ; push size lea eax, [ebp-260] ; eax = buf (256 bytes ב-Stack) push eax call fread add esp, 16 ; ─── סימנים לזיהוי ב-IDA/Ghidra ─────────────────────── ; CALL fopen → חפש String Literal לפני (שם קובץ + mode) ; CALL fread → 4 ארגומנטים: buf, size, count, FILE* ; CALL open → 2-3 ארגומנטים: path, flags [, mode] ; TEST EAX, EAX + JZ אחרי fopen/open = NULL check
הרחבה RE של קובץ Config עם Magic Number ›

IDA מראה: push 0x4D414749 → push offset aRb → push offset aConfig

הפשר: 0x4D414749 = ASCII 'MAGI' (Little-Endian: 49 47 41 4D) = Magic Number

fopen("config", "rb") → fread(&hdr, 4, 1, f) → if(hdr.magic != 0x4D414749) error

← כך מזהים פורמט קובץ קנייני ב-RE!

✏תרגול עצמי — גאמא סגנון3 שאלות
שאלה 6.1 — Dry Run מלא
FILE *f = fopen("test.txt", "w"); fprintf(f, "Hello\nWorld\n"); fclose(f); f = fopen("test.txt", "r"); char line[20]; fgets(line, sizeof(line), f); printf("%zu\n", strlen(line)); fclose(f);מה יודפס?
✓ תשובה: א) 6 — fgets קוראת שורה ראשונה: 'H','e','l','l','o','\n' + '\0' = 7 תווים ב-buffer. strlen לא סופרת '\0'. H-e-l-l-o-\n = 6 תווים. strlen("Hello\n") = 6.
שאלה 6.2 — ניתוח Assembly
ב-Assembly רואים: push 0, push offset path, call open, test eax, eax, js error. מה flag 0 מייצג?
✓ תשובה: ד) O_RDONLY = 0 — O_RDONLY=0, O_WRONLY=1, O_RDWR=2. push 0 → O_RDONLY. test eax, eax + js = בדיקת -1 (שגיאה). זיהוי: קריאת קובץ לבדה ← שלב ראשון ב-RE של binary parser.
שאלה 6.3 — גאמא סגנון
int fd = open("/proc/self/maps", O_RDONLY); char buf[4096]; ssize_t n = read(fd, buf, sizeof(buf) - 1); buf[n] = '\0'; // buf מכיל כעת... close(fd);מה מכיל buf לאחר הקוד?
✓ תשובה: ב) — /proc/self/maps הוא Virtual File שמייצג מצב הזיכרון. read() ממנו מחזיר שורות כמו: 7fff...000 r-xp ... /bin/prog. CTF קלאסי: קרא maps, מצא libc base, חשב system() address.
// אתגר מעשי

אתגר מעשי — קבצים — אבטחה ו-fseek

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

T3 · בינוני400 נק'~25 דק'algorithmic
מניעת Path Traversal
ממש safe_resolve(base, user_path) שמאחד base עם נתיב שסיפק משתמש, מנרמל אותו (מטפל ב-'..' ו-'.'), ומחזיר את הנתיב המנורמל *רק אם* הוא נשאר בתוך base. ניסיון מילוט (../../etc/passwd או נתיב מוחלט) → None. זו הגנת ה-Path Traversal הקלאסית.
דוגמאות
safe_resolve('/srv/www', 'index.html') → '/srv/www/index.html'
safe_resolve('/srv/www', '../../etc/passwd') → None
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס מניעת Path Traversal ›
import posixpath def safe_resolve(base: str, user_path: str): base = posixpath.normpath(base) full = posixpath.normpath(posixpath.join(base, user_path)) # מנרמל '..' ו-'.' if full == base or full.startswith(base + '/'): return full return None # ניסיון מילוט
T2 · בסיסי200 נק'~12 דק'algorithmic
חישוב מיקום fseek
ממש resolve_seek(size, ops) המדמה סדרת fseek. המיקום מתחיל ב-0. כל op הוא (whence, offset): 'SET' מוחלט מתחילת הקובץ, 'CUR' יחסי למיקום הנוכחי, 'END' יחסי לסוף (size+offset). החזר את המיקום הסופי.
דוגמאות
resolve_seek(100, [('SET',10),('CUR',5),('END',-1)]) → 99
resolve_seek(100, [('END',0)]) → 100
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס חישוב מיקום fseek ›
def resolve_seek(size: int, ops: list) -> int: pos = 0 for whence, off in ops: if whence == 'SET': pos = off # מתחילת הקובץ elif whence == 'CUR': pos += off # יחסית למיקום elif whence == 'END': pos = size + off # מסוף הקובץ return pos
// סיכום

סיכום שיעור 36

✓מה למדנו בשיעור 36
  • שתי שכבות: stdio (FILE*, Buffered) ו-POSIX System Calls (FD, ישיר לקרנל)
  • fopen modes: "r"=קרא, "w"=כתוב+מחק, "a"=הוסף, "rb"/"wb"=בינארי (תמיד לbinary!)
  • fread/fwrite: עובדים על Elements — מחזירים מספר Elements (לא bytes!)
  • fseek(f, offset, SEEK_SET/CUR/END) + ftell(f) = ניווט + מיקום
  • Pattern קלאסי: fseek(SEEK_END)+ftell → גודל קובץ, fseek(SEEK_SET) → חזרה
  • FD: מספר שלם (0=stdin, 1=stdout, 2=stderr). open/read/write/close ישירות
  • mmap: מיפוי קובץ לזיכרון → גישה כ-Pointer. PROT_RWX = Shellcode injection
  • Path Traversal: ../ ב-Input → גישה לקבצים מחוץ לתיקייה המותרת
  • TOCTOU: access()+fopen() — חלון Race Condition. תיקון: open()+fstat()
  • /proc/self/maps: כתובות ASLR. /proc/self/exe: הבינארי עצמו
🎯 השיעור הבא
שיעור 37 — Processes ו-Signals ב-Linux: fork, exec, wait ו-IPC
שאלות תרגול ותשובות (19)

שאלה 1.1 — מודי fopen · פתחנו קובץ עם fopen("data.txt", "w"). מה קורה אם הקובץ כבר קיים ומכיל נתונים?

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

✓ תשובה: ד) — "w" = Write mode: אם הקובץ קיים, תוכנו נמחק לחלוטין (O_TRUNC). אם לא קיים, נוצר חדש. לכתיבה ללא מחיקה: השתמש ב-"a" (Append).

שאלה 1.2 — Binary vs Text · מדוע חשוב להשתמש ב-"rb" ולא "r" לפתיחת קובץ ELF/PNG ב-Windows?

  • א. מצב "rb" הוא פשוט המהיר יותר לקריאה מבין השניים, אף שההבדל האמיתי הוא בתרגום תווי סוף השורה שבקריאה כאן
  • ב. מצב "r" ב-Windows מתרגם \r\n אל \n, מה שגורם לשחיתות בקבצים בינאריים כמו ELF או PNG בזמן הקריאה כאן
  • ג. מצב "rb" מאפשר גם כתיבה אל הקובץ, אף שהאות b מסמנת מצב בינארי ואינה מוסיפה כלל הרשאת כתיבה אל הקובץ כאן
  • ד. אין ביניהם כל הבדל ושניהם זהים בכל מערכת הפעלה, אף שב-Windows מצב "r" מתרגם שורות ומשחית קבצים בינאריים
תשובה: ב

✓ תשובה: ב) — ב-Windows, Text Mode ("r") מתרגם \r\n ל-\n בקריאה. קובץ בינארי שמכיל 0x0D 0x0A יהפוך ל-0x0A — שחיתות נתונים! תמיד "rb"/"wb" לקבצים בינאריים.

שאלה 1.3 — fclose · מה קורה אם לא קוראים ל-fclose() לאחר fopen() וכתיבה עם fprintf()?

// פרוטוטיפ: size_t fread(void *buf, size_t size, size_t count, FILE *f);
// מחזיר: מספר ה-Elements שנקראו (לא bytes!). קטן מ-count = EOF/שגיאה

// ─── קריאת קובץ בינארי לתוך Struct ─────────────────────
typedef struct {
    uint32_t magic;     // 4 bytes — סיגנטורה
    uint16_t version;   // 2 bytes
    uint32_t data_size; // 4 bytes
} FileHeader;

FILE *f = fopen("data.bin", "rb");
FileHeader hdr;
size_t n = fread(&hdr, sizeof(FileHeader), 1, f);
if (n != 1) {
    fprintf(stderr, "Failed to read header\n");
    fclose(f); return -1;
}
printf("Magic: 0x%08X\n", hdr.magic);

// ─── כתיבת Struct לקובץ ──────────────────────────────
FileHeader out_hdr = { 0xDEADBEEF, 1, 1024 };
FILE *wf = fopen("out.bin", "wb");
fwrite(&out_hdr, sizeof(FileHeader), 1, wf);
fclose(wf);
  • א. הנתונים תמיד נשמרים אל הדיסק גם בלי fclose, אף שהקריאה ל-fclose היא זו שמציפה את ה-Buffer ושומרת בפועל
  • ב. ייתכן שהנתונים לא יישמרו אל הדיסק, שכן ה-Buffer של הקובץ לא הוצף (Flushed) בהיעדר קריאה ל-fclose כאן
  • ג. הקובץ עצמו נמחק מן הדיסק, אף שהיעדר fclose גורם לכל היותר לאי-שמירת נתונים ולא למחיקת הקובץ כולו כאן כן
  • ד. תתקבל שגיאת Runtime מיידית, אף שהיעדר fclose אינו גורם לשגיאה אלא לכל היותר לאי-שמירת נתונים אל הדיסק כאן
תשובה: ב

✓ תשובה: ב) — fclose() עושה Flush לBuffer ומשחרר FD. ללא fclose: הנתונים שב-Buffer הפנימי עשויים לא להגיע לדיסק. גם: FD Leak — Process מוגבל במספר FDs פתוחים (ברירת מחדל 1024).

שאלה 2.1 — fread מחזיר · int buf[100]; size_t n = fread(buf, sizeof(int), 100, f);הקובץ מכיל רק 37 ints. מה ערך n?

int buf[100];
size_t n = fread(buf, sizeof(int), 100, f);
  • א. 148: הנחה שגויה ש-fread מחזירה את מספר הבתים (37 כפול 4), אף שהיא מחזירה את מספר האלמנטים ולא הבתים כאן
  • ב. 100: הנחה שגויה ש-fread מחזירה תמיד את המספר שביקשנו, אף שהיא מחזירה את מספר האלמנטים שנקראו בפועל כאן
  • ג. 1- (שגיאה): הנחה שגויה ש-fread מחזירה שגיאה, אף שקריאה חלקית מוצלחת מחזירה את מספר האלמנטים שנקראו בפועל
  • ד. 37: הפונקציה fread מחזירה את מספר האלמנטים שנקראו בפועל, ומכיוון שבקובץ יש 37 שלמים היא מחזירה 37 כאן
תשובה: ד

✓ תשובה: ד) 37 — fread מחזיר מספר Elements (לא bytes!). קשנו לקרוא 100 ints אבל יש רק 37 → מחזיר 37. לבדוק EOF: if (n < 100) — הגענו לסוף הקובץ.

שאלה 2.2 — גודל קובץ · מהו הPattern הנכון לחישוב גודל קובץ דרך FILE*?

  • א. sizeof(file): הנחה שגויה, אף ש-sizeof מחזירה את גודל הטיפוס FILE ולא את גודלו של הקובץ עצמו שעל הדיסק כאן
  • ב. fseek(f,0,SEEK_END); long sz = ftell(f); fseek(f,0,SEEK_SET): קפיצה לסוף, קריאת המיקום, וחזרה להתחלה כאן
  • ג. fread(NULL, 1, INT_MAX, f): הנחה שגויה, אף שקריאה אל מצביע NULL אינה חוקית ואינה מחשבת כלל את גודל הקובץ כאן
  • ד. rewind(f); ftell(f): הנחה שגויה, אף ש-rewind מחזירה להתחלה ולכן ftell לאחריה תחזיר תמיד 0 ולא את גודל הקובץ
תשובה: ב

✓ תשובה: ב) — הPattern הקלאסי: (1) fseek ל-SEEK_END, (2) ftell מחזיר offset = גודל הקובץ, (3) fseek חזרה ל-SEEK_SET לפני קריאה. rewind = fseek(f,0,SEEK_SET).

שאלה 2.3 — Dry Run fread · FILE *f = fopen("test.txt", "w"); fprintf(f, "Hello\nWorld\n"); fclose(f); f = fopen("test.txt", "r"); char buf[10]; fread(buf, 1, 5, f); printf("%s\n", buf);מה הבעיה בקוד?

FILE *f = fopen("test.txt", "w");
fprintf(f, "Hello\nWorld\n");
fclose(f);
f = fopen("test.txt", "r");
char buf[10];
fread(buf, 1, 5, f);
printf("%s\n", buf);
  • א. הפונקציה fread אינה יכולה לקרוא פחות מ-10 בתים, אף שהיא יכולה לקרוא כל מספר בתים שנבקש ממנה בקריאה כאן
  • ב. הקובץ אינו קיים, אף שהקוד יצר וכתב אליו קודם לכן, ולכן הקובץ קיים והבעיה היא בהיעדר תו הסיום בתוך buf כאן
  • ג. אין כאן שום בעיה, אף שהיעדר תו הסיום NULL אחרי fread הופך את הדפסת buf ל-Undefined Behavior מסוכן כאן כן
  • ד. המשתנה buf אינו מסתיים בתו NULL לאחר fread, ולכן הדפסתו עם %s היא Undefined Behavior ועלולה לקרוס כאן
תשובה: ד

✓ תשובה: ד) — fread קרא 5 bytes ('H','e','l','l','o'). buf[5] ואילך = ערכי זבל. printf("%s") ידפיס עד '\0' הראשון — UB! תיקון: buf[5] = '\0'; לפני printf.

שאלה 2.4 — fseek SEEK_CUR · לאחר קריאת header של 16 bytes, רוצים לדלג עוד 8 bytes קדימה ולקרוא מהמיקום הזה. מה הקריאה הנכונה?

// FD 0 = stdin  (Standard Input)
// FD 1 = stdout (Standard Output)
// FD 2 = stderr (Standard Error)
// כל fopen יוצר FD חדש (3, 4, 5, ...)

FILE *f = fopen("data.txt", "r");
int fd = fileno(f);   // fd = 3 (בדרך כלל)

// write ישירות ל-stdout = FD 1:
write(1, "Direct to stdout\n", 17);
  • א. fseek(f, 8, SEEK_SET): הנחה שגויה, אף שהיא קופצת אל הבית ה-8 מתחילת הקובץ ולא 8 בתים קדימה מן המיקום כאן
  • ב. fseek(f, 8, SEEK_END): הנחה שגויה, אף שהיא ממקמת 8 בתים אחרי סוף הקובץ ולא קדימה מן המיקום הנוכחי בקובץ
  • ג. fread(NULL, 1, 8, f): הנחה שגויה, אף שקריאה אל מצביע NULL אינה חוקית ואינה הדרך הנכונה לדלג על בתים בקובץ
  • ד. fseek(f, 8, SEEK_CUR): מדלגת 8 בתים קדימה מן המיקום הנוכחי, וזוהי בדיוק הפעולה שהתבקשה בשאלה הזו כאן
תשובה: ד

✓ תשובה: ד) fseek(f, 8, SEEK_CUR) — SEEK_CUR = מהמיקום הנוכחי (16, אחרי קריאת ה-header) קדימה 8 → מיקום 24. א) SEEK_SET,8 = מיקום מוחלט 8 — בתוך ה-header! ג) SEEK_END = מסוף הקובץ. רק SEEK_CUR מבצע דילוג יחסי מהמיקום הנוכחי.

שאלה 3.1 — FDs מוגדרים מראש · מה מספר ה-FD של stderr?

  • א. 0: הנחה שגויה, אף שהמספר 0 שמור לזרם הקלט הסטנדרטי stdin ולא לזרם השגיאות stderr במערכת ההפעלה כאן כן
  • ב. 2: זרם השגיאות הסטנדרטי stderr מקבל תמיד את מספר ה-File Descriptor 2 במערכת ההפעלה לפי המוסכמה כאן
  • ג. 1: הנחה שגויה, אף שהמספר 1 שמור לזרם הפלט הסטנדרטי stdout ולא לזרם השגיאות stderr במערכת ההפעלה כאן כן
  • ד. 3: הנחה שגויה, אף שהמספר 3 הוא ה-FD הפנוי הראשון עבור קבצים חדשים ולא זרם השגיאות stderr הסטנדרטי כאן
תשובה: ב

✓ תשובה: ב) 2 — FD 0=stdin, FD 1=stdout, FD 2=stderr. שלושה אלה תמיד פתוחים בכניסה ל-main(). FD 3+ = קבצים שפותחים בזמן ריצה.

שאלה 3.2 — open flags · איזה flag נדרש כדי לפתוח קובץ לכתיבה ולוודא שהוא ייווצר אם לא קיים?

  • א. O_WRONLY | O_CREAT (עם mode): פותח לכתיבה בלבד ויוצר את הקובץ אם אינו קיים, בדיוק כפי שהתבקש בשאלה כאן
  • ב. O_RDONLY: הנחה שגויה, אף שהוא פותח לקריאה בלבד ואינו מאפשר כלל כתיבה ואף אינו יוצר קובץ חדש אם אינו קיים
  • ג. O_WRONLY בלבד: הנחה שגויה, אף שהוא פותח לכתיבה אך אינו יוצר את הקובץ אם אינו קיים ולכן ייכשל במקרה כזה כאן
  • ד. O_CREAT בלבד: הנחה שגויה, אף שהוא יוצר קובץ אך אינו מציין הרשאת כתיבה ולכן אינו מספיק בפני עצמו לכתיבה כאן
תשובה: א

✓ תשובה: א) O_WRONLY | O_CREAT — O_WRONLY לכתיבה, O_CREAT ליצירה אם לא קיים (דורש argument שלישי — mode כמו 0644). O_WRONLY לבד ייכשל אם הקובץ לא קיים.

שאלה 3.3 — stdio vs syscall · מדוע לפעמים עדיף להשתמש ב-write() ישירות במקום fprintf() בקוד Exploit?

#include <sys/mman.h>
#include <sys/stat.h>

// void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// מחזיר: כתובת הזיכרון הממופה, MAP_FAILED בכישלון

// ─── mmap קובץ לקריאה ────────────────────────────────
int fd = open("data.bin", O_RDONLY);
struct stat st;
fstat(fd, &st);
size_t size = st.st_size;

char *data = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED) { perror("mmap"); close(fd); return -1; }

// עכשיו data הוא Pointer לכל הקובץ בזיכרון!
printf("First byte: 0x%02X\n", (unsigned char)data[0]);

// גישה כ-Struct ישירות!
FileHeader *hdr = (FileHeader *)data;
printf("Magic: 0x%08X\n", hdr->magic);

// ─── ניקוי ───────────────────────────────────────────
munmap(data, size);
close(fd);

// ─── mmap לזיכרון אנונימי ────────────────────────────
void *anon = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
munmap(anon, 4096);
  • א. write() הוא פשוט המהיר יותר לכתיבת טקסט, אף שהיתרון האמיתי שלו הוא בשליחה מיידית ללא Buffering של ספריית libc
  • ב. fprintf() כלל אינו עובד עם File Descriptors, אף שהוא עובד עם FILE* וההבדל האמיתי הוא ב-Buffering ובתלות ב-libc
  • ג. אין בין השניים כל הבדל, אף ש-write() שולח מיידית ללא Buffering בעוד fprintf() משתמש ב-Buffer של ספריית libc
  • ד. write() שולח אל הקרנל מיידית ללא Buffering, ולכן הוא בטוח יותר בניצול חד-חוטי ואינו תלוי כלל בספריית libc
תשובה: ד

✓ תשובה: ד) — ב-Exploit Development: write() = System Call ישיר ללא תלות ב-libc internals. לא צריך לדאוג לBuff state. גם: ב-Shellcode, קוראים ל-syscall ישירות (syscall(1, ...) ב-x64 = write).

שאלה 4.1 — mmap vs fread · מהו היתרון המרכזי של mmap לעומת fread לקריאת קובץ גדול?

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

✓ תשובה: ד) — עם mmap: גישה לoffset כלשהו בקובץ ישירות דרך pointer ארithmetic. הקרנל טוען רק את הדפים שנגשים אליהם (Demand Paging). fread מחייב buffer ו-IO explicit לכל קריאה.

שאלה 4.2 — PROT flags · מה זה PROT_READ | PROT_WRITE | PROT_EXEC בmmap?

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

✓ תשובה: ד) — PROT_RWX = Read+Write+Execute. כותבים Shellcode לזיכרון זה, ואז קופצים אליו — הוא ריץ כקוד! מנגנוני הגנה כמו NX/DEP מונעים PROT_EXEC על ה-Heap/Stack. mmap RWX עוקף זאת.

שאלה 4.3 — /proc/self/maps · ב-CTF, מה ניתן להשיג מקריאת /proc/self/maps?

// ─── קוד פגיע ─────────────────────────────────────────
void read_file(char *filename) {
    char path[256];
    snprintf(path, sizeof(path), "/safe/dir/%s", filename);
    FILE *f = fopen(path, "r");
}
// תוקף שולח: filename = "../../etc/passwd"
// path = "/safe/dir/../../etc/passwd" = "/etc/passwd"!

// ─── הגנה: bדיקת Path ──────────────────────────────
char resolved[PATH_MAX];
realpath(path, resolved);  // מנרמל ומחסל ../
if (strncmp(resolved, "/safe/dir/", 10) != 0) {
    // Path traversal detected!
    return;
}
  • א. כתובות הבסיס של כל ה-Segments של התהליך, מה שמאפשר לעקוף את מנגנון ה-ASLR בזמן ריצה בקלות רבה מאוד כאן
  • ב. רשימה של כל הקבצים שקיימים על הדיסק, אף שהקובץ maps מציג את מפת הזיכרון של התהליך ולא את קבצי הדיסק כאן
  • ג. מידע על המעבדים (CPUs) שבמערכת, אף שהקובץ maps מציג את מפת הזיכרון של התהליך ולא נתונים כלשהם על המעבדים
  • ד. רשימה של כל קריאות המערכת (system calls), אף שהקובץ maps מציג את מפת הזיכרון ולא את קריאות המערכת במערכת
תשובה: א

✓ תשובה: א) — /proc/self/maps מראה כל memory mapping: כתובות start-end, הרשאות (r-xp), שם הקובץ. קריאתו חושפת base address של libc, stack, heap — ומאפשר לחשב כתובות גם עם ASLR מופעל!

שאלה 5.1 — Path Traversal · תוכנה בונה path: "/uploads/" + user_input. אם תוקף שולח ../../etc/shadow, מה path הסופי?

  • א. etc/shadow: רצף ה-.. מטפס מעלה בעץ התיקיות עד אל השורש, ומעניק לתוקף גישה מלאה לקובץ הסיסמאות במערכת
  • ב. uploads/../../etc/shadow (נחסם): הנחה שגויה שהנתיב נחסם, אף שהמערכת פותרת את רצף ה-.. ומגיעה אל etc/shadow
  • ג. תתקבל שגיאת segfault, אף שמדובר בנתיב תקין לחלוטין שהמערכת פותרת ומגיעה דרכו אל קובץ הסיסמאות שבמערכת כאן
  • ד. uploads (חוזר לתיקיית בסיס): הנחה שגויה, אף שרצף ה-.. מטפס מעל תיקיית הבסיס ומגיע עד אל שורש הקבצים שבמערכת
תשובה: א

✓ תשובה: א) /etc/shadow — /uploads/../../etc/shadow = /uploads → / → /etc/shadow. כל ../ עולה תיקייה אחת. הגנה: realpath() → בדיקת prefix, או הסרת ../ מה-input.

שאלה 5.2 — TOCTOU · מדוע access()+fopen() מסוכן? מה הבעיה בסדר הפעולות?

  • א. בין access() ל-fopen() תוקף יכול להחליף את הקובץ בקובץ אחר (Race Condition), וזוהי חולשת TOCTOU קלאסית כאן
  • ב. הפונקציה access() כלל אינה עובדת בסביבת לינוקס, אף שהיא עובדת היטב והבעיה האמיתית היא חלון ה-Race שביניהן כאן
  • ג. הפונקציה fopen() היא פשוט מהירה מדי לביצוע, אף שהבעיה האמיתית היא חלון הזמן שנפתח בין הבדיקה לשימוש בקובץ כאן
  • ד. אין כאן שום בעיה וזהו קוד תקין לחלוטין, אף שחלון הזמן בין הבדיקה לשימוש מהווה חולשת TOCTOU מסוכנת וידועה מאוד
תשובה: א

✓ תשובה: א) — TOCTOU = Time Of Check To Time Of Use. access() בודק הרשאות, אבל עד fopen() — חלון של מאות microseconds. תוקף עם thread/process יכול להחליף symlink. פתרון: open() ישיר + fstat() על ה-FD.

שאלה 5.3 — /proc CTF · ב-CTF נדרשים למצוא כתובת base address של libc בזמן ריצה (ASLR מופעל). מה הדרך הקלה ביותר?

; ─── fopen + fread ב-Assembly (cdecl 32bit) ──────────────
; C: FILE *f = fopen("secret.txt", "r");
    push  offset aR            ; push "r"
    push  offset aSecretTxt    ; push "secret.txt"
    call  fopen
    add   esp, 8
    mov   [ebp-4], eax         ; [ebp-4] = FILE* f
    test  eax, eax
    jz    .error               ; NULL = שגיאה

; C: fread(buf, 1, 256, f);
    push  [ebp-4]              ; push f
    push  256                  ; push count
    push  1                    ; push size
    lea   eax, [ebp-260]       ; eax = buf (256 bytes ב-Stack)
    push  eax
    call  fread
    add   esp, 16

; ─── סימנים לזיהוי ב-IDA/Ghidra ───────────────────────
; CALL fopen → חפש String Literal לפני (שם קובץ + mode)
; CALL fread → 4 ארגומנטים: buf, size, count, FILE*
; CALL open  → 2-3 ארגומנטים: path, flags [, mode]
; TEST EAX, EAX + JZ אחרי fopen/open = NULL check
  • א. להשתמש ב-sizeof(libc): הנחה שגויה, אף ש-sizeof מחזירה גודל של טיפוס בזמן קומפילציה ולא כתובת בזמן ריצה כאן כן
  • ב. לקרוא את הקובץ etc/shadow: הנחה שגויה, אף שהוא מכיל את סיסמאות המשתמשים ולא את כתובת הבסיס של libc כלל כאן
  • ג. לקרוא את proc/self/maps ולחפש בו שורה שמכילה את המחרוזת "libc", ושם מופיעה כתובת הבסיס של הספרייה כאן
  • ד. אין שום דרך שכן ASLR מונע זאת לחלוטין, אף שקריאת maps חושפת בקלות את כתובת הבסיס של libc בזמן ריצה בפועל כאן
תשובה: ג

✓ תשובה: ג) — /proc/self/maps מראה שורות כמו: 7f1234000000-7f1234100000 r-xp ... /lib/x86_64-linux-gnu/libc.so.6. הכתובת הראשונה = libc base. מכאן מחשבים offsets לfunctions כמו system().

שאלה 6.1 — Dry Run מלא · FILE *f = fopen("test.txt", "w"); fprintf(f, "Hello\nWorld\n"); fclose(f); f = fopen("test.txt", "r"); char line[20]; fgets(line, sizeof(line), f); printf("%zu\n", strlen(line)); fclose(f);מה יודפס?

FILE *f = fopen("test.txt", "w");
fprintf(f, "Hello\nWorld\n");
fclose(f);
f = fopen("test.txt", "r");
char line[20];
fgets(line, sizeof(line), f);
printf("%zu\n", strlen(line));
fclose(f);
  • א. 6: הפונקציה fgets קוראת את השורה כולל תו סוף השורה \n, ולכן "Hello\n" הוא באורך 6 תווים שאותם סופר strlen
  • ב. 5: הנחה שגויה ש-fgets מסירה את תו סוף השורה \n, אף שהיא כן שומרת אותו ולכן האורך הוא 6 ולא 5 תווים כאן כן
  • ג. 11: הנחה שגויה שהפונקציה קוראת את שתי השורות יחד, אף ש-fgets עוצרת בתו סוף השורה הראשון שהיא פוגשת בקובץ
  • ד. 7: הנחה שגויה שהאורך הוא 7, אף שהשורה "Hello" בת 5 תווים יחד עם תו סוף השורה \n מסתכמת ב-6 תווים בלבד כאן
תשובה: א

✓ תשובה: א) 6 — fgets קוראת שורה ראשונה: 'H','e','l','l','o','\n' + '\0' = 7 תווים ב-buffer. strlen לא סופרת '\0'. H-e-l-l-o-\n = 6 תווים. strlen("Hello\n") = 6.

שאלה 6.2 — ניתוח Assembly · ב-Assembly רואים: push 0, push offset path, call open, test eax, eax, js error. מה flag 0 מייצג?

  • א. O_WRONLY: הנחה שגויה, אף שדגל הכתיבה O_WRONLY אינו הדגל שערכו 0, שהוא דווקא דגל הקריאה בלבד O_RDONLY כאן
  • ב. O_RDWR: הנחה שגויה, אף שדגל הקריאה-כתיבה O_RDWR אינו הדגל שערכו 0, שהוא דווקא דגל הקריאה בלבד O_RDONLY כאן
  • ג. O_CREAT: הנחה שגויה, אף שדגל היצירה O_CREAT אינו הדגל שערכו 0, שהוא דווקא דגל הקריאה בלבד O_RDONLY במערכת
  • ד. O_RDONLY: הערך 0 מייצג פתיחה לקריאה בלבד, שכן זהו הדגל שערכו המספרי הוא 0 מבין דגלי הפתיחה של open כאן
תשובה: ד

✓ תשובה: ד) O_RDONLY = 0 — O_RDONLY=0, O_WRONLY=1, O_RDWR=2. push 0 → O_RDONLY. test eax, eax + js = בדיקת -1 (שגיאה). זיהוי: קריאת קובץ לבדה ← שלב ראשון ב-RE של binary parser.

שאלה 6.3 — גאמא סגנון · int fd = open("/proc/self/maps", O_RDONLY); char buf[4096]; ssize_t n = read(fd, buf, sizeof(buf) - 1); buf[n] = '\0'; // buf מכיל כעת... close(fd);מה מכיל buf לאחר הקוד?

int fd = open("/proc/self/maps", O_RDONLY);
char buf[4096];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
buf[n] = '\0';
// buf מכיל כעת...
close(fd);
  • א. רשימה של כל הקבצים שקיימים על הדיסק, אף שהקובץ maps מציג את מפת הזיכרון של התהליך ולא את קבצי הדיסק כאן כן
  • ב. מפת הזיכרון (memory map) של התהליך הנוכחי, הכוללת את כתובות כל ה-Segments יחד עם ההרשאות של כל אחד מהם
  • ג. תתקבל שגיאה בטענה ש-proc אינו ניתן לפתיחה, אף שהקובץ proc/self/maps נפתח ונקרא בהצלחה כקובץ וירטואלי רגיל כאן
  • ד. קוד המקור של התוכנית עצמה, אף שהקובץ maps מציג את מפת הזיכרון בזמן ריצה ולא את קוד המקור שממנו הודרה כאן
תשובה: ב

✓ תשובה: ב) — /proc/self/maps הוא Virtual File שמייצג מצב הזיכרון. read() ממנו מחזיר שורות כמו: 7fff...000 r-xp ... /bin/prog. CTF קלאסי: קרא maps, מצא libc base, חשב system() address.