3.3 shellcode מוגבל תרגול
תרגול - shellcode מוגבל¶
בתרגול הזה תתמודדו עם שני סוגי המגבלות שראינו בהרצאה, כל אחד דרך אתגר אמיתי ב-pwnable.kr. אתגר asm יאלץ אתכם לכתוב shellcode של open/read/write תחת ארגז חול של seccomp, ואתגר ascii_easy יאלץ אתכם לעבוד עם בתים מודפסים בלבד וכתובות מודפסות. אל תסתכלו בפתרון לפני שניסיתם - המטרה היא שתרגישו את המגבלה בעצמכם.
חיבור וקבצים¶
אתגר asm רץ כשירות רשת. אחרי שאתם מתחברים ב-SSH כדי לקרוא את המקור, הexploit עצמו מתבצע מול פורט ייעודי:
ssh asm@pwnable.kr -p2222 # the password is on the challenge page; here you read asm.c
# and you send the shellcode to:
nc pwnable.kr 9026 # or remote('pwnable.kr', 9026) in pwntools
אתגר ascii_easy הוא אתגר SSH רגיל:
לפני הכל, קראו את שני קבצי המקור עד הסוף. הבאג בשניהם גלוי - האתגר הוא לעבוד בתוך המגבלה.
תרגיל 1 - מיפוי ארגז החול - seccomp¶
לפני שכותבים בית אחד של shellcode, חייבים לדעת מה מותר. בתרגיל הזה תמפו את המסנן של asm.
המטרה:
- קראו את
asm.c. אתרו את הפונקציהsandbox()וכתבו לעצמכם: אילו קריאות מערכת ברשימת ההיתר, ומה מדיניות ברירת המחדל? - הריצו
seccomp-tools dump ./asm(או מול הבינארי המקומי) וודאו שהפלט תואם לקוד המקור. - ענו: האם
execveמותר? אם לא - מה תעשו במקום לפתוח shell? - אתרו במקור את הנתיב המדויק של קובץ הדגל (הוא ארוך במיוחד) ואת האופן שבו הקלט נקרא (
readאו פונקציית מחרוזת?). מה זה אומר לגבי בתי אפס ב-shellcode שלכם?
רמז: מדיניות ברירת המחדל היא SCMP_ACT_KILL. כל מה שלא ברשימת ההיתר הורג את התהליך.
רמז: אם seccomp-tools לא מותקן, gem install seccomp-tools. הפלט מראה שורות ALLOW לכל קריאה מותרת ו-KILL לשאר.
רמז: הקלט נכנס דרך read(0, ...). read לא עוצר בבית אפס, אז אתם חופשיים להשתמש בהוראות שמכילות אפסים.
תרגיל 2 - shellcode של open/read/write - asm¶
עכשיו כשאתם יודעים מה מותר, כתבו את ה-shellcode שקורא את הדגל ומדפיס אותו.
המטרה:
- כתבו shellcode של x86-64 שמבצע:
open(<flag_path>, O_RDONLY),read(fd, buf, N),write(1, buf, N),exit(0). השתמשו בנתיב הארוך האמיתי מהמקור. - בנו אותו קודם ידנית (בשביל להבין), ואז עם
shellcraftשל pwntools בשביל הגרסה שתשלחו בפועל. - בדקו מקומית עם רתמת בדיקה (harness) שמתקינה seccomp דומה לפני שהיא מריצה את ה-shellcode שלכם - כדי לוודא שאתם באמת בתוך המגבלה ולא מסתמכים על קריאה חסומה.
- שלחו את ה-payload ל-
remote('pwnable.kr', 9026)וקראו את הדגל.
רמז: מספרי הקריאה ב-x86-64: open=2, read=0, write=1, exit=60. הארגומנטים ב-rdi, rsi, rdx.
רמז: אחרי shellcraft.open(path) ה-fd נמצא ב-rax, אז shellcraft.read('rax', 'rsp', 0x100) ישתמש בו ישירות.
רמז: היזהרו מקיצורים כמו shellcraft.cat שעלולים להשתמש ב-sendfile - הוא לא ברשימת ההיתר. הריצו disasm(payload) וודאו שאתם רואים רק open/read/write/exit.
רמז: הבינארי מדפיס give me your x64 shellcode: לפני שהוא קורא. השתמשו ב-recvuntil ואז send.
תרגיל 3 - למה shellcode רגיל נופל בפילטר - ascii_easy¶
לפני שמנצלים, מבינים למה הדרך הרגילה נכשלת. בתרגיל הזה תפגשו את הפילטר של ascii_easy.
המטרה:
- קראו את המקור. אתרו את
is_asciiואת הoverflow ב-vuln(strcpyלbuffer קטן). מהו טווח הבתים המותר? - הריצו
checksecעל הבינארי. האם 32 או 64 ביט? האם יש PIE? האם NX? - מצאו את ההיסט (offset) להreturn address עם
cyclic. שימו לב: כל בתי ה-padding חייבים להיות מודפסים. - נסו לבנות payload "תמים" שהreturn address בו מצביעה למחסנית או לכתובת אקראית. הריצו את הבינארי עם הקלט. מה קורה? הסבירו למה הבתים הלא-מודפסים נחסמים.
רמז: is_ascii מרשה 0x20 <= c <= 0x7f. תו כמו \x00 או \xff יפסול את כל הקלט.
רמז: השתמשו ב-cyclic(100) כקלט, מצאו את ערך ה-eip בזמן הקריסה עם GDB, ו-cyclic_find(...) יגיד לכם את ההיסט.
רמז: מלאו את ה-padding ב-b'A' (0x41) שהוא מודפס, לא ב-\x00.
תרגיל 4 - כתובת מודפסת שמצביעה לקוד - ascii_easy¶
זה הלב של ascii_easy. תמצאו return address שכולה בתים מודפסים ושמצביעה לקוד שימושי בספרייה הממופה.
המטרה:
- מצאו את הכתובת הקבועה שבה נטענת הספרייה המשותפת (חפשו את הקבוע
BASEבמקור, ואמתו עםvmmap/info proc mappingsב-GDB). - כתבו סקריפט שסורק את הספרייה הממופה ומוצא את כל ההיסטים שבהם
BASE + offsetהוא כתובת מודפסת (כל 4 הבתים ב-0x20-0x7e). - מבין הכתובות המודפסות, מצאו אחת שהבתים בה מהווים גאדג'ט או shellcode שימושי (למשל רצף שקורא את הדגל, או גאדג'ט שקופץ לbuffer שלכם).
- בנו את ה-exploit:
padding מודפס + p32(printable_addr). ודאו ש-all(0x20 <= b <= 0x7e for b in payload).
כדי לתרגל את המכניקה בלי השרת, קמפלו והריצו את הדמו המקומי הזה שמשחזר בדיוק את התנאים (ספרייה ב-shellcode ידוע בכתובת קבועה + overflow עם פילטר מודפס):
// demo_ascii.c
// gcc -m32 -fno-stack-protector -no-pie -z execstack -o demo_ascii demo_ascii.c
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
// execve("/bin/sh",0,0) for x86-32, placed in memory in advance (not part of our input)
unsigned char sc[] =
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e"
"\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80";
int is_ascii(int c){ return c >= 0x20 && c <= 0x7e; }
void vuln(char* p){ char buf[20]; strcpy(buf, p); }
int main(int argc, char** argv){
// map at a fixed address that has a printable offset: 0x55552020
void* m = mmap((void*)0x55550000, 0x4000,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED, -1, 0);
memcpy((char*)m + 0x2020, sc, sizeof(sc)); // the shellcode sits at 0x55552020
if(argc != 2){ puts("usage: demo_ascii <ascii>"); return 0; }
for(int i=0; argv[1][i]; i++)
if(!is_ascii((unsigned char)argv[1][i])){ puts("non-ascii!"); return 0; }
vuln(argv[1]);
return 0;
}
רמז: כתובת מודפסת בבינארי 32 ביט צריכה שכל 4 הבתים שלה (little-endian) יהיו ב-0x20-0x7e. הכתובת 0x55552020 היא דוגמה חוקית (20 20 55 55).
רמז: בדמו ה-shellcode כבר יושב ב-0x55552020. כל מה שצריך זה לדרוס את הreturn address בכתובת הזאת. מצאו את ההיסט עם cyclic.
רמז: באתגר האמיתי אין shellcode מוכן בכתובת נוחה - צריך לסרוק את הספרייה הממופה ולחפש כתובת מודפסת שהבתים בה שימושיים. הסורק מההרצאה הוא נקודת הפתיחה.
תרגיל 5 (אתגר) - קידוד אלפאנומרי מלא¶
מה עושים כשאין shellcode מוכן בכתובת מודפסת, וצריך להזריק את הקוד בעצמנו דרך ערוץ שמסנן לבתים מודפסים? מקדדים.
המטרה:
- קחו shellcode רגיל של
execve("/bin/sh",0,0)ל-x86-32. - קדדו אותו לאלפאנומרי מודפס בשתי דרכים לפחות: עם
encoders.i386.ascii_shellcodeשל pwntools, ועםmsfvenom -e x86/alpha_mixed. - ודאו שכל בית בתוצאה הוא ב-
0x20-0x7e. - הריצו את ה-shellcode המקודד דרך טוען עם עמוד RWX (כמו בשיעור 3.1) והוכיחו שנפתח shell.
- הסבירו: איזה רגיסטר המפענח צריך שיצביע ל-shellcode, ולמה כל הרעיון עובד רק כשהמחסנית/העמוד ניתנים להרצה.
רמז: ה-decoder stub מניח שרגיסטר מסוים כבר מצביע לתחילת ה-shellcode. עם pwntools/msfvenom בדקו איזה רגיסטר הם דורשים (למשל EAX) ודאגו שהוא יצביע נכון.
רמז: אחרי הקידוד, assert all(0x20 <= b <= 0x7e for b in enc) הוא הבדיקה שחוסכת לכם דיבוג מיותר.
רמז: הקידוד מגדיל את ה-shellcode פי כמה. אם יש מגבלת אורך על הקלט, בדקו שהתוצאה עדיין נכנסת.