לדלג לתוכן

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 רגיל:

ssh ascii_easy@pwnable.kr -p2222   # there you'll find the binary, source, and flag file

לפני הכל, קראו את שני קבצי המקור עד הסוף. הבאג בשניהם גלוי - האתגר הוא לעבוד בתוך המגבלה.


תרגיל 1 - מיפוי ארגז החול - seccomp

לפני שכותבים בית אחד של shellcode, חייבים לדעת מה מותר. בתרגיל הזה תמפו את המסנן של asm.

המטרה:

  1. קראו את asm.c. אתרו את הפונקציה sandbox() וכתבו לעצמכם: אילו קריאות מערכת ברשימת ההיתר, ומה מדיניות ברירת המחדל?
  2. הריצו seccomp-tools dump ./asm (או מול הבינארי המקומי) וודאו שהפלט תואם לקוד המקור.
  3. ענו: האם execve מותר? אם לא - מה תעשו במקום לפתוח shell?
  4. אתרו במקור את הנתיב המדויק של קובץ הדגל (הוא ארוך במיוחד) ואת האופן שבו הקלט נקרא (read או פונקציית מחרוזת?). מה זה אומר לגבי בתי אפס ב-shellcode שלכם?

רמז: מדיניות ברירת המחדל היא SCMP_ACT_KILL. כל מה שלא ברשימת ההיתר הורג את התהליך.

רמז: אם seccomp-tools לא מותקן, gem install seccomp-tools. הפלט מראה שורות ALLOW לכל קריאה מותרת ו-KILL לשאר.

רמז: הקלט נכנס דרך read(0, ...). read לא עוצר בבית אפס, אז אתם חופשיים להשתמש בהוראות שמכילות אפסים.

תרגיל 2 - shellcode של open/read/write - asm

עכשיו כשאתם יודעים מה מותר, כתבו את ה-shellcode שקורא את הדגל ומדפיס אותו.

המטרה:

  1. כתבו shellcode של x86-64 שמבצע: open(<flag_path>, O_RDONLY), read(fd, buf, N), write(1, buf, N), exit(0). השתמשו בנתיב הארוך האמיתי מהמקור.
  2. בנו אותו קודם ידנית (בשביל להבין), ואז עם shellcraft של pwntools בשביל הגרסה שתשלחו בפועל.
  3. בדקו מקומית עם רתמת בדיקה (harness) שמתקינה seccomp דומה לפני שהיא מריצה את ה-shellcode שלכם - כדי לוודא שאתם באמת בתוך המגבלה ולא מסתמכים על קריאה חסומה.
  4. שלחו את ה-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.

המטרה:

  1. קראו את המקור. אתרו את is_ascii ואת הoverflow ב-vuln (strcpy לbuffer קטן). מהו טווח הבתים המותר?
  2. הריצו checksec על הבינארי. האם 32 או 64 ביט? האם יש PIE? האם NX?
  3. מצאו את ההיסט (offset) להreturn address עם cyclic. שימו לב: כל בתי ה-padding חייבים להיות מודפסים.
  4. נסו לבנות 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 שכולה בתים מודפסים ושמצביעה לקוד שימושי בספרייה הממופה.

המטרה:

  1. מצאו את הכתובת הקבועה שבה נטענת הספרייה המשותפת (חפשו את הקבוע BASE במקור, ואמתו עם vmmap/info proc mappings ב-GDB).
  2. כתבו סקריפט שסורק את הספרייה הממופה ומוצא את כל ההיסטים שבהם BASE + offset הוא כתובת מודפסת (כל 4 הבתים ב-0x20-0x7e).
  3. מבין הכתובות המודפסות, מצאו אחת שהבתים בה מהווים גאדג'ט או shellcode שימושי (למשל רצף שקורא את הדגל, או גאדג'ט שקופץ לbuffer שלכם).
  4. בנו את ה-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 מוכן בכתובת מודפסת, וצריך להזריק את הקוד בעצמנו דרך ערוץ שמסנן לבתים מודפסים? מקדדים.

המטרה:

  1. קחו shellcode רגיל של execve("/bin/sh",0,0) ל-x86-32.
  2. קדדו אותו לאלפאנומרי מודפס בשתי דרכים לפחות: עם encoders.i386.ascii_shellcode של pwntools, ועם msfvenom -e x86/alpha_mixed.
  3. ודאו שכל בית בתוצאה הוא ב-0x20-0x7e.
  4. הריצו את ה-shellcode המקודד דרך טוען עם עמוד RWX (כמו בשיעור 3.1) והוכיחו שנפתח shell.
  5. הסבירו: איזה רגיסטר המפענח צריך שיצביע ל-shellcode, ולמה כל הרעיון עובד רק כשהמחסנית/העמוד ניתנים להרצה.

רמז: ה-decoder stub מניח שרגיסטר מסוים כבר מצביע לתחילת ה-shellcode. עם pwntools/msfvenom בדקו איזה רגיסטר הם דורשים (למשל EAX) ודאגו שהוא יצביע נכון.

רמז: אחרי הקידוד, assert all(0x20 <= b <= 0x7e for b in enc) הוא הבדיקה שחוסכת לכם דיבוג מיותר.

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