לדלג לתוכן

3.4 shellcode מתקדם תרגול

תרגול - shellcode מתקדם

בתרגול הזה תבנו בעצמכם את שלוש הטכניקות המרכזיות של ההרצאה: staged loader (stager + stage-2), egghunter שסורק את הזיכרון ומוצא payload, ו-reverse shell שמתחבר אליכם דרך הרשת. בסוף תשלבו מפענח XOR שמשנה את עצמו. אל תעתיקו את הפתרון - כתבו את האסמבלי בעצמכם, כי כל הטכניקות האלה חוזרות שוב ושוב בהמשך הקורס.

סביבה והנחות

הכל רץ מקומית על לינוקס x86-64. ההנחות: NX כבוי היכן שמצוין (או שאנחנו יוצרים אזור RWX בעצמנו), ASLR לא מפריע (טכניקות ה-egghunter ממילא חסינות אליו). הכלים: python3 עם pwntools, gcc, nasm, objdump, ו-nc להאזנה.

טוען לבדיקה כללית של shellcode (שמרו כ-loader.c, מדביקים את הבתים בפנים):

#include <string.h>
#include <sys/mman.h>

unsigned char sc[] =
    "\x90\x90\x90";   // <-- paste your bytes here

int main(void) {
    void *page = mmap(0, 0x100000, PROT_READ | PROT_WRITE | PROT_EXEC,
                      MAP_ANON | MAP_PRIVATE, -1, 0);
    memcpy(page, sc, sizeof(sc));
    ((void (*)(void))page)();
    return 0;
}

תזכורת למספרי קריאות מערכת ב-x86-64: read=0, mmap=9, mprotect=10, access=21, dup2=33, socket=41, connect=42, accept=43, bind=49, listen=50, execve=59.


תרגיל 1 - stager שקורא ומריץ שלב שני

כתבו stager מינימלי ל-x86-64 שקורא בתים מ-stdin אל תוך אזור ניתן להרצה וקופץ אליהם. הוכיחו שהוא מריץ shellcode ששלחתם אחרי שהוא התחיל לרוץ.

המטרה:

  1. כתבו stager שעושה read(0, buf, 0x1000) ואז jmp buf. בחרו את buf להיות אזור מריץ (המחסנית עצמה מספיקה בסביבת המעבדה).
  2. הרכיבו עם asm(), ומדדו את האורך - הוא צריך להיות קטן (בסביבות 15 בתים).
  3. בנו בדיקה: הריצו את ה-stager עם run_shellcode, ואז שלחו לו את ה-shellcode של execve("/bin/sh") מ-3.1. אמור להיפתח shell.

רמז: כדי לקבל count = 0x1000 בלי בית אפס, אל תעשו mov edx, 0x1000. במקום זה טענו 1 והזיזו שמאלה: push 1; pop rdx; shl rdx, 12.

רמז: את המחסנית אפשר לקחת כיעד קריאה עם mov rsi, rsp, ואז לקפוץ עם jmp rsi אחרי ה-read.

רמז: ב-run_shellcode ה-tube מחובר ל-stdin של ה-stager, אז p.send(stage2) הוא בדיוק ה"שלב שני" שה-stager יקרא.

תרגיל 2 - egghunter

כתבו egghunter ל-x86-64 שסורק את הזיכרון, מוצא egg בן 8 בתים (שני עותקים של 0x90509050), וקופץ לpayload שמיד אחריו. הוכיחו שהוא מוצא payload שהסתרתם עמוק בזיכרון.

המטרה:

  1. כתבו את ה-egghunter עם הטריק של access (מספר 21) לבדיקת עמודים, ו-scasd להשוואת ה-egg.
  2. כתבו harness ב-C שמקצה בלוק זיכרון גדול, מניח בתוכו במקום אקראי את הpayload (egg כפול ואז execve shellcode), ומריץ את ה-egghunter מעמוד RWX נפרד.
  3. קמפלו והריצו. ה-egghunter צריך לסרוק, למצוא את ה-egg, ולפתוח shell.

