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 = ¬es[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 = ¬es[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 = ¬es[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 - הוכחת מושג¶
בחרו באג אחד שמצאתם ותגרמו לתוכנית לקרוס בפועל, כדי לוודא שהבאג אמיתי:
- הדרך הקלה: הזינו כותרת מאוד ארוכה ב-
add, או אינדקס שלילי ב-show, ותראו את ה-Segmentation fault. - אתגר: הזינו
%p %p %p %pככותרת, ואזshowעל אותו פתק. מה מודפס? מה זה אומר על ה-printf(n->title)?
תריצו את זה תחת gdb או pwndbg ותראו איפה בדיוק זה נשבר. זה בדיוק המעבר מ"מצאתי קוד חשוד" ל"הוכחתי שיש כאן באג".
שאלות להעמקה¶
- מבין כל הבאגים שמצאתם, אילו מהם פאזר (fuzzer) היה מוצא בקלות, ואילו הוא כנראה היה מפספס? למה?
- לו הייתם משכתבים את
read_line,add_noteו-show_noteבצורה בטוחה, אילו שינויים מינימליים היו סוגרים את רוב הבאגים בבת אחת? (רמז: תיקון אחד ב-read_lineמרפא כמה תסמינים). - כמה מקורות שונים של קלט יש בתוכנית, ואיזה מהם הכי קל להחמיץ בסקירה מהירה?