לדלג לתוכן

10.1 סקירת קוד מקור תרגול

תרגול - סקירת קוד של מנהל פתקים vulnerable

בתרגול הזה תקבלו תוכנית C אחת - מנהל פתקים קטן בשם notes.c - שיש בה כמה באגים שתולים בכוונה. המשימה שלכם היא לעבוד בדיוק כמו חוקר חולשות: למפות את המקורות והסינקים, לעקוב אחרי זרימת הנתונים, ולמצוא כל הנחה שבורה. אל תסתכלו בפתרון לפני שמצאתם לפחות ארבעה באגים בעצמכם.

התוכנית מדמה שירות קטן שמקבל פתקים מהמשתמש, שומר אותם, ומדפיס אותם לפי בקשה. היא נראית תמימה. היא לא.

// notes.c - compile with: gcc -fno-stack-protector -no-pie -o notes notes.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MAX_NOTES 16

struct note {
    char  title[32];
    char *body;
    int   len;
};

struct note notes[MAX_NOTES];
int note_count = 0;

// reads a line from standard input until a newline character
void read_line(char *buf, int size) {
    int i = 0;
    char c;
    while (read(0, &c, 1) == 1 && c != '\n') {
        buf[i] = c;
        i++;
    }
    buf[i] = '\0';
}

int read_int(void) {
    char tmp[16];
    read_line(tmp, sizeof(tmp));
    return atoi(tmp);
}

void add_note(void) {
    if (note_count >= MAX_NOTES) {
        puts("no room");
        return;
    }
    struct note *n = &notes[note_count];

    char title[64];
    printf("title: ");
    read_line(title, sizeof(title));
    strcpy(n->title, title);

    printf("length: ");
    int len = read_int();
    n->body = malloc(len + 1);
    n->len  = len;

    printf("body: ");
    read(0, n->body, len);
    n->body[len] = '\0';

    note_count++;
    puts("saved");
}

void show_note(void) {
    printf("index: ");
    int idx = read_int();
    if (idx > note_count) {
        puts("bad index");
        return;
    }
    struct note *n = &notes[idx];
    printf(n->title);
    printf("\n%s\n", n->body);
}

void save_note(void) {
    printf("index: ");
    int idx = read_int();
    if (idx >= note_count) return;
    struct note *n = &notes[idx];

    char path[64];
    sprintf(path, "/tmp/notes/%s.txt", n->title);

    if (access(path, W_OK) == 0) {
        FILE *f = fopen(path, "w");
        fwrite(n->body, 1, n->len, f);
        fclose(f);
        puts("written");
    }
}

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    char choice[8];
    while (1) {
        printf("> ");
        read_line(choice, sizeof(choice));
        if      (!strcmp(choice, "add"))  add_note();
        else if (!strcmp(choice, "show")) show_note();
        else if (!strcmp(choice, "save")) save_note();
        else if (!strcmp(choice, "quit")) break;
    }
    return 0;
}

תרגיל 1 - מיפוי משטח התקיפה

לפני שמחפשים באגים, מפים. עברו על הקוד וענו:

  • אילו פונקציות הן מקורות - נקודות שבהן נכנס קלט שהתוקף שולט בו? רשמו את כולן.
  • אילו קריאות הן סינקים - פעולות מסוכנות שבטיחותן תלויה בקלט? עברו על הרשימה מההרצאה (strcpy, sprintf, memcpy/read, malloc, printf, גישה למערך) ומצאו כל מופע.

רמז: יש פונקציה אחת, read_line, שהיא מקור וגם סינק בעצמה. שווה להתחיל דווקא בה.


תרגיל 2 - הבאג בלב הקוד

תסתכלו טוב על read_line. היא מקבלת פרמטר size, אבל תבדקו: האם היא משתמשת בו בכלל בזמן שהיא כותבת ל-buf?

  • רמז 1: עקבו אחרי המשתנה i. מה מגביל אותו? מתי הלולאה עוצרת?
  • רמז 2: אם read_line פגומה, אז כל קורא שלה vulnerable. מי הקוראים? (add_note עבור title, read_int עבור tmp, ו-main עבור choice).
  • רמז 3: מה קורה אם בתפריט הראשי, בתגובה ל->, תקלידו 200 תווים? לאיזה buffer הם נכנסים, וכמה מקום יש לו?

תרשמו: מה מחלקת הבאג, ואיך תתקנו את read_line בשורה אחת.


תרגיל 3 - שרשרת ה-length

