GammaCyber

Strings ב-C

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

// סקירה

Strings ב-C — NULL-Terminator, פונקציות ו-Vulnerabilities

ב-C אין טיפוס String מובנה. מחרוזת היא מערך של char שמסתיים ב-\0. נראה פשוט — אבל כל פונקציית String קלאסית היא מקור פוטנציאלי ל-Buffer Overflow. Morris Worm 1988, Code Red, Heartbleed — כולם נבעו משגיאות String Handling.

✓נושאי השיעור
  • מחרוזת ב-C = מערך char עם NULL-Terminator (\0). sizeof vs strlen
  • char arr[] — ניתן לשינוי. char *ptr = "..." — Read-Only!
  • strlen, strcpy, strncpy, strcat, strncat
  • strcmp, strncmp — מחזירות 0 כשהמחרוזות שוות (לא TRUE!)
  • sprintf מסוכן, snprintf בטוח
  • gets — בוטל ב-C11. תמיד fgets
  • Buffer Overflow דרך String Functions — מנגנון מלא
  • Format String Vulnerability — printf(user_input)
  • פונקציות String ב-Assembly — CALL strcpy, LEA לstring
🎯 איך זה מופיע במיוני גאמא
שאלון C — strlen vs sizeof, strcmp return value, strcpy מסוכן. Black Box CTF — Buffer Overflow דרך gets/strcpy. RE — זיהוי String Operations ב-Assembly (CALL strcpy, LEA ל-rodata).
// פרק 1

מה זה String ב-C

מחרוזת ב-C היא מערך של char שמסתיים בתו NULL ('\0' = ASCII 0). כל פונקציית String קוראת תווים עד שפוגשת את ה-\0 — זה הדרך היחידה שהיא יודעת היכן המחרוזת מסתיימת.

// אתחול String char s1[] = "Hello"; // בזיכרון: ['H','e','l','l','o','\0'] — 6 bytes (5 + NULL!) char s2[10] = "Hi"; // בזיכרון: ['H','i','\0',0,0,0,0,0,0,0] — 10 bytes char *s3 = "World"; // s3 = Pointer ל-String Literal בזיכרון READ-ONLY // ⚠️ s3[0] = 'X' → Undefined Behavior (Segfault!) // ─── הבדל קריטי: מערך לעומת Pointer ─── char arr[] = "Hello"; // arr = מערך Stack — ניתן לשינוי char *ptr = "Hello"; // ptr = Pointer לLiteral — לא לשינוי! arr[0] = 'J'; // OK — arr ב-Stack // ptr[0] = 'J'; // 💥 Segfault! // ─── sizeof vs strlen ─── // sizeof("Hello") = 6 ← כולל \0 // strlen("Hello") = 5 ← ללא \0 — לעולם לא ה-sizeof!
! Off-by-One — שכחנו מקום ל-\0 ›

טעות נפוצה: char *copy = malloc(strlen(s)); — חסר byte ל-\0!

תיקון: char *copy = malloc(strlen(s) + 1); — תמיד strlen + 1!

כלל ברזל: כל הקצאת buffer לString = strlen(s) + 1 (או sizeof(arr) כשיש מערך ידוע).
הרחבה String ב-Assembly: LEA ו-.rodata ›
; C: char *msg = "Hello"; puts(msg); ; Assembly (32bit): .rodata: msg: db 'H','e','l','l','o',0 ; בתים: 48 65 6C 6C 6F 00 ; שימוש ב-main: lea eax, [msg] ; eax = כתובת ה-String (לא הערך!) push eax ; ארגומנט ל-puts call puts ; puts קורא עד \0 ; ← LEA + CALL puts/printf = הדפסת מחרוזת ; ← IDA מזהה String Literals: .rodata:0x403000 aHello db 'Hello',0
✏תרגול עצמי3 שאלות
שאלה 1.1
מה מחזיר sizeof("Hello") לעומת strlen("Hello")?
✓ תשובה: ב) — sizeof("Hello") = גודל המערך בזיכרון = 6 (5 תווים + \0). strlen("Hello") = מספר התווים לפני \0 = 5. הבדל קריטי: malloc(strlen(s)) = חסר byte ל-\0! חייבים malloc(strlen(s)+1).
שאלה 1.2
מה קורה כשמנסים לשנות char *p = "Hello"; p[0] = 'J';?
✓ תשובה: א) — char *p = "Hello" = p מצביע על String Literal שנמצא ב-.rodata (Read-Only Data section). ניסיון לכתוב = Segfault. כדי לכתוב: char arr[] = "Hello" = מועתק ל-Stack ואפשר לשנות.
שאלה 1.3 — Dry Run
char s[] = "GAMMA"; printf("%d %d", (int)strlen(s), (int)sizeof(s));מה הפלט?
✓ תשובה: ב) 5 6 — strlen("GAMMA") = 5 (G,A,M,M,A — ללא \0). sizeof(s) = 6 (5 תווים + \0). s הוא char[] — sizeof על מערך = גודל המערך הכולל.
// פרק 2

פונקציות String — <string.h>

כל פונקציות String נמצאות ב-<string.h>. חייבים לדעת: מה כל אחת עושה, מה היא מחזירה, ומה הסכנה שלה.

strlen — אורך מחרוזת

