לדלג לתוכן

7.3 GDB מתקדם תרגול

תרגול - GDB מתקדם

תרגיל 1 - מציאת סיסמה בבינארי עם GDB

כתבו את התוכנית הבאה, קמפלו אותה בלי -g, ואז התייחסו אליה כאילו לא ראיתם את הקוד:

#include <stdio.h>
#include <string.h>

int main(void) {
    char input[32];
    char password[] = "OpenSesame";

    printf("Enter password: ");
    scanf("%31s", input);

    if (strcmp(input, password) == 0) {
        printf("Access granted!\n");
    } else {
        printf("Access denied.\n");
    }
    return 0;
}

קמפלו:

gcc -o passcheck passcheck.c

עכשיו, בלי להסתכל על הקוד, השתמשו ב-GDB כדי למצוא את הסיסמה:

א. פתחו ב-GDB, הגדירו breakpoint על strcmp@plt, והריצו את התוכנית עם קלט כלשהו.

ב. כשנעצרים ב-strcmp, בדקו את הארגומנטים:

(gdb) x/s $rdi
(gdb) x/s $rsi

מה שני הארגומנטים? אחד מהם הוא הקלט שלכם ואחד הוא הסיסמה.

ג. עכשיו שמצאתם את הסיסמה, הריצו שוב עם הסיסמה הנכונה ווודאו שמקבלים "Access granted!".

ד. אתגר נוסף: בלי למצוא את הסיסמה, שנו את ערך ההחזרה של strcmp כדי "לעקוף" את הבדיקה. (רמז: strcmp מחזיר 0 כשהמחרוזות שוות, ו-eax מכיל את ערך ההחזרה)


תרגיל 2 - מעקב אחרי ארגומנטים של פונקציות

כתבו תוכנית שמקבלת שם קובץ כארגומנט וקוראת את תוכנו:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        return 1;
    }

    FILE *f = fopen(argv[1], "r");
    if (f == NULL) {
        perror("fopen");
        return 1;
    }

    char buf[256];
    while (fgets(buf, sizeof(buf), f) != NULL) {
        printf("%s", buf);
    }

    fclose(f);
    return 0;
}

קמפלו בלי -g:

gcc -o reader reader.c

צרו קובץ טקסט לבדיקה:

echo "Hello from file" > /tmp/testfile.txt

א. הגדירו breakpoint על fopen@plt. הריצו run /tmp/testfile.txt. כשנעצרים:
- מה הארגומנט הראשון (rdi)? בדקו עם x/s $rdi
- מה הארגומנט השני (rsi)? בדקו עם x/s $rsi

ב. הגדירו breakpoint על fgets@plt עם commands שמדפיסים את הארגומנטים ועושים continue אוטומטית:

(gdb) break fgets@plt
(gdb) commands
> printf "fgets(buf=%p, size=%d, file=%p)\n", $rdi, $rsi, $rdx
> continue
> end
(gdb) continue

כמה פעמים fgets נקראת? מה הגודל שמועבר?

ג. הגדירו breakpoint על printf@plt עם commands שמדפיסים את format string. עקבו אחרי מה שמודפס.


תרגיל 3 - איתור נקודת ההחלטה ועקיפתה

כתבו תוכנית שבודקת אם מספר הוא "מספר הקסם":

#include <stdio.h>
#include <stdlib.h>

int check_magic(int num) {
    return (num * 7 + 13) % 100 == 42;
}

int main(void) {
    int num;
    printf("Enter a number: ");
    scanf("%d", &num);

    if (check_magic(num)) {
        printf("Magic number found!\n");
    } else {
        printf("Not a magic number.\n");
    }
    return 0;
}

קמפלו בלי -g:

gcc -o magic magic.c

א. עשו disassemble main ומצאו:
- את הקריאה ל-check_magic
- את ההוראה שבודקת את ערך ההחזרה (test/cmp אחרי ה-call)
- את הקפיצה המותנית שמחליטה בין "Magic number found!" ל-"Not a magic number."

ב. הגדירו breakpoint אחרי הקריאה ל-check_magic. הריצו עם מספר כלשהו. בדקו מה ערך eax (ערך ההחזרה). שנו את eax ל-1 ועשו continue. האם קיבלתם "Magic number found!" למרות שהזנתם מספר לא נכון?

ג. מצאו מהו מספר הקסם הנכון. (רמז: הגדירו breakpoint בתוך check_magic, צעדו עם ni, ועקבו אחרי החישוב. או חשבו מתמטית: (num * 7 + 13) % 100 == 42)

ד. השתמשו ב-jump כדי לדלג ישירות על הבדיקה ולהגיע להדפסת "Magic number found!" בלי לשנות אוגרים.


תרגיל 4 - שימוש ב-watchpoint

כתבו תוכנית עם משתנה גלובלי שמשתנה באופן "מסתורי":

#include <stdio.h>

int counter = 0;

void add_one(void) {
    counter++;
}

void add_five(void) {
    counter += 5;
}

void subtract_three(void) {
    counter -= 3;
}

int main(void) {
    add_one();
    add_five();
    subtract_three();
    add_one();
    add_one();

    printf("Final counter = %d\n", counter);
    return 0;
}

קמפלו בלי -g:

gcc -o counter counter.c

א. מצאו את הכתובת של המשתנה הגלובלי counter:

(gdb) info variables counter

או:
(gdb) print &counter

ב. הגדירו watchpoint על counter:

(gdb) watch *<address_of_counter>

ג. הריצו את התוכנית ועקבו אחרי כל שינוי. GDB יעצור בכל פעם ש-counter משתנה ויראה את הערך הישן והחדש. לכל עצירה, בדקו עם backtrace מאיזו פונקציה השינוי הגיע.

ד. תעדו: מה סדר השינויים? אילו פונקציות שינו את counter ובכמה?


תרגיל 5 - סקריפט GDB מתקדם

כתבו תוכנית שמקבלת מחרוזת ובודקת אותה תו-תו:

#include <stdio.h>
#include <string.h>

int validate(const char *input) {
    const char key[] = {0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x00};
    int len = strlen(key);

    if ((int)strlen(input) != len)
        return 0;

    for (int i = 0; i < len; i++) {
        if (input[i] != key[i])
            return 0;
    }
    return 1;
}

int main(void) {
    char buf[64];
    printf("Enter code: ");
    scanf("%63s", buf);

    if (validate(buf)) {
        printf("Valid!\n");
    } else {
        printf("Invalid.\n");
    }
    return 0;
}

קמפלו בלי -g:

gcc -o validate validate.c

א. עשו disassemble validate ומצאו את הלולאה שמשווה תו-תו.

ב. הגדירו breakpoint בנקודת ההשוואה (ה-cmp שבתוך הלולאה). כתבו סקריפט GDB שמדפיס את התו מהקלט ואת התו הצפוי בכל איטרציה:

(gdb) break *<address_of_cmp>
(gdb) commands
> printf "input[%d] = 0x%02x ('%c'), expected = 0x%02x ('%c')\n", ...
> continue
> end

(רמז: תצטרכו לזהות באילו אוגרים נמצאים התווים שמושווים, על פי ההוראות שלפני ה-cmp)

ג. מהו הcode הנכון? (רמז: הערכים 0x48, 0x65, 0x6c, 0x6c, 0x6f הם קודי ASCII)

ד. הריצו שוב עם הcode הנכון ווודאו שמקבלים "Valid!".

ה. אתגר: מצאו את הcode בלי להריץ את התוכנית בכלל - רק עם objdump -s -j .rodata validate או strings וניתוח הדיסאסמבלי.