הפונקציה add_note קוראת אורך מהמשתמש ומשתמשת בו שלוש פעמים. עקבו אחרי המשתנה len מרגע שנקרא ועד הסוף:

int len = read_int();        // len is attacker-controlled. What values can it take?
n->body = malloc(len + 1);   // what happens here if len == -1? if len == 2147483647?
n->len  = len;
read(0, n->body, len);       // the third parameter of read is size_t. What happens if len is negative?
n->body[len] = '\0';
  • רמז 1: len הוא int חתום, שמגיע מ-atoi. atoi("-1") מחזיר -1. atoi על מספר ענק?
  • רמז 2: הפרמטר השלישי של read הוא size_t - לא חתום. מה קורה כשמעבירים לו int שלילי?
  • רמז 3: כמה בתים באמת הוקצו ב-malloc, וכמה בתים read מוכן לכתוב? האם הם תמיד תואמים?

יש כאן יותר ממחלקת באג אחת. נסו למנות שתיים לפחות (רמז: אחת מהן קשורה לoverflow שלם, אחת לסימן).


תרגיל 4 - הדפסה ואינדוקס

בפונקציה show_note יש שתי בעיות נפרדות. חפשו אותן:

  • רמז 1: הבדיקה היא if (idx > note_count). האינדקסים החוקיים הם 0 עד note_count - 1. האם הבדיקה הזו מגנה על שני הקצוות? מה עם idx שלילי? מה עם idx == note_count?
  • רמז 2: תסתכלו על printf(n->title). מי שולט ב-n->title? מה יקרה אם הכותרת מכילה %p או %n?

תסווגו כל אחת מהשתיים בנפרד.


תרגיל 5 - שמירה לקובץ

בפונקציה save_note מסתתרות שתי בעיות מהמחלקות שראינו בהרצאה:

  • רמז 1: sprintf(path, "/tmp/notes/%s.txt", n->title) - path הוא בגודל 64. כמה בתים יכולה n->title להכיל בפועל (זכרו את הבאג מתרגיל 2)? מה עוד יכול לקרות אם הכותרת מכילה ../?
  • רמז 2: יש כאן access(path, ...) ואז fopen(path, ...). מה קורה בין שתי הקריאות? באיזו מחלקת באג מדובר, ולמה זה מסוכן במיוחד בתוכנית setuid?

תרגיל 6 - כלים

תרגלו את הכלים מההרצאה על הקובץ הזה:

# mark all the dangerous sinks
grep -nE '\b(strcpy|sprintf|memcpy|malloc|read|printf|access|fopen)\b' notes.c

# mark all the sources
grep -nE '\b(read|atoi|read_line|read_int)\b' notes.c
  • האם ה-grep מצא את כל הבאגים שמצאתם ידנית? אילו באגים הוא לא יכול למצוא לבד (כי הם דורשים הבנה של זרימה או של סימן)?
  • נסו לכתוב כלל semgrep שמסמן קריאה ל-printf שהארגומנט הראשון שלה אינו מחרוזת קבועה. זה יתפוס את הבאג ב-show_note.

תרגיל 7 - הוכחת מושג

בחרו באג אחד שמצאתם ותגרמו לתוכנית לקרוס בפועל, כדי לוודא שהבאג אמיתי:

gcc -fno-stack-protector -no-pie -o notes notes.c
  • הדרך הקלה: הזינו כותרת מאוד ארוכה ב-add, או אינדקס שלילי ב-show, ותראו את ה-Segmentation fault.
  • אתגר: הזינו %p %p %p %p ככותרת, ואז show על אותו פתק. מה מודפס? מה זה אומר על ה-printf(n->title)?

תריצו את זה תחת gdb או pwndbg ותראו איפה בדיוק זה נשבר. זה בדיוק המעבר מ"מצאתי קוד חשוד" ל"הוכחתי שיש כאן באג".


שאלות להעמקה

  • מבין כל הבאגים שמצאתם, אילו מהם פאזר (fuzzer) היה מוצא בקלות, ואילו הוא כנראה היה מפספס? למה?
  • לו הייתם משכתבים את read_line, add_note ו-show_note בצורה בטוחה, אילו שינויים מינימליים היו סוגרים את רוב הבאגים בבת אחת? (רמז: תיקון אחד ב-read_line מרפא כמה תסמינים).
  • כמה מקורות שונים של קלט יש בתוכנית, ואיזה מהם הכי קל להחמיץ בסקירה מהירה?