// size_t strlen(const char *s) — מחזיר אורך ללא \0 char s[] = "Hello"; size_t len = strlen(s); // len = 5 // strlen עובד כך (בפנים): // n=0; while (*s != '\0') { n++; s++; }; return n; // ← לכן: מחרוזת ללא \0 → strlen רץ לאינסוף! // טעות נפוצה: char *copy = malloc(strlen(s)); // ❌ חסר byte ל-\0! char *copy2 = malloc(strlen(s) + 1); // ✓ נכון!

strcpy ו-strncpy — העתקה

// char *strcpy(char *dst, const char *src) // מעתיקה src ל-dst כולל \0. לא בודקת גודל! char dst[10]; strcpy(dst, "Hello"); // OK — 6 bytes < 10 strcpy(dst, "Hello World!!!!"); // 💥 Buffer Overflow! // strncpy — גרסה עם הגבלה // char *strncpy(char *dst, const char *src, size_t n) // מעתיקה עד n תווים. לא מוסיפה \0 אם src ארוך מ-n! char dst2[10]; strncpy(dst2, "Hello World", 9); // מעתיק 9 תווים dst2[9] = '\0'; // ← חובה להוסיף \0 ידנית! // הדרך הבטוחה ביותר: snprintf(dst2, sizeof(dst2), "%s", src); // תמיד בטוח + \0

strcat ו-strncat — שרשור

// char *strcat(char *dst, const char *src) // מוצאת \0 של dst, מעתיקה שם את src. לא בודקת גדלים! char buf[20] = "Hello"; strcat(buf, " World"); // buf = "Hello World" — OK, 12 < 20 // strcat(buf, "!!!!!!!!!!!"); // 💥 Overflow! // strncat — עם הגבלה (תמיד מוסיף \0!) strncat(buf, " Bye!!!", sizeof(buf) - strlen(buf) - 1);

strcmp ו-strncmp — השוואה

// int strcmp(const char *s1, const char *s2) // מחזיר: 0 אם שוות, <0 אם s1 < s2, >0 אם s1 > s2 strcmp("abc", "abc") == 0 // זהה strcmp("abc", "abd") < 0 // 'c'(99) < 'd'(100) → שלילי strcmp("abcd", "abc") > 0 // 'd' > '\0'(0) → חיובי // ⚠️ טעות קלאסית: if (strcmp(s1,s2)) — מתבצע כשהמחרוזות שונות! if (strcmp(s1, s2) == 0) { ... } // ✓ נכון: זוגיות if (!strcmp(s1, s2)) { ... } // ✓ גם נכון // strncmp — השוואה עד n תווים (לבדיקת prefix) strncmp("Hello World", "Hello", 5); // 0 — רק 5 תווים ראשונים
! strncpy לא מוסיפה NULL אוטומטית! ›

✗ שגוי: strncpy(buf, src, sizeof(buf)); — עלול לחסר \0 אם strlen(src) >= sizeof(buf)!

נכון: strncpy(buf, src, sizeof(buf)-1); buf[sizeof(buf)-1] = '\0';
או: snprintf(buf, sizeof(buf), "%s", src); — תמיד בטוח
✏תרגול עצמי4 שאלות
שאלה 2.1
מה מחזיר strcmp("abc", "abc")?
✓ תשובה: ב) 0 — strcmp מחזיר 0 כשהמחרוזות זהות. זה הפוך מ-intuition: 0 = False ב-C. לכן: if (strcmp(a,b)) = מתבצע כשהמחרוזות שונות! נכון לכתוב: if (strcmp(a,b) == 0) או if (!strcmp(a,b)).
שאלה 2.2
מה מחזיר strcmp("abd", "abc")?
✓ תשובה: ד) חיובי — strcmp משווה תו-אחר-תו: 'a'='a', 'b'='b', 'd'(100) > 'c'(99) → ערך חיובי (בדרך כלל 1, תלוי מימוש). כלל: s1 > s2 ב-lexicographic order → חיובי.
שאלה 2.3 — Dry Run
char s1[20] = "Hello"; char s2[] = "World"; strcat(s1, " "); strcat(s1, s2); printf("%d %s", (int)strlen(s1), s1);מה הפלט?
✓ תשובה: ב) 11 Hello World — s1="Hello" (5). strcat(s1," ") → s1="Hello " (6). strcat(s1,s2) → s1="Hello World" (11). strlen=11. 11 < 20 = בגדרי הbuffer. פלט: "11 Hello World".
שאלה 2.4 — CTF
ב-CTF מצאתם: if (strcmp(password, "secret")) { grant(); } else { deny(); }. מה קורה אם שולחים "secret"?
✓ תשובה: ד) — באג קלאסי ב-CTF! strcmp מחזיר 0 כשהמחרוזות שוות. 0 = False ב-C. if (strcmp(...)) = if (0) = False → הif לא מתבצע → else → deny(). הסיסמה הנכונה נדחית! וגרוע יותר: כל סיסמה שגויה (strcmp≠0) מזכה ב-grant(). הקוד הפוך — הנכון: if (strcmp(...)==0) { grant(); } else { deny(); }.
// פרק 3

פונקציות מסוכנות — gets, sprintf, strdup

gets בוטלה ב-C11 — היא הייתה כה מסוכנת שהוציאו אותה מהשפה. Morris Worm 1988 ניצל אותה. sprintf גם מסוכן ללא n-variant.

