Strings ב-C
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
מה זה 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!
char s[] = "GAMMA";
printf("%d %d", (int)strlen(s), (int)sizeof(s));מה הפלט?פונקציות 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); // תמיד בטוח + \0strcat ו-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)!
או: snprintf(buf, sizeof(buf), "%s", src); — תמיד בטוח
char s1[20] = "Hello";
char s2[] = "World";
strcat(s1, " ");
strcat(s1, s2);
printf("%d %s", (int)strlen(s1), s1);מה הפלט?if (strcmp(password, "secret")) { grant(); } else { deny(); }. מה קורה אם שולחים "secret"?פונקציות מסוכנות — 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!char input[100]; fgets(input, 100, stdin); printf(input);?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 קופץ לכתובת שהתוקף שלח!- חפשו: 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
lea eax, [ebp-64] / push eax / call gets. מה זה אומר?פונקציות String ב-Assembly
כשרואים Assembly — CALL strcpy, CALL strlen, LEA ל-.rodata הם הסימנים לזיהוי String Operations. הכרתם חיונית ל-Reverse Engineering ולמציאת Vulnerabilities.
| קוד C | Assembly (32bit cdecl) | הערות לRE |
|---|---|---|
| strlen(s) | push s_ptr; call strlen; add esp,4 | EAX = אורך, ללא \0 |
| strcpy(dst,src) | push src; push dst; call strcpy; add esp,8 | dst ראשון! סדר הפוך בStack |
| strcmp(s1,s2) | push s2; push s1; call strcmp; test eax,eax | EAX=0 אם שווה |
| printf("%s",s) | push s; push fmt_ptr; call printf | fmt קרוב ל-.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 ב-REpush offset aSecret
lea eax, [ebp-32]
push eax
call strcmp
test eax, eax
jnz denyמה C code המקביל?אתגר מעשי — מחרוזות C — בטיחות ופונקציות מסוכנות
שיא השיעור: היפוך, ניתוח וזיהוי — כלי ה-RE וה-Low-Level בפעולה. ההגשה נבדקת אוטומטית.
שאלות תרגול ותשובות (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.