רמז: ה-egg חייב להופיע פעמיים ברצף בpayload (8 בתים: \x50\x90\x50\x90\x50\x90\x50\x90) כדי שלא יהיו התאמות מקריות. ה-egghunter דורש שני scasd תואמים ברצף.

רמז: EFAULT הוא 14, וקריאת המערכת מחזירה -14. הבית התחתון של הערך הזה הוא 0xf2, אז הבדיקה היא cmp al, 0xf2.

רמז: הניחו את הpayload בתוך ההקצאה הגדולה, למשל memcpy(big + 0x54321, payload, sizeof(payload)), כדי לאלץ את ה-egghunter לסרוק ממש.

תרגיל 3 - reverse shell

כתבו shellcode של reverse shell ל-x86-64 שמתחבר ל-127.0.0.1:4444, מפנה אליו את stdin/stdout/stderr, ומריץ /bin/sh. הוכיחו שאתם מקבלים shell ב-nc.

המטרה:

  1. כתבו את הרצף: socket -> connect -> שלוש קריאות dup2 -> execve.
  2. בנו את sockaddr_in על המחסנית: family=AF_INET, port=4444 בסדר רשת, ip=127.0.0.1.
  3. בטרמינל אחד הריצו nc -lvnp 4444. בטרמינל שני הריצו את ה-shellcode (דרך הטוען או run_shellcode). ה-listener אמור לקבל shell - נסו id.

רמז: 4444 בסדר רשת הוא 0x115c, ובזיכרון 11 5c. 127.0.0.1 בסדר רשת הוא 7f 00 00 01. שמונת הבתים הראשונים של sockaddr הם 02 00 11 5c 7f 00 00 01, שזה qword 0x0100007f5c110002.

רמז: dup2 צריך לרוץ שלוש פעמים על אותו sockfd עם ה-newfd 2,1,0. לולאה עם dec rsi; jns נותנת בדיוק את הרצף הזה ועוצרת על -1.

רמז: אחרי socket, ה-fd חוזר ב-rax. שמרו אותו מיד ל-rdi (mov rdi, rax), כי גם connect וגם dup2 צריכים אותו ב-rdi.

רמז: אם אתם מתקשים לכתוב ידנית, אפשר לייצר לבדיקה עם shellcraft.amd64.linux.connect('127.0.0.1', 4444), אבל נסו קודם ביד.

תרגיל 4 (אתגר) - מפענח XOR שמשנה את עצמו

עכשיו קחו את ה-reverse shell מתרגיל 3, קודדו אותו כך שלא יכיל בית אפס, ועטפו אותו במפענח XOR שמשחזר אותו בזמן ריצה.

המטרה:

  1. קודדו את בתי ה-reverse shell עם XOR במפתח קבוע (למשל 0xAA). ודאו שהתוצאה המקודדת לא מכילה 0x00 (אם כן, בחרו מפתח אחר).
  2. כתבו מפענח jmp/call/pop שמפענח את הpayload במקום ואז נופל אליו (או קופץ אליו עם ret).
  3. הרכיבו את המפענח + הpayload המקודד ביחד, בדקו ש-assert b'\x00' not in blob עובר, והריצו בטוען. אמור להיפתח shell שמתחבר ל-nc.

רמז: המפתח 0xAA הוא בחירה טובה כי הוא הופך בתים "בעייתיים" רבים. אבל אם בית מסוים ב-XOR עם 0xAA נותן 0x00, המשמעות היא שהבית המקורי היה 0xAA - נסו מפתח אחר.

רמז: תבנית jmp/call/pop נותנת לכם את כתובת הpayload המקודד בלי כתובת מוחלטת. call דוחף את כתובת ההוראה הבאה, שהיא בדיוק תחילת הpayload.

רמז: זכרו שכתיבה על הקוד עצמה דורשת שהעמוד יהיה RWX. הטוען שלנו מקצה עמוד RWX עם mmap, אז זה בסדר. על יעד W^X הייתם צריכים לפענח לאזור אחר.

רמז: הדרך הכי קלה לחשב את הpayload המקודד בפייתון: encoded = bytes(b ^ 0xAA for b in stage2).