gets — הפונקציה שבוטלה לגמרי

// gets(buf) — קוראת שורה עד \n, ללא שום בדיקת גודל char buf[10]; gets(buf); // 💥 אם המשתמש מקיש יותר מ-9 תווים → Buffer Overflow! // ← gets לא בודקת כלום! Morris Worm 1988 ניצל בדיוק את זה // Morris Worm (1988): // Target: fingerd (Finger Daemon) ב-Unix // Exploit: שלח 536 bytes → גלש ל-Stack → שינה כתובת חזרה // זה היה ה-Internet Worm הראשון — הדביק ~6000 מחשבים! // ─── תחליף: fgets ─── char buf2[10]; fgets(buf2, sizeof(buf2), stdin); // קוראת עד 9 תווים + \0 // הסרת \n שfgets משאירה: buf2[strcspn(buf2, "\n")] = '\0';

sprintf ו-snprintf

// sprintf: כמו printf אבל כותב לbuffer — לא בודק גודל! char buf[50]; sprintf(buf, "User: %s, Age: %d", "Alice", 25); // OK // snprintf — הגרסה הבטוחה: char buf2[20]; int written = snprintf(buf2, sizeof(buf2), "Hello %s!", "World"); // written = 12, buf2 = "Hello World!" if (written >= (int)sizeof(buf2)) { /* נחתך! */ } // Format String Bug — אסור לעולם: char user_input[] = "%s%s%s%n"; sprintf(buf, user_input); // 💥 Format String Vulnerability! sprintf(buf, "%s", user_input); // ✓ בטוח

strdup — כפול עם Heap Allocation

// char *strdup(const char *s) — מקצה זיכרון ומעתיקה char *copy = strdup("Hello"); // שקול ל: malloc(strlen("Hello")+1) + strcpy printf("%s\n", copy); // Hello free(copy); // ← חייב לבצע free!
✏תרגול עצמי3 שאלות
שאלה 3.1
למה gets הוצאה מ-C11 לגמרי?
✓ תשובה: א) — gets לא קבלת n parameter — אין שום דרך לבדוק גודל buffer. משתמש מקיש יותר מהbuffer = Overflow מובטח. זה היה כה מסוכן שהוציאו את הפונקציה מהשפה לגמרי ב-C11. תחליף: fgets(buf, sizeof(buf), stdin).
שאלה 3.2
מה ההבדל בין sprintf(buf, fmt, ...) לבין snprintf(buf, n, fmt, ...)?
✓ תשובה: א) — sprintf: כותב כמה שצריך, ללא הגבלה. snprintf(buf, n, fmt): כותב לכל היותר n-1 תווים + תמיד \0. מחזיר גם את המספר שהיה כותב אם היה מקום (לבדיקת truncation). snprintf = הדרך הבטוחה.
שאלה 3.3 — Format String
מה הבעיה ב: char input[100]; fgets(input, 100, stdin); printf(input);?
✓ תשובה: ב) — printf(input) = input מגיעה ישירות כformat string. אם user שולח "%x %x %x" → printf מחפש ארגומנטים ב-Stack → מדפיס ערכים מהzיכרון. "%n" → כותב לזיכרון! תיקון: printf("%s", input) — format string קבוע.
// פרק 4

Buffer Overflow דרך String Functions

Buffer Overflow דרך String Functions הוא הדפוס הקלאסי ביותר ב-CTF ובעולם האמיתי. נלמד איך הוא עובד ברמת הזיכרון.

// קוד פגיע קלאסי void vulnerable(char *input) { char buf[64]; strcpy(buf, input); // ← 💥 אין בדיקת גודל printf("Got: %s\n", buf); } // מה קורה ב-Stack (מלמטה למעלה): // High | [כתובת חזרה = EIP שנשמר ע"י CALL] ← מטרה! // | [EBP שמור] // | [buf[63]] // | ... ← 64 bytes של buf // | [buf[1]] // Low | [buf[0]] ← strcpy מתחיל לכתוב כאן // // אם input ארוך מ-64: // → מחליפה EBP השמור (offset 64) ואז כתובת החזרה (offset 68)! // מה תוקף עושה: // input = [64 bytes 'A'] + [4 bytes EBP מזויף] + [כתובת shellcode] // כשvulnerable() מחזירה: RET קופץ לכתובת שהתוקף שלח!
🔑זיהוי Buffer Overflow ב-CTF
  • חפשו: gets, strcpy, strcat, sprintf ללא n-variant בקוד
  • ב-RE: CALL strcpy עם [ebp-N] כ-dst = buffer ב-Stack, N = גדלו
  • בדיקת Overflow: שלחו cyclic pattern (pwntools) → מצאו EIP overwrite
  • כלים: pwntools (cyclic), GDB+pwndbg, checksec לבדיקת ASLR/Canary/NX
