1.1 תיאורי קבצים וארגומנטים תרגול
תרגול - תיאורי קבצים וארגומנטים¶
בתרגול הזה נפתור שני אתגרים מ-pwnable.kr שבודקים בדיוק את מה שלמדנו בהרצאה: fd ו-input. הראשון הוא אתגר של שורה אחת שממחיש את הטריק של atoi. השני הוא תרגיל יסודי שמעביר אתכם דרך כל חמשת ערוצי הקלט של תהליך, וכולם צריכים לעבור בהצלחה בהרצה אחת.
שני האתגרים נמצאים בקטגוריה [Toddler's Bottle] של pwnable.kr, ופותרים אותם על השרת עצמו דרך SSH. הסיסמה לכל החשבונות בקטגוריה הזו היא guest.
# fd:
ssh fd@pwnable.kr -p2222 # password: guest
# input:
ssh input2@pwnable.kr -p2222 # password: guest
בתוך כל חשבון תמצאו את הבינארי, את קוד המקור (.c) ואת קובץ הדגל flag שאין לכם הרשאה לקרוא ישירות. המטרה בכל אתגר היא לגרום לבינארי עצמו להדפיס את הדגל.
תרגיל 1 - האתגר fd¶
התחברו לחשבון fd וקראו את fd.c. זהו בדיוק סוג הבינארי שראינו בהרצאה: הוא לוקח את argv[1], מפעיל עליו atoi, מחסיר ממנו 0x1234, ומשתמש בתוצאה כתיאור קובץ ל-read. אחר כך הוא משווה את מה שנקרא למחרוזת קבועה, ואם יש התאמה - מדפיס את הדגל.
המטרה: לגרום לתוכנית להדפיס good job :) ואת הדגל.
- רמז 1: איזה תיאור קובץ מחובר לקלט שאתם יכולים להקליד? מה המספר שלו?
- רמז 2: אתם צריכים שהביטוי
atoi(argv[1]) - 0x1234יהיה שווה למספר הזה. פתרו את המשוואה. שימו לב ש-0x1234הוא ערך הקסדצימלי - כמה זה בעשרוני? - רמז 3: אחרי שהעברתם את הארגומנט הנכון, התוכנית מחכה לקלט. מה בדיוק היא משווה אליו? העתיקו את המחרוזת מקוד המקור בול, כולל או לא כולל ירידת שורה.
- רמז 4: אפשר להקליד את הקלט ידנית, אבל נסו גם להזין אותו דרך pipe, למשל עם
echoאו עםpython. חשבו האם ה-pipe מוסיף ירידת שורה או לא, וכיצד זה משפיע על ההשוואה.
אתגר נוסף (למי שסיים): פתרו את fd בשלוש דרכים שונות של הזנת הקלט - הקלדה ידנית, pipe מ-echo, וסקריפט pwntools. ודאו שאתם מבינים למה כל אחת מהן עובדת.
תרגיל 2 - האתגר input¶
זהו הלב של השיעור. התחברו לחשבון input2, קראו את input.c, ותראו שהתוכנית מריצה חמישה שלבים בזה אחר זה. כל שלב בודק ערוץ קלט אחר, וכולם חייבים לעבור באותה הרצה אחת כדי לקבל את הדגל. כשעוברים שלב, התוכנית מדפיסה Stage N clear!.
חמשת השלבים (קראו את הקוד ואמתו את הערכים המדויקים בעצמכם):
- ארגומנטים - argv: התוכנית דורשת
argcמסוים בדיוק, ובודקת ערכים באינדקסים ספציפיים (רמז: אינדקסים שהם קודי ASCII של אותיות). אחד הערכים הוא מחרוזת שמכילה תווי בקרה ורווח, ואחר הוא מחרוזת שהיא בעצם ריקה. - קלט סטנדרטי - stdin ו-stderr: התוכנית קוראת בתים גם מ-fd 0 וגם מ-fd 2, ומשווה אותם לערכים קבועים.
- סביבה - environment: התוכנית קוראת משתנה סביבה בשם מסוים ומשווה את ערכו.
- קובץ - file: התוכנית פותחת קובץ בשם מסוים בתיקיית העבודה, קוראת ממנו, ומשווה.
- רשת - network: התוכנית פותחת socket, מאזינה על פורט שמגיע מאחד הארגומנטים, ומחכה שמישהו יתחבר וישלח בתים מסוימים.
המטרה: לעבור את כל חמשת השלבים בהרצה אחת ולקבל את הדגל.
- רמז 1: לפני שאתם כותבים קוד, פתחו טבלה. עבור כל שלב רשמו: מאיזה ערוץ קוראים, לאיזה אינדקס/שם, ומה הערך המדויק בבתים (בהקסה). אל תנחשו - העתיקו מהמקור.
- רמז 2 (שלב argv): כמה מהערכים הנדרשים בלתי אפשריים להעברה דרך shell רגיל - בית null, מחרוזת ריקה, רווח בתוך ארגומנט. נזכרתם מההרצאה מה עושים במקרה כזה? בונים את
argvבעצמכם ומריצים עםexecveאו עםprocess(argv=[...]). - רמז 3 (שלב stdin/stderr): fd 0 קל - שולחים אליו. fd 2 הוא בדרך כלל פלט. איך גורמים לתוכנית לקרוא מ-fd 2 בדיוק את מה שאתם רוצים? חזרו לחלק בהרצאה על
os.pipe+os.dup2שמחבר את קצה הקריאה של pipe ל-fd 2 לפני ה-execve. - רמז 4 (שלב env): כמו ב-argv, שם המשתנה או ערכו עשויים להכיל בתים שה-shell לא ירשה. העבירו את הסביבה דרך המילון
envשלexecve/process, לא דרך shell. - רמז 5 (שלב קובץ): הקובץ עשוי להיקרא בשם שהוא תו בקרה בודד (למשל ירידת שורה). צרו אותו בתיקיית העבודה של הסקריפט לפני ההרצה, עם התוכן המדויק שהתוכנית מצפה לו.
- רמז 6 (שלב רשת): קראו מהמקור מאיזה ארגומנט מגיע הפורט. בחרו פורט פנוי מעל 1024, העבירו אותו באותו אינדקס ב-
argv, ואחרי שהתוכנית מספיקה לבצעbind/listen- התחברו אליו מהסקריפט ושלחו את הבתים. שימו לב לתזמון: אולי צריךsleepקטן או ניסיון חוזר עד שה-socket מוכן. - רמז 7 (הדגל): הבינארי מריץ
system("/bin/cat flag"), כלומר הוא קורא קובץ בשםflagבתיקיית העבודה שלו, בהרשאות המוגברות שלו. אתם מריצים את הסקריפט מתיקייה שאתם יכולים לכתוב אליה (למשל תת-תיקייה ב-/tmp), אבל הדגל האמיתי נמצא בתיקיית הבית של האתגר. חשבו איך לגרום ל-cat flagבתיקיית העבודה שלכם לקרוא בעצם את הדגל האמיתי. (רמז בתוך רמז: קישור סמלי - symlink.)
בנו את הסקריפט שלב אחר שלב. הוסיפו שלב, ודאו שאתם רואים Stage 1 clear!, ורק אז הוסיפו את השלב הבא. אל תנסו לכתוב את כל חמשת השלבים בבת אחת - קשה לדבג ככה.
תרגיל 3 - הכירו את הכלים שלכם (בונוס)¶
כדי לוודא שאתם באמת שולטים ב-argv ולא מנחשים, כתבו בעצמכם בינארי דמו קטן שמדפיס אבחון:
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("argc = %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] (len=%zu): ", i, strlen(argv[i]));
for (char *p = argv[i]; *p; p++) printf("%02x ", (unsigned char)*p);
printf("\n");
}
return 0;
}
עכשיו הריצו אותו מתוך pwntools עם process(argv=[...]) והעבירו לו ארגומנט שמכיל רווח ומחרוזת ריקה. ודאו שאתם רואים בפלט בדיוק את הבתים שהתכוונתם אליהם. נסו גם להעביר אותו ארגומנט דרך shell רגיל ולראות מה ההבדל. זה יוודא לכם שהכלי עושה מה שאתם חושבים, לפני שאתם תלויים בו באתגר input.