File I/O ב-C
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
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) | ✗ | ✗ | ✗ |
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/שגיאה — זו הבדיקה הנכונהint buf[100];
size_t n = fread(buf, sizeof(int), 100, f);הקובץ מכיל רק 37 ints. מה ערך n?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);מה הבעיה בקוד?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() | זהה בהתנהגות |
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);◄ /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!
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 ל-fstatFile 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 checkFILE *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);מה יודפס?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 לאחר הקוד?אתגר מעשי — קבצים — אבטחה ו-fseek
שיא השיעור: תכנות מערכת ברמה נמוכה — זיכרון, קבצים, תהליכים, מקבילות. ההגשה נבדקת אוטומטית.
סיכום שיעור 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: הבינארי עצמו
שאלות תרגול ותשובות (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.