✏תרגול עצמי3 שאלות
שאלה 4.1
מה מטרת תוקף ב-Buffer Overflow דרך strcpy?
✓ תשובה: ג) — מבנה Stack: buf (64B) → EBP שמור (4B) → כתובת חזרה (4B). כשstrcpy גולש: כותב על EBP, ואז על כתובת החזרה. כשהפונקציה מבצעת RET → קופץ לכתובת שהתוקף שתל → Remote Code Execution.
שאלה 4.2
ב-RE רואים: lea eax, [ebp-64] / push eax / call gets. מה זה אומר?
✓ תשובה: ב) — [ebp-64] = buffer ב-Stack, 64 bytes לפני EBP. CALL gets = קוראת עד \n ללא הגבלה. כדי להגיע ל-EIP: 64 bytes buf + 4 bytes EBP שמור = 68 bytes padding, אז 4 bytes כתובת יעד. Exploit מיידי!
שאלה 4.3 — CTF זיהוי
מהי הפונקציה הבטוחה ביותר לקריאת input מהמשתמש ל-buffer?
✓ תשובה: א) — fgets(buf, n, stdin): קוראת עד n-1 תווים + מוסיפה \0. הבטוחה ביותר. gets = אסורה. scanf("%s") = עצרת ב-whitespace אבל ללא הגבלת גודל! strcpy = לא קורא input. אחרי fgets: buf[strcspn(buf,"\n")] = '\0' להסרת \n.
// פרק 5

פונקציות String ב-Assembly

כשרואים Assembly — CALL strcpy, CALL strlen, LEA ל-.rodata הם הסימנים לזיהוי String Operations. הכרתם חיונית ל-Reverse Engineering ולמציאת Vulnerabilities.

קוד CAssembly (32bit cdecl)הערות לRE
strlen(s)push s_ptr; call strlen; add esp,4EAX = אורך, ללא \0
strcpy(dst,src)push src; push dst; call strcpy; add esp,8dst ראשון! סדר הפוך בStack
strcmp(s1,s2)push s2; push s1; call strcmp; test eax,eaxEAX=0 אם שווה
printf("%s",s)push s; push fmt_ptr; call printffmt קרוב ל-.rodata
gets(buf)push buf; call gets; add esp,4⚠️ Buffer ב-Stack! Overflow מיד
; RE של strcmp לסיסמה: ; C: if (strcmp(password, "secret") == 0) { grant_access(); } lea eax, [ebp-32] ; eax = password (buffer ב-Stack) push offset aSecret ; push "secret" (String Literal) push eax ; push password call strcmp add esp, 8 test eax, eax ; strcmp == 0? jnz .deny ; אם לא → deny call grant_access ; ← הגישה ניתנת! ; ← זיהוי: CALL strcmp + TEST EAX,EAX + JZ/JNZ = password check ב-RE
✏תרגול עצמי2 שאלות
שאלה 5.1 — RE
ב-Assembly רואים: push offset aSecret lea eax, [ebp-32] push eax call strcmp test eax, eax jnz denyמה C code המקביל?
✓ תשובה: ד) — push aSecret (src), push eax=[ebp-32] (password/dst) → CALL strcmp(password, "secret"). TEST EAX,EAX → JNZ deny = אם EAX!=0 (לא שווה) → deny. כלומר: אם סיסמה שגויה → deny. כדי לקבל גישה: strcmp מחזיר 0 → JNZ לא קופץ → ממשיך.
שאלה 5.2 — CTF RE
ב-IDA רואים: CALL gets לאחר LEA EAX, [EBP-0x40]. מה offset ה-EIP overwrite?
✓ תשובה: ד) 68 bytes — [EBP-0x40] = buffer מתחיל 64 bytes לפני EBP. Stack: [buf 64B][EBP שמור 4B][EIP = כתובת חזרה 4B]. כדי להגיע ל-EIP: 64 bytes buf + 4 bytes EBP = 68 bytes padding, אז 4 bytes כתובת יעד. Classic 32bit overflow offset.
// אתגר מעשי

אתגר מעשי — מחרוזות C — בטיחות ופונקציות מסוכנות

שיא השיעור: היפוך, ניתוח וזיהוי — כלי ה-RE וה-Low-Level בפעולה. ההגשה נבדקת אוטומטית.

T2 · בסיסי200 נק'~15 דק'build
העתקת מחרוזת בטוחה ב-C
ממש safe_copy(dst, src, size) שמעתיק לכל היותר size-1 תווים ותמיד מסיים ב-'\0' (בניגוד ל-strcpy המסוכן). בתוכנית: קראו מילה, העתיקו בבטחה ל-buf בגודל 8, והדפיסו את התוצאה. קלט ארוך ייחתך בבטחה.
דוגמאות
SuperSecret → SuperSe (7 תווים, buf=8)
abc → abc
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס העתקת מחרוזת בטוחה ב-C ›
#include <stdio.h> #include <string.h> void safe_copy(char *dst, const char *src, size_t size){ if(size==0) return; size_t i=0; for(; i<size-1 && src[i]; i++) dst[i]=src[i]; // לכל היותר size-1 תווים dst[i]='\0'; // תמיד מסיים ב-NUL } int main(){ char in[256], out[8]; if(scanf("%255s",in)!=1) return 1; safe_copy(out,in,sizeof(out)); printf("%s\n",out); return 0; }
T3 · בינוני350 נק'~20 דק'algorithmic
סורק פונקציות C מסוכנות
ממש find_unsafe(lines) המחזיר (מספר_שורה, שם_פונקציה) לכל שימוש בפונקציית C מסוכנת: gets, strcpy, strcat, sprintf. זהה קריאת-פונקציה ממש (שם ואז '(') — אל תסמן זהות חלקית כמו my_gets_wrapper. מספור מ-1.
דוגמאות
find_unsafe(['gets(buf);']) → [(1, 'gets')]
find_unsafe(['x=1','strcpy(a,b);']) → [(2, 'strcpy')]
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס סורק פונקציות C מסוכנות ›
import re def find_unsafe(lines): danger = ('gets', 'strcpy', 'strcat', 'sprintf') pat = re.compile(r'\b(' + '|'.join(danger) + r')\s*\(') # קריאת-פונקציה ממש out = [] for i, ln in enumerate(lines, 1): m = pat.search(ln) if m: out.append((i, m.group(1))) return out
שאלות תרגול ותשובות (15)

שאלה 1.1 · מה מחזיר sizeof("Hello") לעומת strlen("Hello")?

  • א. שניהם מחזירים את הערך 5, אף ש-sizeof סופר גם את תו הסיום \0 ולכן מחזיר 6 ולא 5 כמו הפונקציה strlen כאן
  • ב. sizeof שווה ל-6 (כולל את תו הסיום \0), ואילו strlen שווה ל-5 (ללא ספירת תו הסיום) בדיוק בשני המקרים
  • ג. הפונקציה sizeof מחזירה 5 ו-strlen מחזירה 6, שהוא בדיוק ההפך מן התוצאה הנכונה של שתי הפונקציות האלו כאן
  • ד. שניהם מחזירים את הערך 6, אף ש-strlen אינה סופרת את תו הסיום \0 ולכן מחזירה 5 ולא 6 כמו הפונקציה sizeof
תשובה: ב

✓ תשובה: ב) — sizeof("Hello") = גודל המערך בזיכרון = 6 (5 תווים + \0). strlen("Hello") = מספר התווים לפני \0 = 5. הבדל קריטי: malloc(strlen(s)) = חסר byte ל-\0! חייבים malloc(strlen(s)+1).

שאלה 1.2 · מה קורה כשמנסים לשנות char *p = "Hello"; p[0] = 'J';?

  • א. Segfault: מחרוזת מסוג String Literal נשמרת בזיכרון לקריאה בלבד, ולכן ניסיון לשנותה גורם לקריסה מיידית כאן
  • ב. המצביע p הופך למחרוזת "Jello", אף שהמחרוזת שמורה בזיכרון לקריאה בלבד ולכן לא ניתן כלל לשנות את תוכנה כאן
  • ג. שום דבר אינו קורה והתוכנית ממשיכה כרגיל, אף שהשינוי של מחרוזת לקריאה בלבד גורם דווקא לקריסה של התוכנית כאן
  • ד. תתקבל שגיאת קומפילציה, אף שהקוד מתקמפל בהצלחה והבעיה מתגלה רק בזמן הריצה בעת ניסיון שינוי המחרוזת כאן
תשובה: א

✓ תשובה: א) — char *p = "Hello" = p מצביע על String Literal שנמצא ב-.rodata (Read-Only Data section). ניסיון לכתוב = Segfault. כדי לכתוב: char arr[] = "Hello" = מועתק ל-Stack ואפשר לשנות.

שאלה 1.3 — Dry Run · char s[] = "GAMMA"; printf("%d %d", (int)strlen(s), (int)sizeof(s));מה הפלט?

char s[] = "GAMMA";
printf("%d %d", (int)strlen(s), (int)sizeof(s));
  • א. 5 5: הנחה שגויה ששני הערכים זהים, אף ש-sizeof כוללת את תו הסיום \0 ולכן מחזירה 6 ולא 5 כמו הפונקציה strlen
  • ב. 5 6: הפונקציה strlen סופרת חמישה תווים, ואילו sizeof מחזירה שישה בתים כי היא כוללת גם את תו הסיום \0 כאן
  • ג. 6 6: הנחה שגויה ששני הערכים הם 6, אף ש-strlen אינה סופרת את תו הסיום \0 ולכן מחזירה 5 ולא 6 כמו sizeof כאן
  • ד. 5 4: הנחה שגויה ש-sizeof מחזירה 4, אף ש-sizeof סופרת את כל התווים כולל תו הסיום ולכן מחזירה 6 ולא 4 כאן כן
תשובה: ב

✓ תשובה: ב) 5 6 — strlen("GAMMA") = 5 (G,A,M,M,A — ללא \0). sizeof(s) = 6 (5 תווים + \0). s הוא char[] — sizeof על מערך = גודל המערך הכולל.

שאלה 2.1 · מה מחזיר strcmp("abc", "abc")?

  • א. 1 (TRUE): הנחה שגויה ש-strcmp מחזירה אמת בעת שוויון, אף שהיא מחזירה דווקא את הערך אפס כאשר המחרוזות זהות
  • ב. 0: המחרוזות זהות זו לזו, ולכן strcmp מחזירה אפס, ויש להיזהר כי הערך אפס נחשב כ-False בתוך תנאי if בקוד
  • ג. 1-: הנחה שגויה ש-strcmp מחזירה ערך שלילי בעת שוויון, אף שהיא מחזירה את הערך אפס בדיוק כאשר המחרוזות זהות
  • ד. 3 (אורך המחרוזת): הנחה שגויה ש-strcmp מחזירה את אורך המחרוזת, אף שהיא מחזירה את הערך אפס כאשר הן זהות כאן
תשובה: ב

✓ תשובה: ב) 0 — strcmp מחזיר 0 כשהמחרוזות זהות. זה הפוך מ-intuition: 0 = False ב-C. לכן: if (strcmp(a,b)) = מתבצע כשהמחרוזות שונות! נכון לכתוב: if (strcmp(a,b) == 0) או if (!strcmp(a,b)).

שאלה 2.2 · מה מחזיר strcmp("abd", "abc")?

  • א. 0: הנחה שגויה שהמחרוזות זהות, אף שהן נבדלות בתו האחרון (d מול c) ולכן strcmp אינה מחזירה אפס במקרה הזה כאן
  • ב. ערך שלילי: הנחה שגויה שהראשונה קטנה יותר, אף שהתו d גדול מן התו c ולכן התוצאה חיובית ולא שלילית במקרה הזה
  • ג. 1 בדיוק: הנחה שגויה שהתוצאה היא בדיוק 1, אף ש-strcmp מחזירה ערך חיובי כלשהו ולאו דווקא את הערך 1 המדויק כאן
  • ד. ערך חיובי: התו d גדול מן התו c בטבלת ה-ASCII, ולכן strcmp מחזירה ערך חיובי כאשר הראשונה גדולה מן השנייה
תשובה: ד

✓ תשובה: ד) חיובי — strcmp משווה תו-אחר-תו: 'a'='a', 'b'='b', 'd'(100) > 'c'(99) → ערך חיובי (בדרך כלל 1, תלוי מימוש). כלל: s1 > s2 ב-lexicographic order → חיובי.

שאלה 2.3 — Dry Run · char s1[20] = "Hello"; char s2[] = "World"; strcat(s1, " "); strcat(s1, s2); printf("%d %s", (int)strlen(s1), s1);מה הפלט?

char s1[20] = "Hello";
char s2[] = "World";
strcat(s1, " ");
strcat(s1, s2);
printf("%d %s", (int)strlen(s1), s1);
  • א. 10 Hello World: הנחה שגויה שהאורך הוא 10, אף שהרווח שבאמצע מוסיף תו נוסף ולכן האורך הכולל הוא 11 בדיוק כאן
  • ב. 11 Hello World: לאחר שרשור "Hello", רווח ו-"World" מתקבלת מחרוזת באורך 11 תווים (5+1+5) בדיוק כאן כן
  • ג. 12 Hello World: הנחה שגויה שהאורך הוא 12, אף שסך התווים של "Hello World" יחד עם הרווח שבאמצע הוא 11 בלבד
  • ד. 5 Hello: הנחה שגויה שהשרשור כלל לא בוצע ונשארה "Hello" בלבד, אף ששתי פעולות ה-strcat כן בוצעו על המחרוזת
תשובה: ב

✓ תשובה: ב) 11 Hello World — s1="Hello" (5). strcat(s1," ") → s1="Hello " (6). strcat(s1,s2) → s1="Hello World" (11). strlen=11. 11 < 20 = בגדרי הbuffer. פלט: "11 Hello World".

שאלה 2.4 — CTF · ב-CTF מצאתם: if (strcmp(password, "secret")) { grant(); } else { deny(); }. מה קורה אם שולחים "secret"?

if (strcmp(password, "secret")) { grant(); } else { deny(); }
  • א. grant(): הנחה שגויה שהסיסמה הנכונה תעניק גישה, אף ש-strcmp מחזירה אפס (False) בעת שוויון ולכן הבדיקה כושלת
  • ב. deny() בשל סיסמה שגויה: הנחה שגויה שהסיסמה שגויה, אף שהיא נכונה והבעיה היא בהיגיון הבדיקה של הפונקציה strcmp
  • ג. התוכנית תקרוס, אף שאין כאן כל סיבה לקריסה אלא רק באג לוגי שגורם ל-deny במקום ל-grant בסיסמה הנכונה בדיוק כן
  • ד. deny(): הקוד באגי, שכן strcmp מחזירה אפס בעת שוויון, ואפס נחשב False, ולכן דווקא ענף ה-else מתבצע ומדחה
תשובה: ד

✓ תשובה: ד) — באג קלאסי ב-CTF! strcmp מחזיר 0 כשהמחרוזות שוות. 0 = False ב-C. if (strcmp(...)) = if (0) = False → הif לא מתבצע → else → deny(). הסיסמה הנכונה נדחית! וגרוע יותר: כל סיסמה שגויה (strcmp≠0) מזכה ב-grant(). הקוד הפוך — הנכון: if (strcmp(...)==0) { grant(); } else { deny(); }.

שאלה 3.1 · למה gets הוצאה מ-C11 לגמרי?

  • א. היא קוראת עד לתו \n בלי כל בדיקת גודל, ולכן Buffer Overflow הוא בלתי נמנע; היא נוצלה בתולעת מוריס 1988
  • ב. מכיוון שהיא פשוט איטית מדי לביצוע, אף שהסיבה האמיתית להסרתה היא היעדר בדיקת הגודל וסכנת הגלישה שבה כאן
  • ג. מכיוון שהיא כלל אינה עובדת בסביבת 64 ביט, אף שהיא עובדת בכל הסביבות אך מסוכנת בשל היעדר בדיקת הגודל בה
  • ד. מכיוון שהפונקציה fgets פשוט יעילה יותר ממנה, אף שהסיבה להסרה היא סכנת האבטחה שבה ולא יעילות ביחס ל-fgets
תשובה: א

✓ תשובה: א) — gets לא קבלת n parameter — אין שום דרך לבדוק גודל buffer. משתמש מקיש יותר מהbuffer = Overflow מובטח. זה היה כה מסוכן שהוציאו את הפונקציה מהשפה לגמרי ב-C11. תחליף: fgets(buf, sizeof(buf), stdin).

שאלה 3.2 · מה ההבדל בין sprintf(buf, fmt, ...) לבין snprintf(buf, n, fmt, ...)?

  • א. sprintf אינה בודקת את גודל ה-buffer ולכן Overflow אפשרי, ואילו snprintf מגבילה ל-n-1 תווים ומוסיפה תמיד \0
  • ב. אין ביניהן כל הבדל, ושתי הפונקציות מגבילות את גודל הכתיבה באותו האופן ומגינות מפני גלישה בכל מקרה שהוא כאן
  • ג. פונקציית snprintf היא פשוט המהירה יותר מבין השתיים, אף שההבדל האמיתי ביניהן הוא בבדיקת גודל ה-buffer כאן כן
  • ד. פונקציית sprintf מיועדת למחרוזות ו-snprintf למספרים, אף שאין זו כלל ההבחנה הנכונה בין שתי הפונקציות הללו כאן
תשובה: א

✓ תשובה: א) — sprintf: כותב כמה שצריך, ללא הגבלה. snprintf(buf, n, fmt): כותב לכל היותר n-1 תווים + תמיד \0. מחזיר גם את המספר שהיה כותב אם היה מקום (לבדיקת truncation). snprintf = הדרך הבטוחה.

שאלה 3.3 — Format String · מה הבעיה ב: char input[100]; fgets(input, 100, stdin); printf(input);?

char input[100]; fgets(input, 100, stdin); printf(input);
  • א. Buffer Overflow: הנחה שגויה, אף שה-fgets כאן מגביל את הקריאה ל-100 תווים ולכן אין כלל גלישה מן ה-buffer כאן
  • ב. Format String Vulnerability: אם הקלט מכיל %x, %s או %n, הפונקציה printf תפרש אותם ותקרא או תכתוב אל הזיכרון
  • ג. Memory Leak: הנחה שגויה, אף שאין בקוד הזה כל הקצאת זיכרון עם malloc ולכן אין כלל אפשרות לדליפת זיכרון בקוד
  • ד. אין כאן שום בעיה, אף שהעברת קלט משתמש כ-Format String ישירות אל printf היא חולשת אבטחה חמורה וידועה מאוד
תשובה: ב

✓ תשובה: ב) — printf(input) = input מגיעה ישירות כformat string. אם user שולח "%x %x %x" → printf מחפש ארגומנטים ב-Stack → מדפיס ערכים מהzיכרון. "%n" → כותב לזיכרון! תיקון: printf("%s", input) — format string קבוע.

שאלה 4.1 · מה מטרת תוקף ב-Buffer Overflow דרך strcpy?

  • א. לגנוב את תוכן ה-buffer עצמו, אף שמטרת התוקף האמיתית היא דריסת כתובת החזרה ולא קריאת תוכן ה-buffer שבמחסנית
  • ב. לגרום לדליפת זיכרון (Memory Leak), אף שמתקפת הגלישה מכוונת לדריסת כתובת החזרה ולא לדליפת זיכרון כלל בכלל
  • ג. להחליף את כתובת החזרה (EIP/RIP) שעל ה-Stack כדי להפנות את המעבד אל הקוד של התוקף ולהשיג הרצת קוד (RCE)
  • ד. לגרום לשחרור כפול (Double Free), אף שזוהי שגיאת Heap ואינה קשורה כלל לגלישת buffer על גבי ה-Stack עצמו כאן
תשובה: ג

✓ תשובה: ג) — מבנה Stack: buf (64B) → EBP שמור (4B) → כתובת חזרה (4B). כשstrcpy גולש: כותב על EBP, ואז על כתובת החזרה. כשהפונקציה מבצעת RET → קופץ לכתובת שהתוקף שתל → Remote Code Execution.

שאלה 4.2 · ב-RE רואים: lea eax, [ebp-64] / push eax / call gets. מה זה אומר?

lea eax, [ebp-64] / push eax / call gets
  • א. מדפיס 64 תווים על המסך, אף שהקוד קורא קלט אל תוך buffer בגודל 64 בתים ואינו מדפיס דבר אל המסך כלל בקוד כאן
  • ב. buffer בגודל 64 בתים על ה-Stack יחד עם gets ללא הגבלה מהווה Buffer Overflow ישיר, וה-EIP נדרס ב-64+4 בתים
  • ג. מקצה buffer בגודל 64 באזור ה-Heap, אף שהכתובת [ebp-64] מצביעה על משתנה מקומי על ה-Stack ולא על ה-Heap כלל
  • ד. מבצע בדיקת גודל של ה-buffer, אף שהפונקציה gets כלל אינה בודקת גודל ולכן הקוד חשוף לגלישה ישירה ומסוכנת כאן
תשובה: ב

✓ תשובה: ב) — [ebp-64] = buffer ב-Stack, 64 bytes לפני EBP. CALL gets = קוראת עד \n ללא הגבלה. כדי להגיע ל-EIP: 64 bytes buf + 4 bytes EBP שמור = 68 bytes padding, אז 4 bytes כתובת יעד. Exploit מיידי!

שאלה 4.3 — CTF זיהוי · מהי הפונקציה הבטוחה ביותר לקריאת input מהמשתמש ל-buffer?

; RE של strcmp לסיסמה:
; C: if (strcmp(password, "secret") == 0) { grant_access(); }
lea eax, [ebp-32]      ; eax = password (buffer ב-Stack)
push offset aSecret    ; push "secret" (String Literal)
push eax               ; push password
call strcmp
add esp, 8
test eax, eax          ; strcmp == 0?
jnz .deny              ; אם לא → deny
call grant_access      ; ← הגישה ניתנת!

; ← זיהוי: CALL strcmp + TEST EAX,EAX + JZ/JNZ = password check ב-RE
  • א. fgets(buf, sizeof(buf), stdin) יחד עם הסרה ידנית של \n: מגבילה את הקריאה לגודל ה-buffer ולכן היא הבטוחה כאן
  • ב. gets(buf): הנחה שגויה, אף שהפונקציה gets כלל אינה בודקת גודל ולכן היא דווקא המסוכנת ביותר לקריאת קלט מהמשתמש
  • ג. scanf("%s", buf): הנחה שגויה, אף ש-%s ב-scanf אינה מגבילה את אורך הקלט ולכן היא חשופה לגלישה בדיוק כמו gets
  • ד. strcpy(buf, input): הנחה שגויה, אף ש-strcpy מעתיקה בלי בדיקת גודל ואינה כלל פונקציה לקריאת קלט מן המשתמש כאן
תשובה: א

✓ תשובה: א) — fgets(buf, n, stdin): קוראת עד n-1 תווים + מוסיפה \0. הבטוחה ביותר. gets = אסורה. scanf("%s") = עצרת ב-whitespace אבל ללא הגבלת גודל! strcpy = לא קורא input. אחרי fgets: buf[strcspn(buf,"\n")] = '\0' להסרת \n.

שאלה 5.1 — RE · ב-Assembly רואים: push offset aSecret lea eax, [ebp-32] push eax call strcmp test eax, eax jnz denyמה C code המקביל?

push offset aSecret
lea eax, [ebp-32]
push eax
call strcmp
test eax, eax
jnz deny
  • א. if (strcmp("secret", password)) { deny(); }: הנחה שגויה בסדר הארגומנטים, אף שסדר ה-lea מראה password ראשון כאן
  • ב. strcpy(password, "secret"): הנחה שגויה שמדובר בהעתקה, אף שהקוד קורא ל-strcmp לצורך השוואה ולא ל-strcpy להעתקה
  • ג. if (strcmp(password, "secret") == 0) { deny(); }: הנחה שגויה, שכן jnz קופצת אל deny דווקא כאשר הן אינן זהות כאן
  • ד. if (strcmp(password, "secret") != 0) { deny(); }: הקפיצה jnz אל deny מתבצעת כאשר המחרוזות אינן זהות זו לזו
תשובה: ד

✓ תשובה: ד) — push aSecret (src), push eax=[ebp-32] (password/dst) → CALL strcmp(password, "secret"). TEST EAX,EAX → JNZ deny = אם EAX!=0 (לא שווה) → deny. כלומר: אם סיסמה שגויה → deny. כדי לקבל גישה: strcmp מחזיר 0 → JNZ לא קופץ → ממשיך.

שאלה 5.2 — CTF RE · ב-IDA רואים: CALL gets לאחר LEA EAX, [EBP-0x40]. מה offset ה-EIP overwrite?

#include <stdio.h>
#include <string.h>
void safe_copy(char *dst, const char *src, size_t size){
    if(size==0) return;
    size_t i=0;
    for(; i<size-1 && src[i]; i++) dst[i]=src[i];   // לכל היותר size-1 תווים
    dst[i]='\0';                                    // תמיד מסיים ב-NUL
}
int main(){
    char in[256], out[8];
    if(scanf("%255s",in)!=1) return 1;
    safe_copy(out,in,sizeof(out));
    printf("%s\n",out);
    return 0;
}
  • א. 0x40 = 64 בתים: הנחה שגויה שדי ב-64 בתים, אף שצריך לדרוס גם את ה-EBP השמור (4 בתים) לפני ההגעה אל כתובת החזרה
  • ב. 0x40 - 4 = 60 בתים: הנחה שגויה שיש להחסיר 4 בתים, אף שיש להוסיף 4 בתים עבור ה-EBP השמור ולא להחסירם כלל כאן
  • ג. 0x40 + 8 = 72 בתים: הנחה שגויה שיש להוסיף 8 בתים, אף שה-EBP השמור הוא 4 בתים בלבד בסביבת 32 ביט ולא 8 בתים
  • ד. 0x40 + 4 = 68 בתים: 64 בתים למילוי ה-buffer ועוד 4 בתים לדריסת ה-EBP השמור, ואז מגיעים אל כתובת החזרה כאן
תשובה: ד

✓ תשובה: ד) 68 bytes — [EBP-0x40] = buffer מתחיל 64 bytes לפני EBP. Stack: [buf 64B][EBP שמור 4B][EIP = כתובת חזרה 4B]. כדי להגיע ל-EIP: 64 bytes buf + 4 bytes EBP = 68 bytes padding, אז 4 bytes כתובת יעד. Classic 32bit overflow offset.